mirror of
https://github.com/ajayyy/SponsorBlock.git
synced 2025-12-08 20:47:11 +03:00
Clean up Preview Bar
Fixes: - Segments hidden by longer segments - Duration with skips not accounting for segment overlaps - Duration with skips not accounting for user's skip choices - Segment category text in preview tooltip overlaps the seek bar - Segment category text in preview tooltip breaks for timestamps over one hour - `previewBar.ts` lacks function argument and return types - Tooltip label not cleaned up on remove - General code style issues
This commit is contained in:
@@ -11,11 +11,6 @@
|
||||
z-index: 40;
|
||||
}
|
||||
|
||||
.sbHidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
.previewbar {
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
@@ -23,12 +18,29 @@
|
||||
|
||||
/* Preview Bar page hacks */
|
||||
|
||||
.sbTooltipTwoTitleThumbnailOffset {
|
||||
bottom: -5px !important;
|
||||
.ytp-tooltip:not(.sponsorCategoryTooltipVisible) .sponsorCategoryTooltip {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sbTooltipOneTitleThumbnailOffset {
|
||||
bottom: 10px !important;
|
||||
.ytp-tooltip.sponsorCategoryTooltipVisible {
|
||||
transform: translateY(-1em);
|
||||
}
|
||||
|
||||
.ytp-big-mode .ytp-tooltip.sponsorCategoryTooltipVisible {
|
||||
transform: translateY(-2em);
|
||||
}
|
||||
|
||||
#movie_player:not(.ytp-big-mode) .ytp-tooltip.sponsorCategoryTooltipVisible > .ytp-tooltip-text-wrapper {
|
||||
transform: translateY(1em);
|
||||
}
|
||||
|
||||
.ytp-big-mode .ytp-tooltip.sponsorCategoryTooltipVisible > .ytp-tooltip-text-wrapper {
|
||||
transform: translateY(0.5em);
|
||||
}
|
||||
|
||||
.ytp-big-mode .ytp-tooltip.sponsorCategoryTooltipVisible > .ytp-tooltip-text-wrapper > .ytp-tooltip-text {
|
||||
display: block;
|
||||
transform: translateY(1em);
|
||||
}
|
||||
|
||||
/* */
|
||||
|
||||
@@ -8,7 +8,7 @@ const utils = new Utils();
|
||||
|
||||
import runThePopup from "./popup";
|
||||
|
||||
import PreviewBar from "./js-components/previewBar";
|
||||
import PreviewBar, {PreviewBarSegment} from "./js-components/previewBar";
|
||||
import SkipNotice from "./render/SkipNotice";
|
||||
import SkipNoticeComponent from "./components/SkipNoticeComponent";
|
||||
import SubmissionNotice from "./render/SubmissionNotice";
|
||||
@@ -252,7 +252,7 @@ function resetValues() {
|
||||
|
||||
//empty the preview bar
|
||||
if (previewBar !== null) {
|
||||
previewBar.set([], [], 0);
|
||||
previewBar.clear();
|
||||
}
|
||||
|
||||
//reset sponsor data found check
|
||||
@@ -368,8 +368,6 @@ async function videoIDChange(id) {
|
||||
}
|
||||
|
||||
function handleMobileControlsMutations(): void {
|
||||
const mobileYouTubeSelector = ".progress-bar-background";
|
||||
|
||||
updateVisibilityOfPlayerControlsButton().then((createdButtons) => {
|
||||
if (createdButtons) {
|
||||
if (sponsorTimesSubmitting != null && sponsorTimesSubmitting.length > 0 && sponsorTimesSubmitting[sponsorTimesSubmitting.length - 1].segment.length >= 2) {
|
||||
@@ -384,7 +382,11 @@ function handleMobileControlsMutations(): void {
|
||||
|
||||
if (previewBar !== null) {
|
||||
if (document.body.contains(previewBar.container)) {
|
||||
updatePreviewBarPositionMobile(document.getElementsByClassName(mobileYouTubeSelector)[0]);
|
||||
const progressBarBackground = document.querySelector<HTMLElement>(".progress-bar-background");
|
||||
|
||||
if (progressBarBackground !== null) {
|
||||
updatePreviewBarPositionMobile(progressBarBackground);
|
||||
}
|
||||
|
||||
return;
|
||||
} else {
|
||||
@@ -415,10 +417,10 @@ function createPreviewBar(): void {
|
||||
];
|
||||
|
||||
for (const selector of progressElementSelectors) {
|
||||
const el = document.querySelectorAll(selector);
|
||||
const el = document.querySelector<HTMLElement>(selector);
|
||||
|
||||
if (el && el.length && el[0]) {
|
||||
previewBar = new PreviewBar(el[0], onMobileYouTube, onInvidious);
|
||||
if (el) {
|
||||
previewBar = new PreviewBar(el, onMobileYouTube, onInvidious);
|
||||
|
||||
updatePreviewBar();
|
||||
|
||||
@@ -819,46 +821,58 @@ function getYouTubeVideoID(url: string) {
|
||||
/**
|
||||
* This function is required on mobile YouTube and will keep getting called whenever the preview bar disapears
|
||||
*/
|
||||
function updatePreviewBarPositionMobile(parent: Element) {
|
||||
function updatePreviewBarPositionMobile(parent: HTMLElement) {
|
||||
if (document.getElementById("previewbar") === null) {
|
||||
previewBar.updatePosition(parent);
|
||||
}
|
||||
}
|
||||
|
||||
function updatePreviewBar() {
|
||||
if (previewBar === null) return;
|
||||
|
||||
if (isAdPlaying) {
|
||||
previewBar.set([], [], 0);
|
||||
previewBar.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (previewBar === null || video === null) return;
|
||||
if (video === null) return;
|
||||
|
||||
let localSponsorTimes = sponsorTimes;
|
||||
if (localSponsorTimes == null) localSponsorTimes = [];
|
||||
const previewBarSegments: PreviewBarSegment[] = [];
|
||||
|
||||
const allSponsorTimes = localSponsorTimes.concat(sponsorTimesSubmitting);
|
||||
if (sponsorTimes) {
|
||||
sponsorTimes.forEach((segment) => {
|
||||
if (segment.hidden !== SponsorHideType.Visible) return;
|
||||
|
||||
//create an array of the sponsor types
|
||||
const types = [];
|
||||
for (let i = 0; i < localSponsorTimes.length; i++) {
|
||||
if (localSponsorTimes[i].hidden === SponsorHideType.Visible) {
|
||||
types.push(localSponsorTimes[i].category);
|
||||
} else {
|
||||
// Don't show this sponsor
|
||||
types.push(null);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < sponsorTimesSubmitting.length; i++) {
|
||||
types.push("preview-" + sponsorTimesSubmitting[i].category);
|
||||
previewBarSegments.push({
|
||||
timestamps: segment.segment as [number, number],
|
||||
category: segment.category,
|
||||
preview: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
previewBar.set(utils.getSegmentsFromSponsorTimes(allSponsorTimes), types, video.duration)
|
||||
sponsorTimesSubmitting.forEach((segment) => {
|
||||
previewBarSegments.push({
|
||||
timestamps: segment.segment as [number, number],
|
||||
category: segment.category,
|
||||
preview: true,
|
||||
});
|
||||
});
|
||||
|
||||
previewBar.set(previewBarSegments, video.duration)
|
||||
|
||||
if (Config.config.showTimeWithSkips) {
|
||||
showTimeWithoutSkips(allSponsorTimes);
|
||||
const skippedSegments = previewBarSegments.filter((segment) => {
|
||||
// Count the segment only if the category is autoskipped
|
||||
return utils.getCategorySelection(segment.category)?.option === CategorySkipOption.AutoSkip;
|
||||
});
|
||||
|
||||
const skippedDuration = utils.getTimestampsDuration(skippedSegments.map(({timestamps}) => timestamps));
|
||||
|
||||
showTimeWithoutSkips(skippedDuration);
|
||||
}
|
||||
|
||||
//update last video id
|
||||
// Update last video id
|
||||
lastPreviewBarUpdate = sponsorVideoID;
|
||||
}
|
||||
|
||||
@@ -1626,26 +1640,17 @@ function updateAdFlag() {
|
||||
}
|
||||
}
|
||||
|
||||
function showTimeWithoutSkips(allSponsorTimes): void {
|
||||
function showTimeWithoutSkips(skippedDuration: number): void {
|
||||
if (onMobileYouTube || onInvidious) return;
|
||||
|
||||
let skipDuration = 0;
|
||||
|
||||
// Calculate skipDuration based from the segments in the preview bar
|
||||
for (let i = 0; i < allSponsorTimes.length; i++) {
|
||||
// If an end time exists
|
||||
if (allSponsorTimes[i].segment[1]) {
|
||||
skipDuration += allSponsorTimes[i].segment[1] - allSponsorTimes[i].segment[0];
|
||||
}
|
||||
|
||||
if (isNaN(skippedDuration) || skippedDuration < 0) {
|
||||
skippedDuration = 0;
|
||||
}
|
||||
|
||||
// YouTube player time display
|
||||
const display = document.getElementsByClassName("ytp-time-display notranslate")[0];
|
||||
const display = document.querySelector(".ytp-time-display.notranslate");
|
||||
if (!display) return;
|
||||
|
||||
const formatedTime = utils.getFormattedTime(video.duration - skipDuration);
|
||||
|
||||
const durationID = "sponsorBlockDurationAfterSkips";
|
||||
let duration = document.getElementById(durationID);
|
||||
|
||||
@@ -1658,5 +1663,5 @@ function showTimeWithoutSkips(allSponsorTimes): void {
|
||||
display.appendChild(duration);
|
||||
}
|
||||
|
||||
duration.innerText = (skipDuration <= 0 || isNaN(skipDuration) || formatedTime.includes("NaN")) ? "" : " ("+formatedTime+")";
|
||||
duration.innerText = skippedDuration <= 0 ? "" : " (" + utils.getFormattedTime(video.duration - skippedDuration) + ")";
|
||||
}
|
||||
|
||||
@@ -9,20 +9,31 @@ import Config from "../config";
|
||||
import Utils from "../utils";
|
||||
const utils = new Utils();
|
||||
|
||||
const TOOLTIP_VISIBLE_CLASS = 'sponsorCategoryTooltipVisible';
|
||||
|
||||
export interface PreviewBarSegment {
|
||||
timestamps: [number, number];
|
||||
category: string;
|
||||
preview: boolean;
|
||||
}
|
||||
|
||||
class PreviewBar {
|
||||
container: HTMLUListElement;
|
||||
parent: any;
|
||||
categoryTooltip?: HTMLDivElement;
|
||||
tooltipContainer?: HTMLElement;
|
||||
|
||||
parent: HTMLElement;
|
||||
onMobileYouTube: boolean;
|
||||
onInvidious: boolean;
|
||||
|
||||
timestamps: number[][];
|
||||
types: string[];
|
||||
segments: PreviewBarSegment[] = [];
|
||||
videoDuration = 0;
|
||||
|
||||
constructor(parent: any, onMobileYouTube: boolean, onInvidious: boolean) {
|
||||
constructor(parent: HTMLElement, onMobileYouTube: boolean, onInvidious: boolean) {
|
||||
this.container = document.createElement('ul');
|
||||
this.container.id = 'previewbar';
|
||||
this.parent = parent;
|
||||
|
||||
this.parent = parent;
|
||||
this.onMobileYouTube = onMobileYouTube;
|
||||
this.onInvidious = onInvidious;
|
||||
|
||||
@@ -34,88 +45,96 @@ class PreviewBar {
|
||||
setupHoverText(): void {
|
||||
if (this.onMobileYouTube || this.onInvidious) return;
|
||||
|
||||
const seekBar = document.querySelector(".ytp-progress-bar-container");
|
||||
|
||||
// Create label placeholder
|
||||
const tooltipTextWrapper = document.querySelector(".ytp-tooltip-text-wrapper");
|
||||
const titleTooltip = document.querySelector(".ytp-tooltip-title");
|
||||
const categoryTooltip = document.createElement("div");
|
||||
categoryTooltip.className = "sbHidden ytp-tooltip-title";
|
||||
categoryTooltip.id = "sponsor-block-category-tooltip"
|
||||
this.categoryTooltip = document.createElement("div");
|
||||
this.categoryTooltip.className = "ytp-tooltip-title sponsorCategoryTooltip";
|
||||
|
||||
tooltipTextWrapper.insertBefore(categoryTooltip, titleTooltip.nextSibling);
|
||||
const tooltipTextWrapper = document.querySelector(".ytp-tooltip-text-wrapper");
|
||||
if (!tooltipTextWrapper || !tooltipTextWrapper.parentElement) return;
|
||||
|
||||
// Grab the tooltip from the text wrapper as the tooltip doesn't have its classes on init
|
||||
this.tooltipContainer = tooltipTextWrapper.parentElement;
|
||||
const titleTooltip = tooltipTextWrapper.querySelector(".ytp-tooltip-title");
|
||||
if (!this.tooltipContainer || !titleTooltip) return;
|
||||
|
||||
tooltipTextWrapper.insertBefore(this.categoryTooltip, titleTooltip.nextSibling);
|
||||
|
||||
const seekBar = document.querySelector(".ytp-progress-bar-container");
|
||||
if (!seekBar) return;
|
||||
|
||||
let mouseOnSeekBar = false;
|
||||
|
||||
seekBar.addEventListener("mouseenter", (event) => {
|
||||
seekBar.addEventListener("mouseenter", () => {
|
||||
mouseOnSeekBar = true;
|
||||
});
|
||||
|
||||
seekBar.addEventListener("mouseleave", (event) => {
|
||||
seekBar.addEventListener("mouseleave", () => {
|
||||
mouseOnSeekBar = false;
|
||||
categoryTooltip.classList.add("sbHidden");
|
||||
});
|
||||
|
||||
const observer = new MutationObserver((mutations, observer) => {
|
||||
if (!mouseOnSeekBar) return;
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
if (!mouseOnSeekBar || !this.categoryTooltip || !this.tooltipContainer) return;
|
||||
|
||||
// See if mutation observed is only this ID (if so, ignore)
|
||||
if (mutations.length == 1 && (mutations[0].target as HTMLElement).id === "sponsor-block-category-tooltip") {
|
||||
// If the mutation observed is only for our tooltip text, ignore
|
||||
if (mutations.length === 1 && (mutations[0].target as HTMLElement).classList.contains("sponsorCategoryTooltip")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tooltips = document.querySelectorAll(".ytp-tooltip-text");
|
||||
for (const tooltip of tooltips) {
|
||||
const splitData = tooltip.textContent.split(":");
|
||||
if (splitData.length === 2 && !isNaN(parseInt(splitData[0])) && !isNaN(parseInt(splitData[1]))) {
|
||||
// Add label
|
||||
const timeInSeconds = parseInt(splitData[0]) * 60 + parseInt(splitData[1]);
|
||||
const tooltipTextElements = tooltipTextWrapper.querySelectorAll(".ytp-tooltip-text");
|
||||
let timeInSeconds: number | null = null;
|
||||
let noYoutubeChapters = false;
|
||||
|
||||
// Find category at that location
|
||||
let category = null;
|
||||
for (let i = 0; i < this.timestamps?.length; i++) {
|
||||
if (this.timestamps[i][0] < timeInSeconds && this.timestamps[i][1] > timeInSeconds){
|
||||
category = this.types[i];
|
||||
for (const tooltipTextElement of tooltipTextElements) {
|
||||
if (tooltipTextElement.classList.contains('ytp-tooltip-text-no-title')) noYoutubeChapters = true;
|
||||
|
||||
const tooltipText = tooltipTextElement.textContent;
|
||||
if (tooltipText === null || tooltipText.length === 0) continue;
|
||||
|
||||
timeInSeconds = utils.getFormattedTimeToSeconds(tooltipText);
|
||||
|
||||
if (timeInSeconds !== null) break;
|
||||
}
|
||||
|
||||
if (timeInSeconds === null) return;
|
||||
|
||||
// Find the segment at that location, using the shortest if multiple found
|
||||
let segment: PreviewBarSegment | null = null;
|
||||
let currentSegmentLength = Infinity;
|
||||
|
||||
for (const seg of this.segments) {
|
||||
if (seg.timestamps[0] <= timeInSeconds && seg.timestamps[1] > timeInSeconds) {
|
||||
const segmentLength = seg.timestamps[1] - seg.timestamps[0];
|
||||
|
||||
if (segmentLength < currentSegmentLength) {
|
||||
currentSegmentLength = segmentLength;
|
||||
segment = seg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (category === null && !categoryTooltip.classList.contains("sbHidden")) {
|
||||
categoryTooltip.classList.add("sbHidden");
|
||||
tooltipTextWrapper.classList.remove("sbTooltipTwoTitleThumbnailOffset");
|
||||
tooltipTextWrapper.classList.remove("sbTooltipOneTitleThumbnailOffset");
|
||||
} else if (category !== null) {
|
||||
categoryTooltip.classList.remove("sbHidden");
|
||||
categoryTooltip.textContent = utils.shortCategoryName(category)
|
||||
|| (chrome.i18n.getMessage("preview") + " " + utils.shortCategoryName(category.split("preview-")[1]));
|
||||
if (segment === null && this.tooltipContainer.classList.contains(TOOLTIP_VISIBLE_CLASS)) {
|
||||
this.tooltipContainer.classList.remove(TOOLTIP_VISIBLE_CLASS);
|
||||
} else if (segment !== null) {
|
||||
this.tooltipContainer.classList.add(TOOLTIP_VISIBLE_CLASS);
|
||||
|
||||
// There is a title now
|
||||
tooltip.classList.remove("ytp-tooltip-text-no-title");
|
||||
|
||||
// Add the correct offset for the number of titles there are
|
||||
if (titleTooltip.textContent !== "") {
|
||||
if (!tooltipTextWrapper.classList.contains("sbTooltipTwoTitleThumbnailOffset")) {
|
||||
tooltipTextWrapper.classList.add("sbTooltipTwoTitleThumbnailOffset");
|
||||
}
|
||||
} else if (!tooltipTextWrapper.classList.contains("sbTooltipOneTitleThumbnailOffset")) {
|
||||
tooltipTextWrapper.classList.add("sbTooltipOneTitleThumbnailOffset");
|
||||
}
|
||||
if (segment.preview) {
|
||||
this.categoryTooltip.textContent = chrome.i18n.getMessage("preview") + " " + utils.shortCategoryName(segment.category);
|
||||
} else {
|
||||
this.categoryTooltip.textContent = utils.shortCategoryName(segment.category);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
// Use the class if the timestamp text uses it to prevent overlapping
|
||||
this.categoryTooltip.classList.toggle("ytp-tooltip-text-no-title", noYoutubeChapters);
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(tooltipTextWrapper, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
subtree: true,
|
||||
});
|
||||
}
|
||||
|
||||
updatePosition(parent: any): void {
|
||||
//below the seek bar
|
||||
// this.parent.insertAdjacentElement("afterEnd", this.container);
|
||||
|
||||
updatePosition(parent: HTMLElement): void {
|
||||
this.parent = parent;
|
||||
|
||||
if (this.onMobileYouTube) {
|
||||
@@ -125,62 +144,82 @@ class PreviewBar {
|
||||
this.container.style.transform = "none";
|
||||
}
|
||||
|
||||
//on the seek bar
|
||||
this.parent.insertAdjacentElement("afterBegin", this.container);
|
||||
// On the seek bar
|
||||
this.parent.prepend(this.container);
|
||||
}
|
||||
|
||||
updateColor(segment: string, color: string, opacity: string): void {
|
||||
const bars = <NodeListOf<HTMLElement>> document.querySelectorAll('[data-vs-segment-type=' + segment + ']');
|
||||
// TODO: call on config changes
|
||||
updateColor(segmentType: string, color: string, opacity: number): void {
|
||||
const bars = <NodeListOf<HTMLElement>> document.querySelectorAll('[data-vs-segment-type=' + segmentType + ']');
|
||||
|
||||
for (const bar of bars) {
|
||||
bar.style.backgroundColor = color;
|
||||
bar.style.opacity = opacity;
|
||||
bar.style.opacity = String(opacity);
|
||||
}
|
||||
}
|
||||
|
||||
set(timestamps: number[][], types: string[], duration: number): void {
|
||||
clear(): void {
|
||||
this.videoDuration = 0;
|
||||
this.segments = [];
|
||||
|
||||
while (this.container.firstChild) {
|
||||
this.container.removeChild(this.container.firstChild);
|
||||
}
|
||||
|
||||
if (!timestamps || !types) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.timestamps = timestamps;
|
||||
this.types = types;
|
||||
set(segments: PreviewBarSegment[], videoDuration: number): void {
|
||||
this.clear();
|
||||
|
||||
// to avoid rounding error resulting in width more than 100%
|
||||
duration = Math.floor(duration * 100) / 100;
|
||||
let width;
|
||||
for (let i = 0; i < timestamps.length; i++) {
|
||||
if (types[i] == null) continue;
|
||||
if (!segments) return;
|
||||
|
||||
width = (timestamps[i][1] - timestamps[i][0]) / duration * 100;
|
||||
width = Math.floor(width * 100) / 100;
|
||||
this.segments = segments;
|
||||
this.videoDuration = videoDuration;
|
||||
|
||||
const bar = this.createBar();
|
||||
bar.setAttribute('data-vs-segment-type', types[i]);
|
||||
this.segments.sort(({timestamps: a}, {timestamps: b}) => {
|
||||
// Sort longer segments before short segments to make shorter segments render later
|
||||
return (b[1] - b[0]) - (a[1] - a[0]);
|
||||
}).forEach((segment) => {
|
||||
const bar = this.createBar(segment);
|
||||
|
||||
bar.style.backgroundColor = Config.config.barTypes[types[i]].color;
|
||||
if (!this.onMobileYouTube) bar.style.opacity = Config.config.barTypes[types[i]].opacity;
|
||||
bar.style.width = width + '%';
|
||||
bar.style.left = (timestamps[i][0] / duration * 100) + "%";
|
||||
bar.style.position = "absolute"
|
||||
|
||||
this.container.insertAdjacentElement("beforeend", bar);
|
||||
}
|
||||
this.container.appendChild(bar);
|
||||
});
|
||||
}
|
||||
|
||||
createBar(): HTMLLIElement {
|
||||
createBar({category, preview, timestamps}: PreviewBarSegment): HTMLLIElement {
|
||||
const bar = document.createElement('li');
|
||||
bar.classList.add('previewbar');
|
||||
bar.innerHTML = ' ';
|
||||
|
||||
const barSegmentType = (preview ? 'preview-' : '') + category;
|
||||
|
||||
bar.setAttribute('data-vs-segment-type', barSegmentType);
|
||||
|
||||
bar.style.backgroundColor = Config.config.barTypes[barSegmentType].color;
|
||||
if (!this.onMobileYouTube) bar.style.opacity = Config.config.barTypes[barSegmentType].opacity;
|
||||
|
||||
bar.style.position = "absolute";
|
||||
bar.style.width = this.timeToPercentage(timestamps[1] - timestamps[0]);
|
||||
bar.style.left = this.timeToPercentage(timestamps[0]);
|
||||
|
||||
return bar;
|
||||
}
|
||||
|
||||
remove(): void {
|
||||
this.container.remove();
|
||||
this.container = undefined;
|
||||
|
||||
if (this.categoryTooltip) {
|
||||
this.categoryTooltip.remove();
|
||||
this.categoryTooltip = undefined;
|
||||
}
|
||||
|
||||
if (this.tooltipContainer) {
|
||||
this.tooltipContainer.classList.remove(TOOLTIP_VISIBLE_CLASS);
|
||||
this.tooltipContainer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
timeToPercentage(time: number): string {
|
||||
return Math.min(100, time / this.videoDuration * 100) + '%';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
53
src/utils.ts
53
src/utils.ts
@@ -158,17 +158,54 @@ class Utils {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets just the timestamps from a sponsorTimes array
|
||||
*
|
||||
* @param sponsorTimes
|
||||
* Merges any overlapping timestamp ranges into single segments and returns them as a new array.
|
||||
*/
|
||||
getSegmentsFromSponsorTimes(sponsorTimes: SponsorTime[]): number[][] {
|
||||
const segments: number[][] = [];
|
||||
for (const sponsorTime of sponsorTimes) {
|
||||
segments.push(sponsorTime.segment);
|
||||
getMergedTimestamps(timestamps: number[][]): [number, number][] {
|
||||
let deduped: [number, number][] = [];
|
||||
|
||||
// Cases ([] = another segment, <> = current range):
|
||||
// [<]>, <[>], <[]>, [<>], [<][>]
|
||||
timestamps.forEach((range) => {
|
||||
// Find segments the current range overlaps
|
||||
const startOverlaps = deduped.findIndex((other) => range[0] >= other[0] && range[0] <= other[1]);
|
||||
const endOverlaps = deduped.findIndex((other) => range[1] >= other[0] && range[1] <= other[1]);
|
||||
|
||||
if (~startOverlaps && ~endOverlaps) {
|
||||
// [<][>] Both the start and end of this range overlap another segment
|
||||
// [<>] This range is already entirely contained within an existing segment
|
||||
if (startOverlaps === endOverlaps) return;
|
||||
|
||||
// Remove the range with the higher index first to avoid the index shifting
|
||||
const other1 = deduped.splice(Math.max(startOverlaps, endOverlaps), 1)[0];
|
||||
const other2 = deduped.splice(Math.min(startOverlaps, endOverlaps), 1)[0];
|
||||
|
||||
// Insert a new segment spanning the start and end of the range
|
||||
deduped.push([Math.min(other1[0], other2[0]), Math.max(other1[1], other2[1])]);
|
||||
} else if (~startOverlaps) {
|
||||
// [<]> The start of this range overlaps another segment, extend its end
|
||||
deduped[startOverlaps][1] = range[1];
|
||||
} else if (~endOverlaps) {
|
||||
// <[>] The end of this range overlaps another segment, extend its beginning
|
||||
deduped[endOverlaps][0] = range[0];
|
||||
} else {
|
||||
// No overlaps, just push in a copy
|
||||
deduped.push(range.slice() as [number, number]);
|
||||
}
|
||||
|
||||
return segments;
|
||||
// <[]> Remove other segments contained within this range
|
||||
deduped = deduped.filter((other) => !(other[0] > range[0] && other[1] < range[1]));
|
||||
});
|
||||
|
||||
return deduped;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total duration of the timestamps, taking into account overlaps.
|
||||
*/
|
||||
getTimestampsDuration(timestamps: number[][]): number {
|
||||
return this.getMergedTimestamps(timestamps).reduce((acc, range) => {
|
||||
return acc + range[1] - range[0];
|
||||
}, 0);
|
||||
}
|
||||
|
||||
getSponsorIndexFromUUID(sponsorTimes: SponsorTime[], UUID: string): number {
|
||||
|
||||
Reference in New Issue
Block a user