您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
为《闪韵灵境谱面编辑器》扩展各种实用的功能
// ==UserScript== // @name 《闪韵灵境谱面编辑器》功能扩展 // @namespace cipher-editor-extension // @version 1.3.1 // @description 为《闪韵灵境谱面编辑器》扩展各种实用的功能 // @author 如梦Nya // @license MIT // @run-at document-body // @grant unsafeWindow // @grant GM_xmlhttpRequest // @connect beatsaver.com // @connect beatsage.com // @match https://cipher-editor-cn.picovr.com/* // @match https://beatsaver.com/* // @match https://pc.woozooo.com/* // @icon https://cipher-editor-cn.picovr.com/favicon.ico // @require https://code.jquery.com/jquery-3.6.0.min.js // ==/UserScript== const $ = window.jQuery let JSZip = undefined // ================================================================================ 工具类 ================================================================================ /** * 数据库操作类 */ class WebDB { constructor() { /** @type {IDBDatabase} */ this.db = undefined } /** * 打开数据库 * @param {string} dbName 数据库名 * @param {number | undefined} dbVersion 数据库版本 * @returns {Promise<WebDB, any>} */ open(dbName, dbVersion) { let self = this return new Promise(function (resolve, reject) { /** @type {IDBFactory} */ const indexDB = unsafeWindow.indexedDB || unsafeWindow.webkitIndexedDB || unsafeWindow.mozIndexedDB let req = indexDB.open(dbName, dbVersion) req.onerror = reject req.onsuccess = function () { self.db = this.result resolve(self) } }) } /** * 查出一条数据 * @param {string} tableName 表名 * @param {string} key 键名 * @returns {Promise<any, any>} */ get(tableName, key) { let self = this return new Promise(function (resolve, reject) { let req = self.db.transaction([tableName]).objectStore(tableName).get(key) req.onerror = reject req.onsuccess = function () { resolve(this.result) } }) } /** * 插入、更新一条数据 * @param {string} tableName 表名 * @param {string} key 键名 * @param {any} value 数据 * @returns {Promise<IDBValidKey, any>} */ put(tableName, key, value) { let self = this return new Promise(function (resolve, reject) { let req = self.db.transaction([tableName], 'readwrite').objectStore(tableName).put(value, key) req.onerror = reject req.onsuccess = function () { resolve(this.result) } }) } /** * 关闭数据库 */ close() { this.db.close() delete this.db } } /** * 闪韵灵境工具类 */ class CipherUtils { /** * 获取当前谱面的信息 */ static getNowBeatmapInfo() { let url = location.href // ID let matchId = url.match(/id=(\w*)/) let id = matchId ? matchId[1] : "" // BeatSaverID let beatsaverId = "" let nameBoxList = $(".css-tpsa02") if (nameBoxList.length > 0) { let name = nameBoxList[0].innerHTML let matchBeatsaverId = name.match(/\[(\w*)\]/) if (matchBeatsaverId) beatsaverId = matchBeatsaverId[1] } // 难度 let matchDifficulty = url.match(/difficulty=(\w*)/) let difficulty = matchDifficulty ? matchDifficulty[1] : "" return { id, difficulty, beatsaverId } } /** * 获取谱面全部信息 * @param {string} id 谱面ID * @returns {object} */ static async getCipherMapFullInfo(id) { let BLITZ_RHYTHM = await new WebDB().open("BLITZ_RHYTHM") let rawSongs = await BLITZ_RHYTHM.get("keyvaluepairs", "persist:songs") BLITZ_RHYTHM.close() let songsInfo = JSON.parse(rawSongs) let songsById = JSON.parse(songsInfo.byId) return songsById[id] } /** * 获取指定谱面的歌曲OGG资源 * @param {string} id 谱面ID * @returns {Promise<Blob, any>} */ static async getSongBlob(id) { let info = await CipherUtils.getCipherMapFullInfo(id) let songFileName = info.songFilename + "" let blob if (info.officialId) { // 官谱 let BLITZ_RHYTHM_official = await new WebDB().open("BLITZ_RHYTHM-official") blob = await BLITZ_RHYTHM_official.get("keyvaluepairs", songFileName) BLITZ_RHYTHM_official.close() } else { // 自定义谱 let BLITZ_RHYTHM_files = await new WebDB().open("BLITZ_RHYTHM-files") blob = await BLITZ_RHYTHM_files.get("keyvaluepairs", songFileName) BLITZ_RHYTHM_files.close() } return blob } /** * 添加歌曲校验数据头 * @param {ArrayBuffer} rawBuffer * @returns {Blob} */ static addSongVerificationCode(rawBuffer) { // 前面追加数据,以通过校验 let rawData = new Uint8Array(rawBuffer) let BYTE_VERIFY_ARRAY = [235, 186, 174, 235, 186, 174, 235, 186, 174, 85, 85] let buffer = new ArrayBuffer(rawData.length + BYTE_VERIFY_ARRAY.length) let dataView = new DataView(buffer) for (let i = 0; i < BYTE_VERIFY_ARRAY.length; i++) { dataView.setUint8(i, BYTE_VERIFY_ARRAY[i]) } for (let i = 0; i < rawData.length; i++) { dataView.setUint8(BYTE_VERIFY_ARRAY.length + i, rawData[i]) } return new Blob([buffer], { type: "application/octet-stream" }) } /** * 获取当前页面类型 * @returns */ static getPageType() { let url = window.location.href let matchs = url.match(/edit\/(\w{1,})/) if (!matchs) { return "home" } else { return matchs[1] } } /** * 获取页面参数 * @returns */ static getPageParmater() { let url = window.location.href let matchs = url.match(/\?import=(\w{1,})@(\w{1,})@(\w{1,})/) if (!matchs) return return { event: "import", source: matchs[1], id: matchs[2], mode: matchs[3], } } /** * 关闭编辑器顶部菜单 */ static closeEditorTopMenu() { $(".css-1k12r02").click() } /** * 显示Loading */ static showLoading() { 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>') 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>') $("#root").append(maskBox) } /** * 隐藏Loading */ static hideLoading() { $("#loading").remove() } /** * 网页弹窗 */ static showIframe(src){ this.hideIframe() 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>') maskBox.click(this.hideIframe) 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>') $("#root").append(maskBox) } /** * 隐藏Loading */ static hideIframe() { $("#iframe_box").remove() } /** * 等待Loading结束 * @returns */ static waitLoading() { return new Promise((resolve, reject) => { let handle = setInterval((() => { let loadingList = $(".css-c81162") if (loadingList && loadingList.length > 0) return clearInterval(handle) resolve() }), 500) }) } } /** * 沙盒工具类 */ class SandBox { /** @type {HTMLIFrameElement | undefined} */ static _sandBoxIframe = undefined /** * 创建一个Iframe沙盒 * @returns {HTMLIFrameElement} */ static getDocument() { if (!SandBox._sandBoxIframe) { let id = GM_info.script.namespace + "_iframe" // 找ID let iframes = $('#' + id) if (iframes.length > 0) SandBox._sandBoxIframe = iframes[0] // 不存在,创建一个 if (!SandBox._sandBoxIframe) { let ifr = document.createElement("iframe"); ifr.id = id ifr.style.display = "none" document.body.appendChild(ifr); SandBox._sandBoxIframe = ifr; } } return SandBox._sandBoxIframe } /** * 动态添加Script * @param {string} url 脚本链接 * @returns {Promise<Element>} */ static dynamicLoadJs(url) { return new Promise(function (resolve, reject) { let ifrdoc = SandBox.getDocument().contentDocument; let script = ifrdoc.createElement('script') script.type = 'text/javascript' script.src = url script.onload = script.onreadystatechange = function () { if (!this.readyState || this.readyState === "loaded" || this.readyState === "complete") { resolve(script) script.onload = script.onreadystatechange = null } } ifrdoc.body.appendChild(script) }); } } /** * BeatSaver工具类 */ class BeatSaverUtils { /** * 搜索歌曲列表 * @param {string} searchKey 搜索关键字 * @param {number} pageCount 搜索页数 * @returns */ static searchSongList(searchKey, pageCount = 1) { return new Promise(function (resolve, reject) { let songList = [] let songInfoMap = {} let count = 0 let cbFlag = false let func = data => { // 填充数据 data.docs.forEach(rawInfo => { let artist = rawInfo.metadata.songAuthorName let bpm = rawInfo.metadata.bpm let cover = rawInfo.versions[0].coverURL let song_name = "[" + rawInfo.id + "]" + rawInfo.metadata.songName let id = 80000000000 + parseInt(rawInfo.id, 36) songList.push({ artist, bpm, cover, song_name, id }) let downloadURL = rawInfo.versions[0].downloadURL let previewURL = rawInfo.versions[0].previewURL songInfoMap[id] = { downloadURL, previewURL } }) if (++count == pageCount) { cbFlag = true resolve({ songList, songInfoMap }) } } for (let i = 0; i < pageCount; i++) { Utils.ajax({ url: "https://api.beatsaver.com/search/text/" + i + "?sortOrder=Relevance&q=" + searchKey, method: "GET", responseType: "json" }).then(func) } }) } /** * 从BeatSaver下载ogg文件 * @param {number} zipUrl 歌曲压缩包链接 * @param {function} onprogress 进度回调 * @returns {Promise<blob, any>} */ static async downloadSongFile(zipUrl, onprogress) { let blob = await Utils.downloadZipFile(zipUrl, onprogress) // 解压出ogg文件 return await BeatSaverUtils.getOggFromZip(blob) } /** * 从压缩包中提取出ogg文件 * @param {blob} zipBlob * @param {boolean | undefined} verification * @returns */ static async getOggFromZip(zipBlob, verification = true) { let zip = await JSZip.loadAsync(zipBlob) let eggFile = undefined for (let fileName in zip.files) { if (!fileName.endsWith(".egg")) continue eggFile = zip.file(fileName) break } if (verification) { let rawBuffer = await eggFile.async("arraybuffer") return CipherUtils.addSongVerificationCode(rawBuffer) } else { return await eggFile.async("blob") } } /** * 获取压缩包下载链接 * @param {string} id 歌曲ID * @return {Promise} */ static getDownloadUrl(id) { return new Promise(function (resolve, reject) { Utils.ajax({ url: "https://api.beatsaver.com/maps/id/" + id, method: "GET", responseType: "json", }).then(data => { resolve(data.versions[0].downloadURL) }).catch(err => { reject(err) }) }) } /** * 从压缩包中提取曲谱难度文件 * @param {Blob} zipBlob * @returns */ static async getBeatmapInfo(zipBlob) { let zip = await JSZip.loadAsync(zipBlob) // 谱面信息 let infoFile for (let fileName in zip.files) { if (fileName.toLowerCase() !== "info.dat") continue infoFile = zip.files[fileName] break } if (!infoFile) throw "请检查压缩包中是否包含info.dat文件" let rawBeatmapInfo = JSON.parse(await infoFile.async("string")) // 难度列表 let difficultyBeatmaps let diffBeatmapSets = rawBeatmapInfo._difficultyBeatmapSets for (let a in diffBeatmapSets) { let info = diffBeatmapSets[a] if (info["_beatmapCharacteristicName"] !== "Standard") continue difficultyBeatmaps = info._difficultyBeatmaps break } // 难度对应文件名 let beatmapInfo = { raw: rawBeatmapInfo, version: rawBeatmapInfo._version, levelAuthorName: rawBeatmapInfo._levelAuthorName, difficulties: [] } for (let index in difficultyBeatmaps) { let difficultyInfo = difficultyBeatmaps[index] let difficulty = difficultyInfo._difficulty let difficultyLabel = "" if (difficultyInfo._customData && difficultyInfo._customData._difficultyLabel) difficultyLabel = difficultyInfo._customData._difficultyLabel beatmapInfo.difficulties.push({ difficulty, difficultyLabel, file: zip.files[difficultyInfo._beatmapFilename] }) } return beatmapInfo } } /** * XMLHttpRequest请求拦截器 */ class XHRIntercept { /** @type {XHRIntercept} */ static _self /** * 初始化 * @returns {XHRIntercept} */ constructor() { if (XHRIntercept._self) return XHRIntercept._self XHRIntercept._self = this // 修改EventListener方法 let rawXhrAddEventListener = XMLHttpRequest.prototype.addEventListener XMLHttpRequest.prototype.addEventListener = function (key, func) { if (key === "progress") { this.onprogress = func } else { rawXhrAddEventListener.apply(this, arguments) } } let rawXhrRemoveEventListener = XMLHttpRequest.prototype.removeEventListener XMLHttpRequest.prototype.removeEventListener = function (key, func) { if (key === "progress") { this.onprogress = undefined } else { rawXhrRemoveEventListener.apply(this, arguments) } } // 修改send方法 /** @type {function[]} */ this.sendIntercepts = [] this.rawXhrSend = XMLHttpRequest.prototype.send XMLHttpRequest.prototype.send = function () { XHRIntercept._self._xhrSend(this, arguments) } } /** * 添加Send拦截器 * @param {function} func */ onXhrSend(func) { if (this.sendIntercepts.indexOf(func) >= 0) return this.sendIntercepts.push(func) } /** * 删除Send拦截器 * @param {function | undefined} func */ offXhrSend(func) { if (typeof func === "function") { let index = this.sendIntercepts.indexOf(func) if (index < 0) return this.sendIntercepts.splice(index, 1) } else { this.sendIntercepts = [] } } /** * 发送拦截器 * @param {XMLHttpRequest} self * @param {IArguments} args */ _xhrSend(self, args) { let complete = () => { this.rawXhrSend.apply(self, args) } for (let i = 0; i < this.sendIntercepts.length; i++) { let flag = this.sendIntercepts[i](self, args, complete) if (flag) return } complete() } } /** * 通用工具类 */ class Utils { /** * 下载压缩包文件 * @param {number} zipUrl 歌曲压缩包链接 * @param {function | undefined} onprogress 进度回调 * @returns {Promise} */ static downloadZipFile(zipUrl, onprogress) { return new Promise(function (resolve, reject) { Utils.ajax({ url: zipUrl, method: "GET", responseType: "blob", onprogress, }).then(data => { resolve(new Blob([data], { type: "application/zip" })) }).catch(reject) }) } /** * 获取音乐文件时长 * @param {Blob} blob */ static getOggDuration(blob) { return new Promise((resolve, reject) => { let ifDoc = SandBox.getDocument().contentDocument let audio = ifDoc.createElement('audio') audio.addEventListener("loadedmetadata", () => { resolve(audio.duration) // $(audio).remove() }) audio.addEventListener('error', () => { reject(audio.error) }) let reader = new FileReader() reader.onerror = () => { reject(reader.error) } reader.onload = (e) => { audio.src = e.target.result } reader.readAsDataURL(new File([blob], "song.ogg", { type: "audio/ogg" })) }) } /** * 异步发起网络请求 * @param {object} config * @returns */ static ajax(config) { return new Promise((resolve, reject) => { config.onload = res => { if (res.status >= 200 && res.status < 300) { try { resolve(JSON.parse(res.response)) } catch { resolve(res.response) } } else { reject("HTTP Code: " + res.status) } } config.onerror = err => { reject(err) } GM_xmlhttpRequest(config) }) } /** * 将Blob转换为Base64 * @param {Blob} blob * @returns {Promise} */ static blobToBase64(blob) { return new Promise(function (resolve, reject) { const fileReader = new FileReader(); fileReader.onload = (e) => { resolve(e.target.result) } fileReader.readAsDataURL(blob) }) } /** * 将Base64格式转换为File * @param {string} base64 * @param {string} filename * @returns */ static base64toFile(base64, filename = 'file') { let arr = base64.split(',') let mime = arr[0].match(/:(.*?);/)[1] let suffix = mime.split('/')[1] let bstr = atob(arr[1]) let n = bstr.length let u8arr = new Uint8Array(n) while (n--) { u8arr[n] = bstr.charCodeAt(n) } return new File([u8arr], `${filename}.${suffix}`, { type: mime, }) } } // ================================================================================ 编辑器拓展 ================================================================================ class SearchSongExtension { constructor() { this.searchFromBeatSaver = false this.songInfoMap = {} this.lastPageType = "other" } // 加载XHR拦截器 initXHRIntercept() { let _this = this let xhrIntercept = new XHRIntercept() /** * @param {XMLHttpRequest} self * @param {IArguments} args * @param {function} complete * @returns {boolean} 是否匹配 */ let onSend = function (self, args, complete) { let url = self._url if (!url || !_this.searchFromBeatSaver) return if (url.startsWith("/song/staticList")) { // 获取歌曲列表 let result = decodeURI(url).match(/songName=(\S*)&/) let key = "" if (result) key = result[1].replace("+", " ") BeatSaverUtils.searchSongList(key, 2).then(res => { self.extraSongList = res.songList _this.songInfoMap = res.songInfoMap complete() }).catch(err => { alert("搜索歌曲失败!") console.error(err) self.extraSongList = [] complete() }) self.addEventListener("readystatechange", function () { if (this.readyState !== this.DONE) return const res = JSON.parse(this.responseText) if (this.extraSongList) { res.data.data = this.extraSongList res.data.total = res.data.data.length this.extraSongList = [] } Object.defineProperty(this, 'responseText', { writable: true }); this.responseText = JSON.stringify(res) setTimeout(() => { _this.fixSongListStyle() _this.addPreviewFunc() }, 200) }); return true } else if (url.startsWith("/beatsaver/")) { let _onprogress = self.onprogress self.onprogress = undefined // 从BeatSaver下载歌曲 let result = decodeURI(url).match(/\d{1,}/) let id = parseInt(result[0]) BeatSaverUtils.downloadSongFile(_this.songInfoMap[id].downloadURL, _onprogress).then(oggBlob => { _this.songInfoMap[id].ogg = oggBlob complete() }).catch(err => { console.error(err) self.onerror(err) }) self.addEventListener("readystatechange", function () { if (this.readyState !== this.DONE) return let result = decodeURI(url).match(/\d{1,}/) let id = parseInt(result[0]) Object.defineProperty(this, 'response', { writable: true }); this.response = _this.songInfoMap[id].ogg }); return true } else if (url.startsWith("/song/ogg")) { // 获取ogg文件下载链接 let result = decodeURI(url).match(/id=(\d*)/) let id = parseInt(result[1]) if (id < 80000000000) return self.addEventListener("readystatechange", function () { if (this.readyState !== this.DONE) return const res = JSON.parse(this.responseText) res.code = 0 res.data = { link: "/beatsaver/" + id } res.msg = "success" Object.defineProperty(this, 'responseText', { writable: true }); this.responseText = JSON.stringify(res) }); complete() return true } } xhrIntercept.onXhrSend(onSend) } /** * 更新数据库 * @param {Boolean} isForce 强制转换 * @returns */ async updateDatabase(isForce) { let BLITZ_RHYTHM = await new WebDB().open("BLITZ_RHYTHM") let BLITZ_RHYTHM_files = await new WebDB().open("BLITZ_RHYTHM-files") let BLITZ_RHYTHM_official = await new WebDB().open("BLITZ_RHYTHM-official") let songInfos = [] let hasChanged = false let songsInfo // 更新歌曲信息 { let rawSongs = await BLITZ_RHYTHM.get("keyvaluepairs", "persist:songs") songsInfo = JSON.parse(rawSongs) let songsById = JSON.parse(songsInfo.byId) for (let key in songsById) { let officialId = songsById[key].officialId if (typeof officialId != "number" || (!isForce && officialId < 80000000000)) continue let songInfo = songsById[key] songInfos.push(JSON.parse(JSON.stringify(songInfo))) songInfo.coverArtFilename = songInfo.coverArtFilename.replace("" + songInfo.officialId, songInfo.id) songInfo.songFilename = songInfo.songFilename.replace("" + songInfo.officialId, songInfo.id) songInfo.officialId = "" songsById[key] = songInfo hasChanged = true } songsInfo.byId = JSON.stringify(songsById) } // 处理文件 for (let index in songInfos) { let songInfo = songInfos[index] // 复制封面和音乐文件 let cover = await BLITZ_RHYTHM_official.get("keyvaluepairs", songInfo.coverArtFilename) let song = await BLITZ_RHYTHM_official.get("keyvaluepairs", songInfo.songFilename) await BLITZ_RHYTHM_files.put("keyvaluepairs", songInfo.coverArtFilename.replace("" + songInfo.officialId, songInfo.id), cover) await BLITZ_RHYTHM_files.put("keyvaluepairs", songInfo.songFilename.replace("" + songInfo.officialId, songInfo.id), song) // 添加info记录 await BLITZ_RHYTHM_files.put("keyvaluepairs", songInfo.id + "_Info.dat", JSON.stringify({ _songFilename: "song.ogg" })) } // 保存数据 if (hasChanged) await BLITZ_RHYTHM.put("keyvaluepairs", "persist:songs", JSON.stringify(songsInfo)) BLITZ_RHYTHM.close() BLITZ_RHYTHM_files.close() BLITZ_RHYTHM_official.close() return hasChanged } /** * 修复歌单布局 */ fixSongListStyle() { let songListBox = $(".css-10szcx0")[0] songListBox.style["grid-template-columns"] = "repeat(3, minmax(0px, 1fr))" let songBox = songListBox.parentNode if ($(".css-1wfsuwr").length > 0) { songBox.style["overflow-y"] = "hidden" songBox.parentNode.style["margin-bottom"] = "" } else { songBox.style["overflow-y"] = "auto" songBox.parentNode.style["margin-bottom"] = "44px" } let itemBox = $(".css-bil4eh") for (let index = 0; index < itemBox.length; index++) itemBox[index].style.width = "230px" } /** * 在歌曲Card中添加双击预览功能 */ addPreviewFunc() { let searchBox = $(".css-1d92frk") $("#preview_tip").remove() searchBox.after("<div style='text-align: center;color:gray;padding-bottom:10px;' id='preview_tip'>双击歌曲可预览曲谱</div>") let infoViewList = $(".css-bil4eh") for (let index = 0; index < infoViewList.length; index++) { infoViewList[index].ondblclick = () => { let name = $(infoViewList[index]).find(".css-1y1rcqj")[0].innerHTML let result = name.match(/^\[(\w*)\]/) if (!result) return let previewUrl = "https://skystudioapps.com/bs-viewer/?id=" + result[1] CipherUtils.showIframe(previewUrl) // window.open(previewUrl) } } } /** * 添加通过BeatSaver搜索歌曲的按钮 */ applySearchButton() { let boxList = $(".css-1u8wof2") // 弹窗 try { if (boxList.length == 0) throw "Box not found" let searchBoxList = boxList.find(".css-70qvj9") if (searchBoxList.length == 0) throw "item too few" // 搜索栏元素数量 if (searchBoxList[0].childNodes.length >= 3) return // 搜索栏元素数量 } catch { if (this.searchFromBeatSaver) this.searchFromBeatSaver = false return } let rawSearchBtn = $(boxList[0]).find("button")[0] // 搜索按钮 // 添加一个按钮 let searchBtn = document.createElement("button") searchBtn.className = rawSearchBtn.className searchBtn.innerHTML = "BeatSaver" $(rawSearchBtn.parentNode).append(searchBtn); // 绑定事件 rawSearchBtn.onmousedown = () => { this.searchFromBeatSaver = false $("#preview_tip").remove() } searchBtn.onmousedown = () => { this.searchFromBeatSaver = true $(rawSearchBtn).click() } } /** * 添加转换官方谱面的按钮 * @returns */ async applyConvertCiphermapButton() { let BLITZ_RHYTHM = await new WebDB().open("BLITZ_RHYTHM") try { let rawSongs = await BLITZ_RHYTHM.get("keyvaluepairs", "persist:songs") let songsInfo = JSON.parse(rawSongs) let songsById = JSON.parse(songsInfo.byId) let songId = CipherUtils.getNowBeatmapInfo().id let officialId = songsById[songId].officialId if (!officialId) return } catch (error) { console.error(error) return } finally { BLITZ_RHYTHM.close() } let divList = $(".css-1tiz3p0") if (divList.length > 0) { if ($("#div-custom").length > 0) return let divBox = $(divList[0]).clone() divBox[0].id = "div-custom" divBox.find(".css-ujbghi")[0].innerHTML = "转换为自定义谱面" divBox.find(".css-1exyu3y")[0].innerHTML = "将官方谱面转换为自定义谱面, 以导出带有音乐文件的完整谱面压缩包。" divBox.find(".css-1y7rp4x")[0].innerText = "开始转换谱面" divBox[0].onclick = e => { // 更新歌曲信息 this.updateDatabase(true).then((hasChanged) => { if (hasChanged) setTimeout(() => { window.location.reload() }, 1000) }).catch(err => { console.log("转换谱面失败:", err) alert("转换谱面失败,请刷新再试!") }) } $(divList[0].parentNode).append(divBox) } } /** * 隐藏按钮 */ hideConvertCiphermapButton() { $("#div-custom").remove() } /** * 定时任务 1s */ handleTimer() { let pageType = CipherUtils.getPageType() if (pageType !== "home") { if (pageType != this.lastPageType) { // 隐藏按钮 if (pageType !== "download") this.hideConvertCiphermapButton() // 更新歌曲信息 this.updateDatabase().then((hasChanged) => { if (hasChanged) setTimeout(() => { window.location.reload() }, 1000) }).catch(err => { console.log("更新数据失败:", err) alert("更新歌曲信息失败,请刷新再试!") }) } else if (pageType === "download") { this.applyConvertCiphermapButton() } } else { this.applySearchButton() } this.lastPageType = pageType } async init() { // 初始化XHR拦截器 this.initXHRIntercept() // 启动定时任务 let timerFunc = () => { CipherUtils.waitLoading().then(() => { setTimeout(timerFunc, 1000) this.handleTimer() }).catch(err => { setTimeout(timerFunc, 1000) console.error(err) }) } timerFunc() } } class ImportBeatmapExtension { constructor() { } /** * 在顶部菜单添加导入按钮 */ addImportButton() { if ($("#importBeatmap").length > 0) return let btnsBoxList = $(".css-4e93fo") if (btnsBoxList.length == 0) return // 按键组 let div = document.createElement("div") div.style["display"] = "flex" // 按钮模板 let btnTemp = $(btnsBoxList[0].childNodes[0]) // 按钮1 let btnImportBs = btnTemp.clone()[0] btnImportBs.id = "importBeatmap" btnImportBs.innerHTML = "导入谱面 BeatSaver链接" btnImportBs.onclick = () => { this.importFromBeatSaver() } btnImportBs.style["font-size"] = "13px" div.append(btnImportBs) // 按钮2 let btnImportZip = btnTemp.clone()[0] btnImportZip.id = "importBeatmap" btnImportZip.innerHTML = "导入谱面 BeatSaber压缩包" btnImportZip.onclick = () => { this.importFromBeatmap() } btnImportZip.style["margin-left"] = "5px" btnImportZip.style["font-size"] = "13px" div.append(btnImportZip) // 添加 btnsBoxList[0].prepend(div) } async importFromBeatSaver() { try { // 获取当前谱面信息 let nowBeatmapInfo = CipherUtils.getNowBeatmapInfo() // 获取谱面信息 let url = prompt('请输入BeatSaver铺面链接', "https://beatsaver.com/maps/" + nowBeatmapInfo.beatsaverId) if (!url) return let result = url.match(/^https:\/\/beatsaver.com\/maps\/(\S*)$/) if (!result) { alert("链接格式错误!") return } CipherUtils.showLoading() let downloadUrl = await BeatSaverUtils.getDownloadUrl(result[1]) let zipBlob = await Utils.downloadZipFile(downloadUrl) await this.importBeatmap(zipBlob, nowBeatmapInfo) } catch (err) { console.error(err) alert("出错啦:" + err) CipherUtils.hideLoading() } } /** * 通过压缩文件导入 */ importFromBeatmap() { try { // 创建上传按钮 let fileSelect = document.createElement('input') fileSelect.type = 'file' fileSelect.style.display = "none" fileSelect.accept = ".zip,.rar" fileSelect.addEventListener("change", (e) => { let files = e.target.files if (files == 0) return CipherUtils.showLoading() let file = files[0] // 获取当前谱面信息 let nowBeatmapInfo = CipherUtils.getNowBeatmapInfo() this.importBeatmap(new Blob([file]), nowBeatmapInfo).catch(err => { CipherUtils.hideLoading() console.error(err) alert("出错啦:" + err) }) }) // 点击按钮 document.body.append(fileSelect) fileSelect.click() fileSelect.remove() } catch (err) { alert("出错啦:" + err) } } /** * 从BeatSaber谱面压缩包导入信息 * @param {Blob} zipBlob * @param {{id:string, difficulty:string, beatsaverId:string}} nowBeatmapInfo * @param {number} targetDifficulty */ async importBeatmap(zipBlob, nowBeatmapInfo, targetDifficulty) { let BLITZ_RHYTHM = await new WebDB().open("BLITZ_RHYTHM") let BLITZ_RHYTHM_files = await new WebDB().open("BLITZ_RHYTHM-files") try { // 获取当前谱面基本信息 let rawSongs = await BLITZ_RHYTHM.get("keyvaluepairs", "persist:songs") let songsInfo = JSON.parse(rawSongs) let songsById = JSON.parse(songsInfo.byId) let songInfo = songsById[nowBeatmapInfo.id] let userName = "" let songDuration = -1 { let rawUser = await BLITZ_RHYTHM.get("keyvaluepairs", "persist:user") userName = JSON.parse(JSON.parse(rawUser).userInfo).name songDuration = Math.floor(songInfo.songDuration * (songInfo.bpm / 60)) } // 获取当前谱面难度信息 let datKey = nowBeatmapInfo.id + "_" + nowBeatmapInfo.difficulty + "_Ring.dat" let datInfo = JSON.parse(await BLITZ_RHYTHM_files.get("keyvaluepairs", datKey)) if (datInfo._version !== "2.3.0") throw "插件不支持该谱面版本!可尝试重新创建谱面" let beatmapInfo = await BeatSaverUtils.getBeatmapInfo(zipBlob) if (beatmapInfo.difficulties.length == 0) throw "该谱面找不到可用的难度" // 选择导入难度 let tarDifficulty = 1 if (targetDifficulty >= 1 && targetDifficulty <= beatmapInfo.difficulties.length) { tarDifficulty = targetDifficulty } else { let defaultDifficulty = "1" let promptTip = "" console.log(beatmapInfo.difficulties) for (let index in beatmapInfo.difficulties) { if (index > 0) promptTip += "\r\n" promptTip += (parseInt(index) + 1) + "." + beatmapInfo.difficulties[index].difficulty } let difficulty = "" while (true) { difficulty = prompt("请问要导入第几个难度(数字):\r\n" + promptTip, defaultDifficulty) if (!difficulty) { // Cancel CipherUtils.hideLoading() return } if (/^\d$/.test(difficulty)) { tarDifficulty = parseInt(difficulty) if (tarDifficulty > 0 && tarDifficulty <= beatmapInfo.difficulties.length) break alert("请输入准确的序号!") } else { alert("请输入准确的序号!") } } } // 开始导入 let difficultyInfo = JSON.parse(await beatmapInfo.difficulties[tarDifficulty - 1].file.async("string")) let changeInfo = this.convertBeatMapInfo(difficultyInfo.version || difficultyInfo._version, difficultyInfo, songDuration) datInfo._notes = changeInfo._notes datInfo._obstacles = changeInfo._obstacles await BLITZ_RHYTHM_files.put("keyvaluepairs", datKey, JSON.stringify(datInfo)) // 设置谱师署名 songInfo.mapAuthorName = userName + " & " + beatmapInfo.levelAuthorName songsInfo.byId = JSON.stringify(songsById) await BLITZ_RHYTHM.put("keyvaluepairs", "persist:songs", JSON.stringify(songsInfo)) // 导入完成 setTimeout(() => { CipherUtils.closeEditorTopMenu() window.location.reload() }, 1000) } catch (error) { throw error } finally { BLITZ_RHYTHM.close() BLITZ_RHYTHM_files.close() } } /** * 转换BeatSaber谱面信息 * @param {string} version * @param {JSON} info * @param {number} songDuration */ convertBeatMapInfo(version, rawInfo, songDuration) { let info = { _notes: [], // 音符 _obstacles: [], // 墙 } if (version.startsWith("3.")) { // 音符 for (let index in rawInfo.colorNotes) { let rawNote = rawInfo.colorNotes[index] if (songDuration > 0 && rawNote.b > songDuration) continue // 去除歌曲结束后的音符 info._notes.push({ _time: rawNote.b, _lineIndex: rawNote.x, _lineLayer: rawNote.y, _type: rawNote.c, _cutDirection: 8, }) } } else if (version.startsWith("2.")) { // 音符 for (let index in rawInfo._notes) { let rawNote = rawInfo._notes[index] if (songDuration > 0 && rawNote._time > songDuration) continue // 去除歌曲结束后的音符 if (rawNote._customData && rawNote._customData._track === "choarrowspazz") continue // 去除某个mod的前级音符 info._notes.push({ _time: rawNote._time, _lineIndex: rawNote._lineIndex, _lineLayer: rawNote._lineLayer, _type: rawNote._type, _cutDirection: 8, }) } // 墙 for (let index in rawInfo._obstacles) { let rawNote = rawInfo._obstacles[index] if (songDuration > 0 && rawNote._time > songDuration) continue // 去除歌曲结束后的墙 info._obstacles.push({ _time: rawNote._time, _duration: rawNote._duration, _type: rawNote._type, _lineIndex: rawNote._lineIndex, _width: rawNote._width, }) } } else { throw ("暂不支持该谱面的版本(" + version + "),请换个链接再试!") } // 因Cipher不支持长墙,所以转为多面墙 let newObstacles = [] for (let index in info._obstacles) { let baseInfo = info._obstacles[index] let startTime = baseInfo._time let endTime = baseInfo._time + baseInfo._duration let duration = baseInfo._duration baseInfo._duration = 0.04 // 头 baseInfo._time = startTime if (songDuration < 0 || (baseInfo._time + baseInfo._duration) < songDuration) newObstacles.push(JSON.parse(JSON.stringify(baseInfo))) // 中间 let count = Math.floor(duration / 1) - 2 // 至少间隔1秒 let dtime = ((endTime - 0.04) - (startTime + 0.04)) / count for (let i = 0; i < count; i++) { baseInfo._time += dtime if (songDuration < 0 || (baseInfo._time + baseInfo._duration) < songDuration) newObstacles.push(JSON.parse(JSON.stringify(baseInfo))) } // 尾 baseInfo._time = endTime - 0.04 if (songDuration < 0 || (baseInfo._time + baseInfo._duration) < songDuration) newObstacles.push(JSON.parse(JSON.stringify(baseInfo))) } info._obstacles = newObstacles return info } async ApplyPageParmater() { let BLITZ_RHYTHM = await new WebDB().open("BLITZ_RHYTHM") let BLITZ_RHYTHM_files = await new WebDB().open("BLITZ_RHYTHM-files") try { let pagePar = CipherUtils.getPageParmater() if (!pagePar) return if (pagePar.event === "import") { if (pagePar.source === "beatsaver") { CipherUtils.showLoading() if (pagePar.mode !== "song" && pagePar.mode !== "all") return let zipUrl = await BeatSaverUtils.getDownloadUrl(pagePar.id) let zipBlob = await Utils.downloadZipFile(zipUrl) let beatsaverInfo = await BeatSaverUtils.getBeatmapInfo(zipBlob) // console.log(beatsaverInfo) let oggBlob = await BeatSaverUtils.getOggFromZip(zipBlob, false) let zip = await JSZip.loadAsync(zipBlob) let coverBlob = await zip.file(beatsaverInfo.raw._coverImageFilename).async("blob") let coverType = beatsaverInfo.raw._coverImageFilename.match(/.(\w{1,})$/)[1] let rawUserStr = await BLITZ_RHYTHM.get("keyvaluepairs", "persist:user") let userName = JSON.parse(JSON.parse(rawUserStr).userInfo).name // Date to ID let date = new Date() let dateArray = [date.getFullYear().toString().padStart(4, "0"), (date.getMonth() + 1).toString().padStart(2, "0"), date.getDate().toString().padStart(2, "0"), date.getHours().toString().padStart(2, "0"), date.getMinutes().toString().padStart(2, "0"), date.getSeconds().toString().padStart(2, "0") + date.getMilliseconds().toString().padStart(3, "0") + (Math.floor(Math.random() * Math.pow(10, 11))).toString().padStart(11, "0")] let id = dateArray.join("_") let selectedDifficulty = "Easy" // Apply Info let cipherMapInfo = { id, officialId: "", name: "[" + pagePar.id + "]" + beatsaverInfo.raw._songName, // subName: beatsaverInfo.raw._songSubName, artistName: beatsaverInfo.raw._songAuthorName, mapAuthorName: userName + ((pagePar.mode === "all") ? (" & " + beatsaverInfo.raw._levelAuthorName) : ""), bpm: beatsaverInfo.raw._beatsPerMinute, offset: beatsaverInfo.raw._songTimeOffset, // swingAmount: 0, // swingPeriod: 0.5, previewStartTime: beatsaverInfo.raw._previewStartTime, previewDuration: beatsaverInfo.raw._previewDuration, songFilename: id + "_song.ogg", songDuration: await Utils.getOggDuration(oggBlob), coverArtFilename: id + "_cover." + coverType, environment: "DefaultEnvironment", selectedDifficulty, difficultiesRingById: { Easy: { id: "Easy", noteJumpSpeed: 10, calories: 3000, startBeatOffset: 0, customLabel: "", ringNoteJumpSpeed: 10, ringNoteStartBeatOffset: 0 }, Normal: { id: "Normal", noteJumpSpeed: 10, calories: 4000, startBeatOffset: 0, customLabel: "", ringNoteJumpSpeed: 10, ringNoteStartBeatOffset: 0 }, Hard: { id: "Hard", noteJumpSpeed: 12, calories: 4500, startBeatOffset: 0, customLabel: "", ringNoteJumpSpeed: 12, ringNoteStartBeatOffset: 0 }, Expert: { id: "Expert", noteJumpSpeed: 15, calories: 5000, startBeatOffset: 0, customLabel: "", ringNoteJumpSpeed: 15, ringNoteStartBeatOffset: 0 } }, createdAt: Date.now(), lastOpenedAt: Date.now(), // demo: false, modSettings: { customColors: { isEnabled: false, colorLeft: "#f21212", colorLeftOverdrive: 0, colorRight: "#006cff", colorRightOverdrive: 0, envColorLeft: "#FFDD55", envColorLeftOverdrive: 0, envColorRight: "#00FFCC", envColorRightOverdrive: 0, obstacleColor: "#f21212", obstacleColorOverdrive: 0, obstacle2Color: "#d500f9", obstacleColorOverdrive2: 0 }, mappingExtensions: { isEnabled: false, numRows: 3, numCols: 4, colWidth: 1, rowHeight: 1 } }, // enabledFastWalls: false, // enabledLightshow: false, } // Apply Difficulty Info if (pagePar.mode === "song") { delete cipherMapInfo.difficultiesRingById.Normal delete cipherMapInfo.difficultiesRingById.Hard delete cipherMapInfo.difficultiesRingById.Expert } else if (pagePar.mode === "all") { let tarDiffList = ["Easy", "Normal", "Hard", "Expert", "ExpertPlus"] let diffMap = {} for (let i = beatsaverInfo.difficulties.length - 1; i >= 0; i--) { let difficultyInfo = beatsaverInfo.difficulties[i] let difficulty = difficultyInfo.difficulty if (difficulty === "ExpertPlus") difficulty = "Expert" cipherMapInfo.selectedDifficulty = selectedDifficulty = difficulty if (!diffMap.hasOwnProperty(difficulty)) { diffMap[difficulty] = beatsaverInfo.difficulties[i].file } else { let index = tarDiffList.indexOf(difficulty) - 1 if (index < 0) continue diffMap[tarDiffList[index]] = beatsaverInfo.difficulties[i].file } } let rawDiffList = ["Easy", "Normal", "Hard", "Expert"] for (let i = 0; i < rawDiffList.length; i++) { let difficulty = rawDiffList[i] if (!diffMap.hasOwnProperty(difficulty)) delete cipherMapInfo.difficultiesRingById[difficulty] } for (let difficulty in diffMap) { let datKey = id + "_" + difficulty + "_Ring.dat" let diffDatInfo = JSON.parse("{\"_version\":\"2.3.0\",\"_events\":[],\"_notes\":[],\"_ringNotes\":[],\"_obstacles\":[],\"_customData\":{\"_bookmarks\":[]}}") let difficultyInfo = JSON.parse(await diffMap[difficulty].async("string")) let changeInfo = this.convertBeatMapInfo(difficultyInfo.version || difficultyInfo._version, difficultyInfo, Math.floor(cipherMapInfo.songDuration * (cipherMapInfo.bpm / 60))) diffDatInfo._notes = changeInfo._notes diffDatInfo._obstacles = changeInfo._obstacles await BLITZ_RHYTHM_files.put("keyvaluepairs", datKey, JSON.stringify(diffDatInfo)) } } // Create Asset File await BLITZ_RHYTHM_files.put("keyvaluepairs", id + "_song.ogg", oggBlob) await BLITZ_RHYTHM_files.put("keyvaluepairs", id + "_cover." + coverType, coverBlob) // Create Cipher Map let songsStr = await BLITZ_RHYTHM.get("keyvaluepairs", "persist:songs") let songsJson = JSON.parse(songsStr) let songPairs = JSON.parse(songsJson.byId) songPairs[id] = cipherMapInfo songsJson.byId = JSON.stringify(songPairs) await BLITZ_RHYTHM.put("keyvaluepairs", "persist:songs", JSON.stringify(songsJson)) // console.log(cipherMapInfo) setTimeout(() => { location.href = "https://cipher-editor-cn.picovr.com/edit/notes?id=" + id + "&difficulty=" + selectedDifficulty + "&mode=Ring" }, 200) return // Dont hide loading } } CipherUtils.hideLoading() } catch (e) { CipherUtils.hideLoading() throw e } finally { BLITZ_RHYTHM.close() BLITZ_RHYTHM_files.close() } } /** * 初始化 */ async init() { await CipherUtils.waitLoading() try { await this.ApplyPageParmater() } catch (error) { console.error(error) alert("导入谱面时发生错误!可刷新页面重试...") } let timerFunc = () => { CipherUtils.waitLoading().then(() => { this.addImportButton() setTimeout(timerFunc, 1000) }) } timerFunc() } } class UploadCiphermapExtension { constructor() { } /** @type {Window | undefined} */ _lzyWindow = undefined _ready = false _uploadUserInfo = false /** @type {{id:number, name:string, timer:number} | undefined} */ _uploadInfo = undefined getLZYWindow() { let self = this return new Promise(function (resolve, reject) { let win = self._lzyWindow if (!win || win.closed) { win = window.open("https://pc.woozooo.com/mydisk.php", null, "height=720,width=1280,resizable=0,status=0,toolbar=0,menubar=0,location=0,status=0") self._lzyWindow = win self._ready = false } if (self._ready) { resolve(win) } else { let handle // let timeoutHandle = setTimeout(() => { // clearInterval(handle) // reject("time out") // // win.close() // }, 10 * 1000) handle = setInterval(() => { if (self._ready) { // clearTimeout(timeoutHandle) clearInterval(handle) resolve(win) } else if (!win || win.closed) { // clearTimeout(timeoutHandle) clearInterval(handle) reject("window close") } }, 100) } }) } /** * 关闭蓝奏云窗口 * @returns */ closeWindow() { if (!this._lzyWindow || this._lzyWindow.closed) return this._lzyWindow.close() } /** * 上传当前谱面 */ async uploadCiphermap() { if (this._uploadInfo) { alert("还有未完成的上传任务,请勿频繁操作") return } let mapId = CipherUtils.getNowBeatmapInfo().id // 获取谱面信息 let BLITZ_RHYTHM = await new WebDB().open("BLITZ_RHYTHM") try { let songsStr = await BLITZ_RHYTHM.get("keyvaluepairs", "persist:songs") let songPairs = JSON.parse(JSON.parse(songsStr).byId) let mapInfo = songPairs[mapId] // console.log(mapInfo) // 提交任务 this._uploadInfo = { id: mapId, name: mapInfo.name, timer: 0 } this._uploadInfo.timer = setTimeout(() => { console.warn("获取谱面压缩包失败: 编辑器超时未响应") this._uploadInfo = undefined alert("获取谱面压缩包失败!") }, 5000) unsafeWindow.postMessage({ event: "query_ciphermap_zip", id: mapId }) } catch (err) { alert("上传时发生错误: " + err) console.error(err) } finally { BLITZ_RHYTHM.close() } } /** * 上传用户信息 * @returns */ async uploadUserInfo() { // 获取谱面信息 let BLITZ_RHYTHM = await new WebDB().open("BLITZ_RHYTHM") try { let userStr = await BLITZ_RHYTHM.get("keyvaluepairs", "persist:user") let userInfo = JSON.parse(JSON.parse(userStr).userInfo) // 提交任务 let info = { event: "upload_user_info", id: userInfo.user_id_str, name: userInfo.name, avatar: userInfo.avatar_url } this.getLZYWindow().then(win => { win.focus() win.postMessage(info, "*") }).catch(err => { // alert("打开网页超时") console.error(err) }) } catch (err) { // alert("上传时发生错误: " + err) console.error("上传谱师信息时发生错误: ", err) } finally { BLITZ_RHYTHM.close() } } /** * 在歌曲下载页面添加上传按钮 */ addUploadButton() { let divList = $(".css-1tiz3p0") if (divList.length > 0) { if ($("#div-upload").length > 0) return let divBox = $(divList[0]).clone() divBox[0].id = "div-upload" divBox.find(".css-ujbghi")[0].innerHTML = "上传至网盘" divBox.find(".css-1exyu3y")[0].innerHTML = "将当前谱面信息上传至蓝奏云网盘。" divBox.find(".css-1y7rp4x")[0].innerText = "开始上传" divBox[0].onclick = e => { // this.uploadUserInfo() this.uploadCiphermap() } $(divList[0].parentNode).append(divBox) } } /** * 隐藏按钮 */ hideUploadButton() { $("#div-upload").remove() } /** * 初始化 */ async init() { // 定时任务 let timerFunc = () => { CipherUtils.waitLoading().then(() => { let pageType = CipherUtils.getPageType() if (pageType === "download") { this.addUploadButton() } else { this.hideUploadButton() } setTimeout(timerFunc, 1000) }).catch(err => { console.error(err) setTimeout(timerFunc, 1000) }) } timerFunc() // 监听信息 window.addEventListener("message", event => { /** @type {{event:string}} */ let data = event.data if (!data || !data.event) return if (data.event === "result_ciphermap_zip") { if (data.code !== 0 || !this._uploadInfo || data.data.id !== this._uploadInfo.id) return clearTimeout(this._uploadInfo.timer) Utils.blobToBase64(data.data.blob).then(base64 => { this.getLZYWindow().then(win => { win.focus() win.postMessage({ event: "upload_ciphermap", mapId: this._uploadInfo.id, name: this._uploadInfo.name, base64 }, "*") this._uploadInfo = undefined }).catch(err => { // alert("打开网页超时") console.error(err) this._uploadInfo = undefined }) }).catch(err => { console.error("转换文件格式时出错:", err) alert("转换文件格式时出错!") }) } }) } } class BeatSageExtension { constructor() { } async importFromBeatSage() { let flag = confirm("1.本功能由BeatSage网站免费提供, BeatSage拥有该功能的所有权。\r\n2.因服务器在境外, 速度与网络环境相关, 一般编谱需要2分钟时间。\r\n3.AI做谱需要大量服务器算力, 喜欢该功能的欢迎前往BeatSage.com官网进行打赏支持。\r\n4.点击“确认”键继续。") if (!flag) return let cipherMapInfo = CipherUtils.getNowBeatmapInfo() let oggBlob = await CipherUtils.getSongBlob(cipherMapInfo.id) let formData = new FormData() let rawDiffList = ["Easy", "Normal", "Hard", "Expert"] let tarDiffList = ["Normal", "Hard", "Expert", "ExpertPlus"] let tarDifficulty = tarDiffList[rawDiffList.indexOf(cipherMapInfo.difficulty)] formData.append("audio_file", oggBlob) formData.append("audio_metadata_title", "song") formData.append("audio_metadata_artist", "auther") formData.append("difficulties", tarDifficulty) formData.append("modes", "Standard") formData.append("events", "DotBlocks") formData.append("environment", "DefaultEnvironment") formData.append("system_tag", "v2") // 发起AI编谱任务 console.log("正在发起AI编谱任务...") let result = await Utils.ajax({ url: "https://beatsage.com/beatsaber_custom_level_create", method: "POST", responseType: "json", data: formData, contentType: false, processData: false, }) console.log("歌曲上传成功, 任务ID为: " + result.id) let reqUrl = "https://beatsage.com/beatsaber_custom_level_heartbeat/" + result.id let downloadUrl = "https://beatsage.com/beatsaber_custom_level_download/" + result.id // 定时查询是否完成 let taskDone = false console.log("正在确认任务进度...") while (!taskDone) { await new Promise((resolve, _) => { setTimeout(resolve, 5 * 1000) }) let result = await Utils.ajax({ url: reqUrl, method: "GET", responseType: "json" }) if (result.status !== "PENDING") { if (result.status === "DONE") { console.log("谱面生成完成, 开始下载文件...") let beatmapZip = await Utils.downloadZipFile(downloadUrl, () => { }) // 导入谱面 await new ImportBeatmapExtension().importBeatmap(beatmapZip, cipherMapInfo, 1) } else { console.log("发生未知错误: " + result.status) throw "Task Failed: " + result.status } taskDone = true } else { console.log("谱面正在生成...") } } } /** * 在顶部菜单添加导入按钮 */ addImportButton() { if ($("#btnBeatSage").length > 0) return let btnsBoxList = $(".css-4e93fo") if (btnsBoxList.length == 0) return // 按钮模板 let btnTemp = $(btnsBoxList[0].childNodes[1]) // 按钮1 let btnBeatSage = btnTemp.clone()[0] btnBeatSage.id = "btnBeatSage" btnBeatSage.innerHTML = "AI编谱 (BeatSage)" btnBeatSage.onclick = () => { CipherUtils.showLoading() this.importFromBeatSage().catch(err => { console.error(err) alert("AI编谱时发生错误! 详情请查看Console") }).finally(() => { CipherUtils.hideLoading() }) } btnBeatSage.style["font-size"] = "13px" // 添加 btnsBoxList[0].prepend(btnBeatSage) } /** * 初始化 */ async init() { let timerFunc = () => { CipherUtils.waitLoading().then(() => { this.addImportButton() setTimeout(timerFunc, 1000) }).catch(err => { console.error(err) setTimeout(timerFunc, 1000) }) } timerFunc() } } // ============================================================================== 其他网站 ============================================================================== class WooZoooHelper { /** @type {number} 谱面存放目录ID */ mapFolderId = -1 FILE_ID = 0 constructor() { } /** * 获取文件夹列表 * @param {number} folderId 目录ID * @returns */ async get_folder_list(folderId = -1) { let formData = new FormData() formData.append("task", 47) formData.append("folder_id", folderId) let result = await Utils.ajax({ method: "POST", responseType: "json", contentType: false, processData: false, url: "/doupload.php", data: formData }) return result.text || [] } /** * 创建文件夹 * @param {string} name 文件夹名称 * @param {string} description 文件夹描述 * @param {number | undefined} parentId 文件夹ID * @returns */ async create_folder(name, description = "", parentId = 0) { let formData = new FormData() formData.append("task", 2) formData.append("parent_id", parentId) formData.append("folder_name", name) formData.append("folder_description", description) let result = await Utils.ajax({ url: "/doupload.php", method: "POST", responseType: "json", contentType: false, processData: false, data: formData }) return { folderId: result.text } } /** * 获取/创建谱面存放目录 */ async getCiphermapFolderId() { // 查找现有文件夹 let folderList = await this.get_folder_list(-1) for (let i in folderList) { let info = folderList[i] if (info.name === "Ciphermaps") return info.fol_id } // 如果没找到,就新建一个 let folderInfo = await this.create_folder("Ciphermaps", "闪韵灵境 谱面") return folderInfo.folderId } /** * 上传文件 * @param {File} file 文件 * @param {number | undefined} folderId 文件夹ID * @returns */ async upload_file(file, folderId = -1) { let formData = new FormData() formData.append("task", 1) formData.append("vie", 2) formData.append("ve", 2) formData.append("id", "WU_FILE_" + this.FILE_ID++) formData.append("name", file.name) formData.append("type", file.type) formData.append("lastModifiedDate", new Date(file.lastModified).toString()) formData.append("size", file.size) formData.append("folder_id_bb_n", folderId) formData.append("upload_file", file) let result = await Utils.ajax({ url: "/html5up.php", method: "POST", responseType: "json", contentType: false, processData: false, data: formData }) let info = result.text[0] return ({ id: info.id, f_id: info.f_id }) } /** * 获取文件描述 * @param {number} fileId 文件ID * @returns */ async get_file_description(fileId) { let formData = new FormData() formData.append("task", 12) formData.append("file_id", fileId) let result = await Utils.ajax({ url: "/doupload.php", method: "POST", responseType: "json", contentType: false, processData: false, data: formData }) return result.info } /** * 设置文件描述 * @param {number} fileId 文件ID * @param {string} description 文件描述 * @returns */ async set_file_description(fileId, description) { let formData = new FormData() formData.append("task", 11) formData.append("file_id", fileId) formData.append("desc", description) let result = await Utils.ajax({ url: "/doupload.php", method: "POST", responseType: "json", contentType: false, processData: false, data: formData }) return result.info } /** * 删除指定文件 * @param {number} fileId 文件ID * @returns */ async delete_file(fileId) { let formData = new FormData() formData.append("task", 6) formData.append("file_id", fileId) let result = await Utils.ajax({ url: "/doupload.php", method: "POST", responseType: "json", contentType: false, processData: false, data: formData }) } /** * 获取API校验码 * @returns */ get_vei() { return $("#mainframe")[0].contentDocument.body.innerHTML.match(/'vei':'(\S{1,})\'/)[1] } /** * 获取用户ID */ get_uid() { return $("#mainframe")[0].contentDocument.body.innerHTML.match(/uid=(\d{1,})/)[1] } /** * 获取文件列表 * @param {number} folderId 目录ID * @returns */ async get_file_list(folderId = -1) { let formData = new FormData() formData.append("pg", 1) formData.append("vei", this.get_vei()) formData.append("task", 5) formData.append("folder_id", folderId) let result = await Utils.ajax({ method: "POST", responseType: "json", contentType: false, processData: false, url: "/doupload.php?uid=" + this.get_uid(), data: formData }) return result.text || [] } /** * 移除相同ID的文件 */ async removeSameFile() { let fileList = await this.get_file_list(this.mapFolderId) let ids = [] let names = [] for (let i = 0; i < fileList.length; i++) { let fileInfo = fileList[i] let fileName = fileInfo.name_all let fileId = fileInfo.id // 删除同名旧文件 if (names.indexOf(fileName) >= 0) { console.log("delete file:", fileName, fileId) await this.delete_file(fileId) return } // 删除同备注旧文件 let mapId = await this.get_file_description(fileId) if (ids.indexOf(mapId) >= 0) { console.log("delete file:", fileName, fileId) await this.delete_file(fileId) return } // 如果是新文件 ids.push(mapId) names.push(fileName) } } /** * 上传谱面 * @param {{base64:string, name:string, mapId:string}} info */ async updateCiphermap(info) { let file = Utils.base64toFile(info.base64, info.name) // file.type = "application/x-zip-compressed" let { id, f_id } = await this.upload_file(file, this.mapFolderId) await this.set_file_description(id, info.mapId) await this.removeSameFile() } /** * 上传用户信息 * @param {{id:string, name:string, avatar:string}} info */ async uploadUserInfo(info) { let file = new File([JSON.stringify(info)], "user.txt", { type: "text/plain;charset=utf-8" }) let { id, f_id } = await this.upload_file(file, this.mapFolderId) await this.set_file_description(id, "User Info") await this.removeSameFile() } /** * 初始化 */ async init() { this.mapFolderId = await this.getCiphermapFolderId() // 监听信息 window.addEventListener("message", event => { /** @type {{event:string}} */ let data = event.data if (!data || !data.event) return if (data.event === "upload_ciphermap") { this.updateCiphermap(data).then(() => { alert("上传成功") }).catch(err => { console.error(err) alert("上传失败:" + err) }) } else if (data.event === "upload_user_info") { delete data.event this.uploadUserInfo(data).then(() => { console.log("上传用户信息成功", data) }).catch(err => { console.error("上传用户信息失败:", err) }) } }) // 完成 window.opener.postMessage({ event: "alive" }, "*") } } class BeatsaverHelper { constructor() { } /** * 添加导入按钮 */ addImportButton() { // 首页 let mapInfoList = $(".beatmap") let logoStr = '<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAYAAADE6YVjAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAARZSURBVHgBzVVbbFRVFF373nm0My3TB0X6UGsEtUKjYEqJgBFjiB8+Eg1GUakfxA/jI/qjxhiqiVjQgNiQ2Nhq4kclhQCtEcWGYMSkApaQ1Ja+EttSWlv6yNB5dGbuPdt9h5lJZxin+GPYycrknFlnr7sf52zgfzDCfzT7Pl5revAUSlEEOxzqKgKYxoCT8WNoBw3GebZjDDA2Fc6Y+TcsYmvmR8wSPsCluAfL5JjT2hRoAkMwLhhAB2bxNcqwktz8nHbIKHAcDdVkdHz1+balYNZwxNyLM4rRIxgVzDJjXmAIlIBTcEU4bweHUBZYlzFdvDNQxnlzP/VPjJ28t7qyRhVqHuTJHx5BjhxzWbmLRbLQy5ikaXfwPNrCz2Aof8ja0tMKtLCOIns7LXffvxS51ZOBme5zLlcJXDFvTNcEUkV6TeATfyM6vTXovmU87i9tJHyY6+DAO/F1eGAu8HiB6m+PZK9CJNQLpUwUOSqw0elEiRb1QhcMpvrgp6op910Q8UJ/2nUCdeyRQy9BySIGR3mu6/suz2/4I/IAXs29DxfDa3DaV4kT/uNSH9DJCBp+jrBJM8OpAmkj4Xp+BcVoSPpnEn04hgfpBM0k9mpZcs3OUjJ+PZxjq6q6jQh/+sfwobuMsEgk0pYboxGYMYQFPnyZJGB9jFs61013DS+z51fdKgKW2zJXMbZPP5nq8nqRECoTAhasO3Ap4k0S+Jgt8S0oQJt03MoE10WkCjxrM4q0bG3ReZ4Lk0Qs0ynpQjV3Nudj1jwqH1CexBVtLcdWmVGkZ1UPIyJUIxaBETt8u+0h3m7WxXnburd5YeqfY0QhiWtBl/gyidTW1ioKy8MQkcVChKWSA3ghQSyWqvWhVo5fiNZsIVcDZxSxzBxVXVLouHO5wdLFTaqXOrRNcQ79Iq23RhiEI2lEJhYVIQ+dNYclR35ZjABTrWb7XJ9vg7TlUJzD0r5yWQlZWBF1HIohGE11x6IiGmmNBhlXWKgHpE0r9+pm3jdLDHwXy8IXjBcd8juHOyX/jyYEQrHIFX5fPJI3EdS9zt1vSHJefx+YqMJjVI5WYa5AhyKJIOugC5tDo0adGlUlSSIK5/ERzqb6tCWtpthKU4ljP7ZwDqItGZ0b1XgYl2VaTMmAWs1Ok6BvDen9B7vCfidsbt2jW11lSi13UZpnJfEK2wIyGiKokAb8QZ7x9VGBOF3FxLLl3dUlJqlDvweFpo/61l3yLdOzdQjqaRftQxqLpkubj3rbgHyclnRUJDUhpyB+KgvYc4fKyx5x78fd+ApevId/M1dDCFqQX6MpFcIZQ+GU4LJMtpBMOFMQEQT42jS09i8KTpmMPcYkng3uYGJNhmHGMU4i8kGQsZP/No7DZ7bCoUVQpD2N5fYnUKwRCjUrqSTV4OgNH2QTo2jCuP8zHPIM4gaM7C9710ds+l9ozEm+RG95C6CyNkvXrNaIchXTLObRCRU8h2+XTONms38AWqb3YtH4f5MAAAAASUVORK5CYII=" width="15px" height="15px">' if (mapInfoList && mapInfoList.length > 0) { for (let a = 0; a < mapInfoList.length; a++) { let mapInfoNode = mapInfoList[a] let linkBoxs = $(mapInfoNode).find(".links") if (!linkBoxs || linkBoxs.length != 1) continue let link2 = linkBoxs.clone()[0] $(link2).insertAfter(linkBoxs[0]) let btnList = $(link2).find("a") let btnImport = $(btnList[2]).clone()[0] let url = "https://cipher-editor-cn.picovr.com/?import=beatsaver@" + btnImport.href.match(/^beatsaver:\/\/(\w{1,})$/)[1] btnImport.ariaLabel = btnImport.title = "导入到闪韵灵境谱面编辑器(仅歌曲)" btnImport.href = url + "@song" btnImport.target = "_blank" $(btnImport).empty() $(btnImport).append(logoStr) let btnImportAll = $(btnImport).clone()[0] btnImportAll.href = url + "@all" btnImportAll.target = "_blank" btnImportAll.ariaLabel = btnImportAll.title = "导入到闪韵灵境谱面编辑器(含音符)" $(btnImportAll).empty() $(btnImportAll).append(logoStr) btnList.remove() link2.append(btnImport) link2.append(btnImportAll) } } // 歌曲详情页 let btnBoxList = $(".ms-auto") let btnList = btnBoxList.find("a") if (btnList && btnList.length < 5) { let url = "https://cipher-editor-cn.picovr.com/?import=beatsaver@" + location.href.match(/^https:\/\/beatsaver\.com\/maps\/(\w{1,})/)[1] let btn1 = $('<a href="' + url + '@song" rel="noopener" target="_blank" title="导入到闪韵灵境谱面编辑器(仅歌曲)" aria-label="导入到闪韵灵境谱面编辑器(仅歌曲)"></a>') btn1.append(logoStr) btnBoxList.append(btn1) let btn2 = $('<a href="' + url + '@all" rel="noopener" target="_blank" title="导入到闪韵灵境谱面编辑器(含音符)" aria-label="导入到闪韵灵境谱面编辑器(含音符)"></a>') btn2.append(logoStr) btnBoxList.append(btn2) } } /** * 初始化 */ async init() { setInterval(this.addImportButton, 500) } } // ================================================================================ 入口 ================================================================================ /** * 谱面编辑器 */ function initEditor() { // 加载拓展 new SearchSongExtension().init() new BeatSageExtension().init() new ImportBeatmapExtension().init() let uploadEx = new UploadCiphermapExtension() uploadEx.init() // 监听信息 window.addEventListener("message", event => { /** @type {{event:string}} */ let data = event.data if (!data || !data.event) return if (data.event === "alive" && event.origin.indexOf("pc.woozooo.com") > 0) { uploadEx._ready = true // console.log(event) } }) window.addEventListener("beforeunload", () => { uploadEx.closeWindow() }) } /** * 蓝奏云 */ function initLZY() { if (!window.opener) return new WooZoooHelper().init() } /** * BeatSaver */ function initBeatsaver() { new BeatsaverHelper().init() } /** * 主入口 */ (async function () { 'use strict'; if (location.href.indexOf("cipher-editor-cn.picovr.com") > 0) { // 依赖库 const sandBox = SandBox.getDocument() await SandBox.dynamicLoadJs("https://cmoyuer.gitee.io/my-resources/js/jszip.min.js") JSZip = sandBox.contentWindow.JSZip initEditor() } else if (location.href.indexOf("pc.woozooo.com/mydisk.php") > 0) { initLZY() } else if (location.href.indexOf("beatsaver.com") > 0) { initBeatsaver() } })()