├── README.md ├── snippet ├── Hulu-Dualsub.snippet ├── Netflix-Dualsub.snippet ├── YouTube-Dualsub.snippet ├── ParamountPlus-Dualsub.snippet ├── HBO-Max-Dualsub.snippet ├── DisneyPlus-Dualsub.snippet ├── Prime-Video-Dualsub.snippet └── Dualsub.snippet ├── Douban.js └── Dualsub.js /README.md: -------------------------------------------------------------------------------- 1 | # Quantumult-X -------------------------------------------------------------------------------- /snippet/Hulu-Dualsub.snippet: -------------------------------------------------------------------------------- 1 | # Hulu Dualsub 2 | # Hulu subtitles add-ons 3 | 4 | hostname = *.huluim.com 5 | 6 | ^http.+huluim.com\/.+\.vtt$ url script-response-body https://raw.githubusercontent.com/Neurogram-R/Quantumult-X/main/Dualsub.js 7 | https:\/\/setting.huluim.com\/\?action=(g|s)et url script-analyze-echo-response https://raw.githubusercontent.com/Neurogram-R/Quantumult-X/main/Dualsub.js 8 | -------------------------------------------------------------------------------- /snippet/Netflix-Dualsub.snippet: -------------------------------------------------------------------------------- 1 | # Netflix Dualsub 2 | # Netflix subtitles add-ons 3 | 4 | hostname = *.nflxvideo.net 5 | 6 | https:\/\/.+nflxvideo.net\/\?o=\d+&v=\d+&e=.+ url script-response-body https://raw.githubusercontent.com/Neurogram-R/Quantumult-X/main/Dualsub.js 7 | https:\/\/setting.nflxvideo.net\/\?action=(g|s)et url script-analyze-echo-response https://raw.githubusercontent.com/Neurogram-R/Quantumult-X/main/Dualsub.js -------------------------------------------------------------------------------- /snippet/YouTube-Dualsub.snippet: -------------------------------------------------------------------------------- 1 | # Youtube Subtrans 2 | # Youtube subtitles add-ons 3 | 4 | hostname = *.youtube.com 5 | 6 | https:\/\/www.youtube.com\/api\/timedtext.+ url script-response-body https://raw.githubusercontent.com/Neurogram-R/Quantumult-X/main/Dualsub.js 7 | https:\/\/setting.youtube.com\/\?action=(g|s)et url script-analyze-echo-response https://raw.githubusercontent.com/Neurogram-R/Quantumult-X/main/Dualsub.js 8 | -------------------------------------------------------------------------------- /snippet/ParamountPlus-Dualsub.snippet: -------------------------------------------------------------------------------- 1 | # ParamountPlus Dualsub 2 | # Paramount+ subtitles add-ons 3 | 4 | hostname = *.cbsaavideo.com, *.cbsivideo.com 5 | 6 | https:\/\/.+cbs(aa|i)video.com\/.+\.vtt(\?m=\d+)* url script-response-body https://raw.githubusercontent.com/Neurogram-R/Quantumult-X/main/Dualsub.js 7 | https:\/\/setting.cbsivideo.com\/\?action=(g|s)et url script-analyze-echo-response https://raw.githubusercontent.com/Neurogram-R/Quantumult-X/main/Dualsub.js -------------------------------------------------------------------------------- /snippet/HBO-Max-Dualsub.snippet: -------------------------------------------------------------------------------- 1 | # HBO Max Dualsub 2 | # HBO Max subtitles add-ons 3 | 4 | hostname = *.api.hbo.com, *.hbomaxcdn.com 5 | 6 | https:\/\/(manifests.v2.api.hbo.com|.+hbomaxcdn.com)\/(hls.m3u8.+|video.+\.vtt) url script-response-body https://raw.githubusercontent.com/Neurogram-R/Quantumult-X/main/Dualsub.js 7 | https:\/\/setting.hbomaxcdn.com\/\?action=(g|s)et url script-analyze-echo-response https://raw.githubusercontent.com/Neurogram-R/Quantumult-X/main/Dualsub.js -------------------------------------------------------------------------------- /snippet/DisneyPlus-Dualsub.snippet: -------------------------------------------------------------------------------- 1 | # DisneyPlus Dualsub 2 | # Disney+, Star+ subtitles add-ons 3 | 4 | hostname = *.media.dssott.com, *.media.starott.com 5 | 6 | https:\/\/.+media.(dss|star)ott.com\/ps01\/disney\/.+(\.vtt|-all-.+\.m3u8.*) url script-response-body https://raw.githubusercontent.com/Neurogram-R/Quantumult-X/main/Dualsub.js 7 | https:\/\/setting.media.dssott.com\/\?action=(g|s)et url script-analyze-echo-response https://raw.githubusercontent.com/Neurogram-R/Quantumult-X/main/Dualsub.js -------------------------------------------------------------------------------- /snippet/Prime-Video-Dualsub.snippet: -------------------------------------------------------------------------------- 1 | # Prime Video Dualsub 2 | # Prime Video subtitles add-ons 3 | 4 | hostname = *.cloudfront.net, *.akamaihd.net, *.avi-cdn.net 5 | 6 | https:\/\/.+(cloudfront|akamaihd|avi-cdn).net\/(.+\.vtt|\w+\/2\$.+\/[a-zA-Z0-9-]+\.m3u8) url script-response-body https://raw.githubusercontent.com/Neurogram-R/Quantumult-X/main/Dualsub.js 7 | https:\/\/setting.cloudfront.net\/\?action=(g|s)et url script-analyze-echo-response https://raw.githubusercontent.com/Neurogram-R/Quantumult-X/main/Dualsub.js -------------------------------------------------------------------------------- /snippet/Dualsub.snippet: -------------------------------------------------------------------------------- 1 | # Dualsub 2 | # Disney+, Star+, HBO Max, Hulu, Netflix, Paramount+, Prime Video, YouTube, etc. subtitles add-ons 3 | 4 | hostname = *.media.dssott.com, *.media.starott.com, *.api.hbo.com, *.hbomaxcdn.com, *.huluim.com, *.nflxvideo.net, *.cbsaavideo.com, *.cbsivideo.com, *.cloudfront.net, *.akamaihd.net, *.avi-cdn.net, *.youtube.com 5 | 6 | ^http.+(media.(dss|star)ott|manifests.v2.api.hbo|hbomaxcdn|nflxvideo|cbs(aa|i)video|cloudfront|akamaihd|avi-cdn|huluim|youtube).(com|net)\/(.+\.vtt($|\?m=\d+)|.+-all-.+\.m3u8.*|hls\.m3u8.+|\?o=\d+&v=\d+&e=.+|\w+\/2\$.+\/[a-zA-Z0-9-]+\.m3u8|api\/timedtext.+) url script-response-body https://raw.githubusercontent.com/Neurogram-R/Quantumult-X/main/Dualsub.js 7 | ^http.+(setting|general).(media.dssott|hbomaxcdn|nflxvideo|youtube|cbsivideo|cloudfront|huluim).(com|net)\/\?action=(g|s)et url script-analyze-echo-response https://raw.githubusercontent.com/Neurogram-R/Quantumult-X/main/Dualsub.js 8 | 9 | -------------------------------------------------------------------------------- /Douban.js: -------------------------------------------------------------------------------- 1 | /* 2 | Douban Movie Add-ons for Quantumult X by Neurogram 3 | 4 | - 豆瓣电影网页插件 5 | - 快捷跳转自定义网站搜索 6 | - 展示在映流媒体平台(TMDB API) 7 | 8 | 使用说明 9 | 10 | [rewrite_local] 11 | https:\/\/m(ovie)*\.douban\.com\/(movie\/)*subject\/.+ url script-response-body Douban.js 12 | 13 | [MITM] 14 | hostname = m.douban.com, movie.douban.com 15 | 16 | Author: 17 | Telegram: Neurogram 18 | GitHub: Neurogram-R 19 | */ 20 | 21 | 22 | const url = $request.url 23 | const movieId = url.match(/subject\/(\d+)/)?.[1] 24 | const platform = url.includes('movie.douban.com') ? 'web' : 'mobile' 25 | 26 | const tmdb_region = 'US' // TMDB 查询区域 27 | const tmdb_api_key = '' // TMDB API Key 28 | 29 | // 可自定义添加网站搜索(格式:['名称', '搜索链接', '图标链接'],%@ 代表电影标题) 30 | const watch_web_data = [ 31 | ['247看', 'https://247kan.com/search?q=%@', 'https://247kan.com/favicon.ico'], 32 | ['Cupfox', 'https://www.cupfox.in/search?q=%@', 'https://picx.zhimg.com/80/v2-de36e385e59fcca2df694b76f108431a.png'], 33 | ['LIBIVO', 'https://www.libvio.fun/search/-------------.html?wd=%@', 'https://www.libvio.fun/statics/img/favicon.ico'] 34 | ] 35 | 36 | function send_request(options) { 37 | return new Promise((resolve, reject) => { 38 | $task.fetch(options).then(response => { 39 | resolve(JSON.parse(response.body)) 40 | }) 41 | }) 42 | } 43 | 44 | async function douban_addons() { 45 | 46 | let body = $response.body 47 | const title = body.match(/"sub-title">([^<]+)/)?.[1] ?? body.match(/(.+)?的剧情简介<\/i>/)?.[1] 48 | 49 | if (!title) $done({}) 50 | 51 | if (tmdb_api_key) { 52 | 53 | const douban_result = await send_request({ 54 | url: `https://frodo.douban.com/api/v2/movie/${movieId}?apiKey=0ac44ae016490db2204ce0a042db2916`, 55 | method: "GET", 56 | headers: { 57 | 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.3(0x18000323) NetType/WIFI Language/en', 58 | 'Referer': 'https://servicewechat.com/wx2f9b06c1de1ccfca/82/page-frame.html' 59 | } 60 | }) 61 | 62 | if (['movie', 'tv'].includes(douban_result.type) && douban_result.original_title) { 63 | 64 | const tmdb_query = await send_request({ 65 | url: `https://api.themoviedb.org/3/search/${douban_result.type}?api_key=${tmdb_api_key}&query=${encodeURIComponent(douban_result.original_title.replace(/Season \d+$/, ''))}&page=1`, 66 | method: "GET" 67 | }) 68 | 69 | if (tmdb_query.results[0]) { 70 | 71 | const tmdb_providers = await send_request({ 72 | url: `https://api.themoviedb.org/3/${douban_result.type}/${tmdb_query.results[0].id}/watch/providers?api_key=${tmdb_api_key}`, 73 | method: "GET" 74 | }) 75 | 76 | if (tmdb_providers.results[tmdb_region]?.flatrate) { 77 | 78 | for (const provider of tmdb_providers.results[tmdb_region].flatrate) { 79 | watch_web_data.push([provider.provider_name, '', `https://image.tmdb.org/t/p/original${provider.logo_path}`]) 80 | } 81 | 82 | } 83 | } 84 | 85 | } 86 | 87 | } 88 | 89 | const html_data = [] 90 | 91 | for (let i = 0; i < watch_web_data.length; i++) { 92 | html_data.push(``) 93 | } 94 | 95 | if (platform == 'web') body = body.replace(/((.|\n)+?)<\/h1>/, `$1${html_data.join('\n')}$2`) 96 | if (platform == 'mobile') body = body.replace(/("sub-title">.+?)(<\/div>)/, `$1
${html_data.join('\n')}$2`) 97 | 98 | $done({ body }) 99 | 100 | } 101 | 102 | douban_addons() 103 | -------------------------------------------------------------------------------- /Dualsub.js: -------------------------------------------------------------------------------- 1 | /* 2 | Dualsub for Quantumult X by Neurogram 3 | 4 | - Disney+, Star+, HBO Max, Prime Video, YouTube official bilingual subtitles 5 | - Disney+, Star+, HBO Max, Hulu, Netflix, Paramount+, Prime Video, etc. external subtitles 6 | - Disney+, Star+, HBO Max, Hulu, Netflix, Paramount+, Prime Video, etc. machine translation bilingual subtitles (Google, DeepL) 7 | - Customized language support 8 | 9 | Manual: 10 | Setting tool for Shortcuts: https://www.icloud.com/shortcuts/8ec4a2a3af514282bf27a11050f39fc2 11 | 12 | Quantumult X: 13 | 14 | [rewrite_local] 15 | 16 | // All in one 17 | ^http.+(media.(dss|star)ott|manifests.v2.api.hbo|hbomaxcdn|nflxvideo|cbs(aa|i)video|cloudfront|akamaihd|avi-cdn|huluim|youtube).(com|net)\/(.+\.vtt($|\?m=\d+)|.+-all-.+\.m3u8.*|hls\.m3u8.+|\?o=\d+&v=\d+&e=.+|\w+\/2\$.+\/[a-zA-Z0-9-]+\.m3u8|api\/timedtext.+) url script-response-body Dualsub.js 18 | ^http.+(setting|general).(media.dssott|hbomaxcdn|nflxvideo|youtube|cbsivideo|cloudfront|huluim).(com|net)\/\?action=(g|s)et url script-analyze-echo-response Dualsub.js 19 | 20 | // Disney+, Star+ individual 21 | https:\/\/.+media.(dss|star)ott.com\/ps01\/disney\/.+(\.vtt|-all-.+\.m3u8.*) url script-response-body Dualsub.js 22 | https:\/\/setting.media.dssott.com\/\?action=(g|s)et url script-analyze-echo-response Dualsub.js 23 | 24 | // HBO Max individual 25 | https:\/\/(manifests.v2.api.hbo.com|.+hbomaxcdn.com)\/(hls.m3u8.+|video.+\.vtt) url script-response-body Dualsub.js 26 | https:\/\/setting.hbomaxcdn.com\/\?action=(g|s)et url script-analyze-echo-response Dualsub.js 27 | 28 | //Hulu individual 29 | ^http.+huluim.com\/.+\.vtt$ url script-response-body Dualsub.js 30 | https:\/\/setting.huluim.com\/\?action=(g|s)et url script-analyze-echo-response Dualsub.js 31 | 32 | // Netflix individual 33 | https:\/\/.+nflxvideo.net\/\?o=\d+&v=\d+&e=.+ url script-response-body Dualsub.js 34 | https:\/\/setting.nflxvideo.net\/\?action=(g|s)et url script-analyze-echo-response Dualsub.js 35 | 36 | // Paramount+ individual 37 | https:\/\/.+cbs(aa|i)video.com\/.+\.vtt(\?m=\d+)* url script-response-body Dualsub.js 38 | https:\/\/setting.cbsivideo.com\/\?action=(g|s)et url script-analyze-echo-response Dualsub.js 39 | 40 | //Prime Video individual 41 | https:\/\/.+(cloudfront|akamaihd|avi-cdn).net\/(.+\.vtt|\w+\/2\$.+\/[a-zA-Z0-9-]+\.m3u8) url script-response-body Dualsub.js 42 | https:\/\/setting.cloudfront.net\/\?action=(g|s)et url script-analyze-echo-response Dualsub.js 43 | 44 | //YouTube individual 45 | https:\/\/www.youtube.com\/api\/timedtext.+ url script-response-body Dualsub.js 46 | https:\/\/setting.youtube.com\/\?action=(g|s)et url script-analyze-echo-response Dualsub.js 47 | 48 | [mitm] 49 | hostname = *.media.dssott.com, *.media.starott.com, *.api.hbo.com, *.hbomaxcdn.com, *.huluim.com, *.nflxvideo.net, *.cbsaavideo.com, *.cbsivideo.com, *.cloudfront.net, *.akamaihd.net, *.avi-cdn.net, *.youtube.com 50 | 51 | Author: 52 | Telegram: Neurogram 53 | GitHub: Neurogram-R 54 | */ 55 | 56 | let url = $request.url 57 | let headers = $request.headers 58 | 59 | let default_settings = { 60 | Disney: { 61 | type: "Official", // Official, Google, DeepL, External, Disable 62 | lang: "English [CC]", 63 | sl: "auto", 64 | tl: "English [CC]", 65 | line: "s", // f, s 66 | dkey: "null", // DeepL API key 67 | s_subtitles_url: "null", 68 | t_subtitles_url: "null", 69 | subtitles: "null", 70 | subtitles_type: "null", 71 | subtitles_sl: "null", 72 | subtitles_tl: "null", 73 | subtitles_line: "null", 74 | external_subtitles: "null" 75 | }, 76 | HBOMax: { 77 | type: "Official", // Official, Google, DeepL, External, Disable 78 | lang: "English CC", 79 | sl: "auto", 80 | tl: "en-US SDH", 81 | line: "s", // f, s 82 | dkey: "null", // DeepL API key 83 | s_subtitles_url: "null", 84 | t_subtitles_url: "null", 85 | subtitles: "null", 86 | subtitles_type: "null", 87 | subtitles_sl: "null", 88 | subtitles_tl: "null", 89 | subtitles_line: "null", 90 | external_subtitles: "null" 91 | }, 92 | Hulu: { 93 | type: "Google", // Google, DeepL, External, Disable 94 | lang: "English", 95 | sl: "auto", 96 | tl: "en", 97 | line: "s", // f, s 98 | dkey: "null", // DeepL API key 99 | s_subtitles_url: "null", 100 | t_subtitles_url: "null", 101 | subtitles: "null", 102 | subtitles_type: "null", 103 | subtitles_sl: "null", 104 | subtitles_tl: "null", 105 | subtitles_line: "null", 106 | external_subtitles: "null" 107 | }, 108 | Netflix: { 109 | type: "Google", // Google, DeepL, External, Disable 110 | lang: "English", 111 | sl: "auto", 112 | tl: "en", 113 | line: "s", // f, s 114 | dkey: "null", // DeepL API key 115 | s_subtitles_url: "null", 116 | t_subtitles_url: "null", 117 | subtitles: "null", 118 | subtitles_type: "null", 119 | subtitles_sl: "null", 120 | subtitles_tl: "null", 121 | subtitles_line: "null", 122 | external_subtitles: "null" 123 | }, 124 | Paramount: { 125 | type: "Google", // Google, DeepL, External, Disable 126 | lang: "English", 127 | sl: "auto", 128 | tl: "en", 129 | line: "s", // f, s 130 | dkey: "null", // DeepL API key 131 | s_subtitles_url: "null", 132 | t_subtitles_url: "null", 133 | subtitles: "null", 134 | subtitles_type: "null", 135 | subtitles_sl: "null", 136 | subtitles_tl: "null", 137 | subtitles_line: "null", 138 | external_subtitles: "null" 139 | }, 140 | PrimeVideo: { 141 | type: "Official", // Official, Google, DeepL, External, Disable 142 | lang: "English [CC]", 143 | sl: "auto", 144 | tl: "English [CC]", 145 | line: "s", // f, s 146 | dkey: "null", // DeepL API key 147 | s_subtitles_url: "null", 148 | t_subtitles_url: "null", 149 | subtitles: "null", 150 | subtitles_type: "null", 151 | subtitles_sl: "null", 152 | subtitles_tl: "null", 153 | subtitles_line: "null", 154 | external_subtitles: "null" 155 | }, 156 | General: { 157 | service: "null", 158 | type: "Google", // Google, DeepL, External, Disable 159 | lang: "English", 160 | sl: "auto", 161 | tl: "en", 162 | line: "s", // f, s 163 | dkey: "null", // DeepL API key 164 | s_subtitles_url: "null", 165 | t_subtitles_url: "null", 166 | subtitles: "null", 167 | subtitles_type: "null", 168 | subtitles_sl: "null", 169 | subtitles_tl: "null", 170 | subtitles_line: "null", 171 | external_subtitles: "null" 172 | }, 173 | YouTube: { 174 | type: "Enable", // Enable, Disable 175 | lang: "English", 176 | sl: "auto", 177 | tl: "en", 178 | line: "sl" 179 | } 180 | } 181 | 182 | let settings = $prefs.valueForKey("settings") 183 | 184 | if (!settings) settings = default_settings 185 | 186 | if (typeof (settings) == "string") settings = JSON.parse(settings) 187 | 188 | let service = "" 189 | if (url.match(/(dss|star)ott.com/)) service = "Disney" 190 | if (url.match(/hbo(maxcdn)*.com/)) service = "HBOMax" 191 | if (url.match(/huluim.com/)) service = "Hulu" 192 | if (url.match(/nflxvideo.net/)) service = "Netflix" 193 | if (url.match(/cbs(aa|i)video.com/)) service = "Paramount" 194 | if (url.match(/(cloudfront|akamaihd|avi-cdn).net/)) service = "PrimeVideo" 195 | if (url.match(/general.media/)) service = "General" 196 | if (url.match(/youtube.com/)) service = "YouTube" 197 | 198 | if (settings.General) { 199 | let general_service = settings.General.service.split(", ") 200 | for (var s in general_service) { 201 | let patt = new RegExp(general_service[s]) 202 | if (url.match(patt)) { 203 | service = "General" 204 | break 205 | } 206 | } 207 | } 208 | 209 | if (!service) $done({}) 210 | 211 | if (!settings[service]) settings[service] = default_settings[service] 212 | let setting = settings[service] 213 | 214 | if (url.match(/action=get/)) { 215 | delete setting.t_subtitles_url 216 | delete setting.subtitles 217 | delete setting.external_subtitles 218 | $done({ status: "HTTP/1.1 200 OK", body: JSON.stringify(setting), headers: { "Content-Type": "application/json" } }) 219 | } 220 | 221 | if (url.match(/action=set/)) { 222 | let new_setting = JSON.parse($request.body) 223 | if (new_setting.type != "External") settings[service].external_subtitles = "null" 224 | if (new_setting.type == "Reset") new_setting = default_settings[service] 225 | if (new_setting.service && service == "General") settings[service].service = new_setting.service.replace(/\r/g, "") 226 | if (new_setting.type) settings[service].type = new_setting.type 227 | if (new_setting.lang) settings[service].lang = new_setting.lang 228 | if (new_setting.sl) settings[service].sl = new_setting.sl 229 | if (new_setting.tl) settings[service].tl = new_setting.tl 230 | if (new_setting.line) settings[service].line = new_setting.line 231 | if (new_setting.dkey && service != "YouTube") settings[service].dkey = new_setting.dkey 232 | if (new_setting.s_subtitles_url) settings[service].s_subtitles_url = new_setting.s_subtitles_url 233 | if (new_setting.t_subtitles_url) settings[service].t_subtitles_url = new_setting.t_subtitles_url 234 | if (new_setting.subtitles) settings[service].subtitles = new_setting.subtitles 235 | if (new_setting.subtitles_type) settings[service].subtitles_type = new_setting.subtitles_type 236 | if (new_setting.subtitles_sl) settings[service].subtitles_sl = new_setting.subtitles_sl 237 | if (new_setting.subtitles_tl) settings[service].subtitles_tl = new_setting.subtitles_tl 238 | if (new_setting.subtitles_line) settings[service].subtitles_line = new_setting.subtitles_line 239 | if (new_setting.external_subtitles) settings[service].external_subtitles = new_setting.external_subtitles.replace(/\r/g, "") 240 | $prefs.setValueForKey(JSON.stringify(settings), "settings") 241 | delete settings[service].t_subtitles_url 242 | delete settings[service].subtitles 243 | delete settings[service].external_subtitles 244 | $done({ status: "HTTP/1.1 200 OK", body: JSON.stringify(settings[service]), headers: { "Content-Type": "application/json" } }) 245 | } 246 | 247 | let body = $response.body 248 | 249 | if (service == "Netflix" && !body.match(/\d+:\d\d:\d\d.\d\d\d -->.+line.+\n.+/g)) $done({}) 250 | 251 | if (setting.type == "Disable") $done({}) 252 | 253 | if (setting.type != "Official" && url.match(/\.m3u8/)) $done({}) 254 | 255 | if (service == "YouTube") { 256 | 257 | let patt = new RegExp(`lang=${setting.tl}`) 258 | 259 | if (url.replace(/&lang=zh(-Hans)*&/, "&lang=zh-CN&").replace(/&lang=zh-Hant&/, "&lang=zh-TW&").match(patt) || url.match(/&tlang=/)) $done({}) 260 | 261 | let t_url = `${url}&tlang=${setting.tl == "zh-CN" ? "zh-Hans" : setting.tl == "zh-TW" ? "zh-Hant" : setting.tl}` 262 | 263 | let options = { 264 | url: t_url, 265 | headers: headers 266 | } 267 | 268 | $task.fetch(options).then(response => { 269 | 270 | if (setting.line == "sl") $done({ body: response.body }) 271 | let timeline = body.match(/

/g) 272 | 273 | if (url.match(/&kind=asr/)) { 274 | body = body.replace(/<\/?s[^>]*>/g, "") 275 | response.body = response.body.replace(/<\/?s[^>]*>/g, "") 276 | timeline = body.match(/

]+>/g) 277 | } 278 | 279 | for (var i in timeline) { 280 | let patt = new RegExp(`${timeline[i]}([^<]+)<\\/p>`) 281 | if (body.match(patt) && response.body.match(patt)) { 282 | if (setting.line == "s") body = body.replace(patt, `${timeline[i]}$1\n${response.body.match(patt)[1]}

