Greasy Fork is available in English.

《闪韵灵境谱面编辑器》功能扩展

为《闪韵灵境谱面编辑器》扩展各种实用的功能

// ==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()
    }
})()