ดึงข้อมูลจากวิดีโอ YouTube และสร้างรายการใหม่ใน Obsidian (ในเครื่อง) เพื่อช่วยให้ง่ายขึ้นในการจดบันทึกเกี่ยวกับวิดีโอ
// ==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 r###lt = null; const selector = this.elements?.channel?.rssUrl; if (selector) { r###lt = (document.querySelector(selector) ?.getAttribute("href") ?.trim() ?? null); } if (!r###lt) { const channelId = this.getChannelId(); if (channelId) { r###lt = "https://www.youtube.com/feeds/videos.xml?channel_id=" + channelId; } } return r###lt; } /** * @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 r###lt = { level: 0, time: null, url: null, text: null }; r###lt.time = element ?.querySelector("#details #time") ?.textContent ?.trim() ?? ""; r###lt.url = "https://www.youtube.com/watch?" + "&v=" + this.getId() + "&t=" + this.constructor.timeToSeconds(r###lt.time); r###lt.text = element ?.querySelector("#details h4.macro-markers") ?.textContent ?.trim() ?? ""; r###lt.episode = new Obsidian.YouTube.Episode(r###lt); 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 r###lt = { level: 0, time: null, url: null, text: null }; if (element.hasAttribute("rounded-container")) { r###lt.level = 0; r###lt.time = element ?.querySelector(".segment-timestamp") ?.textContent ?.trim() ?? ""; r###lt.url = "https://www.youtube.com/watch?" + "&v=" + this.getId() + "&t=" + this.constructor.timeToSeconds(r###lt.time); r###lt.text = element ?.querySelector(".segment-text") ?.textContent ?.trim() ?? ""; } else { /* Эпизоды (заголовки) */ r###lt.level = 3; r###lt.text = element ?.querySelector("h2") ?.textContent ?.trim() ?? ""; const episode = (episodes ?? []) .find(item => item.text === r###lt.text) ?? {}; r###lt.time = episode.time; r###lt.url = episode.url; } const transcript = new Obsidian.YouTube.Transcript(r###lt); 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(); })();