// ==UserScript==
// @name YouTube | Send to Obsidian
// @description Extracts information from a YouTube video and creates a new entry in Obsidian (locally), making it easier to create notes about the video.
// @name:az YouTube | Obsidian'a Göndər
// @description:az YouTube videosundan məlumat çıxarır və yeni bir qeydi Obsidian'da yaradır, videolar haqqında qeydləri asanlaşdırır.
// @name:sq YouTube | Dërgo në Obsidian
// @description:sq Nxjerr informacion nga një video në YouTube dhe krijon një regjistrim të ri në Obsidian (lokalisht), duke lehtësuar krijimin e shënimeve për videon.
// @name:am YouTube | እቢዲያን ውስጥ ላክ
// @description:am ከYouTube ቪዲዮ መረጃ ይላቀቀዋል እና አዲስ መዝገብ በ Obsidian (በእርሱ) ውስጥ ይፈጥራል፣ እንደዚህ እያለችን የቪዲዮውን ማስታወሻዎችን ቀላል አድርጎአል።
// @name:en YouTube | Send to Obsidian
// @description:en Extracts information from a YouTube video and creates a new entry in Obsidian (locally), simplifying note-taking for videos.
// @name:ar YouTube | إرسال إلى Obsidian
// @description:ar يستخرج المعلومات من فيديو YouTube وينشئ مدخلاً جديدًا في Obsidian (محليًا)، مما يسهل تدوين الملاحظات حول الفيديو.
// @name:hy YouTube | Ուղարկել Obsidian-ում
// @description:hy Վերահանում է տեղեկությունը YouTube վիդեոյից և ստեղծում նոր գրառում Obsidian-ում (տեղայնացված), պարզեցնելով վիդեոյի նշումների ստեղծումը.
// @name:af YouTube | Stuur na Obsidian
// @description:af Haal inligting uit 'n YouTube-video uit en skep 'n nuwe inskrywing in Obsidian (plaaslik), wat die maak van aantekeninge oor video's vereenvoudig.
// @name:eu YouTube | Bidali Obsidian-era
// @description:eu YouTube bideo batetik informazioa ateratzen du eta sarrera berri bat sortzen du Obsidian-en (tokian), bideoen oharrak sortzea erraztuz.
// @name:be YouTube | Адправіць у Obsidian
// @description:be Выцягвае інфармацыю з відэа на YouTube і стварае новую запіс у Obsidian (лакальна), палягчаючы стварэнне нататак пра відэа.
// @name:bn YouTube | Obsidian-এ পাঠান
// @description:bn YouTube ভিডিও থেকে তথ্য সংগ্রহ করে এবং Obsidian-এ নতুন এন্ট্রি তৈরি করে (স্থানীয়ভাবে), ভিডিওর নোট তৈরি সহজতর করে।
// @name:my YouTube | Obsidian သို့ပို့ပါ
// @description:my YouTube ဗီဒီယိုမှအချက်အလက်ကိုရယူပြီး Obsidian တွင်အသစ်သောအချက်အလက်ကိုဖန်တီးသည် (ဒေသတွင်း), ဗီဒီယိုမှတ်စုများကိုလွယ်ကူစေသည်။
// @name:bg YouTube | Изпращане в Obsidian
// @description:bg Извлича информация от видеоклип в YouTube и създава нов запис в Obsidian (локално), улеснявайки създаването на бележки за видеото.
// @name:bs YouTube | Pošaljite u Obsidian
// @description:bs Izvlači informacije iz YouTube videa i kreira novi unos u Obsidian (lokalno), olakšavajući kreiranje bilješki o videu.
// @name:cy YouTube | Anfon i Obsidian
// @description:cy Yn tynnu gwybodaeth o fideo YouTube ac yn creu cofnod newydd yn Obsidian (yn lleol), gan symleiddio creu nodiadau ar gyfer fideos.
// @name:hu YouTube | Küldés Obsidianba
// @description:hu Információt nyer ki egy YouTube videóból, és új bejegyzést hoz létre Obsidianban (helyileg), egyszerűsítve a videók megjegyzéseinek létrehozását.
// @name:vi YouTube | Gửi đến Obsidian
// @description:vi Trích xuất thông tin từ video YouTube và tạo một mục mới trong Obsidian (cục bộ), đơn giản hóa việc ghi chú về video.
// @name:gl YouTube | Enviar a Obsidian
// @description:gl Extrae información dun vídeo de YouTube e crea unha nova entrada en Obsidian (localmente), simplificando a creación de notas sobre o vídeo.
// @name:el YouTube | Αποστολή στο Obsidian
// @description:el Εξάγει πληροφορίες από ένα βίντεο στο YouTube και δημιουργεί μια νέα καταχώριση στο Obsidian (τοπικά), απλοποιώντας τη δημιουργία σημειώσεων για βίντεο.
// @name:ka YouTube | გაგზავნა Obsidian-ში
// @description:ka იყენებს ინფორმაციას YouTube ვიდეოდან და ქმნის ახალ ჩანაწერს Obsidian-ში (ადგილობრივად), რაც ამარტივებს ვიდეოზე შენიშვნების შექმნას.
// @name:gu YouTube | Obsidian પર મોકલો
// @description:gu YouTube વિડિયોમાંથી માહિતી કાઢે છે અને Obsidian (સ્થાનિક રીતે) માં નવો એન્ટ્રી બનાવે છે, વિડિયોના નોંધ બનાવવી સરળ બનાવે છે.
// @name:da YouTube | Send til Obsidian
// @description:da Uddrager oplysninger fra en YouTube-video og opretter en ny post i Obsidian (lokalt), hvilket gør det nemmere at oprette noter om videoen.
// @name:zu YouTube | Thumela ku-Obsidian
// @description:zu Ukhipha ulwazi kuvidiyo ye-YouTube bese edala irekhodi elisha ku-Obsidian (endaweni), okwenza kube lula ukudala amanothi wevidiyo.
// @name:he YouTube | שלח לאובסידיאן
// @description:he שולף מידע מתוך סרטון YouTube ויוצר ערך חדש ב-Obsidian (מקומית), מה שמקל על יצירת הערות עבור סרטונים.
// @name:ig YouTube | Zipu na Obsidian
// @description:ig Na-ewepụta ozi sitere na vidiyo YouTube wee mepụta ndekọ ọhụrụ na Obsidian (n'ebe), na-eme ka ọ dị mfe ịmepụta ndetu maka vidiyo.
// @name:yi YouTube | שיקן צו Obsidian
// @description:yi דערקלערט אינפֿאָרמאַציע פון אַ יאָוטובע ווידעא און שאַפֿט אַ נייַע איינסן אין Obsidian (אָרטלעך), סימפּליפיינג די שאַפונג פון טאָן וועגן ווידעא.
// @name:id YouTube | Kirim ke Obsidian
// @description:id Menarik informasi dari video YouTube dan membuat entri baru di Obsidian (lokal), menyederhanakan pembuatan catatan untuk video.
// @name:ga YouTube | Seol chuig Obsidian
// @description:ga Bainfidh eolas as físeán YouTube agus cruthaíonn sé iontráil nua in Obsidian (go háitiúil), ag éascú cruthú nótaí faoi fhíseáin.
// @name:is YouTube | Senda til Obsidian
// @description:is Dregur upplýsingar úr YouTube myndbandi og býr til nýjan þátt í Obsidian (staðbundið), sem auðveldar gerð athugasemda um myndbönd.
// @name:es YouTube | Enviar a Obsidian
// @description:es Extrae información de un video de YouTube y crea una nueva entrada en Obsidian (localmente), simplificando la creación de notas sobre el video.
// @name:it YouTube | Invia a Obsidian
// @description:it Estrae informazioni da un video YouTube e crea una nuova voce in Obsidian (localmente), semplificando la creazione di appunti sui video.
// @name:kn YouTube | Obsidian ಗೆ ಕಳುಹಿಸು
// @description:kn YouTube ವೀಡಿಯೋದಿಂದ ಮಾಹಿತಿಯನ್ನು ಹೊರತೆಗೆದು Obsidian ನಲ್ಲಿ ಹೊಸ ದಾಖಲೆ ಸೃಷ್ಟಿಸುತ್ತದೆ (ಸ್ಥಳೀಯವಾಗಿ), ವೀಡಿಯೋಗಳ ಕುರಿತು ಟಿಪ್ಪಣಿಗಳನ್ನು ಸರಳಗೊಳಿಸುತ್ತದೆ.
// @name:fr YouTube | Envoyer vers Obsidian
// @description:fr Extrait des informations d'une vidéo YouTube et crée une nouvelle entrée dans Obsidian (localement), simplifiant la prise de notes pour les vidéos.
// @name:ja YouTube | Obsidianに送信
// @description:ja YouTubeビデオから情報を抽出し、Obsidianに新しいエントリを作成して、ビデオに関するノート作成を簡単にします。
// @name:ko YouTube | Obsidian으로 보내기
// @description:ko YouTube 동영상에서 정보를 추출하고 Obsidian에 새 항목을 생성하여 동영상 메모 작성 작업을 단순화합니다.
// @name:pt YouTube | Enviar para o Obsidian
// @description:pt Extrai informações de um vídeo do YouTube e cria uma nova entrada no Obsidian (localmente), simplificando a criação de anotações sobre o vídeo.
// @name:pl YouTube | Wyślij do Obsidian
// @description:pl Wyciąga informacje z filmu YouTube i tworzy nowy wpis w Obsidian (lokalnie), ułatwiając tworzenie notatek o filmach.
// @name:fa YouTube | ارسال به Obsidian
// @description:fa اطلاعات را از ویدئوی یوتیوب استخراج کرده و یک ورودی جدید در Obsidian (محلی) ایجاد میکند، و یادداشتبرداری برای ویدئو را سادهتر میسازد.
// @name:ps YouTube | Obsidian ته ولیږئ
// @description:ps د یوټیوب ویډیو څخه معلومات راوباسي او په Obsidian (محلي) کې نوی ریکارډ جوړوي، د ویډیو یادداشتونو جوړولو کار اسانوي.
// @name:pt-BR YouTube | Enviar para o Obsidian
// @description:pt-BR Extrai informações de um vídeo do YouTube e cria uma nova entrada no Obsidian (localmente), simplificando a criação de anotações sobre o vídeo.
// @name:pa YouTube | Obsidian ਨੂੰ ਭੇਜੋ
// @description:pa YouTube ਵੀਡੀਓ ਤੋਂ ਜਾਣਕਾਰੀ ਕੱਢਦਾ ਹੈ ਅਤੇ Obsidian ਵਿੱਚ ਨਵੀਂ ਐਂਟਰੀ ਬਣਾਉਂਦਾ ਹੈ (ਸਥਾਨਕ), ਵੀਡੀਓ ਨੋਟਾਂ ਬਣਾਉਣ ਨੂੰ ਸੌਖਾ ਬਣਾਉਂਦਾ ਹੈ.
// @name:ro YouTube | Trimite în Obsidian
// @description:ro Extrage informații dintr-un videoclip YouTube și creează o nouă intrare în Obsidian (local), simplificând crearea de note despre videoclip.
// @name:ru YouTube | Отправить в Obsidian
// @description:ru Извлекает информацию из видеоролика на YouTube и создает новую запись в Obsidian (локально), упрощая создание заметок о видео.
// @name:sv YouTube | Skicka till Obsidian
// @description:sv Extraherar information från en YouTube-video och skapar ett nytt inlägg i Obsidian (lokalt), vilket förenklar anteckningar om videon.
// @name:ta YouTube | Obsidianக்கு அனுப்பு
// @description:ta YouTube வீடியோவிலிருந்து தகவலை எடுத்து Obsidian இல் புதிய பதிவை உருவாக்குகிறது (உள்ளூரில்), வீடியோவுக்கான குறிப்புகளை எளிதாக்குகிறது.
// @name:th YouTube | ส่งไปที่ Obsidian
// @description:th ดึงข้อมูลจากวิดีโอ YouTube และสร้างรายการใหม่ใน Obsidian (ในเครื่อง) เพื่อช่วยให้ง่ายขึ้นในการจดบันทึกเกี่ยวกับวิดีโอ
// @name:tr YouTube | Obsidian'a Gönder
// @description:tr YouTube videosundan bilgi alır ve Obsidian'da yeni bir giriş oluşturur (yerel olarak), video notlarını oluşturmayı kolaylaştırır.
// @name:uk YouTube | Відправити в Obsidian
// @description:uk Витягує інформацію з відео на YouTube і створює новий запис в Obsidian (локально), спрощуючи створення нотаток про відео.
// @name:ur YouTube | Obsidian میں بھیجیں
// @description:ur یوٹیوب ویڈیو سے معلومات نکالتا ہے اور Obsidian میں ایک نیا اندراج تخلیق کرتا ہے (مقامی طور پر)، ویڈیو کے بارے میں نوٹ لینے کو آسان بناتا ہے.
// @name:uz YouTube | Obsidian-ga yuborish
// @description:uz YouTube videodan ma'lumot chiqaradi va Obsidian-da yangi yozuv yaratadi (mahalliy), videoga eslatmalar yozishni osonlashtiradi.
// @name:fi YouTube | Lähetä Obsidianille
// @description:fi Hakee tietoa YouTube-videosta ja luo uuden merkinnän Obsidianissa (paikallisesti), yksinkertaistaen muistiinpanojen luomista videosta.
// @name:fr YouTube | Envoyer vers Obsidian
// @description:fr Extrait des informations d'une vidéo YouTube et crée une nouvelle entrée dans Obsidian (localement), simplifiant la prise de notes pour les vidéos.
// @name:fy YouTube | Stjoer nei Obsidian
// @description:fy Ekstraheert ynformaasje fan in YouTube-fideo en makket in nije ynfier yn Obsidian (lokaal), wat it notearjen oer de fideo makliker makket.
// @name:ha YouTube | Aika zuwa Obsidian
// @description:ha Yana cire bayanai daga bidiyon YouTube kuma yana ƙirƙirar sabon shigarwa a cikin Obsidian (lokal), yana sauƙaƙa rubuta bayanai game da bidiyon.
// @name:hi YouTube | ओब्सीडियन में भेजें
// @description:hi YouTube वीडियो से जानकारी निकालता है और Obsidian में एक नई प्रविष्टि बनाता है (स्थानीय रूप से), जिससे वीडियो पर नोट्स बनाना आसान हो जाता है.
// @name:hr YouTube | Pošalji u Obsidian
// @description:hr Izvlači informacije iz YouTube videozapisa i stvara novi unos u Obsidianu (lokalno), olakšavajući bilježenje o videu.
// @name:cs YouTube | Odeslat do Obsidianu
// @description:cs Extrahuje informace z YouTube videa a vytvoří nový záznam v Obsidianu (lokálně), což zjednodušuje vytváření poznámek k videu.
// @name:sv YouTube | Skicka till Obsidian
// @description:sv Extraherar information från en YouTube-video och skapar ett nytt inlägg i Obsidian (lokalt), vilket förenklar anteckningar om videon.
// @name:sn YouTube | Tumira ku Obsidian
// @description:sn Inobvisa ruzivo kubva kuYouTube vhidhiyo uye inogadzira rekodhi itsva muObsidian (panzvimbo), zvichiita kuti chinyorwa nezvevhidhiyo zvive nyore kuita.
// @name:eo YouTube | Sendi al Obsidian
// @description:eo Ekstraktas informojn el YouTube-video kaj kreas novan eniron en Obsidian (loke), simpligante notadon pri la video.
// @name:et YouTube | Saada Obsidiansse
// @description:et Ekstraheerib teavet YouTube'i videost ja loob uue kirje Obsidians (kohapeal), muutes videot puudutavate märkmete tegemise lihtsamaks.
// @name:jv YouTube | Kirim menyang Obsidian
// @description:jv Ngekstrak informasi saka video YouTube lan nggawe entri anyar ing Obsidian (lokal), nyederhanakake nggawe cathetan babagan video.
// @name:ja YouTube | Obsidianに送信
// @description:ja YouTubeビデオから情報を抽出し、Obsidianに新しいエントリを作成して、ビデオに関するノート作成を簡単にします。
// @version 1.0.0
// @match https://www.youtube.com/watch?*
// @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @grant GM_addStyle
// @noframes
// @namespace https://maksymstoianov.com/
// @supportURL https://maksymstoianov.com/
// @contributionURL https://maksymstoianov.com/
// @author Maksym Stoianov
// @developer Maksym Stoianov
// @license MIT
// @compatible chrome
// @compatible firefox
// @compatible opera
// @compatible safaricom
// ==/UserScript==
(function () {
'use strict';
class Obsidian {
static preloadImages(urls) {
const images = [];
urls.forEach(url => {
const img = new Image();
img.src = url;
images.push(img);
});
}
/**
* @param {string} input
* @returns {string}
*/
static sanitizeTitle(input) {
return (input.replace(/[:\/\\^|#]/g, ".") ?? "");
}
static merge(message = "", fields = {}, ...args) {
return message.replace(/{{([^}]+?)}}/g, (match, p1) => {
try {
let key, defaultValue, format;
if (p1.includes(":")) {
const parts = p1
.split(/(?<!\\):/)
.map((part) => part.replace(/\\:/g, ":"));
// {{key:defaultValue:format}}
[key, defaultValue, ...format] = parts;
format = (format.length ? format.join(":") : null);
if (typeof format === "string" && !format.length) {
format = null;
}
} else {
// {{key}}
key = p1;
}
// Получаем значение из fields или используем defaultValue, если значение отсутствует или пусто
let value = fields[key];
if (value === undefined || value === null || value === "") {
value = defaultValue ?? "";
}
if (value instanceof Date) {
value = this.formatDate(value, format ?? "yyyy-MM-dd");
}
else if (["string", "number"].includes(typeof value)) {
if (defaultValue === "" && value === "") {
value = match.replace(/:/g, "");
} else if (this.isNumberLike(value)) {
value = Number(value);
}
value = this.sprintf(format ?? "%s", value);
}
else if (typeof value === "object") {
value = JSON.stringify(value);
}
return value;
} catch (error) {
console.warn(`Ошибка при обработке метки ${match}:`, error.message);
}
return match;
});
}
/**
* @param {string} url
* @returns {boolean}
*/
static isYouTube(url) {
return (url.hostname === "www.youtube.com");
}
/**
* Отслеживает появление элемента в DOM.
* @param {string} selector
* @param {function} callback
*/
static onElementInDOM(selector, callback) {
if (!(typeof selector === "string" && selector.length)) {
return false;
}
new MutationObserver((mutationsList, observer) => {
for (const mutation of mutationsList) {
if (mutation.type !== "childList") continue;
mutation.addedNodes.forEach(node => {
if (!(node instanceof Element)) {
return;
}
if (node.matches(selector) || node.querySelector(selector)) {
callback.apply(this, [{
selector,
target: node,
observer
}]);
}
});
}
}).observe(document.body, {
childList: true,
subtree: true
});
return true;
}
/**
* Отслеживает появление элемента на экране.
* @param {string} selector
* @param {function} callback
*/
static onElementVisible(selector, callback) {
if (!(typeof selector === "string" && selector.length)) {
return false;
}
const target = document.querySelector(selector);
if (!target) {
return this.onElementInDOM(selector, function () {
this.onElementVisible(selector, callback);
});
}
new IntersectionObserver(
(entries, observer) => {
entries.forEach(entry => {
if (!entry.isIntersecting) return;
callback.apply(this, [{
selector,
target: entry.target,
observer
}]);
});
},
{
root: null,
rootMargin: "0px",
threshold: 0.1
}
).observe(target);
return true;
}
static run() {
if (this.isYouTube(window.location)) {
new Obsidian.YouTube(window.location);
}
}
}
Obsidian.YouTube = class YouTube {
/**
* @param {string} timeString
* @returns {number}
*/
static timeToSeconds(timeString) {
const [minutes, seconds] = timeString
.split(":")
.map(Number);
return (minutes * 60 + seconds);
}
/**
* @param {string} url
*/
constructor(url) {
this.url = url;
this.elements = {
video: {
element: "video",
id: null,
},
channel: {
id: "head meta[itemprop='identifier']",
url: "head link[itemprop='url']",
rssUrl: "link[title='RSS'][type='application/rss+xml']",
author: "ytd-channel-name a",
},
segments: "#segments-container > *",
episodes: "#structured-description #shelf-container #items > *",
microformat: "#microformat script[type='application/ld+json']",
button1: "#structured-description #primary-button button",
transcript: `[target-id="engagement-panel-searchable-transcript"]`,
shareTargets: "#share-targets"
};
/**
* Запускаем отслеживание для элемента.
*/
Obsidian.onElementInDOM(this.elements.button1,
({ target, observer }) => {
// Запрос транскрипции.
target.click();
observer.disconnect();
}
);
/**
* Запускаем отслеживание для элемента.
*/
Obsidian.onElementVisible(this.elements.transcript,
({ target, observer }) => {
// Спрятать транскрипцию.
target.setAttribute(
"visibility",
"ENGAGEMENT_PANEL_VISIBILITY_HIDDEN"
);
observer.disconnect();
}
);
/**
* Запускаем отслеживание для элемента.
*/
Obsidian.onElementVisible(this.elements.shareTargets,
({ target }) => {
const containerId = "obsidian-button-container";
if (document.getElementById(containerId)) {
return;
}
const container = document.createElement("div");
container.id = containerId;
const button = document.createElement("button");
button.classList.add("style-scope");
button.classList.add("yt-share-target-renderer");
button.onclick = () => this.createNote();
const img = document.createElement("img");
img.src = "https://www.google.com/s2/favicons?sz=64&domain=obsidian.md";
button.appendChild(img);
const span = document.createElement("span");
span.classList.add("style-scope");
span.classList.add("yt-share-target-renderer");
span.setAttribute("style-targe", "title");
span.textContent = "Obsidian";
button.appendChild(span);
container.appendChild(button);
target
.querySelector("yt-third-party-share-target-section-renderer")
?.appendChild(container);
}
);
Obsidian.preloadImages([
"https://www.google.com/s2/favicons?sz=64&domain=obsidian.md"
]);
GM_addStyle(`
#obsidian-button-container button {
color: var(--yt-spec-text-primary);
display: inline-flex;
flex-direction: column;
justify-content: center;
align-items: center;
flex-wrap: nowrap;
margin: 1px 0;
border: none;
border-radius: 3px;
padding: 5px 1px 2px;
outline: none;
text-align: inherit;
font-family: inherit;
background-color: transparent;
cursor: pointer;
}
#obsidian-button-container button img {
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
vertical-align: middle;
width: var(--iron-icon-width, 24px);
height: var(--iron-icon-height, 24px);
animation: var(--iron-icon-animation);
padding: var(--iron-icon-padding);
border-radius: 100%;
--iron-icon-height: 60px;
--iron-icon-width: 60px;
margin-top: var(--iron-icon-margin-top);
margin-left: var(--ytd-margin-base);
margin-right: var(--ytd-margin-base);
margin-bottom: var(--ytd-margin-2x);
}
#obsidian-button-container button span {
color: var(--yt-spec-text-primary);
margin: auto;
width: 68px;
max-height: 42px;
text-align: center;
white-space: normal;
overflow: hidden;
font-family: "Roboto", "Arial", sans-serif;
font-size: 1.2rem;
line-height: 1.8rem;
font-weight: 400;
}
`);
}
/**
* @returns {string}
*/
getId() {
const searchParams = this.getUrl()?.search;
return (
(searchParams
? new URLSearchParams(searchParams).get("v")
: null
) ??
(this.getShortLinkUrl()?.match(/\/([^\/]*)$/) ?? [])[1] ??
null
);
}
/**
* @returns {URL}
*/
getUrl() {
return (this.url ?? null);
}
/**
* @returns {string}
*/
getTitle() {
return (document?.title
?.replace(/\s*-\s*YouTube\s*$/, "") ?? null);
}
/**
* @returns {string}
*/
getChannelId() {
const channelUrl = (
this.getChannelUrl() ??
document.querySelector("#social-links #items a[href^='/channel/']").getAttribute("href")
);
return (
(channelUrl?.match(/channel\/([^\/]+)(\/|$)/) ?? [])[1] ??
null
);
}
/**
* @returns {string}
*/
getChannelName() {
const selector = this.elements?.channel?.author;
return (
this.getJson().author ??
(selector
? document.querySelector(selector)?.textContent?.trim()
: null) ??
null
);
}
/**
* @returns {string}
*/
getChannelUrl() {
const selector = this.elements?.channel?.url;
return (selector
? document.querySelector(selector)?.getAttribute("href")?.trim()
: null) ?? null;
}
/**
* @returns {string}
*/
getChannelRssUrl() {
let result = null;
const selector = this.elements?.channel?.rssUrl;
if (selector) {
result = (document.querySelector(selector)
?.getAttribute("href")
?.trim() ?? null);
}
if (!result) {
const channelId = this.getChannelId();
if (channelId) {
result = "https://www.youtube.com/feeds/videos.xml?channel_id=" + channelId;
}
}
return result;
}
/**
* @returns {string}
*/
getPublishedDate() {
return (
this.getJson().datePublished ??
this.getMetaContent("datePublished") ??
null
);
}
/**
* @returns {string}
*/
getUploadDate() {
return (
this.getJson().uploadDate ??
this.getMetaContent("uploadDate") ??
null
);
}
/**
* @returns {string}
*/
getDate() {
return (
(
(
this.getPublishedDate() ??
this.getUploadDate()
)?.split("T") ??
[]
)[0] ??
null
);
}
/**
* @returns {string[]}
*/
getKeywords() {
const keywords = this.getMetaContent("keywords");
if (!keywords) {
return [];
}
const regex = /\s*("[^"]+"|'[^']+'|[^, ]+)\s*,?\s*/g;
const matches = [];
let match;
while ((match = regex.exec(keywords)) !== null) {
// Убираем кавычки с начала и конца, если они есть
matches.push(match[1].replace(/^["']|["']$/g, ""));
}
// Проверка последнего элемента на троеточие
if (matches[matches.length - 1]?.endsWith("...")) {
matches.pop();
}
return matches;
}
/**
* @returns {string}
*/
getShortLinkUrl() {
return (this.getMetaContent("shortlinkUrl") ?? null);
}
/**
* @returns {string}
*/
getCategory() {
return (
this.getJson().genre ??
this.getMetaContent("genre") ??
null
);
}
/**
* @returns {string}
*/
getDescription() {
return (
this.getJson().description ??
this.getMetaContent("description") ??
null
);
}
/**
* @param {boolean} flag
* - `true` – Array
* - `false` – String
* @returns {(string[]|string)}
*/
getEpisodes(flag) {
let values = [];
const selector = this.elements?.episodes;
if (!selector) {
return null;
}
document
.querySelectorAll(selector)
?.forEach(element => {
try {
const result = {
level: 0,
time: null,
url: null,
text: null
};
result.time = element
?.querySelector("#details #time")
?.textContent
?.trim() ?? "";
result.url = "https://www.youtube.com/watch?"
+ "&v=" + this.getId()
+ "&t=" + this.constructor.timeToSeconds(result.time);
result.text = element
?.querySelector("#details h4.macro-markers")
?.textContent
?.trim() ?? "";
result.episode = new Obsidian.YouTube.Episode(result);
values.push(episode);
} catch (error) {
console.warn(error.message);
}
});
if (!values.length) {
return null;
}
if (flag !== true) {
return "\n## Episodes\n" + values
.map(item => item.toString())
.join("\n");
}
return values;
}
/**
* @param {boolean} flag
* - `true` – Array
* - `false` – String
* @returns {(string[]|string)}
*/
getTranscript(flag) {
let values = [];
const selector = this.elements?.segments;
if (!selector) {
return null;
}
const episodes = this.getEpisodes(true);
document.querySelectorAll(selector)
?.forEach(element => {
try {
const result = {
level: 0,
time: null,
url: null,
text: null
};
if (element.hasAttribute("rounded-container")) {
result.level = 0;
result.time = element
?.querySelector(".segment-timestamp")
?.textContent
?.trim() ?? "";
result.url = "https://www.youtube.com/watch?"
+ "&v=" + this.getId()
+ "&t=" + this.constructor.timeToSeconds(result.time);
result.text = element
?.querySelector(".segment-text")
?.textContent
?.trim() ?? "";
} else {
/* Эпизоды (заголовки) */
result.level = 3;
result.text = element
?.querySelector("h2")
?.textContent
?.trim() ?? "";
const episode = (episodes ?? [])
.find(item => item.text === result.text) ?? {};
result.time = episode.time;
result.url = episode.url;
}
const transcript = new Obsidian.YouTube.Transcript(result);
values.push(transcript);
} catch (error) {
console.warn(error.message);
}
});
if (!values.length) {
return null;
}
if (flag !== true) {
return "\n## Transcript\n" + values
.map(item => item.toString())
.join("\n");
}
return values;
}
/**
* @returns {string}
*/
getMetaContent(input) {
return document
?.querySelector("meta[itemprop='" + input + "'], meta[name='" + input + "']")
?.getAttribute("content")
?.trim() ?? null;
}
/**
* @returns {object}
*/
getJson() {
const selector = this.elements?.microformat;
if (!selector) return {};
let values = document
.querySelector(selector)
?.textContent;
try {
values = (values ? JSON.parse(values) : {});
} catch (error) { }
return (values !== null && typeof values === "object" ? values : {});
}
/**
* @returns {string}
*/
getObsidianUrl() {
const videoId = this.getId();
if (!videoId) {
return;
}
if (this.elements?.video?.element?.paused) {
this.elements.video.element.pause();
}
const _escape = input => (input ?? "")
.replace(/"/g, '\\"');
const url = this.getUrl();
const title = this.getTitle();
const date = this.getDate();
const publishedDate = this.getPublishedDate();
const uploadDate = this.getUploadDate();
const channelName = this.getChannelName();
const keywords = this.getKeywords();
const tags = [
"Video",
"YouTube"
];
const path = [
"RSS",
encodeURIComponent(Obsidian.sanitizeTitle(channelName ?? "")),
"YouTube",
encodeURIComponent((date ?? "") + " " + Obsidian.sanitizeTitle(title ?? videoId ?? "").trim() + ".md")
].join("/");
const content = [
"---",
`media_link: ${url}`,
`channel: "${_escape(channelName ?? "")}"`,
`category: "${_escape(this.getCategory() ?? "")}"`,
"published_date: " + (publishedDate ?? ""),
"upload_date: " + (uploadDate ?? ""),
(keywords.length
? "keywords:\n" + keywords
.map(item => ` - "${_escape(item)}"`)
.join("\n") + "\n"
: ""),
(tags.length
? "tags:\n" + tags
.map(item => ` - "${_escape(item)}"`)
.join("\n") + "\n"
: ""),
`rss_link: ${this.getChannelRssUrl() ?? ""}`,
"---",
`# ${title ?? ""}`,
`\n## Description`,
`${this.getDescription() ?? ""}`,
(this.getTranscript() ?? this.getEpisodes() ?? "")
].join("\n");
return `obsidian://new?file=${path}&content=${encodeURIComponent(content)}`;
}
/**
* @returns {string}
*/
createNote() {
return window.open(this.getObsidianUrl());
}
};
Obsidian.YouTube.Episode = class Episode {
constructor({ level, time, url, text }) {
this.level = (level ?? 0);
this.time = (time ?? null);
this.url = (url ?? null);
this.text = (text ?? null);
}
toString() {
return `${this.level > 0 ? "#".repeat(this.level) : "-"} [${this.time}](${this.url ?? "#"}) ${this.text}`;
}
};
Obsidian.YouTube.Transcript = class Transcript {
constructor({ level, time, url, text }) {
this.level = (level ?? 0);
this.time = (time ?? null);
this.url = (url ?? null);
this.text = (text ?? null);
}
toString() {
return `${this.level > 0 ? "#".repeat(this.level) : "-"} [${this.time}](${this.url ?? "#"}) ${this.text}`;
}
};
Obsidian.run();
})();