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.

  1. // ==UserScript==
  2.  
  3. // @name YouTube | Send to Obsidian
  4. // @description Extracts information from a YouTube video and creates a new entry in Obsidian (locally), making it easier to create notes about the video.
  5.  
  6. // @name:az YouTube | Obsidian'a Göndər
  7. // @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.
  8.  
  9. // @name:sq YouTube | Dërgo në Obsidian
  10. // @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.
  11.  
  12. // @name:am YouTube | እቢዲያን ውስጥ ላክ
  13. // @description:am ከYouTube ቪዲዮ መረጃ ይላቀቀዋል እና አዲስ መዝገብ በ Obsidian (በእርሱ) ውስጥ ይፈጥራል፣ እንደዚህ እያለችን የቪዲዮውን ማስታወሻዎችን ቀላል አድርጎአል።
  14.  
  15. // @name:en YouTube | Send to Obsidian
  16. // @description:en Extracts information from a YouTube video and creates a new entry in Obsidian (locally), simplifying note-taking for videos.
  17.  
  18. // @name:ar YouTube | إرسال إلى Obsidian
  19. // @description:ar يستخرج المعلومات من فيديو YouTube وينشئ مدخلاً جديدًا في Obsidian (محليًا)، مما يسهل تدوين الملاحظات حول الفيديو.
  20.  
  21. // @name:hy YouTube | Ուղարկել Obsidian-ում
  22. // @description:hy Վերահանում է տեղեկությունը YouTube վիդեոյից և ստեղծում նոր գրառում Obsidian-ում (տեղայնացված), պարզեցնելով վիդեոյի նշումների ստեղծումը.
  23.  
  24. // @name:af YouTube | Stuur na Obsidian
  25. // @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.
  26.  
  27. // @name:eu YouTube | Bidali Obsidian-era
  28. // @description:eu YouTube bideo batetik informazioa ateratzen du eta sarrera berri bat sortzen du Obsidian-en (tokian), bideoen oharrak sortzea erraztuz.
  29.  
  30. // @name:be YouTube | Адправіць у Obsidian
  31. // @description:be Выцягвае інфармацыю з відэа на YouTube і стварае новую запіс у Obsidian (лакальна), палягчаючы стварэнне нататак пра відэа.
  32.  
  33. // @name:bn YouTube | Obsidian-এ পাঠান
  34. // @description:bn YouTube ভিডিও থেকে তথ্য সংগ্রহ করে এবং Obsidian-এ নতুন এন্ট্রি তৈরি করে (স্থানীয়ভাবে), ভিডিওর নোট তৈরি সহজতর করে।
  35.  
  36. // @name:my YouTube | Obsidian သို့ပို့ပါ
  37. // @description:my YouTube ဗီဒီယိုမှအချက်အလက်ကိုရယူပြီး Obsidian တွင်အသစ်သောအချက်အလက်ကိုဖန်တီးသည် (ဒေသတွင်း), ဗီဒီယိုမှတ်စုများကိုလွယ်ကူစေသည်။
  38.  
  39. // @name:bg YouTube | Изпращане в Obsidian
  40. // @description:bg Извлича информация от видеоклип в YouTube и създава нов запис в Obsidian (локално), улеснявайки създаването на бележки за видеото.
  41.  
  42. // @name:bs YouTube | Pošaljite u Obsidian
  43. // @description:bs Izvlači informacije iz YouTube videa i kreira novi unos u Obsidian (lokalno), olakšavajući kreiranje bilješki o videu.
  44.  
  45. // @name:cy YouTube | Anfon i Obsidian
  46. // @description:cy Yn tynnu gwybodaeth o fideo YouTube ac yn creu cofnod newydd yn Obsidian (yn lleol), gan symleiddio creu nodiadau ar gyfer fideos.
  47.  
  48. // @name:hu YouTube | Küldés Obsidianba
  49. // @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.
  50.  
  51. // @name:vi YouTube | Gửi đến Obsidian
  52. // @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.
  53.  
  54. // @name:gl YouTube | Enviar a Obsidian
  55. // @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.
  56.  
  57. // @name:el YouTube | Αποστολή στο Obsidian
  58. // @description:el Εξάγει πληροφορίες από ένα βίντεο στο YouTube και δημιουργεί μια νέα καταχώριση στο Obsidian (τοπικά), απλοποιώντας τη δημιουργία σημειώσεων για βίντεο.
  59.  
  60. // @name:ka YouTube | გაგზავნა Obsidian-ში
  61. // @description:ka იყენებს ინფორმაციას YouTube ვიდეოდან და ქმნის ახალ ჩანაწერს Obsidian-ში (ადგილობრივად), რაც ამარტივებს ვიდეოზე შენიშვნების შექმნას.
  62.  
  63. // @name:gu YouTube | Obsidian પર મોકલો
  64. // @description:gu YouTube વિડિયોમાંથી માહિતી કાઢે છે અને Obsidian (સ્થાનિક રીતે) માં નવો એન્ટ્રી બનાવે છે, વિડિયોના નોંધ બનાવવી સરળ બનાવે છે.
  65.  
  66. // @name:da YouTube | Send til Obsidian
  67. // @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.
  68.  
  69. // @name:zu YouTube | Thumela ku-Obsidian
  70. // @description:zu Ukhipha ulwazi kuvidiyo ye-YouTube bese edala irekhodi elisha ku-Obsidian (endaweni), okwenza kube lula ukudala amanothi wevidiyo.
  71.  
  72. // @name:he YouTube | שלח לאובסידיאן
  73. // @description:he שולף מידע מתוך סרטון YouTube ויוצר ערך חדש ב-Obsidian (מקומית), מה שמקל על יצירת הערות עבור סרטונים.
  74.  
  75. // @name:ig YouTube | Zipu na Obsidian
  76. // @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.
  77.  
  78. // @name:yi YouTube | שיקן צו Obsidian
  79. // @description:yi דערקלערט אינפֿאָרמאַציע פון ​​אַ יאָוטובע ווידעא און שאַפֿט אַ נייַע איינסן אין Obsidian (אָרטלעך), סימפּליפיינג די שאַפונג פון טאָן וועגן ווידעא.
  80.  
  81. // @name:id YouTube | Kirim ke Obsidian
  82. // @description:id Menarik informasi dari video YouTube dan membuat entri baru di Obsidian (lokal), menyederhanakan pembuatan catatan untuk video.
  83.  
  84. // @name:ga YouTube | Seol chuig Obsidian
  85. // @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.
  86.  
  87. // @name:is YouTube | Senda til Obsidian
  88. // @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.
  89.  
  90. // @name:es YouTube | Enviar a Obsidian
  91. // @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.
  92.  
  93. // @name:it YouTube | Invia a Obsidian
  94. // @description:it Estrae informazioni da un video YouTube e crea una nuova voce in Obsidian (localmente), semplificando la creazione di appunti sui video.
  95.  
  96. // @name:kn YouTube | Obsidian ಗೆ ಕಳುಹಿಸು
  97. // @description:kn YouTube ವೀಡಿಯೋದಿಂದ ಮಾಹಿತಿಯನ್ನು ಹೊರತೆಗೆದು Obsidian ನಲ್ಲಿ ಹೊಸ ದಾಖಲೆ ಸೃಷ್ಟಿಸುತ್ತದೆ (ಸ್ಥಳೀಯವಾಗಿ), ವೀಡಿಯೋಗಳ ಕುರಿತು ಟಿಪ್ಪಣಿಗಳನ್ನು ಸರಳಗೊಳಿಸುತ್ತದೆ.
  98.  
  99. // @name:fr YouTube | Envoyer vers Obsidian
  100. // @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.
  101.  
  102. // @name:ja YouTube | Obsidianに送信
  103. // @description:ja YouTubeビデオから情報を抽出し、Obsidianに新しいエントリを作成して、ビデオに関するノート作成を簡単にします。
  104.  
  105. // @name:ko YouTube | Obsidian으로 보내기
  106. // @description:ko YouTube 동영상에서 정보를 추출하고 Obsidian에 새 항목을 생성하여 동영상 메모 작성 작업을 단순화합니다.
  107.  
  108. // @name:pt YouTube | Enviar para o Obsidian
  109. // @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.
  110.  
  111. // @name:pl YouTube | Wyślij do Obsidian
  112. // @description:pl Wyciąga informacje z filmu YouTube i tworzy nowy wpis w Obsidian (lokalnie), ułatwiając tworzenie notatek o filmach.
  113.  
  114. // @name:fa YouTube | ارسال به Obsidian
  115. // @description:fa اطلاعات را از ویدئوی یوتیوب استخراج کرده و یک ورودی جدید در Obsidian (محلی) ایجاد می‌کند، و یادداشت‌برداری برای ویدئو را ساده‌تر می‌سازد.
  116.  
  117. // @name:ps YouTube | Obsidian ته ولیږئ
  118. // @description:ps د یوټیوب ویډیو څخه معلومات راوباسي او په Obsidian (محلي) کې نوی ریکارډ جوړوي، د ویډیو یادداشتونو جوړولو کار اسانوي.
  119.  
  120. // @name:pt-BR YouTube | Enviar para o Obsidian
  121. // @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.
  122.  
  123. // @name:pa YouTube | Obsidian ਨੂੰ ਭੇਜੋ
  124. // @description:pa YouTube ਵੀਡੀਓ ਤੋਂ ਜਾਣਕਾਰੀ ਕੱਢਦਾ ਹੈ ਅਤੇ Obsidian ਵਿੱਚ ਨਵੀਂ ਐਂਟਰੀ ਬਣਾਉਂਦਾ ਹੈ (ਸਥਾਨਕ), ਵੀਡੀਓ ਨੋਟਾਂ ਬਣਾਉਣ ਨੂੰ ਸੌਖਾ ਬਣਾਉਂਦਾ ਹੈ.
  125.  
  126. // @name:ro YouTube | Trimite în Obsidian
  127. // @description:ro Extrage informații dintr-un videoclip YouTube și creează o nouă intrare în Obsidian (local), simplificând crearea de note despre videoclip.
  128.  
  129. // @name:ru YouTube | Отправить в Obsidian
  130. // @description:ru Извлекает информацию из видеоролика на YouTube и создает новую запись в Obsidian (локально), упрощая создание заметок о видео.
  131.  
  132. // @name:sv YouTube | Skicka till Obsidian
  133. // @description:sv Extraherar information från en YouTube-video och skapar ett nytt inlägg i Obsidian (lokalt), vilket förenklar anteckningar om videon.
  134.  
  135. // @name:ta YouTube | Obsidianக்கு அனுப்பு
  136. // @description:ta YouTube வீடியோவிலிருந்து தகவலை எடுத்து Obsidian இல் புதிய பதிவை உருவாக்குகிறது (உள்ளூரில்), வீடியோவுக்கான குறிப்புகளை எளிதாக்குகிறது.
  137.  
  138. // @name:th YouTube | ส่งไปที่ Obsidian
  139. // @description:th ดึงข้อมูลจากวิดีโอ YouTube และสร้างรายการใหม่ใน Obsidian (ในเครื่อง) เพื่อช่วยให้ง่ายขึ้นในการจดบันทึกเกี่ยวกับวิดีโอ
  140.  
  141. // @name:tr YouTube | Obsidian'a Gönder
  142. // @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.
  143.  
  144. // @name:uk YouTube | Відправити в Obsidian
  145. // @description:uk Витягує інформацію з відео на YouTube і створює новий запис в Obsidian (локально), спрощуючи створення нотаток про відео.
  146.  
  147. // @name:ur YouTube | Obsidian میں بھیجیں
  148. // @description:ur یوٹیوب ویڈیو سے معلومات نکالتا ہے اور Obsidian میں ایک نیا اندراج تخلیق کرتا ہے (مقامی طور پر)، ویڈیو کے بارے میں نوٹ لینے کو آسان بناتا ہے.
  149.  
  150. // @name:uz YouTube | Obsidian-ga yuborish
  151. // @description:uz YouTube videodan ma'lumot chiqaradi va Obsidian-da yangi yozuv yaratadi (mahalliy), videoga eslatmalar yozishni osonlashtiradi.
  152.  
  153. // @name:fi YouTube | Lähetä Obsidianille
  154. // @description:fi Hakee tietoa YouTube-videosta ja luo uuden merkinnän Obsidianissa (paikallisesti), yksinkertaistaen muistiinpanojen luomista videosta.
  155.  
  156. // @name:fr YouTube | Envoyer vers Obsidian
  157. // @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.
  158.  
  159. // @name:fy YouTube | Stjoer nei Obsidian
  160. // @description:fy Ekstraheert ynformaasje fan in YouTube-fideo en makket in nije ynfier yn Obsidian (lokaal), wat it notearjen oer de fideo makliker makket.
  161.  
  162. // @name:ha YouTube | Aika zuwa Obsidian
  163. // @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.
  164.  
  165. // @name:hi YouTube | ओब्सीडियन में भेजें
  166. // @description:hi YouTube वीडियो से जानकारी निकालता है और Obsidian में एक नई प्रविष्टि बनाता है (स्थानीय रूप से), जिससे वीडियो पर नोट्स बनाना आसान हो जाता है.
  167.  
  168. // @name:hr YouTube | Pošalji u Obsidian
  169. // @description:hr Izvlači informacije iz YouTube videozapisa i stvara novi unos u Obsidianu (lokalno), olakšavajući bilježenje o videu.
  170.  
  171. // @name:cs YouTube | Odeslat do Obsidianu
  172. // @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.
  173.  
  174. // @name:sv YouTube | Skicka till Obsidian
  175. // @description:sv Extraherar information från en YouTube-video och skapar ett nytt inlägg i Obsidian (lokalt), vilket förenklar anteckningar om videon.
  176.  
  177. // @name:sn YouTube | Tumira ku Obsidian
  178. // @description:sn Inobvisa ruzivo kubva kuYouTube vhidhiyo uye inogadzira rekodhi itsva muObsidian (panzvimbo), zvichiita kuti chinyorwa nezvevhidhiyo zvive nyore kuita.
  179.  
  180. // @name:eo YouTube | Sendi al Obsidian
  181. // @description:eo Ekstraktas informojn el YouTube-video kaj kreas novan eniron en Obsidian (loke), simpligante notadon pri la video.
  182.  
  183. // @name:et YouTube | Saada Obsidiansse
  184. // @description:et Ekstraheerib teavet YouTube'i videost ja loob uue kirje Obsidians (kohapeal), muutes videot puudutavate märkmete tegemise lihtsamaks.
  185.  
  186. // @name:jv YouTube | Kirim menyang Obsidian
  187. // @description:jv Ngekstrak informasi saka video YouTube lan nggawe entri anyar ing Obsidian (lokal), nyederhanakake nggawe cathetan babagan video.
  188.  
  189. // @name:ja YouTube | Obsidianに送信
  190. // @description:ja YouTubeビデオから情報を抽出し、Obsidianに新しいエントリを作成して、ビデオに関するノート作成を簡単にします。
  191.  
  192. // @version 1.0.0
  193. // @match https://www.youtube.com/watch?*
  194. // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
  195. // @grant GM_addStyle
  196. // @noframes
  197. // @namespace https://maksymstoianov.com/
  198. // @supportURL https://maksymstoianov.com/
  199. // @contributionURL https://maksymstoianov.com/
  200. // @author Maksym Stoianov
  201. // @developer Maksym Stoianov
  202. // @license MIT
  203. // @compatible chrome
  204. // @compatible firefox
  205. // @compatible opera
  206. // @compatible safaricom
  207. // ==/UserScript==
  208.  
  209. (function () {
  210. 'use strict';
  211.  
  212.  
  213. class Obsidian {
  214.  
  215. static preloadImages(urls) {
  216. const images = [];
  217.  
  218. urls.forEach(url => {
  219. const img = new Image();
  220. img.src = url;
  221. images.push(img);
  222. });
  223. }
  224.  
  225.  
  226.  
  227. /**
  228. * @param {string} input
  229. * @returns {string}
  230. */
  231. static sanitizeTitle(input) {
  232. return (input.replace(/[:\/\\^|#]/g, ".") ?? "");
  233. }
  234.  
  235.  
  236.  
  237. static merge(message = "", fields = {}, ...args) {
  238. return message.replace(/{{([^}]+?)}}/g, (match, p1) => {
  239. try {
  240. let key, defaultValue, format;
  241.  
  242. if (p1.includes(":")) {
  243. const parts = p1
  244. .split(/(?<!\\):/)
  245. .map((part) => part.replace(/\\:/g, ":"));
  246.  
  247. // {{key:defaultValue:format}}
  248. [key, defaultValue, ...format] = parts;
  249.  
  250. format = (format.length ? format.join(":") : null);
  251.  
  252. if (typeof format === "string" && !format.length) {
  253. format = null;
  254. }
  255. } else {
  256. // {{key}}
  257. key = p1;
  258. }
  259.  
  260. // Получаем значение из fields или используем defaultValue, если значение отсутствует или пусто
  261. let value = fields[key];
  262.  
  263. if (value === undefined || value === null || value === "") {
  264. value = defaultValue ?? "";
  265. }
  266.  
  267. if (value instanceof Date) {
  268. value = this.formatDate(value, format ?? "yyyy-MM-dd");
  269. }
  270.  
  271. else if (["string", "number"].includes(typeof value)) {
  272. if (defaultValue === "" && value === "") {
  273. value = match.replace(/:/g, "");
  274. } else if (this.isNumberLike(value)) {
  275. value = Number(value);
  276. }
  277.  
  278. value = this.sprintf(format ?? "%s", value);
  279. }
  280.  
  281. else if (typeof value === "object") {
  282. value = JSON.stringify(value);
  283. }
  284.  
  285. return value;
  286. } catch (error) {
  287. console.warn(`Ошибка при обработке метки ${match}:`, error.message);
  288. }
  289.  
  290. return match;
  291. });
  292. }
  293.  
  294.  
  295.  
  296. /**
  297. * @param {string} url
  298. * @returns {boolean}
  299. */
  300. static isYouTube(url) {
  301. return (url.hostname === "www.youtube.com");
  302. }
  303.  
  304.  
  305.  
  306. /**
  307. * Отслеживает появление элемента в DOM.
  308. * @param {string} selector
  309. * @param {function} callback
  310. */
  311. static onElementInDOM(selector, callback) {
  312. if (!(typeof selector === "string" && selector.length)) {
  313. return false;
  314. }
  315.  
  316. new MutationObserver((mutationsList, observer) => {
  317. for (const mutation of mutationsList) {
  318. if (mutation.type !== "childList") continue;
  319.  
  320. mutation.addedNodes.forEach(node => {
  321. if (!(node instanceof Element)) {
  322. return;
  323. }
  324.  
  325. if (node.matches(selector) || node.querySelector(selector)) {
  326. callback.apply(this, [{
  327. selector,
  328. target: node,
  329. observer
  330. }]);
  331. }
  332. });
  333.  
  334. }
  335. }).observe(document.body, {
  336. childList: true,
  337. subtree: true
  338. });
  339.  
  340. return true;
  341. }
  342.  
  343.  
  344.  
  345. /**
  346. * Отслеживает появление элемента на экране.
  347. * @param {string} selector
  348. * @param {function} callback
  349. */
  350. static onElementVisible(selector, callback) {
  351. if (!(typeof selector === "string" && selector.length)) {
  352. return false;
  353. }
  354.  
  355. const target = document.querySelector(selector);
  356.  
  357. if (!target) {
  358. return this.onElementInDOM(selector, function () {
  359. this.onElementVisible(selector, callback);
  360. });
  361. }
  362.  
  363. new IntersectionObserver(
  364. (entries, observer) => {
  365. entries.forEach(entry => {
  366. if (!entry.isIntersecting) return;
  367.  
  368. callback.apply(this, [{
  369. selector,
  370. target: entry.target,
  371. observer
  372. }]);
  373. });
  374. },
  375. {
  376. root: null,
  377. rootMargin: "0px",
  378. threshold: 0.1
  379. }
  380. ).observe(target);
  381.  
  382. return true;
  383. }
  384.  
  385.  
  386.  
  387. static run() {
  388. if (this.isYouTube(window.location)) {
  389. new Obsidian.YouTube(window.location);
  390. }
  391. }
  392.  
  393. }
  394.  
  395.  
  396.  
  397. Obsidian.YouTube = class YouTube {
  398.  
  399. /**
  400. * @param {string} timeString
  401. * @returns {number}
  402. */
  403. static timeToSeconds(timeString) {
  404. const [minutes, seconds] = timeString
  405. .split(":")
  406. .map(Number);
  407.  
  408. return (minutes * 60 + seconds);
  409. }
  410.  
  411.  
  412.  
  413. /**
  414. * @param {string} url
  415. */
  416. constructor(url) {
  417. this.url = url;
  418.  
  419. this.elements = {
  420. video: {
  421. element: "video",
  422.  
  423. id: null,
  424. },
  425.  
  426. channel: {
  427. id: "head meta[itemprop='identifier']",
  428. url: "head link[itemprop='url']",
  429. rssUrl: "link[title='RSS'][type='application/rss+xml']",
  430. author: "ytd-channel-name a",
  431. },
  432.  
  433. segments: "#segments-container > *",
  434.  
  435. episodes: "#structured-description #shelf-container #items > *",
  436.  
  437. microformat: "#microformat script[type='application/ld+json']",
  438.  
  439. button1: "#structured-description #primary-button button",
  440. transcript: `[target-id="engagement-panel-searchable-transcript"]`,
  441. shareTargets: "#share-targets"
  442. };
  443.  
  444.  
  445. /**
  446. * Запускаем отслеживание для элемента.
  447. */
  448. Obsidian.onElementInDOM(this.elements.button1,
  449. ({ target, observer }) => {
  450. // Запрос транскрипции.
  451. target.click();
  452. observer.disconnect();
  453. }
  454. );
  455.  
  456.  
  457. /**
  458. * Запускаем отслеживание для элемента.
  459. */
  460. Obsidian.onElementVisible(this.elements.transcript,
  461. ({ target, observer }) => {
  462. // Спрятать транскрипцию.
  463. target.setAttribute(
  464. "visibility",
  465. "ENGAGEMENT_PANEL_VISIBILITY_HIDDEN"
  466. );
  467.  
  468. observer.disconnect();
  469. }
  470. );
  471.  
  472.  
  473. /**
  474. * Запускаем отслеживание для элемента.
  475. */
  476. Obsidian.onElementVisible(this.elements.shareTargets,
  477. ({ target }) => {
  478. const containerId = "obsidian-button-container";
  479.  
  480. if (document.getElementById(containerId)) {
  481. return;
  482. }
  483.  
  484. const container = document.createElement("div");
  485. container.id = containerId;
  486.  
  487. const button = document.createElement("button");
  488. button.classList.add("style-scope");
  489. button.classList.add("yt-share-target-renderer");
  490. button.onclick = () => this.createNote();
  491.  
  492. const img = document.createElement("img");
  493. img.src = "https://www.google.com/s2/favicons?sz=64&domain=obsidian.md";
  494. button.appendChild(img);
  495.  
  496. const span = document.createElement("span");
  497. span.classList.add("style-scope");
  498. span.classList.add("yt-share-target-renderer");
  499. span.setAttribute("style-targe", "title");
  500. span.textContent = "Obsidian";
  501. button.appendChild(span);
  502.  
  503. container.appendChild(button);
  504.  
  505. target
  506. .querySelector("yt-third-party-share-target-section-renderer")
  507. ?.appendChild(container);
  508. }
  509. );
  510.  
  511.  
  512. Obsidian.preloadImages([
  513. "https://www.google.com/s2/favicons?sz=64&domain=obsidian.md"
  514. ]);
  515.  
  516.  
  517. GM_addStyle(`
  518. #obsidian-button-container button {
  519. color: var(--yt-spec-text-primary);
  520. display: inline-flex;
  521. flex-direction: column;
  522. justify-content: center;
  523. align-items: center;
  524. flex-wrap: nowrap;
  525. margin: 1px 0;
  526. border: none;
  527. border-radius: 3px;
  528. padding: 5px 1px 2px;
  529. outline: none;
  530. text-align: inherit;
  531. font-family: inherit;
  532. background-color: transparent;
  533. cursor: pointer;
  534. }
  535.  
  536. #obsidian-button-container button img {
  537. display: inline-flex;
  538. align-items: center;
  539. justify-content: center;
  540. position: relative;
  541. vertical-align: middle;
  542. width: var(--iron-icon-width, 24px);
  543. height: var(--iron-icon-height, 24px);
  544. animation: var(--iron-icon-animation);
  545. padding: var(--iron-icon-padding);
  546. border-radius: 100%;
  547. --iron-icon-height: 60px;
  548. --iron-icon-width: 60px;
  549. margin-top: var(--iron-icon-margin-top);
  550. margin-left: var(--ytd-margin-base);
  551. margin-right: var(--ytd-margin-base);
  552. margin-bottom: var(--ytd-margin-2x);
  553. }
  554.  
  555. #obsidian-button-container button span {
  556. color: var(--yt-spec-text-primary);
  557. margin: auto;
  558. width: 68px;
  559. max-height: 42px;
  560. text-align: center;
  561. white-space: normal;
  562. overflow: hidden;
  563. font-family: "Roboto", "Arial", sans-serif;
  564. font-size: 1.2rem;
  565. line-height: 1.8rem;
  566. font-weight: 400;
  567. }
  568. `);
  569.  
  570. }
  571.  
  572.  
  573.  
  574. /**
  575. * @returns {string}
  576. */
  577. getId() {
  578. const searchParams = this.getUrl()?.search;
  579.  
  580. return (
  581. (searchParams
  582. ? new URLSearchParams(searchParams).get("v")
  583. : null
  584. ) ??
  585. (this.getShortLinkUrl()?.match(/\/([^\/]*)$/) ?? [])[1] ??
  586. null
  587. );
  588. }
  589.  
  590.  
  591.  
  592. /**
  593. * @returns {URL}
  594. */
  595. getUrl() {
  596. return (this.url ?? null);
  597. }
  598.  
  599.  
  600.  
  601. /**
  602. * @returns {string}
  603. */
  604. getTitle() {
  605. return (document?.title
  606. ?.replace(/\s*-\s*YouTube\s*$/, "") ?? null);
  607. }
  608.  
  609.  
  610.  
  611. /**
  612. * @returns {string}
  613. */
  614. getChannelId() {
  615. const channelUrl = (
  616. this.getChannelUrl() ??
  617. document.querySelector("#social-links #items a[href^='/channel/']").getAttribute("href")
  618. );
  619.  
  620. return (
  621. (channelUrl?.match(/channel\/([^\/]+)(\/|$)/) ?? [])[1] ??
  622. null
  623. );
  624. }
  625.  
  626.  
  627.  
  628. /**
  629. * @returns {string}
  630. */
  631. getChannelName() {
  632. const selector = this.elements?.channel?.author;
  633.  
  634. return (
  635. this.getJson().author ??
  636. (selector
  637. ? document.querySelector(selector)?.textContent?.trim()
  638. : null) ??
  639. null
  640. );
  641. }
  642.  
  643.  
  644.  
  645. /**
  646. * @returns {string}
  647. */
  648. getChannelUrl() {
  649. const selector = this.elements?.channel?.url;
  650.  
  651. return (selector
  652. ? document.querySelector(selector)?.getAttribute("href")?.trim()
  653. : null) ?? null;
  654. }
  655.  
  656.  
  657.  
  658. /**
  659. * @returns {string}
  660. */
  661. getChannelRssUrl() {
  662. let result = null;
  663. const selector = this.elements?.channel?.rssUrl;
  664.  
  665. if (selector) {
  666. result = (document.querySelector(selector)
  667. ?.getAttribute("href")
  668. ?.trim() ?? null);
  669. }
  670.  
  671. if (!result) {
  672. const channelId = this.getChannelId();
  673.  
  674. if (channelId) {
  675. result = "https://www.youtube.com/feeds/videos.xml?channel_id=" + channelId;
  676. }
  677. }
  678.  
  679. return result;
  680. }
  681.  
  682.  
  683.  
  684. /**
  685. * @returns {string}
  686. */
  687. getPublishedDate() {
  688. return (
  689. this.getJson().datePublished ??
  690. this.getMetaContent("datePublished") ??
  691. null
  692. );
  693. }
  694.  
  695.  
  696.  
  697. /**
  698. * @returns {string}
  699. */
  700. getUploadDate() {
  701. return (
  702. this.getJson().uploadDate ??
  703. this.getMetaContent("uploadDate") ??
  704. null
  705. );
  706. }
  707.  
  708.  
  709.  
  710. /**
  711. * @returns {string}
  712. */
  713. getDate() {
  714. return (
  715. (
  716. (
  717. this.getPublishedDate() ??
  718. this.getUploadDate()
  719. )?.split("T") ??
  720. []
  721. )[0] ??
  722. null
  723. );
  724. }
  725.  
  726.  
  727.  
  728. /**
  729. * @returns {string[]}
  730. */
  731. getKeywords() {
  732. const keywords = this.getMetaContent("keywords");
  733.  
  734. if (!keywords) {
  735. return [];
  736. }
  737.  
  738. const regex = /\s*("[^"]+"|'[^']+'|[^, ]+)\s*,?\s*/g;
  739. const matches = [];
  740. let match;
  741.  
  742. while ((match = regex.exec(keywords)) !== null) {
  743. // Убираем кавычки с начала и конца, если они есть
  744. matches.push(match[1].replace(/^["']|["']$/g, ""));
  745. }
  746.  
  747. // Проверка последнего элемента на троеточие
  748. if (matches[matches.length - 1]?.endsWith("...")) {
  749. matches.pop();
  750. }
  751.  
  752. return matches;
  753. }
  754.  
  755.  
  756.  
  757. /**
  758. * @returns {string}
  759. */
  760. getShortLinkUrl() {
  761. return (this.getMetaContent("shortlinkUrl") ?? null);
  762. }
  763.  
  764.  
  765.  
  766. /**
  767. * @returns {string}
  768. */
  769. getCategory() {
  770. return (
  771. this.getJson().genre ??
  772. this.getMetaContent("genre") ??
  773. null
  774. );
  775. }
  776.  
  777.  
  778.  
  779. /**
  780. * @returns {string}
  781. */
  782. getDescription() {
  783. return (
  784. this.getJson().description ??
  785. this.getMetaContent("description") ??
  786. null
  787. );
  788. }
  789.  
  790.  
  791.  
  792. /**
  793. * @param {boolean} flag
  794. * - `true` – Array
  795. * - `false` – String
  796. * @returns {(string[]|string)}
  797. */
  798. getEpisodes(flag) {
  799. let values = [];
  800.  
  801. const selector = this.elements?.episodes;
  802.  
  803. if (!selector) {
  804. return null;
  805. }
  806.  
  807. document
  808. .querySelectorAll(selector)
  809. ?.forEach(element => {
  810. try {
  811. const result = {
  812. level: 0,
  813. time: null,
  814. url: null,
  815. text: null
  816. };
  817.  
  818. result.time = element
  819. ?.querySelector("#details #time")
  820. ?.textContent
  821. ?.trim() ?? "";
  822.  
  823. result.url = "https://www.youtube.com/watch?"
  824. + "&v=" + this.getId()
  825. + "&t=" + this.constructor.timeToSeconds(result.time);
  826.  
  827. result.text = element
  828. ?.querySelector("#details h4.macro-markers")
  829. ?.textContent
  830. ?.trim() ?? "";
  831.  
  832. result.episode = new Obsidian.YouTube.Episode(result);
  833.  
  834. values.push(episode);
  835. } catch (error) {
  836. console.warn(error.message);
  837. }
  838. });
  839.  
  840. if (!values.length) {
  841. return null;
  842. }
  843.  
  844. if (flag !== true) {
  845. return "\n## Episodes\n" + values
  846. .map(item => item.toString())
  847. .join("\n");
  848. }
  849.  
  850. return values;
  851. }
  852.  
  853.  
  854.  
  855. /**
  856. * @param {boolean} flag
  857. * - `true` – Array
  858. * - `false` – String
  859. * @returns {(string[]|string)}
  860. */
  861. getTranscript(flag) {
  862. let values = [];
  863.  
  864. const selector = this.elements?.segments;
  865.  
  866. if (!selector) {
  867. return null;
  868. }
  869.  
  870. const episodes = this.getEpisodes(true);
  871.  
  872. document.querySelectorAll(selector)
  873. ?.forEach(element => {
  874. try {
  875. const result = {
  876. level: 0,
  877. time: null,
  878. url: null,
  879. text: null
  880. };
  881.  
  882. if (element.hasAttribute("rounded-container")) {
  883. result.level = 0;
  884.  
  885. result.time = element
  886. ?.querySelector(".segment-timestamp")
  887. ?.textContent
  888. ?.trim() ?? "";
  889.  
  890. result.url = "https://www.youtube.com/watch?"
  891. + "&v=" + this.getId()
  892. + "&t=" + this.constructor.timeToSeconds(result.time);
  893.  
  894. result.text = element
  895. ?.querySelector(".segment-text")
  896. ?.textContent
  897. ?.trim() ?? "";
  898. } else {
  899. /* Эпизоды (заголовки) */
  900. result.level = 3;
  901.  
  902. result.text = element
  903. ?.querySelector("h2")
  904. ?.textContent
  905. ?.trim() ?? "";
  906.  
  907. const episode = (episodes ?? [])
  908. .find(item => item.text === result.text) ?? {};
  909.  
  910. result.time = episode.time;
  911. result.url = episode.url;
  912. }
  913.  
  914. const transcript = new Obsidian.YouTube.Transcript(result);
  915.  
  916. values.push(transcript);
  917. } catch (error) {
  918. console.warn(error.message);
  919. }
  920. });
  921.  
  922. if (!values.length) {
  923. return null;
  924. }
  925.  
  926. if (flag !== true) {
  927. return "\n## Transcript\n" + values
  928. .map(item => item.toString())
  929. .join("\n");
  930. }
  931.  
  932. return values;
  933. }
  934.  
  935.  
  936.  
  937. /**
  938. * @returns {string}
  939. */
  940. getMetaContent(input) {
  941. return document
  942. ?.querySelector("meta[itemprop='" + input + "'], meta[name='" + input + "']")
  943. ?.getAttribute("content")
  944. ?.trim() ?? null;
  945. }
  946.  
  947.  
  948.  
  949. /**
  950. * @returns {object}
  951. */
  952. getJson() {
  953. const selector = this.elements?.microformat;
  954. if (!selector) return {};
  955.  
  956. let values = document
  957. .querySelector(selector)
  958. ?.textContent;
  959.  
  960. try {
  961. values = (values ? JSON.parse(values) : {});
  962. } catch (error) { }
  963.  
  964. return (values !== null && typeof values === "object" ? values : {});
  965. }
  966.  
  967.  
  968.  
  969. /**
  970. * @returns {string}
  971. */
  972. getObsidianUrl() {
  973. const videoId = this.getId();
  974.  
  975. if (!videoId) {
  976. return;
  977. }
  978.  
  979. if (this.elements?.video?.element?.paused) {
  980. this.elements.video.element.pause();
  981. }
  982.  
  983. const _escape = input => (input ?? "")
  984. .replace(/"/g, '\\"');
  985.  
  986. const url = this.getUrl();
  987. const title = this.getTitle();
  988. const date = this.getDate();
  989. const publishedDate = this.getPublishedDate();
  990. const uploadDate = this.getUploadDate();
  991. const channelName = this.getChannelName();
  992. const keywords = this.getKeywords();
  993. const tags = [
  994. "Video",
  995. "YouTube"
  996. ];
  997.  
  998. const path = [
  999. "RSS",
  1000. encodeURIComponent(Obsidian.sanitizeTitle(channelName ?? "")),
  1001. "YouTube",
  1002. encodeURIComponent((date ?? "") + " " + Obsidian.sanitizeTitle(title ?? videoId ?? "").trim() + ".md")
  1003. ].join("/");
  1004.  
  1005. const content = [
  1006. "---",
  1007. `media_link: ${url}`,
  1008. `channel: "${_escape(channelName ?? "")}"`,
  1009. `category: "${_escape(this.getCategory() ?? "")}"`,
  1010. "published_date: " + (publishedDate ?? ""),
  1011. "upload_date: " + (uploadDate ?? ""),
  1012. (keywords.length
  1013. ? "keywords:\n" + keywords
  1014. .map(item => ` - "${_escape(item)}"`)
  1015. .join("\n") + "\n"
  1016. : ""),
  1017. (tags.length
  1018. ? "tags:\n" + tags
  1019. .map(item => ` - "${_escape(item)}"`)
  1020. .join("\n") + "\n"
  1021. : ""),
  1022. `rss_link: ${this.getChannelRssUrl() ?? ""}`,
  1023. "---",
  1024. `# ${title ?? ""}`,
  1025. `\n## Description`,
  1026. `${this.getDescription() ?? ""}`,
  1027. (this.getTranscript() ?? this.getEpisodes() ?? "")
  1028. ].join("\n");
  1029.  
  1030. return `obsidian://new?file=${path}&content=${encodeURIComponent(content)}`;
  1031. }
  1032.  
  1033.  
  1034.  
  1035. /**
  1036. * @returns {string}
  1037. */
  1038. createNote() {
  1039. return window.open(this.getObsidianUrl());
  1040. }
  1041.  
  1042. };
  1043.  
  1044.  
  1045.  
  1046. Obsidian.YouTube.Episode = class Episode {
  1047.  
  1048. constructor({ level, time, url, text }) {
  1049. this.level = (level ?? 0);
  1050. this.time = (time ?? null);
  1051. this.url = (url ?? null);
  1052. this.text = (text ?? null);
  1053. }
  1054.  
  1055.  
  1056.  
  1057. toString() {
  1058. return `${this.level > 0 ? "#".repeat(this.level) : "-"} [${this.time}](${this.url ?? "#"}) ${this.text}`;
  1059. }
  1060.  
  1061. };
  1062.  
  1063.  
  1064.  
  1065. Obsidian.YouTube.Transcript = class Transcript {
  1066.  
  1067. constructor({ level, time, url, text }) {
  1068. this.level = (level ?? 0);
  1069. this.time = (time ?? null);
  1070. this.url = (url ?? null);
  1071. this.text = (text ?? null);
  1072. }
  1073.  
  1074.  
  1075.  
  1076. toString() {
  1077. return `${this.level > 0 ? "#".repeat(this.level) : "-"} [${this.time}](${this.url ?? "#"}) ${this.text}`;
  1078. }
  1079.  
  1080. };
  1081.  
  1082.  
  1083.  
  1084. Obsidian.run();
  1085. })();