YouTube | Envoyer vers Obsidian

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.

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