├── README.md ├── module ├── Bangs.sgmodule ├── Hulu-Dualsub.sgmodule ├── Netflix-Dualsub.sgmodule ├── YouTube-Dualsub.sgmodule ├── HBO-Max-Dualsub.sgmodule ├── Sur2b.sgmodule ├── ParamountPlus-Dualsub.sgmodule ├── DisneyPlus-Dualsub.sgmodule ├── Prime-Video-Dualsub.sgmodule ├── Q-Search.sgmodule └── Dualsub.sgmodule ├── Q-Search.js ├── AppPricer.js ├── Douban.js ├── Sur2b.js └── Dualsub.js /README.md: -------------------------------------------------------------------------------- 1 | # Surge -------------------------------------------------------------------------------- /module/Bangs.sgmodule: -------------------------------------------------------------------------------- 1 | #!name=Bangs 2 | #!desc=DuckDuckGo !Bangs Add-ons by Neurogram 3 | 4 | 5 | [URL Rewrite] 6 | https:\/\/www.google.com\/search\?q=([^&]*![^&]+).* https://duckduckgo.com/?q=$1 302 7 | 8 | 9 | [MITM] 10 | hostname = %APPEND% www.google.com 11 | -------------------------------------------------------------------------------- /module/Hulu-Dualsub.sgmodule: -------------------------------------------------------------------------------- 1 | #!name=Hulu Dualsub 2 | #!desc=Hulu subtitles add-ons 3 | 4 | [Script] 5 | Hulu-Dualsub = type=http-response,pattern=^http.+huluim.com\/.+\.vtt$,requires-body=1,max-size=0,timeout=30,script-path=https://raw.githubusercontent.com/Neurogram-R/Surge/master/Dualsub.js 6 | Hulu-Dualsub-Setting = type=http-request,pattern=https:\/\/setting.huluim.com\/\?action=(g|s)et,requires-body=1,max-size=0,script-path=https://raw.githubusercontent.com/Neurogram-R/Surge/master/Dualsub.js 7 | 8 | [MITM] 9 | hostname = %APPEND% *.huluim.com 10 | -------------------------------------------------------------------------------- /module/Netflix-Dualsub.sgmodule: -------------------------------------------------------------------------------- 1 | #!name=Netflix Dualsub 2 | #!desc=Netflix subtitles add-ons 3 | 4 | [Script] 5 | Netflix-Dualsub = type=http-response,pattern=https:\/\/.+nflxvideo.net\/\?o=\d+&v=\d+&e=.+,requires-body=1,max-size=0,timeout=30,script-path=https://raw.githubusercontent.com/Neurogram-R/Surge/master/Dualsub.js 6 | Netflix-Dualsub-Setting = type=http-request,pattern=https:\/\/setting.nflxvideo.net\/\?action=(g|s)et,requires-body=1,max-size=0,script-path=https://raw.githubusercontent.com/Neurogram-R/Surge/master/Dualsub.js 7 | 8 | [MITM] 9 | hostname = %APPEND% *.nflxvideo.net -------------------------------------------------------------------------------- /module/YouTube-Dualsub.sgmodule: -------------------------------------------------------------------------------- 1 | #!name=Youtube Subtrans 2 | #!desc=Youtube subtitles add-ons 3 | 4 | [Script] 5 | YouTube-Dualsub = type=http-response,pattern=https:\/\/www.youtube.com\/api\/timedtext.+,requires-body=1,max-size=0,timeout=30,script-path=https://raw.githubusercontent.com/Neurogram-R/Surge/master/Dualsub.js 6 | YouTube-Dualsub-Setting = type=http-request,pattern=https:\/\/setting.youtube.com\/\?action=(g|s)et,requires-body=1,max-size=0,script-path=https://raw.githubusercontent.com/Neurogram-R/Surge/master/Dualsub.js 7 | 8 | [MITM] 9 | hostname = %APPEND% *.youtube.com 10 | -------------------------------------------------------------------------------- /module/HBO-Max-Dualsub.sgmodule: -------------------------------------------------------------------------------- 1 | #!name=HBO Max Dualsub 2 | #!desc=HBO Max subtitles add-ons 3 | 4 | [Script] 5 | HBO-Max-Dualsub = type=http-response,pattern=https:\/\/(manifests.v2.api.hbo.com|.+hbomaxcdn.com)\/(hls.m3u8.+|video.+\.vtt),requires-body=1,max-size=0,timeout=30,script-path=https://raw.githubusercontent.com/Neurogram-R/Surge/master/Dualsub.js 6 | HBO-Max-Dualsub-Setting = type=http-request,pattern=https:\/\/setting.hbomaxcdn.com\/\?action=(g|s)et,requires-body=1,max-size=0,script-path=https://raw.githubusercontent.com/Neurogram-R/Surge/master/Dualsub.js 7 | 8 | [MITM] 9 | hostname = %APPEND% *.api.hbo.com, *.hbomaxcdn.com -------------------------------------------------------------------------------- /module/Sur2b.sgmodule: -------------------------------------------------------------------------------- 1 | #!name=Sur2b 2 | #!desc=YouTube video summaries, subtitle translation 3 | #!category=Streaming 4 | #!author=Neurogram 5 | 6 | [Script] 7 | Sur2b = type=http-response,pattern=https:\/\/www.youtube.com\/api\/timedtext\?,requires-body=1,max-size=0,binary-body-mode=0,timeout=30,script-path=https://raw.githubusercontent.com/Neurogram-R/Surge/master/Sur2b.js 8 | Sur2bConf = type=http-request,pattern=https:\/\/www.youtube.com\/api\/timedtextConf,requires-body=1,max-size=0,binary-body-mode=0,script-path=https://raw.githubusercontent.com/Neurogram-R/Surge/master/Sur2b.js 9 | 10 | [MITM] 11 | hostname = %APPEND% www.youtube.com 12 | -------------------------------------------------------------------------------- /module/ParamountPlus-Dualsub.sgmodule: -------------------------------------------------------------------------------- 1 | #!name=ParamountPlue Dualsub 2 | #!desc=Paramount+ subtitles add-ons 3 | 4 | [Script] 5 | ParamountPlus-Dualsub = type=http-response,pattern=https:\/\/.+cbs(aa|i)video.com\/.+\.vtt(\?m=\d+)*,requires-body=1,max-size=0,timeout=30,script-path=https://raw.githubusercontent.com/Neurogram-R/Surge/master/Dualsub.js 6 | ParamountPlus-Dualsub-Setting = type=http-request,pattern=https:\/\/setting.cbsivideo.com\/\?action=(g|s)et,requires-body=1,max-size=0,script-path=https://raw.githubusercontent.com/Neurogram-R/Surge/master/Dualsub.js 7 | 8 | [MITM] 9 | hostname = %APPEND% *.cbsaavideo.com, *.cbsivideo.com 10 | -------------------------------------------------------------------------------- /module/DisneyPlus-Dualsub.sgmodule: -------------------------------------------------------------------------------- 1 | #!name=DisneyPlus Dualsub 2 | #!desc=Disney+, Star+ subtitles add-ons 3 | 4 | [Script] 5 | DisneyPlus-Dualsub = type=http-response,pattern=https:\/\/.+media.(dss|star)ott.com\/ps01\/disney\/.+(\.vtt|-all-.+\.m3u8.*),requires-body=1,max-size=0,timeout=30,script-path=https://raw.githubusercontent.com/Neurogram-R/Surge/master/Dualsub.js 6 | DisneyPlus-Dualsub-Setting = type=http-request,pattern=https:\/\/setting.media.dssott.com\/\?action=(g|s)et,requires-body=1,max-size=0,script-path=https://raw.githubusercontent.com/Neurogram-R/Surge/master/Dualsub.js 7 | 8 | [MITM] 9 | hostname = %APPEND% *.media.dssott.com, *.media.starott.com -------------------------------------------------------------------------------- /module/Prime-Video-Dualsub.sgmodule: -------------------------------------------------------------------------------- 1 | #!name=Prime Video Dualsub 2 | #!desc=Prime Video subtitles add-ons 3 | 4 | [Script] 5 | Prime-Video-Dualsub = type=http-response,pattern=https:\/\/.+(cloudfront|akamaihd|avi-cdn).net\/(.+\.vtt|\w+\/2\$.+\/[a-zA-Z0-9-]+\.m3u8),requires-body=1,max-size=0,timeout=30,script-path=https://raw.githubusercontent.com/Neurogram-R/Surge/master/Dualsub.js 6 | Prime-Video-Dualsub-Setting = type=http-request,pattern=https:\/\/setting.cloudfront.net\/\?action=(g|s)et,requires-body=1,max-size=0,script-path=https://raw.githubusercontent.com/Neurogram-R/Surge/master/Dualsub.js 7 | 8 | [MITM] 9 | hostname = %APPEND% *.cloudfront.net, *.akamaihd.net, *.avi-cdn.net -------------------------------------------------------------------------------- /module/Q-Search.sgmodule: -------------------------------------------------------------------------------- 1 | #!name=Q-Search 2 | #!desc=Safari Search Add-ons by Neurogram 3 | 4 | 5 | [URL Rewrite] 6 | ^https:\/\/duckduckgo.com\/\?q=bd\+([^&]+).+ https://www.baidu.com/s?wd=$1 302 7 | ^https:\/\/duckduckgo.com\/\?q=db\+([^&]+).+ https://m.douban.com/search/?query=$1 302 8 | ^https:\/\/duckduckgo.com\/\?q=gh\+([^&]+).+ https://github.com/search?q=$1 302 9 | ^https:\/\/duckduckgo.com\/\?q=gm\+([^&]+).+ https://www.google.com/search?&tbm=isch&q=$1 302 10 | ^https:\/\/duckduckgo.com\/\?q=yd\+([^&]+).+ http://dict.youdao.com/search?q=$1 302 11 | ^https:\/\/duckduckgo.com\/\?q=ddg\+([^&]+).+ https://duckduckgo.com/?ia=about&q=$1 302 12 | ^https:\/\/duckduckgo.com\/\?q=([^&]+).+ https://www.google.com/search?q=$1 302 13 | 14 | 15 | [MITM] 16 | hostname = %APPEND% duckduckgo.com 17 | -------------------------------------------------------------------------------- /module/Dualsub.sgmodule: -------------------------------------------------------------------------------- 1 | #!name=Dualsub 2 | #!desc=Disney+, Star+, HBO Max, Hulu, Netflix, Paramount+, Prime Video, YouTube, etc. subtitles add-ons 3 | 4 | [Script] 5 | Dualsub = type=http-response,pattern=^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.+),requires-body=1,max-size=0,timeout=30,script-path=https://raw.githubusercontent.com/Neurogram-R/Surge/master/Dualsub.js 6 | Dualsub-setting = type=http-request,pattern=^http.+(setting|general).(media.dssott|hbomaxcdn|nflxvideo|youtube|cbsivideo|cloudfront|huluim).(com|net)\/\?action=(g|s)et,requires-body=1,max-size=0,script-path=https://raw.githubusercontent.com/Neurogram-R/Surge/master/Dualsub.js 7 | 8 | [MITM] 9 | hostname = %APPEND% *.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 10 | -------------------------------------------------------------------------------- /Q-Search.js: -------------------------------------------------------------------------------- 1 | /* 2 | Q-Search for Surge by Neurogram 3 | 4 | - Safari Search Add-ons by Neurogram 5 | 6 | 使用说明: 7 | 8 | [Script] 9 | Q-Search = type=http-request,pattern=^https:\/\/duckduckgo.com\/\?q=.+,script-path=Q-Search.js 10 | 11 | [MITM] 12 | hostname = duckduckgo.com 13 | 14 | 注:先进入设置更改 Safari 默认搜索为 DuckDuckGo 15 | 16 | 关于作者 17 | Telegram: Neurogram 18 | GitHub: Neurogram-R 19 | */ 20 | 21 | const engineData = { 22 | "bd": "https://www.baidu.com/s?wd=%@", 23 | "db": "https://m.douban.com/search/?query=%@", 24 | "gh": "https://github.com/search?q=%@", 25 | "gl": "https://www.google.com/search?q=%@", 26 | "gm": "https://www.google.com/search?&tbm=isch&q=%@", 27 | "yd": "http://dict.youdao.com/search?q=%@", 28 | "ddg": "https://duckduckgo.com/?ia=about&q=%@", 29 | "@default": "gl" 30 | } 31 | 32 | let commands = Object.keys(engineData) 33 | let url = $request.url 34 | let keyword = url.match(/duckduckgo.com\/\?q=([^&]+)/) 35 | if (keyword) { 36 | keyword = keyword[1] 37 | let patt = new RegExp(`^(${commands.join("|")})\\+`, "g") 38 | let command = keyword.match(patt) 39 | if (command) { 40 | url = engineData[command[0].replace(/\+/, "")].replace(/%@/, keyword.replace(command[0], "")) 41 | } else { 42 | url = engineData[engineData["@default"]].replace(/%@/, keyword) 43 | } 44 | $done({ 45 | response: { 46 | status: 302, 47 | headers: { 48 | Location: url, 49 | } 50 | } 51 | }) 52 | } else { 53 | $done({}) 54 | } -------------------------------------------------------------------------------- /AppPricer.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | App Pricer for Surge by Neurogram 4 | 5 | - App 价格监控 6 | 7 | */ 8 | 9 | const region = "cn" 10 | const appIds = ["1312014438", "1423330822", "1085978097"] 11 | 12 | var cacheData = $persistentStore.read() 13 | if (!cacheData) { 14 | cacheData = {} 15 | } else { 16 | cacheData = JSON.parse(cacheData) 17 | } 18 | 19 | $httpClient.post('https://itunes.apple.com/lookup?id=' + appIds + "&country=" + region, function (error, response, data) { 20 | if (error) { 21 | console.log(error); 22 | $notification.post("App Pricer", "获取价格失败") 23 | $done() 24 | } else { 25 | let appData = JSON.parse(data).results 26 | let priceChanged = "" 27 | let newAppAdded = "" 28 | for (var i = 0; i < appData.length; i++) { 29 | if (cacheData[appData[i].trackId]) { 30 | if (appData[i].formattedPrice != cacheData[appData[i].trackId].price) { 31 | priceChanged = priceChanged + "🏷 " + appData[i].trackName + " " + cacheData[appData[i].trackId].price + " → " + appData[i].formattedPrice + "\n" 32 | cacheData[appData[i].trackId].price = appData[i].formattedPrice 33 | } 34 | } else { 35 | newAppAdded = newAppAdded + "🏷 " + appData[i].trackName + " " + appData[i].formattedPrice + "\n" 36 | cacheData[appData[i].trackId] = { 37 | name: appData[i].trackName, 38 | price: appData[i].formattedPrice 39 | } 40 | } 41 | } 42 | if (priceChanged) { 43 | $notification.post("Price Changed", "", priceChanged) 44 | } 45 | if (newAppAdded) { 46 | $notification.post("New Apps Added", "", newAppAdded) 47 | } 48 | $persistentStore.write(JSON.stringify(cacheData)) 49 | $done() 50 | } 51 | }) 52 | -------------------------------------------------------------------------------- /Douban.js: -------------------------------------------------------------------------------- 1 | /* 2 | Douban Movie Add-ons for Surge by Neurogram 3 | 4 | - 豆瓣电影网页插件 5 | - 快捷跳转自定义网站搜索 6 | - 展示在映流媒体平台(TMDB API) 7 | 8 | 使用说明 9 | 10 | [Script] 11 | Douban = type=http-response, pattern=https:\/\/m(ovie)*\.douban\.com\/(movie\/)*subject\/.+, requires-body=1, max-size=0, timeout=30, script-path=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, method = 'get') { 37 | return new Promise((resolve, reject) => { 38 | $httpClient[method](options, function (error, response, data) { 39 | if (error) return reject('Error') 40 | resolve(JSON.parse(data)) 41 | }) 42 | }) 43 | } 44 | 45 | async function douban_addons() { 46 | 47 | let body = $response.body 48 | const title = body.match(/"sub-title">([^<]+)/)?.[1] ?? body.match(/(.+)?的剧情简介<\/i>/)?.[1] 49 | 50 | if (!title) $done({}) 51 | 52 | if (tmdb_api_key) { 53 | 54 | const douban_result = await send_request({ 55 | url: `https://frodo.douban.com/api/v2/movie/${movieId}?apiKey=0ac44ae016490db2204ce0a042db2916`, 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 | }) 67 | 68 | if (tmdb_query.results[0]) { 69 | 70 | const tmdb_providers = await send_request({ 71 | url: `https://api.themoviedb.org/3/${douban_result.type}/${tmdb_query.results[0].id}/watch/providers?api_key=${tmdb_api_key}` 72 | }) 73 | 74 | if (tmdb_providers.results[tmdb_region]?.flatrate) { 75 | 76 | for (const provider of tmdb_providers.results[tmdb_region].flatrate) { 77 | watch_web_data.push([provider.provider_name, '', `https://image.tmdb.org/t/p/original${provider.logo_path}`]) 78 | } 79 | 80 | } 81 | } 82 | 83 | } 84 | 85 | } 86 | 87 | const html_data = [] 88 | 89 | for (let i = 0; i < watch_web_data.length; i++) { 90 | html_data.push(``) 91 | } 92 | 93 | if (platform == 'web') body = body.replace(/((.|\n)+?)<\/h1>/, `$1${html_data.join('\n')}$2`) 94 | if (platform == 'mobile') body = body.replace(/("sub-title">.+?)(<\/div>)/, `$1
${html_data.join('\n')}$2`) 95 | 96 | $done({ body }) 97 | 98 | } 99 | 100 | douban_addons() 101 | -------------------------------------------------------------------------------- /Sur2b.js: -------------------------------------------------------------------------------- 1 | /* 2 | Sur2b by Neurogram 3 | 4 | - YouTube video summaries, subtitle translation 5 | 6 | Manual: 7 | Setting tool for Shortcuts: https://neurogram.notion.site/Sur2b-28623efaff9680609b0dcae24aed8061 8 | 9 | Surge: 10 | 11 | [Script] 12 | Sur2b = type=http-response,pattern=https:\/\/www.youtube.com\/api\/timedtext\?,requires-body=1,max-size=0,binary-body-mode=0,timeout=30,script-path=https://raw.githubusercontent.com/Neurogram-R/Surge/master/Sur2b.js 13 | Sur2bConf = type=http-request,pattern=https:\/\/www.youtube.com\/api\/timedtextConf,requires-body=1,max-size=0,binary-body-mode=0,script-path=https://raw.githubusercontent.com/Neurogram-R/Surge/master/Sur2b.js 14 | 15 | [MITM] 16 | hostname = www.youtube.com 17 | 18 | Author: 19 | Telegram: Neurogram 20 | GitHub: Neurogram-R 21 | */ 22 | 23 | 24 | const url = $request.url; 25 | let body, subtitleData; 26 | let conf = $persistentStore.read('Sur2bConf'); 27 | const autoGenSub = url.includes('&kind=asr'); 28 | const videoID = url.match(/(\?|&)v=([^&]+)/)?.[2]; 29 | const sourceLang = url.match(/&lang=([^&]+)/)?.[1]; 30 | let cache = $persistentStore.read('Sur2bCache') || '{}'; 31 | cache = JSON.parse(cache); 32 | 33 | (async () => { 34 | 35 | if (url.includes('timedtextConf')) { 36 | const newConf = JSON.parse($request.body); 37 | if (newConf.delCache) $persistentStore.write('{}', 'Sur2bCache'); 38 | delete newConf.delCache; 39 | $persistentStore.write(JSON.stringify(newConf), 'Sur2bConf'); 40 | return $done({ response: { body: 'OK' } }); 41 | }; 42 | 43 | if (!conf) { 44 | $notification.post('Sur2b', '', '请先通过捷径配置脚本'); 45 | return $done({}); 46 | }; 47 | 48 | conf = JSON.parse(conf); 49 | 50 | body = $response.body; 51 | subtitleData = processTimedText(body); 52 | 53 | if (!subtitleData.processedText) { 54 | $notification.post('Sur2b', '', '未匹配到字幕内容'); 55 | return $done({}); 56 | }; 57 | 58 | let summaryContent, translatedBody; 59 | 60 | if (conf.videoSummary && subtitleData.maxT <= conf.summaryMaxMinutes * 60 * 1000) summaryContent = await summarizer(); 61 | if (conf.videoTranslation && subtitleData.maxT <= conf.translationMaxMinutes * 60 * 1000) translatedBody = await translator(); 62 | 63 | if ((summaryContent || translatedBody) && videoID && sourceLang) { 64 | 65 | if (!cache[videoID]) cache[videoID] = {}; 66 | if (!cache[videoID][sourceLang]) cache[videoID][sourceLang] = {}; 67 | 68 | if (summaryContent) { 69 | cache[videoID][sourceLang].summary = { 70 | content: summaryContent, 71 | timestamp: new Date().getTime() 72 | }; 73 | }; 74 | 75 | if (translatedBody) { 76 | if (!cache[videoID][sourceLang].translation) cache[videoID][sourceLang].translation = {}; 77 | cache[videoID][sourceLang].translation[conf.targetLanguage] = { 78 | content: translatedBody, 79 | timestamp: new Date().getTime() 80 | }; 81 | }; 82 | 83 | }; 84 | 85 | cleanCache(); 86 | $persistentStore.write(JSON.stringify(cache), 'Sur2bCache'); 87 | 88 | $done({ body }); 89 | 90 | })(); 91 | 92 | async function summarizer() { 93 | 94 | if (cache[videoID]?.[sourceLang]?.summary) { 95 | $notification.post('YouTube 视频摘要', '', cache[videoID][sourceLang].summary.content); 96 | return; 97 | }; 98 | 99 | const options = { 100 | url: conf.openAIProxyUrl, 101 | headers: { 102 | 'Content-Type': 'application/json', 103 | 'Authorization': 'Bearer ' + conf.openAIAPIKey 104 | }, 105 | body: { 106 | model: conf.openAIModel, 107 | messages: [ 108 | { 109 | role: 'user', 110 | content: conf.summaryPrompts.replace(/{{subtitles}}/, subtitleData.processedText) 111 | } 112 | ] 113 | } 114 | }; 115 | 116 | try { 117 | if (!conf.openAIProxyUrl) throw new Error('未配置 AI 总结接口链接'); 118 | if (!conf.openAIAPIKey) throw new Error('未配置 AI 总结接口 API Key'); 119 | if (!conf.openAIModel) throw new Error('未配置 AI 总结接口模型'); 120 | 121 | const resp = await sendRequest(options, 'post'); 122 | if (resp.error) throw new Error(resp.error.message); 123 | const content = resp.choices[0].message.content; 124 | $notification.post('YouTube 视频摘要', '', content); 125 | return content; 126 | } catch (err) { 127 | $notification.post('YouTube 视频摘要', '摘要请求失败', err); 128 | return; 129 | }; 130 | 131 | }; 132 | 133 | 134 | async function translator() { 135 | 136 | if (cache[videoID]?.[sourceLang]?.translation?.[conf.targetLanguage]) { 137 | body = cache[videoID][sourceLang].translation[conf.targetLanguage].content; 138 | return; 139 | }; 140 | 141 | let patt = new RegExp(`&lang=${conf.targetLanguage}&`, 'i'); 142 | 143 | if (conf.targetLanguage == 'zh-CN' || conf.targetLanguage == 'ZH-HANS') patt = /&lang=zh(-Hans)*&/i; 144 | if (conf.targetLanguage == 'zh-TW' || conf.targetLanguage == 'ZH-HANT') patt = /&lang=zh-Hant&/i; 145 | 146 | if (url.includes('&tlang=') || patt.test(url)) return; 147 | 148 | if (/&lang=zh(-Han)*/i.test(url) && /^zh-(CN|TW|HAN)/i.test(conf.targetLanguage)) return await chineseTransform(); 149 | 150 | if (autoGenSub) return; 151 | 152 | const originalSubs = []; 153 | 154 | const regex = /

([^<]+)<\/p>/g; 155 | 156 | let match; 157 | 158 | while ((match = regex.exec(body)) !== null) { 159 | originalSubs.push(match[1]); 160 | } 161 | 162 | if (originalSubs.length === 0) return; 163 | 164 | const targetSubs = []; 165 | const batchSize = 50; 166 | 167 | for (let i = 0; i < originalSubs.length; i += batchSize) { 168 | const batch = originalSubs.slice(i, i + batchSize); 169 | try { 170 | const translatedBatch = await translateSwitcher(batch); 171 | targetSubs.push(...translatedBatch); 172 | } catch (error) { 173 | $notification.post('YouTube 视频翻译', '翻译请求失败', error); 174 | return; 175 | } 176 | }; 177 | 178 | let subIndex = 0; 179 | const translatedBody = body.replace(regex, (fullMatch) => { 180 | if (subIndex < targetSubs.length && subIndex < originalSubs.length) { 181 | const originalText = originalSubs[subIndex]; 182 | const translatedText = targetSubs[subIndex]; 183 | 184 | let finalSubText; 185 | 186 | switch (conf.subLine) { 187 | case 1: 188 | finalSubText = `${translatedText}\n${originalText}`; 189 | break; 190 | case 2: 191 | finalSubText = `${originalText}\n${translatedText}`; 192 | break; 193 | case 0: 194 | default: 195 | finalSubText = translatedText; 196 | break; 197 | } 198 | 199 | subIndex++; 200 | 201 | const attributesMatch = fullMatch.match(/

/); 202 | return `

