mirror of
https://github.com/ajayyy/SponsorBlock.git
synced 2025-12-12 06:27:14 +03:00
Merge branch 'master' into EthanBnntt-patch-1
This commit is contained in:
15
src/utils/compatibility.ts
Normal file
15
src/utils/compatibility.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import Config from "../config";
|
||||
|
||||
export function runCompatibilityChecks() {
|
||||
if (Config.config.showZoomToFillError2 && document.URL.includes("watch?v=")) {
|
||||
setTimeout(() => {
|
||||
const zoomToFill = document.querySelector(".zoomtofillBtn");
|
||||
|
||||
if (zoomToFill) {
|
||||
alert(chrome.i18n.getMessage("zoomToFillUnsupported"));
|
||||
}
|
||||
|
||||
Config.config.showZoomToFillError2 = false;
|
||||
}, 10000);
|
||||
}
|
||||
}
|
||||
@@ -1,48 +1,5 @@
|
||||
import Config from "../config";
|
||||
import { Keybind } from "../types";
|
||||
|
||||
export function showDonationLink(): boolean {
|
||||
return navigator.vendor !== "Apple Computer, Inc." && Config.config.showDonationLink;
|
||||
}
|
||||
|
||||
export function isSafari(): boolean {
|
||||
return navigator.vendor === "Apple Computer, Inc.";
|
||||
}
|
||||
|
||||
export function keybindEquals(first: Keybind, second: Keybind): boolean {
|
||||
if (first == null || second == null ||
|
||||
Boolean(first.alt) != Boolean(second.alt) || Boolean(first.ctrl) != Boolean(second.ctrl) || Boolean(first.shift) != Boolean(second.shift) ||
|
||||
first.key == null && first.code == null || second.key == null && second.code == null)
|
||||
return false;
|
||||
if (first.code != null && second.code != null)
|
||||
return first.code === second.code;
|
||||
if (first.key != null && second.key != null)
|
||||
return first.key.toUpperCase() === second.key.toUpperCase();
|
||||
return false;
|
||||
}
|
||||
|
||||
export function formatKey(key: string): string {
|
||||
if (key == null)
|
||||
return "";
|
||||
else if (key == " ")
|
||||
return "Space";
|
||||
else if (key.length == 1)
|
||||
return key.toUpperCase();
|
||||
else
|
||||
return key;
|
||||
}
|
||||
|
||||
export function keybindToString(keybind: Keybind): string {
|
||||
if (keybind == null || keybind.key == null)
|
||||
return "";
|
||||
|
||||
let ret = "";
|
||||
if (keybind.ctrl)
|
||||
ret += "Ctrl+";
|
||||
if (keybind.alt)
|
||||
ret += "Alt+";
|
||||
if (keybind.shift)
|
||||
ret += "Shift+";
|
||||
|
||||
return ret += formatKey(keybind.key);
|
||||
}
|
||||
@@ -148,5 +148,13 @@ export function getGuidelineInfo(category: Category): TextBox[] {
|
||||
icon: "icons/check-smaller.svg",
|
||||
text: chrome.i18n.getMessage(`category_${category}_guideline3`)
|
||||
}];
|
||||
default:
|
||||
return [{
|
||||
icon: "icons/segway.png",
|
||||
text: chrome.i18n.getMessage(`generic_guideline1`)
|
||||
}, {
|
||||
icon: "icons/right-arrow.svg",
|
||||
text: chrome.i18n.getMessage(`generic_guideline2`)
|
||||
}];
|
||||
}
|
||||
}
|
||||
44
src/utils/crossExtension.ts
Normal file
44
src/utils/crossExtension.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import * as CompileConfig from "../../config.json";
|
||||
|
||||
import Config from "../config";
|
||||
import { isSafari } from "../../maze-utils/src/config";
|
||||
import { isFirefoxOrSafari } from "../../maze-utils/src";
|
||||
|
||||
export function isDeArrowInstalled(): Promise<boolean> {
|
||||
if (Config.config.deArrowInstalled) {
|
||||
return Promise.resolve(true);
|
||||
} else {
|
||||
return new Promise((resolve) => {
|
||||
const extensionIds = getExtensionIdsToImportFrom();
|
||||
|
||||
let count = 0;
|
||||
for (const id of extensionIds) {
|
||||
chrome.runtime.sendMessage(id, { message: "isInstalled" }, (response) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
count++;
|
||||
|
||||
if (count === extensionIds.length) {
|
||||
resolve(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(response);
|
||||
if (response) {
|
||||
Config.config.deArrowInstalled = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getExtensionIdsToImportFrom(): string[] {
|
||||
if (isSafari()) {
|
||||
return CompileConfig.extensionImportList.safari;
|
||||
} else if (isFirefoxOrSafari()) {
|
||||
return CompileConfig.extensionImportList.firefox;
|
||||
} else {
|
||||
return CompileConfig.extensionImportList.chromium;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,15 @@
|
||||
import { ActionType, Category, SegmentUUID, SponsorSourceType, SponsorTime } from "../types";
|
||||
import { shortCategoryName } from "./categoryUtils";
|
||||
import { GenericUtils } from "./genericUtils";
|
||||
import * as CompileConfig from "../../config.json";
|
||||
import { getFormattedTime, getFormattedTimeToSeconds } from "../../maze-utils/src/formating";
|
||||
import { generateUserID } from "../../maze-utils/src/setup";
|
||||
|
||||
const inTest = typeof chrome === "undefined";
|
||||
|
||||
const chapterNames = CompileConfig.categoryList.filter((code) => code !== "chapter")
|
||||
.map((code) => ({
|
||||
code,
|
||||
name: !inTest ? chrome.i18n.getMessage("category_" + code) : code
|
||||
names: !inTest ? [chrome.i18n.getMessage("category_" + code), shortCategoryName(code)] : [code]
|
||||
}));
|
||||
|
||||
export function exportTimes(segments: SponsorTime[]): string {
|
||||
@@ -26,9 +27,9 @@ export function exportTimes(segments: SponsorTime[]): string {
|
||||
function exportTime(segment: SponsorTime): string {
|
||||
const name = segment.description || shortCategoryName(segment.category);
|
||||
|
||||
return `${GenericUtils.getFormattedTime(segment.segment[0], true)}${
|
||||
return `${getFormattedTime(segment.segment[0], true)}${
|
||||
segment.segment[1] && segment.segment[0] !== segment.segment[1]
|
||||
? ` - ${GenericUtils.getFormattedTime(segment.segment[1], true)}` : ""} ${name}`;
|
||||
? ` - ${getFormattedTime(segment.segment[1], true)}` : ""} ${name}`;
|
||||
}
|
||||
|
||||
export function importTimes(data: string, videoDuration: number): SponsorTime[] {
|
||||
@@ -37,25 +38,31 @@ export function importTimes(data: string, videoDuration: number): SponsorTime[]
|
||||
for (const line of lines) {
|
||||
const match = line.match(/(?:((?:\d+:)?\d+:\d+)+(?:\.\d+)?)|(?:\d+(?=s| second))/g);
|
||||
if (match) {
|
||||
const startTime = GenericUtils.getFormattedTimeToSeconds(match[0]);
|
||||
const startTime = getFormattedTimeToSeconds(match[0]);
|
||||
if (startTime !== null) {
|
||||
const specialCharsMatcher = /^(?:\s+seconds?)?[-:()\s]*|(?:\s+at)?[-:()\s]+$/g
|
||||
const titleLeft = line.split(match[0])[0].replace(specialCharsMatcher, "");
|
||||
// Remove "seconds", "at", special characters, and ")" if there was a "("
|
||||
const specialCharMatchers = [{
|
||||
matcher: /^(?:\s+seconds?)?[-:()\s]*|(?:\s+at)?[-:(\s]+$/g
|
||||
}, {
|
||||
matcher: /[-:()\s]*$/g,
|
||||
condition: (value) => !!value.match(/^\s*\(/)
|
||||
}];
|
||||
const titleLeft = removeIf(line.split(match[0])[0], specialCharMatchers);
|
||||
let titleRight = null;
|
||||
const split2 = line.split(match[1] || match[0]);
|
||||
titleRight = split2[split2.length - 1].replace(specialCharsMatcher, "");
|
||||
titleRight = removeIf(split2[split2.length - 1], specialCharMatchers)
|
||||
|
||||
const title = titleLeft?.length > titleRight?.length ? titleLeft : titleRight;
|
||||
if (title) {
|
||||
const determinedCategory = chapterNames.find(c => c.name === title)?.code as Category;
|
||||
const determinedCategory = chapterNames.find(c => c.names.includes(title))?.code as Category;
|
||||
|
||||
const segment: SponsorTime = {
|
||||
segment: [startTime, GenericUtils.getFormattedTimeToSeconds(match[1])],
|
||||
segment: [startTime, getFormattedTimeToSeconds(match[1])],
|
||||
category: determinedCategory ?? ("chapter" as Category),
|
||||
actionType: determinedCategory ? ActionType.Skip : ActionType.Chapter,
|
||||
description: title,
|
||||
source: SponsorSourceType.Local,
|
||||
UUID: GenericUtils.generateUserID() as SegmentUUID
|
||||
UUID: generateUserID() as SegmentUUID
|
||||
};
|
||||
|
||||
if (result.length > 0 && result[result.length - 1].segment[1] === null) {
|
||||
@@ -75,6 +82,17 @@ export function importTimes(data: string, videoDuration: number): SponsorTime[]
|
||||
return result;
|
||||
}
|
||||
|
||||
function removeIf(value: string, matchers: Array<{ matcher: RegExp; condition?: (value: string) => boolean }>): string {
|
||||
let result = value;
|
||||
for (const matcher of matchers) {
|
||||
if (!matcher.condition || matcher.condition(value)) {
|
||||
result = result.replace(matcher.matcher, "");
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function exportTimesAsHashParam(segments: SponsorTime[]): string {
|
||||
const hashparamSegments = segments.map(segment => ({
|
||||
actionType: segment.actionType,
|
||||
@@ -85,3 +103,8 @@ export function exportTimesAsHashParam(segments: SponsorTime[]): string {
|
||||
|
||||
return `#segments=${JSON.stringify(hashparamSegments)}`;
|
||||
}
|
||||
|
||||
|
||||
export function normalizeChapterName(description: string): string {
|
||||
return description.toLowerCase().replace(/\.|:|-/g, "").replace(/\s+/g, " ");
|
||||
}
|
||||
@@ -1,91 +1,3 @@
|
||||
/** Function that can be used to wait for a condition before returning. */
|
||||
async function wait<T>(condition: () => T, timeout = 5000, check = 100, predicate?: (obj: T) => boolean): Promise<T> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
clearInterval(interval);
|
||||
reject("TIMEOUT");
|
||||
}, timeout);
|
||||
|
||||
const intervalCheck = () => {
|
||||
const result = condition();
|
||||
if (predicate ? predicate(result) : result) {
|
||||
resolve(result);
|
||||
clearInterval(interval);
|
||||
}
|
||||
};
|
||||
|
||||
const interval = setInterval(intervalCheck, check);
|
||||
|
||||
//run the check once first, this speeds it up a lot
|
||||
intervalCheck();
|
||||
});
|
||||
}
|
||||
|
||||
function getFormattedTimeToSeconds(formatted: string): number | null {
|
||||
const fragments = /^(?:(?:(\d+):)?(\d+):)?(\d*(?:[.,]\d+)?)$/.exec(formatted);
|
||||
|
||||
if (fragments === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hours = fragments[1] ? parseInt(fragments[1]) : 0;
|
||||
const minutes = fragments[2] ? parseInt(fragments[2] || '0') : 0;
|
||||
const seconds = fragments[3] ? parseFloat(fragments[3].replace(',', '.')) : 0;
|
||||
|
||||
return hours * 3600 + minutes * 60 + seconds;
|
||||
}
|
||||
|
||||
function getFormattedTime(seconds: number, precise?: boolean): string {
|
||||
seconds = Math.max(seconds, 0);
|
||||
|
||||
const hours = Math.floor(seconds / 60 / 60);
|
||||
const minutes = Math.floor(seconds / 60) % 60;
|
||||
let minutesDisplay = String(minutes);
|
||||
let secondsNum = seconds % 60;
|
||||
if (!precise) {
|
||||
secondsNum = Math.floor(secondsNum);
|
||||
}
|
||||
|
||||
let secondsDisplay = String(precise ? secondsNum.toFixed(3) : secondsNum);
|
||||
|
||||
if (secondsNum < 10) {
|
||||
//add a zero
|
||||
secondsDisplay = "0" + secondsDisplay;
|
||||
}
|
||||
if (hours && minutes < 10) {
|
||||
//add a zero
|
||||
minutesDisplay = "0" + minutesDisplay;
|
||||
}
|
||||
if (isNaN(hours) || isNaN(minutes)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const formatted = (hours ? hours + ":" : "") + minutesDisplay + ":" + secondsDisplay;
|
||||
|
||||
return formatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the error message in a nice string
|
||||
*
|
||||
* @param {int} statusCode
|
||||
* @returns {string} errorMessage
|
||||
*/
|
||||
function getErrorMessage(statusCode: number, responseText: string): string {
|
||||
const postFix = ((responseText && !(responseText.includes(`cf-wrapper`) || responseText.includes("<!DOCTYPE html>"))) ? "\n\n" + responseText : "");
|
||||
// display response body for 4xx
|
||||
if([400, 429, 409, 0].includes(statusCode)) {
|
||||
return chrome.i18n.getMessage(statusCode + "") + " " + chrome.i18n.getMessage("errorCode") + statusCode + postFix;
|
||||
} else if (statusCode >= 500 && statusCode <= 599) {
|
||||
// 503 == 502
|
||||
if (statusCode == 503) statusCode = 502;
|
||||
return chrome.i18n.getMessage(statusCode + "") + " " + chrome.i18n.getMessage("errorCode") + statusCode
|
||||
+ "\n\n" + chrome.i18n.getMessage("statusReminder");
|
||||
} else {
|
||||
return chrome.i18n.getMessage("connectionError") + statusCode + postFix;
|
||||
}
|
||||
}
|
||||
|
||||
/* Gets percieved luminance of a color */
|
||||
function getLuminance(color: string): number {
|
||||
const {r, g, b} = hexToRgb(color);
|
||||
@@ -113,44 +25,7 @@ function indexesOf<T>(array: T[], value: T): number[] {
|
||||
return array.map((v, i) => v === value ? i : -1).filter(i => i !== -1);
|
||||
}
|
||||
|
||||
function objectToURI<T>(url: string, data: T, includeQuestionMark: boolean): string {
|
||||
let counter = 0;
|
||||
for (const key in data) {
|
||||
const seperator = (url.includes("?") || counter > 0) ? "&" : (includeQuestionMark ? "?" : "");
|
||||
const value = (typeof(data[key]) === "string") ? data[key] as unknown as string : JSON.stringify(data[key]);
|
||||
url += seperator + encodeURIComponent(key) + "=" + encodeURIComponent(value);
|
||||
|
||||
counter++;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
function generateUserID(length = 36): string {
|
||||
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let result = "";
|
||||
if (window.crypto && window.crypto.getRandomValues) {
|
||||
const values = new Uint32Array(length);
|
||||
window.crypto.getRandomValues(values);
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += charset[values[i] % charset.length];
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += charset[Math.floor(Math.random() * charset.length)];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export const GenericUtils = {
|
||||
wait,
|
||||
getFormattedTime,
|
||||
getFormattedTimeToSeconds,
|
||||
getErrorMessage,
|
||||
getLuminance,
|
||||
generateUserID,
|
||||
indexesOf,
|
||||
objectToURI
|
||||
indexesOf
|
||||
}
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import Config from "../config";
|
||||
import Utils from "../utils";
|
||||
import * as CompileConfig from "../../config.json";
|
||||
|
||||
const utils = new Utils();
|
||||
|
||||
export async function checkLicenseKey(licenseKey: string): Promise<boolean> {
|
||||
const result = await utils.asyncRequestToServer("GET", "/api/verifyToken", {
|
||||
licenseKey
|
||||
});
|
||||
|
||||
try {
|
||||
if (result.ok && JSON.parse(result.responseText).allowed) {
|
||||
Config.config.payments.chaptersAllowed = true;
|
||||
Config.config.showChapterInfoMessage = false;
|
||||
Config.config.payments.lastCheck = Date.now();
|
||||
Config.forceSyncUpdate("payments");
|
||||
|
||||
return true;
|
||||
}
|
||||
} catch (e) { } //eslint-disable-line no-empty
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* The other one also tried refreshing, so returns a promise
|
||||
*/
|
||||
export function noRefreshFetchingChaptersAllowed(): boolean {
|
||||
return Config.config.payments.chaptersAllowed || CompileConfig["freeChapterAccess"];
|
||||
}
|
||||
|
||||
export async function fetchingChaptersAllowed(): Promise<boolean> {
|
||||
if (Config.config.payments.freeAccess || CompileConfig["freeChapterAccess"]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
//more than 14 days
|
||||
if (Config.config.payments.licenseKey && Date.now() - Config.config.payments.lastCheck > 14 * 24 * 60 * 60 * 1000) {
|
||||
const licensePromise = checkLicenseKey(Config.config.payments.licenseKey);
|
||||
|
||||
if (!Config.config.payments.chaptersAllowed) {
|
||||
return licensePromise;
|
||||
}
|
||||
}
|
||||
|
||||
if (Config.config.payments.chaptersAllowed) return true;
|
||||
|
||||
if (Config.config.payments.lastCheck === 0 && Date.now() - Config.config.payments.lastFreeCheck > 2 * 24 * 60 * 60 * 1000) {
|
||||
Config.config.payments.lastFreeCheck = Date.now();
|
||||
Config.forceSyncUpdate("payments");
|
||||
|
||||
// Check for free access if no license key, and it is the first time
|
||||
const result = await utils.asyncRequestToServer("GET", "/api/userInfo", {
|
||||
value: "freeChaptersAccess",
|
||||
userID: Config.config.userID
|
||||
});
|
||||
|
||||
try {
|
||||
if (result.ok) {
|
||||
const userInfo = JSON.parse(result.responseText);
|
||||
|
||||
Config.config.payments.lastCheck = Date.now();
|
||||
if (userInfo.freeChaptersAccess) {
|
||||
Config.config.payments.freeAccess = true;
|
||||
Config.config.payments.chaptersAllowed = true;
|
||||
Config.config.showChapterInfoMessage = false;
|
||||
Config.forceSyncUpdate("payments");
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (e) { } //eslint-disable-line no-empty
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
8
src/utils/pageCleaner.ts
Normal file
8
src/utils/pageCleaner.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export function cleanPage() {
|
||||
// For live-updates
|
||||
if (document.readyState === "complete") {
|
||||
for (const element of document.querySelectorAll("#categoryPillParent, .playerButton, .sponsorThumbnailLabel, #submissionNoticeContainer, .sponsorSkipNoticeContainer, #sponsorBlockPopupContainer, .skipButtonControlBarContainer, #previewbar, .sponsorBlockChapterBar")) {
|
||||
element.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ActionType, Category, SponsorSourceType, SponsorTime, VideoID } from "../types";
|
||||
import { GenericUtils } from "./genericUtils";
|
||||
import { getFormattedTimeToSeconds } from "../../maze-utils/src/formating";
|
||||
|
||||
export function getControls(): HTMLElement {
|
||||
const controlsSelectors = [
|
||||
@@ -9,12 +9,14 @@ export function getControls(): HTMLElement {
|
||||
".player-controls-top",
|
||||
// Invidious/videojs video element's controls element
|
||||
".vjs-control-bar",
|
||||
// Piped shaka player
|
||||
".shaka-bottom-controls"
|
||||
];
|
||||
|
||||
for (const controlsSelector of controlsSelectors) {
|
||||
const controls = document.querySelectorAll(controlsSelector);
|
||||
const controls = Array.from(document.querySelectorAll(controlsSelector)).filter(el => !isInPreviewPlayer(el));
|
||||
|
||||
if (controls && controls.length > 0) {
|
||||
if (controls.length > 0) {
|
||||
return <HTMLElement> controls[controls.length - 1];
|
||||
}
|
||||
}
|
||||
@@ -22,29 +24,14 @@ export function getControls(): HTMLElement {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isInPreviewPlayer(element: Element): boolean {
|
||||
return !!element.closest("#inline-preview-player");
|
||||
}
|
||||
|
||||
export function isVisible(element: HTMLElement): boolean {
|
||||
return element && element.offsetWidth > 0 && element.offsetHeight > 0;
|
||||
}
|
||||
|
||||
export function findValidElementFromSelector(selectors: string[]): HTMLElement {
|
||||
return findValidElementFromGenerator(selectors, (selector) => document.querySelector(selector));
|
||||
}
|
||||
|
||||
export function findValidElement(elements: HTMLElement[] | NodeListOf<HTMLElement>): HTMLElement {
|
||||
return findValidElementFromGenerator(elements);
|
||||
}
|
||||
|
||||
function findValidElementFromGenerator<T>(objects: T[] | NodeListOf<HTMLElement>, generator?: (obj: T) => HTMLElement): HTMLElement {
|
||||
for (const obj of objects) {
|
||||
const element = generator ? generator(obj as T) : obj as HTMLElement;
|
||||
if (element && isVisible(element)) {
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getHashParams(): Record<string, unknown> {
|
||||
const windowHash = window.location.hash.slice(1);
|
||||
if (windowHash) {
|
||||
@@ -68,24 +55,26 @@ export function getHashParams(): Record<string, unknown> {
|
||||
|
||||
export function getExistingChapters(currentVideoID: VideoID, duration: number): SponsorTime[] {
|
||||
const chaptersBox = document.querySelector("ytd-macro-markers-list-renderer");
|
||||
const title = document.querySelector("[target-id=engagement-panel-macro-markers-auto-chapters] #title-text");
|
||||
if (title?.textContent?.includes("Key moment")) return [];
|
||||
|
||||
const chapters: SponsorTime[] = [];
|
||||
// .ytp-timed-markers-container indicates that key-moments are present, which should not be divided
|
||||
if (chaptersBox && !(document.querySelector(".ytp-timed-markers-container")?.childElementCount > 0)) {
|
||||
if (chaptersBox) {
|
||||
let lastSegment: SponsorTime = null;
|
||||
const links = chaptersBox.querySelectorAll("ytd-macro-markers-list-item-renderer > a");
|
||||
for (const link of links) {
|
||||
const timeElement = link.querySelector("#time") as HTMLElement;
|
||||
const description = link.querySelector("#details h4") as HTMLElement;
|
||||
if (timeElement && description?.innerText?.length > 0 && link.getAttribute("href")?.includes(currentVideoID)) {
|
||||
const time = GenericUtils.getFormattedTimeToSeconds(timeElement.innerText);
|
||||
const time = getFormattedTimeToSeconds(timeElement.innerText.replace(/\./g, ":"));
|
||||
if (time === null) return [];
|
||||
|
||||
|
||||
if (lastSegment) {
|
||||
lastSegment.segment[1] = time;
|
||||
chapters.push(lastSegment);
|
||||
}
|
||||
|
||||
|
||||
lastSegment = {
|
||||
segment: [time, null],
|
||||
category: "chapter" as Category,
|
||||
@@ -106,25 +95,6 @@ export function getExistingChapters(currentVideoID: VideoID, duration: number):
|
||||
return chapters;
|
||||
}
|
||||
|
||||
export function localizeHtmlPage(): void {
|
||||
//Localize by replacing __MSG_***__ meta tags
|
||||
const localizedTitle = getLocalizedMessage(document.title);
|
||||
if (localizedTitle) document.title = localizedTitle;
|
||||
|
||||
const body = document.querySelector(".sponsorBlockPageBody");
|
||||
const localizedMessage = getLocalizedMessage(body.innerHTML.toString());
|
||||
if (localizedMessage) body.innerHTML = localizedMessage;
|
||||
}
|
||||
|
||||
export function getLocalizedMessage(text: string): string | false {
|
||||
const valNewH = text.replace(/__MSG_(\w+)__/g, function(match, v1) {
|
||||
return v1 ? chrome.i18n.getMessage(v1).replace(/</g, "<")
|
||||
.replace(/"/g, """).replace(/\n/g, "<br/>") : "";
|
||||
});
|
||||
|
||||
if (valNewH != text) {
|
||||
return valNewH;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
export function isPlayingPlaylist() {
|
||||
return !!document.URL.includes("&list=");
|
||||
}
|
||||
115
src/utils/thumbnails.ts
Normal file
115
src/utils/thumbnails.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { isOnInvidious, parseYouTubeVideoIDFromURL } from "../../maze-utils/src/video";
|
||||
import Config from "../config";
|
||||
import { getVideoLabel } from "./videoLabels";
|
||||
import { setThumbnailListener } from "../../maze-utils/src/thumbnailManagement";
|
||||
|
||||
export async function labelThumbnails(thumbnails: HTMLImageElement[]): Promise<void> {
|
||||
await Promise.all(thumbnails.map((t) => labelThumbnail(t)));
|
||||
}
|
||||
|
||||
export async function labelThumbnail(thumbnail: HTMLImageElement): Promise<HTMLElement | null> {
|
||||
if (!Config.config?.fullVideoSegments || !Config.config?.fullVideoLabelsOnThumbnails) {
|
||||
hideThumbnailLabel(thumbnail);
|
||||
return null;
|
||||
}
|
||||
|
||||
const link = (isOnInvidious() ? thumbnail.parentElement : thumbnail.querySelector("#thumbnail")) as HTMLAnchorElement
|
||||
if (!link || link.nodeName !== "A" || !link.href) return null; // no link found
|
||||
const videoID = parseYouTubeVideoIDFromURL(link.href)?.videoID;
|
||||
if (!videoID) {
|
||||
hideThumbnailLabel(thumbnail);
|
||||
return null;
|
||||
}
|
||||
|
||||
const category = await getVideoLabel(videoID);
|
||||
if (!category) {
|
||||
hideThumbnailLabel(thumbnail);
|
||||
return null;
|
||||
}
|
||||
|
||||
const { overlay, text } = createOrGetThumbnail(thumbnail);
|
||||
|
||||
overlay.style.setProperty('--category-color', `var(--sb-category-preview-${category}, var(--sb-category-${category}))`);
|
||||
overlay.style.setProperty('--category-text-color', `var(--sb-category-text-preview-${category}, var(--sb-category-text-${category}))`);
|
||||
text.innerText = chrome.i18n.getMessage(`category_${category}`);
|
||||
overlay.classList.add("sponsorThumbnailLabelVisible");
|
||||
|
||||
return overlay;
|
||||
}
|
||||
|
||||
function getOldThumbnailLabel(thumbnail: HTMLImageElement): HTMLElement | null {
|
||||
return thumbnail.querySelector(".sponsorThumbnailLabel") as HTMLElement | null;
|
||||
}
|
||||
|
||||
function hideThumbnailLabel(thumbnail: HTMLImageElement): void {
|
||||
const oldLabel = getOldThumbnailLabel(thumbnail);
|
||||
if (oldLabel) {
|
||||
oldLabel.classList.remove("sponsorThumbnailLabelVisible");
|
||||
}
|
||||
}
|
||||
|
||||
function createOrGetThumbnail(thumbnail: HTMLImageElement): { overlay: HTMLElement; text: HTMLElement } {
|
||||
const oldElement = getOldThumbnailLabel(thumbnail);
|
||||
if (oldElement) {
|
||||
return {
|
||||
overlay: oldElement as HTMLElement,
|
||||
text: oldElement.querySelector("span") as HTMLElement
|
||||
};
|
||||
}
|
||||
|
||||
const overlay = document.createElement("div") as HTMLElement;
|
||||
overlay.classList.add("sponsorThumbnailLabel");
|
||||
// Disable hover autoplay
|
||||
overlay.addEventListener("pointerenter", (e) => {
|
||||
e.stopPropagation();
|
||||
thumbnail.dispatchEvent(new PointerEvent("pointerleave", { bubbles: true }));
|
||||
});
|
||||
overlay.addEventListener("pointerleave", (e) => {
|
||||
e.stopPropagation();
|
||||
thumbnail.dispatchEvent(new PointerEvent("pointerenter", { bubbles: true }));
|
||||
});
|
||||
|
||||
const icon = createSBIconElement();
|
||||
const text = document.createElement("span");
|
||||
overlay.appendChild(icon);
|
||||
overlay.appendChild(text);
|
||||
thumbnail.appendChild(overlay);
|
||||
|
||||
return {
|
||||
overlay,
|
||||
text
|
||||
};
|
||||
}
|
||||
|
||||
function createSBIconElement(): SVGSVGElement {
|
||||
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
svg.setAttribute("viewBox", "0 0 565.15 568");
|
||||
const use = document.createElementNS("http://www.w3.org/2000/svg", "use");
|
||||
use.setAttribute("href", "#SponsorBlockIcon");
|
||||
svg.appendChild(use);
|
||||
return svg;
|
||||
}
|
||||
|
||||
|
||||
// Inserts the icon svg definition, so it can be used elsewhere
|
||||
function insertSBIconDefinition() {
|
||||
const container = document.createElement("span");
|
||||
|
||||
// svg from /public/icons/PlayerStartIconSponsorBlocker.svg, with useless stuff removed
|
||||
container.innerHTML = `
|
||||
<svg viewBox="0 0 565.15 568" style="display: none">
|
||||
<defs>
|
||||
<g id="SponsorBlockIcon">
|
||||
<path d="M282.58,568a65,65,0,0,1-34.14-9.66C95.41,463.94,2.54,300.46,0,121A64.91,64.91,0,0,1,34,62.91a522.56,522.56,0,0,1,497.16,0,64.91,64.91,0,0,1,34,58.12c-2.53,179.43-95.4,342.91-248.42,437.3A65,65,0,0,1,282.58,568Zm0-548.31A502.24,502.24,0,0,0,43.4,80.22a45.27,45.27,0,0,0-23.7,40.53c2.44,172.67,91.81,330,239.07,420.83a46.19,46.19,0,0,0,47.61,0C453.64,450.73,543,293.42,545.45,120.75a45.26,45.26,0,0,0-23.7-40.54A502.26,502.26,0,0,0,282.58,19.69Z"/>
|
||||
<path d="M 284.70508 42.693359 A 479.9 479.9 0 0 0 54.369141 100.41992 A 22.53 22.53 0 0 0 42.669922 120.41992 C 45.069922 290.25992 135.67008 438.63977 270.83008 522.00977 A 22.48 22.48 0 0 0 294.32031 522.00977 C 429.48031 438.63977 520.08047 290.25992 522.48047 120.41992 A 22.53 22.53 0 0 0 510.7793 100.41992 A 479.9 479.9 0 0 0 284.70508 42.693359 z M 220.41016 145.74023 L 411.2793 255.93945 L 220.41016 366.14062 L 220.41016 145.74023 z "/>
|
||||
</g>
|
||||
</defs>
|
||||
</svg>`;
|
||||
document.body.appendChild(container.children[0]);
|
||||
}
|
||||
|
||||
export function setupThumbnailListener(): void {
|
||||
setThumbnailListener(labelThumbnails, () => {
|
||||
insertSBIconDefinition();
|
||||
}, () => Config.isReady());
|
||||
}
|
||||
@@ -22,5 +22,7 @@ export function urlTimeToSeconds(time: string): number {
|
||||
return hours * 3600 + minutes * 60 + seconds;
|
||||
} else if (/\d+/.test(time)) {
|
||||
return parseInt(time, 10);
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
70
src/utils/videoLabels.ts
Normal file
70
src/utils/videoLabels.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { Category, CategorySkipOption, VideoID } from "../types";
|
||||
import { getHash } from "../../maze-utils/src/hash";
|
||||
import Utils from "../utils";
|
||||
import { logWarn } from "./logger";
|
||||
|
||||
const utils = new Utils();
|
||||
|
||||
export interface LabelCacheEntry {
|
||||
timestamp: number;
|
||||
videos: Record<VideoID, Category>;
|
||||
}
|
||||
|
||||
const labelCache: Record<string, LabelCacheEntry> = {};
|
||||
const cacheLimit = 1000;
|
||||
|
||||
async function getLabelHashBlock(hashPrefix: string): Promise<LabelCacheEntry | null> {
|
||||
// Check cache
|
||||
const cachedEntry = labelCache[hashPrefix];
|
||||
if (cachedEntry) {
|
||||
return cachedEntry;
|
||||
}
|
||||
|
||||
const response = await utils.asyncRequestToServer("GET", `/api/videoLabels/${hashPrefix}`);
|
||||
if (response.status !== 200) {
|
||||
// No video labels or server down
|
||||
labelCache[hashPrefix] = {
|
||||
timestamp: Date.now(),
|
||||
videos: {},
|
||||
};
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(response.responseText);
|
||||
|
||||
const newEntry: LabelCacheEntry = {
|
||||
timestamp: Date.now(),
|
||||
videos: Object.fromEntries(data.map(video => [video.videoID, video.segments[0].category])),
|
||||
};
|
||||
labelCache[hashPrefix] = newEntry;
|
||||
|
||||
if (Object.keys(labelCache).length > cacheLimit) {
|
||||
// Remove oldest entry
|
||||
const oldestEntry = Object.entries(labelCache).reduce((a, b) => a[1].timestamp < b[1].timestamp ? a : b);
|
||||
delete labelCache[oldestEntry[0]];
|
||||
}
|
||||
|
||||
return newEntry;
|
||||
} catch (e) {
|
||||
logWarn(`Error parsing video labels: ${e}`);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getVideoLabel(videoID: VideoID): Promise<Category | null> {
|
||||
const prefix = (await getHash(videoID, 1)).slice(0, 3);
|
||||
const result = await getLabelHashBlock(prefix);
|
||||
|
||||
if (result) {
|
||||
const category = result.videos[videoID];
|
||||
if (category && utils.getCategorySelection(category).option !== CategorySkipOption.Disabled) {
|
||||
return category;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import { objectToURI } from "../../maze-utils/src";
|
||||
import { getHash } from "../../maze-utils/src/hash";
|
||||
import Config from "../config";
|
||||
import GenericNotice, { NoticeOptions } from "../render/GenericNotice";
|
||||
import { ContentContainer } from "../types";
|
||||
import Utils from "../utils";
|
||||
import { GenericUtils } from "./genericUtils";
|
||||
const utils = new Utils();
|
||||
|
||||
export interface ChatConfig {
|
||||
@@ -13,7 +14,7 @@ export interface ChatConfig {
|
||||
|
||||
export async function openWarningDialog(contentContainer: ContentContainer): Promise<void> {
|
||||
const userInfo = await utils.asyncRequestToServer("GET", "/api/userInfo", {
|
||||
userID: Config.config.userID,
|
||||
publicUserID: await getHash(Config.config.userID),
|
||||
values: ["warningReason"]
|
||||
});
|
||||
|
||||
@@ -21,7 +22,7 @@ export async function openWarningDialog(contentContainer: ContentContainer): Pro
|
||||
const warningReason = JSON.parse(userInfo.responseText)?.warningReason;
|
||||
const userNameData = await utils.asyncRequestToServer("GET", "/api/getUsername?userID=" + Config.config.userID);
|
||||
const userName = userNameData.ok ? JSON.parse(userNameData.responseText).userName : "";
|
||||
const publicUserID = await utils.getHash(Config.config.userID);
|
||||
const publicUserID = await getHash(Config.config.userID);
|
||||
|
||||
let notice: GenericNotice = null;
|
||||
const options: NoticeOptions = {
|
||||
@@ -62,5 +63,5 @@ export async function openWarningDialog(contentContainer: ContentContainer): Pro
|
||||
}
|
||||
|
||||
export function openChat(config: ChatConfig): void {
|
||||
window.open("https://chat.sponsor.ajay.app/#" + GenericUtils.objectToURI("", config, false));
|
||||
}
|
||||
window.open("https://chat.sponsor.ajay.app/#" + objectToURI("", config, false));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user