소스 검색

Bug fixes for local song code

Add new folder naming scheme
StarGazer1258 5 년 전
부모
커밋
b02b5c840d

+ 1 - 1
package-lock.json

@@ -1,6 +1,6 @@
 {
   "name": "beatdrop",
-  "version": "2.5.0-beta",
+  "version": "2.5.1-beta",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {

+ 1 - 1
package.json

@@ -2,7 +2,7 @@
   "name": "beatdrop",
   "description": "A desktop app for downloading Beat Saber songs.",
   "author": "Nathaniel Johns (StarGazer1258)",
-  "version": "2.5.0-beta",
+  "version": "2.5.1-beta",
   "private": false,
   "license": "CC-BY-NC-SA-4.0",
   "repository": {

+ 2 - 2
src/actions/detailsActions.js

@@ -26,7 +26,7 @@ export const loadDetailsFromFile = file => dispatch => {
     if(err) return
     let details = JSON.parse(data)
     let dir = path.dirname(file)
-    details.coverURL = `file://${ path.join(dir, details.coverImagePath) }`
+    details.coverURL = `file://${ path.join(dir, (details.coverImagePath || details._coverImageFilename)) }`
     details.file = path.join(dir, 'info.json' || 'info.dat')
     dispatch({
       type: LOAD_DETAILS,
@@ -34,7 +34,7 @@ export const loadDetailsFromFile = file => dispatch => {
     })
     dispatch({
       type: LOAD_DETAILS,
-      payload: { audioSource: `file://${ path.join(dir, details.difficultyLevels[0].audioPath) }` }
+      payload: { audioSource: `file://${ path.join(dir, details._songFilename) }` }
     })
     dispatch({
       type: SET_DETAILS_LOADING,

+ 119 - 100
src/actions/queueActions.js

@@ -47,15 +47,15 @@ export const downloadSong = (identity) => (dispatch, getState) => {
         }, 1000)
         fetch(`https://beatsaver.com/api/maps/by-hash/${hash}`)
           .then(res =>  res.json())
-          .then(results => {
+          .then(song => {
             let utc = Date.now()
-            if(results) {
+            if(song) {
               dispatch({
                 type: ADD_TO_QUEUE,
-                payload: { ...results, utc }
+                payload: { ...song, utc }
               })
               let req = request.get({
-                url: `http://www.beatsaver.com${results.downloadURL}`,
+                url: `http://www.beatsaver.com${song.downloadURL}`,
                 encoding: null
               }, (err, r, data) => {
                 try {
@@ -97,19 +97,33 @@ export const downloadSong = (identity) => (dispatch, getState) => {
                   dispatch({
                     type: DISPLAY_WARNING,
                     payload: {
-                      text: `There was an error unpacking the song "${results.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, ${results.uploader.username}, if problem persists.`
+                      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 = results.key
+                infoObject.key = song.key
                 infoObject.hash = hash
                 zip.updateFile(infoEntry.entryName, JSON.stringify(infoObject))
-                let extractTo = (getState().settings.folderStructure === 'idKey' ? results.key : results.name.replace(/[\\/:*?"<>|.]/g, ''))
-                zip.extractAllTo(path.join(getState().settings.installationDirectory, 'CustomSongs', extractTo))
+                let extractTo
+                switch(getState().settings.folderStructure) {
+                  case 'keySongNameArtistName':
+                    extractTo = `${ song.key } (${ song.metadata.songName.replace(/[\\/:*?"<>|.]/g, '') } - ${ song.metadata.songAuthorName })`
+                    break
+                  case 'key':
+                    extractTo = song.key
+                    break
+                  case 'songName':
+                    extractTo = song.name.replace(/[\\/:*?"<>|.]/g, '')
+                    break
+                  default:
+                    extractTo = `${ song.key } (${ song.name.replace(/[\\/:*?"<>|.]/g, '') } - ${ song.songAuthorName })`
+                    break
+                }
+                zip.extractAllTo(path.join(getState().settings.installationDirectory, 'Beat Saber_Data', 'CustomLevels', extractTo))
                 dispatch({
                   type: SET_DOWNLOADED_SONGS,
-                  payload: [...getState().songs.downloadedSongs, { hash, file: path.join(getState().settings.installationDirectory, 'CustomSongs', extractTo, infoEntry.entryName) }]
+                  payload: [...getState().songs.downloadedSongs, { hash, file: path.join(getState().settings.installationDirectory, 'Beat Saber_Data', 'CustomLevels', extractTo, infoEntry.entryName) }]
                 })
                 state = { ...getState() }
                 dispatch({
@@ -139,7 +153,7 @@ export const downloadSong = (identity) => (dispatch, getState) => {
                   type: UPDATE_PROGRESS,
                   payload: {
                     utc,
-                    hash: results.hash,
+                    hash: song.hash,
                     progress: Math.trunc((chunks / len) * 100)
                   }
                 })
@@ -203,111 +217,116 @@ export const downloadSong = (identity) => (dispatch, getState) => {
     }, 1000)
     fetch(`https://beatsaver.com/api/maps/by-hash/${hash}`)
       .then(res =>  res.json())
-      .then(results => {
+      .then(song => {
         let utc = Date.now()
-        if(results.songs.length === 1) {
-          dispatch({
-            type: ADD_TO_QUEUE,
-            payload: { ...results.songs[0], utc }
-          })
-          let req = request.get({
-            url: results.downloadURL,
-            encoding: null
-          }, (err, r, data) => {
-            try {
-              // eslint-disable-next-line
-              if(err || r ? r.statusCode !== 200 : false) throw 'Error downloading!'
-            } catch(err) {
-              state = { ...getState() }
-                dispatch({
-                  type: SET_DOWNLOADING_COUNT,
-                  payload: --state.songs.downloadingCount
-                })
-                if(state.songs.waitingToDownload.length > 0) {
-                  let toDownload = state.songs.waitingToDownload.pop()
-                  dispatch({
-                    type: SET_WAIT_LIST,
-                    payload: state.songs.waitingToDownload
-                  })
-                  downloadSong(toDownload)(dispatch, getState)
-                }
+        dispatch({
+          type: ADD_TO_QUEUE,
+          payload: { ...song, utc }
+        })
+        let req = request.get({
+          url: `https://beatsaver.com/${ song.downloadURL }`,
+          encoding: null
+        }, (err, r, data) => {
+          try {
+            // eslint-disable-next-line
+            if(err || r ? r.statusCode !== 200 : false) throw 'Error downloading!'
+          } catch(err) {
+            state = { ...getState() }
+              dispatch({
+                type: SET_DOWNLOADING_COUNT,
+                payload: --state.songs.downloadingCount
+              })
+              if(state.songs.waitingToDownload.length > 0) {
+                let toDownload = state.songs.waitingToDownload.pop()
                 dispatch({
-                  type: DISPLAY_WARNING,
-                  payload: {
-                    text: `There was an error downloading the song with hash ${hash}. There may have been an error with BeatSaver's servers or the song may no longer be available. Please try again and contact the BeatSaver developers if problem persists.`
-                  }
+                  type: SET_WAIT_LIST,
+                  payload: state.songs.waitingToDownload
                 })
-                return
-            }
-            let zip = new AdmZip(data)
-            let zipEntries = zip.getEntries()
-            let infoEntry, infoObject
-            for(let i = 0; i < zipEntries.length; i++) {
-              if(zipEntries[i].entryName.substr(zipEntries[i].entryName.length - 9, 9) === 'info.json') {
-                infoEntry = zipEntries[i]
+                downloadSong(toDownload)(dispatch, getState)
               }
-            }
-            try {
-              infoObject = JSON.parse(infoEntry.getData().toString('UTF8'))
-            } catch(err) {
               dispatch({
                 type: DISPLAY_WARNING,
                 payload: {
-                  text: `There was an error unpacking the song "${results.songs[0].songName}." 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, ${results.songs[0].uploader}, if problem persists.`
+                  text: `There was an error downloading the song with hash ${hash}. There may have been an error with BeatSaver's servers or the song may no longer be available. Please try again and contact the BeatSaver developers if problem persists.`
                 }
               })
               return
+          }
+          let zip = new AdmZip(data)
+          let zipEntries = zip.getEntries()
+          let infoEntry, infoObject
+          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, 9) === 'info.dat') {
+              infoEntry = zipEntries[i]
             }
-            infoObject.key = results.songs[0].key
-            infoObject.hash = hash
-            zip.updateFile(infoEntry.entryName, JSON.stringify(infoObject))
-            let extractTo = (getState().settings.folderStructure === 'idKey' ? results.songs[0].key : results.songs[0].name.replace(/[\\/:*?"<>|.]/g, ''))
-            zip.extractAllTo(path.join(getState().settings.installationDirectory, 'CustomSongs', extractTo))
+          }
+          try {
+            infoObject = JSON.parse(infoEntry.getData().toString('UTF8'))
+          } catch(err) {
             dispatch({
-              type: SET_DOWNLOADED_SONGS,
-              payload: [...getState().songs.downloadedSongs, { hash, file: path.join(getState().settings.installationDirectory, 'CustomSongs', extractTo, infoEntry.entryName) }]
+              type: DISPLAY_WARNING,
+              payload: {
+                text: `There was an error unpacking the song "${song.songName}." 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.`
+              }
             })
-            state = { ...getState() }
+            return
+          }
+          infoObject.key = song.key
+          infoObject.hash = hash
+          zip.updateFile(infoEntry.entryName, JSON.stringify(infoObject))
+          let extractTo
+          switch(getState().settings.folderStructure) {
+            case 'keySongNameArtistName':
+              extractTo = `${ song.key } (${ song.metadata.songName.replace(/[\\/:*?"<>|.]/g, '') } - ${ song.metadata.songAuthorName })`
+              break
+            case 'key':
+              extractTo = song.key
+              break
+            case 'songName':
+              extractTo = song.name.replace(/[\\/:*?"<>|.]/g, '')
+              break
+            default:
+              extractTo = `${ song.key } (${ song.name.replace(/[\\/:*?"<>|.]/g, '') } - ${ song.songAuthorName })`
+              break
+          }
+          zip.extractAllTo(path.join(getState().settings.installationDirectory, 'Beat Saber_Data', 'CustomLevels', extractTo))
+          dispatch({
+            type: SET_DOWNLOADED_SONGS,
+            payload: [...getState().songs.downloadedSongs, { hash, file: path.join(getState().settings.installationDirectory, 'Beat Saber_Data', 'CustomLevels', extractTo, infoEntry.entryName) }]
+          })
+          state = { ...getState() }
+          dispatch({
+            type: SET_DOWNLOADING_COUNT,
+            payload: --state.songs.downloadingCount
+          })
+          if(state.songs.waitingToDownload.length > 0) {
+            let toDownload = state.songs.waitingToDownload.pop()
             dispatch({
-              type: SET_DOWNLOADING_COUNT,
-              payload: --state.songs.downloadingCount
+              type: SET_WAIT_LIST,
+              payload: state.songs.waitingToDownload
             })
-            if(state.songs.waitingToDownload.length > 0) {
-              let toDownload = state.songs.waitingToDownload.pop()
-              dispatch({
-                type: SET_WAIT_LIST,
-                payload: state.songs.waitingToDownload
-              })
-              downloadSong(toDownload)(dispatch, getState)
-            }
-          })
+            downloadSong(toDownload)(dispatch, getState)
+          }
+        })
 
-          let len = 0
-          let chunks = 0
+        let len = 0
+        let chunks = 0
 
-          req.on('response', (data) => {
-            len = data.headers['content-length']
-          })
+        req.on('response', (data) => {
+          len = data.headers['content-length']
+        })
 
-          req.on('data', (chunk) => {
-            chunks += chunk.length
-            dispatch({
-              type: UPDATE_PROGRESS,
-              payload: {
-                utc,
-                hash: results.songs[0].hashMd5,
-                progress: Math.trunc((chunks / len) * 100)
-              }
-            })
-          })
-        } else {
+        req.on('data', (chunk) => {
+          chunks += chunk.length
           dispatch({
-            type: DISPLAY_WARNING,
+            type: UPDATE_PROGRESS,
             payload: {
-              text: `There was an error downloading the song with hash ${hash}. The song requested is no longer be available for download.`
+              utc,
+              hash: song.hash,
+              progress: Math.trunc((chunks / len) * 100)
             }
           })
-        }
+        })
       })
       .catch(err => {
         state = { ...getState() }
@@ -346,19 +365,19 @@ export const deleteSong = (identity) => (dispatch, getState) => {
     type: SET_VIEW,
     payload: getState().view.previousView
   })
-  let dirs = file.split('\\')
-  let csd = dirs.indexOf('CustomSongs')
-  for(let i = 2; i < file.split('\\').length - csd; i++) {
+  let dirs = file.split(path.sep)
+  let cld = dirs.indexOf('CustomLevels')
+  for(let i = 2; i < file.split(path.sep).length - cld; i++) {
     dirs.pop()
   }
   let downloadedSongs = [ ...getState().songs.downloadedSongs ]
   downloadedSongs.splice(downloadedSongs.findIndex(song => song.file === file), 1)
-  rimraf(dirs.join('\\'), (err) => {
+  rimraf(dirs.join(path.sep), (err) => {
     if(err) {
       dispatch({
         type: DISPLAY_WARNING,
         payload: {
-          text: `There was an error deleting the song located at ${dirs.join('\\')}. BeatDrop may have insufficient permissions or the file may be in use by another program. Please try closing any programs that may be using this song and try again.`
+          text: `There was an error deleting the song located at ${dirs.join(path.sep)}. BeatDrop may have insufficient permissions or the file may be in use by another program. Please try closing any programs that may be using this song and try again.`
         }
       })
       return
@@ -408,7 +427,7 @@ export const checkDownloadedSongs = () => (dispatch, getState) => {
               if(!--pending) cb(null, songs)
             })
           } else {
-            if(files[i].toLowerCase() === 'info.json') {
+            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)
@@ -462,7 +481,7 @@ export const checkDownloadedSongs = () => (dispatch, getState) => {
     payload: true
   })
 
-  walk(path.join(getState().settings.installationDirectory, 'CustomSongs'), (err, songs) => {
+  walk(path.join(getState().settings.installationDirectory, 'Beat Saber_Data', 'CustomLevels'), (err, songs) => {
     dispatch({
       type: SET_DOWNLOADED_SONGS,
       payload: songs

+ 3 - 3
src/actions/searchActions.js

@@ -59,9 +59,9 @@ export const submitSearch = keywords => dispatch => {
       localResultsReady = true
     }
   }
-  fs.access(path.join(state.settings.installationDirectory, 'CustomSongs'), err => {
-    if (err) alert('Could not find CustomSongs directory. Please make sure you have your installation directory set correctly and have the proper plugins installed.')
-    Walker(path.join(store.getState().settings.installationDirectory, 'CustomSongs'))
+  fs.access(path.join(state.settings.installationDirectory, 'Beat Saber_Data', 'CustomLevels'), err => {
+    if (err) alert('Could not find CustomLevels directory. Please make sure you have your installation directory set correctly and have the proper plugins installed.')
+    Walker(path.join(state.settings.installationDirectory, 'Beat Saber_Data', 'CustomLevels'))
       .on('file', file => {
         if (file.substr(file.length - 9) === 'info.json') {
           localSongCount++

+ 42 - 6
src/actions/songListActions.js

@@ -175,6 +175,39 @@ export const fetchLocalSongs = () => (dispatch, getState) => {
     type: SET_RESOURCE,
     payload: LIBRARY.SONGS
   })
+
+  let downloadedSongs = getState().songs.downloadedSongs
+  let songs = []
+  for(let i = 0; i < downloadedSongs.length; i++) {
+    fs.readFile(downloadedSongs[i].file, 'UTF-8', (err, data) => {
+      if(err) {
+        return
+      }
+      let song
+      try {
+        song = JSON.parse(data)
+      } catch(err) {
+        return
+      }
+      song.coverUrl = `file://${ path.join(path.dirname(downloadedSongs[i].file), (song.coverImagePath || song._coverImageFilename)) }`
+      song.file = downloadedSongs[i].file
+      songs.push(song)
+      console.log(song)
+      if(i >= downloadedSongs.length - 1) {
+        console.log('Bam!')
+        dispatch({
+          type: FETCH_LOCAL_SONGS,
+          payload: songs
+        })
+        dispatch({
+          type: SET_LOADING,
+          payload: false
+        })
+      }
+    })
+  }
+  
+  /*
   let songs = []
   let count = 0
   let ended = false
@@ -192,12 +225,12 @@ export const fetchLocalSongs = () => (dispatch, getState) => {
       return
     }
   }
-  fs.access(path.join(state.settings.installationDirectory, 'CustomSongs'), (err) => {
+  fs.access(path.join(state.settings.installationDirectory, 'Beat Saber_Data', 'CustomLevels'), (err) => {
     if(err) {
       installEssentialMods()(dispatch, getState)
-      fs.mkdirSync(path.join(state.settings.installationDirectory, 'CustomSongs'))
+      fs.mkdirSync(path.join(state.settings.installationDirectory, 'Beat Saber_Data', 'CustomLevels'))
     }
-    fs.readdir(path.join(state.settings.installationDirectory, 'CustomSongs'), (err, files) => {
+    fs.readdir(path.join(state.settings.installationDirectory, 'Beat Saber_Data', 'CustomLevels'), (err, files) => {
       if (err) return
       if (!files.length) {
         dispatch({
@@ -218,10 +251,11 @@ export const fetchLocalSongs = () => (dispatch, getState) => {
         })
       }
     })
-    Walker(path.join(getState().settings.installationDirectory, 'CustomSongs'))
+    Walker(path.join(state.settings.installationDirectory, 'Beat Saber_Data', 'CustomLevels'))
       .on('file', (file) => {
+        console.log(file)
         let dir = path.dirname(file)
-        if(file.substr(file.length - 9) === 'info.json') {
+        if(file.substr(file.length - 8) === 'info.dat' || file.substr(file.length - 9) === 'info.json') {
           if(!isModInstalled('SongCore')(dispatch, getState)) installEssentialMods()(dispatch, getState)
           count++
           fs.readFile(file, 'UTF-8', (err, data) => {
@@ -234,13 +268,14 @@ export const fetchLocalSongs = () => (dispatch, getState) => {
               return
             }
             song.coverUrl = `file://${ path.join(dir, (song.coverImagePath || song._coverImageFilename)) }`
-            song.file = path.join(dir, 'info.json')
+            song.file = path.join(file)
             songs.push(song)
             decrementCounter()
           })
         }
       })
       .on('end', () => {
+        console.log('end')
         if(count === 0) {
           dispatch({
             type: FETCH_LOCAL_SONGS,
@@ -255,6 +290,7 @@ export const fetchLocalSongs = () => (dispatch, getState) => {
         ended = true
       })
   })
+  */
 }
 
 export const loadMore = () => (dispatch, getState) => {

+ 2 - 1
src/components/ReleaseNotesModal.js

@@ -25,7 +25,7 @@ class ReleaseNotesModal extends Component {
             <h2 style={ { color: 'lightgreen' } }>What's new?</h2>
             <hr style={ { borderColor: 'lightgreen' } } />
             <ul>
-              <li>All fixes for now!</li>
+              <li>New option for song folder naming: <b>Key (Song Name - Song Artist)</b></li>
             </ul>
             <h2 style={ { color: 'salmon' } }>What's fixed?</h2>
             <hr style={ { borderColor: 'salmon' } } />
@@ -34,6 +34,7 @@ class ReleaseNotesModal extends Component {
               <li>UI now has <b>better compatibility with macOS.</b></li>
               <li>Implemented a bunch of <b>stability enhancements for playlists.</b></li>
               <li>Fixed a bug where <b>app would crash when moving to next song in queue after error.</b></li>
+              <li>2.5.1: Fixed <b>bugs in new local song code.</b></li>
             </ul>
             <br />
             <Button type="primary" onClick={ () => { this.props.setLatestReleaseNotes(require('../../package.json').version) } }>Awesome!</Button>

+ 2 - 1
src/components/SettingsView.js

@@ -88,7 +88,8 @@ class SettingsView extends Component {
         <Button onClick={ this.props.checkDownloadedSongs }>Scan for Songs</Button><Button onClick={ this.props.checkInstalledMods }>{ this.props.scanningForMods ? 'Scanning...' : 'Scan for Mods' }</Button><br /><br />
         <label htmlFor="folder-structure-select">Folder Structure</label><br /><br />
         <select id="folder-structure-select" name="folder-structure-select" value={ this.props.settings.folderStructure } onChange={ (e) => { this.props.setFolderStructure(e.target.value) } }>
-          <option value="idKey">ID/Key</option>
+          <option value="keySongNameArtistName">Key ( Song Name - Song Artist )</option>
+          <option value="idKey">Key</option>
           <option value="songName">Song Name</option>
         </select>
         <hr />

+ 80 - 48
src/components/SongDetails.js

@@ -25,51 +25,84 @@ const exitDetailsShortcut = function (e) { if(e.keyCode === 27) { this.props.set
 function Difficulties(props) {
   let difficulties = props.difficulties
   let badges = []
-  if(difficulties.easy) {
-    badges.push({
-      text: 'Easy',
-      backgroundColor: 'teal',
-      color: 'white'
-    })
-  }
-  if(difficulties.normal) {
-    badges.push({
-      text: 'Normal',
-      backgroundColor: 'green',
-      color: 'white'
-    })
-  }
-  if(difficulties.hard) {
-    badges.push({
-      text: 'Hard',
-      backgroundColor: 'orange',
-      color: 'white'
-    })
-  }
-  if(difficulties.expert) {
-    badges.push({
-      text: 'Expert',
-      backgroundColor: 'darkred',
-      color: 'white'
-    })
-  }
-  if(difficulties.expertPlus) {
-    badges.push({
-      text: 'Expert+',
-      backgroundColor: 'purple',
-      color: 'white'
-    })
-  }
-  return (
-    <div className="details-difficulties">
-      <div><b>Available Difficulties:</b></div>
-      {
-        badges.map((badge, i) => {
-          return <Badge key={ i } backgroundColor={ badge.backgroundColor } color={ badge.color }>{badge.text}</Badge>
+  if(Array.isArray(difficulties)) {
+    for(let i = 0; i < difficulties[0]._difficultyBeatmaps.length; i++) {
+      if(difficulties[0]._difficultyBeatmaps[i]._difficulty === 'Easy') {
+        badges.push({
+          text: 'Easy',
+          backgroundColor: 'teal',
+          color: 'white'
         })
       }
-    </div>
-  )
+      if(difficulties[0]._difficultyBeatmaps[i]._difficulty === 'Normal') {
+        badges.push({
+          text: 'Normal',
+          backgroundColor: 'green',
+          color: 'white'
+        })
+      }
+      if(difficulties[0]._difficultyBeatmaps[i]._difficulty === 'Hard') {
+        badges.push({
+          text: 'Hard',
+          backgroundColor: 'orange',
+          color: 'white'
+        })
+      }
+      if(difficulties[0]._difficultyBeatmaps[i]._difficulty === 'Expert') {
+        badges.push({
+          text: 'Expert',
+          backgroundColor: 'darkred',
+          color: 'white'
+        })
+      }
+      if(difficulties[0]._difficultyBeatmaps[i]._difficulty === 'ExpertPlus') {
+        badges.push({
+          text: 'Expert+',
+          backgroundColor: 'purple',
+          color: 'white'
+        })
+      }
+    }
+  } else {
+    if(difficulties.easy) {
+      badges.push({
+        text: 'Easy',
+        backgroundColor: 'teal',
+        color: 'white'
+      })
+    }
+    if(difficulties.normal) {
+      badges.push({
+        text: 'Normal',
+        backgroundColor: 'green',
+        color: 'white'
+      })
+    }
+    if(difficulties.hard) {
+      badges.push({
+        text: 'Hard',
+        backgroundColor: 'orange',
+        color: 'white'
+      })
+    }
+    if(difficulties.expert) {
+      badges.push({
+        text: 'Expert',
+        backgroundColor: 'darkred',
+        color: 'white'
+      })
+    }
+    if(difficulties.expertPlus) {
+      badges.push({
+        text: 'Expert+',
+        backgroundColor: 'purple',
+        color: 'white'
+      })
+    }
+  }
+  return badges.map((badge, i) => {
+    return <Badge key={ i } backgroundColor={ badge.backgroundColor } color={ badge.color }>{badge.text}</Badge>
+  })
 }
 
 function Description(props) {
@@ -148,15 +181,14 @@ class SongDetails extends Component {
         </div>
       )
     } else {
-      console.log(this.props)
       return (
         <div id="song-details">
           <div className="close-icon" title="Close" onClick={ () => {this.props.setView(this.props.previousView)} }></div>
           <img className="cover-image" src={ this.props.details.coverURL.startsWith('file://') ? this.props.details.coverURL : `https://beatsaver.com${this.props.details.coverURL}` } alt='' />
           <div className="details-info">
-            <span className="details-title" title={ this.props.details.metadata ? this.props.details.metadata.songName : this.props.songName }>{this.props.details.metadata ? this.props.details.metadata.songName : this.props.songName}</span>
-            <div className="details-subtitle" title={ this.props.details.metadata ? this.props.details.metadata.songSubName : this.props.details.songSubName }>{this.props.details.metadata ? this.props.details.metadata.songSubName : this.props.details.songSubName}</div>
-            <div className="details-artist" title={ this.props.details.metadata ? this.props.details.metadata.songAuthorName : this.props.details.authorName }>{this.props.details.metadata ? this.props.details.metadata.songAuthorName : this.props.details.authorName}</div>
+            <span className="details-title" title={ this.props.details.songName || this.props.details._songName || this.props.details.metadata.songName }>{ this.props.details.songName || this.props.details._songName || this.props.details.metadata.songName }</span>
+            <div className="details-subtitle" title={ this.props.details.songSubName || this.props.details._songSubName || this.props.details.metadata ? this.props.details.metadata.songSubName : '' }>{ this.props.details.songSubName || this.props.details._songSubName || this.props.details.metadata ? this.props.details.metadata.songSubName : '' }</div>
+            <div className="details-artist" title={ this.props.details.authorName || this.props.details.songAuthorName || this.props.details._songAuthorName || this.props.details.metadata.songAuthorName }>{ this.props.details.authorName || this.props.details.songAuthorName || this.props.details._songAuthorName || this.props.details.metadata.songAuthorName }</div>
             {this.props.downloadedSongs.some(song => song.hash === this.props.details.hash) ? <div className="song-in-library">This song is in your library.</div> : null}
             <div className="action-buttons">
               {(!!this.props.details.file || this.props.downloadedSongs.some(song => song.hash === this.props.details.hash)) ?
@@ -173,7 +205,7 @@ class SongDetails extends Component {
             </div>
             <Description details={ this.props.details } />
             <Uploader details={ this.props.details } />
-            <Difficulties difficulties={ this.props.details.difficultyLevels || this.props.details.metadata.difficulties } />
+            <Difficulties difficulties={ this.props.details.difficultyLevels || this.props.details._difficultyBeatmapSets || this.props.details.metadata.difficulties } />
             <div className="preview"><b>Preview:</b><br /><audio id="preview" src={ this.props.details.audioSource } controls controlsList="nodownload" /></div>
           </div>
           <BeatSaver details={ this.props.details } />

+ 5 - 5
src/components/SongList.js

@@ -92,11 +92,11 @@ class SongList extends Component {
               <ContextMenuTrigger id={ song.hash || song.hashMd5 }>
                 <SongListItem
                     key={ makeRenderKey(songTags) }
-                    title={ song.metadata ? song.metadata.songName : song.songName }
+                    title={ song.metadata ? song.metadata.songName : song.songName || song._songName }
                     ratings={ song.stats ? song.stats.rating : song.ratings }
-                    artist={ song.metadata ? song.metadata.songAuthorName : song.authorName }
-                    uploader={!!song.uploader ? song.uploader || song.uploader.username : ''}
-                    difficulties={ song.metadata ? song.metadata.difficulties : song.difficultyLevels || song.difficulties }
+                    artist={ song.metadata ? song.metadata.songAuthorName : song.authorName || song._songAuthorName }
+                    uploader={ !!song.uploader ? song.uploader || song.uploader.username : '' }
+                    difficulties={ song.difficultyLevels || song.difficulties || song._difficultyBeatmapSets || ((song.metadata !== undefined) ? song.metadata.difficulties : null) }
                     imageSource={ song.coverURL || song.coverUrl }
                     songKey={ song.key }
                     hash={ song.hash || song.hashMd5 }
@@ -108,7 +108,7 @@ class SongList extends Component {
                     uploadDate={ !!song.uploaded ? new Date(Date.parse(song.uploaded)).toLocaleString() : '' } />
                 <ContextMenu id={ song.hash || song.hashMd5 }>
                   <MenuItem onClick={ (e) => {e.stopPropagation(); (!!song.file || this.props.songs.downloadedSongs.some(dsong => dsong.hash === (song.hash || song.hashMd5))) ? this.props.deleteSong(song.file || song.hash || song.hashMd5) : this.props.downloadSong(song.hash || song.hashMd5)} }>
-                    {`${(!!song.file || this.props.songs.downloadedSongs.some(dsong => dsong.hash === (song.hash || song.hashMd5))) ? 'Delete'  : 'Download'} ${song.songName}`}
+                    {`${(!!song.file || this.props.songs.downloadedSongs.some(dsong => dsong.hash === (song.hash || song.hashMd5))) ? 'Delete'  : 'Download'} ${song.songName || song._songName || song.metadata.songName}`}
                   </MenuItem>
                   <MenuItem onClick={ (e) => {e.stopPropagation(); this.setState({ song }); this.props.setPlaylistPickerOpen(true)} }>
                     Add to Playlist

+ 74 - 34
src/components/SongListItem.js

@@ -36,40 +36,80 @@ function Details(props) {
 function Difficulties(props) {
   let difficulties = props.difficulties
   let badges = []
-  if(difficulties.easy) {
-    badges.push({
-      text: 'Easy',
-      backgroundColor: 'teal',
-      color: 'white'
-    })
-  }
-  if(difficulties.normal) {
-    badges.push({
-      text: 'Normal',
-      backgroundColor: 'green',
-      color: 'white'
-    })
-  }
-  if(difficulties.hard) {
-    badges.push({
-      text: 'Hard',
-      backgroundColor: 'orange',
-      color: 'white'
-    })
-  }
-  if(difficulties.expert) {
-    badges.push({
-      text: 'Expert',
-      backgroundColor: 'darkred',
-      color: 'white'
-    })
-  }
-  if(difficulties.expertPlus) {
-    badges.push({
-      text: 'Expert+',
-      backgroundColor: 'purple',
-      color: 'white'
-    })
+  if(Array.isArray(difficulties)) {
+    for(let i = 0; i < difficulties[0]._difficultyBeatmaps.length; i++) {
+      if(difficulties[0]._difficultyBeatmaps[i]._difficulty === 'Easy') {
+        badges.push({
+          text: 'Easy',
+          backgroundColor: 'teal',
+          color: 'white'
+        })
+      }
+      if(difficulties[0]._difficultyBeatmaps[i]._difficulty === 'Normal') {
+        badges.push({
+          text: 'Normal',
+          backgroundColor: 'green',
+          color: 'white'
+        })
+      }
+      if(difficulties[0]._difficultyBeatmaps[i]._difficulty === 'Hard') {
+        badges.push({
+          text: 'Hard',
+          backgroundColor: 'orange',
+          color: 'white'
+        })
+      }
+      if(difficulties[0]._difficultyBeatmaps[i]._difficulty === 'Expert') {
+        badges.push({
+          text: 'Expert',
+          backgroundColor: 'darkred',
+          color: 'white'
+        })
+      }
+      if(difficulties[0]._difficultyBeatmaps[i]._difficulty === 'ExpertPlus') {
+        badges.push({
+          text: 'Expert+',
+          backgroundColor: 'purple',
+          color: 'white'
+        })
+      }
+    }
+  } else {
+    if(difficulties.easy) {
+      badges.push({
+        text: 'Easy',
+        backgroundColor: 'teal',
+        color: 'white'
+      })
+    }
+    if(difficulties.normal) {
+      badges.push({
+        text: 'Normal',
+        backgroundColor: 'green',
+        color: 'white'
+      })
+    }
+    if(difficulties.hard) {
+      badges.push({
+        text: 'Hard',
+        backgroundColor: 'orange',
+        color: 'white'
+      })
+    }
+    if(difficulties.expert) {
+      badges.push({
+        text: 'Expert',
+        backgroundColor: 'darkred',
+        color: 'white'
+      })
+    }
+    if(difficulties.expertPlus) {
+      badges.push({
+        text: 'Expert+',
+        backgroundColor: 'purple',
+        color: 'white'
+      })
+    }
   }
   return badges.map((badge, i) => {
     return <Badge key={ i } backgroundColor={ badge.backgroundColor } color={ badge.color }>{badge.text}</Badge>

+ 1 - 1
src/reducers/index.js

@@ -12,7 +12,7 @@ import queueReducer from './queueReducer'
 import warningsReducer from './warningsReducer'
 import playlistsReducer from './playlistsReducer'
 import searchReducer from './searchReducer'
-import modReducer from './modReducer';
+import modReducer from './modReducer'
 
 export default combineReducers({
   songs: songListReducer,

+ 1 - 1
src/reducers/settingsReducer.js

@@ -9,7 +9,7 @@ const initialState = {
   offlineMode: false,
   theme: 'light',
   themeImagePath: '',
-  folderStructure: 'idKey',
+  folderStructure: 'keySongNameArtistName',
   updateChannel: 'latest',
   latestReleaseNotes: '0.0.0'
 }