`) 283 | if (setting.line == "f") body = body.replace(patt, `${timeline[i]}${response.body.match(patt)[1]}\n$1

`) 284 | } 285 | } 286 | 287 | $done({ body }) 288 | 289 | }) 290 | 291 | } 292 | 293 | let subtitles_urls_data = setting.t_subtitles_url 294 | 295 | if (setting.type == "Official" && url.match(/\.m3u8/)) { 296 | settings[service].t_subtitles_url = "null" 297 | $prefs.setValueForKey(JSON.stringify(settings), "settings") 298 | 299 | let patt = new RegExp(`TYPE=SUBTITLES.+NAME="${setting.tl.replace(/(\[|\]|\(|\))/g, "\\$1")}.+URI="([^"]+)`) 300 | 301 | if (body.match(patt)) { 302 | 303 | let host = "" 304 | if (service == "Disney") host = url.match(/https.+media.(dss|star)ott.com\/ps01\/disney\/[^\/]+\//)[0] 305 | 306 | let subtitles_data_link = `${host}${body.match(patt)[1]}` 307 | 308 | if (service == "PrimeVideo") { 309 | correct_host = subtitles_data_link.match(/https:\/\/(.+(cloudfront|akamaihd|avi-cdn).net)/)[1] 310 | headers.Host = correct_host 311 | } 312 | 313 | let options = { 314 | url: subtitles_data_link, 315 | method: "GET", 316 | headers: headers 317 | } 318 | 319 | $task.fetch(options).then(response => { 320 | let subtitles_data = "" 321 | if (service == "Disney") subtitles_data = response.body.match(/.+-MAIN.+\.vtt/g) 322 | if (service == "HBOMax") subtitles_data = response.body.match(/http.+\.vtt/g) 323 | if (service == "PrimeVideo") subtitles_data = response.body.match(/.+\.vtt/g) 324 | 325 | if (service == "Disney") host = host + "r/" 326 | if (service == "PrimeVideo") host = subtitles_data_link.match(/https.+\//)[0] 327 | 328 | if (subtitles_data) { 329 | subtitles_data = subtitles_data.join("\n") 330 | if (service == "Disney" || service == "PrimeVideo") subtitles_data = subtitles_data.replace(/(.+)/g, `${host}$1`) 331 | settings[service].t_subtitles_url = subtitles_data 332 | $prefs.setValueForKey(JSON.stringify(settings), "settings") 333 | } 334 | 335 | if (service == "Disney" && subtitles_data_link.match(/.+-MAIN.+/) && response.body.match(/,\nseg.+\.vtt/g)) { 336 | subtitles_data = response.body.match(/,\nseg.+\.vtt/g) 337 | let url_path = subtitles_data_link.match(/\/r\/(.+)/)[1].replace(/\w+\.m3u8/, "") 338 | settings[service].t_subtitles_url = subtitles_data.join("\n").replace(/,\n/g, host + url_path) 339 | $prefs.setValueForKey(JSON.stringify(settings), "settings") 340 | } 341 | 342 | $done({}) 343 | }) 344 | 345 | } 346 | 347 | if (!body.match(patt)) $done({}) 348 | } 349 | 350 | if (url.match(/\.(web)?vtt/) || service == "Netflix" || service == "General") { 351 | if (service != "Netflix" && url == setting.s_subtitles_url && setting.subtitles != "null" && setting.subtitles_type == setting.type && setting.subtitles_sl == setting.sl && setting.subtitles_tl == setting.tl && setting.subtitles_line == setting.line) $done({ body: setting.subtitles }) 352 | 353 | if (setting.type == "Official") { 354 | if (subtitles_urls_data == "null") $done({}) 355 | subtitles_urls_data = subtitles_urls_data.match(/.+\.vtt/g) 356 | if (subtitles_urls_data) official_subtitles(subtitles_urls_data) 357 | } 358 | 359 | if (setting.type == "Google") machine_subtitles("Google") 360 | 361 | if (setting.type == "DeepL") machine_subtitles("DeepL") 362 | 363 | if (setting.type == "External") external_subtitles() 364 | } 365 | 366 | function external_subtitles() { 367 | let patt = new RegExp(`(\\d+\\n)*\\d+:\\d\\d:\\d\\d.\\d\\d\\d --> \\d+:\\d\\d:\\d\\d.\\d.+(\\n|.)+`) 368 | if (!setting.external_subtitles.match(patt)) $done({}) 369 | if (!body.match(patt)) $done({}) 370 | let external = setting.external_subtitles.replace(/(\d+:\d\d:\d\d),(\d\d\d)/g, "$1.$2") 371 | body = body.replace(patt, external.match(patt)[0]) 372 | $done({ body: body }) 373 | } 374 | 375 | async function machine_subtitles(type) { 376 | 377 | body = body.replace(/\r/g, "") 378 | body = body.replace(/(\d+:\d\d:\d\d.\d\d\d --> \d+:\d\d:\d\d.\d.+\n.+)\n(.+)/g, "$1 $2") 379 | body = body.replace(/(\d+:\d\d:\d\d.\d\d\d --> \d+:\d\d:\d\d.\d.+\n.+)\n(.+)/g, "$1 $2") 380 | 381 | let dialogue = body.match(/\d+:\d\d:\d\d.\d\d\d --> \d+:\d\d:\d\d.\d.+\n.+/g) 382 | 383 | if (!dialogue) $done({}) 384 | 385 | let timeline = body.match(/\d+:\d\d:\d\d.\d\d\d --> \d+:\d\d:\d\d.\d.+/g) 386 | 387 | let s_sentences = [] 388 | for (var i in dialogue) { 389 | s_sentences.push(`${type == "Google" ? "~" + i + "~" : "&text="}${dialogue[i].replace(/<\/*(c\.[^>]+|i|c)>/g, "").replace(/\d+:\d\d:\d\d.\d\d\d --> \d+:\d\d:\d\d.\d.+\n/, "")}`) 390 | } 391 | s_sentences = groupAgain(s_sentences, type == "Google" ? 80 : 50) 392 | 393 | let t_sentences = [] 394 | let trans_result = [] 395 | 396 | if (type == "Google") { 397 | for (var p in s_sentences) { 398 | let options = { 399 | url: `https://translate.google.com/translate_a/single?client=it&dt=qca&dt=t&dt=rmt&dt=bd&dt=rms&dt=sos&dt=md&dt=gt&dt=ld&dt=ss&dt=ex&otf=2&dj=1&hl=en&ie=UTF-8&oe=UTF-8&sl=${setting.sl}&tl=${setting.tl}`, 400 | method: "POST", 401 | headers: { 402 | "User-Agent": "GoogleTranslate/6.29.59279 (iPhone; iOS 15.4; en; iPhone14,2)" 403 | }, 404 | body: `q=${encodeURIComponent(s_sentences[p].join("\n"))}` 405 | } 406 | 407 | let trans = await send_request(options) 408 | 409 | if (trans.sentences) { 410 | let sentences = trans.sentences 411 | for (var k in sentences) { 412 | if (sentences[k].trans) trans_result.push(sentences[k].trans.replace(/\n$/g, "").replace(/\n/g, " ").replace(/〜|~/g, "~")) 413 | } 414 | } 415 | } 416 | 417 | if (trans_result.length > 0) { 418 | t_sentences = trans_result.join(" ").match(/~\d+~[^~]+/g) 419 | } 420 | 421 | } 422 | 423 | if (type == "DeepL") { 424 | for (var l in s_sentences) { 425 | let options = { 426 | url: "https://api-free.deepl.com/v2/translate", 427 | method: "POST", 428 | body: `auth_key=${setting.dkey}${setting.sl == "auto" ? "" : `&source_lang=${setting.sl}`}&target_lang=${setting.tl}${s_sentences[l].join("")}` 429 | } 430 | 431 | let trans = await send_request(options) 432 | 433 | if (trans.translations) trans_result.push(trans.translations) 434 | } 435 | 436 | if (trans_result.length > 0) { 437 | for (var o in trans_result) { 438 | for (var u in trans_result[o]) { 439 | t_sentences.push(trans_result[o][u].text.replace(/\n/g, " ")) 440 | } 441 | } 442 | } 443 | } 444 | 445 | if (t_sentences.length > 0) { 446 | let g_t_sentences = t_sentences.join("\n").replace(/\s\n/g, "\n") 447 | 448 | for (var j in dialogue) { 449 | let patt = new RegExp(`(${timeline[j]})`) 450 | if (setting.line == "s") patt = new RegExp(`(${dialogue[j].replace(/(\[|\]|\(|\)|\?)/g, "\\$1")})`) 451 | 452 | let patt2 = new RegExp(`~${j}~\\s*(.+)`) 453 | 454 | if (g_t_sentences.match(patt2) && type == "Google") body = body.replace(patt, `$1\n${g_t_sentences.match(patt2)[1]}`) 455 | 456 | if (type == "DeepL") body = body.replace(patt, `$1\n${t_sentences[j]}`) 457 | 458 | } 459 | 460 | if (service != "Netflix") { 461 | settings[service].s_subtitles_url = url 462 | settings[service].subtitles = body 463 | settings[service].subtitles_type = setting.type 464 | settings[service].subtitles_sl = setting.sl 465 | settings[service].subtitles_tl = setting.tl 466 | settings[service].subtitles_line = setting.line 467 | $prefs.setValueForKey(JSON.stringify(settings), "settings") 468 | } 469 | } 470 | 471 | $done({ body: body }) 472 | 473 | } 474 | 475 | async function official_subtitles(subtitles_urls_data) { 476 | let result = [] 477 | 478 | if (service == "Disney" || service == "HBOMax") { 479 | let subtitles_index = parseInt(url.match(/(\d+)\.vtt/)[1]) 480 | 481 | let start = subtitles_index - 3 < 0 ? 0 : subtitles_index - 3 482 | 483 | subtitles_urls_data = subtitles_urls_data.slice(start, subtitles_index + 4) 484 | } 485 | 486 | for (var k in subtitles_urls_data) { 487 | let options = { 488 | url: subtitles_urls_data[k], 489 | method: "GET", 490 | headers: headers 491 | } 492 | result.push(await send_request(options)) 493 | } 494 | 495 | body = body.replace(/\r/g, "") 496 | body = body.replace(/(\d+:\d\d:\d\d.\d\d\d --> \d+:\d\d:\d\d.\d.+\n.+)\n(.+)/g, "$1 $2") 497 | body = body.replace(/(\d+:\d\d:\d\d.\d\d\d --> \d+:\d\d:\d\d.\d.+\n.+)\n(.+)/g, "$1 $2") 498 | 499 | let timeline = body.match(/\d+:\d\d:\d\d.\d\d\d --> \d+:\d\d:\d\d.\d.+/g) 500 | 501 | for (var i in timeline) { 502 | let patt1 = new RegExp(`(${timeline[i]})`) 503 | if (setting.line == "s") patt1 = new RegExp(`(${timeline[i]}(\\n.+)+)`) 504 | 505 | let time = timeline[i].match(/^\d+:\d\d:\d\d/)[0] 506 | 507 | let patt2 = new RegExp(`${time}.\\d\\d\\d --> \\d+:\\d\\d:\\d\\d.\\d.+(\\n.+)+`) 508 | 509 | let dialogue = result.join("\n\n").match(patt2) 510 | 511 | if (dialogue) body = body.replace( 512 | patt1, 513 | `$1\n${dialogue[0] 514 | .replace(/\d+:\d\d:\d\d.\d\d\d --> \d+:\d\d:\d\d.\d.+\n/, "") 515 | .replace(/\n/, " ")}` 516 | ) 517 | } 518 | 519 | settings[service].s_subtitles_url = url 520 | settings[service].subtitles = body 521 | settings[service].subtitles_type = setting.type 522 | settings[service].subtitles_sl = setting.sl 523 | settings[service].subtitles_tl = setting.tl 524 | settings[service].subtitles_line = setting.line 525 | $prefs.setValueForKey(JSON.stringify(settings), "settings") 526 | 527 | $done({ body: body }) 528 | } 529 | 530 | function send_request(options) { 531 | return new Promise((resolve, reject) => { 532 | $task.fetch(options).then(response => { 533 | resolve(options.method == "GET" ? response.body : JSON.parse(response.body)) 534 | }) 535 | }) 536 | } 537 | 538 | function groupAgain(data, num) { 539 | var result = [] 540 | for (var i = 0; i < data.length; i += num) { 541 | result.push(data.slice(i, i + num)) 542 | } 543 | return result 544 | } 545 | --------------------------------------------------------------------------------