diff --git a/android/app/build.gradle b/android/app/build.gradle index 397c6466..502c64c8 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.audiobookshelf.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 19 - versionName "0.9.3-beta" + versionCode 20 + versionName "0.9.4-beta" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/components/app/Appbar.vue b/components/app/Appbar.vue index dcfcebe9..518aa2a9 100644 --- a/components/app/Appbar.vue +++ b/components/app/Appbar.vue @@ -8,7 +8,13 @@ arrow_back
-

AudioBookshelf

+
+ + + +

{{ currentLibraryName }}

+
+
@@ -47,6 +53,12 @@ export default { } }, computed: { + currentLibrary() { + return this.$store.getters['libraries/getCurrentLibrary'] + }, + currentLibraryName() { + return this.currentLibrary ? this.currentLibrary.name : 'Main' + }, showBack() { return this.$route.name !== 'index' }, @@ -65,6 +77,9 @@ export default { } }, methods: { + clickShowLibraryModal() { + this.$store.commit('libraries/setShowModal', true) + }, back() { if (this.$route.name === 'audiobook-id-edit') { this.$router.push(`/audiobook/${this.$route.params.id}`) diff --git a/components/app/Bookshelf.vue b/components/app/Bookshelf.vue index 01ff735d..b7f82431 100644 --- a/components/app/Bookshelf.vue +++ b/components/app/Bookshelf.vue @@ -12,6 +12,9 @@
No Audiobooks
Clear Filter
+
+
Loading...
+
@@ -25,6 +28,9 @@ export default { } }, computed: { + isLoading() { + return this.$store.state.audiobooks.isLoading + }, cardWidth() { return 140 }, @@ -81,10 +87,17 @@ export default { this.calcShelves() } }, + async loadAudiobooks() { + var currentLibrary = await this.$localStore.getCurrentLibrary() + if (currentLibrary) { + this.$store.commit('libraries/setCurrentLibrary', currentLibrary.id) + } + this.$store.dispatch('audiobooks/load') + }, socketConnected(isConnected) { if (isConnected) { console.log('Connected - Load from server') - this.$store.dispatch('audiobooks/load') + this.loadAudiobooks() } else { console.log('Disconnected - Reset to local storage') this.$store.commit('audiobooks/reset') @@ -106,7 +119,7 @@ export default { this.$server.on('connected', this.socketConnected) if (this.$server.connected) { - this.$store.dispatch('audiobooks/load') + this.loadAudiobooks() } else { console.log('Bookshelf - Server not connected using downloaded') } diff --git a/components/app/StreamContainer.vue b/components/app/StreamContainer.vue index 05febdd2..54a28708 100644 --- a/components/app/StreamContainer.vue +++ b/components/app/StreamContainer.vue @@ -112,14 +112,16 @@ export default { return `${this.$store.state.serverUrl}/Logo.png` } if (this.cover.startsWith('http')) return this.cover - var _clean = this.cover.replace(/\\/g, '/') - if (_clean.startsWith('/local')) { - var _cover = process.env.NODE_ENV !== 'production' && process.env.PROD !== '1' ? _clean.replace('/local', '') : _clean - return `${this.$store.state.serverUrl}${_cover}?token=${this.userToken}&ts=${Date.now()}` - } else if (_clean.startsWith('/metadata')) { - return `${this.$store.state.serverUrl}${_clean}?token=${this.userToken}&ts=${Date.now()}` - } - return _clean + var coverSrc = this.$store.getters['audiobooks/getBookCoverSrc'](this.audiobook) + return coverSrc + // var _clean = this.cover.replace(/\\/g, '/') + // if (_clean.startsWith('/local')) { + // var _cover = process.env.NODE_ENV !== 'production' && process.env.PROD !== '1' ? _clean.replace('/local', '') : _clean + // return `${this.$store.state.serverUrl}${_cover}?token=${this.userToken}&ts=${Date.now()}` + // } else if (_clean.startsWith('/metadata')) { + // return `${this.$store.state.serverUrl}${_clean}?token=${this.userToken}&ts=${Date.now()}` + // } + // return _clean } }, methods: { diff --git a/components/cards/BookCover.vue b/components/cards/BookCover.vue index 0ddbc014..5c3879b9 100644 --- a/components/cards/BookCover.vue +++ b/components/cards/BookCover.vue @@ -97,16 +97,16 @@ export default { fullCoverUrl() { if (this.downloadCover) return this.downloadCover else if (!this.networkConnected) return this.placeholderUrl - - if (this.cover.startsWith('http')) return this.cover - var _clean = this.cover.replace(/\\/g, '/') - if (_clean.startsWith('/local')) { - var _cover = process.env.NODE_ENV !== 'production' && process.env.PROD !== '1' ? _clean.replace('/local', '') : _clean - return `${this.$store.state.serverUrl}${_cover}?token=${this.userToken}&ts=${Date.now()}` - } else if (_clean.startsWith('/metadata')) { - return `${this.$store.state.serverUrl}${_clean}?token=${this.userToken}&ts=${Date.now()}` - } - return _clean + return this.$store.getters['audiobooks/getBookCoverSrc'](this.audiobook) + // if (this.cover.startsWith('http')) return this.cover + // var _clean = this.cover.replace(/\\/g, '/') + // if (_clean.startsWith('/local')) { + // var _cover = process.env.NODE_ENV !== 'production' && process.env.PROD !== '1' ? _clean.replace('/local', '') : _clean + // return `${this.$store.state.serverUrl}${_cover}?token=${this.userToken}&ts=${Date.now()}` + // } else if (_clean.startsWith('/metadata')) { + // return `${this.$store.state.serverUrl}${_clean}?token=${this.userToken}&ts=${Date.now()}` + // } + // return _clean }, cover() { return this.book.cover || this.placeholderUrl diff --git a/components/modals/LibrariesModal.vue b/components/modals/LibrariesModal.vue new file mode 100644 index 00000000..1d8c3206 --- /dev/null +++ b/components/modals/LibrariesModal.vue @@ -0,0 +1,65 @@ + + + diff --git a/layouts/default.vue b/layouts/default.vue index 0d184585..7076952c 100644 --- a/layouts/default.vue +++ b/layouts/default.vue @@ -6,6 +6,7 @@ + @@ -29,13 +30,16 @@ export default { } }, methods: { - connected(isConnected) { + async connected(isConnected) { if (this.$route.name === 'connect') { if (isConnected) { this.$router.push('/') } } this.syncUserProgress() + + // Load libraries + this.$store.dispatch('libraries/load') }, updateAudiobookProgressOnServer(audiobookProgress) { if (this.$server.socket) { @@ -363,8 +367,6 @@ export default { mounted() { if (!this.$server) return console.error('No Server') - console.log('Default Mounted') - this.$server.on('connected', this.connected) this.$server.on('initialStream', this.initialStream) diff --git a/package.json b/package.json index 9b931c19..7b2c2337 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-app", - "version": "v0.9.3-beta", + "version": "v0.9.4-beta", "author": "advplyr", "scripts": { "dev": "nuxt --hostname localhost --port 1337", diff --git a/pages/audiobook/_id/index.vue b/pages/audiobook/_id/index.vue index bdbf4048..9b9a85bb 100644 --- a/pages/audiobook/_id/index.vue +++ b/pages/audiobook/_id/index.vue @@ -272,7 +272,9 @@ export default { console.log('Single track, start download no prep needed') var track = audiobook.tracks[0] var fileext = track.ext - var url = `${this.$store.state.serverUrl}/local/${track.path}` + + var relTrackPath = track.path.replace('\\', '/').replace(this.audiobook.path.replace('\\', '/'), '') + var url = `${this.$store.state.serverUrl}/s/book/${this.audiobookId}/${relTrackPath}` this.startDownload(url, fileext, downloadObject) } else { // Multi-track merge @@ -291,14 +293,16 @@ export default { var cover = this.book.cover if (cover.startsWith('http')) return cover - var _clean = cover.replace(/\\/g, '/') - if (_clean.startsWith('/local')) { - var _cover = process.env.NODE_ENV !== 'production' && process.env.PROD !== '1' ? _clean.replace('/local', '') : _clean - return `${this.$store.state.serverUrl}${_cover}?token=${this.userToken}&ts=${Date.now()}` - } else if (_clean.startsWith('/metadata')) { - return `${this.$store.state.serverUrl}${_clean}?token=${this.userToken}&ts=${Date.now()}` - } - return _clean + var coverSrc = this.$store.getters['audiobooks/getBookCoverSrc'](this.audiobook) + return coverSrc + // var _clean = cover.replace(/\\/g, '/') + // if (_clean.startsWith('/local')) { + // var _cover = process.env.NODE_ENV !== 'production' && process.env.PROD !== '1' ? _clean.replace('/local', '') : _clean + // return `${this.$store.state.serverUrl}${_cover}?token=${this.userToken}&ts=${Date.now()}` + // } else if (_clean.startsWith('/metadata')) { + // return `${this.$store.state.serverUrl}${_clean}?token=${this.userToken}&ts=${Date.now()}` + // } + // return _clean }, async startDownload(url, fileext, download) { this.$toast.update(download.toastId, { content: `Downloading "${download.audiobook.book.title}"...` }) @@ -306,7 +310,9 @@ export default { var coverDownloadUrl = this.getCoverUrlForDownload() var coverFilename = null if (coverDownloadUrl) { - var coverExt = Path.extname(coverDownloadUrl) || '.jpg' + var coverNoQueryString = coverDownloadUrl.split('?')[0] + + var coverExt = Path.extname(coverNoQueryString) || '.jpg' coverFilename = `cover-${download.id}${coverExt}` } @@ -337,7 +343,7 @@ export default { var download = this.$store.getters['downloads/getDownload'](prepareDownload.audiobookId) if (download) { var fileext = prepareDownload.ext - var url = `${this.$store.state.serverUrl}/downloads/${prepareDownload.id}/${prepareDownload.filename}` + var url = `${this.$store.state.serverUrl}/downloads/${prepareDownload.id}/${prepareDownload.filename}?token=${this.userToken}` this.startDownload(url, fileext, download) } else { console.error('Prepare download killed but download not found', prepareDownload) diff --git a/plugins/store.js b/plugins/store.js index 36839d13..c9988695 100644 --- a/plugins/store.js +++ b/plugins/store.js @@ -503,6 +503,29 @@ class LocalStorage { } } + async setCurrentLibrary(library) { + try { + if (library) { + await Storage.set({ key: 'library', value: JSON.stringify(library) }) + } else { + await Storage.remove({ key: 'library' }) + } + } catch (error) { + console.error('[LocalStorage] Failed to set library', error) + } + } + + async getCurrentLibrary() { + try { + var _value = (await Storage.get({ key: 'library' }) || {}).value || null + if (!_value) return null + return JSON.parse(_value) + } catch (error) { + console.error('[LocalStorage] Failed to get current library', error) + return null + } + } + async setDownloadFolder(folderObj) { try { if (folderObj) { diff --git a/store/audiobooks.js b/store/audiobooks.js index 751ee4f0..dcace2ac 100644 --- a/store/audiobooks.js +++ b/store/audiobooks.js @@ -9,7 +9,10 @@ export const state = () => ({ listeners: [], genres: [...STANDARD_GENRES], tags: [], - series: [] + series: [], + loadedLibraryId: 'main', + lastLoad: 0, + isLoading: false }) export const getters = { @@ -63,21 +66,73 @@ export const getters = { var _genres = [] state.audiobooks.filter(ab => !!(ab.book && ab.book.genres)).forEach(ab => _genres = _genres.concat(ab.book.genres)) return [...new Set(_genres)].sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : 1) + }, + getBookCoverSrc: (state, getters, rootState, rootGetters) => (bookItem, placeholder = '/book_placeholder.jpg') => { + var book = bookItem.book + if (!book || !book.cover || book.cover === placeholder) return placeholder + var cover = book.cover + + // Absolute URL covers (should no longer be used) + if (cover.startsWith('http:') || cover.startsWith('https:')) return cover + + // Server hosted covers + try { + // Ensure cover is refreshed if cached + var bookLastUpdate = book.lastUpdate || Date.now() + var userToken = rootGetters['user/getToken'] + + // Map old covers to new format /s/book/{bookid}/* + if (cover.substr(1).startsWith('local')) { + cover = cover.replace('local', `s/book/${bookItem.id}`) + if (cover.includes(bookItem.path)) { // Remove book path + cover = cover.replace(bookItem.path, '').replace('//', '/').replace('\\\\', '/') + } + } + + var url = new URL(cover, rootState.serverUrl) + return url + `?token=${userToken}&ts=${bookLastUpdate}` + } catch (err) { + console.error(err) + return placeholder + } } } export const actions = { - load({ commit, dispatch }) { - return this.$axios - .$get(`/api/audiobooks`) + load({ state, commit, rootState }) { + if (!rootState.user || !rootState.user.user) { + console.error('audiobooks/load - User not set') + return false + } + + var currentLibraryId = rootState.libraries.currentLibraryId + + if (currentLibraryId === state.loadedLibraryId) { + // Don't load again if already loaded in the last 5 minutes + var lastLoadDiff = Date.now() - state.lastLoad + if (lastLoadDiff < 5 * 60 * 1000) { + // Already up to date + return false + } + } else { + commit('reset') + commit('setLoading', true) + } + commit('setLoadedLibrary', currentLibraryId) + + this.$axios + .$get(`/api/library/${currentLibraryId}/audiobooks`) .then((data) => { - console.log('Audiobooks request data', data) commit('set', data) - dispatch('setNativeAudiobooks') + commit('setLastLoad') + commit('setLoading', false) }) .catch((error) => { console.error('Failed', error) + commit('set', []) + commit('setLoading', false) }) + return true }, useDownloaded({ commit, rootGetters }) { commit('set', rootGetters['downloads/getAudiobooks']) @@ -100,6 +155,15 @@ export const actions = { } export const mutations = { + setLoadedLibrary(state, val) { + state.loadedLibraryId = val + }, + setLoading(state, val) { + state.isLoading = val + }, + setLastLoad(state, val) { + state.lastLoad = val + }, reset(state) { state.audiobooks = [] state.genres = [...STANDARD_GENRES] diff --git a/store/libraries.js b/store/libraries.js new file mode 100644 index 00000000..41c89517 --- /dev/null +++ b/store/libraries.js @@ -0,0 +1,121 @@ +export const state = () => ({ + libraries: [], + lastLoad: 0, + listeners: [], + currentLibraryId: 'main', + showModal: false, + folders: [], + folderLastUpdate: 0 +}) + +export const getters = { + getCurrentLibrary: state => { + return state.libraries.find(lib => lib.id === state.currentLibraryId) + } +} + +export const actions = { + fetch({ state, commit, rootState }, libraryId) { + if (!rootState.user || !rootState.user.user) { + console.error('libraries/fetch - User not set') + return false + } + + var library = state.libraries.find(lib => lib.id === libraryId) + if (library) { + commit('setCurrentLibrary', libraryId) + return library + } + + return this.$axios + .$get(`/api/library/${libraryId}`) + .then((data) => { + commit('addUpdate', data) + commit('setCurrentLibrary', libraryId) + return data + }) + .catch((error) => { + console.error('Failed', error) + return false + }) + }, + // Return true if calling load + load({ state, commit, rootState }) { + if (!rootState.user || !rootState.user.user) { + console.error('libraries/load - User not set') + return false + } + + // Don't load again if already loaded in the last 5 minutes + var lastLoadDiff = Date.now() - state.lastLoad + if (lastLoadDiff < 5 * 60 * 1000) { + // Already up to date + return false + } + + this.$axios + .$get(`/api/libraries`) + .then((data) => { + commit('set', data) + commit('setLastLoad') + }) + .catch((error) => { + console.error('Failed', error) + commit('set', []) + }) + return true + }, + +} + +export const mutations = { + setFolders(state, folders) { + state.folders = folders + }, + setFoldersLastUpdate(state) { + state.folderLastUpdate = Date.now() + }, + setShowModal(state, val) { + state.showModal = val + }, + setLastLoad(state) { + state.lastLoad = Date.now() + }, + setCurrentLibrary(state, val) { + state.currentLibraryId = val + }, + set(state, libraries) { + console.log('set libraries', libraries) + state.libraries = libraries + state.listeners.forEach((listener) => { + listener.meth() + }) + }, + addUpdate(state, library) { + var index = state.libraries.findIndex(a => a.id === library.id) + if (index >= 0) { + state.libraries.splice(index, 1, library) + } else { + state.libraries.push(library) + } + + state.listeners.forEach((listener) => { + listener.meth() + }) + }, + remove(state, library) { + state.libraries = state.libraries.filter(a => a.id !== library.id) + + state.listeners.forEach((listener) => { + listener.meth() + }) + }, + addListener(state, listener) { + var index = state.listeners.findIndex(l => l.id === listener.id) + if (index >= 0) state.listeners.splice(index, 1, listener) + else state.listeners.push(listener) + }, + removeListener(state, listenerId) { + state.listeners = state.listeners.filter(l => l.id !== listenerId) + } +} \ No newline at end of file