|
@@ -5,7 +5,7 @@ import { isModInstalled, installEssentialMods } from './modActions';
|
|
|
const { remote } = window.require('electron')
|
|
|
const fs = remote.require('fs')
|
|
|
const path = remote.require('path')
|
|
|
-const md5 = remote.require('md5')
|
|
|
+const crypto = remote.require('crypto')
|
|
|
const AdmZip = remote.require('adm-zip')
|
|
|
const request = remote.require('request')
|
|
|
const rimraf = remote.require('rimraf')
|
|
@@ -85,26 +85,12 @@ export const downloadSong = (identity) => (dispatch, getState) => {
|
|
|
}
|
|
|
let zip = new AdmZip(data)
|
|
|
let zipEntries = zip.getEntries()
|
|
|
- let infoEntry, infoObject
|
|
|
+ let infoEntry
|
|
|
for(let i = 0; i < zipEntries.length; i++) {
|
|
|
- if(zipEntries[i].entryName.substr(zipEntries[i].entryName.length - 9, 9) === 'info.json' || zipEntries[i].entryName.substr(zipEntries[i].entryName.length - 8, 8) === 'info.dat') {
|
|
|
+ if(zipEntries[i].entryName.split(path.sep).pop() === 'info.dat') {
|
|
|
infoEntry = zipEntries[i]
|
|
|
}
|
|
|
}
|
|
|
- try {
|
|
|
- infoObject = JSON.parse(infoEntry.getData().toString('UTF8'))
|
|
|
- } catch(err) {
|
|
|
- dispatch({
|
|
|
- type: DISPLAY_WARNING,
|
|
|
- payload: {
|
|
|
- text: `There was an error unpacking the song "${song.name}." The song's files may be corrupt or use formatting other than UTF-8 (Why UTF-8? The IETF says so! https://tools.ietf.org/html/rfc8259#section-8.1). Please try again and contact the song's uploader, ${song.uploader.username}, if problem persists.`
|
|
|
- }
|
|
|
- })
|
|
|
- return
|
|
|
- }
|
|
|
- infoObject.key = song.key
|
|
|
- infoObject.hash = hash
|
|
|
- zip.updateFile(infoEntry.entryName, JSON.stringify(infoObject))
|
|
|
let extractTo
|
|
|
switch(getState().settings.folderStructure) {
|
|
|
case 'keySongNameArtistName':
|
|
@@ -121,6 +107,42 @@ export const downloadSong = (identity) => (dispatch, getState) => {
|
|
|
break
|
|
|
}
|
|
|
zip.extractAllTo(path.join(getState().settings.installationDirectory, 'Beat Saber_Data', 'CustomLevels', extractTo))
|
|
|
+ let metadataFile = path.join(getState().settings.installationDirectory, 'Beat Saber_Data', 'CustomLevels', extractTo, 'metadata.dat')
|
|
|
+ fs.access(metadataFile, accessErr => {
|
|
|
+ if(accessErr) {
|
|
|
+ fs.writeFile(metadataFile, JSON.stringify({ key: song.key, hash: song.hash, downloadTime: utc }), err => {
|
|
|
+ if(err) {
|
|
|
+ dispatch({
|
|
|
+ type: DISPLAY_WARNING,
|
|
|
+ payload: {
|
|
|
+ text: `Failed to write metadata file for ${ song.name }. Go to settings and press "Scan for Songs" to try again.`
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ fs.readFile(metadataFile, 'UTF-8', (err, metadata) => {
|
|
|
+ if(err) {
|
|
|
+ dispatch({
|
|
|
+ type: DISPLAY_WARNING,
|
|
|
+ payload: {
|
|
|
+ text: `Failed to read metadata file for ${ song.name }. Go to settings and press "Scan for Songs" to try again.`
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ fs.writeFile(metadataFile, JSON.stringify({ ...JSON.parse(metadata), key: song.key, hash: song.hash, downloadTime: utc }), err => {
|
|
|
+ if(err) {
|
|
|
+ dispatch({
|
|
|
+ type: DISPLAY_WARNING,
|
|
|
+ payload: {
|
|
|
+ text: `Failed to write metadata file for ${ song.name }. Go to settings and press "Scan for Songs" to try again.`
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ })
|
|
|
+ })
|
|
|
+ })
|
|
|
dispatch({
|
|
|
type: SET_DOWNLOADED_SONGS,
|
|
|
payload: [...getState().songs.downloadedSongs, { hash, file: path.join(getState().settings.installationDirectory, 'Beat Saber_Data', 'CustomLevels', extractTo, infoEntry.entryName) }]
|
|
@@ -393,6 +415,65 @@ export const deleteSong = (identity) => (dispatch, getState) => {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
+export const hashAndWriteToMetadata = (infoFile) => dispatch => {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ let metadataFile = path.join(path.dirname(infoFile), 'metadata.dat')
|
|
|
+ fs.readFile(infoFile, { encoding: 'UTF-8' }, (infoReadErr, infoData) => { // Read the info.dat file
|
|
|
+ if(infoReadErr) {
|
|
|
+ dispatch({
|
|
|
+ type: DISPLAY_WARNING,
|
|
|
+ payload: {
|
|
|
+ text: `Failed to read info file ${ infoFile }. Go to settings and press "Scan for Songs" to try again.`
|
|
|
+ }
|
|
|
+ })
|
|
|
+ reject(infoReadErr)
|
|
|
+ }
|
|
|
+ let song = JSON.parse(infoData),
|
|
|
+ dataToHash = '',
|
|
|
+ fileToHash,
|
|
|
+ hash
|
|
|
+ fs.readFile(metadataFile, 'UTF-8', (readMetadataErr, metadataData) => {
|
|
|
+ if(readMetadataErr || !JSON.parse(metadataData).hasOwnProperty('hash')) {
|
|
|
+ try {
|
|
|
+ dataToHash += infoData
|
|
|
+ for(let set = 0; set < song._difficultyBeatmapSets.length; set++) {
|
|
|
+ for (let map = 0; map < song._difficultyBeatmapSets[set]._difficultyBeatmaps.length; map++) {
|
|
|
+ fileToHash = path.join(path.dirname(infoFile), song._difficultyBeatmapSets[set]._difficultyBeatmaps[map]._beatmapFilename)
|
|
|
+ dataToHash += fs.readFileSync(fileToHash, 'UTF8')
|
|
|
+ }
|
|
|
+ }
|
|
|
+ hash = crypto.createHash('sha1') // Calculate song hash
|
|
|
+ .update(dataToHash)
|
|
|
+ .digest('hex')
|
|
|
+ } catch(hashingErr) {
|
|
|
+ dispatch({
|
|
|
+ type: DISPLAY_WARNING,
|
|
|
+ payload: {
|
|
|
+ text: `Failed to calculate hash: ${ fileToHash } could not be accessed.`
|
|
|
+ }
|
|
|
+ })
|
|
|
+ reject(hashingErr)
|
|
|
+ }
|
|
|
+ fs.writeFile(metadataFile, JSON.stringify({ ...(readMetadataErr ? {} : JSON.parse(metadataData)), hash: (readMetadataErr ? hash : JSON.parse(metadataData).hash), scannedTime: Date.now() }), writeErr => { // Save metadata.dat file
|
|
|
+ if(writeErr) {
|
|
|
+ dispatch({
|
|
|
+ type: DISPLAY_WARNING,
|
|
|
+ payload: {
|
|
|
+ text: `Failed to write metadata file for ${ song.name }. Go to settings and press "Scan for Songs" to try again.`
|
|
|
+ }
|
|
|
+ })
|
|
|
+ reject(writeErr)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ hash = JSON.parse(metadataData).hash
|
|
|
+ }
|
|
|
+ resolve(hash)
|
|
|
+ })
|
|
|
+ })
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
export const checkDownloadedSongs = () => (dispatch, getState) => {
|
|
|
let discoveredFiles = 0, processedFiles = 0
|
|
|
dispatch({
|
|
@@ -403,7 +484,7 @@ export const checkDownloadedSongs = () => (dispatch, getState) => {
|
|
|
type: SET_PROCESSED_FILES,
|
|
|
payload: processedFiles
|
|
|
})
|
|
|
- const walk = function(pathName, cb) {
|
|
|
+ function walk(pathName, cb) {
|
|
|
let songs = []
|
|
|
fs.readdir(pathName, (err, files) => {
|
|
|
if(err) return cb(err)
|
|
@@ -427,55 +508,37 @@ export const checkDownloadedSongs = () => (dispatch, getState) => {
|
|
|
if(!--pending) cb(null, songs)
|
|
|
})
|
|
|
} else {
|
|
|
- if(files[i].toLowerCase() === 'info.dat' || files[i].toLowerCase() === 'info.json') {
|
|
|
- fs.readFile(file, { encoding: 'UTF-8' }, (err, data) => {
|
|
|
- if(err) return cb(err)
|
|
|
- let song = JSON.parse(data)
|
|
|
- if(song.hasOwnProperty('hash')) {
|
|
|
- songs.push({ hash: song.hash, file })
|
|
|
- dispatch({
|
|
|
- type: SET_PROCESSED_FILES,
|
|
|
- payload: ++processedFiles
|
|
|
- })
|
|
|
- if(!--pending) cb(null, songs)
|
|
|
- } else {
|
|
|
- let to_hash = ''
|
|
|
- try {
|
|
|
- for (let i = 0; i < song.difficultyLevels.length; i++) {
|
|
|
- to_hash += fs.readFileSync(path.join(path.dirname(file), song.difficultyLevels[i].jsonPath), 'UTF8')
|
|
|
- }
|
|
|
- let hash = md5(to_hash)
|
|
|
- song.hash = hash
|
|
|
- fs.writeFile(file, JSON.stringify(song), 'UTF8', (err) => { if(err) return })
|
|
|
+ switch(files[i].toLowerCase()) {
|
|
|
+ case 'info.dat': // In case of an info file
|
|
|
+ hashAndWriteToMetadata(file)(dispatch, getState)
|
|
|
+ .then(hash => {
|
|
|
songs.push({ hash, file })
|
|
|
- } catch(err) {
|
|
|
dispatch({
|
|
|
- type: DISPLAY_WARNING,
|
|
|
- payload: {
|
|
|
- text: `Failed to generate hash: a file could not be accessed.`
|
|
|
- }
|
|
|
+ type: SET_PROCESSED_FILES,
|
|
|
+ payload: ++processedFiles
|
|
|
})
|
|
|
- }
|
|
|
- dispatch({
|
|
|
- type: SET_PROCESSED_FILES,
|
|
|
- payload: ++processedFiles
|
|
|
+ if(!--pending) cb(null, songs)
|
|
|
+ }, _ => {
|
|
|
+ dispatch({
|
|
|
+ type: SET_PROCESSED_FILES,
|
|
|
+ payload: ++processedFiles
|
|
|
+ })
|
|
|
+ if(!--pending) cb(null, songs)
|
|
|
})
|
|
|
- if(!--pending) cb(null, songs)
|
|
|
- }
|
|
|
- })
|
|
|
- } else {
|
|
|
- dispatch({
|
|
|
- type: SET_PROCESSED_FILES,
|
|
|
- payload: ++processedFiles
|
|
|
- })
|
|
|
- if(!--pending) cb(null, songs)
|
|
|
+ break
|
|
|
+ default:
|
|
|
+ dispatch({
|
|
|
+ type: SET_PROCESSED_FILES,
|
|
|
+ payload: ++processedFiles
|
|
|
+ })
|
|
|
+ if(!--pending) cb(null, songs)
|
|
|
+ break
|
|
|
}
|
|
|
}
|
|
|
})
|
|
|
}
|
|
|
})
|
|
|
}
|
|
|
-
|
|
|
dispatch({
|
|
|
type: SET_SCANNING_FOR_SONGS,
|
|
|
payload: true
|
|
@@ -483,18 +546,18 @@ export const checkDownloadedSongs = () => (dispatch, getState) => {
|
|
|
walk(path.join(getState().settings.installationDirectory, 'Beat Saber_Data', 'CustomLevels'), (err, songs) => {
|
|
|
if (err) {
|
|
|
dispatch({
|
|
|
+ type: DISPLAY_WARNING,
|
|
|
+ payload: {
|
|
|
+ text: `Could not find CustomLevels folder. Please make sure you have your installation directory and type are set correctly.`
|
|
|
+ }
|
|
|
+ })
|
|
|
+ fs.mkdir(path.join(getState().settings.installationDirectory, 'Beat Saber_Data', 'CustomLevels'), () => {
|
|
|
+ dispatch({
|
|
|
type: DISPLAY_WARNING,
|
|
|
payload: {
|
|
|
- text: `Could not find Custom Levels directory. Please make sure you have your installation directory and type are set correctly.`
|
|
|
+ text: `Attempted to create CustomLevels folder and failed, likely due to permissions.`
|
|
|
}
|
|
|
- })
|
|
|
- fs.mkdir(path.join(getState().settings.installationDirectory, 'Beat Saber_Data', 'CustomLevels'), () => {
|
|
|
- dispatch({
|
|
|
- type: DISPLAY_WARNING,
|
|
|
- payload: {
|
|
|
- text: `Attempted to create Custom levels folder and failed, likely due to permissions.`
|
|
|
- }
|
|
|
- })
|
|
|
+ })
|
|
|
})
|
|
|
}
|
|
|
dispatch({
|