const NamesToConvert = new Map();
NamesToConvert.set('VisualPolitik EN', "VisualPolitik");
const TitlesToTrim = new Map();
TitlesToTrim.set("VisualPolitik", [" | VisualPolitik EN"," | @VisualPolitikEN"," | <U+202A>@VisualPolitikEN<U+202C>", " <U+202A>@VisualPolitikEN<U+202C>", " - VisualPolitik EN"]);
const DescPatterns = ['\.com',
'\.org',
'\.gov',
'patreon',
'---',
'====',
'\.mp',
'bsky\.app',
'email list',
`utm_source`,
`utm_medium`,
'\.xyz',
'\.io',
'nocopyrightsounds',
'referral links',
'\.gg',
'sponsor of',
'\.is',
'bit\.ly',
'join this channel',
'free shipping',
'ground\.news',
'amzn\.to',
'twitch\.tv',
'PayPal',
'please subscribe',
'youtube membership',
'affiliate links',
'help this channel',
'licenses used:',
'bc1qnkl3nk0zt7w0xzrgur9pnkcduj7a3xxllcn7d4',
'0x60f088B10b03115405d313f964BeA93eF0Bd3DbF',
"Copyright Disclaimer"]
Node.prototype.getElementsByPartialClassName = function(partialName) {
return this.querySelectorAll(`[class*='${partialName}']`);
}
function getElementFromParent(resultPrefix, parentPrefix) {
const parent = document.getElementsByPartialClassName( parentPrefix)
return Array.from(parent)
.flatMap(desc => Array.from(desc.getElementsByPartialClassName(resultPrefix)))
}
String.prototype.removewhitespaces = function() {
// show(this.match("/\xA0/g"));
// show(this.match("/\n/g"));
// show(this.match("/\u202C/g"));
return this.replace(/\n/g, '').replace(/\xA0/g, " ").// \xA0 is ASCI 160 - no breaking space
replace(/\u202C/g, "").trim();
}
String.prototype.uncapitalize = function() {
return this.split(' ').map(word => {
if (word.isCapitalized()) {
return word.slice(0, 1).toUpperCase() + word.slice(1).toLowerCase();
} else {
return word;
}
}
).join(' ');
}
;
String.prototype.isCapitalized = function() {
var isCapitalized = false;
for (var c of this) {
if (c !== c.toUpperCase()) {
return false;
}
}
return true;
}
String.prototype.escape = function() {
return this.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
}
var hiddenUI = false;
function hideUI() {
const top = document.getElementsByClassName("playerFullscreenTitleOverlay")[0].style;
const bottom = document.getElementsByClassName("shaka-bottom-controls shaka-no-propagation")[0].style;
if (hiddenUI) {
top.display = "";
bottom.display = "";
} else {
top.display = "none";
bottom.display = "none";
}
hiddenUI = !hiddenUI;
}
class Chapters {
constructor(generate, timestamps) {
if (this.constructor === Chapters) {
throw new Error("Chapters is an abstract class and cannot be instantiated directly.");
}
this.generate = generate
this.timestamps = timestamps
this.data = [];
return this;
}
formatTimestamp(time, url, pos) {
const i = pos + 1;
const ts = this.getTimestamp(time.split(":").reverse());
return "[" + i + "](" + url + "?t=" + ts + ")"
}
getTimestamp(array) {
if (!(array instanceof Array)) {
throw new TypeError('Argument must be an instance of Array');
}
if (array.length > 2) {
throw new Error('Days are unsupported');
}
let seconds = 0;
for (let i = 0; i < array.length; i++) {
const val = parseInt(array[i]);
if (i == 0) {
seconds += val;
} else if (i == 1) {
seconds += val * 60;
} else {
seconds += val * 60 * 60;
}
}
return seconds;
}
}
class YoutubeChapters extends Chapters {
read(url) {
if (!this.generate)
return this.data
const container = document.querySelector("#content").getElementsByClassName("style-scope ytd-macro-markers-list-renderer")
if (container.length < 1)
return this.data
const chapters = container[0].getElementsByClassName("style-scope ytd-macro-markers-list-renderer")
for (let i = 0; i < chapters.length; i++) {
const data = chapters[i].children[1].textContent.trim().split("\n")
const desc = data[0].trim()
const time = data[2].trim()
if (this.timestamps) {
this.data[i] = desc + " " + this.formatTimestamp(time, url, i)
} else {
this.data[i] = desc
}
}
return this.data
}
}
class GrayJayChapters extends Chapters {
read(url) {
if (!this.generate)
return this.data
const descriptions = document.getElementsByPartialClassName("_progressBarChapter_")
for (let i = 0; i < descriptions.length; i++) {
const desc = descriptions[i].textContent
this.data[i] = desc
}
if (!this.timestamps)
return this.data
const timestamps = getElementFromParent("timestamp-link", "_description_")
for (let i = 0; i < timestamps.length; i++) {
const time = timestamps[i].textContent.trim()
this.data[i] += " " + this.formatTimestamp(time, url, i)
}
return this.data
}
}
class FreetubeChapters extends Chapters {
read(url) {
if (!this.generate)
return this.data
const htmlCol = document.getElementsByClassName("chapterTimestamp");
for (let i = 0; i < htmlCol.length; i++) {
const desc = htmlCol[i].nextElementSibling.outerText.removewhitespaces()
if (this.timestamps) {
const url = this.formatTimestamp(htmlCol[i].outerText.removewhitespaces(), url, i)
this.data[i] = desc + " " + url
} else {
this.data[i] = desc
}
}
return this.data
}
}
class Extractor {
constructor(chapters) {
if (this.constructor === Extractor) {
throw new Error("Extractor is an abstract class and cannot be instantiated directly.");
}
this.channel = this.getChannel().removewhitespaces()
this.title = this.getTitle()
this.url = this.getURL()
this.date = this.getPublishDate()
this.description = this.getDescription()
this.chapters = chapters.read(this.url)
}
getURL() {
// eg: file:///app/freetube/resources/app.asar/dist/index.html#/watch/HtgvZreimGI
return "https:youtu.be/" + document.URL.split("/").pop();
}
getChannel() {
throw new Error("Calling abstract method 'getChannel()'");
}
getTitle() {
throw new Error("Calling abstract method 'getTitle()'");
}
getPublishDate() {
throw new Error("Calling abstract method 'getPublishDate()'");
}
getDescription() {
throw new Error("Calling abstract method 'getDescription()'");
}
}
class GrayJayExtractor extends Extractor {
getURL() {
const ele = document.getElementsByPartialClassName("_containerCasting_")[0].children[0]
const tmp = ele.getAttribute("src").split("/")
return "https:youtu.be/" + tmp[tmp.length-2]
}
getChannel() {
return getElementFromParent("_authorName_", "_authorDescription")[0].textContent
}
getTitle() {
return document.getElementsByPartialClassName("_title_")[0].textContent
}
getPublishDate() {
return new Date()
//return "Publish date is unsupported in GrayJay"
}
getDescription() {
return document.getElementsByPartialClassName("_description_")[0].textContent.split('\n');
}
}
class FreetubeExtractor extends Extractor {
getChannel() {
return getElement("channelName");
}
getTitle() {
return getElement("videoTitle");
}
getPublishDate() {
const text = getElement("datePublishedAndViewCount").replace('Published on ', '');
return text.slice(0, text.indexOf('•') - 1)
.removewhitespaces();
}
getDescription() {
return document.querySelector("#app > div.ft-flex-box.flexBox.routerView > div > div.infoArea.infoAreaSticky > div.ft-card.watchVideo.videoDescription > p")
.textContent.split('\n');
}
}
class YoutubeExtractor extends Extractor {
getChannel() {
return document.querySelector("#text > a").textContent;
}
getTitle() {
return document.querySelector("#title > h1 > yt-formatted-string").textContent
}
getPublishDate() {
return document.querySelector("#info-strings > yt-formatted-string").textContent
}
getDescription() {
return document.querySelector("#attributed-snippet-text > span").textContent.split('\n');
}
}
class ListFormater {
constructor(names, titles, extractor, uncapitalizeTitles) {
this.names = names //NamesToConvert
this.titles = titles //TitlesToTrim
this.channel = this.channel(extractor.channel)
this.title = this.title(extractor.title, this.channel, uncapitalizeTitles) // ToDo: consider changing second argument to extractor.channel (will break TitlesToTrim for channels that also use NamesToConvert)
this.url = extractor.url
this.date = this.publishDate(extractor.date)
this.chapters = extractor.chapters
}
format() {
let text = "- " + this.title + " - [" + this.date + "](" + this.url + ") - [[" + this.channel + "]]";
for (let i = 0; i < this.chapters.length; i++) {
text += "\n\t- " + this.chapters[i];
}
return text;
}
title(title, channel, uncapitalizeTitles) {
const trimText = this.titles.get(channel);
if (trimText !== undefined) {
trimText.forEach( (text) => {
text = new RegExp(text.escape(),'i');
title = title.replace(text, '');
});
}
if (uncapitalizeTitles) {
return title.removewhitespaces().uncapitalize();
} else {
return title.removewhitespaces()
}
}
channel(channel) {
const newChannelName = this.names.get(channel)
return defaultValue(newChannelName, channel)
}
publishDate(date) {
return new Date(date).toLocaleDateString();
}
subtractDays(days) {
let date = new Date();
date.setDate(date.getDate() - days);
this.date = date.toLocaleDateString();
}
}
function formatDescription(lines){
const lines1 = lines.filter(str =>
str.trim() !== "" &&
DescPatterns.every(pattern => !str.toLowerCase().includes(pattern.toLowerCase()))
);
// console.log("Unparsed: %s", lines)
return lines1.join('\n')
}
function getElement(className) {
return document.getElementsByClassName(className)[0].textContent;
}
function defaultValue(value, defaultValue) {
return value === undefined ? defaultValue : value;
}
function show(str) {
console.log(str)
}
function determinePlatform() {
if (document.URL.includes("freetube")) {
return "freetube"
}
if (document.title === "Grayjay") {
return "grayjay"
}
if (document.URL.includes("youtube.com")) {
return "youtube"
}
return "Couldn't determine platform"
}
function extractorFactory(platform, generateChapters, timestamps) {
if (platform === "freetube") {
chapters = new FreetubeChapters(generateChapters, timestamps)
return new FreetubeExtractor(chapters)
} else if (platform === "youtube") {
chapters = new YoutubeChapters(generateChapters, timestamps)
return new YoutubeExtractor(chapters)
} else if (platform === "grayjay"){
chapters = new GrayJayChapters(generateChapters, timestamps)
return new GrayJayExtractor(chapters)
}
throw new Error("Unknown platform.Supported: freetube OR youtube OR grayjay ; got " + platform);
}
platform = determinePlatform()
generateChapters = true
timestamps = false
uncapitalizeTitles = true
dateDifference = 0
data = extractorFactory(platform, generateChapters, timestamps)
formater = new ListFormater(NamesToConvert, TitlesToTrim, data, uncapitalizeTitles)
if (dateDifference !== 0) {
formater.subtractDays(dateDifference)
}
output = formater.format()
if (platform === "freetube") {
const {clipboard} = require('electron');
await clipboard.writeText(output);
}
show(output);
show(formatDescription(data.description))