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
+
@@ -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 @@
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
{{ library.name }}
+
+
+
+
+
+
+
+
+
+
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