Add pre-fetching on hover with a small segment data cache

This commit is contained in:
Ajay 2024-09-02 03:22:33 -04:00
parent e181c64775
commit dbf80b4929
5 changed files with 157 additions and 55 deletions

@ -1 +1 @@
Subproject commit ab431ec8ba764b4a7a07d2debc91b5903c65db5e Subproject commit e735dcabe8f378d49b64b04097dbdf6dc7525228

View file

@ -1,7 +1,6 @@
import Config from "./config"; import Config from "./config";
import { import {
ActionType, ActionType,
ActionTypes,
Category, Category,
CategorySkipOption, CategorySkipOption,
ChannelIDInfo, ChannelIDInfo,
@ -18,7 +17,6 @@ import {
VideoInfo, VideoInfo,
} from "./types"; } from "./types";
import Utils from "./utils"; import Utils from "./utils";
import * as CompileConfig from "../config.json";
import PreviewBar, { PreviewBarSegment } from "./js-components/previewBar"; import PreviewBar, { PreviewBarSegment } from "./js-components/previewBar";
import SkipNotice from "./render/SkipNotice"; import SkipNotice from "./render/SkipNotice";
import SkipNoticeComponent from "./components/SkipNoticeComponent"; import SkipNoticeComponent from "./components/SkipNoticeComponent";
@ -52,6 +50,7 @@ import { asyncRequestToServer } from "./utils/requests";
import { isMobileControlsOpen } from "./utils/mobileUtils"; import { isMobileControlsOpen } from "./utils/mobileUtils";
import { defaultPreviewTime } from "./utils/constants"; import { defaultPreviewTime } from "./utils/constants";
import { onVideoPage } from "../maze-utils/src/pageInfo"; import { onVideoPage } from "../maze-utils/src/pageInfo";
import { getSegmentsForVideo } from "./utils/segmentData";
cleanPage(); cleanPage();
@ -269,7 +268,7 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
sendResponse({ hasVideo: getVideoID() != null }); sendResponse({ hasVideo: getVideoID() != null });
// fetch segments // fetch segments
if (getVideoID()) { if (getVideoID()) {
sponsorsLookup(false); sponsorsLookup(false, true);
} }
break; break;
@ -364,7 +363,7 @@ function contentConfigUpdateListener(changes: StorageChangesObject) {
updateVisibilityOfPlayerControlsButton() updateVisibilityOfPlayerControlsButton()
break; break;
case "categorySelections": case "categorySelections":
sponsorsLookup(); sponsorsLookup(true, true);
break; break;
case "barTypes": case "barTypes":
setCategoryColorCSSVariables(); setCategoryColorCSSVariables();
@ -1133,42 +1132,20 @@ function setupCategoryPill() {
categoryPill.attachToPage(isOnMobileYouTube(), isOnInvidious(), voteAsync); categoryPill.attachToPage(isOnMobileYouTube(), isOnInvidious(), voteAsync);
} }
async function sponsorsLookup(keepOldSubmissions = true) { async function sponsorsLookup(keepOldSubmissions = true, ignoreCache = false) {
const categories: string[] = Config.config.categorySelections.map((category) => category.name);
const extraRequestData: Record<string, unknown> = {};
const hashParams = getHashParams();
if (hashParams.requiredSegment) extraRequestData.requiredSegment = hashParams.requiredSegment;
const videoID = getVideoID() const videoID = getVideoID()
if (!videoID) { if (!videoID) {
console.error("[SponsorBlock] Attempted to fetch segments with a null/undefined videoID."); console.error("[SponsorBlock] Attempted to fetch segments with a null/undefined videoID.");
return; return;
} }
const hashPrefix = (await getHash(videoID, 1)).slice(0, 4) as VideoID & HashedValue;
const response = await asyncRequestToServer('GET', "/api/skipSegments/" + hashPrefix, { const segmentData = await getSegmentsForVideo(videoID, ignoreCache);
categories: CompileConfig.categoryList,
actionTypes: ActionTypes,
...extraRequestData
}, {
"X-CLIENT-NAME": `${chrome.runtime.id}/v${chrome.runtime.getManifest().version}`
});
// store last response status // store last response status
lastResponseStatus = response?.status; lastResponseStatus = segmentData.status;
if (segmentData.status === 200) {
const receivedSegments = segmentData.segments;
if (response?.ok) {
const enabledActionTypes = getEnabledActionTypes();
const receivedSegments: SponsorTime[] = JSON.parse(response.responseText)
?.filter((video) => video.videoID === getVideoID())
?.map((video) => video.segments)?.[0]
?.filter((segment) => enabledActionTypes.includes(segment.actionType) && categories.includes(segment.category))
?.map((segment) => ({
...segment,
source: SponsorSourceType.Server
}))
?.sort((a, b) => a.segment[0] - b.segment[0]);
if (receivedSegments && receivedSegments.length) { if (receivedSegments && receivedSegments.length) {
sponsorDataFound = true; sponsorDataFound = true;
@ -1208,6 +1185,7 @@ async function sponsorsLookup(keepOldSubmissions = true) {
} }
// See if some segments should be hidden // See if some segments should be hidden
const hashPrefix = (await getHash(videoID, 1)).slice(0, 4) as VideoID & HashedValue;
const downvotedData = Config.local.downvotedSegments[hashPrefix]; const downvotedData = Config.local.downvotedSegments[hashPrefix];
if (downvotedData) { if (downvotedData) {
for (const segment of sponsorTimes) { for (const segment of sponsorTimes) {
@ -1280,18 +1258,6 @@ function importExistingChapters(wait: boolean) {
} }
} }
function getEnabledActionTypes(forceFullVideo = false): ActionType[] {
const actionTypes = [ActionType.Skip, ActionType.Poi, ActionType.Chapter];
if (Config.config.muteSegments) {
actionTypes.push(ActionType.Mute);
}
if (Config.config.fullVideoSegments || forceFullVideo) {
actionTypes.push(ActionType.Full);
}
return actionTypes;
}
async function lockedCategoriesLookup(): Promise<void> { async function lockedCategoriesLookup(): Promise<void> {
const hashPrefix = (await getHash(getVideoID(), 1)).slice(0, 4); const hashPrefix = (await getHash(getVideoID(), 1)).slice(0, 4);
const response = await asyncRequestToServer("GET", "/api/lockCategories/" + hashPrefix); const response = await asyncRequestToServer("GET", "/api/lockCategories/" + hashPrefix);
@ -2016,7 +1982,7 @@ function startOrEndTimingNewSegment() {
Config.forceLocalUpdate("unsubmittedSegments"); Config.forceLocalUpdate("unsubmittedSegments");
// Make sure they know if someone has already submitted something it while they were watching // Make sure they know if someone has already submitted something it while they were watching
sponsorsLookup(); sponsorsLookup(true, true);
updateEditButtonsOnPlayer(); updateEditButtonsOnPlayer();
updateSponsorTimesSubmitting(false); updateSponsorTimesSubmitting(false);

View file

@ -23,8 +23,6 @@ export function asyncRequestToCustomServer(type: string, url: string, data = {},
export async function asyncRequestToServer(type: string, address: string, data = {}, headers = {}): Promise<FetchResponse> { export async function asyncRequestToServer(type: string, address: string, data = {}, headers = {}): Promise<FetchResponse> {
const serverAddress = Config.config.testingServer ? CompileConfig.testingServerAddress : Config.config.serverAddress; const serverAddress = Config.config.testingServer ? CompileConfig.testingServerAddress : Config.config.serverAddress;
console.log(address, headers)
return await (asyncRequestToCustomServer(type, serverAddress + address, data, headers)); return await (asyncRequestToCustomServer(type, serverAddress + address, data, headers));
} }

104
src/utils/segmentData.ts Normal file
View file

@ -0,0 +1,104 @@
import { DataCache } from "../../maze-utils/src/cache";
import { getHash, HashedValue } from "../../maze-utils/src/hash";
import Config from "../config";
import * as CompileConfig from "../../config.json";
import { ActionType, ActionTypes, SponsorSourceType, SponsorTime, VideoID } from "../types";
import { getHashParams } from "./pageUtils";
import { asyncRequestToServer } from "./requests";
const segmentDataCache = new DataCache<VideoID, SegmentResponse>(() => {
return {
segments: null,
status: 200
};
}, 5);
const pendingList: Record<VideoID, Promise<SegmentResponse>> = {};
export interface SegmentResponse {
segments: SponsorTime[] | null;
status: number;
}
export async function getSegmentsForVideo(videoID: VideoID, ignoreCache: boolean): Promise<SegmentResponse> {
if (!ignoreCache) {
const cachedData = segmentDataCache.getFromCache(videoID);
if (cachedData) {
segmentDataCache.cacheUsed(videoID);
return cachedData;
}
}
if (pendingList[videoID]) {
return await pendingList[videoID];
}
const pendingData = fetchSegmentsForVideo(videoID);
pendingList[videoID] = pendingData;
const result = await pendingData;
delete pendingList[videoID];
return result;
}
async function fetchSegmentsForVideo(videoID: VideoID): Promise<SegmentResponse> {
const categories: string[] = Config.config.categorySelections.map((category) => category.name);
const extraRequestData: Record<string, unknown> = {};
const hashParams = getHashParams();
if (hashParams.requiredSegment) extraRequestData.requiredSegment = hashParams.requiredSegment;
const hashPrefix = (await getHash(videoID, 1)).slice(0, 4) as VideoID & HashedValue;
const response = await asyncRequestToServer('GET', "/api/skipSegments/" + hashPrefix, {
categories: CompileConfig.categoryList,
actionTypes: ActionTypes,
...extraRequestData
}, {
"X-CLIENT-NAME": `${chrome.runtime.id}/v${chrome.runtime.getManifest().version}`
});
if (response.ok) {
const enabledActionTypes = getEnabledActionTypes();
const receivedSegments: SponsorTime[] = JSON.parse(response.responseText)
?.filter((video) => video.videoID === videoID)
?.map((video) => video.segments)?.[0]
?.filter((segment) => enabledActionTypes.includes(segment.actionType) && categories.includes(segment.category))
?.map((segment) => ({
...segment,
source: SponsorSourceType.Server
}))
?.sort((a, b) => a.segment[0] - b.segment[0]);
if (receivedSegments && receivedSegments.length) {
const result = {
segments: receivedSegments,
status: response.status
};
segmentDataCache.setupCache(videoID).segments = result.segments;
return result;
} else {
// Setup with null data
segmentDataCache.setupCache(videoID);
}
}
return {
segments: null,
status: response.status
};
}
function getEnabledActionTypes(forceFullVideo = false): ActionType[] {
const actionTypes = [ActionType.Skip, ActionType.Poi, ActionType.Chapter];
if (Config.config.muteSegments) {
actionTypes.push(ActionType.Mute);
}
if (Config.config.fullVideoSegments || forceFullVideo) {
actionTypes.push(ActionType.Full);
}
return actionTypes;
}

View file

@ -1,10 +1,15 @@
import { isOnInvidious, parseYouTubeVideoIDFromURL } from "../../maze-utils/src/video"; import { isOnInvidious, parseYouTubeVideoIDFromURL } from "../../maze-utils/src/video";
import Config from "../config"; import Config from "../config";
import { getVideoLabel } from "./videoLabels"; import { getVideoLabel } from "./videoLabels";
import { setThumbnailListener } from "../../maze-utils/src/thumbnailManagement"; import { getThumbnailSelector, setThumbnailListener } from "../../maze-utils/src/thumbnailManagement";
import { VideoID } from "../types";
import { getSegmentsForVideo } from "./segmentData";
export async function labelThumbnails(thumbnails: HTMLImageElement[]): Promise<void> { export async function handleThumbnails(thumbnails: HTMLImageElement[]): Promise<void> {
await Promise.all(thumbnails.map((t) => labelThumbnail(t))); await Promise.all(thumbnails.map((t) => {
labelThumbnail(t);
setupThumbnailHover(t);
}));
} }
export async function labelThumbnail(thumbnail: HTMLImageElement): Promise<HTMLElement | null> { export async function labelThumbnail(thumbnail: HTMLImageElement): Promise<HTMLElement | null> {
@ -13,9 +18,7 @@ export async function labelThumbnail(thumbnail: HTMLImageElement): Promise<HTMLE
return null; return null;
} }
const link = (isOnInvidious() ? thumbnail.parentElement : thumbnail.querySelector("#thumbnail")) as HTMLAnchorElement const videoID = extractVideoID(thumbnail);
if (!link || link.nodeName !== "A" || !link.href) return null; // no link found
const videoID = parseYouTubeVideoIDFromURL(link.href)?.videoID;
if (!videoID) { if (!videoID) {
hideThumbnailLabel(thumbnail); hideThumbnailLabel(thumbnail);
return null; return null;
@ -37,6 +40,37 @@ export async function labelThumbnail(thumbnail: HTMLImageElement): Promise<HTMLE
return overlay; return overlay;
} }
export async function setupThumbnailHover(thumbnail: HTMLImageElement): Promise<void> {
// Cache would be reset every load due to no SPA
if (isOnInvidious()) return;
const mainElement = thumbnail.closest("#dismissible") as HTMLElement;
if (mainElement) {
mainElement.removeEventListener("mouseenter", thumbnailHoverListener);
mainElement.addEventListener("mouseenter", thumbnailHoverListener);
}
}
function thumbnailHoverListener(e: MouseEvent) {
if (!chrome.runtime?.id) return;
const thumbnail = (e.target as HTMLElement).querySelector(getThumbnailSelector()) as HTMLImageElement;
if (!thumbnail) return;
// Pre-fetch data for this video
const videoID = extractVideoID(thumbnail);
if (videoID) {
void getSegmentsForVideo(videoID, false);
}
}
function extractVideoID(thumbnail: HTMLImageElement): VideoID | null {
const link = (isOnInvidious() ? thumbnail.parentElement : thumbnail.querySelector("#thumbnail")) as HTMLAnchorElement
if (!link || link.nodeName !== "A" || !link.href) return null; // no link found
return parseYouTubeVideoIDFromURL(link.href)?.videoID;
}
function getOldThumbnailLabel(thumbnail: HTMLImageElement): HTMLElement | null { function getOldThumbnailLabel(thumbnail: HTMLImageElement): HTMLElement | null {
return thumbnail.querySelector(".sponsorThumbnailLabel") as HTMLElement | null; return thumbnail.querySelector(".sponsorThumbnailLabel") as HTMLElement | null;
} }
@ -109,7 +143,7 @@ function insertSBIconDefinition() {
} }
export function setupThumbnailListener(): void { export function setupThumbnailListener(): void {
setThumbnailListener(labelThumbnails, () => { setThumbnailListener(handleThumbnails, () => {
insertSBIconDefinition(); insertSBIconDefinition();
}, () => Config.isReady()); }, () => Config.isReady());
} }