mirror of
https://github.com/ajayyy/SponsorBlock.git
synced 2024-09-19 20:51:28 +02:00
Add pre-fetching on hover with a small segment data cache
This commit is contained in:
parent
e181c64775
commit
dbf80b4929
5 changed files with 157 additions and 55 deletions
|
@ -1 +1 @@
|
|||
Subproject commit ab431ec8ba764b4a7a07d2debc91b5903c65db5e
|
||||
Subproject commit e735dcabe8f378d49b64b04097dbdf6dc7525228
|
|
@ -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;
|
||||
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) {
|
||||
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);
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
104
src/utils/segmentData.ts
Normal file
104
src/utils/segmentData.ts
Normal 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;
|
||||
}
|
|
@ -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";
|
||||
import { getThumbnailSelector, setThumbnailListener } from "../../maze-utils/src/thumbnailManagement";
|
||||
import { VideoID } from "../types";
|
||||
import { getSegmentsForVideo } from "./segmentData";
|
||||
|
||||
export async function labelThumbnails(thumbnails: HTMLImageElement[]): Promise<void> {
|
||||
await Promise.all(thumbnails.map((t) => labelThumbnail(t)));
|
||||
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());
|
||||
}
|
Loading…
Reference in a new issue