Class101 Subtitle Downloader

Download vtt subtitle from class101.net

  1. // ==UserScript==
  2. // @name Class101 Subtitle Downloader
  3. // @name:ko 클래스101 자막 다운로더
  4. // @namespace http://tampermonkey.net/
  5. // @version 1.4
  6. // @description Download vtt subtitle from class101.net
  7. // @description:ko class101.net에서 vtt자막 다운로드
  8. // @author Ravenclaw5874
  9. // @match https://class101.net/*
  10. // @icon https://www.google.com/s2/favicons?sz=64&domain=class101.net
  11. // @grant GM_registerMenuCommand
  12. // @license MIT License
  13. // ==/UserScript==
  14.  
  15. //현재 요소가 몇번째 자식인지 알려줌.
  16. function getIndex(element, childs = element.parentNode.childNodes) {
  17. for (let i = 0; i < childs.length; i++) {
  18. if (childs[i] === element) {
  19. return i;
  20. }
  21. }
  22. }
  23.  
  24. //Xpath로 요소 찾기 확장형
  25. Node.prototype.xpath = function (xpath) {
  26. return document.evaluate(xpath, this, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
  27. }
  28.  
  29. function findFirstUniqueValue(arr) {
  30. // 객체를 사용하여 각 값의 빈도수를 카운트합니다.
  31. const frequency = arr.reduce((acc, val) => {
  32. acc[val] = (acc[val] || 0) + 1;
  33. return acc;
  34. }, {});
  35.  
  36. // 첫 번째로 빈도수가 1인 값을 반환합니다.
  37. for (let i = 0; i < arr.length; i++) {
  38. if (frequency[arr[i]] === 1) {
  39. return arr[i];
  40. }
  41. }
  42.  
  43. // 유일한 값이 없는 경우 undefined를 반환합니다.
  44. return undefined;
  45. }
  46.  
  47. //className을 포함하는 부모를 올라가면서 찾기
  48. function findParentWithClassName(element, className, nth = 1) {
  49. let parent = element.parentElement;
  50. let count = 1;
  51.  
  52. while (parent) {
  53. if (parent.classList.contains(className)) {
  54. // 원하는 className을 가진 부모를 찾았습니다.
  55. if (nth === count) { return parent; } // n번째 className 부모
  56. count++; // n번째 부모가 아님.
  57. }
  58. parent = parent.parentElement;
  59. }
  60.  
  61. // 원하는 className을 가진 부모를 찾지 못했습니다.
  62. return null;
  63. }
  64.  
  65. //파일 이름을 결정. "0101 안녕하세요"
  66. function makeFilename() {
  67. const css_array = document.xpath("//p[text()='CHAPTER 1']").parentNode.parentNode.parentNode.className;
  68. const css_current = findFirstUniqueValue(Array.from(document.querySelectorAll(`div.${css_array} > button > div`)).map(e => e.className));
  69.  
  70. const current = document.querySelector(`button > div.${css_current}`)
  71. const chapter = findParentWithClassName(current, css_array, 2);
  72. const chapter_name = chapter.querySelector("p").textContent;
  73. const chapters = chapter.parentNode.querySelectorAll(`:scope > div.${chapter.className}`);
  74.  
  75. const mainIndex = getIndex(chapter, chapters).toString().padStart(2, '0');
  76. const subIndex = (getIndex(current.parentNode) + 1).toString().padStart(2, '0');
  77. const title = current.querySelector("div > div:nth-child(1) > div:nth-child(1) > span").innerText;
  78.  
  79. const filename = `${mainIndex}${subIndex} ${title}`;
  80.  
  81. return filename;
  82. }
  83.  
  84. //자막 다운로드
  85. async function downloadSubtitle() {
  86. const lang = document.documentElement.getAttribute('lang');
  87. const filename = makeFilename();
  88. const url = document.querySelector(`track[srclang=${lang}]`).src;
  89.  
  90. try {
  91. const response = await fetch(url);
  92.  
  93. if (response.status !== 200) {
  94. throw new Error(`Unable to download file. HTTP status: ${response.status}`);
  95. }
  96. const blob = await response.blob();
  97.  
  98. const link = document.createElement('a');
  99. link.href = URL.createObjectURL(blob);
  100. link.download = `${filename}.vtt`;
  101.  
  102. link.click()
  103. }
  104. catch (error) {
  105. console.error('error:', error.message);
  106. }
  107. }
  108.  
  109. (function() {
  110. 'use strict';
  111. // Your code here...
  112. const lang = document.documentElement.getAttribute('lang');
  113. const commandName = navigator.language === 'ko'? '자막 다운로드': 'Download Subtitle';
  114. GM_registerMenuCommand(commandName, downloadSubtitle)
  115.  
  116. })();