YouTube | Send to Obsidian

Extracts information from a YouTube video and creates a new entry in Obsidian (locally), making it easier to create notes about the video.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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();
})();