├── AppPricer.js ├── Douban.js ├── Dualsub.js ├── Q-Search.js ├── README.md └── module ├── DisneyPlus-Dualsub.sgmodule ├── Dualsub.sgmodule ├── HBO-Max-Dualsub.sgmodule ├── Hulu-Dualsub.sgmodule ├── Netflix-Dualsub.sgmodule ├── ParamountPlus-Dualsub.sgmodule ├── Prime-Video-Dualsub.sgmodule ├── Q-Search.sgmodule └── YouTube-Dualsub.sgmodule /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Surge -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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 | --------------------------------------------------------------------------------