動畫瘋資訊+

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

  1. // ==UserScript==
  2. // @name 動畫瘋資訊+
  3. // @description 在動畫瘋中自動擷取動畫常見相關資訊,如CAST以及主題曲。
  4. // @namespace nathan60107
  5. // @author nathan60107(貝果)
  6. // @version 1.1.3
  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. d.forEach((it) => { console.log(it) })
  35. }
  36.  
  37. function regexEscape(pattern) {
  38. return pattern.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1')
  39. }
  40.  
  41. async function isPrivateFF() {
  42. return new Promise((resolve) => {
  43. detectIncognito().then((result) => {
  44. if (result.browserName === 'Firefox' && result.isPrivate) return resolve(true)
  45. return resolve(false)
  46. });
  47. })
  48. }
  49.  
  50. function titleProcess(title) {
  51. return title.replaceAll('-', '\\-').replaceAll('#', '')
  52. }
  53.  
  54. function timeProcess(time) {
  55. if (!time || time === '不明') return null
  56. let [, year, month] = time.match(/([0-9]{4})-([0-9]{2})-([0-9]{2})/)
  57. return [
  58. `${year}-${parseInt(month) - 1}~`,
  59. `${year}-${parseInt(month)}~`,
  60. `${year}-${parseInt(month) + 1}~`,
  61. ]
  62. }
  63.  
  64. async function getBahaData() {
  65. let bahaDbUrl = $('a:contains(作品資料)')[0].href
  66. let bahaHtml = $((await GET(bahaDbUrl)).responseText)
  67. let nameJp = bahaHtml.find('.ACG-info-container > h2')[0].innerText
  68. let nameEn = bahaHtml.find('.ACG-info-container > h2')[1].innerText
  69. let urlObj = new URL(bahaHtml.find('.ACG-box1listB > li:contains("官方網站") > a')[0]?.href ?? 'https://empty')
  70. let fullUrl = urlObj.searchParams.get('url')
  71. let time = bahaHtml.find('.ACG-box1listA > li:contains("當地")')[0]?.innerText?.split(':')[1]
  72.  
  73. return {
  74. nameJp: titleProcess(nameJp),
  75. nameEn: titleProcess(nameEn),
  76. site: fullUrl ? new URL(fullUrl).hostname.replace('www.', '') : '',
  77. fullUrl: fullUrl,
  78. time: timeProcess(time),
  79. }
  80. }
  81.  
  82. async function GET(url) {
  83. return new Promise((resolve, reject) => {
  84. GM_xmlhttpRequest({
  85. method: "GET",
  86. url: url,
  87. onload: (response) => {
  88. resolve(response)
  89. },
  90. onerror: (response) => { reject(response) },
  91. });
  92. })
  93. }
  94.  
  95. async function POST(url, payload, headers = {}) {
  96. let data = new URLSearchParams(payload).toString()
  97. return new Promise((resolve, reject) => {
  98. GM_xmlhttpRequest({
  99. method: "POST",
  100. url: url,
  101. data: data,
  102. headers: {
  103. ...headers
  104. },
  105. onload: (response) => {
  106. resolve(response)
  107. },
  108. onerror: (response) => {
  109. reject(response)
  110. },
  111. })
  112. })
  113. }
  114.  
  115. function getJson(str) {
  116. try {
  117. return JSON.parse(str)
  118. } catch {
  119. return {}
  120. }
  121. }
  122.  
  123. async function google(type, keyword) {
  124. let site = ''
  125. let match = ''
  126. switch (type) {
  127. case 'syoboi':
  128. site = 'https://cal.syoboi.jp/tid'
  129. match = 'https://cal.syoboi.jp/tid'
  130. break
  131. case 'allcinema':
  132. site = 'https://www.allcinema.net/cinema/'
  133. match = /https:\/\/www\.allcinema\.net\/cinema\/([0-9]{1,7})/
  134. break
  135. }
  136.  
  137. let googleUrlObj = new URL('https://www.google.com/search?as_qdr=all&as_occt=any')
  138. googleUrlObj.searchParams.append('as_q', keyword)
  139. googleUrlObj.searchParams.append('as_sitesearch', site)
  140. let googleUrl = googleUrlObj.toString()
  141.  
  142. let googleHtml = (await GET(googleUrl)).responseText
  143. if (googleHtml.includes('為何顯示此頁')) throw { type: 'google', url: googleUrl }
  144. let googleResult = $($.parseHTML(googleHtml)).find('#res .v7W49e a')
  145. for (let goo of googleResult) {
  146. let link = goo.href.replace('http://', 'https://')
  147. if (link.match(match)) return link
  148. }
  149. return ''
  150. }
  151.  
  152. async function searchSyoboi() {
  153. let { site, time, fullUrl } = bahaData
  154. if (!site || !time) return ''
  155.  
  156. let exceptionSite = [
  157. 'tv-tokyo.co.jp',
  158. 'tbs.co.jp',
  159. 'sunrise-inc.co.jp'
  160. ]
  161. if (exceptionSite.includes(site)) {
  162. // https://stackoverflow.com/a/33305263
  163. let exSiteList = exceptionSite.reduce((acc, cur) => {
  164. return acc.concat([regexEscape(`${cur}/anime/`), regexEscape(`${cur}/`)])
  165. }, [])
  166.  
  167. for (const ex of exSiteList) {
  168. let regexResult = fullUrl.match(new RegExp(`(${ex}[^\/]+)`))?.[1]
  169. if (regexResult) {
  170. site = regexResult
  171. break
  172. }
  173. }
  174. }
  175.  
  176. let searchUrlObj = new URL('https://cal.syoboi.jp/find?sd=0&ch=&st=&cm=&r=0&rd=&v=0')
  177. searchUrlObj.searchParams.append('kw', site)
  178. let searchUrl = searchUrlObj.toString()
  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 (time.some(t => resultTime.includes(t))) {
  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. if (!allcinemaUrl) return null
  215.  
  216. let allcinemaId = allcinemaUrl.match(/https:\/\/www\.allcinema\.net\/cinema\/([0-9]{1,7})/)[1]
  217. let allcinemaHtml = (await GET(allcinemaUrl))
  218. let title = allcinemaHtml.responseText.match(/<title>([^<]*<\/title>)/)[1]
  219.  
  220. let allcinemaXsrfToken = allcinemaHtml.responseHeaders.match(/XSRF-TOKEN=([^=]*); expires/)[1]
  221. let allcinemaSession = allcinemaHtml.responseHeaders.match(/allcinema_session=([^=]*); expires/)[1]
  222. let allcinemaCsrfToken = allcinemaHtml.responseText.match(/var csrf_token = '([^']+)';/)[1]
  223. let allcinemaHeader = {
  224. ...(await isPrivateFF()
  225. ? { 'Cookie': `XSRF-TOKEN=${allcinemaXsrfToken}; allcinema_session=${allcinemaSession}` }
  226. : {}
  227. ),
  228. 'X-CSRF-TOKEN': allcinemaCsrfToken,
  229. 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
  230. }
  231.  
  232. let castData = allcinemaHtml.responseText.match(/"cast":(.*)};/)[1]
  233. let castJson = getJson(castData)
  234. let cast = castJson.jobs[0].persons.map(it => ({
  235. char: it.castname,
  236. cv: it.person.personnamemain.personname
  237. }))
  238. let songData = await POST('https://www.allcinema.net/ajax/cinema', {
  239. ajax_data: 'moviesounds',
  240. key: allcinemaId,
  241. page_limit: 10
  242. }, allcinemaHeader)
  243. let songJson = getJson(songData.responseText)
  244. let song = songJson.moviesounds.sounds.map(it => {
  245. return {
  246. type: songType(it.sound.usetype),
  247. title: `「${it.sound.soundtitle}」`,
  248. singer: it.sound.credit.staff.jobs.
  249. filter(job => job.job.jobname.includes('歌'))
  250. [0]?.persons[0].person.personnamemain.personname
  251. }
  252. })
  253.  
  254. return {
  255. source: allcinemaUrl,
  256. title, cast, song
  257. }
  258. }
  259.  
  260. async function getSyoboi(searchGoogle = false) {
  261. changeState('syoboi')
  262.  
  263. let nameJp = bahaData.nameJp
  264. if (nameJp === '') return null
  265. let syoboiUrl = await (searchGoogle ? google('syoboi', nameJp) : searchSyoboi())
  266. if (!syoboiUrl) return null
  267. let syoboiHtml = (await GET(syoboiUrl)).responseText
  268. let title = syoboiHtml.match(/<title>([^<]*)<\/title>/)[1]
  269.  
  270. let cast = []
  271. let castData = $($.parseHTML(syoboiHtml)).find('.cast table tr')
  272. for (let role of castData) {
  273. cast.push({
  274. char: $(role).find('th').text(),
  275. cv: $(role).find('td').text()
  276. })
  277. }
  278.  
  279. let song = []
  280. let songData = $($.parseHTML(syoboiHtml)).find('.op, .ed, .st, .section:contains("主題歌")') // https://stackoverflow.com/a/42575222
  281. for (let sd of songData) {
  282. song.push({
  283. type: songType(sd.className),
  284. title: $(sd).find('.title')[0].childNodes[2].data,
  285. singer: $(sd).find('th:contains("歌")').parent().children()[1]?.innerText,
  286. })
  287. }
  288.  
  289. return {
  290. source: syoboiUrl,
  291. title, cast, song
  292. }
  293. }
  294.  
  295. async function searchWiki(json) {
  296. let searchWikiUrl = (nameList) => {
  297. let wikiUrlObj = new URL('https://ja.wikipedia.org/w/api.php')
  298. const params = {
  299. action: 'query',
  300. format: 'json',
  301. prop: 'langlinks|pageprops',
  302. titles: nameList,
  303. redirects: 1,
  304. lllang: 'zh',
  305. lllimit: 100,
  306. ppprop: 'disambiguation'
  307. }
  308. for (let [k, v] of Object.entries(params)) {
  309. wikiUrlObj.searchParams.append(k, v)
  310. }
  311. return wikiUrlObj.toString()
  312. }
  313.  
  314. let castList = _.chunk(_.uniq(json.map(j => j.cvName2 ?? j.cv)), 50)
  315. let result = {
  316. query: {
  317. pages: {},
  318. normalized: [],
  319. redirects: [],
  320. }
  321. }
  322.  
  323. for (let cast50 of castList) {
  324. let nameList = cast50.join('|')
  325. let wikiApi = searchWikiUrl(nameList)
  326. let wikiJson = JSON.parse((await GET(wikiApi)).responseText)
  327.  
  328. Object.assign(result.query.pages, wikiJson.query.pages)
  329. result.query.normalized.push(...wikiJson.query.normalized || [])
  330. result.query.redirects.push(...wikiJson.query.redirects || [])
  331. }
  332.  
  333. return result
  334. }
  335.  
  336. async function getCastHtml(json) {
  337. function replaceEach(array, getFrom = (it) => it.from, getTo = (it) => it.to) {
  338. array?.forEach((it) => {
  339. json.forEach((j, index) => {
  340. if (j.cv === getFrom(it) || j.cvName2 === getFrom(it)) {
  341. json[index].cvName2 = getTo(it)
  342. }
  343. })
  344. })
  345. }
  346.  
  347. let wikiJson = await searchWiki(json)
  348. let disamb = _.filter(wikiJson.query.pages, ['pageprops', { disambiguation: '' }])
  349. let normalized = wikiJson.query.normalized
  350. let redirects = wikiJson.query.redirects
  351.  
  352. // Deal with wiki page normalized, redirects and disambiguation.
  353. replaceEach(normalized)
  354. replaceEach(redirects)
  355. if (disamb.length) {
  356. replaceEach(disamb, (it) => it.title, (it) => `${it.title} (声優)`)
  357.  
  358. wikiJson = await searchWiki(json)
  359. redirects = wikiJson.query.redirects
  360. replaceEach(redirects)
  361. }
  362.  
  363. return json.map(j => {
  364. let wikiPage = _.filter(wikiJson.query.pages, page =>
  365. page.title === j.cv || page.title === j.cvName2
  366. )[0]
  367. let zhName = wikiPage.langlinks?.[0]['*']
  368. let wikiUrl = zhName ? `https://zh.wikipedia.org/zh-tw/${zhName}` : `https://ja.wikipedia.org/wiki/${j.cvName2 ?? j.cv}`
  369. let wikiText = zhName ? 'Wiki' : 'WikiJP'
  370.  
  371. return `
  372. <div>${j.char ?? ''}</div>
  373. <div>${j.cv}</div>
  374. ${wikiPage.missing === ''
  375. ? '<div></div>'
  376. : `<a href="${wikiUrl}" target="_blank">🔗${wikiText}</a>`}
  377. `}).join('')
  378. }
  379.  
  380. function getSongHtml(json) {
  381. return json.map(j => `
  382. <div>${j.type}${j.title}</div>
  383. <div>${j.singer ?? '-'}</div>
  384. <a href="https://www.youtube.com/results?search_query=${j.title.slice(1, j.title.length - 1)} ${j.singer ?? ''}" target="_blank">
  385. 🔎Youtube
  386. </a>
  387. `).join('')
  388. }
  389.  
  390. function getCss() {
  391. return `
  392. /* Old baha CSS */
  393. .data_type {
  394. width: 100%;
  395. margin-left: 12px;
  396. padding: 12px 0;
  397. }
  398. .data_type li {
  399. float: left;
  400. margin-right: 24px;
  401. margin-bottom: 8px;
  402. font-size: 1.4em;
  403. color: var(--text-default-color);
  404. }
  405. .data_type span {
  406. display: inline-block;
  407. font-size: 0.8em;
  408. padding: 6px;
  409. margin-right: 10px;
  410. color: var(--text-default-color);
  411. background: var(--btn-more);
  412. border-radius: 4px;
  413. text-align: center;
  414. }
  415. /* CSS for anigamerinfo+ */
  416. #ani-info {
  417. display: flex;
  418. flex-direction: column;
  419. }
  420. #ani-info .grid {
  421. display: grid;
  422. gap: 10px;
  423. margin-top: 10px
  424. }
  425. #ani-info a {
  426. color: rgb(51, 145, 255)
  427. }
  428. #ani-info .bluebtn {
  429. font-size: 13px;
  430. }
  431. #ani-info .grid.cast {
  432. grid-template-columns: repeat(3, auto);
  433. }
  434. #ani-info .grid.song {
  435. grid-template-columns: repeat(3, auto);
  436. }
  437. /* CSS for anigamer */
  438. .is-hint {
  439. display: none;
  440. }
  441. .ani-tabs {
  442. overflow: scroll;
  443. /* IE and Edge */
  444. -ms-overflow-style: none !important;
  445. /* Firefox */
  446. scrollbar-width: none !important;
  447. }
  448. .ani-tabs::-webkit-scrollbar {
  449. /* Chrome and Safari */
  450. display: none !important;
  451. }
  452. .ani-tabs__item {
  453. flex-shrink: 0;
  454. }
  455. .tool-bar-mask {
  456. background-image: none !important;
  457. }
  458. `
  459. }
  460.  
  461. async function changeState(state, params) {
  462. switch (state) {
  463. case 'init':
  464. $('.anime-option').append(`
  465. <style type='text/css'>${getCss()}</style>
  466. <div id="ani-info">
  467. <ul class="data_type">
  468. <li>
  469. <span>aniInfo+</span>
  470. <i id="ani-info-msg">歡迎使用動畫瘋資訊+</i>
  471. </li>
  472. </ul>
  473. </div>
  474. `)
  475. break
  476. case 'btn':
  477. $('#ani-info-msg').html(`
  478. <div id="ani-info-main" class="bluebtn" onclick="aniInfoMain()">
  479. 讀取動畫資訊
  480. </div>
  481. `)
  482. $('#ani-info-main')[0].addEventListener("click", main, {
  483. once: true
  484. });
  485. break
  486. case 'google':
  487. $('#ani-info-msg').html(`Google搜尋失敗,請點擊<a href="${params.url}" target="_blank">連結</a>解除reCAPTCHA後重整此網頁。`)
  488. break
  489. case 'syoboi':
  490. $('#ani-info-msg').html(`嘗試取得syoboi資料中...`)
  491. break
  492. case 'allcinema':
  493. $('#ani-info-msg').html(`嘗試取得allcinema資料中...`)
  494. break
  495. case 'fail':
  496. $('#ani-info-msg').html(`無法取得資料 ${params.error}`)
  497. break
  498. case 'result': {
  499. let castHtml = await getCastHtml(params.cast)
  500. let songHtml = getSongHtml(params.song)
  501. $('#ani-info').html('')
  502. if (castHtml) $('#ani-info').append(`
  503. <ul class="data_type">
  504. <li>
  505. <span>CAST</span>
  506. <div class="grid cast">${castHtml}</div>
  507. </li>
  508. </ul>
  509. `)
  510. if (songHtml) $('#ani-info').append(`
  511. <ul class="data_type">
  512. <li>
  513. <span>主題曲</span>
  514. <div class="grid song">${songHtml}</div>
  515. </li>
  516. </ul>
  517. `)
  518. $('#ani-info').append(`
  519. <ul class="data_type">
  520. <li>
  521. <span>aniInfo+</span>
  522. 資料來源:<a href="${params.source}" target="_blank">${params.title}</a>
  523. </li>
  524. </ul>
  525. `)
  526. break
  527. }
  528. case 'debug': {
  529. let aaa = await getSyoboi()
  530. let bbb = await getSyoboi(true)
  531. let ccc = await getAllcinema()
  532. let ddd = await getAllcinema(false)
  533. $('#ani-info').html('')
  534. $('#ani-info').append(`
  535. <ul class="data_type">
  536. <li>
  537. <span>aniInfo+</span>
  538. <br>
  539. syoboi:<a href="${aaa?.source}" target="_blank">${aaa?.title}</a>
  540. <br>
  541. allcinema(jp):<a href="${ccc?.source}" target="_blank">${ccc?.title}</a>
  542. <br>
  543. allcinema(en):<a href="${ddd?.source}" target="_blank">${ddd?.title}</a>
  544. <br>
  545. syoboi(google):<a href="${bbb?.source}" target="_blank">${bbb?.title}</a>
  546. <br>
  547. </li>
  548. </ul>
  549. `)
  550. break
  551. }
  552. }
  553. }
  554.  
  555. async function main() {
  556. let debug = false
  557. try {
  558. if (debug) {
  559. changeState('debug')
  560. return
  561. }
  562. let result = null
  563. result = await getSyoboi(false)
  564. if (!result) result = await getAllcinema(true)
  565. if (!result) result = await getAllcinema(false)
  566. if (!result) result = await getSyoboi(true)
  567.  
  568. if (result) changeState('result', result)
  569. else changeState('fail', { error: '' })
  570. } catch (e) {
  571. if (e.type === 'google') {
  572. changeState('google', { url: e.url })
  573. } else {
  574. changeState('fail', { error: e })
  575. }
  576. }
  577. }
  578.  
  579. (async function () {
  580. globalThis.bahaData = await getBahaData()
  581. changeState('init')
  582.  
  583. // Set user option default value.
  584. if (GM_getValue('auto') == undefined) { GM_setValue('auto', true); }
  585.  
  586. // Set user option menu in Tampermonkey.
  587. let isAuto = GM_getValue('auto');
  588. GM_registerMenuCommand(`設定為${isAuto ? '手動' : '自動'}執行`, () => {
  589. GM_setValue('auto', !GM_getValue('auto'));
  590. location.reload();
  591. });
  592.  
  593. // Do task or set button to wait for click and do task.
  594. if (isAuto) main()
  595. else changeState('btn')
  596. })();
  597.  
  598. /**
  599. * Reference:
  600. * [Write userscript in VSC](https://stackoverflow.com/a/55568568)
  601. * [Same above but video](https://www.youtube.com/watch?v=7bWwkTWJy40)
  602. * [Detect browser private mode](https://stackoverflow.com/a/69678895/13069889)
  603. * [and its cdn](https://cdn.jsdelivr.net/gh/Joe12387/detectIncognito@main/detectIncognito.min.js)
  604. * [FF observe GM request](https://firefox-source-docs.mozilla.org/devtools-user/browser_toolbox/index.html)
  605. * [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)
  606. * [Always use en/decodeURIComponent](https://stackoverflow.com/a/747845)
  607. */

QingJ © 2025

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