// ==UserScript== // @name Youtube 双语字幕下载 v14 (中文+任选的一门双语,比如英语) (v13 开始自动字幕不再支持双语,自动字幕只能下载翻译后的中文) // @include https://*youtube.com/* // @author Cheng Zheng // @version 14 // @copyright Zheng Cheng // @grant GM_xmlhttpRequest // @grant unsafeWindow // @description 字幕格式是 "中文 \n 英语"(\n 是换行符的意思) // @license MIT // @namespace https://greasyfork.org/users/5711 // @require https://code.jquery.com/jquery-1.12.4.min.js // @require https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js // @downloadURL none // ==/UserScript== /* 使用提示: 如果本脚本不能使用,有一定概率是因为 jQuery 的 CDN 在你的网络环境下无法载入, 就是文件顶部的这一句有问题: @require https://code.jquery.com/jquery-1.12.4.min.js 解决办法:去网上随便找一个 jQuery 的 CDN 地址,替换掉这个,比如 https://cdn.bootcdn.net/ajax/libs/jquery/1.12.4/jquery.js https://cdn.staticfile.org/jquery/1.12.4/jquery.min.js 作者联系方式: QQ 1003211008 邮件 guokrfans@gmail.com Github@1c7 本脚本使用场景: Tampermonkey (Chrome 上的一款插件) (意思是需要安装在 Tampermonkey 里来运行) 解决什么问题: 下载中外双语的字幕,格式是 中文 \n 外语, \n 是换行符的意思。 术语说明: auto 自动字幕 closed 完整字幕 (或者叫人工字幕也可以) 原理说明: 对于"完整字幕", Youtube 返回的时间轴完全一致,因此只需要结合在一起即可,相对比较简单。 对于"自动字幕",中文是一个个句子,英文是一个个单词,格式不同,时间轴也不同 因此,会基于中文的句子时间(时间轴),把英文放进去 特别感谢: ytian(tianyebj):解决英文字幕匹配错误的问题 (https://github.com/1c7/Youtube-Auto-Subtitle-Download/pull/11) 备忘: 如果要把字符串保存下来, 使用: downloadString(srt_string, "text/plain", file_name); 用于测试的视频: https://www.youtube.com/watch?v=JfBZfnkg1uM 版本更新日志 2022-12-23 把 v13 升级到 v14,因为 Youtube 又更新 UI 了。 2021-12-21 把 v12 升级到 v13 有多名用户分别在微博,微信,邮件,greasyfork, 四个渠道向我反馈了无法使用问题。 此时版本是 v12 测试视频1 https://www.youtube.com/watch?v=OWaFPsVa3ig&t=16s (有1个字幕,英语(自动生成)) 绿色下拉菜单里选择 option "中文 + 英语 (自动生成)" 的确没有任何反应,无法下载。 实测发现: 英文字幕长度 284行,中文字幕长度 162行,没法一一对应,所以出错了。 测试视频2 https://www.youtube.com/watch?v=3RkhZgRNC1k (有2个字幕,英语(美国),英语(自动生成)) 如果是中文+英语(美国) 那么两个都是885行,可以正常下载 但是 中文+英语(自动生成),英语是937行,中文是471行。 结论: 如果是中文+完整字幕,那么长度是一一对应的。没问题 如果是中文+自动生成字幕,那么长度不一样,就会有问题。 解决办法: 如果是自动字幕,只下载中文。 完整字幕不需要做额外修改,保留现在这样就行,可以正常工作。 */ ; (function () { 'use strict' // Config var NO_SUBTITLE = '无字幕' var HAVE_SUBTITLE = '下载双语字幕 (中文+外语)' const NEW_LINE = '\n' const BUTTON_ID = 'youtube-dual-lang-downloader-by-1c7-last-update-2020-12-3' const anchor_element = "#above-the-fold #title"; // Config var HASH_BUTTON_ID = `#${BUTTON_ID}` // initialize var first_load = true // indicate if first load this webpage or not var youtube_playerResponse_1c7 = null // for auto subtitle unsafeWindow.caption_array = [] // store all subtitle // trigger when first load // $(document).ready(function () { // console.log('开始执行??'); // start() // }) // trigger when loading new page // (actually this would also trigger when first loading, that's not what we want, that's why we need to use firsr_load === false) // (new Material design version would trigger this "yt-navigate-finish" event. old version would not.) var body = document.getElementsByTagName('body')[0] body.addEventListener('yt-navigate-finish', function (event) { if (current_page_is_video_page() === false) { return } youtube_playerResponse_1c7 = event.detail.response.playerResponse // for auto subtitle unsafeWindow.caption_array = [] // clean up (important, otherwise would have more and more item and cause error) // if use click to another page, init again to get correct subtitle if (first_load === false) { remove_subtitle_download_button() init() } }) // trigger when loading new page // (old version would trigger "spfdone" event. new Material design version not sure yet.) window.addEventListener('spfdone', function (e) { if (current_page_is_video_page()) { remove_subtitle_download_button() var checkExist = setInterval(function () { if ($('#watch7-headline').length) { init() clearInterval(checkExist) } }, 330) } }) // return true / false // Detect [new version UI(material design)] OR [old version UI] // I tested this, accurated. function new_material_design_version() { var old_title_element = document.getElementById('watch7-headline') if (old_title_element) { return false } else { return true } } // return true / false function current_page_is_video_page() { return get_video_id() !== null } // return string like "RW1ChiWyiZQ", from "https://www.youtube.com/watch?v=RW1ChiWyiZQ" // or null function get_video_id() { return getURLParameter('v') } //https://stackoverflow.com/questions/11582512/how-to-get-url-parameters-with-javascript/11582513#11582513 function getURLParameter(name) { return ( decodeURIComponent( (new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec( location.search ) || [null, ''])[1].replace(/\+/g, '%20') ) || null ) } function remove_subtitle_download_button() { $(HASH_BUTTON_ID).remove() } // 把 console.log 包装一层, 方便"开启"/"关闭" // 这样可以在代码里遗留很多 console.log,实际运行时"关闭"掉不输出, 调试时"开启" // function logging(...args) { // if(typeof(console) !== 'undefined') { // console.log(...args); // } // } function inject_our_script() { var div = document.createElement('div'), select = document.createElement('select'), option = document.createElement('option'); div.setAttribute( 'style', `display: table; margin-top:4px; border: 1px solid rgb(0, 183, 90); cursor: pointer; color: rgb(255, 255, 255); border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; background-color: #00B75A; ` ) div.id = BUTTON_ID div.title = 'Youtube Subtitle Downloader' // display when cursor hover select.id = 'captions_selector' select.disabled = true select.setAttribute( 'style', `display:block; border: 1px solid rgb(0, 183, 90); cursor: pointer; color: rgb(255, 255, 255); background-color: #00B75A; padding: 4px; ` ) option.textContent = 'Loading...' option.selected = true select.appendChild(option) // 下拉菜单里,选择一项后触发下载 select.addEventListener( 'change', function () { download_subtitle(this) }, false ) div.appendChild(select) // put // 加载语言列表 function load_language_list(select) { // auto var auto_subtitle_exist = false // 自动字幕是否存在(默认 false) // closed var closed_subtitle_exist = false // get auto subtitle var auto_subtitle_url = get_auto_subtitle_xml_url() if (auto_subtitle_url != false) { auto_subtitle_exist = true } // if there are "closed" subtitle? var captionTracks = get_captionTracks() if ( captionTracks != undefined && typeof captionTracks === 'object' && captionTracks.length > 0 ) { closed_subtitle_exist = true } // if no subtitle at all, just say no and stop if (auto_subtitle_exist == false && closed_subtitle_exist == false) { select.options[0].textContent = NO_SUBTITLE disable_download_button() return false } // if at least one type of subtitle exist select.options[0].textContent = HAVE_SUBTITLE select.disabled = false var option = null // for