@@ -79,25 +79,10 @@ export default class WebAudio {
79
79
this . rules = this . sites [ filter . hostname ] ;
80
80
if ( this . rules ) {
81
81
if ( ! Array . isArray ( this . rules ) ) { this . rules = [ this . rules ] ; }
82
- this . initRules ( ) ;
82
+ this . rules . forEach ( ( rule ) => { this . initRule ( rule ) ; } ) ;
83
83
if ( this . enabledRuleIds . length > 0 ) {
84
84
this . supportedPage = true ;
85
- if ( [ 'm.youtube.com' , 'tv.youtube.com' , 'www.youtube.com' ] . includes ( filter . hostname ) ) {
86
- this . youTube = true ;
87
- // Issue 251: YouTube is now filtering words out of auto-generated captions/subtitles
88
- const youTubeAutoCensor = '[ __ ]' ;
89
- const lists = this . wordlistId == 0 ? [ ] : [ this . wordlistId ] ;
90
- const youTubeAutoCensorOptions : WordOptions = { lists : lists , matchMethod : Constants . MatchMethods . Partial , repeat : false , separators : false , sub : '' } ;
91
- this . filter . cfg . addWord ( youTubeAutoCensor , youTubeAutoCensorOptions ) ;
92
- }
93
-
94
- if ( this . watcherRuleIds . length > 0 ) {
95
- this . watcherRuleIds . forEach ( ( ruleId ) => {
96
- setInterval ( this . watcher , this . rules [ ruleId ] . checkInterval , this , ruleId ) ;
97
- } ) ;
98
- }
99
-
100
- if ( this . cueRuleIds . length > 0 ) { setInterval ( this . watchForVideo , 250 , this ) ; }
85
+ this . initYouTube ( ) ;
101
86
}
102
87
}
103
88
}
@@ -263,7 +248,10 @@ export default class WebAudio {
263
248
}
264
249
265
250
hideSubtitles ( rule : AudioRule , subtitles ?) {
266
- if ( rule . displaySelector ) {
251
+ if ( rule . displayVisibility && rule . _displayElement ) {
252
+ // TODO: Only tested with Watcher: HBO Max. This may be a much better solution
253
+ rule . _displayElement . style . visibility = 'hidden' ;
254
+ } else if ( rule . displaySelector ) {
267
255
const root = rule . rootNode && subtitles && subtitles [ 0 ] ? subtitles [ 0 ] . getRootNode ( ) : document ;
268
256
if ( root ) {
269
257
const container = root . querySelector ( rule . displaySelector ) as HTMLElement ;
@@ -299,7 +287,6 @@ export default class WebAudio {
299
287
if ( rule . externalSubFormatKey === undefined ) { rule . externalSubFormatKey = 'format' ; }
300
288
if ( rule . externalSubTrackLabel === undefined ) { rule . externalSubTrackLabel = 'APF' ; }
301
289
}
302
- this . initDisplaySelector ( rule ) ;
303
290
}
304
291
305
292
initDisplaySelector ( rule : AudioRule ) {
@@ -309,14 +296,16 @@ export default class WebAudio {
309
296
}
310
297
}
311
298
299
+ initDynamicRule ( rule : AudioRule ) {
300
+ rule . _dynamic = true ;
301
+ if ( rule . dynamicTargetMode == undefined ) { rule . disabled == true ; }
302
+ }
303
+
312
304
initElementChildRule ( rule : AudioRule ) {
313
305
if ( ! rule . parentSelector && ! rule . parentSelectorAll ) { rule . disabled = true ; }
314
- this . initDisplaySelector ( rule ) ;
315
306
}
316
307
317
- initElementRule ( rule : AudioRule ) {
318
- this . initDisplaySelector ( rule ) ;
319
- }
308
+ initElementRule ( rule : AudioRule ) { }
320
309
321
310
initFillerAudio ( name : string = '' ) : HTMLAudioElement {
322
311
const fillerConfig = WebAudio . FillerConfig [ name ] ;
@@ -337,51 +326,62 @@ export default class WebAudio {
337
326
}
338
327
}
339
328
340
- initRules ( ) {
341
- this . rules . forEach ( ( rule , index ) => {
342
- if (
343
- rule . mode === undefined
344
- || ( ( rule . mode == 'element' || rule . mode == 'elementChild' ) && ! rule . tagName )
345
- // Skip this rule if it doesn't apply to the current page
346
- || ( rule . iframe === true && this . filter . iframe == null )
347
- || ( rule . iframe === false && this . filter . iframe != null )
348
- ) {
349
- rule . disabled = true ;
350
- }
329
+ initRule ( rule : AudioRule ) {
330
+ const ruleId = this . rules . indexOf ( rule ) ;
331
+ if (
332
+ rule . mode === undefined
333
+ || ( ( rule . mode == 'element' || rule . mode == 'elementChild' ) && ! rule . tagName )
334
+ // Skip this rule if it doesn't apply to the current page
335
+ || ( rule . iframe === true && this . filter . iframe == null )
336
+ || ( rule . iframe === false && this . filter . iframe != null )
337
+ ) {
338
+ rule . disabled = true ;
339
+ }
340
+
341
+ if ( ! rule . disabled ) {
342
+ // Setup rule defaults
343
+ if ( rule . filterSubtitles == null ) { rule . filterSubtitles = true ; }
344
+ this . initDisplaySelector ( rule ) ;
345
+
346
+ // Allow rules to override global settings
347
+ if ( rule . muteMethod == null ) { rule . muteMethod = this . filter . cfg . muteMethod ; }
348
+ if ( rule . showSubtitles == null ) { rule . showSubtitles = this . filter . cfg . showSubtitles ; }
351
349
350
+ // Ensure proper rule values
351
+ if ( rule . tagName != null && rule . tagName != '#text' ) { rule . tagName = rule . tagName . toUpperCase ( ) ; }
352
+
353
+ switch ( rule . mode ) {
354
+ case 'cue' :
355
+ this . initCueRule ( rule ) ;
356
+ if ( ! rule . disabled ) { this . cueRuleIds . push ( ruleId ) ; }
357
+ break ;
358
+ case 'dynamic' :
359
+ this . initDynamicRule ( rule ) ;
360
+ break ;
361
+ case 'elementChild' :
362
+ this . initElementChildRule ( rule ) ;
363
+ break ;
364
+ case 'element' :
365
+ this . initElementRule ( rule ) ;
366
+ break ;
367
+ case 'text' :
368
+ this . initTextRule ( rule ) ;
369
+ break ;
370
+ case 'watcher' :
371
+ this . initWatcherRule ( rule ) ;
372
+ if ( ! rule . disabled ) { this . watcherRuleIds . push ( ruleId ) ; }
373
+ break ;
374
+ }
352
375
if ( ! rule . disabled ) {
353
- // Setup rule defaults
354
- if ( rule . filterSubtitles == null ) { rule . filterSubtitles = true ; }
355
-
356
- // Allow rules to override global settings
357
- if ( rule . muteMethod == null ) { rule . muteMethod = this . filter . cfg . muteMethod ; }
358
- if ( rule . showSubtitles == null ) { rule . showSubtitles = this . filter . cfg . showSubtitles ; }
359
-
360
- // Ensure proper rule values
361
- if ( rule . tagName != null && rule . tagName != '#text' ) { rule . tagName = rule . tagName . toUpperCase ( ) ; }
362
-
363
- switch ( rule . mode ) {
364
- case 'cue' :
365
- this . initCueRule ( rule ) ;
366
- if ( ! rule . disabled ) { this . cueRuleIds . push ( index ) ; }
367
- break ;
368
- case 'elementChild' :
369
- this . initElementChildRule ( rule ) ;
370
- break ;
371
- case 'element' :
372
- this . initElementRule ( rule ) ;
373
- break ;
374
- case 'text' :
375
- this . initTextRule ( rule ) ;
376
- break ;
377
- case 'watcher' :
378
- this . initWatcherRule ( rule ) ;
379
- if ( ! rule . disabled ) { this . watcherRuleIds . push ( index ) ; }
380
- break ;
376
+ this . enabledRuleIds . push ( ruleId ) ;
377
+
378
+ if ( rule . mode == 'cue' && this . cueRuleIds . length === 1 ) { // Only for first rule
379
+ setInterval ( this . watchForVideo , 250 , this ) ;
380
+ } else if ( rule . mode == 'watcher' ) {
381
+ setInterval ( this . watcher , rule . checkInterval , this , ruleId ) ;
381
382
}
382
- if ( ! rule . disabled ) { this . enabledRuleIds . push ( index ) ; }
383
383
}
384
- } ) ;
384
+ }
385
385
}
386
386
387
387
initTextRule ( rule : AudioRule ) {
@@ -394,7 +394,17 @@ export default class WebAudio {
394
394
if ( rule . ignoreMutations === undefined ) { rule . ignoreMutations = true ; }
395
395
if ( rule . simpleUnmute === undefined ) { rule . simpleUnmute = true ; }
396
396
if ( rule . videoSelector === undefined ) { rule . videoSelector = WebAudio . DefaultVideoSelector ; }
397
- this . initDisplaySelector ( rule ) ;
397
+ }
398
+
399
+ initYouTube ( ) {
400
+ if ( [ 'm.youtube.com' , 'tv.youtube.com' , 'www.youtube.com' ] . includes ( this . filter . hostname ) ) {
401
+ this . youTube = true ;
402
+ // Issue 251: YouTube is now filtering words out of auto-generated captions/subtitles
403
+ const youTubeAutoCensor = '[ __ ]' ;
404
+ const lists = this . wordlistId == 0 ? [ ] : [ this . wordlistId ] ;
405
+ const youTubeAutoCensorOptions : WordOptions = { lists : lists , matchMethod : Constants . MatchMethods . Partial , repeat : false , separators : false , sub : '' } ;
406
+ this . filter . cfg . addWord ( youTubeAutoCensor , youTubeAutoCensorOptions ) ;
407
+ }
398
408
}
399
409
400
410
mute ( rule ?: AudioRule , video ?: HTMLVideoElement ) : void {
@@ -686,12 +696,45 @@ export default class WebAudio {
686
696
if ( initialCall ) { this . lastProcessedText = captions . textContent ; }
687
697
}
688
698
699
+ // TODO: Only tested with HBO Max
700
+ processWatcherCaptionsArray ( rule : AudioRule , captions : HTMLElement [ ] , data : WatcherData ) {
701
+ const originalText = captions . map ( ( caption ) => caption . textContent ) . join ( ' ' ) ;
702
+
703
+ // Don't process the same filter again
704
+ if ( this . lastProcessedText && this . lastProcessedText === originalText ) {
705
+ data . skipped = true ;
706
+ return false ;
707
+ } else { // These are new captions, unmute if muted
708
+ this . unmute ( rule ) ;
709
+ this . lastProcessedText = '' ;
710
+ data . filtered = false ;
711
+ }
712
+
713
+ captions . forEach ( ( caption ) => {
714
+ rule . displayVisibility = true ; // Requires .textContent()
715
+ // Don't process empty/whitespace nodes
716
+ if ( caption . textContent && caption . textContent . trim ( ) ) {
717
+ const result = this . replaceTextResult ( caption . textContent ) ;
718
+ if ( result . modified ) {
719
+ this . mute ( rule ) ;
720
+ data . filtered = true ;
721
+ if ( rule . filterSubtitles ) { caption . textContent = result . filtered ; }
722
+ }
723
+ }
724
+ } ) ;
725
+
726
+ this . lastProcessedText = captions . map ( ( caption ) => caption . textContent ) . join ( ' ' ) ;
727
+ }
728
+
689
729
replaceTextResult ( string : string , stats : boolean = true ) {
690
730
return this . filter . replaceTextResult ( string , this . wordlistId , stats ) ;
691
731
}
692
732
693
733
showSubtitles ( rule , subtitles ?) {
694
- if ( rule . displaySelector ) {
734
+ if ( rule . displayVisibility && rule . _displayElement ) {
735
+ // TODO: Only tested with Watcher: HBO Max. This may be a much better solution
736
+ rule . _displayElement . style . visibility = 'visible' ;
737
+ } else if ( rule . displaySelector ) {
695
738
const root = rule . rootNode && subtitles && subtitles [ 0 ] ? subtitles [ 0 ] . getRootNode ( ) : document ;
696
739
if ( root ) {
697
740
const container = root . querySelector ( rule . displaySelector ) ;
@@ -753,6 +796,15 @@ export default class WebAudio {
753
796
if ( parent && parent . contains ( node ) ) { return ruleId ; }
754
797
}
755
798
break ;
799
+ case 'dynamic' :
800
+ // HBO Max: When playing a video, this node gets added, but doesn't include any context. Grabbing classList and then start watching.
801
+ if ( node . textContent === rule . dynamicTextKey ) {
802
+ rule . mode = rule . dynamicTargetMode ;
803
+ // TODO: Only working for HBO Max right now
804
+ rule . parentSelectorAll = `${ node . tagName . toLowerCase ( ) } .${ Array . from ( node . classList ) . join ( '.' ) } ${ rule . parentSelectorAll } ` ;
805
+ this . initRule ( rule ) ;
806
+ }
807
+ break ;
756
808
}
757
809
}
758
810
@@ -798,25 +850,46 @@ export default class WebAudio {
798
850
799
851
if ( video && instance . playing ( video ) ) {
800
852
if ( rule . ignoreMutations ) { instance . filter . stopObserving ( ) ; } // Stop observing when video is playing
853
+ const data : WatcherData = { initialCall : true } ;
854
+ let captions ;
801
855
802
- const captions = document . querySelector ( rule . subtitleSelector ) as HTMLElement ;
803
- if ( captions && captions . textContent && captions . textContent . trim ( ) ) {
804
- const data : WatcherData = { initialCall : true } ;
805
- instance . processWatcherCaptions ( rule , captions , data ) ;
806
- if ( data . skipped ) { return false ; }
807
-
808
- // Hide/show captions/subtitles
809
- switch ( rule . showSubtitles ) {
810
- case Constants . ShowSubtitles . Filtered : if ( data . filtered ) { instance . showSubtitles ( rule ) ; } else { instance . hideSubtitles ( rule ) ; } break ;
811
- case Constants . ShowSubtitles . Unfiltered : if ( data . filtered ) { instance . hideSubtitles ( rule ) ; } else { instance . showSubtitles ( rule ) ; } break ;
812
- case Constants . ShowSubtitles . None : instance . hideSubtitles ( rule ) ; break ;
856
+ if ( rule . parentSelectorAll ) { // TODO: Only tested with HBO Max
857
+ const parents = Array . from ( document . querySelectorAll ( rule . parentSelectorAll ) ) . filter ( ( result ) => {
858
+ return rule . _dynamic && result . textContent !== rule . dynamicTextKey ;
859
+ } ) as HTMLElement [ ] ;
860
+
861
+ if (
862
+ ! rule . _displayElement
863
+ && parents [ 0 ]
864
+ && parents [ 0 ] . parentElement
865
+ && parents [ 0 ] . parentElement . parentElement
866
+ && parents [ 0 ] . parentElement . parentElement . parentElement
867
+ ) {
868
+ rule . _displayElement = parents [ 0 ] . parentElement . parentElement . parentElement ;
869
+ }
870
+ captions = parents . map ( ( parent ) => parent . querySelector ( rule . subtitleSelector ) ) ;
871
+ if ( captions . length ) {
872
+ instance . processWatcherCaptionsArray ( rule , captions , data ) ;
873
+ } else { // If there are no captions/subtitles: unmute and hide
874
+ instance . watcherSimpleUnmute ( rule , video ) ;
875
+ }
876
+ } else if ( rule . subtitleSelector ) {
877
+ captions = document . querySelector ( rule . subtitleSelector ) as HTMLElement ;
878
+ if ( captions && captions . textContent && captions . textContent . trim ( ) ) {
879
+ instance . processWatcherCaptions ( rule , captions , data ) ;
880
+ } else { // If there are no captions/subtitles: unmute and hide
881
+ instance . watcherSimpleUnmute ( rule , video ) ;
813
882
}
883
+ }
814
884
815
- if ( data . filtered ) { instance . filter . updateCounterBadge ( ) ; }
816
- } else if ( rule . simpleUnmute ) { // If there are no captions/subtitles: unmute and hide
817
- instance . unmute ( rule , video ) ;
818
- if ( rule . showSubtitles > 0 ) { instance . hideSubtitles ( rule ) ; }
885
+ if ( data . skipped ) { return false ; }
886
+ // Hide/show captions/subtitles
887
+ switch ( rule . showSubtitles ) {
888
+ case Constants . ShowSubtitles . Filtered : if ( data . filtered ) { instance . showSubtitles ( rule ) ; } else { instance . hideSubtitles ( rule ) ; } break ;
889
+ case Constants . ShowSubtitles . Unfiltered : if ( data . filtered ) { instance . hideSubtitles ( rule ) ; } else { instance . showSubtitles ( rule ) ; } break ;
890
+ case Constants . ShowSubtitles . None : instance . hideSubtitles ( rule ) ; break ;
819
891
}
892
+ if ( data . filtered ) { instance . filter . updateCounterBadge ( ) ; }
820
893
} else {
821
894
if ( rule . ignoreMutations ) { instance . filter . startObserving ( ) ; } // Start observing when video is not playing
822
895
}
@@ -888,6 +961,11 @@ export default class WebAudio {
888
961
}
889
962
}
890
963
964
+ watcherSimpleUnmute ( rule : AudioRule , video : HTMLVideoElement ) {
965
+ this . unmute ( rule , video ) ;
966
+ if ( rule . showSubtitles > 0 ) { this . hideSubtitles ( rule , rule . _displayElement ) ; }
967
+ }
968
+
891
969
youTubeAutoSubsCurrentRow ( node ) : boolean {
892
970
return ! ! ( node . parentElement . parentElement == node . parentElement . parentElement . parentElement . lastChild ) ;
893
971
}
0 commit comments