Greasy Fork

Greasy Fork is available in English.

闪韵灵境谱面导入扩展

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

当前为 2023-09-16 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        BlitzRhythm Editor Extra Beatmap Import
// @name:en     Extra Beatmap Import
// @name:zh-CN     闪韵灵境谱面导入扩展
// @namespace   cipher-editor-mod-extra-beatmap-import
// @version     1.0.1
// @description     Import BeatSaber beatmap into the BlitzRhythm editor
// @description:en  Import BeatSaber beatmap into the BlitzRhythm editor
// @description:zh-CN 将BeatSaber谱面导入到闪韵灵境编辑器内
// @author      Moyuer
// @author:zh-CN   如梦Nya
// @source      https://github.com/CMoyuer/BlitzRhythm-Editor-Mod-Loader
// @license     MIT
// @run-at      document-body
// @grant       unsafeWindow
// @grant       GM_xmlhttpRequest
// @connect     beatsaver.com
// @match       https://cipher-editor-cn.picovr.com/*
// @match       https://cipher-editor-va.picovr.com/*
// @icon        https://cipher-editor-va.picovr.com/favicon.ico
// @require     https://code.jquery.com/jquery-3.6.0.min.js
// @require     http://greasyfork.icu/scripts/473358-jszip/code/main.js?version=1237031
// @require     http://greasyfork.icu/scripts/473361-xml-http-request-interceptor/code/main.js?version=1237032
// @require     http://greasyfork.icu/scripts/473362-web-indexeddb-helper/code/main.js?version=1237033
// @require     http://greasyfork.icu/scripts/474680-blitzrhythm-editor-mod-base-lib/code/main.js?version=1246657
// ==/UserScript==

const I18N = {
    en: { // English
        parameter: {
            download_timeout: {
                name: "Download Timeout",
                description: "Timeout for download for beatmap",
            }
        },
        methods: {
            // test: {
            //     name: "Test",
            //     description: "Just a test button",
            // },
        },
        code: {
            // search: {
            //     fail: "Search song failed!",
            //     tip_timeout: "It seems that the search has timed out. Do you need to modify the timeout parameter?"
            // },
        }
    },
    zh: { // Chinese
        parameter: {
            download_timeout: {
                name: "下载超时",
                description: "下载谱面的超时时间",
            }
        },
        methods: {
            // test: {
            //     name: "测试",
            //     description: "只是一个测试按钮",
            // }
        },
        code: {
            // search: {
            //     fail: "搜索歌曲失败!",
            //     tip_timeout: "看来搜索超时了, 是否需要修改超时时间?"
            // },
        }
    }
}

const PARAMETER = [
    {
        id: "download_timeout",
        name: $t("parameter.download_timeout.name"),
        description: $t("parameter.download_timeout.description"),
        type: "number",
        default: 60 * 1000,
        min: 1000,
        max: 2 * 60 * 1000
    }
]

const METHODS = [
    // {
    //     name: $t("methods.test.name"),
    //     description: $t("methods.test.description"),
    //     func: () => {
    //         log($t("methods.test.name"))
    //     }
    // }, 
]

let pluginEnabled = false
let timerHandle = 0

function onEnabled() {
    pluginEnabled = true
    let timerFunc = () => {
        if (!pluginEnabled) return
        CipherUtils.waitLoading().then(() => {
            tick()
        }).catch(err => {
            console.error(err)
        }).finally(() => {
            timerHandle = setTimeout(timerFunc, 250)
        })
    }
    timerFunc()
}

function onDisabled() {
    if (timerHandle > 0) {
        clearTimeout(timerHandle)
        timerHandle = 0
    }
    pluginEnabled = false
}

function onParameterValueChanged(id, val) {
    log("onParameterValueChanged", id, val)
    // log("debug", $p(id))
}

// =====================================================================================

/**
 * 闪韵灵境工具类
 */
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 {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 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)
        })
    }
}

/**
 * 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
    }
}

/**
 * 通用工具类
 */
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 reader = new FileReader()
            reader.onerror = () => {
                reject(reader.error)
            }
            reader.onload = (e) => {
                let audio = document.createElement('audio')
                audio.addEventListener("loadedmetadata", () => {
                    resolve(audio.duration)
                    $(audio).remove()
                })
                audio.addEventListener('error', () => {
                    reject(audio.error)
                    $(audio).remove()
                })
                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)
        })
    }
}

// =====================================================================================

/**
 * 在顶部菜单添加导入按钮
 */
function 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 = importFromBeatSaver
    btnImportBs.style["font-size"] = "13px"
    div.append(btnImportBs)
    // 按钮2
    let btnImportZip = btnTemp.clone()[0]
    btnImportZip.id = "importBeatmap"
    btnImportZip.innerHTML = "导入谱面 BeatSaber压缩包"
    btnImportZip.onclick = importFromBeatmapZip
    btnImportZip.style["margin-left"] = "5px"
    btnImportZip.style["font-size"] = "13px"
    div.append(btnImportZip)
    // 添加
    btnsBoxList[0].prepend(div)
}

async function 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 importBeatmap(zipBlob, nowBeatmapInfo)
    } catch (err) {
        console.error(err)
        alert("出错啦:" + err)
        CipherUtils.hideLoading()
    }
}

/**
 * 通过压缩文件导入
 */
function importFromBeatmapZip() {
    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()
            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 function importBeatmap(zipBlob, nowBeatmapInfo, targetDifficulty) {
    let BLITZ_RHYTHM = await WebDB.open("BLITZ_RHYTHM")
    let BLITZ_RHYTHM_files = await 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 = 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
 */
function 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 function ApplyPageParmater() {
    let BLITZ_RHYTHM = await WebDB.open("BLITZ_RHYTHM")
    let BLITZ_RHYTHM_files = await 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 = 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()
    }
}

/**
 * 定时任务 1s
 */
function tick() {
    addImportButton()
}

(function () {
    'use strict'

    // Import beatmap via url parameter
    ApplyPageParmater().catch(res => {
        console.error(res)
        alert("导入谱面时发生错误!可刷新页面重试...")
    })
})()