${finalSubText}

`; 203 | } 204 | 205 | return fullMatch; 206 | }); 207 | 208 | body = translatedBody; 209 | 210 | return translatedBody; 211 | }; 212 | 213 | async function translateSwitcher(subs) { 214 | switch (conf.translationProvider) { 215 | case 'Google': 216 | return await googleTranslator(subs); 217 | case 'DeepL': 218 | return await deepLTranslator(subs); 219 | default: 220 | throw new Error(`未知的翻译服务: ${conf.translationProvider}`); 221 | } 222 | }; 223 | 224 | async function googleTranslator(subs) { 225 | const options = { 226 | 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=auto&tl=${conf.targetLanguage}`, 227 | headers: { 228 | 'User-Agent': 'GoogleTranslate/6.29.59279 (iPhone; iOS 15.4; en; iPhone14,2)' 229 | }, 230 | body: `q=${encodeURIComponent('

' + subs.join('\n

'))}` 231 | }; 232 | 233 | const resp = await sendRequest(options, 'post'); 234 | 235 | if (!resp.sentences) throw new Error(`Google 翻译失败: ${JSON.stringify(resp)}`); 236 | 237 | const combinedTrans = resp.sentences.map(s => s.trans).join(''); 238 | 239 | const splitSentences = combinedTrans.split('

'); 240 | 241 | const targetSubs = splitSentences 242 | .filter(sentence => sentence && sentence.trim().length > 0) 243 | .map(sentence => { 244 | return sentence.replace(/\s*[\r\n]+\s*/g, ' ').trim(); 245 | }); 246 | 247 | return targetSubs; 248 | }; 249 | 250 | 251 | async function deepLTranslator(subs) { 252 | if (!conf.deepLAPIKey) throw new Error('未配置 DeepL API Key'); 253 | 254 | const options = { 255 | url: conf.deepLUrl || 'https://api-free.deepl.com/v2/translate', 256 | headers: { 257 | 'Content-Type': 'application/json', 258 | 'Authorization': 'DeepL-Auth-Key ' + conf.deepLAPIKey, 259 | }, 260 | body: { 261 | text: subs, 262 | target_lang: conf.targetLanguage 263 | } 264 | }; 265 | 266 | const resp = await sendRequest(options, 'post'); 267 | 268 | if (!resp.translations) throw new Error(`DeepL 翻译失败: ${JSON.stringify(resp)}`); 269 | 270 | const targetSubs = resp.translations.map(translation => translation.text); 271 | 272 | return targetSubs; 273 | }; 274 | 275 | async function chineseTransform() { 276 | 277 | let from = 'cn'; 278 | let to = 'tw'; 279 | 280 | if (/^zh-(CN|HANS)/i.test(conf.targetLanguage)) [from, to] = [to, from]; 281 | 282 | const openccJS = await sendRequest({ 283 | url: 'https://cdn.jsdelivr.net/npm/opencc-js@1.0.5/dist/umd/full.js' 284 | }) 285 | eval(openccJS); 286 | 287 | const converter = OpenCC.Converter({ from: from, to: to }); 288 | 289 | body = converter(body) 290 | }; 291 | 292 | function processTimedText(xml) { 293 | const regex = /

]*>(.*?)<\/p>/gs; 294 | 295 | let match; 296 | let maxT = 0; 297 | const results = []; 298 | 299 | while ((match = regex.exec(xml)) !== null) { 300 | const t = parseInt(match[1], 10); 301 | const content = match[2].trim(); 302 | let lineText = ''; 303 | 304 | if (content.startsWith(']*>([^<]+)<\/s>/g; 306 | const words = Array.from(content.matchAll(sTagRegex), m => m[1]); 307 | if (words.length > 0) { 308 | lineText = words.join(''); 309 | } 310 | } else { 311 | lineText = content; 312 | } 313 | 314 | lineText = decodeHTMLEntities(lineText).trim(); 315 | 316 | if (lineText) { 317 | if (t > maxT) { 318 | maxT = t; 319 | } 320 | 321 | const totalSeconds = Math.floor(t / 1000); 322 | const hours = Math.floor(totalSeconds / 3600); 323 | const minutes = Math.floor((totalSeconds % 3600) / 60); 324 | const seconds = totalSeconds % 60; 325 | const paddedSeconds = String(seconds).padStart(2, '0'); 326 | let formattedTime; 327 | 328 | if (hours > 0) { 329 | const paddedMinutes = String(minutes).padStart(2, '0'); 330 | formattedTime = `(${hours}:${paddedMinutes}:${paddedSeconds})`; 331 | } else { 332 | formattedTime = `(${minutes}:${paddedSeconds})`; 333 | } 334 | 335 | results.push(`${formattedTime} ${lineText}`); 336 | } 337 | } 338 | 339 | const processedText = results.join('\n'); 340 | 341 | return { 342 | processedText: processedText, 343 | maxT: maxT 344 | }; 345 | }; 346 | 347 | function decodeHTMLEntities(text) { 348 | const entities = { 349 | '&': '&', 350 | '<': '<', 351 | '>': '>', 352 | '"': '"', 353 | ''': '\'' 354 | }; 355 | return text.replace(/&|<|>|"|'/g, match => entities[match]); 356 | }; 357 | 358 | function sendRequest(options, method = 'get') { 359 | return new Promise((resolve, reject) => { 360 | $httpClient[method](options, (error, response, data) => { 361 | if (error) { 362 | return reject(error); 363 | }; 364 | try { 365 | resolve(JSON.parse(data)); 366 | } catch { 367 | resolve(data); 368 | }; 369 | }); 370 | }); 371 | }; 372 | 373 | function cleanCache() { 374 | const now = Date.now(); 375 | const maxMs = conf.cacheMaxHours * 60 * 60 * 1000; 376 | 377 | for (const itemKey of Object.keys(cache)) { 378 | const item = cache[itemKey]; 379 | 380 | for (const lang of Object.keys(item)) { 381 | const langObj = item[lang]; 382 | 383 | if (langObj.summary && now - langObj.summary.timestamp > maxMs) { 384 | delete langObj.summary; 385 | }; 386 | 387 | if (langObj.translation) { 388 | 389 | for (const tLang of Object.keys(langObj.translation)) { 390 | const tObj = langObj.translation[tLang]; 391 | if (now - tObj.timestamp > maxMs) { 392 | delete langObj.translation[tLang]; 393 | }; 394 | }; 395 | 396 | if (Object.keys(langObj.translation).length === 0) { 397 | delete langObj.translation; 398 | }; 399 | }; 400 | 401 | if ((!langObj.summary) && (!langObj.translation)) delete item[lang]; 402 | }; 403 | 404 | if (Object.keys(item).length === 0) delete cache[itemKey]; 405 | }; 406 | 407 | return cache; 408 | } 409 | -------------------------------------------------------------------------------- /Dualsub.js: -------------------------------------------------------------------------------- 1 | /* 2 | Dualsub for Surge 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 | Surge: 13 | 14 | [Script] 15 | 16 | // all in one 17 | Dualsub = type=http-response,pattern=^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.+),requires-body=1,max-size=0,timeout=30,script-path=Dualsub.js 18 | Dualsub-setting = type=http-request,pattern=^http.+(setting|general).(media.dssott|hbomaxcdn|nflxvideo|youtube|cbsivideo|cloudfront|huluim).(com|net)\/\?action=(g|s)et,requires-body=1,max-size=0,script-path=Dualsub.js 19 | 20 | // individual 21 | DisneyPlus-Dualsub = type=http-response,pattern=https:\/\/.+media.(dss|star)ott.com\/ps01\/disney\/.+(\.vtt|-all-.+\.m3u8.*),requires-body=1,max-size=0,timeout=30,script-path=Dualsub.js 22 | DisneyPlus-Dualsub-Setting = type=http-request,pattern=https:\/\/setting.media.dssott.com\/\?action=(g|s)et,requires-body=1,max-size=0,script-path=Dualsub.js 23 | 24 | HBO-Max-Dualsub = type=http-response,pattern=https:\/\/(manifests.v2.api.hbo.com|.+hbomaxcdn.com)\/(hls.m3u8.+|video.+\.vtt),requires-body=1,max-size=0,timeout=30,script-path=Dualsub.js 25 | HBO-Max-Dualsub-Setting = type=http-request,pattern=https:\/\/setting.hbomaxcdn.com\/\?action=(g|s)et,requires-body=1,max-size=0,script-path=Dualsub.js 26 | 27 | Hulu-Dualsub = type=http-response,pattern=^http.+huluim.com\/.+\.vtt$,requires-body=1,max-size=0,timeout=30,script-path=Dualsub.js 28 | Hulu-Dualsub-Setting = type=http-request,pattern=https:\/\/setting.huluim.com\/\?action=(g|s)et,requires-body=1,max-size=0,script-path=Dualsub.js 29 | 30 | Netflix-Dualsub = type=http-response,pattern=https:\/\/.+nflxvideo.net\/\?o=\d+&v=\d+&e=.+,requires-body=1,max-size=0,timeout=30,script-path=Dualsub.js 31 | Netflix-Dualsub-Setting = type=http-request,pattern=https:\/\/setting.nflxvideo.net\/\?action=(g|s)et,requires-body=1,max-size=0,script-path=Dualsub.js 32 | 33 | ParamountPlus-Dualsub = type=http-response,pattern=https:\/\/.+cbs(aa|i)video.com\/.+\.vtt(\?m=\d+)*,requires-body=1,max-size=0,timeout=30,script-path=Dualsub.js 34 | ParamountPlus-Dualsub-Setting = type=http-request,pattern=https:\/\/setting.cbsivideo.com\/\?action=(g|s)et,requires-body=1,max-size=0,script-path=Dualsub.js 35 | 36 | Prime-Video-Dualsub = type=http-response,pattern=https:\/\/.+(cloudfront|akamaihd|avi-cdn).net\/(.+\.vtt|\w+\/2\$.+\/[a-zA-Z0-9-]+\.m3u8),requires-body=1,max-size=0,timeout=30,script-path=Dualsub.js 37 | Prime-Video-Dualsub-Setting = type=http-request,pattern=https:\/\/setting.cloudfront.net\/\?action=(g|s)et,requires-body=1,max-size=0,script-path=Dualsub.js 38 | 39 | YouTube-Dualsub = type=http-response,pattern=https:\/\/www.youtube.com\/api\/timedtext.+,requires-body=1,max-size=0,timeout=30,script-path=Dualsub.js 40 | YouTube-Dualsub-Setting = type=http-request,pattern=https:\/\/setting.youtube.com\/\?action=(g|s)et,requires-body=1,max-size=0,script-path=Dualsub.js 41 | 42 | [MITM] 43 | 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 44 | 45 | Author: 46 | Telegram: Neurogram 47 | GitHub: Neurogram-R 48 | */ 49 | 50 | let url = $request.url 51 | let headers = $request.headers 52 | 53 | let default_settings = { 54 | Disney: { 55 | type: "Official", // Official, Google, DeepL, External, Disable 56 | lang: "English [CC]", 57 | sl: "auto", 58 | tl: "English [CC]", 59 | line: "s", // f, s 60 | dkey: "null", // DeepL API key 61 | s_subtitles_url: "null", 62 | t_subtitles_url: "null", 63 | subtitles: "null", 64 | subtitles_type: "null", 65 | subtitles_sl: "null", 66 | subtitles_tl: "null", 67 | subtitles_line: "null", 68 | external_subtitles: "null" 69 | }, 70 | HBOMax: { 71 | type: "Official", // Official, Google, DeepL, External, Disable 72 | lang: "English CC", 73 | sl: "auto", 74 | tl: "en-US SDH", 75 | line: "s", // f, s 76 | dkey: "null", // DeepL API key 77 | s_subtitles_url: "null", 78 | t_subtitles_url: "null", 79 | subtitles: "null", 80 | subtitles_type: "null", 81 | subtitles_sl: "null", 82 | subtitles_tl: "null", 83 | subtitles_line: "null", 84 | external_subtitles: "null" 85 | }, 86 | Hulu: { 87 | type: "Google", // Google, DeepL, External, Disable 88 | lang: "English", 89 | sl: "auto", 90 | tl: "en", 91 | line: "s", // f, s 92 | dkey: "null", // DeepL API key 93 | s_subtitles_url: "null", 94 | t_subtitles_url: "null", 95 | subtitles: "null", 96 | subtitles_type: "null", 97 | subtitles_sl: "null", 98 | subtitles_tl: "null", 99 | subtitles_line: "null", 100 | external_subtitles: "null" 101 | }, 102 | Netflix: { 103 | type: "Google", // Google, DeepL, External, Disable 104 | lang: "English", 105 | sl: "auto", 106 | tl: "en", 107 | line: "s", // f, s 108 | dkey: "null", // DeepL API key 109 | s_subtitles_url: "null", 110 | t_subtitles_url: "null", 111 | subtitles: "null", 112 | subtitles_type: "null", 113 | subtitles_sl: "null", 114 | subtitles_tl: "null", 115 | subtitles_line: "null", 116 | external_subtitles: "null" 117 | }, 118 | Paramount: { 119 | type: "Google", // Google, DeepL, External, Disable 120 | lang: "English", 121 | sl: "auto", 122 | tl: "en", 123 | line: "s", // f, s 124 | dkey: "null", // DeepL API key 125 | s_subtitles_url: "null", 126 | t_subtitles_url: "null", 127 | subtitles: "null", 128 | subtitles_type: "null", 129 | subtitles_sl: "null", 130 | subtitles_tl: "null", 131 | subtitles_line: "null", 132 | external_subtitles: "null" 133 | }, 134 | PrimeVideo: { 135 | type: "Official", // Official, Google, DeepL, External, Disable 136 | lang: "English [CC]", 137 | sl: "auto", 138 | tl: "English [CC]", 139 | line: "s", // f, s 140 | dkey: "null", // DeepL API key 141 | s_subtitles_url: "null", 142 | t_subtitles_url: "null", 143 | subtitles: "null", 144 | subtitles_type: "null", 145 | subtitles_sl: "null", 146 | subtitles_tl: "null", 147 | subtitles_line: "null", 148 | external_subtitles: "null" 149 | }, 150 | General: { 151 | service: "null", 152 | type: "Google", // Google, DeepL, External, Disable 153 | lang: "English", 154 | sl: "auto", 155 | tl: "en", 156 | line: "s", // f, s 157 | dkey: "null", // DeepL API key 158 | s_subtitles_url: "null", 159 | t_subtitles_url: "null", 160 | subtitles: "null", 161 | subtitles_type: "null", 162 | subtitles_sl: "null", 163 | subtitles_tl: "null", 164 | subtitles_line: "null", 165 | external_subtitles: "null" 166 | }, 167 | YouTube: { 168 | type: "Enable", // Enable, Disable 169 | lang: "English", 170 | sl: "auto", 171 | tl: "en", 172 | line: "sl" 173 | } 174 | } 175 | 176 | let settings = $persistentStore.read() 177 | 178 | if (!settings) settings = default_settings 179 | 180 | if (typeof (settings) == "string") settings = JSON.parse(settings) 181 | 182 | let service = "" 183 | if (url.match(/(dss|star)ott.com/)) service = "Disney" 184 | if (url.match(/hbo(maxcdn)*.com/)) service = "HBOMax" 185 | if (url.match(/huluim.com/)) service = "Hulu" 186 | if (url.match(/nflxvideo.net/)) service = "Netflix" 187 | if (url.match(/cbs(aa|i)video.com/)) service = "Paramount" 188 | if (url.match(/(cloudfront|akamaihd|avi-cdn).net/)) service = "PrimeVideo" 189 | if (url.match(/general.media/)) service = "General" 190 | if (url.match(/youtube.com/)) service = "YouTube" 191 | 192 | if (settings.General) { 193 | let general_service = settings.General.service.split(", ") 194 | for (var s in general_service) { 195 | let patt = new RegExp(general_service[s]) 196 | if (url.match(patt)) { 197 | service = "General" 198 | break 199 | } 200 | } 201 | } 202 | 203 | if (!service) $done({}) 204 | 205 | if (!settings[service]) settings[service] = default_settings[service] 206 | let setting = settings[service] 207 | 208 | if (url.match(/action=get/)) { 209 | delete setting.t_subtitles_url 210 | delete setting.subtitles 211 | delete setting.external_subtitles 212 | $done({ response: { body: JSON.stringify(setting), headers: { "Content-Type": "application/json" } } }) 213 | } 214 | 215 | if (url.match(/action=set/)) { 216 | let new_setting = JSON.parse($request.body) 217 | if (new_setting.type != "External") settings[service].external_subtitles = "null" 218 | if (new_setting.type == "Reset") new_setting = default_settings[service] 219 | if (new_setting.service && service == "General") settings[service].service = new_setting.service.replace(/\r/g, "") 220 | if (new_setting.type) settings[service].type = new_setting.type 221 | if (new_setting.lang) settings[service].lang = new_setting.lang 222 | if (new_setting.sl) settings[service].sl = new_setting.sl 223 | if (new_setting.tl) settings[service].tl = new_setting.tl 224 | if (new_setting.line) settings[service].line = new_setting.line 225 | if (new_setting.dkey && service != "YouTube") settings[service].dkey = new_setting.dkey 226 | if (new_setting.s_subtitles_url) settings[service].s_subtitles_url = new_setting.s_subtitles_url 227 | if (new_setting.t_subtitles_url) settings[service].t_subtitles_url = new_setting.t_subtitles_url 228 | if (new_setting.subtitles) settings[service].subtitles = new_setting.subtitles 229 | if (new_setting.subtitles_type) settings[service].subtitles_type = new_setting.subtitles_type 230 | if (new_setting.subtitles_sl) settings[service].subtitles_sl = new_setting.subtitles_sl 231 | if (new_setting.subtitles_tl) settings[service].subtitles_tl = new_setting.subtitles_tl 232 | if (new_setting.subtitles_line) settings[service].subtitles_line = new_setting.subtitles_line 233 | if (new_setting.external_subtitles) settings[service].external_subtitles = new_setting.external_subtitles.replace(/\r/g, "") 234 | $persistentStore.write(JSON.stringify(settings)) 235 | delete settings[service].t_subtitles_url 236 | delete settings[service].subtitles 237 | delete settings[service].external_subtitles 238 | $done({ response: { body: JSON.stringify(settings[service]), headers: { "Content-Type": "application/json" } } }) 239 | } 240 | 241 | if (setting.type == "Disable") $done({}) 242 | 243 | if (setting.type != "Official" && url.match(/\.m3u8/)) $done({}) 244 | 245 | let body = $response.body 246 | 247 | if (!body) $done({}) 248 | 249 | if (service == "YouTube") { 250 | 251 | let patt = new RegExp(`lang=${setting.tl}`) 252 | 253 | if (url.replace(/&lang=zh(-Hans)*&/, "&lang=zh-CN&").replace(/&lang=zh-Hant&/, "&lang=zh-TW&").match(patt) || url.match(/&tlang=/)) $done({}) 254 | 255 | let t_url = `${url}&tlang=${setting.tl == "zh-CN" ? "zh-Hans" : setting.tl == "zh-TW" ? "zh-Hant" : setting.tl}` 256 | 257 | let options = { 258 | url: t_url, 259 | headers: headers 260 | } 261 | 262 | $httpClient.get(options, function (error, response, data) { 263 | 264 | if (setting.line == "sl") $done({ body: data }) 265 | let timeline = body.match(/

/g) 266 | 267 | if (url.match(/&kind=asr/)) { 268 | body = body.replace(/<\/?s[^>]*>/g, "") 269 | data = data.replace(/<\/?s[^>]*>/g, "") 270 | timeline = body.match(/

]+>/g) 271 | } 272 | 273 | for (var i in timeline) { 274 | let patt = new RegExp(`${timeline[i]}([^<]+)<\\/p>`) 275 | if (body.match(patt) && data.match(patt)) { 276 | if (setting.line == "s") body = body.replace(patt, `${timeline[i]}$1\n${data.match(patt)[1]}

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

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