aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
authorAjay <[email protected]>2024-09-02 03:22:33 -0400
committerAjay <[email protected]>2024-09-02 03:22:33 -0400
commitdbf80b492902ecc55fc5b207ae5aa600be4a3bd0 (patch)
tree7145fa899079b3f1312c666268aadcccee99b52a /src
parente181c64775d7bec43901e3a88c107ba96a0d54ce (diff)
downloadSponsorBlock-dbf80b492902ecc55fc5b207ae5aa600be4a3bd0.tar.gz
SponsorBlock-dbf80b492902ecc55fc5b207ae5aa600be4a3bd0.zip
Add pre-fetching on hover with a small segment data cache
Diffstat (limited to 'src')
-rw-r--r--src/content.ts58
-rw-r--r--src/utils/requests.ts2
-rw-r--r--src/utils/segmentData.ts104
-rw-r--r--src/utils/thumbnails.ts50
4 files changed, 158 insertions, 56 deletions
diff --git a/src/content.ts b/src/content.ts
index c8cd4879..398102aa 100644
--- a/src/content.ts
+++ b/src/content.ts
@@ -1,7 +1,6 @@
import Config from "./config";
import {
ActionType,
- ActionTypes,
Category,
CategorySkipOption,
ChannelIDInfo,
@@ -18,7 +17,6 @@ import {
VideoInfo,
} from "./types";
import Utils from "./utils";
-import * as CompileConfig from "../config.json";
import PreviewBar, { PreviewBarSegment } from "./js-components/previewBar";
import SkipNotice from "./render/SkipNotice";
import SkipNoticeComponent from "./components/SkipNoticeComponent";
@@ -52,6 +50,7 @@ import { asyncRequestToServer } from "./utils/requests";
import { isMobileControlsOpen } from "./utils/mobileUtils";
import { defaultPreviewTime } from "./utils/constants";
import { onVideoPage } from "../maze-utils/src/pageInfo";
+import { getSegmentsForVideo } from "./utils/segmentData";
cleanPage();
@@ -269,7 +268,7 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
sendResponse({ hasVideo: getVideoID() != null });
// fetch segments
if (getVideoID()) {
- sponsorsLookup(false);
+ sponsorsLookup(false, true);
}
break;
@@ -364,7 +363,7 @@ function contentConfigUpdateListener(changes: StorageChangesObject) {
updateVisibilityOfPlayerControlsButton()
break;
case "categorySelections":
- sponsorsLookup();
+ sponsorsLookup(true, true);
break;
case "barTypes":
setCategoryColorCSSVariables();
@@ -1133,42 +1132,20 @@ function setupCategoryPill() {
categoryPill.attachToPage(isOnMobileYouTube(), isOnInvidious(), voteAsync);
}
-async function sponsorsLookup(keepOldSubmissions = true) {
- 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;
-
+async function sponsorsLookup(keepOldSubmissions = true, ignoreCache = false) {
const videoID = getVideoID()
if (!videoID) {
console.error("[SponsorBlock] Attempted to fetch segments with a null/undefined videoID.");
return;
}
- 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}`
- });
+
+ const segmentData = await getSegmentsForVideo(videoID, ignoreCache);
// store last response status
- lastResponseStatus = response?.status;
-
- 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]);
+ lastResponseStatus = segmentData.status;
+ if (segmentData.status === 200) {
+ const receivedSegments = segmentData.segments;
+
if (receivedSegments && receivedSegments.length) {
sponsorDataFound = true;
@@ -1208,6 +1185,7 @@ async function sponsorsLookup(keepOldSubmissions = true) {
}
// 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];
if (downvotedData) {
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> {
const hashPrefix = (await getHash(getVideoID(), 1)).slice(0, 4);
const response = await asyncRequestToServer("GET", "/api/lockCategories/" + hashPrefix);
@@ -2016,7 +1982,7 @@ function startOrEndTimingNewSegment() {
Config.forceLocalUpdate("unsubmittedSegments");
// Make sure they know if someone has already submitted something it while they were watching
- sponsorsLookup();
+ sponsorsLookup(true, true);
updateEditButtonsOnPlayer();
updateSponsorTimesSubmitting(false);
diff --git a/src/utils/requests.ts b/src/utils/requests.ts
index acbde374..8ce80601 100644
--- a/src/utils/requests.ts
+++ b/src/utils/requests.ts
@@ -23,8 +23,6 @@ export function asyncRequestToCustomServer(type: string, url: string, data = {},
export async function asyncRequestToServer(type: string, address: string, data = {}, headers = {}): Promise<FetchResponse> {
const serverAddress = Config.config.testingServer ? CompileConfig.testingServerAddress : Config.config.serverAddress;
- console.log(address, headers)
-
return await (asyncRequestToCustomServer(type, serverAddress + address, data, headers));
}
diff --git a/src/utils/segmentData.ts b/src/utils/segmentData.ts
new file mode 100644
index 00000000..1c2e631a
--- /dev/null
+++ b/src/utils/segmentData.ts
@@ -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;
+} \ No newline at end of file
diff --git a/src/utils/thumbnails.ts b/src/utils/thumbnails.ts
index 61d28f18..0fb2579e 100644
--- a/src/utils/thumbnails.ts
+++ b/src/utils/thumbnails.ts
@@ -1,10 +1,15 @@
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)));
+import { getThumbnailSelector, setThumbnailListener } from "../../maze-utils/src/thumbnailManagement";
+import { VideoID } from "../types";
+import { getSegmentsForVideo } from "./segmentData";
+
+export async function handleThumbnails(thumbnails: HTMLImageElement[]): Promise<void> {
+ await Promise.all(thumbnails.map((t) => {
+ labelThumbnail(t);
+ setupThumbnailHover(t);
+ }));
}
export async function labelThumbnail(thumbnail: HTMLImageElement): Promise<HTMLElement | null> {
@@ -13,9 +18,7 @@ export async function labelThumbnail(thumbnail: HTMLImageElement): Promise<HTMLE
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;
+ const videoID = extractVideoID(thumbnail);
if (!videoID) {
hideThumbnailLabel(thumbnail);
return null;
@@ -37,6 +40,37 @@ export async function labelThumbnail(thumbnail: HTMLImageElement): Promise<HTMLE
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 {
return thumbnail.querySelector(".sponsorThumbnailLabel") as HTMLElement | null;
}
@@ -109,7 +143,7 @@ function insertSBIconDefinition() {
}
export function setupThumbnailListener(): void {
- setThumbnailListener(labelThumbnails, () => {
+ setThumbnailListener(handleThumbnails, () => {
insertSBIconDefinition();
}, () => Config.isReady());
} \ No newline at end of file