Skip to content

Commit 2f5ca23

Browse files
authored
Merge pull request #303 from richardfrost/hbomax
Hbomax Update
2 parents 30f6d86 + 25abe75 commit 2f5ca23

File tree

3 files changed

+167
-86
lines changed

3 files changed

+167
-86
lines changed

src/script/lib/globals.d.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
interface AudioRule {
2+
_displayElement?: HTMLElement; // Element to display/hide
3+
_dynamic?: boolean; // [Dynamic] Set to true on a dynamic rule
24
checkInterval?: number; // [Watcher] Set a custom watch interval (in ms, Default: 20)
35
className?: string; // [Element] node.className.includes()
46
containsSelector?: string; // [Element] node.querySelector() [Not commonly used]
@@ -8,6 +10,10 @@ interface AudioRule {
810
displayHide?: string; // [Element,ElementChild,Watcher] Display style for hiding captions (Default: 'none')
911
displaySelector?: string; // [Element,ElementChild,Watcher] Alternate selector to hide/show captions
1012
displayShow?: string; // [Element,ElementChild,Watcher] Display style for showing captions (Default: '')
13+
displayVisibility?: boolean; // [Watcher*] Use visibility to show/hide caption container
14+
dynamicClasslist?: string; // [Dynamic] Set when a dynamicTextKey is found
15+
dynamicTargetMode?: string; // [Dynamic] Target mode for dynamic rule
16+
dynamicTextKey?: string; // [Dynamic] Key used to identify a dynamic caption node
1117
externalSub?: boolean; // [Cue] [BETA]: Convert external captions/subtitles obtained from remote source to VTTCues
1218
externalSubFormatKey?: string; // [Cue] [BETA]: Key name for caption/subtitle format (Default: 'format')
1319
externalSubTrackLabel?: string; // [Cue] [BETA]: Label used for processed TextTrack
@@ -17,7 +23,7 @@ interface AudioRule {
1723
hasChildrenElements?: boolean; // [Element] node.childElementCount > 0 [Not commonly used]
1824
iframe?: boolean | undefined; // [All] Pages to run on (true: only iframes, false: no iframes, undefined: all)
1925
ignoreMutations?: boolean; // [Element,ElementChild,Text,Watcher] Ignore mutations when filtering captions/subtitles
20-
mode: string; // [All*] 'cue', 'element', 'elementChild', 'text', 'watcher'
26+
mode: string; // [All*] 'cue', 'dynamic', 'element', 'elementChild', 'text', 'watcher'
2127
muteMethod?: number; // [All] Override global muteMthod (0: tab, 1: video)
2228
note?: string; // [All] Note about the rule
2329
parentSelector?: string; // [ElementChild?,Text,Watcher] parent.contains(node)

src/script/webAudio.ts

Lines changed: 159 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -79,25 +79,10 @@ export default class WebAudio {
7979
this.rules = this.sites[filter.hostname];
8080
if (this.rules) {
8181
if (!Array.isArray(this.rules)) { this.rules = [this.rules]; }
82-
this.initRules();
82+
this.rules.forEach((rule) => { this.initRule(rule); });
8383
if (this.enabledRuleIds.length > 0) {
8484
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();
10186
}
10287
}
10388
}
@@ -263,7 +248,10 @@ export default class WebAudio {
263248
}
264249

265250
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) {
267255
const root = rule.rootNode && subtitles && subtitles[0] ? subtitles[0].getRootNode() : document;
268256
if (root) {
269257
const container = root.querySelector(rule.displaySelector) as HTMLElement;
@@ -299,7 +287,6 @@ export default class WebAudio {
299287
if (rule.externalSubFormatKey === undefined) { rule.externalSubFormatKey = 'format'; }
300288
if (rule.externalSubTrackLabel === undefined) { rule.externalSubTrackLabel = 'APF'; }
301289
}
302-
this.initDisplaySelector(rule);
303290
}
304291

305292
initDisplaySelector(rule: AudioRule) {
@@ -309,14 +296,16 @@ export default class WebAudio {
309296
}
310297
}
311298

299+
initDynamicRule(rule: AudioRule) {
300+
rule._dynamic = true;
301+
if (rule.dynamicTargetMode == undefined) { rule.disabled == true; }
302+
}
303+
312304
initElementChildRule(rule: AudioRule) {
313305
if (!rule.parentSelector && !rule.parentSelectorAll) { rule.disabled = true; }
314-
this.initDisplaySelector(rule);
315306
}
316307

317-
initElementRule(rule: AudioRule) {
318-
this.initDisplaySelector(rule);
319-
}
308+
initElementRule(rule: AudioRule) { }
320309

321310
initFillerAudio(name: string = ''): HTMLAudioElement {
322311
const fillerConfig = WebAudio.FillerConfig[name];
@@ -337,51 +326,62 @@ export default class WebAudio {
337326
}
338327
}
339328

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; }
351349

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+
}
352375
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);
381382
}
382-
if (!rule.disabled) { this.enabledRuleIds.push(index); }
383383
}
384-
});
384+
}
385385
}
386386

