diff --git a/client/assets/fonts.css b/client/assets/fonts.css
index 874570ac..43de8964 100644
--- a/client/assets/fonts.css
+++ b/client/assets/fonts.css
@@ -1,9 +1,15 @@
-/* fallback */
+
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
- src: url(/fonts/material-icons.woff2) format('woff2');
+ src: url(/fonts/MaterialIcons.woff2) format('woff2');
+}
+@font-face {
+ font-family: 'Material Icons Outlined';
+ font-style: normal;
+ font-weight: 400;
+ src: url(/fonts/MaterialIconsOutlined.woff2) format('woff2');
}
.material-icons {
@@ -23,6 +29,23 @@
.material-icons:not(.text-xs):not(.text-sm):not(.text-md):not(.text-base):not(.text-lg):not(.text-xl):not(.text-2xl):not(.text-3xl):not(.text-4xl):not(.text-5xl):not(.text-6xl) {
font-size: 1.5rem;
}
+.material-icons-outlined {
+ font-family: 'Material Icons Outlined';
+ font-weight: normal;
+ font-style: normal;
+ line-height: 1;
+ letter-spacing: normal;
+ text-transform: none;
+ display: inline-block;
+ white-space: nowrap;
+ word-wrap: normal;
+ direction: ltr;
+ -webkit-font-feature-settings: 'liga';
+ -webkit-font-smoothing: antialiased;
+}
+.material-icons-outlined:not(.text-xs):not(.text-sm):not(.text-md):not(.text-base):not(.text-lg):not(.text-xl):not(.text-2xl):not(.text-3xl):not(.text-4xl):not(.text-5xl):not(.text-6xl) {
+ font-size: 1.5rem;
+}
@font-face {
diff --git a/client/components/AudioPlayer.vue b/client/components/AudioPlayer.vue
index 78b3978f..22f4c241 100644
--- a/client/components/AudioPlayer.vue
+++ b/client/components/AudioPlayer.vue
@@ -74,7 +74,7 @@
-
+
@@ -114,7 +114,8 @@ export default {
seekLoading: false,
showChaptersModal: false,
currentTime: 0,
- trackOffsetLeft: 16 // Track is 16px from edge
+ trackOffsetLeft: 16, // Track is 16px from edge
+ playStartTime: 0
}
},
computed: {
@@ -152,6 +153,33 @@ export default {
}
},
methods: {
+ audioPlayed() {
+ if (!this.$refs.audio) return
+ console.log('Audio Played', this.$refs.audio.paused, this.$refs.audio.currentTime)
+ this.playStartTime = Date.now()
+ this.isPaused = this.$refs.audio.paused
+ },
+ audioPaused() {
+ if (!this.$refs.audio) return
+ console.log('Audio Paused', this.$refs.audio.paused, this.$refs.audio.currentTime)
+ this.isPaused = this.$refs.audio.paused
+ },
+ audioError(err) {
+ if (!this.$refs.audio) return
+ console.error('Audio Error', this.$refs.audio.paused, this.$refs.audio.currentTime, err)
+ },
+ audioEnded() {
+ if (!this.$refs.audio) return
+ console.log('Audio Ended', this.$refs.audio.paused, this.$refs.audio.currentTime)
+ },
+ audioStalled() {
+ if (!this.$refs.audio) return
+ console.warn('Audio Ended', this.$refs.audio.paused, this.$refs.audio.currentTime)
+ },
+ audioSuspended() {
+ if (!this.$refs.audio) return
+ console.warn('Audio Suspended', this.$refs.audio.paused, this.$refs.audio.currentTime)
+ },
selectChapter(chapter) {
this.seek(chapter.start)
this.showChaptersModal = false
@@ -418,20 +446,6 @@ export default {
this.$refs.playedTrack.style.width = ptWidth + 'px'
this.playedTrackWidth = ptWidth
},
- paused() {
- if (!this.$refs.audio) {
- console.error('No audio on paused()')
- return
- }
- this.isPaused = this.$refs.audio.paused
- },
- playing() {
- if (!this.$refs.audio) {
- console.error('No audio on playing()')
- return
- }
- this.isPaused = this.$refs.audio.paused
- },
audioLoadedData() {
this.totalDuration = this.audioEl.duration
this.$emit('loaded', this.totalDuration)
@@ -477,6 +491,11 @@ export default {
this.hlsInstance.on(Hls.Events.ERROR, (e, data) => {
console.error('[HLS] Error', data.type, data.details, data)
+
+ if (this.$refs.audio) {
+ console.log('Hls error check audio', this.$refs.audio.paused, this.$refs.audio.currentTime, this.$refs.audio.readyState)
+ }
+
if (data.details === Hls.ErrorDetails.BUFFER_STALLED_ERROR) {
console.error('[HLS] BUFFER STALLED ERROR')
}
diff --git a/client/components/app/BookShelf.vue b/client/components/app/BookShelf.vue
index 2f37aa92..19f62cc5 100644
--- a/client/components/app/BookShelf.vue
+++ b/client/components/app/BookShelf.vue
@@ -47,7 +47,7 @@
-
No {{ showGroups ? 'Series' : 'Audiobooks' }}
+
No {{ showGroups ? page : 'Audiobooks' }}
Clear Filter
Back to Library
@@ -197,6 +197,13 @@ export default {
} else if (this.page === 'search') {
var audiobookSearchResults = this.searchResults ? this.searchResults.audiobooks || [] : []
return audiobookSearchResults.map((absr) => absr.audiobook)
+ } else if (this.page === 'collections') {
+ return (this.$store.state.user.collections || []).map((c) => {
+ return {
+ type: 'collection',
+ ...c
+ }
+ })
} else {
var seriesGroups = this.$store.getters['audiobooks/getSeriesGroups']()
if (this.selectedSeries) {
@@ -214,6 +221,7 @@ export default {
this.$store.commit('showEditModal', audiobook)
},
clickGroup(group) {
+ if (this.page === 'collections') return
this.$emit('update:selectedSeries', group.name)
},
clearFilter() {
@@ -292,7 +300,7 @@ export default {
this.setBookshelfEntities()
},
buildSearchParams() {
- if (this.page === 'search' || this.page === 'series') {
+ if (this.page === 'search' || this.page === 'series' || this.page === 'collections') {
return ''
}
diff --git a/client/components/app/BookShelfToolbar.vue b/client/components/app/BookShelfToolbar.vue
index 3ecbf5b7..8f7dabe4 100644
--- a/client/components/app/BookShelfToolbar.vue
+++ b/client/components/app/BookShelfToolbar.vue
@@ -28,7 +28,7 @@
-
+
@@ -85,6 +85,8 @@ export default {
} else if (this.page === 'search') {
var audiobookSearchResults = this.searchResults ? this.searchResults.audiobooks || [] : []
return audiobookSearchResults.length
+ } else if (this.page === 'collections') {
+ return (this.$store.state.user.collections || []).length
} else {
var groups = this.$store.getters['audiobooks/getSeriesGroups']()
if (this.selectedSeries) {
diff --git a/client/components/app/SideRail.vue b/client/components/app/SideRail.vue
index e4fbc07a..ce2c724c 100644
--- a/client/components/app/SideRail.vue
+++ b/client/components/app/SideRail.vue
@@ -6,7 +6,7 @@
-
Home
+
Home
@@ -16,7 +16,7 @@
-
Library
+
Library
@@ -26,11 +26,22 @@
-
Series
+
Series
+
+
+ collections_bookmark
+
+ Collections
+
+
+
+
warning
@@ -41,6 +52,7 @@
{{ numIssues }}
+
diff --git a/client/components/modals/collections/UserCollectionItem.vue b/client/components/modals/collections/UserCollectionItem.vue
new file mode 100644
index 00000000..689d464c
--- /dev/null
+++ b/client/components/modals/collections/UserCollectionItem.vue
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+
{{ collection.name }}
+
+
+ add
+ remove
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client/components/ui/IconBtn.vue b/client/components/ui/IconBtn.vue
index 36817b31..ddd622cd 100644
--- a/client/components/ui/IconBtn.vue
+++ b/client/components/ui/IconBtn.vue
@@ -1,6 +1,6 @@
@@ -12,7 +12,8 @@ export default {
bgColor: {
type: String,
default: 'primary'
- }
+ },
+ outlined: Boolean
},
data() {
return {}
diff --git a/client/layouts/default.vue b/client/layouts/default.vue
index 97430777..66917c96 100644
--- a/client/layouts/default.vue
+++ b/client/layouts/default.vue
@@ -7,6 +7,7 @@
+
@@ -181,6 +182,15 @@ export default {
// console.log('Received user audiobook update', payload)
this.$store.commit('user/updateUserAudiobook', payload)
},
+ collectionAdded(collection) {
+ this.$store.commit('user/addUpdateCollection', collection)
+ },
+ collectionUpdated(collection) {
+ this.$store.commit('user/addUpdateCollection', collection)
+ },
+ collectionRemoved(collection) {
+ this.$store.commit('user/removeCollection', collection)
+ },
downloadToastClick(download) {
if (!download || !download.audiobookId) {
return console.error('Invalid download object', download)
@@ -294,6 +304,11 @@ export default {
this.socket.on('user_stream_update', this.userStreamUpdate)
this.socket.on('current_user_audiobook_update', this.currentUserAudiobookUpdate)
+ // User Collection Listeners
+ this.socket.on('collection_added', this.collectionAdded)
+ this.socket.on('collection_updated', this.collectionUpdated)
+ this.socket.on('collection_removed', this.collectionRemoved)
+
// Scan Listeners
this.socket.on('scan_start', this.scanStart)
this.socket.on('scan_complete', this.scanComplete)
diff --git a/client/middleware/routed.js b/client/middleware/routed.js
index a592aff7..e58291f4 100644
--- a/client/middleware/routed.js
+++ b/client/middleware/routed.js
@@ -11,7 +11,7 @@ export default function (context) {
return
}
- if (route.name.startsWith('config') || route.name === 'upload' || route.name === 'account' || route.name.startsWith('audiobook-id')) {
+ if (route.name.startsWith('config') || route.name === 'upload' || route.name === 'account' || route.name.startsWith('audiobook-id') || route.name.startsWith('collection-id')) {
if (from.name !== route.name && from.name !== 'audiobook-id-edit' && !from.name.startsWith('config') && from.name !== 'upload' && from.name !== 'account') {
var _history = [...store.state.routeHistory]
if (!_history.length || _history[_history.length - 1] !== from.fullPath) {
diff --git a/client/pages/audiobook/_id/index.vue b/client/pages/audiobook/_id/index.vue
index 4729262f..dc4c63a0 100644
--- a/client/pages/audiobook/_id/index.vue
+++ b/client/pages/audiobook/_id/index.vue
@@ -113,6 +113,10 @@
+
+
+
+
Open RSS Feed
@@ -433,6 +437,10 @@ export default {
downloadClick() {
this.$store.commit('showEditModalOnTab', { audiobook: this.audiobook, tab: 'download' })
},
+ collectionsClick() {
+ this.$store.commit('setSelectedAudiobook', this.audiobook)
+ this.$store.commit('globals/setShowUserCollectionsModal', true)
+ },
resize() {
this.windowWidth = window.innerWidth
}
diff --git a/client/pages/collection/_id.vue b/client/pages/collection/_id.vue
new file mode 100644
index 00000000..47fbabb9
--- /dev/null
+++ b/client/pages/collection/_id.vue
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+ {{ collectionName }}
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client/pages/library/_library/bookshelf/_id.vue b/client/pages/library/_library/bookshelf/_id.vue
index 44418d09..f7cfb741 100644
--- a/client/pages/library/_library/bookshelf/_id.vue
+++ b/client/pages/library/_library/bookshelf/_id.vue
@@ -47,6 +47,10 @@ export default {
var libraryPage = params.id || ''
store.commit('audiobooks/setLibraryPage', libraryPage)
+ if (libraryPage === 'collections') {
+ store.dispatch('user/loadUserCollections')
+ }
+
return {
id: libraryPage,
libraryId,
diff --git a/client/static/fonts/material-icons.woff2 b/client/static/fonts/MaterialIcons.woff2
similarity index 100%
rename from client/static/fonts/material-icons.woff2
rename to client/static/fonts/MaterialIcons.woff2
diff --git a/client/static/fonts/MaterialIconsOutlined.woff2 b/client/static/fonts/MaterialIconsOutlined.woff2
new file mode 100644
index 00000000..aa2ef9c1
Binary files /dev/null and b/client/static/fonts/MaterialIconsOutlined.woff2 differ
diff --git a/client/store/globals.js b/client/store/globals.js
new file mode 100644
index 00000000..7f999bbb
--- /dev/null
+++ b/client/store/globals.js
@@ -0,0 +1,18 @@
+
+export const state = () => ({
+ showUserCollectionsModal: false
+})
+
+export const getters = {
+
+}
+
+export const actions = {
+
+}
+
+export const mutations = {
+ setShowUserCollectionsModal(state, val) {
+ state.showUserCollectionsModal = val
+ }
+}
\ No newline at end of file
diff --git a/client/store/index.js b/client/store/index.js
index c8c76888..9810c1fb 100644
--- a/client/store/index.js
+++ b/client/store/index.js
@@ -139,6 +139,9 @@ export const mutations = {
setDeveloperMode(state, val) {
state.developerMode = val
},
+ setSelectedAudiobook(state, val) {
+ Vue.set(state, 'selectedAudiobook', val)
+ },
setSelectedAudiobooks(state, audiobooks) {
Vue.set(state, 'selectedAudiobooks', audiobooks)
},
diff --git a/client/store/user.js b/client/store/user.js
index bc508ee0..5462dc7d 100644
--- a/client/store/user.js
+++ b/client/store/user.js
@@ -10,7 +10,9 @@ export const state = () => ({
playbackRate: 1,
bookshelfCoverSize: 120
},
- settingsListeners: []
+ settingsListeners: [],
+ collections: [],
+ collectionsLoaded: false
})
export const getters = {
@@ -69,6 +71,20 @@ export const actions = {
console.error('Failed to update settings', error)
return false
})
+ },
+ loadUserCollections({ state, commit }) {
+ if (state.collectionsLoaded) {
+ console.log('Collections already loaded')
+ return state.collections
+ }
+
+ return this.$axios.$get('/api/collections').then((collections) => {
+ commit('setCollections', collections)
+ return collections
+ }).catch((error) => {
+ console.error('Failed to get collections', error)
+ return []
+ })
}
}
@@ -112,5 +128,20 @@ export const mutations = {
},
removeSettingsListener(state, listenerId) {
state.settingsListeners = state.settingsListeners.filter(l => l.id !== listenerId)
+ },
+ setCollections(state, collections) {
+ state.collectionsLoaded = true
+ state.collections = collections
+ },
+ addUpdateCollection(state, collection) {
+ var index = state.collections.findIndex(c => c.id === collection.id)
+ if (index >= 0) {
+ state.collections.splice(index, 1, collection)
+ } else {
+ state.collections.push(collection)
+ }
+ },
+ removeCollection(state, collection) {
+ state.collections = state.collections.filter(c => c.id !== collection.id)
}
}
\ No newline at end of file
diff --git a/server/ApiController.js b/server/ApiController.js
index acc42fad..cbe543bb 100644
--- a/server/ApiController.js
+++ b/server/ApiController.js
@@ -10,6 +10,7 @@ const BookFinder = require('./BookFinder')
const Library = require('./objects/Library')
const User = require('./objects/User')
+const UserCollection = require('./objects/UserCollection')
class ApiController {
constructor(MetadataPath, db, scanner, auth, streamManager, rssFeeds, downloadManager, coverController, backupManager, watcher, emitter, clientEmitter) {
@@ -75,6 +76,14 @@ class ApiController {
this.router.patch('/user/:id', this.updateUser.bind(this))
this.router.delete('/user/:id', this.deleteUser.bind(this))
+ this.router.get('/collections', this.getUserCollections.bind(this))
+ this.router.get('/collection/:id', this.getUserCollection.bind(this))
+ this.router.post('/collection', this.createUserCollection.bind(this))
+ this.router.post('/collection/:id/book', this.addBookToUserCollection.bind(this))
+ this.router.delete('/collection/:id/book/:bookId', this.removeBookFromUserCollection.bind(this))
+ this.router.patch('/collection/:id', this.updateUserCollection.bind(this))
+ this.router.delete('/collection/:id', this.deleteUserCollection.bind(this))
+
this.router.patch('/serverSettings', this.updateServerSettings.bind(this))
this.router.delete('/backup/:id', this.deleteBackup.bind(this))
@@ -82,8 +91,6 @@ class ApiController {
this.router.post('/authorize', this.authorize.bind(this))
- this.router.get('/genres', this.getGenres.bind(this))
-
this.router.post('/feed', this.openRssFeed.bind(this))
this.router.get('/download/:id', this.download.bind(this))
@@ -368,6 +375,15 @@ class ApiController {
}
}
+ // remove book from collections
+ var collectionsWithBook = this.db.collections.filter(c => c.books.includes(audiobook.id))
+ for (let i = 0; i < collectionsWithBook.length; i++) {
+ var collection = collectionsWithBook[i]
+ collection.removeBook(audiobook.id)
+ await this.db.updateEntity('collection', collection)
+ this.clientEmitter(collection.userId, 'collection_updated', collection.toJSONExpanded(this.db.audiobooks))
+ }
+
var audiobookJSON = audiobook.toJSONMinified()
await this.db.removeEntity('audiobook', audiobook.id)
this.emitter('audiobook_removed', audiobookJSON)
@@ -761,6 +777,13 @@ class ApiController {
})
}
+ // delete user collections
+ var userCollections = this.db.collections.filter(c => c.userId === user.id)
+ var collectionsToRemove = userCollections.map(uc => uc.id)
+ for (let i = 0; i < collectionsToRemove.length; i++) {
+ await this.db.removeEntity('collection', collectionsToRemove[i])
+ }
+
// Todo: check if user is logged in and cancel streams
var userJson = user.toJSONForBrowser()
@@ -771,6 +794,95 @@ class ApiController {
})
}
+ async getUserCollections(req, res) {
+ var collections = this.db.collections.filter(c => c.userId === req.user.id)
+ var expandedCollections = collections.map(c => c.toJSONExpanded(this.db.audiobooks))
+ res.json(expandedCollections)
+ }
+
+ async getUserCollection(req, res) {
+ var collection = this.db.collections.find(c => c.id === req.params.id)
+ if (!collection) {
+ return res.status(404).send('Collection not found')
+ }
+ res.json(collection.toJSONExpanded(this.db.audiobooks))
+ }
+
+ async createUserCollection(req, res) {
+ var newCollection = new UserCollection()
+ req.body.userId = req.user.id
+ var success = newCollection.setData(req.body)
+ if (!success) {
+ return res.status(500).send('Invalid collection data')
+ }
+ var jsonExpanded = newCollection.toJSONExpanded(this.db.audiobooks)
+ await this.db.insertEntity('collection', newCollection)
+ this.clientEmitter(req.user.id, 'collection_added', jsonExpanded)
+ res.json(jsonExpanded)
+ }
+
+ async addBookToUserCollection(req, res) {
+ var collection = this.db.collections.find(c => c.id === req.params.id)
+ if (!collection) {
+ return res.status(404).send('Collection not found')
+ }
+ var audiobook = this.db.audiobooks.find(ab => ab.id === req.body.id)
+ if (!audiobook) {
+ return res.status(500).send('Book not found')
+ }
+ if (audiobook.libraryId !== collection.libraryId) {
+ return res.status(500).send('Book in different library')
+ }
+ if (collection.books.includes(req.body.id)) {
+ return res.status(500).send('Book already in collection')
+ }
+ collection.addBook(req.body.id)
+ var jsonExpanded = collection.toJSONExpanded(this.db.audiobooks)
+ await this.db.updateEntity('collection', collection)
+ this.clientEmitter(req.user.id, 'collection_updated', jsonExpanded)
+ res.json(jsonExpanded)
+ }
+
+ async removeBookFromUserCollection(req, res) {
+ var collection = this.db.collections.find(c => c.id === req.params.id)
+ if (!collection) {
+ return res.status(404).send('Collection not found')
+ }
+
+ if (collection.books.includes(req.params.bookId)) {
+ collection.removeBook(req.params.bookId)
+ var jsonExpanded = collection.toJSONExpanded(this.db.audiobooks)
+ await this.db.updateEntity('collection', collection)
+ this.clientEmitter(req.user.id, 'collection_updated', jsonExpanded)
+ }
+ res.json(collection.toJSONExpanded(this.db.audiobooks))
+ }
+
+ async updateUserCollection(req, res) {
+ var collection = this.db.collections.find(c => c.id === req.params.id)
+ if (!collection) {
+ return res.status(404).send('Collection not found')
+ }
+ var wasUpdated = collection.update(req.body)
+ var jsonExpanded = collection.toJSONExpanded(this.db.audiobooks)
+ if (wasUpdated) {
+ await this.db.updateEntity('collection', collection)
+ this.clientEmitter(req.user.id, 'collection_updated', jsonExpanded)
+ }
+ res.json(jsonExpanded)
+ }
+
+ async deleteUserCollection(req, res) {
+ var collection = this.db.collections.find(c => c.id === req.params.id)
+ if (!collection) {
+ return res.status(404).send('Collection not found')
+ }
+ var jsonExpanded = collection.toJSONExpanded(this.db.audiobooks)
+ await this.db.removeEntity('collection', collection.id)
+ this.clientEmitter(req.user.id, 'collection_removed', jsonExpanded)
+ res.sendStatus(200)
+ }
+
async updateServerSettings(req, res) {
if (!req.user.isRoot) {
Logger.error('User other than root attempting to update server settings', req.user)
@@ -846,12 +958,6 @@ class ApiController {
})
}
- getGenres(req, res) {
- res.json({
- genres: this.db.getGenres()
- })
- }
-
async getDirectories(dir, relpath, excludedDirs, level = 0) {
try {
var paths = await fs.readdir(dir)
diff --git a/server/BackupManager.js b/server/BackupManager.js
index b3747957..0d032408 100644
--- a/server/BackupManager.js
+++ b/server/BackupManager.js
@@ -311,11 +311,13 @@ class BackupManager {
var librariesDbDir = Path.join(configPath, 'libraries')
var settingsDbDir = Path.join(configPath, 'settings')
var usersDbDir = Path.join(configPath, 'users')
+ var collectionsDbDir = Path.join(configPath, 'collections')
archive.directory(audiobooksDbDir, 'config/audiobooks')
archive.directory(librariesDbDir, 'config/libraries')
archive.directory(settingsDbDir, 'config/settings')
archive.directory(usersDbDir, 'config/users')
+ archive.directory(collectionsDbDir, 'config/collections')
if (metadataBooksPath) {
Logger.debug(`[BackupManager] Backing up Metadata Books "${metadataBooksPath}"`)
diff --git a/server/Db.js b/server/Db.js
index 5e2febaa..dc86c1c0 100644
--- a/server/Db.js
+++ b/server/Db.js
@@ -5,6 +5,7 @@ const jwt = require('jsonwebtoken')
const Logger = require('./Logger')
const Audiobook = require('./objects/Audiobook')
const User = require('./objects/User')
+const UserCollection = require('./objects/UserCollection')
const Library = require('./objects/Library')
const ServerSettings = require('./objects/ServerSettings')
@@ -16,16 +17,19 @@ class Db {
this.UsersPath = Path.join(ConfigPath, 'users')
this.LibrariesPath = Path.join(ConfigPath, 'libraries')
this.SettingsPath = Path.join(ConfigPath, 'settings')
+ this.CollectionsPath = Path.join(ConfigPath, 'collections')
this.audiobooksDb = new njodb.Database(this.AudiobooksPath)
this.usersDb = new njodb.Database(this.UsersPath)
this.librariesDb = new njodb.Database(this.LibrariesPath, { datastores: 2 })
this.settingsDb = new njodb.Database(this.SettingsPath, { datastores: 2 })
+ this.collectionsDb = new njodb.Database(this.CollectionsPath, { datastores: 2 })
this.users = []
this.libraries = []
this.audiobooks = []
this.settings = []
+ this.collections = []
this.serverSettings = null
}
@@ -34,14 +38,18 @@ class Db {
if (entityName === 'user') return this.usersDb
else if (entityName === 'audiobook') return this.audiobooksDb
else if (entityName === 'library') return this.librariesDb
- return this.settingsDb
+ else if (entityName === 'settings') return this.settingsDb
+ else if (entityName === 'collection') return this.collectionsDb
+ return null
}
getEntityArrayKey(entityName) {
if (entityName === 'user') return 'users'
else if (entityName === 'audiobook') return 'audiobooks'
else if (entityName === 'library') return 'libraries'
- return 'settings'
+ else if (entityName === 'settings') return 'settings'
+ else if (entityName === 'collection') return 'collections'
+ return null
}
getDefaultUser(token) {
@@ -76,6 +84,7 @@ class Db {
this.usersDb = new njodb.Database(this.UsersPath)
this.librariesDb = new njodb.Database(this.LibrariesPath, { datastores: 2 })
this.settingsDb = new njodb.Database(this.SettingsPath, { datastores: 2 })
+ this.collectionsDb = new njodb.Database(this.CollectionsPath, { datastores: 2 })
return this.init()
}
@@ -125,7 +134,11 @@ class Db {
}
}
})
- await Promise.all([p1, p2, p3, p4])
+ var p5 = this.collectionsDb.select(() => true).then((results) => {
+ this.collections = results.data.map(l => new UserCollection(l))
+ Logger.info(`[DB] ${this.collections.length} Collections Loaded`)
+ })
+ await Promise.all([p1, p2, p3, p4, p5])
}
updateAudiobook(audiobook) {
@@ -286,23 +299,5 @@ class Db {
return false
})
}
-
- getGenres() {
- var allGenres = []
- this.db.audiobooks.forEach((audiobook) => {
- allGenres = allGenres.concat(audiobook.genres)
- })
- allGenres = [...new Set(allGenres)] // Removes duplicates
- return allGenres
- }
-
- getTags() {
- var allTags = []
- this.db.audiobooks.forEach((audiobook) => {
- allTags = allTags.concat(audiobook.tags)
- })
- allTags = [...new Set(allTags)] // Removes duplicates
- return allTags
- }
}
module.exports = Db
diff --git a/server/objects/Stream.js b/server/objects/Stream.js
index 59597369..904ac4e4 100644
--- a/server/objects/Stream.js
+++ b/server/objects/Stream.js
@@ -7,6 +7,8 @@ const { secondsToTimestamp } = require('../utils/fileUtils')
const { writeConcatFile } = require('../utils/ffmpegHelpers')
const hlsPlaylistGenerator = require('../utils/hlsPlaylistGenerator')
+// const UserListeningSession = require('./UserListeningSession')
+
class Stream extends EventEmitter {
constructor(streamPath, client, audiobook) {
super()
@@ -32,6 +34,9 @@ class Stream extends EventEmitter {
this.furthestSegmentCreated = 0
this.clientCurrentTime = 0
+ // this.listeningSession = new UserListeningSession()
+ // this.listeningSession.setData(audiobook, client.user)
+
this.init()
}
diff --git a/server/objects/UserCollection.js b/server/objects/UserCollection.js
new file mode 100644
index 00000000..29cb36c0
--- /dev/null
+++ b/server/objects/UserCollection.js
@@ -0,0 +1,88 @@
+const Logger = require('../Logger')
+
+class UserCollection {
+ constructor(collection) {
+ this.id = null
+ this.libraryId = null
+ this.userId = null
+
+ this.name = null
+ this.description = null
+
+ this.cover = null
+ this.coverFullPath = null
+ this.books = []
+
+ this.lastUpdate = null
+ this.createdAt = null
+
+ if (collection) {
+ this.construct(collection)
+ }
+ }
+
+ toJSON() {
+ return {
+ id: this.id,
+ libraryId: this.libraryId,
+ userId: this.userId,
+ name: this.name,
+ description: this.description,
+ cover: this.cover,
+ coverFullPath: this.coverFullPath,
+ books: [...this.books],
+ lastUpdate: this.lastUpdate,
+ createdAt: this.createdAt
+ }
+ }
+
+ toJSONExpanded(audiobooks) {
+ var json = this.toJSON()
+ json.books = json.books.map(bookId => {
+ var _ab = audiobooks.find(ab => ab.id === bookId)
+ return _ab ? _ab.toJSON() : null
+ }).filter(b => !!b)
+ return json
+ }
+
+ construct(collection) {
+ this.id = collection.id
+ this.libraryId = collection.libraryId
+ this.userId = collection.userId
+ this.name = collection.name
+ this.description = collection.description || null
+ this.cover = collection.cover || null
+ this.coverFullPath = collection.coverFullPath || null
+ this.books = collection.books ? [...collection.books] : []
+ this.lastUpdate = collection.lastUpdate || null
+ this.createdAt = collection.createdAt || null
+ }
+
+ setData(data) {
+ if (!data.userId || !data.libraryId || !data.name) {
+ return false
+ }
+ this.id = (Math.trunc(Math.random() * 1000) + Date.now()).toString(36)
+ this.userId = data.userId
+ this.libraryId = data.libraryId
+ this.name = data.name
+ this.description = data.description || null
+ this.cover = data.cover || null
+ this.coverFullPath = data.coverFullPath || null
+ this.books = data.books ? [...data.books] : []
+ this.lastUpdate = Date.now()
+ this.createdAt = Date.now()
+ return true
+ }
+
+ addBook(bookId) {
+ this.books.push(bookId)
+ this.lastUpdate = Date.now()
+ }
+
+ removeBook(bookId) {
+ this.books = this.books.filter(bid => bid !== bookId)
+ this.lastUpdate = Date.now()
+ }
+}
+module.exports = UserCollection
\ No newline at end of file
diff --git a/server/objects/UserListeningSession.js b/server/objects/UserListeningSession.js
new file mode 100644
index 00000000..2cb2e39b
--- /dev/null
+++ b/server/objects/UserListeningSession.js
@@ -0,0 +1,57 @@
+const Logger = require('../Logger')
+
+class UserListeningSession {
+ constructor(session) {
+ this.userId = null
+ this.audiobookId = null
+ this.audiobookTitle = null
+ this.audiobookAuthor = null
+
+ this.timeListening = null
+ this.lastUpdate = null
+ this.startedAt = null
+ this.finishedAt = null
+
+ if (session) {
+ this.construct(session)
+ }
+ }
+
+ toJSON() {
+ return {
+ userId: this.userId,
+ audiobookId: this.audiobookId,
+ audiobookTitle: this.audiobookTitle,
+ audiobookAuthor: this.audiobookAuthor,
+ timeListening: this.timeListening,
+ lastUpdate: this.lastUpdate,
+ startedAt: this.startedAt,
+ finishedAt: this.finishedAt
+ }
+ }
+
+ construct(session) {
+ this.userId = session.userId
+ this.audiobookId = session.audiobookId
+ this.audiobookTitle = session.audiobookTitle
+ this.audiobookAuthor = session.audiobookAuthor
+
+ this.timeListening = session.timeListening || null
+ this.lastUpdate = session.lastUpdate || null
+ this.startedAt = session.startedAt
+ this.finishedAt = session.finishedAt || null
+ }
+
+ setData(audiobook, user) {
+ this.userId = user.id
+ this.audiobookId = audiobook.id
+ this.audiobookTitle = audiobook.title || ''
+ this.audiobookAuthor = audiobook.author || ''
+
+ this.timeListening = 0
+ this.lastUpdate = Date.now()
+ this.startedAt = Date.now()
+ this.finishedAt = null
+ }
+}
+module.exports = UserListeningSession
\ No newline at end of file