Add functions for importing/exporting segments

This commit is contained in:
Ajay
2022-02-26 01:07:29 -05:00
parent eb35f5c543
commit f81cfbecfe
11 changed files with 382 additions and 70 deletions

View File

@@ -8,6 +8,7 @@ import { Registration } from "./types";
window.SB = Config;
import Utils from "./utils";
import { GenericUtils } from "./utils/genericUtils";
const utils = new Utils({
registerFirefoxContentScript,
unregisterFirefoxContentScript
@@ -77,7 +78,7 @@ chrome.runtime.onInstalled.addListener(function () {
chrome.tabs.create({url: chrome.extension.getURL("/help/index.html")});
//generate a userID
const newUserID = utils.generateUserID();
const newUserID = GenericUtils.generateUserID();
//save this UUID
Config.config.userID = newUserID;
@@ -120,7 +121,7 @@ async function submitVote(type: number, UUID: string, category: string) {
if (userID == undefined || userID === "undefined") {
//generate one
userID = utils.generateUserID();
userID = GenericUtils.generateUserID();
Config.config.userID = userID;
}

View File

@@ -13,6 +13,7 @@ import ThumbsUpSvg from "../svg-icons/thumbs_up_svg";
import ThumbsDownSvg from "../svg-icons/thumbs_down_svg";
import PencilSvg from "../svg-icons/pencil_svg";
import { downvoteButtonColor, SkipNoticeAction } from "../utils/noticeUtils";
import { GenericUtils } from "../utils/genericUtils";
export interface SkipNoticeProps {
segments: SponsorTime[];
@@ -511,7 +512,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
const sponsorVideoID = this.props.contentContainer().sponsorVideoID;
const sponsorTimesSubmitting : SponsorTime = {
segment: this.segments[index].segment,
UUID: utils.generateUserID() as SegmentUUID,
UUID: GenericUtils.generateUserID() as SegmentUUID,
category: this.segments[index].category,
actionType: this.segments[index].actionType,
source: SponsorSourceType.Local

View File

@@ -180,9 +180,9 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
style={timeDisplayStyle}
className="sponsorTimeDisplay"
onClick={this.toggleEditTime.bind(this)}>
{utils.getFormattedTime(segment[0], true) +
{GenericUtils.getFormattedTime(segment[0], true) +
((!isNaN(segment[1]) && sponsorTime.actionType !== ActionType.Poi)
? " " + chrome.i18n.getMessage("to") + " " + utils.getFormattedTime(segment[1], true) : "")}
? " " + chrome.i18n.getMessage("to") + " " + GenericUtils.getFormattedTime(segment[1], true) : "")}
</div>
);
}
@@ -324,7 +324,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
timeAsNumber = 0;
}
sponsorTimeEdits[index] = utils.getFormattedTime(timeAsNumber, true);
sponsorTimeEdits[index] = GenericUtils.getFormattedTime(timeAsNumber, true);
if (sponsorTime.actionType === ActionType.Poi) sponsorTimeEdits[1] = sponsorTimeEdits[0];
this.setState({sponsorTimeEdits});
@@ -523,8 +523,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
/** Returns an array in the sponsorTimeEdits form (formatted time string) from a normal seconds sponsor time */
getFormattedSponsorTimesEdits(sponsorTime: SponsorTime): [string, string] {
return [utils.getFormattedTime(sponsorTime.segment[0], true),
utils.getFormattedTime(sponsorTime.segment[1], true)];
return [GenericUtils.getFormattedTime(sponsorTime.segment[0], true),
GenericUtils.getFormattedTime(sponsorTime.segment[1], true)];
}
saveEditTimes(): void {

View File

@@ -1568,7 +1568,7 @@ function startOrEndTimingNewSegment() {
if (!isSegmentCreationInProgress()) {
sponsorTimesSubmitting.push({
segment: [roundedTime],
UUID: utils.generateUserID() as SegmentUUID,
UUID: GenericUtils.generateUserID() as SegmentUUID,
category: Config.config.defaultCategory,
actionType: ActionType.Skip,
source: SponsorSourceType.Local
@@ -2000,7 +2000,7 @@ function getSegmentsMessage(sponsorTimes: SponsorTime[]): string {
for (let i = 0; i < sponsorTimes.length; i++) {
for (let s = 0; s < sponsorTimes[i].segment.length; s++) {
let timeMessage = utils.getFormattedTime(sponsorTimes[i].segment[s]);
let timeMessage = GenericUtils.getFormattedTime(sponsorTimes[i].segment[s]);
//if this is an end time
if (s == 1) {
timeMessage = " " + chrome.i18n.getMessage("to") + " " + timeMessage;
@@ -2147,7 +2147,7 @@ function showTimeWithoutSkips(skippedDuration: number): void {
display.appendChild(duration);
}
const durationAfterSkips = utils.getFormattedTime(video?.duration - skippedDuration)
const durationAfterSkips = GenericUtils.getFormattedTime(video?.duration - skippedDuration)
duration.innerText = (durationAfterSkips == null || skippedDuration <= 0) ? "" : " (" + durationAfterSkips + ")";
}
@@ -2166,7 +2166,7 @@ function checkForPreloadedSegment() {
if (!sponsorTimesSubmitting.some((s) => s.segment[0] === segment.segment[0] && s.segment[1] === s.segment[1])) {
sponsorTimesSubmitting.push({
segment: segment.segment,
UUID: utils.generateUserID() as SegmentUUID,
UUID: GenericUtils.generateUserID() as SegmentUUID,
category: segment.category ? segment.category : Config.config.defaultCategory,
actionType: segment.actionType ? segment.actionType : ActionType.Skip,
source: SponsorSourceType.Local

View File

@@ -9,6 +9,7 @@ import Config from "../config";
import { ActionType, Category, SegmentContainer, SponsorSourceType, SponsorTime } from "../types";
import Utils from "../utils";
import { partition } from "../utils/arrayUtils";
import { shortCategoryName } from "../utils/categoryUtils";
import { GenericUtils } from "../utils/genericUtils";
const utils = new Utils();
@@ -154,7 +155,7 @@ class PreviewBar {
private setTooltipTitle(segment: PreviewBarSegment, tooltip: HTMLElement): void {
if (segment) {
const name = segment.description || utils.shortCategoryName(segment.category);
const name = segment.description || shortCategoryName(segment.category);
if (segment.unsubmitted) {
tooltip.textContent = chrome.i18n.getMessage("unsubmitted") + " " + name;
} else {
@@ -603,7 +604,7 @@ class PreviewBar {
chapterButton.disabled = false;
const chapterTitle = chaptersContainer.querySelector(".ytp-chapter-title-content") as HTMLDivElement;
chapterTitle.innerText = chosenSegment.description || utils.shortCategoryName(chosenSegment.category);
chapterTitle.innerText = chosenSegment.description || shortCategoryName(chosenSegment.category);
} else {
// Hide chapters menu again
chaptersContainer.style.display = "none";

View File

@@ -6,6 +6,7 @@ import { Message, MessageResponse, IsInfoFoundMessageResponse } from "./messageT
import { showDonationLink } from "./utils/configUtils";
import { AnimationUtils } from "./utils/animationUtils";
import { GenericUtils } from "./utils/genericUtils";
import { shortCategoryName } from "./utils/categoryUtils";
const utils = new Utils();
interface MessageListener {
@@ -474,15 +475,15 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
extraInfo = " (" + chrome.i18n.getMessage("manuallyHidden") + ")";
}
const name = segmentTimes[i].description || utils.shortCategoryName(category);
const name = segmentTimes[i].description || shortCategoryName(category);
const textNode = document.createTextNode(name + extraInfo);
const segmentTimeFromToNode = document.createElement("div");
if (segmentTimes[i].actionType === ActionType.Full) {
segmentTimeFromToNode.innerText = chrome.i18n.getMessage("full");
} else {
segmentTimeFromToNode.innerText = utils.getFormattedTime(segmentTimes[i].segment[0], true) +
segmentTimeFromToNode.innerText = GenericUtils.getFormattedTime(segmentTimes[i].segment[0], true) +
(actionType !== ActionType.Poi
? " " + chrome.i18n.getMessage("to") + " " + utils.getFormattedTime(segmentTimes[i].segment[1], true)
? " " + chrome.i18n.getMessage("to") + " " + GenericUtils.getFormattedTime(segmentTimes[i].segment[1], true)
: "");
}

View File

@@ -298,24 +298,6 @@ export default class Utils {
return permissionRegex;
}
generateUserID(length = 36): string {
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let result = "";
if (window.crypto && window.crypto.getRandomValues) {
const values = new Uint32Array(length);
window.crypto.getRandomValues(values);
for (let i = 0; i < length; i++) {
result += charset[values[i] % charset.length];
}
return result;
} else {
for (let i = 0; i < length; i++) {
result += charset[Math.floor(Math.random() * charset.length)];
}
return result;
}
}
/**
* Sends a request to a custom server
*
@@ -412,40 +394,6 @@ export default class Utils {
return url;
}
getFormattedTime(seconds: number, precise?: boolean): string {
seconds = Math.max(seconds, 0);
const hours = Math.floor(seconds / 60 / 60);
const minutes = Math.floor(seconds / 60) % 60;
let minutesDisplay = String(minutes);
let secondsNum = seconds % 60;
if (!precise) {
secondsNum = Math.floor(secondsNum);
}
let secondsDisplay = String(precise ? secondsNum.toFixed(3) : secondsNum);
if (secondsNum < 10) {
//add a zero
secondsDisplay = "0" + secondsDisplay;
}
if (hours && minutes < 10) {
//add a zero
minutesDisplay = "0" + minutesDisplay;
}
if (isNaN(hours) || isNaN(minutes)) {
return null;
}
const formatted = (hours ? hours + ":" : "") + minutesDisplay + ":" + secondsDisplay;
return formatted;
}
shortCategoryName(categoryName: string): string {
return chrome.i18n.getMessage("category_" + categoryName + "_short") || chrome.i18n.getMessage("category_" + categoryName);
}
isContentScript(): boolean {
return window.location.protocol === "http:" || window.location.protocol === "https:";
}

View File

@@ -45,3 +45,7 @@ export function getCategorySuffix(category: Category): string {
return "";
}
}
export function shortCategoryName(categoryName: string): string {
return chrome.i18n.getMessage("category_" + categoryName + "_short") || chrome.i18n.getMessage("category_" + categoryName);
}

65
src/utils/exporter.ts Normal file
View File

@@ -0,0 +1,65 @@
import { ActionType, Category, SegmentUUID, SponsorSourceType, SponsorTime } from "../types";
import { shortCategoryName } from "./categoryUtils";
import { GenericUtils } from "./genericUtils";
export function exportTimes(segments: SponsorTime[]): string {
let result = "";
for (const segment of segments) {
if (![ActionType.Full, ActionType.Mute].includes(segment.actionType)
&& segment.source !== SponsorSourceType.YouTube) {
result += exportTime(segment) + "\n";
}
}
return result;
}
function exportTime(segment: SponsorTime): string {
const name = segment.description || shortCategoryName(segment.category);
return `${GenericUtils.getFormattedTime(segment.segment[0])}${
segment.segment[1] && segment.segment[0] !== segment.segment[1]
? ` - ${GenericUtils.getFormattedTime(segment.segment[1])}` : ""} ${name}`;
}
export function importTimes(data: string, videoDuration: number): SponsorTime[] {
const lines = data.split("\n");
const result: SponsorTime[] = [];
for (const line of lines) {
const match = line.match(/(?:(\d+:\d+)+(?:\.\d+)?)|(?:\d+(?=s| second))/g);
if (match) {
const startTime = GenericUtils.getFormattedTimeToSeconds(match[0]);
if (startTime) {
const specialCharsMatcher = /^(?:\s+seconds?)?[:()-\s]*|(?:\s+at)?[:()-\s]+$/g
const titleLeft = line.split(match[0])[0].replace(specialCharsMatcher, "");
let titleRight = null;
const split2 = line.split(match[1] || match[0]);
titleRight = split2[split2.length - 1].replace(specialCharsMatcher, "");
const title = titleLeft?.length > titleRight?.length ? titleLeft : titleRight;
if (title) {
const segment: SponsorTime = {
segment: [startTime, GenericUtils.getFormattedTimeToSeconds(match[1])],
category: "chapter" as Category,
actionType: ActionType.Chapter,
description: title,
source: SponsorSourceType.Local,
UUID: GenericUtils.generateUserID() as SegmentUUID
};
if (result.length > 0 && result[result.length - 1].segment[1] === null) {
result[result.length - 1].segment[1] = segment.segment[0];
}
result.push(segment);
}
}
}
}
if (result.length > 0 && result[result.length - 1].segment[1] === null) {
result[result.length - 1].segment[1] = videoDuration;
}
return result;
}

View File

@@ -35,6 +35,36 @@ function getFormattedTimeToSeconds(formatted: string): number | null {
return hours * 3600 + minutes * 60 + seconds;
}
function getFormattedTime(seconds: number, precise?: boolean): string {
seconds = Math.max(seconds, 0);
const hours = Math.floor(seconds / 60 / 60);
const minutes = Math.floor(seconds / 60) % 60;
let minutesDisplay = String(minutes);
let secondsNum = seconds % 60;
if (!precise) {
secondsNum = Math.floor(secondsNum);
}
let secondsDisplay = String(precise ? secondsNum.toFixed(3) : secondsNum);
if (secondsNum < 10) {
//add a zero
secondsDisplay = "0" + secondsDisplay;
}
if (hours && minutes < 10) {
//add a zero
minutesDisplay = "0" + minutesDisplay;
}
if (isNaN(hours) || isNaN(minutes)) {
return null;
}
const formatted = (hours ? hours + ":" : "") + minutesDisplay + ":" + secondsDisplay;
return formatted;
}
/**
* Gets the error message in a nice string
*
@@ -80,9 +110,29 @@ function hexToRgb(hex: string): {r: number, g: number, b: number} {
} : null;
}
function generateUserID(length = 36): string {
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let result = "";
if (window.crypto && window.crypto.getRandomValues) {
const values = new Uint32Array(length);
window.crypto.getRandomValues(values);
for (let i = 0; i < length; i++) {
result += charset[values[i] % charset.length];
}
return result;
} else {
for (let i = 0; i < length; i++) {
result += charset[Math.floor(Math.random() * charset.length)];
}
return result;
}
}
export const GenericUtils = {
wait,
getFormattedTime,
getFormattedTimeToSeconds,
getErrorMessage,
getLuminance
getLuminance,
generateUserID
}

241
test/extractor.test.ts Normal file
View File

@@ -0,0 +1,241 @@
/**
* @jest-environment jsdom
*/
import { ActionType, Category, SegmentUUID, SponsorSourceType, SponsorTime } from "../src/types";
import { exportTimes, importTimes } from "../src/utils/exporter";
describe("Export segments", () => {
it("Some segments", () => {
const segments: SponsorTime[] = [{
segment: [0, 10],
category: "chapter" as Category,
actionType: ActionType.Chapter,
description: "Chapter 1",
source: SponsorSourceType.Server,
UUID: "1" as SegmentUUID
}, {
segment: [20, 20],
category: "poi_highlight" as Category,
actionType: ActionType.Poi,
description: "Highlight",
source: SponsorSourceType.Server,
UUID: "2" as SegmentUUID
}, {
segment: [30, 40],
category: "sponsor" as Category,
actionType: ActionType.Skip,
description: "Sponsor", // Force a description since chrome is not defined
source: SponsorSourceType.Server,
UUID: "3" as SegmentUUID
}, {
segment: [50, 60],
category: "selfpromo" as Category,
actionType: ActionType.Mute,
description: "Selfpromo",
source: SponsorSourceType.Server,
UUID: "4" as SegmentUUID
}, {
segment: [0, 0],
category: "selfpromo" as Category,
actionType: ActionType.Full,
description: "Selfpromo",
source: SponsorSourceType.Server,
UUID: "5" as SegmentUUID
}, {
segment: [80, 90],
category: "interaction" as Category,
actionType: ActionType.Skip,
description: "Interaction",
source: SponsorSourceType.YouTube,
UUID: "6" as SegmentUUID
}];
const result = exportTimes(segments);
expect(result).toBe(
"0:00 - 0:10 Chapter 1\n" +
"0:20 Highlight\n" +
"0:30 - 0:40 Sponsor\n"
);
});
});
describe("Import segments", () => {
it("1:20 to 1:21 thing", () => {
const input = ` 1:20 to 1:21 thing
1:25 to 1:28 another thing`;
const result = importTimes(input, 120);
expect(result).toMatchObject([{
segment: [80, 81],
description: "thing",
category: "chapter" as Category
}, {
segment: [85, 88],
description: "another thing",
category: "chapter" as Category
}]);
});
it("thing 1:20 to 1:21", () => {
const input = ` thing 1:20 to 1:21
another thing 1:25 to 1:28 ext`;
const result = importTimes(input, 120);
expect(result).toMatchObject([{
segment: [80, 81],
description: "thing",
category: "chapter" as Category
}, {
segment: [85, 88],
description: "another thing",
category: "chapter" as Category
}]);
});
it("1:20 - 1:21 thing", () => {
const input = ` 1:20 - 1:21 thing
1:25 - 1:28 another thing`;
const result = importTimes(input, 120);
expect(result).toMatchObject([{
segment: [80, 81],
description: "thing",
category: "chapter" as Category
}, {
segment: [85, 88],
description: "another thing",
category: "chapter" as Category
}]);
});
it("1:20 1:21 thing", () => {
const input = ` 1:20 1:21 thing
1:25 1:28 another thing`;
const result = importTimes(input, 120);
expect(result).toMatchObject([{
segment: [80, 81],
description: "thing",
category: "chapter" as Category
}, {
segment: [85, 88],
description: "another thing",
category: "chapter" as Category
}]);
});
it("1:20 thing", () => {
const input = ` 1:20 thing
1:25 another thing`;
const result = importTimes(input, 120);
expect(result).toMatchObject([{
segment: [80, 85],
description: "thing",
category: "chapter" as Category
}, {
segment: [85, 120],
description: "another thing",
category: "chapter" as Category
}]);
});
it("1:20: thing", () => {
const input = ` 1:20: thing
1:25: another thing`;
const result = importTimes(input, 120);
expect(result).toMatchObject([{
segment: [80, 85],
description: "thing",
category: "chapter" as Category
}, {
segment: [85, 120],
description: "another thing",
category: "chapter" as Category
}]);
});
it("1:20 (thing)", () => {
const input = ` 1:20 (thing)
1:25 (another thing)`;
const result = importTimes(input, 120);
expect(result).toMatchObject([{
segment: [80, 85],
description: "thing",
category: "chapter" as Category
}, {
segment: [85, 120],
description: "another thing",
category: "chapter" as Category
}]);
});
it("thing 1:20", () => {
const input = ` thing 1:20
another thing 1:25`;
const result = importTimes(input, 120);
expect(result).toMatchObject([{
segment: [80, 85],
description: "thing",
category: "chapter" as Category
}, {
segment: [85, 120],
description: "another thing",
category: "chapter" as Category
}]);
});
it("thing at 1:20", () => {
const input = ` thing at 1:20
another thing at 1:25`;
const result = importTimes(input, 120);
expect(result).toMatchObject([{
segment: [80, 85],
description: "thing",
category: "chapter" as Category
}, {
segment: [85, 120],
description: "another thing",
category: "chapter" as Category
}]);
});
it("thing at 1s", () => {
const input = ` thing at 1s
another thing at 5s`;
const result = importTimes(input, 120);
expect(result).toMatchObject([{
segment: [1, 5],
description: "thing",
category: "chapter" as Category
}, {
segment: [5, 120],
description: "another thing",
category: "chapter" as Category
}]);
});
it("thing at 1 second", () => {
const input = ` thing at 1 second
another thing at 5 seconds`;
const result = importTimes(input, 120);
expect(result).toMatchObject([{
segment: [1, 5],
description: "thing",
category: "chapter" as Category
}, {
segment: [5, 120],
description: "another thing",
category: "chapter" as Category
}]);
});
});