Freetube script to export notes to obsidian

This is my script for exporting links from freetube to obsidian using clipboard.

Usage

  • Open dev tools (F12) in freetube
  • Go to tab sources
  • Create new snippet, past code and run it (ctr+enter)
  • When compiled or when exportYoutubeMetadata(); is called it will print formatted link on console and copy it to system clipboard.

Link formatting

By default it will export link like this:

- Video title - [DD/MM/YYYY](https:youtu.be/xxxxxxxxxxx) - [[Channel]]

Customisation

Link Format

  • Change function formatOutput()

Change Channel Name

  • There is map NamesToConvert that stores channel names that should be converted to other name.
  • First string is original channel name
  • Second is the name that you want in your link.

Video titles

  • Some channels are putting they channel name to video tittle. TitlesToTrim is used to remove anything from video tittle for given channel.
  • First string is channel name.
    • If you change it NamesToConvert you should use second value from this map as key
  • Second value is array of strings that should be removed.
    • Make sure that you have copied exact string you want to remove, some whitespaces can cause troubles.
    • You can remove multiple strings, for single channel but they have to be in this format
    TitlesToTrim.set("channel1", ["remove"]);
    TitlesToTrim.set("channel2", ["remove1", "remove2"]);
    

Script

  • See first comment

Contributions

  • I would want to add button to freetube UI that run this code without using dev tools, but I’m not js dev, and never worked with electron apps. If you know how to do this, I’m open for suggestions.
1 Like
const {clipboard} = require('electron');

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"]);

class Chapters {
    constructor(){
        this.data = [];
        return this;
    }

    generate(exportURLs, videoURL) {
        const htmlCol = document.getElementsByClassName("chapterTimestamp");

        for (let i=0;i<htmlCol.length;i++) {
            const url = exportURLs ? this.getChapterURL(
                htmlCol[i], videoURL, i) : "";
            const desc = this.getDescription(htmlCol[i]);
            
            if (exportURLs) {
                this.data[i] = desc + " " + url
            } else {
                this.data[i] = desc
            }

        }
    }

    getChapterURL(htmlDiv, url, pos) {
        const i = pos + 1;
        const ts = this.getTimestamp(htmlDiv.outerText
                .removewhitespaces()
                .split(":").reverse());
        return "["+i+"]("+url+"?t="+ts+")"
    }

    getDescription(htmlHandle) {
        const sib = htmlHandle.nextElementSibling;

        return sib.outerText.removewhitespaces()
    }

    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;
    }
}

function getElement (className) {
    return document.getElementsByClassName(className)[0].
        textContent;
}

function getYoutubeTitle(channel) {
    title = getElement("videoTitle");
    trimText = TitlesToTrim.get(channel);
    if (trimText !== undefined) {
        trimText.forEach((text) => {
            text = new RegExp(text.escape(), 'i');
            title = title.replace(text, '');
        });
    }
    return title.removewhitespaces().uncapitalize();
}

function defaultValue(value, defaultValue) {
  return value === undefined ? defaultValue : value;
}

function getYoutubeChannel() {
    const str = getElement("channelName").removewhitespaces();
    const newChannelName = NamesToConvert.get(str)

    return defaultValue(newChannelName, str)
}

function getYouTubeURL() {
    // eg: file:///app/freetube/resources/app.asar/dist/index.html#/watch/XXXXXXXXXXX
    const freeURL = document.URL.split("/").pop();
    return "https:youtu.be/" + freeURL
}

function getPublishDate() {
    const text = getElement("datePublishedAndViewCount").
        replace('Published on ', '');
    const date = text.slice(0, text.indexOf('•') - 1).removewhitespaces();

    return new Date(date).toLocaleDateString();
}

function formatOutput(title, currentURL, channel, date, chapters) {
    let text = "- " + title + " - [" + date + "](" + currentURL + ") - [[" + channel + "]]";
    for (let i=0;i<chapters.data.length;i++) {
        text += "\n\t- " + chapters.data[i];
    }
    return text;
}

async function exportToClipboard(output) {
    await clipboard.writeText(output);
}

function exportYoutubeMetadata(chapters, chapterURLs) {
    const channel = getYoutubeChannel();
    const title = getYoutubeTitle(channel);
    const currentURL = getYouTubeURL();
    const publishdate = getPublishDate();
    const chapter = new Chapters();
    if (chapters == true)
        chapter.generate(chapterURLs, currentURL);
    
    const output = formatOutput(title, currentURL, channel, publishdate, chapter);
    exportToClipboard(output);
    show(output);
    show(currentURL);
}


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 () {
    for (var c of this) {
        if (c !== c.toUpperCase()) {
            return false;
        }
    }
    return true;
}

String.prototype.escape = function () {
    return this.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
}

function show(str) {
    console.log(str)
}

exportYoutubeMetadata(true, false);

I have added two functions to this script.

  1. Now exportYoutubeMetadata(exportChapters?, exportTimestamps?) take two booleans as arguments. First determines if video chapters will be exported, second will determine if exported chapters has generated timestamps. Default script behaviour is generate chapters without timestamps.
  2. It will automatically uncapitalize video titles. You can disable it by removing call .uncapitalize() from getYoutubeTitle() function.
Examples

Without timestamps:

- Something Is Causing Stars To Escape Super Fast From the Nearby Dwarf Galaxy - [21/02/2025](https:youtu.be/HjGOTBDLZYk) - [[Anton Petrov]]
	- Hypervelocity stars from Large Magellanic Cloud
	- What are hypervelocity stars and how do they form?
	- Studies on hypervelocity stars in the Milky Way and the halo
	- New study makes a surprising discovery
	- What's forming them? Potential explanations
	- Implications and conclusions

With timestamps:

- Something Is Causing Stars To Escape Super Fast From the Nearby Dwarf Galaxy - [21/02/2025](https:youtu.be/HjGOTBDLZYk) - [[Anton Petrov]]
	- Hypervelocity stars from Large Magellanic Cloud [1](https:youtu.be/HjGOTBDLZYk?t=0)
	- What are hypervelocity stars and how do they form? [2](https:youtu.be/HjGOTBDLZYk?t=32)
	- Studies on hypervelocity stars in the Milky Way and the halo [3](https:youtu.be/HjGOTBDLZYk?t=305)
	- New study makes a surprising discovery [4](https:youtu.be/HjGOTBDLZYk?t=365)
	- What's forming them? Potential explanations [5](https:youtu.be/HjGOTBDLZYk?t=445)
	- Implications and conclusions [6](https:youtu.be/HjGOTBDLZYk?t=480)