Implement support for YouTube TV (tv.youtube.com)

This commit is contained in:
tech234a
2025-01-12 23:01:33 -05:00
parent 4a217300a2
commit de1d6bd76a
11 changed files with 160 additions and 26 deletions

View File

@@ -15,6 +15,7 @@ import { findValidElement } from "../../maze-utils/src/dom";
import { addCleanupListener } from "../../maze-utils/src/cleanup";
import { hasAutogeneratedChapters, isVisible } from "../utils/pageUtils";
import { isVorapisInstalled } from "../utils/compatibility";
import { isOnYTTV } from "../../maze-utils/src/video";
const TOOLTIP_VISIBLE_CLASS = 'sponsorCategoryTooltipVisible';
const MIN_CHAPTER_SIZE = 0.003;
@@ -41,6 +42,12 @@ class PreviewBar {
categoryTooltip?: HTMLDivElement;
categoryTooltipContainer?: HTMLElement;
chapterTooltip?: HTMLDivElement;
// ScrubTooltips for YTTV only
categoryScrubTooltip?: HTMLDivElement;
categoryScrubTooltipContainer?: HTMLElement;
chapterScrubTooltip?: HTMLDivElement;
lastSmallestSegment: Record<string, {
index: number;
segment: PreviewBarSegment;
@@ -49,6 +56,7 @@ class PreviewBar {
parent: HTMLElement;
onMobileYouTube: boolean;
onInvidious: boolean;
onYTTV: boolean;
progressBar: HTMLElement;
segments: PreviewBarSegment[] = [];
@@ -70,14 +78,19 @@ class PreviewBar {
unfilteredChapterGroups: ChapterGroup[];
chapterGroups: ChapterGroup[];
constructor(parent: HTMLElement, onMobileYouTube: boolean, onInvidious: boolean, chapterVote: ChapterVote, updateExistingChapters: () => void, test=false) {
constructor(parent: HTMLElement, onMobileYouTube: boolean, onInvidious: boolean, onYTTV: boolean, chapterVote: ChapterVote, updateExistingChapters: () => void, test=false) {
if (test) return;
this.container = document.createElement('ul');
this.container.id = 'previewbar';
if (onYTTV) {
this.container.classList.add("sponsorblock-yttv-container");
}
this.parent = parent;
this.onMobileYouTube = onMobileYouTube;
this.onInvidious = onInvidious;
this.onYTTV = onYTTV;
this.chapterVote = chapterVote;
this.updateExistingChapters = updateExistingChapters;
@@ -97,26 +110,49 @@ class PreviewBar {
// Create label placeholder
this.categoryTooltip = document.createElement("div");
this.categoryTooltip.className = "ytp-tooltip-title sponsorCategoryTooltip";
if (isOnYTTV()) {
this.categoryTooltip.className = "sponsorCategoryTooltip";
} else {
this.categoryTooltip.className = "ytp-tooltip-title sponsorCategoryTooltip";
}
this.chapterTooltip = document.createElement("div");
this.chapterTooltip.className = "ytp-tooltip-title sponsorCategoryTooltip";
if (isOnYTTV()) {
this.chapterTooltip.className = "sponsorCategoryTooltip";
} else {
this.chapterTooltip.className = "ytp-tooltip-title sponsorCategoryTooltip";
}
// global chaper tooltip or duration tooltip
// YT, Vorapis, unknown
const tooltipTextWrapper = document.querySelector(".ytp-tooltip-text-wrapper, .ytp-progress-tooltip-text-container") ?? document.querySelector("#progress-bar-container.ytk-player > #hover-time-info");
const originalTooltip = tooltipTextWrapper.querySelector(".ytp-tooltip-title:not(.sponsorCategoryTooltip), .ytp-progress-tooltip-text:not(.sponsorCategoryTooltip)") as HTMLElement;
if (isOnYTTV()) {
this.categoryScrubTooltip = document.createElement("div");
this.categoryScrubTooltip.className = "sponsorCategoryTooltip";
this.chapterScrubTooltip = document.createElement("div");
this.chapterScrubTooltip.className = "sponsorCategoryTooltip";
}
// global chapter tooltip or duration tooltip
// YT, Vorapis, unknown, YTTV
const tooltipTextWrapper = document.querySelector(".ytp-tooltip-text-wrapper, .ytp-progress-tooltip-text-container, .yssi-slider .ys-seek-details .time-info-bar") ?? document.querySelector("#progress-bar-container.ytk-player > #hover-time-info");
const originalTooltip = tooltipTextWrapper.querySelector(".ytp-tooltip-title:not(.sponsorCategoryTooltip), .ytp-progress-tooltip-text:not(.sponsorCategoryTooltip), .current-time:not(.sponsorCategoryTooltip)") as HTMLElement;
if (!tooltipTextWrapper || !tooltipTextWrapper.parentElement) return;
// Grab the tooltip from the text wrapper as the tooltip doesn't have its classes on init
this.categoryTooltipContainer = tooltipTextWrapper.parentElement;
// YT, Vorapis
const titleTooltip = tooltipTextWrapper.querySelector(".ytp-tooltip-title, .ytp-progress-tooltip-text") as HTMLElement;
// YT, Vorapis, YTTV
const titleTooltip = tooltipTextWrapper.querySelector(".ytp-tooltip-title, .ytp-progress-tooltip-text, .current-time") as HTMLElement;
if (!this.categoryTooltipContainer || !titleTooltip) return;
tooltipTextWrapper.insertBefore(this.categoryTooltip, titleTooltip.nextSibling);
tooltipTextWrapper.insertBefore(this.chapterTooltip, titleTooltip.nextSibling);
const seekBar = document.querySelector(".ytp-progress-bar-container");
if (isOnYTTV()) {
const scrubTooltipTextWrapper = document.querySelector(".yssi-slider .ysl-filmstrip-lens .time-info-bar")
if (!this.categoryTooltipContainer) return;
scrubTooltipTextWrapper.appendChild(this.categoryScrubTooltip);
scrubTooltipTextWrapper.appendChild(this.chapterScrubTooltip);
}
const seekBar = (document.querySelector(".ytp-progress-bar-container, .ypcs-scrub-slider-slot.ytu-player-controls"));
if (!seekBar) return;
let mouseOnSeekBar = false;
@@ -163,6 +199,12 @@ class PreviewBar {
this.categoryTooltipContainer.classList.remove(TOOLTIP_VISIBLE_CLASS);
originalTooltip.style.removeProperty("display");
}
if (this.onYTTV) {
this.setTooltipTitle(mainSegment, this.categoryTooltip);
this.setTooltipTitle(secondarySegment, this.chapterTooltip);
this.setTooltipTitle(mainSegment, this.categoryScrubTooltip);
this.setTooltipTitle(secondarySegment, this.chapterScrubTooltip);
}
} else {
this.categoryTooltipContainer.classList.add(TOOLTIP_VISIBLE_CLASS);
if (mainSegment !== null && secondarySegment !== null) {
@@ -174,6 +216,10 @@ class PreviewBar {
this.setTooltipTitle(mainSegment, this.categoryTooltip);
this.setTooltipTitle(secondarySegment, this.chapterTooltip);
if (this.onYTTV) {
this.setTooltipTitle(mainSegment, this.categoryScrubTooltip);
this.setTooltipTitle(secondarySegment, this.chapterScrubTooltip);
}
if (isVorapisInstalled()) {
const tooltipParent = tooltipTextWrapper.parentElement!;
@@ -226,7 +272,12 @@ class PreviewBar {
}
// On the seek bar
this.parent.prepend(this.container);
if (this.onYTTV) {
// order of sibling elements matters on YTTV
this.parent.insertBefore(this.container, this.parent.firstChild.nextSibling.nextSibling);
} else {
this.parent.prepend(this.container);
}
}
clear(): void {
@@ -362,6 +413,10 @@ class PreviewBar {
bar.style.marginRight = `${this.chapterMargin}px`;
}
if (this.onYTTV) {
bar.classList.add("previewbar-yttv");
}
return bar;
}
@@ -868,8 +923,10 @@ class PreviewBar {
})[0];
const chapterButton = this.getChapterButton(chaptersContainer);
chapterButton.classList.remove("ytp-chapter-container-disabled");
chapterButton.disabled = false;
if (chapterButton) {
chapterButton.classList.remove("ytp-chapter-container-disabled");
chapterButton.disabled = false;
}
const chapterTitle = chaptersContainer.querySelector(".ytp-chapter-title-content") as HTMLDivElement;
chapterTitle.style.display = "none";
@@ -878,6 +935,9 @@ class PreviewBar {
const elem = document.createElement("div");
chapterTitle.parentElement.insertBefore(elem, chapterTitle);
elem.classList.add("sponsorChapterText");
if (document.location.host === "tv.youtube.com") {
elem.style.lineHeight = "initial";
}
return elem;
})()) as HTMLDivElement;
chapterCustomText.innerText = chosenSegment.description || shortCategoryName(chosenSegment.category);
@@ -890,7 +950,15 @@ class PreviewBar {
if (chosenSegment.source === SponsorSourceType.Server) {
const chapterVoteContainer = this.chapterVote.getContainer();
if (!chapterButton.contains(chapterVoteContainer)) {
if (document.location.host === "tv.youtube.com") {
if (!chaptersContainer.contains(chapterVoteContainer)) {
const oldVoteContainers = document.querySelectorAll("#chapterVote");
if (oldVoteContainers.length > 0) {
oldVoteContainers.forEach((oldVoteContainer) => oldVoteContainer.remove());
}
chaptersContainer.appendChild(chapterVoteContainer);
}
} else if (!chapterButton.contains(chapterVoteContainer)) {
const oldVoteContainers = document.querySelectorAll("#chapterVote");
if (oldVoteContainers.length > 0) {
oldVoteContainers.forEach((oldVoteContainer) => oldVoteContainer.remove());
@@ -929,6 +997,18 @@ class PreviewBar {
}
private getChaptersContainer(): HTMLElement {
if (document.location.host === "tv.youtube.com") {
if (!document.querySelector(".ytp-chapter-container")) {
const dest = document.querySelector(".ypcs-control-buttons-left");
if (!dest) return null;
const sbChapterContainer = document.createElement("div");
sbChapterContainer.className = "ytp-chapter-container";
const sbChapterTitleContent = document.createElement("div");
sbChapterTitleContent.className = "ytp-chapter-title-content";
sbChapterContainer.appendChild(sbChapterTitleContent);
dest.appendChild(sbChapterContainer);
}
}
return document.querySelector(".ytp-chapter-container") as HTMLElement;
}

View File

@@ -9,6 +9,7 @@ export interface SkipButtonControlBarProps {
skip: (segment: SponsorTime) => void;
selectSegment: (UUID: SegmentUUID) => void;
onMobileYouTube: boolean;
onYTTV: boolean;
}
export class SkipButtonControlBar {
@@ -21,6 +22,7 @@ export class SkipButtonControlBar {
showKeybindHint = true;
onMobileYouTube: boolean;
onYTTV: boolean;
enabled = false;
@@ -40,6 +42,7 @@ export class SkipButtonControlBar {
constructor(props: SkipButtonControlBarProps) {
this.skip = props.skip;
this.onMobileYouTube = props.onMobileYouTube;
this.onYTTV = props.onYTTV;
this.container = document.createElement("div");
this.container.classList.add("skipButtonControlBarContainer");
@@ -50,6 +53,10 @@ export class SkipButtonControlBar {
this.skipIcon.src = chrome.runtime.getURL("icons/skipIcon.svg");
this.skipIcon.classList.add("ytp-button");
this.skipIcon.id = "sbSkipIconControlBarImage";
if (this.onYTTV) {
this.skipIcon.style.width = "24px";
this.skipIcon.style.height = "24px";
}
this.textContainer = document.createElement("div");
@@ -84,7 +91,7 @@ export class SkipButtonControlBar {
this.chapterText = document.querySelector(".ytp-chapter-container");
if (mountingContainer && !mountingContainer.contains(this.container)) {
if (this.onMobileYouTube) {
if (this.onMobileYouTube || this.onYTTV) {
mountingContainer.appendChild(this.container);
} else {
mountingContainer.insertBefore(this.container, this.chapterText);
@@ -101,8 +108,10 @@ export class SkipButtonControlBar {
}
private getMountingContainer(): HTMLElement {
if (!this.onMobileYouTube) {
if (!this.onMobileYouTube && !this.onYTTV) {
return document.querySelector(".ytp-left-controls");
} else if (this.onYTTV) {
return document.querySelector(".ypcs-control-buttons-left");
} else {
return document.getElementById("player-container-id");
}