diff options
author | Ajay Ramachandran <[email protected]> | 2021-06-23 21:09:59 -0400 |
---|---|---|
committer | Ajay Ramachandran <[email protected]> | 2021-06-23 21:09:59 -0400 |
commit | e80b7afe809fef5d11d7621aa2299ae27d5a7957 (patch) | |
tree | b358c8ce6a7691e0fda19eb2129906a9c45e7512 | |
parent | a6728d34a03f93176741e32b39fe683e0625ec90 (diff) | |
download | SponsorBlock-e80b7afe809fef5d11d7621aa2299ae27d5a7957.tar.gz SponsorBlock-e80b7afe809fef5d11d7621aa2299ae27d5a7957.zip |
Add initial unlisted detection for getting a list of unlisted videos
A temporary measure which will be removed next month
https://support.google.com/youtube/answer/9230970
-rw-r--r-- | src/config.ts | 4 | ||||
-rw-r--r-- | src/content.ts | 72 | ||||
-rw-r--r-- | src/render/GenericNotice.tsx | 111 | ||||
-rw-r--r-- | src/render/SkipNotice.tsx | 24 | ||||
-rw-r--r-- | src/render/SubmissionNotice.tsx | 20 | ||||
-rw-r--r-- | src/utils.ts | 23 |
6 files changed, 217 insertions, 37 deletions
diff --git a/src/config.ts b/src/config.ts index 6877400a..75392f98 100644 --- a/src/config.ts +++ b/src/config.ts @@ -38,6 +38,8 @@ interface SBConfig { testingServer: boolean, refetchWhenNotFound: boolean, ytInfoPermissionGranted: boolean, + askAboutUnlistedVideos: boolean, + allowExpirements: boolean, // What categories should be skipped categorySelections: CategorySelection[], @@ -174,6 +176,8 @@ const Config: SBObject = { testingServer: false, refetchWhenNotFound: true, ytInfoPermissionGranted: false, + askAboutUnlistedVideos: true, + allowExpirements: true, categorySelections: [{ name: "sponsor", diff --git a/src/content.ts b/src/content.ts index 85bae210..5a7286d7 100644 --- a/src/content.ts +++ b/src/content.ts @@ -13,6 +13,7 @@ import SkipNotice from "./render/SkipNotice"; import SkipNoticeComponent from "./components/SkipNoticeComponent"; import SubmissionNotice from "./render/SubmissionNotice"; import { Message, MessageResponse } from "./messageTypes"; +import GenericNotice from "./render/GenericNotice"; // Hack to get the CSS loaded on permission-based sites (Invidious) utils.wait(() => Config.config !== null, 5000, 10).then(addCSS); @@ -271,6 +272,9 @@ async function videoIDChange(id) { // Update whitelist data when the video data is loaded whitelistCheck(); + // Temporary expirement + unlistedCheck(); + //setup the preview bar if (previewBar === null) { if (onMobileYouTube) { @@ -391,7 +395,7 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?: return; } - if (video.paused) return; + if (!video || video.paused) return; if (Config.config.disableSkipping || channelWhitelisted || (channelIDInfo.status === ChannelIDStatus.Fetching && Config.config.forceChannelCheck)){ return; @@ -864,6 +868,72 @@ async function whitelistCheck() { if (Config.config.forceChannelCheck && sponsorTimes?.length > 0) startSkipScheduleCheckingForStartSponsors(); } +async function unlistedCheck() { + if (!Config.config.allowExpirements || !Config.config.askAboutUnlistedVideos) return; + + try { + await utils.wait(() => !!videoInfo && !!document.getElementById("info-text") + && !!document.querySelector(".ytd-video-primary-info-renderer > .badge > yt-icon > svg"), 6000, 1000); + + const isUnlisted = document.querySelector(".ytd-video-primary-info-renderer > .badge > yt-icon > svg > g > path") + ?.getAttribute("d")?.includes("M3.9 12c0-1.71 1.39-3.1 3.1-3.1h"); // Icon of unlisted badge + const yearMatches = document.querySelector("#info-text > #info-strings > yt-formatted-string") + ?.innerHTML?.match(/20[0-9]{2}/); + const year = yearMatches ? parseInt(yearMatches[0]) : -1; + const isOld = !isNaN(year) && year < 2017 && year > 2004; + const isHighViews = parseInt(videoInfo?.videoDetails?.viewCount) > 20000; + + console.log({ + isUnlisted, + year, + isOld, + isHighViews + }) + + if (isUnlisted && isOld && isHighViews) { + // Ask if they want to submit this videoID + const notice = new GenericNotice(skipNoticeContentContainer, "unlistedWarning", { + title: "Help prevent this from disappearing", + textBoxes: ("This video is detected as unlisted and uploaded before 2017\n" + + "Old unlisted videos are being set to private soon\n" + + "We are collecting *public* videos to back up\n" + + "Would you like anonymously to submit this video?").split("\n"), + buttons: [ + { + name: "Opt-out of all future experiments", + listener: () => { + Config.config.allowExpirements = false; + + notice.close(); + } + }, + { + name: "Never show this", + listener: () => { + Config.config.askAboutUnlistedVideos = false; + + notice.close(); + } + }, + { + name: "Submit", + listener: () => { + utils.asyncRequestToServer("POST", "/api/unlistedVideo", { + videoID: sponsorVideoID + }); + + notice.close(); + } + } + ] + }); + } + + } catch (e) { + return; + } +} + /** * Returns info about the next upcoming sponsor skip */ diff --git a/src/render/GenericNotice.tsx b/src/render/GenericNotice.tsx new file mode 100644 index 00000000..08570eab --- /dev/null +++ b/src/render/GenericNotice.tsx @@ -0,0 +1,111 @@ +import * as React from "react"; +import * as ReactDOM from "react-dom"; +import NoticeComponent from "../components/NoticeComponent"; + +import Utils from "../utils"; +const utils = new Utils(); + +import { ContentContainer } from "../types"; +import NoticeTextSelectionComponent from "../components/NoticeTextSectionComponent"; + +export interface ButtonListener { + name: string, + listener: (e?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void +} + +export interface NoticeOptions { + title: string, + textBoxes?: string[], + buttons?: ButtonListener[], + fadeIn?: boolean, + timed?: boolean +} + +export default class GenericNotice { + // Contains functions and variables from the content script needed by the skip notice + contentContainer: ContentContainer; + + noticeElement: HTMLDivElement; + noticeRef: React.MutableRefObject<NoticeComponent>; + + constructor(contentContainer: ContentContainer, idSuffix: string, options: NoticeOptions) { + this.noticeRef = React.createRef(); + + this.contentContainer = contentContainer; + + const referenceNode = utils.findReferenceNode(); + + this.noticeElement = document.createElement("div"); + this.noticeElement.id = "sponsorSkipNoticeContainer" + idSuffix; + + referenceNode.prepend(this.noticeElement); + + ReactDOM.render( + <NoticeComponent + noticeTitle={options.title} + idSuffix={idSuffix} + fadeIn={options.fadeIn ?? true} + timed={options.timed ?? true} + ref={this.noticeRef} + closeListener={() => this.close()} > + + {this.getMessageBox(idSuffix, options.textBoxes)} + + <tr id={"sponsorSkipNoticeSpacer" + idSuffix} + className="sponsorBlockSpacer"> + </tr> + + <div className="sponsorSkipNoticeRightSection" + style={{position: "relative"}}> + + {this.getButtons(options.buttons)} + </div> + </NoticeComponent>, + this.noticeElement + ); + } + + getMessageBox(idSuffix: string, textBoxes: string[]): JSX.Element[] { + if (textBoxes) { + const result = []; + for (let i = 0; i < textBoxes.length; i++) { + result.push( + <NoticeTextSelectionComponent idSuffix={idSuffix} + key={i} + text={textBoxes[i]} /> + ) + } + + return result; + } else { + return null; + } + } + + getButtons(buttons?: ButtonListener[]): JSX.Element[] { + if (buttons) { + const result: JSX.Element[] = []; + + for (const button of buttons) { + result.push( + <button className="sponsorSkipObject sponsorSkipNoticeButton sponsorSkipNoticeRightButton" + key={button.name} + onClick={(e) => button.listener(e)}> + + {button.name} + </button> + ) + } + + return result; + } else { + return null; + } + } + + close(): void { + ReactDOM.unmountComponentAtNode(this.noticeElement); + + this.noticeElement.remove(); + } +}
\ No newline at end of file diff --git a/src/render/SkipNotice.tsx b/src/render/SkipNotice.tsx index 426b1460..58600034 100644 --- a/src/render/SkipNotice.tsx +++ b/src/render/SkipNotice.tsx @@ -1,6 +1,9 @@ import * as React from "react"; import * as ReactDOM from "react-dom"; +import Utils from "../utils"; +const utils = new Utils(); + import SkipNoticeComponent, { SkipNoticeAction } from "../components/SkipNoticeComponent"; import { SponsorTime, ContentContainer } from "../types"; @@ -21,26 +24,7 @@ class SkipNotice { this.autoSkip = autoSkip; this.contentContainer = contentContainer; - //get reference node - let referenceNode = document.getElementById("player-container-id") - ?? document.getElementById("movie_player") - ?? document.querySelector("#main-panel.ytmusic-player-page") // YouTube music - ?? document.querySelector("#player-container .video-js") // Invidious - ?? document.querySelector(".main-video-section > .video-container"); // Cloudtube - if (referenceNode == null) { - //for embeds - const player = document.getElementById("player"); - referenceNode = player.firstChild as HTMLElement; - let index = 1; - - //find the child that is the video player (sometimes it is not the first) - while (index < player.children.length && (!referenceNode.classList.contains("html5-video-player") || !referenceNode.classList.contains("ytp-embed"))) { - referenceNode = player.children[index] as HTMLElement; - - index++; - } - } - + const referenceNode = utils.findReferenceNode(); const amountOfPreviousNotices = document.getElementsByClassName("sponsorSkipNotice").length; //this is the suffix added at the end of every id diff --git a/src/render/SubmissionNotice.tsx b/src/render/SubmissionNotice.tsx index 8c267854..5f646063 100644 --- a/src/render/SubmissionNotice.tsx +++ b/src/render/SubmissionNotice.tsx @@ -1,6 +1,9 @@ import * as React from "react"; import * as ReactDOM from "react-dom"; +import Utils from "../utils"; +const utils = new Utils(); + import SubmissionNoticeComponent from "../components/SubmissionNoticeComponent"; import { ContentContainer } from "../types"; @@ -20,22 +23,7 @@ class SubmissionNotice { this.contentContainer = contentContainer; this.callback = callback; - //get reference node - let referenceNode = document.getElementById("player-container-id") - || document.getElementById("movie_player") || document.querySelector("#player-container .video-js"); - if (referenceNode == null) { - //for embeds - const player = document.getElementById("player"); - referenceNode = player.firstChild as HTMLElement; - let index = 1; - - //find the child that is the video player (sometimes it is not the first) - while (!referenceNode.classList.contains("html5-video-player") || !referenceNode.classList.contains("ytp-embed")) { - referenceNode = player.children[index] as HTMLElement; - - index++; - } - } + const referenceNode = utils.findReferenceNode(); this.noticeElement = document.createElement("div"); this.noticeElement.id = "submissionNoticeContainer"; diff --git a/src/utils.ts b/src/utils.ts index fd5684f4..4aa21dbf 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -366,6 +366,29 @@ export default class Utils { }); } + findReferenceNode(): HTMLElement { + let referenceNode = document.getElementById("player-container-id") + ?? document.getElementById("movie_player") + ?? document.querySelector("#main-panel.ytmusic-player-page") // YouTube music + ?? document.querySelector("#player-container .video-js") // Invidious + ?? document.querySelector(".main-video-section > .video-container"); // Cloudtube + if (referenceNode == null) { + //for embeds + const player = document.getElementById("player"); + referenceNode = player.firstChild as HTMLElement; + let index = 1; + + //find the child that is the video player (sometimes it is not the first) + while (index < player.children.length && (!referenceNode.classList.contains("html5-video-player") || !referenceNode.classList.contains("ytp-embed"))) { + referenceNode = player.children[index] as HTMLElement; + + index++; + } + } + + return referenceNode; + } + getFormattedTime(seconds: number, precise?: boolean): string { const hours = Math.floor(seconds / 60 / 60); const minutes = Math.floor(seconds / 60) % 60; |