闪韵灵境谱面导入扩展

将BeatSaber谱面导入到闪韵灵境编辑器内

  1. // ==UserScript==
  2. // @name BlitzRhythm Editor Extra Beatmap Import
  3. // @name:en Extra Beatmap Import
  4. // @name:zh-CN 闪韵灵境谱面导入扩展
  5. // @namespace cipher-editor-mod-extra-beatmap-import
  6. // @version 1.1.0
  7. // @description Import BeatSaber beatmap into the BlitzRhythm editor
  8. // @description:en Import BeatSaber beatmap into the BlitzRhythm editor
  9. // @description:zh-CN 将BeatSaber谱面导入到闪韵灵境编辑器内
  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://greasyfork.org/scripts/473358-jszip/code/main.js?version=1237031
  23. // @require https://greasyfork.org/scripts/473361-xml-http-request-interceptor/code/main.js
  24. // @require https://greasyfork.org/scripts/473362-web-indexeddb-helper/code/main.js
  25. // @require https://greasyfork.org/scripts/474680-blitzrhythm-editor-mod-base-lib/code/main.js
  26. // ==/UserScript==
  27.  
  28. const I18N = {
  29. en: { // English
  30. parameter: {
  31. download_timeout: {
  32. name: "Download Timeout",
  33. description: "Timeout for download for beatmap",
  34. }
  35. },
  36. methods: {},
  37. code: {
  38. tip: {
  39. info_file_not_found: "Please check whether the zip file contains the info.dat file!",
  40. input_bs_url: "Please enter the BeatSaver beatmap URL:",
  41. url_format_error: "URL format error!",
  42. not_support_map_ver: "Not support this beatmap version! You can try to recreate the beatmap.",
  43. not_found_diff: "No available difficulty found for this map!",
  44. input_diff: "Enter the difficulty level (index) you want to import:\r\n",
  45. input_index_err: "Please enter the correct index!",
  46. not_support_bs_ver: "This map version ({0}) is not supported yet, please change the URL and try again!",
  47. import_map_err: "An error occurred while importing map! You can refresh and try again..."
  48. },
  49. button: {
  50. import_from_url: "Import from BeatSaver URL",
  51. import_from_file: "Import from BeatSaber zip",
  52. }
  53. }
  54. },
  55. zh: { // Chinese
  56. parameter: {
  57. download_timeout: {
  58. name: "下载超时",
  59. description: "下载谱面的超时时间",
  60. }
  61. },
  62. methods: {},
  63. code: {
  64. tip: {
  65. info_file_not_found: "请检查压缩包中是否包含info.dat文件",
  66. input_bs_url: "请输入BeatSaver铺面链接",
  67. url_format_error: "链接格式错误!",
  68. not_support_map_ver: "插件不支持该谱面版本!可尝试重新创建谱面",
  69. not_found_diff: "该谱面找不到可用的难度",
  70. input_diff: "请问要导入第几个难度(数字):\r\n",
  71. input_index_err: "请输入准确的序号!",
  72. not_support_bs_ver: "暂不支持该谱面的版本({0}),请换个链接再试!",
  73. import_map_err: "导入谱面时发生错误!可刷新页面重试..."
  74. },
  75. button: {
  76. import_from_url: "导入谱面 BeatSaver链接",
  77. import_from_file: "导入谱面 BeatSaber压缩包",
  78. }
  79. }
  80. }
  81. }
  82.  
  83. const PARAMETER = [
  84. {
  85. id: "download_timeout",
  86. name: $t("parameter.download_timeout.name"),
  87. description: $t("parameter.download_timeout.description"),
  88. type: "number",
  89. default: 60 * 1000,
  90. min: 1000,
  91. max: 2 * 60 * 1000
  92. }
  93. ]
  94.  
  95. const METHODS = [
  96. // {
  97. // name: $t("methods.test.name"),
  98. // description: $t("methods.test.description"),
  99. // func: () => {
  100. // log($t("methods.test.name"))
  101. // }
  102. // },
  103. ]
  104.  
  105. let pluginEnabled = false
  106. let timerHandle = 0
  107.  
  108. function onEnabled() {
  109. pluginEnabled = true
  110. let timerFunc = () => {
  111. if (!pluginEnabled) return
  112. CipherUtils.waitLoading().then(() => {
  113. tick()
  114. }).catch(err => {
  115. console.error(err)
  116. }).finally(() => {
  117. timerHandle = setTimeout(timerFunc, 250)
  118. })
  119. }
  120. timerFunc()
  121. }
  122.  
  123. function onDisabled() {
  124. if (timerHandle > 0) {
  125. clearTimeout(timerHandle)
  126. timerHandle = 0
  127. }
  128. pluginEnabled = false
  129. }
  130.  
  131. function onParameterValueChanged(id, val) {
  132. log("onParameterValueChanged", id, val)
  133. // log("debug", $p(id))
  134. }
  135.  
  136. // =====================================================================================
  137.  
  138. /**
  139. * 闪韵灵境工具类
  140. */
  141. class CipherUtils {
  142. /**
  143. * 获取当前谱面的信息
  144. */
  145. static getNowBeatmapInfo() {
  146. let url = location.href
  147. // ID
  148. let matchId = url.match(/id=(\w*)/)
  149. let id = matchId ? matchId[1] : ""
  150. // BeatSaverID
  151. let beatsaverId = ""
  152. let nameBoxList = $(".css-tpsa02")
  153. if (nameBoxList.length > 0) {
  154. let name = nameBoxList[0].innerHTML
  155. let matchBeatsaverId = name.match(/\[(\w*)\]/)
  156. if (matchBeatsaverId) beatsaverId = matchBeatsaverId[1]
  157. }
  158. // 难度
  159. let matchDifficulty = url.match(/difficulty=(\w*)/)
  160. let difficulty = matchDifficulty ? matchDifficulty[1] : ""
  161. return { id, difficulty, beatsaverId }
  162. }
  163.  
  164. /**
  165. * 添加歌曲校验数据头
  166. * @param {ArrayBuffer} rawBuffer
  167. * @returns {Blob}
  168. */
  169. static addSongVerificationCode(rawBuffer) {
  170. // 前面追加数据,以通过校验
  171. let rawData = new Uint8Array(rawBuffer)
  172. let BYTE_VERIFY_ARRAY = [235, 186, 174, 235, 186, 174, 235, 186, 174, 85, 85]
  173.  
  174. let buffer = new ArrayBuffer(rawData.length + BYTE_VERIFY_ARRAY.length)
  175. let dataView = new DataView(buffer)
  176. for (let i = 0; i < BYTE_VERIFY_ARRAY.length; i++) {
  177. dataView.setUint8(i, BYTE_VERIFY_ARRAY[i])
  178. }
  179. for (let i = 0; i < rawData.length; i++) {
  180. dataView.setUint8(BYTE_VERIFY_ARRAY.length + i, rawData[i])
  181. }
  182. return new Blob([buffer], { type: "application/octet-stream" })
  183. }
  184.  
  185. /**
  186. * 获取页面参数
  187. * @returns
  188. */
  189. static getPageParmater() {
  190. let url = window.location.href
  191. let matchs = url.match(/\?import=(\w{1,})@(\w{1,})@(\w{1,})/)
  192. if (!matchs) return
  193. return {
  194. event: "import",
  195. source: matchs[1],
  196. id: matchs[2],
  197. mode: matchs[3],
  198. }
  199. }
  200.  
  201. /**
  202. * 关闭编辑器顶部菜单
  203. */
  204. static closeEditorTopMenu() {
  205. $(".css-1k12r02").click()
  206. }
  207.  
  208. /**
  209. * 显示Loading
  210. */
  211. static showLoading() {
  212. 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>')
  213. 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>')
  214. $("#root").append(maskBox)
  215. }
  216.  
  217. /**
  218. * 隐藏Loading
  219. */
  220. static hideLoading() {
  221. $("#loading").remove()
  222. }
  223.  
  224. /**
  225. * 网页弹窗
  226. */
  227. static showIframe(src) {
  228. this.hideIframe()
  229. 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>')
  230. maskBox.click(this.hideIframe)
  231. 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>')
  232. $("#root").append(maskBox)
  233. }
  234.  
  235. /**
  236. * 隐藏Loading
  237. */
  238. static hideIframe() {
  239. $("#iframe_box").remove()
  240. }
  241.  
  242. /**
  243. * 等待Loading结束
  244. * @returns
  245. */
  246. static waitLoading() {
  247. return new Promise((resolve, reject) => {
  248. let handle = setInterval((() => {
  249. let loadingList = $(".css-c81162")
  250. if (loadingList && loadingList.length > 0) return
  251. clearInterval(handle)
  252. resolve()
  253. }), 500)
  254. })
  255. }
  256. }
  257.  
  258. /**
  259. * BeatSaver工具类
  260. */
  261. class BeatSaverUtils {
  262. /**
  263. * 搜索歌曲列表
  264. * @param {string} searchKey 搜索关键字
  265. * @param {number} pageCount 搜索页数
  266. * @returns
  267. */
  268. static searchSongList(searchKey, pageCount = 1) {
  269. return new Promise(function (resolve, reject) {
  270. let songList = []
  271. let songInfoMap = {}
  272. let count = 0
  273. let cbFlag = false
  274. let func = data => {
  275. // 填充数据
  276. data.docs.forEach(rawInfo => {
  277. let artist = rawInfo.metadata.songAuthorName
  278. let bpm = rawInfo.metadata.bpm
  279. let cover = rawInfo.versions[0].coverURL
  280. let song_name = "[" + rawInfo.id + "]" + rawInfo.metadata.songName
  281. let id = 80000000000 + parseInt(rawInfo.id, 36)
  282. songList.push({ artist, bpm, cover, song_name, id })
  283.  
  284. let downloadURL = rawInfo.versions[0].downloadURL
  285. let previewURL = rawInfo.versions[0].previewURL
  286. songInfoMap[id] = { downloadURL, previewURL }
  287. })
  288. if (++count == pageCount) {
  289. cbFlag = true
  290. resolve({ songList, songInfoMap })
  291. }
  292. }
  293. for (let i = 0; i < pageCount; i++) {
  294. Utils.ajax({
  295. url: "https://api.beatsaver.com/search/text/" + i + "?sortOrder=Relevance&q=" + searchKey,
  296. method: "GET",
  297. responseType: "json"
  298. }).then(func)
  299. }
  300. })
  301. }
  302.  
  303.  
  304. /**
  305. * 从BeatSaver下载ogg文件
  306. * @param {number} zipUrl 歌曲压缩包链接
  307. * @param {function} onprogress 进度回调
  308. * @returns {Promise<blob, any>}
  309. */
  310. static async downloadSongFile(zipUrl, onprogress) {
  311. let blob = await Utils.downloadZipFile(zipUrl, onprogress)
  312. // 解压出ogg文件
  313. return await BeatSaverUtils.getOggFromZip(blob)
  314. }
  315.  
  316. /**
  317. * 从压缩包中提取出ogg文件
  318. * @param {blob} zipBlob
  319. * @param {boolean | undefined} verification
  320. * @returns
  321. */
  322. static async getOggFromZip(zipBlob, verification = true) {
  323. let zip = await JSZip.loadAsync(zipBlob)
  324. let eggFile = undefined
  325. for (let fileName in zip.files) {
  326. if (!fileName.endsWith(".egg")) continue
  327. eggFile = zip.file(fileName)
  328. break
  329. }
  330. if (verification) {
  331. let rawBuffer = await eggFile.async("arraybuffer")
  332. return CipherUtils.addSongVerificationCode(rawBuffer)
  333. } else {
  334. return await eggFile.async("blob")
  335. }
  336. }
  337.  
  338. /**
  339. * 获取压缩包下载链接
  340. * @param {string} id 歌曲ID
  341. * @return {Promise}
  342. */
  343. static getDownloadUrl(id) {
  344. return new Promise(function (resolve, reject) {
  345. Utils.ajax({
  346. url: "https://api.beatsaver.com/maps/id/" + id,
  347. method: "GET",
  348. responseType: "json",
  349. }).then(data => {
  350. resolve(data.versions[0].downloadURL)
  351. }).catch(err => {
  352. reject(err)
  353. })
  354. })
  355. }
  356.  
  357. /**
  358. * 从压缩包中提取曲谱难度文件
  359. * @param {Blob} zipBlob
  360. * @returns
  361. */
  362. static async getBeatmapInfo(zipBlob) {
  363. let zip = await JSZip.loadAsync(zipBlob)
  364. // 谱面信息
  365. let infoFile
  366. for (let fileName in zip.files) {
  367. if (fileName.toLowerCase() !== "info.dat") continue
  368. infoFile = zip.files[fileName]
  369. break
  370. }
  371. if (!infoFile) throw $t("code.tip.info_file_not_found")
  372. let rawBeatmapInfo = JSON.parse(await infoFile.async("string"))
  373. // 难度列表
  374. let difficultyBeatmaps
  375. let diffBeatmapSets = rawBeatmapInfo._difficultyBeatmapSets
  376. for (let a in diffBeatmapSets) {
  377. let info = diffBeatmapSets[a]
  378. if (info["_beatmapCharacteristicName"] !== "Standard") continue
  379. difficultyBeatmaps = info._difficultyBeatmaps
  380. break
  381. }
  382. // 难度对应文件名
  383. let beatmapInfo = {
  384. raw: rawBeatmapInfo,
  385. version: rawBeatmapInfo._version,
  386. levelAuthorName: rawBeatmapInfo._levelAuthorName,
  387. difficulties: []
  388. }
  389. for (let index in difficultyBeatmaps) {
  390. let difficultyInfo = difficultyBeatmaps[index]
  391. let difficulty = difficultyInfo._difficulty
  392. let difficultyLabel = ""
  393. if (difficultyInfo._customData && difficultyInfo._customData._difficultyLabel)
  394. difficultyLabel = difficultyInfo._customData._difficultyLabel
  395. beatmapInfo.difficulties.push({
  396. difficulty,
  397. difficultyLabel,
  398. file: zip.files[difficultyInfo._beatmapFilename]
  399. })
  400. }
  401. return beatmapInfo
  402. }
  403. }
  404.  
  405. /**
  406. * 通用工具类
  407. */
  408. class Utils {
  409. /**
  410. * 下载压缩包文件
  411. * @param {number} zipUrl 歌曲压缩包链接
  412. * @param {function | undefined} onprogress 进度回调
  413. * @returns {Promise}
  414. */
  415. static downloadZipFile(zipUrl, onprogress) {
  416. return new Promise(function (resolve, reject) {
  417. Utils.ajax({
  418. url: zipUrl,
  419. method: "GET",
  420. responseType: "blob",
  421. onprogress,
  422. }).then(data => {
  423. resolve(new Blob([data], { type: "application/zip" }))
  424. }).catch(reject)
  425. })
  426. }
  427.  
  428. /**
  429. * 获取音乐文件时长
  430. * @param {Blob} blob
  431. */
  432. static getOggDuration(blob) {
  433. return new Promise((resolve, reject) => {
  434. let reader = new FileReader()
  435. reader.onerror = () => {
  436. reject(reader.error)
  437. }
  438. reader.onload = (e) => {
  439. let audio = document.createElement('audio')
  440. audio.addEventListener("loadedmetadata", () => {
  441. resolve(audio.duration)
  442. $(audio).remove()
  443. })
  444. audio.addEventListener('error', () => {
  445. reject(audio.error)
  446. $(audio).remove()
  447. })
  448. audio.src = e.target.result
  449. }
  450. reader.readAsDataURL(new File([blob], "song.ogg", { type: "audio/ogg" }))
  451. })
  452. }
  453.  
  454. /**
  455. * 异步发起网络请求
  456. * @param {object} config
  457. * @returns
  458. */
  459. static ajax(config) {
  460. return new Promise((resolve, reject) => {
  461. config.onload = res => {
  462. if (res.status >= 200 && res.status < 300) {
  463. try {
  464. resolve(JSON.parse(res.response))
  465. } catch {
  466. resolve(res.response)
  467. }
  468. }
  469. else {
  470. reject("HTTP Code: " + res.status)
  471. }
  472. }
  473. config.onerror = err => {
  474. reject(err)
  475. }
  476. GM_xmlhttpRequest(config)
  477. })
  478. }
  479. }
  480.  
  481. // =====================================================================================
  482.  
  483. /**
  484. * 在顶部菜单添加导入按钮
  485. */
  486. function addImportButton() {
  487. if ($("#importBeatmap").length > 0) return
  488. let btnsBoxList = $(".css-4e93fo")
  489. if (btnsBoxList.length == 0) return
  490. // 按键组
  491. let div = document.createElement("div")
  492. div.style["display"] = "flex"
  493. // 按钮模板
  494. let btnTemp = $(btnsBoxList[0].childNodes[0])
  495. // 按钮1
  496. let btnImportBs = btnTemp.clone()[0]
  497. btnImportBs.id = "importBeatmap"
  498. btnImportBs.innerHTML = $t("code.button.import_from_url")
  499. btnImportBs.onclick = importFromBeatSaver
  500. btnImportBs.style["font-size"] = "13px"
  501. div.append(btnImportBs)
  502. // 按钮2
  503. let btnImportZip = btnTemp.clone()[0]
  504. btnImportZip.id = "importBeatmap"
  505. btnImportZip.innerHTML = $t("code.button.import_from_file")
  506. btnImportZip.onclick = importFromBeatmapZip
  507. btnImportZip.style["margin-left"] = "5px"
  508. btnImportZip.style["font-size"] = "13px"
  509. div.append(btnImportZip)
  510. // 添加
  511. btnsBoxList[0].prepend(div)
  512. }
  513.  
  514. async function importFromBeatSaver() {
  515. try {
  516. // 获取当前谱面信息
  517. let nowBeatmapInfo = CipherUtils.getNowBeatmapInfo()
  518.  
  519. // 获取谱面信息
  520. let url = prompt($t("code.tip.input_bs_url"), "https://beatsaver.com/maps/" + nowBeatmapInfo.beatsaverId)
  521. if (!url) return
  522. let result = url.match(/^https:\/\/beatsaver.com\/maps\/(\S*)$/)
  523. if (!result) {
  524. alert($t("code.tip.url_format_error"))
  525. return
  526. }
  527. CipherUtils.showLoading()
  528. let downloadUrl = await BeatSaverUtils.getDownloadUrl(result[1])
  529. let zipBlob = await Utils.downloadZipFile(downloadUrl)
  530. await importBeatmap(zipBlob, nowBeatmapInfo)
  531. } catch (err) {
  532. console.error(err)
  533. alert("Import Failed: " + err)
  534. CipherUtils.hideLoading()
  535. }
  536. }
  537.  
  538. /**
  539. * 通过压缩文件导入
  540. */
  541. function importFromBeatmapZip() {
  542. try {
  543. // 创建上传按钮
  544. let fileSelect = document.createElement('input')
  545. fileSelect.type = 'file'
  546. fileSelect.style.display = "none"
  547.  
  548. fileSelect.accept = ".zip,.rar"
  549. fileSelect.addEventListener("change", (e) => {
  550. let files = e.target.files
  551. if (files == 0) return
  552. CipherUtils.showLoading()
  553. let file = files[0]
  554. // 获取当前谱面信息
  555. let nowBeatmapInfo = CipherUtils.getNowBeatmapInfo()
  556. importBeatmap(new Blob([file]), nowBeatmapInfo).catch(err => {
  557. CipherUtils.hideLoading()
  558. console.error(err)
  559. alert("Import Failed: " + err)
  560. })
  561. })
  562. // 点击按钮
  563. document.body.append(fileSelect)
  564. fileSelect.click()
  565. fileSelect.remove()
  566. } catch (err) {
  567. alert("Import Failed: " + err)
  568. }
  569. }
  570.  
  571. /**
  572. * 从BeatSaber谱面压缩包导入信息
  573. * @param {Blob} zipBlob
  574. * @param {{id:string, difficulty:string, beatsaverId:string}} nowBeatmapInfo
  575. * @param {number} targetDifficulty
  576. */
  577. async function importBeatmap(zipBlob, nowBeatmapInfo, targetDifficulty) {
  578. let BLITZ_RHYTHM = await WebDB.open("BLITZ_RHYTHM")
  579. let BLITZ_RHYTHM_files = await WebDB.open("BLITZ_RHYTHM-files")
  580. try {
  581. // 获取当前谱面基本信息
  582. let rawSongs = await BLITZ_RHYTHM.get("keyvaluepairs", "persist:songs")
  583. let songsInfo = JSON.parse(rawSongs)
  584. let songsById = JSON.parse(songsInfo.byId)
  585. let songInfo = songsById[nowBeatmapInfo.id]
  586.  
  587. let userName = ""
  588. let songDuration = -1
  589. {
  590. let rawUser = await BLITZ_RHYTHM.get("keyvaluepairs", "persist:user")
  591. userName = JSON.parse(JSON.parse(rawUser).userInfo).name
  592.  
  593. songDuration = Math.floor(songInfo.songDuration * (songInfo.bpm / 60))
  594. }
  595. // 获取当前谱面难度信息
  596. let datKey = nowBeatmapInfo.id + "_" + nowBeatmapInfo.difficulty + "_Ring.dat"
  597. let datInfo = JSON.parse(await BLITZ_RHYTHM_files.get("keyvaluepairs", datKey))
  598. if (datInfo._version !== "2.3.0")
  599. throw $t("code.tip.not_support_map_ver")
  600. let beatmapInfo = await BeatSaverUtils.getBeatmapInfo(zipBlob)
  601. if (beatmapInfo.difficulties.length == 0)
  602. throw $t("code.tip.not_found_diff")
  603.  
  604. // 选择导入难度
  605. let tarDifficulty = 1
  606. if (targetDifficulty >= 1 && targetDifficulty <= beatmapInfo.difficulties.length) {
  607. tarDifficulty = targetDifficulty
  608. } else {
  609. let defaultDifficulty = "1"
  610. let promptTip = ""
  611. console.log(beatmapInfo.difficulties)
  612. for (let index in beatmapInfo.difficulties) {
  613. if (index > 0) promptTip += "\r\n"
  614. promptTip += (parseInt(index) + 1) + "." + beatmapInfo.difficulties[index].difficulty
  615. }
  616. let difficulty = ""
  617. while (true) {
  618. difficulty = prompt($t("code.tip.input_diff") + promptTip, defaultDifficulty)
  619. if (!difficulty) {
  620. // Cancel
  621. CipherUtils.hideLoading()
  622. return
  623. }
  624. if (/^\d$/.test(difficulty)) {
  625. tarDifficulty = parseInt(difficulty)
  626. if (tarDifficulty > 0 && tarDifficulty <= beatmapInfo.difficulties.length) break
  627. }
  628. alert($t("code.tip.input_index_err"))
  629. }
  630. }
  631. // 开始导入
  632. let difficultyInfo = JSON.parse(await beatmapInfo.difficulties[tarDifficulty - 1].file.async("string"))
  633. let changeInfo = convertBeatMapInfo(difficultyInfo.version || difficultyInfo._version, difficultyInfo, songDuration)
  634. datInfo._notes = changeInfo._notes
  635. datInfo._obstacles = changeInfo._obstacles
  636. await BLITZ_RHYTHM_files.put("keyvaluepairs", datKey, JSON.stringify(datInfo))
  637. // 设置谱师署名
  638. songInfo.mapAuthorName = userName + " & " + beatmapInfo.levelAuthorName
  639. songsInfo.byId = JSON.stringify(songsById)
  640. await BLITZ_RHYTHM.put("keyvaluepairs", "persist:songs", JSON.stringify(songsInfo))
  641.  
  642. // 导入完成
  643. setTimeout(() => {
  644. CipherUtils.closeEditorTopMenu()
  645. window.location.reload()
  646. }, 1000)
  647. } catch (error) {
  648. throw error
  649. } finally {
  650. BLITZ_RHYTHM.close()
  651. BLITZ_RHYTHM_files.close()
  652. }
  653. }
  654.  
  655. /**
  656. * 转换BeatSaber谱面信息
  657. * @param {string} version
  658. * @param {JSON} info
  659. * @param {number} songDuration
  660. */
  661. function convertBeatMapInfo(version, rawInfo, songDuration) {
  662. let info = {
  663. _notes: [], // 音符
  664. _obstacles: [], // 墙
  665. }
  666. if (version.startsWith("3.")) {
  667. // 音符
  668. for (let index in rawInfo.colorNotes) {
  669. let rawNote = rawInfo.colorNotes[index]
  670. if (songDuration > 0 && rawNote.b > songDuration) continue // 去除歌曲结束后的音符
  671. info._notes.push({
  672. _time: rawNote.b,
  673. _lineIndex: rawNote.x,
  674. _lineLayer: rawNote.y,
  675. _type: rawNote.c,
  676. _cutDirection: 8,
  677. })
  678. }
  679. } else if (version.startsWith("2.")) {
  680. // 音符
  681. for (let index in rawInfo._notes) {
  682. let rawNote = rawInfo._notes[index]
  683. if (songDuration > 0 && rawNote._time > songDuration) continue // 去除歌曲结束后的音符
  684. if (rawNote._customData && rawNote._customData._track === "choarrowspazz") continue // 去除某个mod的前级音符
  685. info._notes.push({
  686. _time: rawNote._time,
  687. _lineIndex: rawNote._lineIndex,
  688. _lineLayer: rawNote._lineLayer,
  689. _type: rawNote._type,
  690. _cutDirection: 8,
  691. })
  692. }
  693. // 墙
  694. for (let index in rawInfo._obstacles) {
  695. let rawNote = rawInfo._obstacles[index]
  696. if (songDuration > 0 && rawNote._time > songDuration) continue // 去除歌曲结束后的墙
  697. info._obstacles.push({
  698. _time: rawNote._time,
  699. _duration: rawNote._duration,
  700. _type: rawNote._type,
  701. _lineIndex: rawNote._lineIndex,
  702. _width: rawNote._width,
  703. })
  704. }
  705. } else {
  706. throw $t("code.tip.not_support_bs_ver", version)
  707. }
  708. // 因Cipher不支持长墙,所以转为多面墙
  709. let newObstacles = []
  710. for (let index in info._obstacles) {
  711. let baseInfo = info._obstacles[index]
  712. let startTime = baseInfo._time
  713. let endTime = baseInfo._time + baseInfo._duration
  714. let duration = baseInfo._duration
  715. baseInfo._duration = 0.04
  716. // 头
  717. baseInfo._time = startTime
  718. if (songDuration < 0 || (baseInfo._time + baseInfo._duration) < songDuration)
  719. newObstacles.push(JSON.parse(JSON.stringify(baseInfo)))
  720. // 中间
  721. let count = Math.floor(duration / 1) - 2 // 至少间隔1秒
  722. let dtime = ((endTime - 0.04) - (startTime + 0.04)) / count
  723. for (let i = 0; i < count; i++) {
  724. baseInfo._time += dtime
  725. if (songDuration < 0 || (baseInfo._time + baseInfo._duration) < songDuration)
  726. newObstacles.push(JSON.parse(JSON.stringify(baseInfo)))
  727. }
  728. // 尾
  729. baseInfo._time = endTime - 0.04
  730. if (songDuration < 0 || (baseInfo._time + baseInfo._duration) < songDuration)
  731. newObstacles.push(JSON.parse(JSON.stringify(baseInfo)))
  732. }
  733. info._obstacles = newObstacles
  734. return info
  735. }
  736.  
  737. async function ApplyPageParmater() {
  738. let BLITZ_RHYTHM = await WebDB.open("BLITZ_RHYTHM")
  739. let BLITZ_RHYTHM_files = await WebDB.open("BLITZ_RHYTHM-files")
  740. try {
  741. let pagePar = CipherUtils.getPageParmater()
  742. if (!pagePar) return
  743.  
  744. if (pagePar.event === "import") {
  745. if (pagePar.source === "beatsaver") {
  746. CipherUtils.showLoading()
  747. if (pagePar.mode !== "song" && pagePar.mode !== "all") return
  748. let zipUrl = await BeatSaverUtils.getDownloadUrl(pagePar.id)
  749. let zipBlob = await Utils.downloadZipFile(zipUrl)
  750. let beatsaverInfo = await BeatSaverUtils.getBeatmapInfo(zipBlob)
  751. // console.log(beatsaverInfo)
  752. let oggBlob = await BeatSaverUtils.getOggFromZip(zipBlob, false)
  753.  
  754. let zip = await JSZip.loadAsync(zipBlob)
  755. let coverBlob = await zip.file(beatsaverInfo.raw._coverImageFilename).async("blob")
  756. let coverType = beatsaverInfo.raw._coverImageFilename.match(/.(\w{1,})$/)[1]
  757.  
  758. let rawUserStr = await BLITZ_RHYTHM.get("keyvaluepairs", "persist:user")
  759. let userName = JSON.parse(JSON.parse(rawUserStr).userInfo).name
  760.  
  761. // Date to ID
  762. let date = new Date()
  763. let dateArray = [date.getFullYear().toString().padStart(4, "0"), (date.getMonth() + 1).toString().padStart(2, "0"), date.getDate().toString().padStart(2, "0"),
  764. date.getHours().toString().padStart(2, "0"), date.getMinutes().toString().padStart(2, "0"),
  765. date.getSeconds().toString().padStart(2, "0") + date.getMilliseconds().toString().padStart(3, "0") + (Math.floor(Math.random() * Math.pow(10, 11))).toString().padStart(11, "0")]
  766. let id = dateArray.join("_")
  767.  
  768. let selectedDifficulty = "Easy"
  769.  
  770. // Apply Info
  771. let cipherMapInfo = {
  772. id,
  773. officialId: "",
  774. name: "[" + pagePar.id + "]" + beatsaverInfo.raw._songName,
  775. // subName: beatsaverInfo.raw._songSubName,
  776. artistName: beatsaverInfo.raw._songAuthorName,
  777. mapAuthorName: userName + ((pagePar.mode === "all") ? (" & " + beatsaverInfo.raw._levelAuthorName) : ""),
  778. bpm: beatsaverInfo.raw._beatsPerMinute,
  779. offset: beatsaverInfo.raw._songTimeOffset,
  780. // swingAmount: 0,
  781. // swingPeriod: 0.5,
  782. previewStartTime: beatsaverInfo.raw._previewStartTime,
  783. previewDuration: beatsaverInfo.raw._previewDuration,
  784. songFilename: id + "_song.ogg",
  785. songDuration: await Utils.getOggDuration(oggBlob),
  786. coverArtFilename: id + "_cover." + coverType,
  787. environment: "DefaultEnvironment",
  788. selectedDifficulty,
  789. difficultiesRingById: {
  790. Easy: {
  791. id: "Easy",
  792. noteJumpSpeed: 10,
  793. calories: 3000,
  794. startBeatOffset: 0,
  795. customLabel: "",
  796. ringNoteJumpSpeed: 10,
  797. ringNoteStartBeatOffset: 0
  798. },
  799. Normal: {
  800. id: "Normal",
  801. noteJumpSpeed: 10,
  802. calories: 4000,
  803. startBeatOffset: 0,
  804. customLabel: "",
  805. ringNoteJumpSpeed: 10,
  806. ringNoteStartBeatOffset: 0
  807. },
  808. Hard: {
  809. id: "Hard",
  810. noteJumpSpeed: 12,
  811. calories: 4500,
  812. startBeatOffset: 0,
  813. customLabel: "",
  814. ringNoteJumpSpeed: 12,
  815. ringNoteStartBeatOffset: 0
  816. },
  817. Expert: {
  818. id: "Expert",
  819. noteJumpSpeed: 15,
  820. calories: 5000,
  821. startBeatOffset: 0,
  822. customLabel: "",
  823. ringNoteJumpSpeed: 15,
  824. ringNoteStartBeatOffset: 0
  825. }
  826. },
  827. createdAt: Date.now(),
  828. lastOpenedAt: Date.now(),
  829. // demo: false,
  830. modSettings: {
  831. customColors: {
  832. isEnabled: false,
  833. colorLeft: "#f21212",
  834. colorLeftOverdrive: 0,
  835. colorRight: "#006cff",
  836. colorRightOverdrive: 0,
  837. envColorLeft: "#FFDD55",
  838. envColorLeftOverdrive: 0,
  839. envColorRight: "#00FFCC",
  840. envColorRightOverdrive: 0,
  841. obstacleColor: "#f21212",
  842. obstacleColorOverdrive: 0,
  843. obstacle2Color: "#d500f9",
  844. obstacleColorOverdrive2: 0
  845. },
  846. mappingExtensions: {
  847. isEnabled: false,
  848. numRows: 3,
  849. numCols: 4,
  850. colWidth: 1,
  851. rowHeight: 1
  852. }
  853. },
  854. // enabledFastWalls: false,
  855. // enabledLightshow: false,
  856. }
  857.  
  858. // Apply Difficulty Info
  859. if (pagePar.mode === "song") {
  860. delete cipherMapInfo.difficultiesRingById.Normal
  861. delete cipherMapInfo.difficultiesRingById.Hard
  862. delete cipherMapInfo.difficultiesRingById.Expert
  863. } else if (pagePar.mode === "all") {
  864. let tarDiffList = ["Easy", "Normal", "Hard", "Expert", "ExpertPlus"]
  865. let diffMap = {}
  866. for (let i = beatsaverInfo.difficulties.length - 1; i >= 0; i--) {
  867. let difficultyInfo = beatsaverInfo.difficulties[i]
  868. let difficulty = difficultyInfo.difficulty
  869. if (difficulty === "ExpertPlus") difficulty = "Expert"
  870. cipherMapInfo.selectedDifficulty = selectedDifficulty = difficulty
  871. if (!diffMap.hasOwnProperty(difficulty)) {
  872. diffMap[difficulty] = beatsaverInfo.difficulties[i].file
  873. } else {
  874. let index = tarDiffList.indexOf(difficulty) - 1
  875. if (index < 0) continue
  876. diffMap[tarDiffList[index]] = beatsaverInfo.difficulties[i].file
  877. }
  878. }
  879. let rawDiffList = ["Easy", "Normal", "Hard", "Expert"]
  880. for (let i = 0; i < rawDiffList.length; i++) {
  881. let difficulty = rawDiffList[i]
  882. if (!diffMap.hasOwnProperty(difficulty))
  883. delete cipherMapInfo.difficultiesRingById[difficulty]
  884. }
  885. for (let difficulty in diffMap) {
  886. let datKey = id + "_" + difficulty + "_Ring.dat"
  887. let diffDatInfo = JSON.parse("{\"_version\":\"2.3.0\",\"_events\":[],\"_notes\":[],\"_ringNotes\":[],\"_obstacles\":[],\"_customData\":{\"_bookmarks\":[]}}")
  888. let difficultyInfo = JSON.parse(await diffMap[difficulty].async("string"))
  889. let changeInfo = convertBeatMapInfo(difficultyInfo.version || difficultyInfo._version, difficultyInfo, Math.floor(cipherMapInfo.songDuration * (cipherMapInfo.bpm / 60)))
  890. diffDatInfo._notes = changeInfo._notes
  891. diffDatInfo._obstacles = changeInfo._obstacles
  892. await BLITZ_RHYTHM_files.put("keyvaluepairs", datKey, JSON.stringify(diffDatInfo))
  893. }
  894. }
  895.  
  896. // Create Asset File
  897. await BLITZ_RHYTHM_files.put("keyvaluepairs", id + "_song.ogg", oggBlob)
  898. await BLITZ_RHYTHM_files.put("keyvaluepairs", id + "_cover." + coverType, coverBlob)
  899.  
  900. // Create Cipher Map
  901. let songsStr = await BLITZ_RHYTHM.get("keyvaluepairs", "persist:songs")
  902. let songsJson = JSON.parse(songsStr)
  903. let songPairs = JSON.parse(songsJson.byId)
  904. songPairs[id] = cipherMapInfo
  905. songsJson.byId = JSON.stringify(songPairs)
  906. await BLITZ_RHYTHM.put("keyvaluepairs", "persist:songs", JSON.stringify(songsJson))
  907.  
  908. // console.log(cipherMapInfo)
  909.  
  910. setTimeout(() => {
  911. location.href = "https://cipher-editor-cn.picovr.com/edit/notes?id=" + id + "&difficulty=" + selectedDifficulty + "&mode=Ring"
  912. }, 200)
  913. return // Dont hide loading
  914. }
  915. }
  916. CipherUtils.hideLoading()
  917. } catch (e) {
  918. CipherUtils.hideLoading()
  919. throw e
  920. } finally {
  921. BLITZ_RHYTHM.close()
  922. BLITZ_RHYTHM_files.close()
  923. }
  924. }
  925.  
  926. /**
  927. * 定时任务 1s
  928. */
  929. function tick() {
  930. addImportButton()
  931. }
  932.  
  933. (function () {
  934. 'use strict'
  935.  
  936. // Import beatmap via url parameter
  937. ApplyPageParmater().catch(res => {
  938. console.error(res)
  939. alert($t("code.tip.import_map_err"))
  940. })
  941. })()