387387
initTextRule(rule: AudioRule) {
@@ -394,7 +394,17 @@ export default class WebAudio {
394394
if (rule.ignoreMutations === undefined) { rule.ignoreMutations = true; }
395395
if (rule.simpleUnmute === undefined) { rule.simpleUnmute = true; }
396396
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+
}
398408
}
399409

400410
mute(rule?: AudioRule, video?: HTMLVideoElement): void {
@@ -686,12 +696,45 @@ export default class WebAudio {
686696
if (initialCall) { this.lastProcessedText = captions.textContent; }
687697
}
688698

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+
689729
replaceTextResult(string: string, stats: boolean = true) {
690730
return this.filter.replaceTextResult(string, this.wordlistId, stats);
691731
}
692732

693733
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) {
695738
const root = rule.rootNode && subtitles && subtitles[0] ? subtitles[0].getRootNode() : document;
696739
if (root) {
697740
const container = root.querySelector(rule.displaySelector);
@@ -753,6 +796,15 @@ export default class WebAudio {
753796
if (parent && parent.contains(node)) { return ruleId; }
754797
}
755798
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;
756808
}
757809
}
758810

@@ -798,25 +850,46 @@ export default class WebAudio {
798850

799851
if (video && instance.playing(video)) {
800852
if (rule.ignoreMutations) { instance.filter.stopObserving(); } // Stop observing when video is playing
853+
const data: WatcherData = { initialCall: true };
854+
let captions;
801855

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);
813882
}
883+
}
814884

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;
819891
}
892+
if (data.filtered) { instance.filter.updateCounterBadge(); }
820893
} else {
821894
if (rule.ignoreMutations) { instance.filter.startObserving(); } // Start observing when video is not playing
822895
}
@@ -888,6 +961,11 @@ export default class WebAudio {
888961
}
889962
}
890963

964+
watcherSimpleUnmute(rule: AudioRule, video: HTMLVideoElement) {
965+
this.unmute(rule, video);
966+
if (rule.showSubtitles > 0) { this.hideSubtitles(rule, rule._displayElement); }
967+
}
968+
891969
youTubeAutoSubsCurrentRow(node): boolean {
892970
return !!(node.parentElement.parentElement == node.parentElement.parentElement.parentElement.lastChild);
893971
}

src/script/webAudioSites.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,7 @@ export default class WebAudioSites {
8787
],
8888
'www.paramountplus.com': [{ mode: 'cue', videoCueLanguage: 'en', videoCueRequireShowing: false }],
8989
'play.google.com': [{ className: 'lava-timed-text-window', mode: 'element', subtitleSelector: 'span.lava-timed-text-caption', tagName: 'DIV' }],
90-
'play.hbomax.com': [
91-
{ mode: 'elementChild', parentSelectorAll: 'div.class3 > span, div.class28 > span', showSubtitles: 0, tagName: 'SPAN' },
92-
{ mode: 'elementChild', parentSelectorAll: 'div.class2.class1 > span > span', showSubtitles: 0, tagName: 'SPAN' },
93-
],
90+
'play.hbomax.com': [{ displayVisibility: true, dynamicTargetMode: 'watcher', dynamicTextKey: 'Example Text', mode: 'dynamic', parentSelectorAll: '> span', subtitleSelector: 'span' }],
9491
'www.hulu.com': [
9592
{ className: 'caption-text-box', displaySelector: 'div.caption-text-box', mode: 'element', subtitleSelector: 'p', tagName: 'DIV' },
9693
{ displaySelector: 'div.CaptionBox', mode: 'elementChild', parentSelector: 'div.CaptionBox', tagName: 'P' }

0 commit comments

Comments
 (0)