Greasy Fork is available in English.

BlitzRhythm Editor Extra Beatmap Import

Import BeatSaber beatmap into the BlitzRhythm editor

// ==UserScript==
// @name        BlitzRhythm Editor Extra Beatmap Import
// @name:en     Extra Beatmap Import
// @name:zh-CN     闪韵灵境谱面导入扩展
// @namespace   cipher-editor-mod-extra-beatmap-import
// @version     1.1.0
// @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     https://greasyfork.org/scripts/473358-jszip/code/main.js?version=1237031
// @require     https://greasyfork.org/scripts/473361-xml-http-request-interceptor/code/main.js
// @require     https://greasyfork.org/scripts/473362-web-indexeddb-helper/code/main.js
// @require     https://greasyfork.org/scripts/474680-blitzrhythm-editor-mod-base-lib/code/main.js
// ==/UserScript==

const I18N = {
    en: { // English
        parameter: {
            download_timeout: {
                name: "Download Timeout",
                description: "Timeout for download for beatmap",
            }
        },
        methods: {},
        code: {
            tip: {
                info_file_not_found: "Please check whether the zip file contains the info.dat file!",
                input_bs_url: "Please enter the BeatSaver beatmap URL:",
                url_format_error: "URL format error!",
                not_support_map_ver: "Not support this beatmap version! You can try to recreate the beatmap.",
                not_found_diff: "No available difficulty found for this map!",
                input_diff: "Enter the difficulty level (index) you want to import:\r\n",
                input_index_err: "Please enter the correct index!",
                not_support_bs_ver: "This map version ({0}) is not supported yet, please change the URL and try again!",
                import_map_err: "An error occurred while importing map! You can refresh and try again..."
            },
            button: {
                import_from_url: "Import from BeatSaver URL",
                import_from_file: "Import from BeatSaber zip",
            }
        }
    },
    zh: { // Chinese
        parameter: {
            download_timeout: {
                name: "下载超时",
                description: "下载谱面的超时时间",
            }
        },
        methods: {},
        code: {
            tip: {
                info_file_not_found: "请检查压缩包中是否包含info.dat文件",
                input_bs_url: "请输入BeatSaver铺面链接",
                url_format_error: "链接格式错误!",
                not_support_map_ver: "插件不支持该谱面版本!可尝试重新创建谱面",
                not_found_diff: "该谱面找不到可用的难度",
                input_diff: "请问要导入第几个难度(数字):\r\n",
                input_index_err: "请输入准确的序号!",
                not_support_bs_ver: "暂不支持该谱面的版本({0}),请换个链接再试!",
                import_map_err: "导入谱面时发生错误!可刷新页面重试..."
            },
            button: {
                import_from_url: "导入谱面 BeatSaver链接",
                import_from_file: "导入谱面 BeatSaber压缩包",
            }
        }
    }
}

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 $t("code.tip.info_file_not_found")
        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 = $t("code.button.import_from_url")
    btnImportBs.onclick = importFromBeatSaver
    btnImportBs.style["font-size"] = "13px"
    div.append(btnImportBs)
    // 按钮2
    let btnImportZip = btnTemp.clone()[0]
    btnImportZip.id = "importBeatmap"
    btnImportZip.innerHTML = $t("code.button.import_from_file")
    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($t("code.tip.input_bs_url"), "https://beatsaver.com/maps/" + nowBeatmapInfo.beatsaverId)
        if (!url) return
        let result = url.match(/^https:\/\/beatsaver.com\/maps\/(\S*)$/)
        if (!result) {
            alert($t("code.tip.url_format_error"))
            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("Import Failed: " + 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("Import Failed: " + err)
            })
        })
        // 点击按钮
        document.body.append(fileSelect)
        fileSelect.click()
        fileSelect.remove()
    } catch (err) {
        alert("Import Failed: " + 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 $t("code.tip.not_support_map_ver")
        let beatmapInfo = await BeatSaverUtils.getBeatmapInfo(zipBlob)
        if (beatmapInfo.difficulties.length == 0)
            throw $t("code.tip.not_found_diff")

        // 选择导入难度
        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($t("code.tip.input_diff") + promptTip, defaultDifficulty)
                if (!difficulty) {
                    // Cancel
                    CipherUtils.hideLoading()
                    return
                }
                if (/^\d$/.test(difficulty)) {
                    tarDifficulty = parseInt(difficulty)
                    if (tarDifficulty > 0 && tarDifficulty <= beatmapInfo.difficulties.length) break
                }
                alert($t("code.tip.input_index_err"))
            }
        }
        // 开始导入
        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 $t("code.tip.not_support_bs_ver", 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($t("code.tip.import_map_err"))
    })
})()