mirror of
https://github.com/ajayyy/SponsorBlock.git
synced 2024-11-10 01:01:55 +01: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 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);
|
||||||
|
|
|
@ -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
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 { 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());
|
||||||
}
|
}
|
Loading…
Reference in a new issue