動畫瘋資訊+

在動畫瘋中自動擷取動畫常見相關資訊,如CAST以及主題曲。

目前为 2022-09-16 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name 動畫瘋資訊+
  3. // @description 在動畫瘋中自動擷取動畫常見相關資訊,如CAST以及主題曲。
  4. // @namespace nathan60107
  5. // @author nathan60107(貝果)
  6. // @version 1.0.2
  7. // @homepage https://home.gamer.com.tw/creationCategory.php?owner=nathan60107&c=425332
  8. // @match https://ani.gamer.com.tw/animeVideo.php?sn=*
  9. // @icon https://ani.gamer.com.tw/apple-touch-icon-144.jpg
  10. // @require https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js
  11. // @grant GM_setValue
  12. // @grant GM_getValue
  13. // @grant GM_registerMenuCommand
  14. // @grant GM_xmlhttpRequest
  15. // @connect google.com
  16. // @connect www.allcinema.net
  17. // @connect cal.syoboi.jp
  18. // @connect acg.gamer.com.tw
  19. // @connect ja.wikipedia.org
  20. // @noframes
  21. // ==/UserScript==
  22.  
  23. //---------------------External libarary---------------------//
  24. /**
  25. *
  26. * detectIncognito v1.1.0 - (c) 2022 Joe Rutkowski <Joe@dreggle.com> (https://github.com/Joe12387/detectIncognito)
  27. *
  28. **/
  29. var detectIncognito = function () { return new Promise(function (t, o) { var e, n = "Unknown"; function r(e) { t({ isPrivate: e, browserName: n }) } function i(e) { return e === eval.toString().length } function a() { (void 0 !== navigator.maxTouchPoints ? function () { try { window.indexedDB.open("test", 1).onupgradeneeded = function (e) { var t = e.target.result; try { t.createObjectStore("test", { autoIncrement: !0 }).put(new Blob), r(!1) } catch (e) { /BlobURLs are not yet supported/.test(e.message) ? r(!0) : r(!1) } } } catch (e) { r(!1) } } : function () { var e = window.openDatabase, t = window.localStorage; try { e(null, null, null, null) } catch (e) { return r(!0), 0 } try { t.setItem("test", "1"), t.removeItem("test") } catch (e) { return r(!0), 0 } r(!1) })() } function c() { navigator.webkitTemporaryStorage.queryUsageAndQuota(function (e, t) { r(t < (void 0 !== (t = window).performance && void 0 !== t.performance.memory && void 0 !== t.performance.memory.jsHeapSizeLimit ? performance.memory.jsHeapSizeLimit : 1073741824)) }, function (e) { o(new Error("detectIncognito somehow failed to query storage quota: " + e.message)) }) } function d() { void 0 !== Promise && void 0 !== Promise.allSettled ? c() : (0, window.webkitRequestFileSystem)(0, 1, function () { r(!1) }, function () { r(!0) }) } void 0 !== (e = navigator.vendor) && 0 === e.indexOf("Apple") && i(37) ? (n = "Safari", a()) : void 0 !== (e = navigator.vendor) && 0 === e.indexOf("Google") && i(33) ? (e = navigator.userAgent, n = e.match(/Chrome/) ? void 0 !== navigator.brave ? "Brave" : e.match(/Edg/) ? "Edge" : e.match(/OPR/) ? "Opera" : "Chrome" : "Chromium", d()) : void 0 !== document.documentElement && void 0 !== document.documentElement.style.MozAppearance && i(37) ? (n = "Firefox", r(void 0 === navigator.serviceWorker)) : void 0 !== navigator.msSaveBlob && i(39) ? (n = "Internet Explorer", r(void 0 === window.indexedDB)) : o(new Error("detectIncognito cannot determine the browser")) }) };
  30. //---------------------External libarary---------------------//
  31.  
  32. let $ = jQuery
  33. let dd = (...d) => {
  34. if (BAHAID !== 'nathan60107') return
  35. d.forEach((it) => { console.log(it) })
  36. }
  37.  
  38. function regexEscape(pattern) {
  39. return pattern.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1')
  40. }
  41.  
  42. async function isPrivateFF() {
  43. return new Promise((resolve) => {
  44. detectIncognito().then((result) => {
  45. if (result.browserName === 'Firefox' && result.isPrivate) return resolve(true)
  46. return resolve(false)
  47. });
  48. })
  49. }
  50.  
  51. function titleProcess(title) {
  52. return title.replaceAll('-', '\\-').replaceAll('#', '')
  53. }
  54.  
  55. function timeProcess(time) {
  56. if (!time || time === '不明') return ''
  57. let [, year, month] = time.match(/([0-9]{4})-([0-9]{2})-([0-9]{2})/)
  58. return `${year}-${parseInt(month)}~`
  59. }
  60.  
  61. async function getBahaData() {
  62. let bahaDbUrl = $('.data_intro .bluebtn')[1].href
  63. let bahaHtml = $((await GET(bahaDbUrl)).responseText)
  64. let nameJp = bahaHtml.find('.ACG-mster_box1 > h2')[0].innerText
  65. let nameEn = bahaHtml.find('.ACG-mster_box1 > h2')[1].innerText
  66. let urlObj = new URL(bahaHtml.find('.ACG-box1listB > li:contains("官方網站") > a')[0]?.href ?? 'https://empty')
  67. let fullUrl = urlObj.searchParams.get('url')
  68. let time = bahaHtml.find('.ACG-box1listA > li:contains("當地")')[0]?.innerText?.split(':')[1]
  69.  
  70. return {
  71. nameJp: titleProcess(nameJp),
  72. nameEn: titleProcess(nameEn),
  73. site: fullUrl ? new URL(fullUrl).hostname.replace('www.', '') : '',
  74. fullUrl: fullUrl,
  75. time: timeProcess(time),
  76. }
  77. }
  78.  
  79. async function GET(url) {
  80. return new Promise((resolve, reject) => {
  81. GM_xmlhttpRequest({
  82. method: "GET",
  83. url: url,
  84. onload: (response) => {
  85. resolve(response)
  86. },
  87. onerror: (response) => { reject(response) },
  88. });
  89. })
  90. }
  91.  
  92. async function POST(url, payload, headers = {}) {
  93. let data = new URLSearchParams(payload).toString()
  94. return new Promise((resolve, reject) => {
  95. GM_xmlhttpRequest({
  96. method: "POST",
  97. url: url,
  98. data: data,
  99. headers: {
  100. ...headers
  101. },
  102. onload: (response) => {
  103. resolve(response)
  104. },
  105. onerror: (response) => {
  106. reject(response)
  107. },
  108. })
  109. })
  110. }
  111.  
  112. function getJson(str) {
  113. try {
  114. return JSON.parse(str)
  115. } catch {
  116. dd('json error')
  117. return {}
  118. }
  119. }
  120.  
  121. async function google(type, keyword) {
  122. let site = ''
  123. let match = ''
  124. switch (type) {
  125. case 'syoboi':
  126. site = 'https://cal.syoboi.jp/tid'
  127. match = 'https://cal.syoboi.jp/tid'
  128. break
  129. case 'allcinema':
  130. site = 'https://www.allcinema.net/cinema/'
  131. match = /https:\/\/www\.allcinema\.net\/cinema\/([0-9]{1,7})/
  132. break
  133. }
  134.  
  135. let googleUrlObj = new URL('https://www.google.com/search?as_qdr=all&as_occt=any')
  136. googleUrlObj.searchParams.append('as_q', keyword)
  137. googleUrlObj.searchParams.append('as_sitesearch', site)
  138. let googleUrl = googleUrlObj.toString()
  139. dd(`Google result: ${googleUrl}`)
  140.  
  141. let googleHtml = (await GET(googleUrl)).responseText
  142. if (googleHtml.includes('為何顯示此頁')) throw { type: 'google', url: googleUrl }
  143. let googleResult = $($.parseHTML(googleHtml)).find('#res .v7W49e a')
  144. for (let goo of googleResult) {
  145. let link = goo.href.replace('http://', 'https://')
  146. if (link.match(match)) return link
  147. }
  148. return ''
  149. }
  150.  
  151. async function searchSyoboi() {
  152. let { site, time, fullUrl } = bahaData
  153. if (!site || !time) return ''
  154.  
  155. let exceptionSite = [
  156. 'tv-tokyo.co.jp',
  157. 'tbs.co.jp',
  158. 'sunrise-inc.co.jp'
  159. ]
  160. if (exceptionSite.includes(site)) {
  161. // https://stackoverflow.com/a/33305263
  162. let exSiteList = exceptionSite.reduce((acc, cur) => {
  163. return acc.concat([regexEscape(`${cur}/anime/`), regexEscape(`${cur}/`)])
  164. }, [])
  165.  
  166. for (const ex of exSiteList) {
  167. let regexResult = fullUrl.match(new RegExp(`(${ex}[^\/]+)`))?.[1]
  168. if (regexResult) {
  169. site = regexResult
  170. break
  171. }
  172. }
  173. }
  174.  
  175. let searchUrlObj = new URL('https://cal.syoboi.jp/find?sd=0&ch=&st=&cm=&r=0&rd=&v=0')
  176. searchUrlObj.searchParams.append('kw', site)
  177. let searchUrl = searchUrlObj.toString()
  178. dd(`Syoboi result: ${searchUrl}`)
  179.  
  180. let syoboiHtml = (await GET(searchUrl)).responseText
  181. let syoboiResults = $($.parseHTML(syoboiHtml)).find('.tframe td')
  182. for (let result of syoboiResults) {
  183. let resultTime = $(result).find('.findComment')[0].innerText
  184.  
  185. if (resultTime.includes(time)) {
  186. let resultUrl = $(result).find('a').attr('href')
  187. return `https://cal.syoboi.jp${resultUrl}`
  188. }
  189. }
  190. return ''
  191. }
  192.  
  193. function songType(type) {
  194. type = type.toLowerCase().replace('section ', '')
  195. switch (type) {
  196. case 'op':
  197. return 'OP'
  198. case 'ed':
  199. return 'ED'
  200. case 'st':
  201. case '挿入歌':
  202. return '插入曲'
  203. default:
  204. return '主題曲'
  205. }
  206. }
  207.  
  208. async function getAllcinema(jpTitle = true) {
  209. changeState('allcinema')
  210.  
  211. let animeName = jpTitle ? bahaData.nameJp : bahaData.nameEn
  212. if (animeName === '') return null
  213. let allcinemaUrl = await google('allcinema', animeName)
  214. dd(`Allcinema url: ${allcinemaUrl}`)
  215. if (!allcinemaUrl) return null
  216.  
  217. let allcinemaId = allcinemaUrl.match(/https:\/\/www\.allcinema\.net\/cinema\/([0-9]{1,7})/)[1]
  218. let allcinemaHtml = (await GET(allcinemaUrl))
  219. let title = allcinemaHtml.responseText.match(/<title>([^<]*<\/title>)/)[1]
  220.  
  221. let allcinemaXsrfToken = allcinemaHtml.responseHeaders.match(/XSRF-TOKEN=([^=]*); expires/)[1]
  222. let allcinemaSession = allcinemaHtml.responseHeaders.match(/allcinema_session=([^=]*); expires/)[1]
  223. let allcinemaCsrfToken = allcinemaHtml.responseText.match(/var csrf_token = '([^']+)';/)[1]
  224. let allcinemaHeader = {
  225. ...(await isPrivateFF()
  226. ? { 'Cookie': `XSRF-TOKEN=${allcinemaXsrfToken}; allcinema_session=${allcinemaSession}` }
  227. : {}
  228. ),
  229. 'X-CSRF-TOKEN': allcinemaCsrfToken,
  230. 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
  231. }
  232.  
  233. let castData = allcinemaHtml.responseText.match(/"cast":(.*)};/)[1]
  234. let castJson = getJson(castData)
  235. let cast = castJson.jobs[0].persons.map(it => ({
  236. char: it.castname,
  237. cv: it.person.personnamemain.personname
  238. }))
  239. let songData = await POST('https://www.allcinema.net/ajax/cinema', {
  240. ajax_data: 'moviesounds',
  241. key: allcinemaId,
  242. page_limit: 10
  243. }, allcinemaHeader)
  244. let songJson = getJson(songData.responseText)
  245. let song = songJson.moviesounds.sounds.map(it => {
  246. return {
  247. type: songType(it.sound.usetype),
  248. title: `「${it.sound.soundtitle}」`,
  249. singer: it.sound.credit.staff.jobs.
  250. filter(job => job.job.jobname.includes('歌'))
  251. [0]?.persons[0].person.personnamemain.personname
  252. }
  253. })
  254. // dd(castJson, songJson)
  255.  
  256. return {
  257. source: allcinemaUrl,
  258. title, cast, song
  259. }
  260. }
  261.  
  262. async function getSyoboi(searchGoogle = false) {
  263. changeState('syoboi')
  264.  
  265. let nameJp = bahaData.nameJp
  266. if (nameJp === '') return null
  267. let syoboiUrl = await (searchGoogle ? google('syoboi', nameJp) : searchSyoboi())
  268. dd(`Syoboi url: ${syoboiUrl}`)
  269. if (!syoboiUrl) return null
  270. let syoboiHtml = (await GET(syoboiUrl)).responseText
  271. let title = syoboiHtml.match(/<title>([^<]*)<\/title>/)[1]
  272.  
  273. let cast = []
  274. let castData = $($.parseHTML(syoboiHtml)).find('.cast table tr')
  275. for (let role of castData) {
  276. cast.push({
  277. char: $(role).find('th').text(),
  278. cv: $(role).find('td > a').text()
  279. })
  280. }
  281.  
  282. let song = []
  283. let songData = $($.parseHTML(syoboiHtml)).find('.op, .ed, .st, .section:contains("主題歌")') // https://stackoverflow.com/a/42575222
  284. for (let sd of songData) {
  285. song.push({
  286. type: songType(sd.className),
  287. title: $(sd).find('.title')[0].childNodes[2].data,
  288. singer: $(sd).find('th:contains("歌")').parent().children()[1]?.innerText,
  289. })
  290. }
  291.  
  292. return {
  293. source: syoboiUrl,
  294. title, cast, song
  295. }
  296. }
  297.  
  298. async function searchWiki(json) {
  299. let searchWikiUrl = (nameList) => {
  300. let wikiUrlObj = new URL('https://ja.wikipedia.org/w/api.php')
  301. const params = {
  302. action: 'query',
  303. format: 'json',
  304. prop: 'langlinks|pageprops',
  305. titles: nameList,
  306. redirects: 1,
  307. lllang: 'zh',
  308. lllimit: 100,
  309. ppprop: 'disambiguation'
  310. }
  311. for (let [k, v] of Object.entries(params)) {
  312. wikiUrlObj.searchParams.append(k, v)
  313. }
  314. return wikiUrlObj.toString()
  315. }
  316.  
  317. let castList = _.chunk(_.uniq(json.map(j => j.cvName2 ?? j.cv)), 50)
  318. let result = {
  319. query: {
  320. pages: {},
  321. normalized: [],
  322. redirects: [],
  323. }
  324. }
  325.  
  326. for (let cast50 of castList) {
  327. let nameList = cast50.join('|')
  328. let wikiApi = searchWikiUrl(nameList)
  329. let wikiJson = JSON.parse((await GET(wikiApi)).responseText)
  330.  
  331. Object.assign(result.query.pages, wikiJson.query.pages)
  332. result.query.normalized.push(...wikiJson.query.normalized || [])
  333. result.query.redirects.push(...wikiJson.query.redirects || [])
  334. }
  335.  
  336. return result
  337. }
  338.  
  339. async function getCastHtml(json) {
  340. function replaceEach(array, getFrom = (it) => it.from, getTo = (it) => it.to) {
  341. array?.forEach((it) => {
  342. json.forEach((j, index) => {
  343. if (j.cv === getFrom(it) || j.cvName2 === getFrom(it)) {
  344. json[index].cvName2 = getTo(it)
  345. }
  346. })
  347. })
  348. }
  349.  
  350. let wikiJson = await searchWiki(json)
  351. let disamb = _.filter(wikiJson.query.pages, ['pageprops', { disambiguation: '' }])
  352. let normalized = wikiJson.query.normalized
  353. let redirects = wikiJson.query.redirects
  354. // dd(wikiJson, normalized, redirects, disamb)
  355.  
  356. // Deal with wiki page normalized, redirects and disambiguation.
  357. replaceEach(normalized)
  358. replaceEach(redirects)
  359. if (disamb.length) {
  360. replaceEach(disamb, (it) => it.title, (it) => `${it.title} (声優)`)
  361.  
  362. wikiJson = await searchWiki(json)
  363. redirects = wikiJson.query.redirects
  364. replaceEach(redirects)
  365. }
  366.  
  367. return json.map(j => {
  368. let wikiPage = _.filter(wikiJson.query.pages, page =>
  369. page.title === j.cv || page.title === j.cvName2
  370. )[0]
  371. let zhName = wikiPage.langlinks?.[0]['*']
  372. let wikiUrl = zhName ? `https://zh.wikipedia.org/zh-tw/${zhName}` : `https://ja.wikipedia.org/wiki/${j.cvName2 ?? j.cv}`
  373. let wikiText = zhName ? 'Wiki' : 'WikiJP'
  374.  
  375. return `
  376. <div>${j.char ?? ''}</div>
  377. <div>${j.cv}</div>
  378. ${wikiPage.missing === ''
  379. ? '<div></div>'
  380. : `<a href="${wikiUrl}" target="_blank">🔗${wikiText}</a>`}
  381. `}).join('')
  382. }
  383.  
  384. function getSongHtml(json) {
  385. return json.map(j => `
  386. <div>${j.type}${j.title}</div>
  387. <div>${j.singer ?? '-'}</div>
  388. <a href="https://www.youtube.com/results?search_query=${j.title.slice(1, j.title.length - 1)} ${j.singer ?? ''}" target="_blank">
  389. 🔎Youtube
  390. </a>
  391. `).join('')
  392. }
  393.  
  394. function getCss() {
  395. return `
  396. #ani-info .grid {
  397. display: grid;
  398. gap: 10px;
  399. margin-top: 10px
  400. }
  401. #ani-info a {
  402. color: rgb(51, 145, 255)
  403. }
  404. #ani-info .bluebtn {
  405. font-size: 13px;
  406. }
  407. #ani-info .grid.cast {
  408. grid-template-columns: repeat(3, auto);
  409. }
  410. #ani-info .grid.song {
  411. grid-template-columns: repeat(3, auto);
  412. }
  413. `
  414. }
  415.  
  416. async function changeState(state, params) {
  417. switch (state) {
  418. case 'init':
  419. $('.anime-option').append(`
  420. <style type='text/css'>${getCss()}</style>
  421. <div id="ani-info">
  422. <ul class="data_type">
  423. <li>
  424. <span>aniInfo+</span>
  425. <i id="ani-info-msg">歡迎使用動畫瘋資訊+</i>
  426. </li>
  427. </ul>
  428. </div>
  429. `)
  430. break
  431. case 'btn':
  432. $('#ani-info-msg').html(`
  433. <div id="ani-info-main" class="bluebtn" onclick="aniInfoMain()">
  434. 讀取動畫資訊
  435. </div>
  436. `)
  437. $('#ani-info-main')[0].addEventListener("click", main, {
  438. once: true
  439. });
  440. break
  441. case 'google':
  442. $('#ani-info-msg').html(`Google搜尋失敗,請點擊<a href="${params.url}" target="_blank">連結</a>解除reCAPTCHA後重整此網頁。`)
  443. break
  444. case 'syoboi':
  445. $('#ani-info-msg').html(`嘗試取得syoboi資料中...`)
  446. break
  447. case 'allcinema':
  448. $('#ani-info-msg').html(`嘗試取得allcinema資料中...`)
  449. break
  450. case 'fail':
  451. $('#ani-info-msg').html(`無法取得資料 ${params.error}`)
  452. break
  453. case 'result': {
  454. let castHtml = await getCastHtml(params.cast)
  455. let songHtml = getSongHtml(params.song)
  456. $('#ani-info').html('')
  457. if (castHtml) $('#ani-info').append(`
  458. <ul class="data_type">
  459. <li>
  460. <span>CAST</span>
  461. <div class="grid cast">${castHtml}</div>
  462. </li>
  463. </ul>
  464. `)
  465. if (songHtml) $('#ani-info').append(`
  466. <ul class="data_type">
  467. <li>
  468. <span>主題曲</span>
  469. <div class="grid song">${songHtml}</div>
  470. </li>
  471. </ul>
  472. `)
  473. $('#ani-info').append(`
  474. <ul class="data_type">
  475. <li>
  476. <span>aniInfo+</span>
  477. 資料來源:<a href="${params.source}" target="_blank">${params.title}</a>
  478. </li>
  479. </ul>
  480. `)
  481. break
  482. }
  483. case 'debug': {
  484. let aaa = await getSyoboi()
  485. let bbb = await getSyoboi(true)
  486. let ccc = await getAllcinema()
  487. let ddd = await getAllcinema(false)
  488. $('#ani-info').html('')
  489. $('#ani-info').append(`
  490. <ul class="data_type">
  491. <li>
  492. <span>aniInfo+</span>
  493. <br>
  494. syoboi:<a href="${aaa?.source}" target="_blank">${aaa?.title}</a>
  495. <br>
  496. allcinema(jp):<a href="${ccc?.source}" target="_blank">${ccc?.title}</a>
  497. <br>
  498. allcinema(en):<a href="${ddd?.source}" target="_blank">${ddd?.title}</a>
  499. <br>
  500. syoboi(google):<a href="${bbb?.source}" target="_blank">${bbb?.title}</a>
  501. <br>
  502. </li>
  503. </ul>
  504. `)
  505. break
  506. }
  507. }
  508. }
  509.  
  510. async function main() {
  511. let debug = false
  512. try {
  513. if (debug) {
  514. changeState('debug')
  515. return
  516. }
  517. let result = null
  518. result = await getSyoboi(false)
  519. if (!result) result = await getAllcinema(true)
  520. if (!result) result = await getAllcinema(false)
  521. if (!result) result = await getSyoboi(true)
  522.  
  523. if (result) changeState('result', result)
  524. else changeState('fail', { error: '' })
  525. } catch (e) {
  526. if (e.type === 'google') {
  527. changeState('google', { url: e.url })
  528. } else {
  529. changeState('fail', { error: e })
  530. }
  531. }
  532. }
  533.  
  534. (async function () {
  535. globalThis.bahaData = await getBahaData()
  536. changeState('init')
  537.  
  538. // Set user option default value.
  539. if (GM_getValue('auto') == undefined) { GM_setValue('auto', true); }
  540.  
  541. // Set user option menu in Tampermonkey.
  542. let isAuto = GM_getValue('auto');
  543. GM_registerMenuCommand(`設定為${isAuto ? '手動' : '自動'}執行`, () => {
  544. GM_setValue('auto', !GM_getValue('auto'));
  545. location.reload();
  546. });
  547.  
  548. // Do task or set button to wait for click and do task.
  549. if (isAuto) main()
  550. else changeState('btn')
  551. })();
  552.  
  553. /**
  554. * Reference:
  555. * [Write userscript in VSC](https://stackoverflow.com/a/55568568)
  556. * [Same above but video](https://www.youtube.com/watch?v=7bWwkTWJy40)
  557. * [Detect browser private mode](https://stackoverflow.com/a/69678895/13069889)
  558. * [and its cdn](https://cdn.jsdelivr.net/gh/Joe12387/detectIncognito@main/detectIncognito.min.js)
  559. * [FF observe GM request](https://firefox-source-docs.mozilla.org/devtools-user/browser_toolbox/index.html)
  560. * [Wiki API](https://ja.wikipedia.org/wiki/%E7%89%B9%E5%88%A5:ApiSandbox#action=query&format=json&prop=langlinks%7Cpageprops&titles=%E6%A2%B6%E5%8E%9F%E5%B2%B3%E4%BA%BA%7C%E5%B0%8F%E6%9E%97%E8%A3%95%E4%BB%8B%7C%E4%B8%AD%E4%BA%95%E5%92%8C%E5%93%89%7CM%E3%83%BBA%E3%83%BBO%7C%E9%88%B4%E6%9D%91%E5%81%A5%E4%B8%80%7C%E4%B8%8A%E6%A2%9D%E6%B2%99%E6%81%B5%E5%AD%90%7C%E6%A5%A0%E5%A4%A7%E5%85%B8%7C%E8%88%88%E6%B4%A5%E5%92%8C%E5%B9%B8%7C%E6%97%A5%E9%87%8E%E8%81%A1%7C%E9%96%A2%E6%99%BA%E4%B8%80%7C%E6%82%A0%E6%9C%A8%E7%A2%A7%7C%E5%89%8D%E9%87%8E%E6%99%BA%E6%98%AD&redirects=1&lllang=zh&lllimit=100&ppprop=disambiguation)
  561. * [Always use en/decodeURIComponent](https://stackoverflow.com/a/747845)
  562. */

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址