闪韵灵境歌曲搜索扩展

通过其他网站搜索更多的歌曲

  1. // ==UserScript==
  2. // @name BlitzRhythm Editor Extra Song Search
  3. // @name:en Extra Song Search
  4. // @name:zh-CN 闪韵灵境歌曲搜索扩展
  5. // @namespace cipher-editor-mod-extra-song-search
  6. // @version 1.1.1
  7. // @description Search for more songs from other websites
  8. // @description:en Search for more songs from other websites
  9. // @description:zh-CN 通过其他网站搜索更多的歌曲
  10. // @author Moyuer
  11. // @author:zh-CN 如梦Nya
  12. // @source https://github.com/CMoyuer/BlitzRhythm-Editor-Mod-Loader
  13. // @license MIT
  14. // @run-at document-body
  15. // @grant unsafeWindow
  16. // @grant GM_xmlhttpRequest
  17. // @connect beatsaver.com
  18. // @match https://cipher-editor-cn.picovr.com/*
  19. // @match https://cipher-editor-va.picovr.com/*
  20. // @icon https://cipher-editor-va.picovr.com/favicon.ico
  21. // @require https://code.jquery.com/jquery-3.6.0.min.js
  22. // @require https://gf.qytechs.cn/scripts/473358-jszip/code/main.js
  23. // @require https://gf.qytechs.cn/scripts/473361-xml-http-request-interceptor/code/main.js
  24. // @require https://gf.qytechs.cn/scripts/473362-web-indexeddb-helper/code/main.js
  25. // @require https://gf.qytechs.cn/scripts/474680-blitzrhythm-editor-mod-base-lib/code/main.js
  26. // ==/UserScript==
  27.  
  28. const I18N = {
  29. en: { // English
  30. parameter: {
  31. search_page_sum: {
  32. name: "Search Page Count",
  33. description: "Number of pages searched from BeatSaver at one time",
  34. },
  35. search_timeout: {
  36. name: "Search Timeout",
  37. description: "Timeout for searching for songs",
  38. }
  39. },
  40. methods: {
  41. // test: {
  42. // name: "Test",
  43. // description: "Just a test button",
  44. // },
  45. },
  46. code: {
  47. search: {
  48. fail: "Search song failed!",
  49. tip_timeout: "It seems that the search has timed out. Do you need to modify the timeout parameter?"
  50. },
  51. convert: {
  52. title: "Convert To Custom Beatmap",
  53. description: "Convert official beatmaps to custom beatmaps to export beatmap with ogg file.",
  54. btn_name: "Start Convert",
  55. tip_failed: "Conversion failed, please refresh and try again!"
  56. }
  57. }
  58. },
  59. zh: { // Chinese
  60. parameter: {
  61. search_page_sum: {
  62. name: "搜索页面数量",
  63. description: "每次从BeatSaver搜索歌曲的页数,页数越多速度越慢",
  64. },
  65. search_timeout: {
  66. name: "搜索超时",
  67. description: "搜索歌曲的超时时间",
  68. }
  69. },
  70. methods: {
  71. // test: {
  72. // name: "测试",
  73. // description: "只是一个测试按钮",
  74. // },
  75. },
  76. code: {
  77. search: {
  78. fail: "搜索歌曲失败!",
  79. tip_timeout: "看来搜索超时了, 是否需要修改超时时间?"
  80. },
  81. convert: {
  82. title: "转换为自定义谱面",
  83. description: "将官方谱面转换为自定义谱面, 以导出带有Ogg文件的完整谱面压缩包。",
  84. btn_name: "开始转换谱面",
  85. tip_failed: "转换谱面失败,请刷新再试!"
  86. }
  87. }
  88. }
  89. }
  90.  
  91. const PARAMETER = [
  92. {
  93. id: "search_page_sum",
  94. name: $t("parameter.search_page_sum.name"),
  95. description: $t("parameter.search_page_sum.description"),
  96. type: "number",
  97. default: 1,
  98. min: 1,
  99. max: 10
  100. },
  101. {
  102. id: "search_timeout",
  103. name: $t("parameter.search_timeout.name"),
  104. description: $t("parameter.search_timeout.description"),
  105. type: "number",
  106. default: 10 * 1000,
  107. min: 1000,
  108. max: 20 * 1000
  109. }
  110. ]
  111.  
  112. const METHODS = [
  113. // {
  114. // name: $t("methods.test.name"),
  115. // description: $t("methods.test.description"),
  116. // func: () => {
  117. // log($t("methods.test.name"))
  118. // }
  119. // },
  120. ]
  121.  
  122. let pluginEnabled = false
  123. let timerHandle = 0
  124.  
  125. function onEnabled() {
  126. pluginEnabled = true
  127. let timerFunc = () => {
  128. if (!pluginEnabled) return
  129. CipherUtils.waitLoading().then(() => {
  130. tick()
  131. }).catch(err => {
  132. console.error(err)
  133. }).finally(() => {
  134. timerHandle = setTimeout(timerFunc, 250)
  135. })
  136. }
  137. timerFunc()
  138. }
  139.  
  140. function onDisabled() {
  141. if (timerHandle > 0) {
  142. clearTimeout(timerHandle)
  143. timerHandle = 0
  144. }
  145. pluginEnabled = false
  146. searchFromBeatSaver = false
  147. }
  148.  
  149. function onParameterValueChanged(id, val) {
  150. log("onParameterValueChanged", id, val)
  151. // log("debug", $p(id))
  152. }
  153.  
  154. // =====================================================================================
  155.  
  156. /**
  157. * 闪韵灵境工具类
  158. */
  159. class CipherUtils {
  160. /**
  161. * 获取当前谱面的信息
  162. */
  163. static getNowBeatmapInfo() {
  164. let url = location.href
  165. // ID
  166. let matchId = url.match(/id=(\w*)/)
  167. let id = matchId ? matchId[1] : ""
  168. // BeatSaverID
  169. let beatsaverId = ""
  170. let nameBoxList = $(".css-tpsa02")
  171. if (nameBoxList.length > 0) {
  172. let name = nameBoxList[0].innerHTML
  173. let matchBeatsaverId = name.match(/\[(\w*)\]/)
  174. if (matchBeatsaverId) beatsaverId = matchBeatsaverId[1]
  175. }
  176. // 难度
  177. let matchDifficulty = url.match(/difficulty=(\w*)/)
  178. let difficulty = matchDifficulty ? matchDifficulty[1] : ""
  179. return { id, difficulty, beatsaverId }
  180. }
  181.  
  182. /**
  183. * 添加歌曲校验数据头
  184. * @param {ArrayBuffer} rawBuffer
  185. * @returns {Blob}
  186. */
  187. static addSongVerificationCode(rawBuffer) {
  188. // 前面追加数据,以通过校验
  189. let rawData = new Uint8Array(rawBuffer)
  190. let BYTE_VERIFY_ARRAY = [235, 186, 174, 235, 186, 174, 235, 186, 174, 85, 85]
  191.  
  192. let buffer = new ArrayBuffer(rawData.length + BYTE_VERIFY_ARRAY.length)
  193. let dataView = new DataView(buffer)
  194. for (let i = 0; i < BYTE_VERIFY_ARRAY.length; i++) {
  195. dataView.setUint8(i, BYTE_VERIFY_ARRAY[i])
  196. }
  197. for (let i = 0; i < rawData.length; i++) {
  198. dataView.setUint8(BYTE_VERIFY_ARRAY.length + i, rawData[i])
  199. }
  200. return new Blob([buffer], { type: "application/octet-stream" })
  201. }
  202.  
  203. /**
  204. * 获取当前页面类型
  205. * @returns
  206. */
  207. static getPageType() {
  208. let url = window.location.href
  209. let matchs = url.match(/edit\/(\w{1,})/)
  210. if (!matchs) {
  211. return "home"
  212. } else {
  213. return matchs[1]
  214. }
  215. }
  216.  
  217. /**
  218. * 显示Loading
  219. */
  220. static showLoading() {
  221. let maskBox = $('<div style="position:fixed;top:0;left:0;width:100%;height:100%;background-color:rgba(0,0,0,0.5);z-index:9999;" id="loading"></div>')
  222. maskBox.append('<span style="display: block;position: absolute;width:40px;height:40px;left: calc(50vw - 20px);top: calc(50vh - 20px);"><svg viewBox="22 22 44 44"><circle cx="44" cy="44" r="20.2" fill="none" stroke-width="3.6" class="css-14891ef"></circle></svg></span>')
  223. $("#root").append(maskBox)
  224. }
  225.  
  226. /**
  227. * 隐藏Loading
  228. */
  229. static hideLoading() {
  230. $("#loading").remove()
  231. }
  232.  
  233. /**
  234. * 网页弹窗
  235. */
  236. static showIframe(src) {
  237. this.hideIframe()
  238. let maskBox = $('<div style="position:fixed;top:0;left:0;width:100%;height:100%;background-color:rgba(0,0,0,0.5);z-index:9999;" id="iframe_box"></div>')
  239. maskBox.click(this.hideIframe)
  240. maskBox.append('<iframe src="' + src + '" style="width:calc(100vw - 400px);height:calc(100vh - 200px);position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);border-radius:12px;"></iframe>')
  241. $("#root").append(maskBox)
  242. }
  243.  
  244. /**
  245. * 隐藏Loading
  246. */
  247. static hideIframe() {
  248. $("#iframe_box").remove()
  249. }
  250.  
  251. /**
  252. * 等待Loading结束
  253. * @returns
  254. */
  255. static waitLoading() {
  256. return new Promise((resolve, reject) => {
  257. let handle = setInterval((() => {
  258. let loadingList = $(".css-c81162")
  259. if (loadingList && loadingList.length > 0) return
  260. clearInterval(handle)
  261. resolve()
  262. }), 500)
  263. })
  264. }
  265. }
  266.  
  267. /**
  268. * BeatSaver工具类
  269. */
  270. class BeatSaverUtils {
  271. /**
  272. * 搜索歌曲列表
  273. * @param {string} searchKey 搜索关键字
  274. * @param {number} pageCount 搜索页数
  275. * @returns
  276. */
  277. static searchSongList(searchKey, pageCount = 1) {
  278. return new Promise(function (resolve, reject) {
  279. let songList = []
  280. let songInfoMap = {}
  281. let count = 0
  282. let cbFlag = false
  283. let timeoutCount = 0
  284.  
  285. let beatsaverMappingStr = localStorage.getItem("BeatSaverMapping")
  286. let beatSaverMapping = beatsaverMappingStr ? JSON.parse(beatsaverMappingStr) : {
  287. mapping: {}
  288. }
  289.  
  290. let funDone = () => {
  291. if (++count != pageCount) return
  292. cbFlag = true
  293. resolve({ songList, songInfoMap })
  294. if (timeoutCount > 0) {
  295. let flag = confirm($t("code.search.tip_timeout"))
  296. if (flag) showSetupPage()
  297. }
  298. }
  299. let funSuccess = data => {
  300. // 填充数据
  301. data.docs.forEach(rawInfo => {
  302. let artist = rawInfo.metadata.songAuthorName
  303. let bpm = rawInfo.metadata.bpm
  304. let cover = rawInfo.versions[0].coverURL
  305. let song_name = "[" + rawInfo.id + "]" + rawInfo.metadata.songName
  306. let id = beatSaverMapping.mapping[rawInfo.id]
  307. if (typeof id !== "number")
  308. id = 80000000000 + parseInt(rawInfo.id, 36)
  309. songList.push({ artist, bpm, cover, song_name, id })
  310. let downloadURL = rawInfo.versions[0].downloadURL
  311. let previewURL = rawInfo.versions[0].previewURL
  312. songInfoMap[id] = { rawInfo, downloadURL, previewURL }
  313. })
  314. funDone()
  315. }
  316. let funFail = res => {
  317. if (res[0] === "timeout") timeoutCount++
  318. funDone()
  319. }
  320. for (let i = 0; i < pageCount; i++) {
  321. Utils.ajax({
  322. url: "https://api.beatsaver.com/search/text/" + i + "?sortOrder=Relevance&q=" + searchKey,
  323. method: "GET",
  324. responseType: "json",
  325. timeout: $p("search_timeout")
  326. }).then(funSuccess).catch(funFail)
  327. }
  328. })
  329. }
  330.  
  331.  
  332. /**
  333. * 从BeatSaver下载ogg文件
  334. * @param {number} zipUrl 歌曲压缩包链接
  335. * @param {function} onprogress 进度回调
  336. * @returns {Promise<blob, any>}
  337. */
  338. static async downloadSongFile(zipUrl, onprogress) {
  339. let blob = await Utils.downloadZipFile(zipUrl, onprogress)
  340. // 解压出ogg文件
  341. return await BeatSaverUtils.getOggFromZip(blob)
  342. }
  343.  
  344. /**
  345. * 从压缩包中提取出ogg文件
  346. * @param {blob} zipBlob
  347. * @param {boolean | undefined} verification
  348. * @returns
  349. */
  350. static async getOggFromZip(zipBlob, verification = true) {
  351. let zip = await JSZip.loadAsync(zipBlob)
  352. let eggFile = undefined
  353. for (let fileName in zip.files) {
  354. if (!fileName.endsWith(".egg")) continue
  355. eggFile = zip.file(fileName)
  356. break
  357. }
  358. if (verification) {
  359. let rawBuffer = await eggFile.async("arraybuffer")
  360. return CipherUtils.addSongVerificationCode(rawBuffer)
  361. } else {
  362. return await eggFile.async("blob")
  363. }
  364. }
  365. }
  366.  
  367. /**
  368. * 通用工具类
  369. */
  370. class Utils {
  371. /**
  372. * 下载压缩包文件
  373. * @param {number} zipUrl 歌曲压缩包链接
  374. * @param {function | undefined} onprogress 进度回调
  375. * @returns {Promise}
  376. */
  377. static downloadZipFile(zipUrl, onprogress) {
  378. return new Promise(function (resolve, reject) {
  379. Utils.ajax({
  380. url: zipUrl,
  381. method: "GET",
  382. responseType: "blob",
  383. onprogress,
  384. }).then(data => {
  385. resolve(new Blob([data], { type: "application/zip" }))
  386. }).catch(reject)
  387. })
  388. }
  389.  
  390. /**
  391. * 异步发起网络请求
  392. * @param {object} config
  393. * @returns
  394. */
  395. static ajax(config) {
  396. return new Promise((resolve, reject) => {
  397. config.onload = res => {
  398. if (res.status >= 200 && res.status < 300) {
  399. try {
  400. resolve(JSON.parse(res.response))
  401. } catch {
  402. resolve(res.response)
  403. }
  404. }
  405. else {
  406. reject("HTTP Code: " + res.status)
  407. }
  408. }
  409. config.onerror = (...data) => {
  410. reject(["error", ...data])
  411. }
  412. config.ontimeout = (...data) => {
  413. reject(["timeout", ...data])
  414. }
  415. GM_xmlhttpRequest(config)
  416. })
  417. }
  418. }
  419.  
  420. // =====================================================================================
  421.  
  422. let searchFromBeatSaver = false
  423. let songInfoMap = {}
  424. let lastPageType = "other"
  425.  
  426. // 加载XHR拦截器
  427. function initXHRIntercept() {
  428. let _this = this
  429. let xhrIntercept = new XHRIntercept()
  430. /**
  431. * @param {XMLHttpRequest} self
  432. * @param {IArguments} args
  433. * @param {function} complete
  434. * @returns {boolean} 是否匹配
  435. */
  436. let onSend = function (self, args, complete) {
  437. let url = self._url
  438. if (!url || !searchFromBeatSaver) return
  439. if (url.startsWith("/song/staticList")) {
  440. // 获取歌曲列表
  441. let result = decodeURI(url).match(/songName=(\S*)&/)
  442. let key = ""
  443. if (result) key = result[1].replace("+", " ")
  444. CipherUtils.showLoading()
  445. BeatSaverUtils.searchSongList(key, $p("search_page_sum")).then(res => {
  446. self.extraSongList = res.songList
  447. songInfoMap = res.songInfoMap
  448. complete()
  449. }).catch(err => {
  450. alert($t("code.search.fail"))
  451. console.error(err)
  452. self.extraSongList = []
  453. complete()
  454. }).finally(() => {
  455. CipherUtils.hideLoading()
  456. })
  457.  
  458. self.addEventListener("readystatechange", function () {
  459. if (this.readyState !== this.DONE) return
  460. const res = JSON.parse(this.responseText)
  461. if (this.extraSongList) {
  462. res.data.data = this.extraSongList
  463. res.data.total = res.data.data.length
  464. this.extraSongList = []
  465. }
  466. Object.defineProperty(this, 'responseText', {
  467. writable: true
  468. });
  469. this.responseText = JSON.stringify(res)
  470. setTimeout(() => {
  471. fixSongListStyle()
  472. addPreviewFunc()
  473. }, 200)
  474. });
  475. return true
  476. } else if (url.startsWith("/beatsaver/")) {
  477. let _onprogress = self.onprogress
  478. self.onprogress = undefined
  479.  
  480. // 从BeatSaver下载歌曲
  481. let result = decodeURI(url).match(/\d{1,}/)
  482. let id = parseInt(result[0])
  483. BeatSaverUtils.downloadSongFile(songInfoMap[id].downloadURL, _onprogress).then(oggBlob => {
  484. songInfoMap[id].ogg = oggBlob
  485. saveBeatSaverMapping(id, songInfoMap[id].rawInfo)
  486. complete()
  487. }).catch(err => {
  488. console.error(err)
  489. self.onerror(err)
  490. })
  491.  
  492. self.addEventListener("readystatechange", function () {
  493. if (this.readyState !== this.DONE) return
  494. let result = decodeURI(url).match(/\d{1,}/)
  495. let id = parseInt(result[0])
  496. Object.defineProperty(this, 'response', {
  497. writable: true
  498. });
  499. this.response = songInfoMap[id].ogg
  500. });
  501. return true
  502. } else if (url.startsWith("/song/ogg")) {
  503. // 获取ogg文件下载链接
  504. let result = decodeURI(url).match(/id=(\d*)/)
  505. let id = parseInt(result[1])
  506. if (id < 80000000000) return
  507. self.addEventListener("readystatechange", function () {
  508. if (this.readyState !== this.DONE) return
  509. const res = JSON.parse(this.responseText)
  510. res.code = 0
  511. res.data = { link: "/beatsaver/" + id }
  512. res.msg = "success"
  513. Object.defineProperty(this, 'responseText', {
  514. writable: true
  515. });
  516. this.responseText = JSON.stringify(res)
  517. });
  518. complete()
  519. return true
  520. }
  521. }
  522. xhrIntercept.onSend(onSend)
  523. }
  524.  
  525. // Save BeatSaver Info
  526. function saveBeatSaverMapping(id, rawInfo) {
  527. let beatsaverMappingStr = localStorage.getItem("BeatSaverMapping")
  528. let beatSaverMapping = beatsaverMappingStr ? JSON.parse(beatsaverMappingStr) : {}
  529. if (!beatSaverMapping.mapping) beatSaverMapping.mapping = {}
  530. beatSaverMapping.mapping[rawInfo.id] = id
  531. localStorage.setItem("BeatSaverMapping", JSON.stringify(beatSaverMapping))
  532. }
  533.  
  534. /**
  535. * 更新数据库
  536. * @param {Boolean} isForce 强制转换
  537. * @returns
  538. */
  539. async function updateDatabase(isForce) {
  540. let BLITZ_RHYTHM = await WebDB.open("BLITZ_RHYTHM")
  541. let BLITZ_RHYTHM_files = await WebDB.open("BLITZ_RHYTHM-files")
  542. let BLITZ_RHYTHM_official = await WebDB.open("BLITZ_RHYTHM-official")
  543. let songInfos = []
  544. let hasChanged = false
  545. let songsInfo
  546. // 更新歌曲信息
  547. {
  548. let rawSongs = await BLITZ_RHYTHM.get("keyvaluepairs", "persist:songs")
  549. songsInfo = JSON.parse(rawSongs)
  550. let songsById = JSON.parse(songsInfo.byId)
  551. for (let key in songsById) {
  552. let officialId = songsById[key].officialId
  553. if (typeof officialId != "number" || (!isForce && officialId < 80000000000)) continue
  554. let songInfo = songsById[key]
  555. songInfos.push(JSON.parse(JSON.stringify(songInfo)))
  556. songInfo.coverArtFilename = songInfo.coverArtFilename.replace("" + songInfo.officialId, songInfo.id)
  557. songInfo.songFilename = songInfo.songFilename.replace("" + songInfo.officialId, songInfo.id)
  558. songInfo.officialId = ""
  559.  
  560. // Add Source Info
  561. if (!songInfo.modSettings) songInfo.modSettings = {}
  562. if (!songInfo.modSettings.source) songInfo.modSettings.source = {}
  563. try {
  564. let beatsaverMapping = JSON.parse(localStorage.getItem("BeatSaverMapping") || "{}")
  565. let mapping = beatsaverMapping.mapping || {}
  566. for (let bsId in mapping) {
  567. if (mapping[bsId] !== officialId) continue
  568. songInfo.modSettings.source.beatsaverId = bsId
  569. break
  570. }
  571. } catch (error) {
  572. console.error("Add source info failed:", error)
  573. }
  574. songsById[key] = songInfo
  575. hasChanged = true
  576. }
  577. songsInfo.byId = JSON.stringify(songsById)
  578. }
  579. // 处理文件
  580. for (let index in songInfos) {
  581. let songInfo = songInfos[index]
  582. // 复制封面和音乐文件
  583. let cover = await BLITZ_RHYTHM_official.get("keyvaluepairs", songInfo.coverArtFilename)
  584. let song = await BLITZ_RHYTHM_official.get("keyvaluepairs", songInfo.songFilename)
  585. await BLITZ_RHYTHM_files.put("keyvaluepairs", songInfo.coverArtFilename.replace("" + songInfo.officialId, songInfo.id), cover)
  586. await BLITZ_RHYTHM_files.put("keyvaluepairs", songInfo.songFilename.replace("" + songInfo.officialId, songInfo.id), song)
  587. // 添加info记录
  588. await BLITZ_RHYTHM_files.put("keyvaluepairs", songInfo.id + "_Info.dat", JSON.stringify({ _songFilename: "song.ogg" }))
  589. }
  590. // 保存数据
  591. if (hasChanged) await BLITZ_RHYTHM.put("keyvaluepairs", "persist:songs", JSON.stringify(songsInfo))
  592. BLITZ_RHYTHM.close()
  593. BLITZ_RHYTHM_files.close()
  594. BLITZ_RHYTHM_official.close()
  595. return hasChanged
  596. }
  597. /**
  598. * 修复歌单布局
  599. */
  600. function fixSongListStyle() {
  601. let songListBox = $(".css-10szcx0")[0]
  602. songListBox.style["grid-template-columns"] = "repeat(3, minmax(0px, 1fr))"
  603. let songBox = songListBox.parentNode
  604. if ($(".css-1wfsuwr").length > 0) {
  605. songBox.style["overflow-y"] = "hidden"
  606. songBox.parentNode.style["margin-bottom"] = ""
  607. } else {
  608. songBox.style["overflow-y"] = "auto"
  609. songBox.parentNode.style["margin-bottom"] = "44px"
  610. }
  611. let itemBox = $(".css-bil4eh")
  612. for (let index = 0; index < itemBox.length; index++)
  613. itemBox[index].style.width = "230px"
  614. }
  615. /**
  616. * 在歌曲Card中添加双击预览功能
  617. */
  618. function addPreviewFunc() {
  619. let searchBox = $(".css-1d92frk")
  620. $("#preview_tip").remove()
  621. searchBox.after("<div style='text-align: center;color:gray;padding-bottom:10px;' id='preview_tip'>双击歌曲可预览曲谱</div>")
  622. let infoViewList = $(".css-bil4eh")
  623. for (let index = 0; index < infoViewList.length; index++) {
  624. infoViewList[index].ondblclick = () => {
  625. let name = $(infoViewList[index]).find(".css-1y1rcqj")[0].innerHTML
  626. let result = name.match(/^\[(\w*)\]/)
  627. if (!result) return
  628. let previewUrl = "https://skystudioapps.com/bs-viewer/?id=" + result[1]
  629. CipherUtils.showIframe(previewUrl)
  630. // window.open(previewUrl)
  631. }
  632. }
  633. }
  634. /**
  635. * 添加通过BeatSaver搜索歌曲的按钮
  636. */
  637. function applySearchButton() {
  638. let boxList = $(".css-1u8wof2") // 弹窗
  639. try {
  640. if (boxList.length == 0) throw "Box not found"
  641. let searchBoxList = boxList.find(".css-70qvj9")
  642. if (searchBoxList.length == 0) throw "item too few" // 搜索栏元素数量
  643. if (searchBoxList[0].childNodes.length >= 3) return // 搜索栏元素数量
  644. } catch {
  645. if (searchFromBeatSaver) searchFromBeatSaver = false
  646. return
  647. }
  648.  
  649. let rawSearchBtn = $(boxList[0]).find("button")[0] // 搜索按钮
  650.  
  651. // 添加一个按钮
  652. let searchBtn = document.createElement("button")
  653. searchBtn.className = rawSearchBtn.className
  654. searchBtn.innerHTML = "BeatSaver"
  655. $(rawSearchBtn.parentNode).append(searchBtn);
  656.  
  657. // 绑定事件
  658. rawSearchBtn.onmousedown = () => {
  659. searchFromBeatSaver = false
  660. $("#preview_tip").remove()
  661. }
  662. searchBtn.onmousedown = () => {
  663. searchFromBeatSaver = true
  664. $(rawSearchBtn).click()
  665. }
  666. }
  667. /**
  668. * 添加转换官方谱面的按钮
  669. * @returns
  670. */
  671. async function applyConvertCiphermapButton() {
  672. let BLITZ_RHYTHM = await WebDB.open("BLITZ_RHYTHM")
  673. try {
  674. let rawSongs = await BLITZ_RHYTHM.get("keyvaluepairs", "persist:songs")
  675. let songsInfo = JSON.parse(rawSongs)
  676. let songsById = JSON.parse(songsInfo.byId)
  677. let songId = CipherUtils.getNowBeatmapInfo().id
  678. let officialId = songsById[songId].officialId
  679. if (!officialId) return
  680. } catch (error) {
  681. console.error(error)
  682. return
  683. } finally {
  684. BLITZ_RHYTHM.close()
  685. }
  686.  
  687. let divList = $(".css-1tiz3p0")
  688. if (divList.length > 0) {
  689. if ($("#div-custom").length > 0) return
  690. let divBox = $(divList[0]).clone()
  691. divBox[0].id = "div-custom"
  692. divBox.find(".css-ujbghi")[0].innerHTML = $t("code.convert.title")
  693. divBox.find(".css-1exyu3y")[0].innerHTML = $t("code.convert.description")
  694. divBox.find(".css-1y7rp4x")[0].innerText = $t("code.convert.btn_name")
  695. divBox[0].onclick = e => {
  696. // 更新歌曲信息
  697. this.updateDatabase(true).then((hasChanged) => {
  698. if (hasChanged) setTimeout(() => { window.location.reload() }, 1000)
  699. }).catch(err => {
  700. console.log("Convert map failed:", err)
  701. alert($t("code.convert.btn_name"))
  702. })
  703. }
  704. $(divList[0].parentNode).append(divBox)
  705. }
  706. }
  707.  
  708. /**
  709. * 隐藏按钮
  710. */
  711. function hideConvertCiphermapButton() {
  712. $("#div-custom").remove()
  713. }
  714. /**
  715. * 定时任务 1s
  716. */
  717. function tick() {
  718. let pageType = CipherUtils.getPageType()
  719. if (pageType !== "home") {
  720. if (pageType != lastPageType) {
  721. // 隐藏按钮
  722. if (pageType !== "download")
  723. hideConvertCiphermapButton()
  724. // 更新歌曲信息
  725. updateDatabase().then((hasChanged) => {
  726. if (hasChanged) setTimeout(() => { window.location.reload() }, 1000)
  727. }).catch(err => {
  728. console.log("Update map info failed:", err)
  729. alert($t("tip_failed"))
  730. })
  731. } else if (pageType === "download") {
  732. applyConvertCiphermapButton()
  733. }
  734. } else {
  735. applySearchButton()
  736. }
  737. lastPageType = pageType
  738. }
  739.  
  740. (function () {
  741. 'use strict'
  742.  
  743. // 初始化XHR拦截器
  744. initXHRIntercept()
  745. })()

QingJ © 2025

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