aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAjay Ramachandran <[email protected]>2020-12-29 22:51:50 -0500
committerGitHub <[email protected]>2020-12-29 22:51:50 -0500
commit06f09f5fd96c4273ea3eb22e146dd900a7a5bb8d (patch)
tree54806c1dd74b8383ad09f17f5080184da8fcb3a4
parent6d1c51e7ec13c7cae4fd76c0861fe7f8ae5f6c53 (diff)
parent83a9526e5263403822e5f31a6dbe2a174123c859 (diff)
downloadSponsorBlock-06f09f5fd96c4273ea3eb22e146dd900a7a5bb8d.tar.gz
SponsorBlock-06f09f5fd96c4273ea3eb22e146dd900a7a5bb8d.zip
Merge pull request #549 from opl-/feat/preview-bar-cleanup
Clean up Preview Bar
-rw-r--r--public/content.css30
-rw-r--r--src/content.ts114
-rw-r--r--src/js-components/previewBar.ts389
-rw-r--r--src/utils.ts55
4 files changed, 338 insertions, 250 deletions
diff --git a/public/content.css b/public/content.css
index 62420f2c..a9a6c6eb 100644
--- a/public/content.css
+++ b/public/content.css
@@ -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 !important;
+}
+
+.ytp-tooltip.sponsorCategoryTooltipVisible {
+ transform: translateY(-1em) !important;
+}
+
+.ytp-big-mode .ytp-tooltip.sponsorCategoryTooltipVisible {
+ transform: translateY(-2em) !important;
+}
+
+#movie_player:not(.ytp-big-mode) .ytp-tooltip.sponsorCategoryTooltipVisible > .ytp-tooltip-text-wrapper {
+ transform: translateY(1em) !important;
+}
+
+.ytp-big-mode .ytp-tooltip.sponsorCategoryTooltipVisible > .ytp-tooltip-text-wrapper {
+ transform: translateY(0.5em) !important;
}
-.sbTooltipOneTitleThumbnailOffset {
- bottom: 10px !important;
+.ytp-big-mode .ytp-tooltip.sponsorCategoryTooltipVisible > .ytp-tooltip-text-wrapper > .ytp-tooltip-text {
+ display: block !important;
+ transform: translateY(1em) !important;
}
/* */
diff --git a/src/content.ts b/src/content.ts
index 91295952..aba53f24 100644
--- a/src/content.ts
+++ b/src/content.ts
@@ -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";
@@ -234,7 +234,7 @@ function resetValues() {
//empty the preview bar
if (previewBar !== null) {
- previewBar.set([], [], 0);
+ previewBar.clear();
}
//reset sponsor data found check
@@ -358,11 +358,13 @@ async function videoIDChange(id) {
}
function handleMobileControlsMutations(): void {
- const mobileYouTubeSelector = ".progress-bar-background";
-
if (previewBar !== null) {
if (document.body.contains(previewBar.container)) {
- updatePreviewBarPositionMobile(document.getElementsByClassName(mobileYouTubeSelector)[0] as HTMLElement);
+ const progressBarBackground = document.querySelector<HTMLElement>(".progress-bar-background");
+
+ if (progressBarBackground !== null) {
+ updatePreviewBarPositionMobile(progressBarBackground);
+ }
return;
} else {
@@ -393,11 +395,11 @@ function createPreviewBar(): void {
];
for (const selector of progressElementSelectors) {
- const el = document.querySelectorAll(selector);
+ const el = document.querySelector<HTMLElement>(selector);
+
+ if (el) {
+ previewBar = new PreviewBar(el, onMobileYouTube, onInvidious);
- if (el && el.length && el[0]) {
- previewBar = new PreviewBar(el[0] as HTMLElement, onMobileYouTube, onInvidious);
-
updatePreviewBar();
break;
@@ -812,39 +814,46 @@ function updatePreviewBarPositionMobile(parent: HTMLElement) {
}
function updatePreviewBar(): void {
- if(isAdPlaying) {
- previewBar.set([], [], 0);
+ if (previewBar === null) return;
+
+ if (isAdPlaying) {
+ 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);
-
- //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);
+ if (sponsorTimes) {
+ sponsorTimes.forEach((segment) => {
+ if (segment.hidden !== SponsorHideType.Visible) return;
+
+ previewBarSegments.push({
+ segment: segment.segment as [number, number],
+ category: segment.category,
+ preview: false,
+ });
+ });
}
- previewBar.set(utils.getSegmentsFromSponsorTimes(allSponsorTimes), types, video.duration)
+ sponsorTimesSubmitting.forEach((segment) => {
+ previewBarSegments.push({
+ segment: segment.segment as [number, number],
+ category: segment.category,
+ preview: true,
+ });
+ });
+
+ previewBar.set(previewBarSegments, video.duration)
if (Config.config.showTimeWithSkips) {
- showTimeWithoutSkips(allSponsorTimes);
+ const skippedDuration = utils.getTimestampsDuration(previewBarSegments.map(({segment}) => segment));
+
+ showTimeWithoutSkips(skippedDuration);
}
- //update last video id
+ // Update last video id
lastPreviewBarUpdate = sponsorVideoID;
}
@@ -1614,37 +1623,28 @@ function updateAdFlag(): void {
}
}
-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];
- }
-
- }
-
- // YouTube player time display
- const display = document.getElementsByClassName("ytp-time-display notranslate")[0];
- if (!display) return;
-
- const formatedTime = utils.getFormattedTime(video.duration - skipDuration);
-
- const durationID = "sponsorBlockDurationAfterSkips";
+ if (isNaN(skippedDuration) || skippedDuration < 0) {
+ skippedDuration = 0;
+ }
+
+ // YouTube player time display
+ const display = document.querySelector(".ytp-time-display.notranslate");
+ if (!display) return;
+
+ const durationID = "sponsorBlockDurationAfterSkips";
let duration = document.getElementById(durationID);
- // Create span if needed
- if(duration === null) {
- duration = document.createElement('span');
+ // Create span if needed
+ if (duration === null) {
+ duration = document.createElement('span');
duration.id = durationID;
duration.classList.add("ytp-time-duration");
- display.appendChild(duration);
- }
-
- duration.innerText = (skipDuration <= 0 || isNaN(skipDuration) || formatedTime.includes("NaN")) ? "" : " ("+formatedTime+")";
+ display.appendChild(duration);
+ }
+
+ duration.innerText = skippedDuration <= 0 ? "" : " (" + utils.getFormattedTime(video.duration - skippedDuration) + ")";
}
diff --git a/src/js-components/previewBar.ts b/src/js-components/previewBar.ts
index a1f4e2c3..7f011f0d 100644
--- a/src/js-components/previewBar.ts
+++ b/src/js-components/previewBar.ts
@@ -1,6 +1,6 @@
/*
- This is based on code from VideoSegments.
- https://github.com/videosegments/videosegments/commits/f1e111bdfe231947800c6efdd51f62a4e7fef4d4/segmentsbar/segmentsbar.js
+This is based on code from VideoSegments.
+https://github.com/videosegments/videosegments/commits/f1e111bdfe231947800c6efdd51f62a4e7fef4d4/segmentsbar/segmentsbar.js
*/
'use strict';
@@ -9,179 +9,218 @@ import Config from "../config";
import Utils from "../utils";
const utils = new Utils();
+const TOOLTIP_VISIBLE_CLASS = 'sponsorCategoryTooltipVisible';
+
+export interface PreviewBarSegment {
+ segment: [number, number];
+ category: string;
+ preview: boolean;
+}
+
class PreviewBar {
- container: HTMLUListElement;
- parent: HTMLElement;
- onMobileYouTube: boolean;
- onInvidious: boolean;
-
- timestamps: number[][];
- types: string[];
-
- constructor(parent: HTMLElement, onMobileYouTube: boolean, onInvidious: boolean) {
- this.container = document.createElement('ul');
- this.container.id = 'previewbar';
- this.parent = parent;
-
- this.onMobileYouTube = onMobileYouTube;
- this.onInvidious = onInvidious;
-
- this.updatePosition(parent);
-
- this.setupHoverText();
- }
-
- 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"
-
- tooltipTextWrapper.insertBefore(categoryTooltip, titleTooltip.nextSibling);
-
- let mouseOnSeekBar = false;
-
- seekBar.addEventListener("mouseenter", () => {
- mouseOnSeekBar = true;
- });
-
- seekBar.addEventListener("mouseleave", () => {
- mouseOnSeekBar = false;
- categoryTooltip.classList.add("sbHidden");
- });
-
- const observer = new MutationObserver((mutations) => {
- if (!mouseOnSeekBar) 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") {
- 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]);
-
- // 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];
- }
- }
-
- 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]));
-
- // 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");
- }
- }
-
- break;
- }
- }
- });
-
- observer.observe(tooltipTextWrapper, {
- childList: true,
- subtree: true
- });
- }
-
- updatePosition(parent: HTMLElement): void {
- //below the seek bar
- // this.parent.insertAdjacentElement("afterEnd", this.container);
-
- this.parent = parent;
-
- if (this.onMobileYouTube) {
- parent.style.backgroundColor = "rgba(255, 255, 255, 0.3)";
- parent.style.opacity = "1";
-
- this.container.style.transform = "none";
- }
-
- //on the seek bar
- this.parent.insertAdjacentElement("afterbegin", this.container);
- }
-
- updateColor(segment: string, color: string, opacity: string): void {
- const bars = <NodeListOf<HTMLElement>> document.querySelectorAll('[data-vs-segment-type=' + segment + ']');
- for (const bar of bars) {
- bar.style.backgroundColor = color;
- bar.style.opacity = opacity;
- }
- }
-
- set(timestamps: number[][], types: string[], duration: number): void {
- while (this.container.firstChild) {
- this.container.removeChild(this.container.firstChild);
- }
-
- if (!timestamps || !types) {
- return;
- }
-
- this.timestamps = timestamps;
- this.types = types;
-
- // 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;
-
- width = (timestamps[i][1] - timestamps[i][0]) / duration * 100;
- width = Math.floor(width * 100) / 100;
-
- const bar = this.createBar();
- bar.setAttribute('data-vs-segment-type', types[i]);
-
- 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);
- }
- }
-
- createBar(): HTMLLIElement {
- const bar = document.createElement('li');
- bar.classList.add('previewbar');
- bar.innerHTML = '&nbsp;';
- return bar;
- }
-
- remove(): void {
- this.container.remove();
- this.container = undefined;
- }
+ container: HTMLUListElement;
+ categoryTooltip?: HTMLDivElement;
+ tooltipContainer?: HTMLElement;
+
+ parent: HTMLElement;
+ onMobileYouTube: boolean;
+ onInvidious: boolean;
+
+ segments: PreviewBarSegment[] = [];
+ videoDuration = 0;
+
+ constructor(parent: HTMLElement, onMobileYouTube: boolean, onInvidious: boolean) {
+ this.container = document.createElement('ul');
+ this.container.id = 'previewbar';
+
+ this.parent = parent;
+ this.onMobileYouTube = onMobileYouTube;
+ this.onInvidious = onInvidious;
+
+ this.updatePosition(parent);
+
+ this.setupHoverText();
+ }
+
+ setupHoverText(): void {
+ if (this.onMobileYouTube || this.onInvidious) return;
+
+ // Create label placeholder
+ this.categoryTooltip = document.createElement("div");
+ this.categoryTooltip.className = "ytp-tooltip-title sponsorCategoryTooltip";
+
+ 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", () => {
+ mouseOnSeekBar = true;
+ });
+
+ seekBar.addEventListener("mouseleave", () => {
+ mouseOnSeekBar = false;
+ });
+
+ const observer = new MutationObserver((mutations) => {
+ if (!mouseOnSeekBar || !this.categoryTooltip || !this.tooltipContainer) return;
+
+ // 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 tooltipTextElements = tooltipTextWrapper.querySelectorAll(".ytp-tooltip-text");
+ let timeInSeconds: number | null = null;
+ let noYoutubeChapters = false;
+
+ 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.segment[0] <= timeInSeconds && seg.segment[1] > timeInSeconds) {
+ const segmentLength = seg.segment[1] - seg.segment[0];
+
+ if (segmentLength < currentSegmentLength) {
+ currentSegmentLength = segmentLength;
+ segment = seg;
+ }
+ }
+ }
+
+ 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);
+
+ if (segment.preview) {
+ this.categoryTooltip.textContent = chrome.i18n.getMessage("preview") + " " + utils.shortCategoryName(segment.category);
+ } else {
+ this.categoryTooltip.textContent = utils.shortCategoryName(segment.category);
+ }
+
+ // 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,
+ });
+ }
+
+ updatePosition(parent: HTMLElement): void {
+ this.parent = parent;
+
+ if (this.onMobileYouTube) {
+ parent.style.backgroundColor = "rgba(255, 255, 255, 0.3)";
+ parent.style.opacity = "1";
+
+ this.container.style.transform = "none";
+ }
+
+ // On the seek bar
+ this.parent.prepend(this.container);
+ }
+
+ // 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 = String(opacity);
+ }
+ }
+
+ clear(): void {
+ this.videoDuration = 0;
+ this.segments = [];
+
+ while (this.container.firstChild) {
+ this.container.removeChild(this.container.firstChild);
+ }
+ }
+
+ set(segments: PreviewBarSegment[], videoDuration: number): void {
+ this.clear();
+
+ if (!segments) return;
+
+ this.segments = segments;
+ this.videoDuration = videoDuration;
+
+ this.segments.sort(({segment: a}, {segment: 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);
+
+ this.container.appendChild(bar);
+ });
+ }
+
+ createBar({category, preview, segment}: PreviewBarSegment): HTMLLIElement {
+ const bar = document.createElement('li');
+ bar.classList.add('previewbar');
+ bar.innerHTML = '&nbsp;';
+
+ 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(segment[1] - segment[0]);
+ bar.style.left = this.timeToPercentage(segment[0]);
+
+ return bar;
+ }
+
+ remove(): void {
+ this.container.remove();
+
+ 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) + '%';
+ }
}
-export default PreviewBar; \ No newline at end of file
+export default PreviewBar;
diff --git a/src/utils.ts b/src/utils.ts
index 2331b2d4..6d7af44d 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -154,17 +154,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]);
+ }
+
+ // <[]> Remove other segments contained within this range
+ deduped = deduped.filter((other) => !(other[0] > range[0] && other[1] < range[1]));
+ });
+
+ return deduped;
+ }
- return segments;
+ /**
+ * 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 {