class ImportSongs{ constructor(...args){ this.init(...args) } init(limited, otherFiles, noPlugins, pluginAmount){ this.limited = limited this.tjaFiles = [] this.osuFiles = [] this.assetFiles = {} this.pluginFiles = [] this.otherFiles = otherFiles || {} this.noPlugins = noPlugins this.pluginAmount = pluginAmount this.songs = [] this.stylesheet = [] this.plugins = [] this.songTitle = this.otherFiles.songTitle || {} this.uraRegex = /\s*[\((]裏[\))]$/ this.courseTypes = { "easy": 0, "normal": 1, "hard": 2, "oni": 3, "ura": 4 } this.categoryAliases = {} assets.categories.forEach(cat => { this.categoryAliases[cat.title.toLowerCase()] = cat.id if(cat.aliases){ cat.aliases.forEach(alias => { this.categoryAliases[alias.toLowerCase()] = cat.id }) } if(cat.title_lang){ for(var i in cat.title_lang){ this.categoryAliases[cat.title_lang[i].toLowerCase()] = cat.id } } }) this.assetSelectors = {} for(var selector in assets.cssBackground){ var filename = assets.cssBackground[selector] var index = filename.lastIndexOf(".") if(index !== -1){ filename = filename.slice(0, index) } this.assetSelectors[filename] = selector } this.comboVoices = ["v_combo_50"].concat(Array.from(Array(50), (d, i) => "v_combo_" + ((i + 1) * 100))) } load(files){ var extensionRegex = /\.[^\/]+$/ files.sort((a, b) => { var path1 = a.path.replace(extensionRegex, "") var path2 = b.path.replace(extensionRegex, "") return path1 > path2 ? 1 : -1 }) var metaFiles = [] for(var i = 0; i < files.length; i++){ var file = files[i] var name = file.name.toLowerCase() var path = file.path.toLowerCase() if(name.endsWith(".tja") || name.endsWith(".tjf")){ this.tjaFiles.push({ file: file, index: i }) }else if(name.endsWith(".osu")){ this.osuFiles.push({ file: file, index: i }) }else if(!this.limited && (name === "genre.ini" || name === "box.def") || name === "songtitle.txt"){ var level = (file.path.match(/\//g) || []).length metaFiles.push({ file: file, level: (level * 2) + (name === "genre.ini" ? 1 : 0) }) }else if(!this.limited && (path.indexOf("/taiko-web assets/") !== -1 || path.indexOf("taiko-web assets/") === 0)){ if(!(name in this.assetFiles)){ this.assetFiles[name] = file } }else if(name.endsWith(".taikoweb.js")){ this.pluginFiles.push({ file: file, index: i }) }else{ this.otherFiles[path] = file } } if(!this.noPlugins && this.pluginFiles.length){ var pluginPromises = [] this.pluginFiles.forEach(fileObj => { pluginPromises.push(this.addPlugin(fileObj).catch(e => console.warn(e))) }) return Promise.all(pluginPromises).then(() => { var startPromises = [] var pluginAmount = 0 if(this.plugins.length && confirm(strings.plugins.warning.replace("%s", strings.plugins.plugin[strings.plural.select(this.plugins.length)].replace("%s", this.plugins.length.toString() ) ))){ this.plugins.forEach(obj => { var plugin = plugins.add(obj.data, { name: obj.name, raw: true }) if(plugin){ pluginAmount++ plugin.imported = true startPromises.push(plugin.start()) } }) } return Promise.all(startPromises).then(() => { var importSongs = new ImportSongs(this.limited, this.otherFiles, true, pluginAmount) return importSongs.load(files) }) }) } var metaPromises = [] metaFiles.forEach(fileObj => { metaPromises.push(this.addMeta(fileObj)) }) return Promise.all(metaPromises).then(() => { var songPromises = [] this.tjaFiles.forEach(fileObj => { songPromises.push(this.addTja(fileObj).catch(e => console.warn(e))) }) this.osuFiles.forEach(fileObj => { songPromises.push(this.addOsu(fileObj).catch(e => console.warn(e))) }) songPromises.push(this.addAssets()) return Promise.all(songPromises) }).then(this.loaded.bind(this)) } addMeta(fileObj){ var file = fileObj.file var level = fileObj.level var name = file.name.toLowerCase() return file.read(name === "songtitle.txt" ? undefined : "utf-8").then(data => { var data = data.replace(/\0/g, "").split("\n") var category if(name === "genre.ini"){ var key for(var i = 0; i < data.length; i++){ var line = data[i].trim().toLowerCase() if(line.startsWith("[") && line.endsWith("]")){ key = line.slice(1, -1) }else if(key === "genre"){ var equalsPos = line.indexOf("=") if(equalsPos !== -1 && line.slice(0, equalsPos).trim() === "genrename"){ var value = line.slice(equalsPos + 1).trim() if(value.toLowerCase() in this.categoryAliases){ category = value }else{ category = data[i].trim().slice(equalsPos + 1).trim() } break } } } }else if(name === "box.def"){ for(var i = 0; i < data.length; i++){ var line = data[i].trim().toLowerCase() if(line.startsWith("#title:")){ var value = line.slice(7).trim() if(value.toLowerCase() in this.categoryAliases){ category = value } }else if(line.startsWith("#genre:")){ var value = line.slice(7).trim() if(value.toLowerCase() in this.categoryAliases){ category = value }else{ category = data[i].trim().slice(7).trim() } break } } }else if(name === "songtitle.txt"){ var lastTitle for(var i = 0; i < data.length; i++){ var line = data[i].trim() if(line){ var lang = line.slice(0, 2) if(line.charAt(2) !== " " || !(lang in allStrings)){ this.songTitle[line] = {} lastTitle = line }else if(lastTitle){ this.songTitle[lastTitle][lang] = line.slice(3).trim() } } } } if(category){ var metaPath = file.path.toLowerCase().slice(0, file.name.length * -1) var filesLoop = fileObj => { var tjaPath = fileObj.file.path.toLowerCase().slice(0, fileObj.file.name.length * -1) if(tjaPath.startsWith(metaPath) && (!("categoryLevel" in fileObj) || fileObj.categoryLevel < level)){ if(category.toLowerCase() in this.categoryAliases){ fileObj.category_id = this.categoryAliases[category.toLowerCase()] }else{ fileObj.category = category } fileObj.categoryLevel = level } } this.tjaFiles.forEach(filesLoop) this.osuFiles.forEach(filesLoop) } }).catch(e => { console.warn(e) }) } addTja(fileObj){ var file = fileObj.file var index = fileObj.index var category = fileObj.category var category_id = fileObj.category_id if(!this.limited){ var filePromise = file.read("sjis") // utf-8にしたい }else{ var filePromise = Promise.resolve() } return filePromise.then(dataRaw => { var data = dataRaw ? dataRaw.replace(/\0/g, "").split("\n") : [] var tja = new ParseTja(data, "oni", 0, 0, true) var songObj = { id: index + 1, order: index + 1, title: file.name.slice(0, file.name.lastIndexOf(".")), type: "tja", chart: file, courses: {}, music: "muted", custom: true } if(this.limited){ songObj.unloaded = true } var coursesAdded = false var titleLang = {} var titleLangAdded = false var subtitleLangAdded = false var subtitleLang = {} var dir = file.path.toLowerCase() dir = dir.slice(0, dir.lastIndexOf("/") + 1) for(var diff in tja.metadata){ var meta = tja.metadata[diff] songObj.title = meta.title || file.name.slice(0, file.name.lastIndexOf(".")) var subtitle = meta.subtitle || "" if(subtitle.startsWith("--") || subtitle.startsWith("++")){ subtitle = subtitle.slice(2).trim() } songObj.subtitle = subtitle songObj.preview = meta.demostart || 0 songObj.courses[diff] = { stars: meta.level || 0, branch: !!meta.branch } coursesAdded = true if(meta.wave){ songObj.music = this.otherFiles[dir + meta.wave.toLowerCase()] || songObj.music } if(meta.genre){ if(meta.genre.toLowerCase() in this.categoryAliases){ songObj.category_id = this.categoryAliases[meta.genre.toLowerCase()] }else{ songObj.category = meta.genre } } if(meta.taikowebskin){ songObj.song_skin = this.getSkin(dir, meta.taikowebskin) } if(meta.maker){ var maker = meta.maker var url = null var gt = maker.lastIndexOf(">") if(gt === maker.length - 1){ var lt = maker.lastIndexOf("<") if(lt !== -1 && lt !== gt - 2){ url = maker.slice(lt + 1, gt).trim() if(url.startsWith("http://") || url.startsWith("https://")){ maker = maker.slice(0, lt) }else{ url = null } } } songObj.maker = { name: maker, url: url, id: 1 } } if(meta.lyrics){ var lyricsFile = this.normPath(this.joinPath(dir, meta.lyrics)) if(lyricsFile in this.otherFiles){ songObj.lyrics = true songObj.lyricsFile = this.otherFiles[lyricsFile] } }else if(meta.inlineLyrics){ songObj.lyrics = true } for(var id in allStrings){ var songTitle = songObj.title var ura = "" if(songTitle){ var uraPos = songTitle.search(this.uraRegex) if(uraPos !== -1){ ura = songTitle.slice(uraPos) songTitle = songTitle.slice(0, uraPos) } } if(meta["title" + id]){ titleLang[id] = meta["title" + id] titleLangAdded = true }else if(songTitle in this.songTitle && this.songTitle[songTitle][id]){ titleLang[id] = this.songTitle[songTitle][id] + ura titleLangAdded = true } if(meta["subtitle" + id]){ subtitleLang[id] = meta["subtitle" + id] subtitleLangAdded = true } } } if(titleLangAdded){ songObj.title_lang = titleLang } if(subtitleLangAdded){ songObj.subtitle_lang = subtitleLang } if(!songObj.category_id && !songObj.category){ if(!category && category_id === undefined){ songObj.category_id = this.getCategory(file, [songTitle || songObj.title, file.name.slice(0, file.name.lastIndexOf("."))]) }else if(category){ songObj.category = category songObj.orginalCategory = category }else{ songObj.category_id = category_id } } if(coursesAdded || songObj.unloaded){ this.songs[index] = songObj } if(!this.limited){ var hash = md5.base64(dataRaw).slice(0, -2) songObj.hash = hash scoreStorage.songTitles[songObj.title] = hash var score = scoreStorage.get(hash, false, true) if(score){ score.title = songObj.title } } }) } addOsu(fileObj){ var file = fileObj.file var index = fileObj.index var category = fileObj.category var category_id = fileObj.category_id if(!this.limited){ var filePromise = file.read() }else{ var filePromise = Promise.resolve() } return filePromise.then(dataRaw => { var data = dataRaw ? dataRaw.replace(/\0/g, "").split("\n") : [] var osu = new ParseOsu(data, "oni", 0, 0, true) var dir = file.path.toLowerCase() dir = dir.slice(0, dir.lastIndexOf("/") + 1) var songObj = { id: index + 1, order: index + 1, type: "osu", chart: file, subtitle: osu.metadata.ArtistUnicode || osu.metadata.Artist, subtitle_lang: { en: osu.metadata.Artist || osu.metadata.ArtistUnicode }, preview: osu.generalInfo.PreviewTime ? osu.generalInfo.PreviewTime / 1000 : 0, courses: {}, music: (osu.generalInfo.AudioFilename ? this.otherFiles[dir + osu.generalInfo.AudioFilename.toLowerCase()] : "") || "muted" } if(!this.limited){ songObj.courses.oni = { stars: parseInt(osu.difficulty.overallDifficulty) || 0, branch: false } }else{ songObj.unloaded = true } var filename = file.name.slice(0, file.name.lastIndexOf(".")) var title = osu.metadata.TitleUnicode || osu.metadata.Title || file.name.slice(0, file.name.lastIndexOf(".")) if(title){ var suffix = "" var matches = filename.match(/\[.+?\]$/) if(matches){ suffix = " " + matches[0] } songObj.title = title + suffix songObj.title_lang = { en: (osu.metadata.Title || osu.metadata.TitleUnicode || title) + suffix } }else{ songObj.title = filename } this.songs[index] = songObj if(!category && category_id === undefined){ songObj.category_id = this.getCategory(file, [osu.metadata.TitleUnicode, osu.metadata.Title, file.name.slice(0, file.name.lastIndexOf("."))]) }else if(category){ songObj.category = category songObj.orginalCategory = category }else{ songObj.category_id = category_id } if(!this.limited){ var hash = md5.base64(dataRaw).slice(0, -2) songObj.hash = hash scoreStorage.songTitles[songObj.title] = hash var score = scoreStorage.get(hash, false, true) if(score){ score.title = songObj.title } } }) } addAssets(){ var promises = [] for(let name in this.assetFiles){ let id = this.getFilename(name) var file = this.assetFiles[name] var index = name.lastIndexOf(".") if(name === "vectors.json"){ promises.push(file.read().then(response => { vectors = JSON.parse(response) })) } if(name.endsWith(".png") || name.endsWith(".gif")){ let image = document.createElement("img") promises.push(pageEvents.load(image).then(() => { if(id in this.assetSelectors){ var selector = this.assetSelectors[id] var gradient = "" if(selector === "#song-search"){ gradient = loader.songSearchGradient } this.stylesheet.push(loader.cssRuleset({ [selector]: { "background-image": gradient + "url(\"" + image.src + "\")" } })) } })) image.id = name promises.push(file.blob().then(blob => { image.src = URL.createObjectURL(blob) })) loader.assetsDiv.appendChild(image) var oldImage = assets.image[id] if(oldImage && oldImage.parentNode){ URL.revokeObjectURL(oldImage.src) oldImage.parentNode.removeChild(oldImage) } assets.image[id] = image } if(assets.audioSfx.indexOf(name) !== -1){ assets.sounds[id].clean() promises.push(this.loadSound(file, name, snd.sfxGain)) } if(assets.audioMusic.indexOf(name) !== -1){ assets.sounds[id].clean() promises.push(this.loadSound(file, name, snd.musicGain)) } if(assets.audioSfxLR.indexOf(name) !== -1){ assets.sounds[id + "_p1"].clean() assets.sounds[id + "_p2"].clean() promises.push(this.loadSound(file, name, snd.sfxGain).then(sound => { assets.sounds[id + "_p1"] = assets.sounds[id].copy(snd.sfxGainL) assets.sounds[id + "_p2"] = assets.sounds[id].copy(snd.sfxGainR) })) } if(assets.audioSfxLoud.indexOf(name) !== -1){ assets.sounds[id].clean() promises.push(this.loadSound(file, name, snd.sfxLoudGain)) } if(this.comboVoices.indexOf(id) !== -1){ promises.push(snd.sfxGain.load(file).then(sound => { assets.sounds[id] = sound assets.sounds[id + "_p1"] = assets.sounds[id].copy(snd.sfxGainL) assets.sounds[id + "_p2"] = assets.sounds[id].copy(snd.sfxGainR) })) } } return Promise.all(promises) } loadSound(file, name, gain){ var id = this.getFilename(name) return gain.load(file).then(sound => { assets.sounds[id] = sound }) } getFilename(name){ return name.slice(0, name.lastIndexOf(".")) } addPlugin(fileObj){ var file = fileObj.file var filePromise = file.read() return filePromise.then(dataRaw => { var name = file.name.slice(0, file.name.lastIndexOf(".taikoweb.js")) this.plugins.push({ name: name, data: dataRaw }) }) } getCategory(file, exclude){ var path = file.path.toLowerCase().split("/") for(var i = path.length - 2; i >= 0; i--){ var hasTitle = false for(var j in exclude){ if(exclude[j] && path[i].indexOf(exclude[j].toLowerCase()) !== -1){ hasTitle = true break } } if(!hasTitle){ for(var cat in this.categoryAliases){ if(path[i].indexOf(cat) !== -1){ return this.categoryAliases[cat] } } } } } getSkin(dir, config){ var configArray = config.toLowerCase().split(",") var configObj = {} for(var i in configArray){ var string = configArray[i].trim() var space = string.indexOf(" ") if(space !== -1){ configObj[string.slice(0, space).trim()] = string.slice(space + 1).trim() } } if(!configObj.dir){ configObj.dir = "" } configObj.prefix = "custom " var skinnable = ["song", "stage", "don"] for(var i in skinnable){ var skinName = skinnable[i] var skinValue = configObj[skinName] if(skinValue && skinValue !== "none"){ var fileName = "bg_" + skinName + "_" + configObj.name var skinPath = this.joinPath(dir, configObj.dir, fileName) for(var j = 0; j < 2; j++){ if(skinValue !== "static"){ var suffix = (j === 0 ? "_a" : "_b") + ".png" }else{ var suffix = ".png" } var skinFull = this.normPath(skinPath + suffix) if(skinFull in this.otherFiles){ configObj[fileName + suffix] = this.otherFiles[skinFull] }else{ configObj[skinName] = null } if(skinValue === "static"){ break } } } } return configObj } loaded(){ this.songs = this.songs.filter(song => typeof song !== "undefined") if(this.stylesheet.length){ var style = document.createElement("style") style.appendChild(document.createTextNode(this.stylesheet.join("\n"))) document.head.appendChild(style) } if(this.songs.length){ if(this.limited){ assets.otherFiles = this.otherFiles assets.otherFiles.songTitle = this.songTitle } return Promise.resolve(this.songs) }else{ if(this.noPlugins && this.pluginAmount || Object.keys(this.assetFiles).length){ return Promise.resolve() }else{ return Promise.reject("nosongs") } } this.clean() } joinPath(){ var resultPath = arguments[0] for(var i = 1; i < arguments.length; i++){ var pPath = arguments[i] if(pPath && (pPath[0] === "/" || pPath[0] === "\\")){ resultPath = pPath }else{ var lastChar = resultPath.slice(-1) if(resultPath && (lastChar !== "/" || lastChar !== "\\")){ resultPath = resultPath + "/" } resultPath = resultPath + pPath } } return resultPath } normPath(path){ path = path.replace(/\\/g, "/").toLowerCase() while(path[0] === "/"){ path = path.slice(1) } var comps = path.split("/") for(var i = 0; i < comps.length; i++){ if(comps[i] === "." || comps[i] === ""){ comps.splice(i, 1) i-- }else if(i !== 0 && comps[i] === ".." && comps[i - 1] !== ".."){ comps.splice(i - 1, 2) i -= 2 } } return comps.join("/") } clean(){ delete this.songs delete this.tjaFiles delete this.osuFiles delete this.assetFiles delete this.otherFiles } }