Sfoglia il codice sorgente

Finish support for new BeatSaver API

Bug and stability fixes
StarGazer1258 4 anni fa
parent
commit
c02a5b8e3b

+ 1 - 1
package-lock.json

@@ -1,6 +1,6 @@
 {
   "name": "beatdrop",
-  "version": "2.3.2",
+  "version": "2.5.0-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.3.3",
+  "version": "2.5.0-beta",
   "private": false,
   "license": "CC-BY-NC-SA-4.0",
   "repository": {

+ 76 - 131
src/actions/detailsActions.js

@@ -7,15 +7,10 @@ const fs = remote.require('fs')
 const path = remote.require('path')
 
 /**
- * Loads and presents the details page for a song.
- * @param {string} identity The key/hash/file for a song
+ * Loads and presents the details page for a song from a file.
+ * @param {string} file The path to the song
  */
-export const loadDetails = (identity) => (dispatch, getState) => {
-  /*
-    Key:  !isNaN(identity.replace('-', ''))     fetch from api
-    Hash: (/^[a-f0-9]{32}$/).test(identity)     check if local, else from api
-    File: else                                  fetch from local
-  */
+export const loadDetailsFromFile = file => dispatch => {
   dispatch({
     type: CLEAR_DETAILS
   })
@@ -23,9 +18,49 @@ export const loadDetails = (identity) => (dispatch, getState) => {
     type: SET_DETAILS_LOADING,
     payload: true
   })
-  if(identity) {
-    let detailsLink = identity.startsWith("C:") ? identity : 'https://beatsaver.com/api/maps/detail/' + identity;
-    fetch(detailsLink)
+  dispatch({
+    type: SET_VIEW,
+    payload: SONG_DETAILS
+  })
+  fs.readFile(file, 'UTF-8', (err, data) => {
+    if(err) return
+    let details = JSON.parse(data)
+    let dir = path.dirname(file)
+    details.coverURL = `file://${ path.join(dir, details.coverImagePath) }`
+    details.file = path.join(dir, 'info.json' || 'info.dat')
+    dispatch({
+      type: LOAD_DETAILS,
+      payload: details
+    })
+    dispatch({
+      type: LOAD_DETAILS,
+      payload: { audioSource: `file://${ path.join(dir, details.difficultyLevels[0].audioPath) }` }
+    })
+    dispatch({
+      type: SET_DETAILS_LOADING,
+      payload: false
+    })
+  })
+}
+
+/**
+ * Loads and presents the details page for a song from a key.
+ * @param {string} key The key of the song
+ */
+export const loadDetailsFromKey = key => dispatch => {
+  dispatch({
+    type: CLEAR_DETAILS
+  })
+  dispatch({
+    type: SET_DETAILS_LOADING,
+    payload: true
+  })
+  dispatch({
+    type: SET_VIEW,
+    payload: SONG_DETAILS
+  })
+  if((/^[a-f0-9]+$/).test(key)) {
+    fetch(`https://beatsaver.com/api/maps/detail/${key}`)
       .then(res => {
         if(res.status === 404){
           dispatch({
@@ -62,136 +97,46 @@ export const loadDetails = (identity) => (dispatch, getState) => {
           payload: false
         })
       })
-      .catch(() => { })
-    dispatch({
-      type: SET_VIEW,
-      payload: SONG_DETAILS
-    })
-    let ratingsLink = identity.startsWith("C:") ? identity : `https://bsaber.com/wp-json/bsaber-api/songs/${identity}/ratings`;
-    fetch(ratingsLink)
-      .then(res => res.json())
-      .then(bsaberData => {
+      .catch(err => {
         dispatch({
-          type: LOAD_DETAILS,
-          payload: { ratings: bsaberData }
+          type: DISPLAY_WARNING,
+          payload: {
+            text: `Error downloading song: ${err}. For help, please file a bug report with this error message.`
+          }
         })
       })
-      .catch(() => { dispatch({
-        type: LOAD_DETAILS,
-        payload: {
-          ratings: {
-            overall_rating: 0,
-            average_ratings: {
-              fun_factor: 0,
-              rythm: 0,
-              flow: 0,
-              pattern_quality: 0,
-              readability: 0,
-              level_quality: 0
-            }
-          }
-        }
-      }) })
-  } else {
-    if((/^[a-f0-9]{32}$/).test(identity)) {
-      if(getState().songs.downloadedSongs.some(song => song.hash === identity)) {
-        let file = getState().songs.downloadedSongs[getState().songs.downloadedSongs.findIndex(song => song.hash === identity)].file
-        fs.readFile(file, 'UTF-8', (err, data) => {
-          if(err) return
-          let song = JSON.parse(data)
-          let dirs = file.split('\\')
-          dirs.pop()
-          let dir = dirs.join('\\')
-          song.coverUrl = path.join(dir, song.coverImagePath)
-          song.file = path.join(dir, 'info.json' || 'info.dat')
-          console.log(song)
+      fetch(`https://bsaber.com/wp-json/bsaber-api/songs/${key}/ratings`)
+        .then(res => res.json())
+        .then(bsaberData => {
           dispatch({
             type: LOAD_DETAILS,
-            payload: { audioSource: path.join(dir, song.difficultyLevels[0].audioPath) }
+            payload: { ratings: bsaberData }
           })
+        })
+        .catch(() => {
           dispatch({
             type: LOAD_DETAILS,
-            payload: { song }
-          })
-          dispatch({
-            type: SET_DETAILS_LOADING,
-            payload: false
-          })
-        })
-        dispatch({
-          type: SET_VIEW,
-          payload: SONG_DETAILS
-        })
-      } else {
-        fetch(`https://beatsaver.com/api/maps/detail/${identity}`)
-          .then(res =>  res.json())
-          .then(results => {
-            if(results.songs.length === 1) {
-              let song = results.songs[0]
-              fetch(song.downloadURL)
-                .then(res => res.arrayBuffer())
-                .then(data => {
-                  let zip = new AdmZip(new Buffer(data))
-                  let entries = zip.getEntries()
-                  for(let i = 0; i < entries.length; i++) {
-                    console.log(entries[i].name)
-                    if(entries[i].name.split('.')[1] === 'ogg' || entries[i].name.split('.')[1] === 'wav' || entries[i].name.split('.')[1] === 'mp3' || entries[i].name.split('.')[1] === 'egg') {
-                      dispatch({
-                        type: LOAD_DETAILS,
-                        payload: { audioSource: URL.createObjectURL(new Blob([entries[i].getData()])) }
-                      })
-                    }
-                  }
-                })
-              fetch(`https://bsaber.com/wp-json/bsaber-api/songs/${identity}/ratings`)
-                .then(res => res.json())
-                .then(bsaberData => {
-                  dispatch({
-                    type: LOAD_DETAILS,
-                    payload: { ratings: bsaberData }
-                  })
-                })
-                dispatch({
-                  type: LOAD_DETAILS,
-                  payload: { song }
-                })
-                dispatch({
-                  type: SET_DETAILS_LOADING,
-                  payload: false
-                })
+            payload: {
+              ratings: {
+                overall_rating: 0,
+                average_ratings: {
+                  fun_factor: 0,
+                  rythm: 0,
+                  flow: 0,
+                  pattern_quality: 0,
+                  readability: 0,
+                  level_quality: 0
+                }
+              }
             }
           })
-          dispatch({
-            type: SET_VIEW,
-            payload: SONG_DETAILS
-          })
-      }
-    } else {
-      fs.readFile(identity, 'UTF-8', (err, data) => {
-        if(err) return
-        let song = JSON.parse(data)
-        let dirs = identity.split('\\')
-        dirs.pop()
-        let dir = dirs.join('\\')
-        song.coverUrl = path.join(dir, song.coverImagePath)
-        song.file = path.join(dir, 'info.json' || 'info.dat')
-        dispatch({
-          type: LOAD_DETAILS,
-          payload: { audioSource: path.join(dir, song.difficultyLevels[0].audioPath) }
-        })
-        dispatch({
-          type: LOAD_DETAILS,
-          payload: { song }
-        })
-        dispatch({
-          type: SET_DETAILS_LOADING,
-          payload: false
         })
-      })
-      dispatch({
-        type: SET_VIEW,
-        payload: SONG_DETAILS
-      })
-    }
+  } else {
+    dispatch({
+      type: DISPLAY_WARNING,
+      payload: {
+        text: `Error loading details from key: "${key}" is not a valid key. If you are seeing this, please file  a bug report.`
+      }
+    })
   }
 }

+ 1 - 5
src/actions/modActions.js

@@ -448,11 +448,7 @@ export const deactivateMod = modName => (dispatch, getState) => {
     for(let i = 0; i < mod.files.length; i++) {
       try {
         if(fs.lstatSync(path.join(getState().settings.installationDirectory, mod.files[i])).isDirectory()) continue
-        let dir = mod.files[i]
-        let dirs = dir.split('/')
-        dirs.pop()
-        dir = dirs.join('/')
-        zip.addLocalFile(path.join(getState().settings.installationDirectory, mod.files[i]), dir)
+        zip.addLocalFile(path.join(getState().settings.installationDirectory, mod.files[i]), path.dirname(mod.files[i]))
         fs.unlinkSync(path.join(getState().settings.installationDirectory, mod.files[i]))
       } catch {}
     }

+ 14 - 23
src/actions/playlistsActions.js

@@ -190,10 +190,7 @@ export const loadPlaylistDetails = playlistFile => (dispatch, getState) => {
                 return
               }
               let song = JSON.parse(data)
-              let dirs = file.split('\\')
-              dirs.pop()
-              let dir = dirs.join('\\')
-              song.coverUrl = path.join(dir, song.coverImagePath || song._coverImageFilename)
+              song.coverURL = `file://${path.join(path.dirname(file), song.coverImagePath || song._coverImageFilename)}`
               dispatch({
                 type: LOAD_PLAYLIST_SONGS,
                 payload: { ...song, file, order: i }
@@ -202,15 +199,14 @@ export const loadPlaylistDetails = playlistFile => (dispatch, getState) => {
           } else {
             fetch(`https://beatsaver.com/api/maps/by-hash/${playlist.songs[i].hash}`)
             .then(res => res.json())
-            .then(results => {
-              if(results.songs.length === 1) {
-                dispatch({
-                  type: LOAD_PLAYLIST_SONGS,
-                  payload: { ...results.songs[0], order: i }
-                })
-              }
+            .then(song => {
+              song.coverURL = `https://beatsaver.com/${song.coverURL}`
+              dispatch({
+                type: LOAD_PLAYLIST_SONGS,
+                payload: { ...song, order: i }
+              })
             })
-            .catch(_ => {
+            .catch(err => {
               dispatch({
                 type: LOAD_PLAYLIST_SONGS,
                 payload: { ...playlist.songs[i], order: i }
@@ -221,8 +217,9 @@ export const loadPlaylistDetails = playlistFile => (dispatch, getState) => {
           fetch(`https://beatsaver.com/api/maps/detail/${playlist.songs[i].key}`)
             .then(res => res.json())
             .then(details => {
-              if(state.songs.downloadedSongs.some(song => song.hash === details.song.hash)) {
-                let file = state.songs.downloadedSongs[state.songs.downloadedSongs.findIndex(song => song.hash === details.song.hashMd5)].file
+              details.coverURL = `https://beatsaver.com/${details.coverURL}`
+              if(state.songs.downloadedSongs.some(song => song.hash === details.hash)) {
+                let file = state.songs.downloadedSongs[state.songs.downloadedSongs.findIndex(song => song.hash === details.hash)].file
                 fs.readFile(file, 'UTF8', (err, data) => {
                   if(err) {
                     dispatch({
@@ -232,10 +229,7 @@ export const loadPlaylistDetails = playlistFile => (dispatch, getState) => {
                     return
                   }
                   let song = JSON.parse(data)
-                  let dirs = file.split('\\')
-                  dirs.pop()
-                  let dir = dirs.join('\\')
-                  song.coverUrl = path.join(dir, song.coverImagePath)
+                  song.coverURL = `file://${path.join(path.dirname(file), song.coverImagePath || song._coverImageFilename)}`
                   dispatch({
                     type: LOAD_PLAYLIST_SONGS,
                     payload: { ...song, file, order: i }
@@ -244,7 +238,7 @@ export const loadPlaylistDetails = playlistFile => (dispatch, getState) => {
               } else {
                 dispatch({
                   type: LOAD_PLAYLIST_SONGS,
-                  payload: { ...details.song, order: i }
+                  payload: { ...details, order: i }
                 })
               }
             })
@@ -336,13 +330,10 @@ export const addSongToPlaylist = (song, playlistFile) => (dispatch, getState) =>
       if(song.file) {
         let file = song.file
         delete song.file
-        let dirs = file.split('\\')
-        dirs.pop()
-        let dir = dirs.join('\\')
         let to_hash = ''
         for(let i = 0; i < song.difficultyLevels.length; i++) {
           try {
-            to_hash += fs.readFileSync(path.join(dir, song.difficultyLevels[i].jsonPath), 'UTF8')
+            to_hash += fs.readFileSync(path.join(path.dirname(file), song.difficultyLevels[i].jsonPath), 'UTF8')
           } catch(err) {
             dispatch({
               type: DISPLAY_WARNING,

+ 1 - 1
src/actions/queueActions.js

@@ -73,7 +73,7 @@ export const downloadSong = (identity) => (dispatch, getState) => {
                         type: SET_WAIT_LIST,
                         payload: state.songs.waitingToDownload
                       })
-                      downloadSong(toDownload)(dispatch)
+                      downloadSong(toDownload)(dispatch, getState)
                     }
                     dispatch({
                       type: DISPLAY_WARNING,

+ 1 - 4
src/actions/searchActions.js

@@ -63,9 +63,6 @@ export const submitSearch = keywords => dispatch => {
     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'))
       .on('file', file => {
-        let dirs = file.split('\\')
-        dirs.pop()
-        let dir = dirs.join('\\')
         if (file.substr(file.length - 9) === 'info.json') {
           localSongCount++
           fs.readFile(file, 'UTF-8', (err, data) => {
@@ -77,7 +74,7 @@ export const submitSearch = keywords => dispatch => {
               decrementCounter()
               return
             }
-            song.coverUrl = path.join(dir, song.coverImagePath)
+            song.coverUrl = path.join(path.dirname(file), song.coverImagePath)
             song.file = file
             localSongs.push(song)
             decrementCounter()

+ 2 - 4
src/actions/songListActions.js

@@ -220,9 +220,7 @@ export const fetchLocalSongs = () => (dispatch, getState) => {
     })
     Walker(path.join(getState().settings.installationDirectory, 'CustomSongs'))
       .on('file', (file) => {
-        let dirs = file.split('\\')
-        dirs.pop()
-        let dir = dirs.join('\\')
+        let dir = path.dirname(file)
         if(file.substr(file.length - 9) === 'info.json') {
           if(!isModInstalled('SongCore')(dispatch, getState)) installEssentialMods()(dispatch, getState)
           count++
@@ -235,7 +233,7 @@ export const fetchLocalSongs = () => (dispatch, getState) => {
               decrementCounter()
               return
             }
-            song.coverUrl = path.join(dir, song.coverImagePath)
+            song.coverUrl = `file://${ path.join(dir, (song.coverImagePath || song._coverImageFilename)) }`
             song.file = path.join(dir, 'info.json')
             songs.push(song)
             decrementCounter()

+ 4 - 5
src/components/App.js

@@ -10,20 +10,18 @@ import DownloadQueue from './DownloadQueue';
 import UpdateDialog from './UpdateDialog';
 import SongScanningDialog from './SongScanningDialog';
 import ReleaseNotesModal  from './ReleaseNotesModal'
+import CrashMessage from './CrashMessage'
 
 import { connect } from 'react-redux'
 
-
 import { setHasError } from '../actions/windowActions'
 import { downloadSong } from '../actions/queueActions'
 import { loadModDetails, installMod } from '../actions/modActions'
-import { loadDetails } from '../actions/detailsActions'
+import { loadDetailsFromKey } from '../actions/detailsActions'
 import { setView } from '../actions/viewActions'
 
 import { SONG_DETAILS, SONG_LIST, MOD_DETAILS, MODS_VIEW } from '../views'
 
-import CrashMessage from './CrashMessage';
-
 const { ipcRenderer } = window.require('electron')
 
 class App extends Component {
@@ -39,7 +37,8 @@ class App extends Component {
             } else {
               setView(SONG_LIST)(store.dispatch, store.getState)
             }
-            loadDetails(message.songs.details[i])(store.dispatch, store.getState)
+            loadDetailsFromKey()(store.dispatch, store.getState)
+            
           }
           for(let i = 0; i < message.mods.details.length; i++) {
             if(store.getState().view.view === MOD_DETAILS && store.getState().view.previousView !== MOD_DETAILS) {

+ 5 - 4
src/components/CoverGridItem.js

@@ -7,7 +7,7 @@ import Loader from '../assets/loading-dots2.png'
 
 import { connect } from 'react-redux'
 import PropTypes from 'prop-types'
-import { loadDetails } from '../actions/detailsActions'
+import { loadDetailsFromFile, loadDetailsFromKey } from '../actions/detailsActions'
 import { setScrollTop } from '../actions/songListActions'
 
 const getColors = window.require('get-image-colors')
@@ -97,7 +97,7 @@ componentWillReceiveProps(props) {
       )
     } else {
       return (
-        <div key={ this.props.key } className='cover-grid-item' onClick={ () => { this.props.setScrollTop(document.getElementById('cover-grid-container').scrollTop); this.props.loadDetails(this.props.file || this.props.songKey) } }>
+        <div key={ this.props.key } className='cover-grid-item' onClick={ () => { this.props.setScrollTop(document.getElementById('cover-grid-container').scrollTop); if(this.props.file) { this.props.loadDetailsFromFile(this.props.file) } else { this.props.loadDetailsFromKey(this.props.songKey) } } }>
           <img className="cover-image" src={ this.props.coverImage } alt=""/>
           {(!!this.props.file || this.props.downloadedSongs.some(dsong => dsong.hash === this.props.hash)) ? <LibraryIndicator /> : null}
           <div style={ { backgroundColor: this.state.bgc, color: this.state.textColor } } className="cover-grid-info-tab">
@@ -114,11 +114,12 @@ componentWillReceiveProps(props) {
 }
 
 CoverGridItem.propTypes = ({
-  loadDetails: PropTypes.func.isRequired
+  loadDetailsFromFile: PropTypes.func.isRequired,
+  loadDetailsFromKey: PropTypes.func.isRequired
 })
 
 let mapStateToProps = state => ({
   downloadedSongs: state.songs.downloadedSongs
 })
 
-export default connect(mapStateToProps, { loadDetails, setScrollTop })(CoverGridItem)
+export default connect(mapStateToProps, { loadDetailsFromFile, loadDetailsFromKey, setScrollTop })(CoverGridItem)

File diff suppressed because it is too large
+ 2 - 3
src/components/PlaylistDetails.js


+ 5 - 8
src/components/ReleaseNotesModal.js

@@ -25,18 +25,15 @@ class ReleaseNotesModal extends Component {
             <h2 style={ { color: 'lightgreen' } }>What's new?</h2>
             <hr style={ { borderColor: 'lightgreen' } } />
             <ul>
-              <li>Added <b>custom theme image</b> setting. You can now download songs while watching anime or looking at cute cats!</li>
-              <li>Added <b>game version support.</b> This means you can set your game version and get only the correct mods for the version. (May require mod reinstallation to update.)</li>
-              <li>We have a new Wave tier Patron! Welcome to the credits <b>Marc Smith!</b></li>
-              <li>2.3.2: Add <b>game version tag</b> to all mods. Now you can see for what version of the game a mod was made for.</li>
+              <li>All fixes for now!</li>
             </ul>
             <h2 style={ { color: 'salmon' } }>What's fixed?</h2>
             <hr style={ { borderColor: 'salmon' } } />
             <ul>
-              <li><b>Song scanning</b> has been completely rewritten to be <b>much more stable!</b></li>
-              <li>The <b>song scanning modal</b> now <b>provides more information</b> and <b>persists until exited.</b></li>
-              <li>2.3.1: Fixed bug where <b>app would crash on startup.</b> (Thanks <b>Rocker</b>!)</li>
-              <li>2.3.3: Fixed bug where <b>playlists would not save properly.</b></li>
+              <li>BeatDrop is now <b>compatible with the new BeatSaver API.</b></li>
+              <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>
             </ul>
             <br />
             <Button type="primary" onClick={ () => { this.props.setLatestReleaseNotes(require('../../package.json').version) } }>Awesome!</Button>

+ 6 - 5
src/components/SearchView.js

@@ -2,7 +2,7 @@ import React, { Component } from "react"
 import '../css/SearchView.scss'
 
 import { submitSearch } from '../actions/searchActions'
-import { loadDetails } from '../actions/detailsActions'
+import { loadDetailsFromFile, loadDetailsFromKey } from '../actions/detailsActions'
 import { connect } from 'react-redux'
 import SearchLoading from '../assets/search-loading.svg'
 import PropTypes from 'prop-types'
@@ -20,7 +20,7 @@ class SearchView extends Component {
         <div>{this.props.results.library.map((song, i) => {
             console.log(song)
           return (
-            <div className="search-result" onClick={ () => { this.props.loadDetails(song.file) } } key={ i }>
+            <div className="search-result" onClick={ () => { this.props.loadDetailsFromFile(song.file) } } key={ i }>
               <img src={ song.coverUrl } alt=""/>
               <div className="search-result-info">
                 <b>{song.songName}</b><br/>
@@ -33,7 +33,7 @@ class SearchView extends Component {
         {/*<h2 style={ { display: 'inline-block', marginRight: '5px' } }>BeatSaver</h2><span>{this.props.results.beatSaver.length} result{(this.props.results.beatSaver.length !== 1 ? 's' : '')}</span>*/}
         <div>{this.props.results.beatSaver.map((song, i) => {
           return (
-            <div className="search-result" onClick={ () => { this.props.loadDetails(song.key) } } key={ i }>
+            <div className="search-result" onClick={ () => { this.props.loadDetailsFromKey(song.key) } } key={ i }>
               <img src={ song.coverUrl } alt="" />
               <div className="search-result-info">
                 <b>{song.songName}</b><br/>
@@ -51,7 +51,8 @@ SearchView.propTypes = {
   results: PropTypes.object.isRequired,
   loading: PropTypes.bool.isRequired,
   submitSearch: PropTypes.func.isRequired,
-  loadDetails: PropTypes.func.isRequired
+  loadDetailsFromFile: PropTypes.func.isRequired,
+  loadDetailsFromKey: PropTypes.func.isRequired
 }
 
 let mapStateToProps = state => ({
@@ -59,6 +60,6 @@ let mapStateToProps = state => ({
   loading: state.loading
 })
 
-export default connect(mapStateToProps, { submitSearch, loadDetails })(SearchView)
+export default connect(mapStateToProps, { submitSearch, loadDetailsFromFile, loadDetailsFromKey })(SearchView)
 
 //<div id="search-sources"><input type="checkbox" name="library-source-checkbox" id="library-source-checkbox" onChange={() => {}} defaultChecked/><label htmlFor="library-source-checkbox">Library</label><input type="checkbox" name="beatsaver-source-checkbox" id="beatsaver-source-checkbox" defaultChecked/><label htmlFor="beatsaver-source-checkbox">BeatSaver</label></div>

+ 3 - 3
src/components/SongDetails.js

@@ -83,7 +83,7 @@ function Uploader(props) {
 }
 
 function BeatSaver(props) {
-  if(props.details.stats.downloads === undefined) return null
+  if(props.details.stats === undefined) return null
   return (
     <div className="details-ratings">
       <b>BeatSaver Details:</b>
@@ -152,7 +152,7 @@ class SongDetails extends Component {
       return (
         <div id="song-details">
           <div className="close-icon" title="Close" onClick={ () => {this.props.setView(this.props.previousView)} }></div>
-          <img className="cover-image" src={ `https://beatsaver.com${this.props.details.coverURL}` } alt='' />
+          <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>
@@ -173,7 +173,7 @@ class SongDetails extends Component {
             </div>
             <Description details={ this.props.details } />
             <Uploader details={ this.props.details } />
-            <Difficulties difficulties={ this.props.details.metadata.difficulties || this.props.details.difficultyLevels } />
+            <Difficulties difficulties={ this.props.details.difficultyLevels || 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 } />

+ 6 - 5
src/components/SongListItem.js

@@ -9,7 +9,7 @@ import LibraryIndicator from './LibraryIndicator'
 
 import { connect } from 'react-redux'
 import PropTypes from 'prop-types'
-import { loadDetails } from '../actions/detailsActions'
+import { loadDetailsFromFile, loadDetailsFromKey } from '../actions/detailsActions'
 import { setScrollTop } from '../actions/songListActions'
 
 import { COMPACT_LIST } from '../views'
@@ -91,8 +91,8 @@ class SongListItem extends Component {
       )
     } else {
       return (
-        <li className={ `song-list-item${this.props.view.subView === 'compact-list' ? ' compact' : ''}` } onClick={ () => { this.props.setScrollTop(document.getElementById('song-list').scrollTop); this.props.loadDetails(this.props.file || this.props.songKey) } }>
-          <img className="cover-image" src={ this.props.imageSource.startsWith('C:')? this.props.imageSource : `https://beatsaver.com${this.props.imageSource}` } alt={ this.props.songKey } />
+        <li className={ `song-list-item${this.props.view.subView === 'compact-list' ? ' compact' : ''}` } onClick={ () => { this.props.setScrollTop(document.getElementById('song-list').scrollTop); if(this.props.file) { this.props.loadDetailsFromFile(this.props.file) } else { this.props.loadDetailsFromKey(this.props.songKey) } } }>
+          <img className="cover-image" src={ this.props.imageSource.startsWith('file://') ? this.props.imageSource : `https://beatsaver.com/${ this.props.imageSource }` } alt={ this.props.songKey } />
           {(!!this.props.file || this.props.downloadedSongs.some(dsong => dsong.hash === this.props.hash)) && this.props.view.songView !== COMPACT_LIST ? <LibraryIndicator /> : null}
           <div className="song-details">
             <div className="song-title">{this.props.title}<span className="id">{!!this.props.songKey ? this.props.songKey : ''}</span></div>
@@ -109,7 +109,8 @@ class SongListItem extends Component {
 }
 
 SongListItem.propTypes = ({
-  loadDetails: PropTypes.func.isRequired,
+  loadDetailsFromFile: PropTypes.func.isRequired,
+  loadDetailsFromKey: PropTypes.func.isRequired,
   details: PropTypes.object.isRequired
 })
 
@@ -119,4 +120,4 @@ const mapStateToProps = state => ({
   downloadedSongs: state.songs.downloadedSongs
 })
 
-export default connect(mapStateToProps, { loadDetails, setScrollTop })(SongListItem)
+export default connect(mapStateToProps, { loadDetailsFromFile, loadDetailsFromKey, setScrollTop })(SongListItem)

+ 8 - 0
src/utilities.js

@@ -2,4 +2,12 @@ export const makeRenderKey = (tags) => {
   let key = ''
   for(let i = 0; i < tags.length; i++) if(tags[i].boolean) key += tags[i].tag
   return key
+}
+
+export const isKey = key => {
+  return (/^[a-f0-9]$/).test(key)
+}
+
+export const isHash = hash => {
+  return (/^[a-f0-9]{32}$/).test(hash)
 }