-
Other Files
+
Library Files
{{ files.length }}
-
Full Path
expand_more
@@ -22,19 +19,19 @@
Filetype |
Download |
-
+
- {{ showFullPath ? file.fullPath : file.path }}
+ {{ showFullPath ? file.metadata.path : file.metadata.relPath }}
|
auto_stories
- {{ file.filetype }}
+ {{ file.metadata.ext }}
|
- download
+ download
|
@@ -51,10 +48,8 @@ export default {
type: Array,
default: () => []
},
- audiobook: {
- type: Object,
- default: () => null
- }
+ libraryItemId: String,
+ isMissing: Boolean
},
data() {
return {
@@ -63,40 +58,19 @@ export default {
}
},
computed: {
- audiobookId() {
- return this.audiobook.id
- },
- audiobookPath() {
- return this.audiobook.path
- },
- otherFilesCleaned() {
- return this.files.map((file) => {
- return {
- ...file,
- relativePath: this.getRelativePath(file.path)
- }
- })
- },
userToken() {
return this.$store.getters['user/getToken']
},
- isMissing() {
- return this.audiobook.isMissing
- },
userCanDownload() {
return this.$store.getters['user/getUserCanDownload']
}
},
methods: {
readEbookClick(file) {
- this.$store.commit('showEReaderForFile', { audiobook: this.audiobook, file })
+ // this.$store.commit('showEReaderForFile', { audiobook: this.audiobook, file })
},
clickBar() {
this.showFiles = !this.showFiles
- },
- getRelativePath(path) {
- var relativePath = path.replace(/\\/g, '/').replace(this.audiobookPath.replace(/\\/g, '/') + '/', '')
- return this.$encodeUriPath(relativePath)
}
},
mounted() {}
diff --git a/client/components/tables/TracksTable.vue b/client/components/tables/TracksTable.vue
index 3dd9fe96..14ed84b7 100644
--- a/client/components/tables/TracksTable.vue
+++ b/client/components/tables/TracksTable.vue
@@ -8,7 +8,7 @@
Full Path
-
+
Manage Tracks
@@ -25,20 +25,20 @@
Duration |
Download |
-
+
{{ track.index }}
|
- {{ showFullPath ? track.fullPath : track.filename }} |
+ {{ showFullPath ? track.metadata.path : track.metadata.filename }} |
- {{ $bytesPretty(track.size) }}
+ {{ $bytesPretty(track.metadata.size) }}
|
{{ $secondsToTimestamp(track.duration) }}
|
- download
+ download
|
@@ -55,10 +55,7 @@ export default {
type: Array,
default: () => []
},
- audiobook: {
- type: Object,
- default: () => null
- }
+ libraryItemId: String
},
data() {
return {
@@ -67,20 +64,6 @@ export default {
}
},
computed: {
- audiobookId() {
- return this.audiobook.id
- },
- audiobookPath() {
- return this.audiobook.path
- },
- tracksCleaned() {
- return this.tracks.map((track) => {
- return {
- ...track,
- relativePath: this.getRelativePath(track.path)
- }
- })
- },
userToken() {
return this.$store.getters['user/getToken']
},
@@ -94,10 +77,6 @@ export default {
methods: {
clickBar() {
this.showTracks = !this.showTracks
- },
- getRelativePath(path) {
- var relativePath = path.replace(/\\/g, '/').replace(this.audiobookPath.replace(/\\/g, '/') + '/', '')
- return this.$encodeUriPath(relativePath)
}
},
mounted() {}
diff --git a/client/nuxt.config.js b/client/nuxt.config.js
index c74e955d..63519131 100644
--- a/client/nuxt.config.js
+++ b/client/nuxt.config.js
@@ -74,7 +74,6 @@ module.exports = {
proxy: {
'/dev/': { target: 'http://localhost:3333', pathRewrite: { '^/dev/': '' } },
- '/local/': { target: process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '/' },
'/lib/': { target: process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '/' },
'/ebook/': { target: process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '/' },
'/s/': { target: process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '/' },
diff --git a/client/pages/audiobook/_id/index.vue b/client/pages/audiobook/_id/index.vue
index de0a6dfb..bfd1d497 100644
--- a/client/pages/audiobook/_id/index.vue
+++ b/client/pages/audiobook/_id/index.vue
@@ -164,7 +164,7 @@
-
+
diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue
new file mode 100644
index 00000000..33802422
--- /dev/null
+++ b/client/pages/item/_id/index.vue
@@ -0,0 +1,475 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ play_circle_filled
+
+
+
+
edit
+
+
+
+
+
+
+
+
+ {{ title }}
+
+
{{ subtitle }}
+
+
+
+
+ by {{ author }},
+
+
by Unknown
+
+
{{ _series.text }}
+
+
+
+ Narrated By
+
+
+
+ {{ narrator }},
+
+
+
+
+
+ Publish Year
+
+
+ {{ publishYear }}
+
+
+
+
+ Genres
+
+
+
+ {{ genre }},
+
+
+
+
+
+ Duration
+
+
+ {{ durationPretty }}
+
+
+
+
+ Size
+
+
+ {{ sizePretty }}
+
+
+
+
+
+
+
+
+
warning_amber
+
Book has no audio tracks but has valid ebook files. The e-reader is experimental and can be turned on in config.
+
+
+
+
+
Your Progress: {{ Math.round(progressPercent * 100) }}%
+
Finished {{ $formatDate(userProgressFinishedAt, 'MM/dd/yyyy') }}
+
{{ $elapsedPretty(userTimeRemaining) }} remaining
+
Started {{ $formatDate(userProgressStartedAt, 'MM/dd/yyyy') }}
+
+
+ close
+
+
+
+
+
+ play_arrow
+ {{ streaming ? 'Streaming' : 'Play' }}
+
+
+ error
+ {{ isMissing ? 'Missing' : 'Incomplete' }}
+
+
+
+ auto_stories
+ Read
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Missing Parts ({{ missingParts.length }})
+
+
{{ missingPartChunks.join(', ') }}
+
+
+
+
+ Invalid Parts ({{ invalidParts.length }})
+
+
+
{{ part.filename }}: {{ part.error }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/players/CastPlayer.js b/client/players/CastPlayer.js
index 79687038..1d841f5e 100644
--- a/client/players/CastPlayer.js
+++ b/client/players/CastPlayer.js
@@ -76,7 +76,7 @@ export default class CastPlayer extends EventEmitter {
this.currentTime = startTime
- var coverImg = this.ctx.$store.getters['audiobooks/getBookCoverSrc'](audiobook)
+ var coverImg = this.ctx.$store.getters['audiobooks/getLibraryItemCoverSrc'](audiobook)
if (process.env.NODE_ENV === 'development') {
this.coverUrl = coverImg
} else {
diff --git a/client/plugins/chromecast.js b/client/plugins/chromecast.js
index 30287630..00cf3e3f 100644
--- a/client/plugins/chromecast.js
+++ b/client/plugins/chromecast.js
@@ -3,7 +3,7 @@ export default (ctx) => {
// Fetch background covers for chromecast (temp)
var covers = await ctx.$axios.$get(`/api/libraries/${ctx.$store.state.libraries.currentLibraryId}/books/all?limit=40&minified=1`).then((data) => {
return data.results.filter((b) => b.book.cover).map((ab) => {
- var coverUrl = ctx.$store.getters['audiobooks/getBookCoverSrc'](ab)
+ var coverUrl = ctx.$store.getters['audiobooks/getLibraryItemCoverSrc'](ab)
if (process.env.NODE_ENV === 'development') return coverUrl
return `${window.location.origin}${coverUrl}`
})
diff --git a/client/store/audiobooks.js b/client/store/audiobooks.js
index b1aabde3..da412176 100644
--- a/client/store/audiobooks.js
+++ b/client/store/audiobooks.js
@@ -12,25 +12,21 @@ export const state = () => ({
})
export const getters = {
- getBookCoverSrc: (state, getters, rootState, rootGetters) => (bookItem, placeholder = '/book_placeholder.jpg') => {
- if (!bookItem) return placeholder
- var book = bookItem.book
- if (!book || !book.cover || book.cover === placeholder) return placeholder
+ getLibraryItemCoverSrc: (state, getters, rootState, rootGetters) => (libraryItem, placeholder = '/book_placeholder.jpg') => {
+ if (!libraryItem) return placeholder
+ var media = libraryItem.media
+ if (!media || !media.coverPath || media.coverPath === placeholder) return placeholder
// Absolute URL covers (should no longer be used)
- if (book.cover.startsWith('http:') || book.cover.startsWith('https:')) return book.cover
+ if (media.coverPath.startsWith('http:') || media.coverPath.startsWith('https:')) return media.coverPath
var userToken = rootGetters['user/getToken']
- var bookLastUpdate = book.lastUpdate || Date.now()
-
- if (!bookItem.id) {
- console.error('No book item id', bookItem)
- }
+ var lastUpdate = libraryItem.updatedAt || Date.now()
if (process.env.NODE_ENV !== 'production') { // Testing
- return `http://localhost:3333/api/books/${bookItem.id}/cover?token=${userToken}&ts=${bookLastUpdate}`
+ return `http://localhost:3333/api/items/${libraryItem.id}/cover?token=${userToken}&ts=${lastUpdate}`
}
- return `/api/books/${bookItem.id}/cover?token=${userToken}&ts=${bookLastUpdate}`
+ return `/api/items/${libraryItem.id}/cover?token=${userToken}&ts=${lastUpdate}`
}
}
diff --git a/client/store/index.js b/client/store/index.js
index 3d85edf6..567fff98 100644
--- a/client/store/index.js
+++ b/client/store/index.js
@@ -9,6 +9,7 @@ export const state = () => ({
showEditModal: false,
showEReader: false,
selectedAudiobook: null,
+ selectedLibraryItem: null,
selectedAudiobookFile: null,
developerMode: false,
selectedAudiobooks: [],
@@ -108,14 +109,14 @@ export const mutations = {
setStreamAudiobook(state, audiobook) {
state.streamAudiobook = audiobook
},
- showEditModal(state, audiobook) {
+ showEditModal(state, libraryItem) {
state.editModalTab = 'details'
- state.selectedAudiobook = audiobook
+ state.selectedLibraryItem = libraryItem
state.showEditModal = true
},
- showEditModalOnTab(state, { audiobook, tab }) {
+ showEditModalOnTab(state, { libraryItem, tab }) {
state.editModalTab = tab
- state.selectedAudiobook = audiobook
+ state.selectedLibraryItem = libraryItem
state.showEditModal = true
},
setEditModalTab(state, tab) {
@@ -124,15 +125,15 @@ export const mutations = {
setShowEditModal(state, val) {
state.showEditModal = val
},
- showEReader(state, audiobook) {
+ showEReader(state, libraryItem) {
state.selectedAudiobookFile = null
- state.selectedAudiobook = audiobook
+ state.selectedLibraryItem = libraryItem
state.showEReader = true
},
- showEReaderForFile(state, { audiobook, file }) {
+ showEReaderForFile(state, { libraryItem, file }) {
state.selectedAudiobookFile = file
- state.selectedAudiobook = audiobook
+ state.selectedLibraryItem = libraryItem
state.showEReader = true
},
@@ -142,8 +143,8 @@ export const mutations = {
setDeveloperMode(state, val) {
state.developerMode = val
},
- setSelectedAudiobook(state, val) {
- Vue.set(state, 'selectedAudiobook', val)
+ setSelectedLibraryItem(state, val) {
+ Vue.set(state, 'selectedLibraryItem', val)
},
setSelectedAudiobooks(state, audiobooks) {
Vue.set(state, 'selectedAudiobooks', audiobooks)
diff --git a/client/store/libraries.js b/client/store/libraries.js
index 514bf5b3..d7fee8a7 100644
--- a/client/store/libraries.js
+++ b/client/store/libraries.js
@@ -124,7 +124,7 @@ export const actions = {
}
this.$axios
- .$get(`/api/libraries/${state.currentLibraryId}/filters`)
+ .$get(`/api/libraries/${state.currentLibraryId}/filterdata`)
.then((data) => {
commit('setLibraryFilterData', data)
})
diff --git a/docs/LibraryItemModelDemo.js b/docs/LibraryItemModelDemo.js
index b4d03827..5fc44e06 100644
--- a/docs/LibraryItemModelDemo.js
+++ b/docs/LibraryItemModelDemo.js
@@ -19,8 +19,9 @@ new LibraryItem({
lastScan: 1646784672127,
scanVersion: 1.72,
isMissing: false,
- entityType: 'book',
- entity: { // Book.js
+ mediaType: 'book',
+ media: { // Book.js
+ coverPath: '/metadata/books/li_abai123wir/cover.webp',
metadata: { // BookMetadata.js
title: 'Wizards First Rule',
subtitle: null,
diff --git a/server/ApiController.js b/server/ApiController.js
index 90034b2c..9ee97346 100644
--- a/server/ApiController.js
+++ b/server/ApiController.js
@@ -14,6 +14,7 @@ const UserController = require('./controllers/UserController')
const CollectionController = require('./controllers/CollectionController')
const MeController = require('./controllers/MeController')
const BackupController = require('./controllers/BackupController')
+const LibraryItemController = require('./controllers/LibraryItemController')
const BookFinder = require('./finders/BookFinder')
const AuthorFinder = require('./finders/AuthorFinder')
@@ -53,19 +54,29 @@ class ApiController {
this.router.patch('/libraries/:id', LibraryController.middleware.bind(this), LibraryController.update.bind(this))
this.router.delete('/libraries/:id', LibraryController.middleware.bind(this), LibraryController.delete.bind(this))
- this.router.get('/libraries/:id/books/all', LibraryController.middleware.bind(this), LibraryController.getBooksForLibrary2.bind(this))
- this.router.get('/libraries/:id/books', LibraryController.middleware.bind(this), LibraryController.getBooksForLibrary.bind(this))
+ this.router.get('/libraries/:id/items', LibraryController.middleware.bind(this), LibraryController.getLibraryItems.bind(this))
this.router.get('/libraries/:id/series', LibraryController.middleware.bind(this), LibraryController.getAllSeriesForLibrary.bind(this))
this.router.get('/libraries/:id/series/:series', LibraryController.middleware.bind(this), LibraryController.getSeriesForLibrary.bind(this))
this.router.get('/libraries/:id/collections', LibraryController.middleware.bind(this), LibraryController.getCollectionsForLibrary.bind(this))
- this.router.get('/libraries/:id/categories', LibraryController.middleware.bind(this), LibraryController.getLibraryCategories.bind(this))
- this.router.get('/libraries/:id/filters', LibraryController.middleware.bind(this), LibraryController.getLibraryFilters.bind(this))
+ this.router.get('/libraries/:id/personalized', LibraryController.middleware.bind(this), LibraryController.getLibraryUserPersonalized.bind(this))
+ this.router.get('/libraries/:id/filterdata', LibraryController.middleware.bind(this), LibraryController.getLibraryFilterData.bind(this))
this.router.get('/libraries/:id/search', LibraryController.middleware.bind(this), LibraryController.search.bind(this))
this.router.get('/libraries/:id/stats', LibraryController.middleware.bind(this), LibraryController.stats.bind(this))
this.router.get('/libraries/:id/authors', LibraryController.middleware.bind(this), LibraryController.getAuthors.bind(this))
this.router.post('/libraries/:id/matchbooks', LibraryController.middleware.bind(this), LibraryController.matchBooks.bind(this))
this.router.post('/libraries/order', LibraryController.reorder.bind(this))
+ // Legacy
+ this.router.get('/libraries/:id/books/all', LibraryController.middleware.bind(this), LibraryController.getBooksForLibrary.bind(this))
+ this.router.get('/libraries/:id/categories', LibraryController.middleware.bind(this), LibraryController.getLibraryCategories.bind(this))
+ this.router.get('/libraries/:id/filters', LibraryController.middleware.bind(this), LibraryController.getLibraryFilters.bind(this))
+
+ //
+ // Item Routes
+ //
+ this.router.get('/items/:id', LibraryItemController.middleware.bind(this), LibraryItemController.findOne.bind(this))
+ this.router.get('/items/:id/cover', LibraryItemController.middleware.bind(this), LibraryItemController.getCover.bind(this))
+
//
// Book Routes
//
diff --git a/server/CacheManager.js b/server/CacheManager.js
index bece235f..cf5f611e 100644
--- a/server/CacheManager.js
+++ b/server/CacheManager.js
@@ -10,14 +10,14 @@ class CacheManager {
this.CoverCachePath = Path.join(this.CachePath, 'covers')
}
- async handleCoverCache(res, audiobook, options = {}) {
+ async handleCoverCache(res, libraryItem, options = {}) {
const format = options.format || 'webp'
const width = options.width || 400
const height = options.height || null
res.type(`image/${format}`)
- var path = Path.join(this.CoverCachePath, `${audiobook.id}_${width}${height ? `x${height}` : ''}`) + '.' + format
+ var path = Path.join(this.CoverCachePath, `${libraryItem.id}_${width}${height ? `x${height}` : ''}`) + '.' + format
// Cache exists
if (await fs.pathExists(path)) {
@@ -35,7 +35,7 @@ class CacheManager {
// Write cache
await fs.ensureDir(this.CoverCachePath)
- let writtenFile = await resizeImage(audiobook.book.coverFullPath, path, width, height)
+ let writtenFile = await resizeImage(libraryItem.media.coverPath, path, width, height)
if (!writtenFile) return res.sendStatus(400)
var readStream = fs.createReadStream(writtenFile)
diff --git a/server/Server.js b/server/Server.js
index 44d6cb64..bfc48297 100644
--- a/server/Server.js
+++ b/server/Server.js
@@ -69,7 +69,6 @@ class Server {
Logger.logManager = this.logManager
- this.expressApp = null
this.server = null
this.io = null
@@ -154,8 +153,6 @@ class Server {
await this.init()
const app = express()
- this.expressApp = app
-
this.server = http.createServer(app)
app.use(this.auth.cors)
@@ -167,9 +164,6 @@ class Server {
const distPath = Path.join(global.appRoot, '/client/dist')
app.use(express.static(distPath))
- // Old static path for covers
- app.use('/local', this.authMiddleware.bind(this), express.static(global.AudiobookPath))
-
// Metadata folder static path
app.use('/metadata', this.authMiddleware.bind(this), express.static(global.MetadataPath))
@@ -179,6 +173,9 @@ class Server {
// Static folder
app.use(express.static(Path.join(global.appRoot, 'static')))
+ app.use('/api', this.authMiddleware.bind(this), this.apiController.router)
+ app.use('/hls', this.authMiddleware.bind(this), this.hlsController.router)
+
// Static file routes
app.get('/lib/:library/:folder/*', this.authMiddleware.bind(this), (req, res) => {
var library = this.db.libraries.find(lib => lib.id === req.params.library)
@@ -192,6 +189,7 @@ class Server {
})
// Book static file routes
+ // LEGACY
app.get('/s/book/:id/*', this.authMiddleware.bind(this), (req, res) => {
var audiobook = this.db.audiobooks.find(ab => ab.id === req.params.id)
if (!audiobook) return res.status(404).send('Book not found with id ' + req.params.id)
@@ -201,6 +199,16 @@ class Server {
res.sendFile(fullPath)
})
+ // Library Item static file routes
+ app.get('/s/item/:id/*', this.authMiddleware.bind(this), (req, res) => {
+ var item = this.db.libraryItems.find(ab => ab.id === req.params.id)
+ if (!item) return res.status(404).send('Item not found with id ' + req.params.id)
+
+ var remainingPath = req.params['0']
+ var fullPath = Path.join(item.path, remainingPath)
+ res.sendFile(fullPath)
+ })
+
// EBook static file routes
app.get('/ebook/:library/:folder/*', (req, res) => {
var library = this.db.libraries.find(lib => lib.id === req.params.library)
@@ -214,8 +222,10 @@ class Server {
})
// Client dynamic routes
- app.get('/audiobook/:id', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
- app.get('/audiobook/:id/edit', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
+ app.get('/audiobook/:id', (req, res) => res.sendFile(Path.join(distPath, 'index.html'))) // LEGACY
+ app.get('/audiobook/:id/edit', (req, res) => res.sendFile(Path.join(distPath, 'index.html'))) // LEGACY
+ app.get('/item/:id', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
+ app.get('/item/:id/edit', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
app.get('/library/:library', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
app.get('/library/:library/search', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
app.get('/library/:library/bookshelf/:id?', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
@@ -224,31 +234,16 @@ class Server {
app.get('/config/users/:id', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
app.get('/collection/:id', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
- app.use('/api', this.authMiddleware.bind(this), this.apiController.router)
- app.use('/hls', this.authMiddleware.bind(this), this.hlsController.router)
-
- // Incomplete work in progress
- // app.use('/feeds', this.rssFeeds.router)
-
+ app.post('/login', this.getLoginRateLimiter(), (req, res) => this.auth.login(req, res))
app.post('/upload', this.authMiddleware.bind(this), this.handleUpload.bind(this))
-
- var loginRateLimiter = this.getLoginRateLimiter()
- app.post('/login', loginRateLimiter, (req, res) => this.auth.login(req, res))
-
app.post('/logout', this.authMiddleware.bind(this), this.logout.bind(this))
-
app.get('/ping', (req, res) => {
Logger.info('Recieved ping')
res.json({ success: true })
})
- // Used in development to set-up streams without authentication
- if (process.env.NODE_ENV !== 'production') {
- app.use('/test-hls', this.hlsController.router)
- }
-
this.server.listen(this.Port, this.Host, () => {
- Logger.info(`Running on http://${this.Host}:${this.Port}`)
+ Logger.info(`Listening on http://${this.Host}:${this.Port}`)
})
this.io = new SocketIO.Server(this.server, {
@@ -265,21 +260,25 @@ class Server {
}
socket.sheepClient = this.clients[socket.id]
- Logger.info('[SOCKET] Socket Connected', socket.id)
+ Logger.info('[Server] Socket Connected', socket.id)
socket.on('auth', (token) => this.authenticateSocket(socket, token))
+ // TODO: Most of these web socket listeners will be moved to API routes instead
+ // with the goal of the web socket connection being a nice-to-have not need-to-have
+
// Scanning
socket.on('scan', this.scan.bind(this))
socket.on('cancel_scan', this.cancelScan.bind(this))
socket.on('scan_audiobook', (audiobookId) => this.scanAudiobook(socket, audiobookId))
socket.on('save_metadata', (audiobookId) => this.saveMetadata(socket, audiobookId))
- // Streaming
+ // Streaming (only still used in the mobile app)
socket.on('open_stream', (audiobookId) => this.streamManager.openStreamSocketRequest(socket, audiobookId))
socket.on('close_stream', () => this.streamManager.closeStreamRequest(socket))
socket.on('stream_sync', (syncData) => this.streamManager.streamSync(socket, syncData))
+ // Used to sync when playing local book on mobile, will be moved to API route
socket.on('progress_update', (payload) => this.audiobookProgressUpdate(socket, payload))
// Downloading
@@ -299,10 +298,6 @@ class Server {
socket.on('update_bookmark', (payload) => this.updateBookmark(socket, payload))
socket.on('delete_bookmark', (payload) => this.deleteBookmark(socket, payload))
- socket.on('test', () => {
- socket.emit('test_received', socket.id)
- })
-
socket.on('disconnect', () => {
Logger.removeSocketListener(socket.id)
diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js
index dc699390..52f2ea80 100644
--- a/server/controllers/LibraryController.js
+++ b/server/controllers/LibraryController.js
@@ -39,10 +39,9 @@ class LibraryController {
async findOne(req, res) {
if (req.query.include && req.query.include === 'filterdata') {
- var books = this.db.audiobooks.filter(ab => ab.libraryId === req.library.id)
return res.json({
- filterdata: libraryHelpers.getDistinctFilterData(books),
- issues: libraryHelpers.getNumIssues(books),
+ filterdata: libraryHelpers.getDistinctFilterDataNew(req.libraryItems),
+ issues: libraryHelpers.getNumIssues(req.libraryItems),
library: req.library
})
}
@@ -91,38 +90,70 @@ class LibraryController {
return res.json(libraryJson)
}
- // api/libraries/:id/books
- getBooksForLibrary(req, res) {
+ // api/libraries/:id/items
+ // TODO: Optimize this method, audiobooks are iterated through several times but can be combined
+ getLibraryItems(req, res) {
var libraryId = req.library.id
- var audiobooks = this.db.audiobooks.filter(ab => ab.libraryId === libraryId)
-
- if (req.query.filter) {
- audiobooks = libraryHelpers.getFiltered(audiobooks, req.query.filter, req.user)
+ var media = req.query.media || 'all'
+ var libraryItems = this.db.libraryItems.filter(li => {
+ if (li.libraryId !== libraryId) return false
+ if (media != 'all') return li.mediaType == media
+ return true
+ })
+ var payload = {
+ results: [],
+ total: libraryItems.length,
+ limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0,
+ page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0,
+ sortBy: req.query.sort,
+ sortDesc: req.query.desc === '1',
+ filterBy: req.query.filter,
+ media,
+ minified: req.query.minified === '1',
+ collapseseries: req.query.collapseseries === '1'
}
- if (req.query.sort) {
- var orderByNumber = req.query.sort === 'book.volumeNumber'
- var direction = req.query.desc === '1' ? 'desc' : 'asc'
- audiobooks = sort(audiobooks)[direction]((ab) => {
- // Supports dot notation strings i.e. "book.title"
- var value = req.query.sort.split('.').reduce((a, b) => a[b], ab)
- if (orderByNumber && !isNaN(value)) return Number(value)
- return value
+ if (payload.filterBy) {
+ libraryItems = libraryHelpers.getFilteredLibraryItems(libraryItems, payload.filterBy, req.user)
+ payload.total = libraryItems.length
+ }
+
+ if (payload.sortBy) {
+ var sortKey = payload.sortBy
+
+ // old sort key
+ if (sortKey.startsWith('book.')) {
+ sortKey = sortKey.replace('book.', 'media.metadata.')
+ }
+
+ // Handle server setting sortingIgnorePrefix
+ if (sortKey === 'media.metadata.title' && this.db.serverSettings.sortingIgnorePrefix) {
+ // BookMetadata.js has titleIgnorePrefix getter
+ sortKey += 'IgnorePrefix'
+ }
+
+ var direction = payload.sortDesc ? 'desc' : 'asc'
+ libraryItems = naturalSort(libraryItems)[direction]((li) => {
+
+ // Supports dot notation strings i.e. "media.metadata.title"
+ return sortKey.split('.').reduce((a, b) => a[b], li)
})
}
- if (req.query.limit && !isNaN(req.query.limit)) {
- var page = req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0
- var limit = Number(req.query.limit)
- var startIndex = page * limit
- audiobooks = audiobooks.slice(startIndex, startIndex + limit)
+ // TODO: Potentially implement collapse series again
+ libraryItems = libraryItems.map(ab => payload.minified ? ab.toJSONMinified() : ab.toJSON())
+
+ if (payload.limit) {
+ var startIndex = payload.page * payload.limit
+ libraryItems = libraryItems.slice(startIndex, startIndex + payload.limit)
}
- res.json(audiobooks)
+ payload.results = libraryItems
+ res.json(payload)
}
// api/libraries/:id/books/all
// TODO: Optimize this method, audiobooks are iterated through several times but can be combined
- getBooksForLibrary2(req, res) {
+ getBooksForLibrary(req, res) {
var libraryId = req.library.id
var audiobooks = this.db.audiobooks.filter(ab => ab.libraryId === libraryId)
@@ -274,6 +305,7 @@ class LibraryController {
res.json(payload)
}
+ // LEGACY
// api/libraries/:id/books/filters
async getLibraryFilters(req, res) {
var library = req.library
@@ -281,6 +313,45 @@ class LibraryController {
res.json(libraryHelpers.getDistinctFilterData(books))
}
+ async getLibraryFilterData(req, res) {
+ res.json(libraryHelpers.getDistinctFilterDataNew(req.libraryItems))
+ }
+
+ // api/libraries/:id/books/personalized
+ async getLibraryUserPersonalized(req, res) {
+ var libraryItems = req.libraryItems
+ var limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12
+ var minified = req.query.minified === '1'
+
+ var booksWithUserAb = libraryHelpers.getBooksWithUserAudiobook(req.user, libraryItems)
+
+ var categories = [
+ {
+ id: 'continue-reading',
+ label: 'Continue Reading',
+ type: 'books',
+ entities: libraryHelpers.getBooksMostRecentlyRead(booksWithUserAb, limitPerShelf, minified)
+ },
+ {
+ id: 'recently-added',
+ label: 'Recently Added',
+ type: 'books',
+ entities: libraryHelpers.getBooksMostRecentlyAdded(libraryItems, limitPerShelf, minified)
+ },
+ {
+ id: 'read-again',
+ label: 'Read Again',
+ type: 'books',
+ entities: libraryHelpers.getBooksMostRecentlyFinished(booksWithUserAb, limitPerShelf, minified)
+ }
+ ].filter(cats => { // Remove categories with no items
+ return cats.entities.length
+ })
+
+ res.json(categories)
+ }
+
+ // LEGACY
// api/libraries/:id/books/categories
async getLibraryCategories(req, res) {
var library = req.library
@@ -491,6 +562,7 @@ class LibraryController {
return res.status(404).send('Library not found')
}
req.library = library
+ req.libraryItems = this.db.libraryItems.filter(li => li.libraryId === library.id)
next()
}
}
diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js
new file mode 100644
index 00000000..da6a6349
--- /dev/null
+++ b/server/controllers/LibraryItemController.js
@@ -0,0 +1,37 @@
+const Logger = require('../Logger')
+const { reqSupportsWebp } = require('../utils/index')
+
+class LibraryItemController {
+ constructor() { }
+
+ findOne(req, res) {
+ if (req.query.expanded == 1) return res.json(req.libraryItem.toJSONExpanded())
+ res.json(req.libraryItem)
+ }
+
+ // GET api/items/:id/cover
+ async getCover(req, res) {
+ let { query: { width, height, format }, libraryItem } = req
+
+ const options = {
+ format: format || (reqSupportsWebp(req) ? 'webp' : 'jpeg'),
+ height: height ? parseInt(height) : null,
+ width: width ? parseInt(width) : null
+ }
+ return this.cacheManager.handleCoverCache(res, libraryItem, options)
+ }
+
+ middleware(req, res, next) {
+ var item = this.db.libraryItems.find(li => li.id === req.params.id)
+ if (!item || !item.media || !item.media.coverPath) return res.sendStatus(404)
+
+ // Check user can access this audiobooks library
+ if (!req.user.checkCanAccessLibrary(item.libraryId)) {
+ return res.sendStatus(403)
+ }
+
+ req.libraryItem = item
+ next()
+ }
+}
+module.exports = new LibraryItemController()
\ No newline at end of file
diff --git a/server/finders/AuthorFinder.js b/server/finders/AuthorFinder.js
index 3232fcbb..d4936a04 100644
--- a/server/finders/AuthorFinder.js
+++ b/server/finders/AuthorFinder.js
@@ -1,7 +1,7 @@
const fs = require('fs-extra')
const Logger = require('../Logger')
const Path = require('path')
-const Author = require('../objects/Author')
+const Author = require('../objects/legacy/Author')
const Audnexus = require('../providers/Audnexus')
const { downloadFile } = require('../utils/fileUtils')
diff --git a/server/objects/AudioFileMetadata.js b/server/objects/AudioFileMetadata.js
deleted file mode 100644
index 0b6abe11..00000000
--- a/server/objects/AudioFileMetadata.js
+++ /dev/null
@@ -1,129 +0,0 @@
-class AudioFileMetadata {
- constructor(metadata) {
- this.tagAlbum = null
- this.tagArtist = null
- this.tagGenre = null
- this.tagTitle = null
- this.tagSeries = null
- this.tagSeriesPart = null
- this.tagTrack = null
- this.tagDisc = null
- this.tagSubtitle = null
- this.tagAlbumArtist = null
- this.tagDate = null
- this.tagComposer = null
- this.tagPublisher = null
- this.tagComment = null
- this.tagDescription = null
- this.tagEncoder = null
- this.tagEncodedBy = null
- this.tagIsbn = null
- this.tagLanguage = null
- this.tagASIN = null
-
- if (metadata) {
- this.construct(metadata)
- }
- }
-
- toJSON() {
- // Only return the tags that are actually set
- var json = {}
- for (const key in this) {
- if (key.startsWith('tag') && this[key]) {
- json[key] = this[key]
- }
- }
- return json
- }
-
- construct(metadata) {
- this.tagAlbum = metadata.tagAlbum || null
- this.tagArtist = metadata.tagArtist || null
- this.tagGenre = metadata.tagGenre || null
- this.tagTitle = metadata.tagTitle || null
- this.tagSeries = metadata.tagSeries || null
- this.tagSeriesPart = metadata.tagSeriesPart || null
- this.tagTrack = metadata.tagTrack || null
- this.tagDisc = metadata.tagDisc || null
- this.tagSubtitle = metadata.tagSubtitle || null
- this.tagAlbumArtist = metadata.tagAlbumArtist || null
- this.tagDate = metadata.tagDate || null
- this.tagComposer = metadata.tagComposer || null
- this.tagPublisher = metadata.tagPublisher || null
- this.tagComment = metadata.tagComment || null
- this.tagDescription = metadata.tagDescription || null
- this.tagEncoder = metadata.tagEncoder || null
- this.tagEncodedBy = metadata.tagEncodedBy || null
- this.tagIsbn = metadata.tagIsbn || null
- this.tagLanguage = metadata.tagLanguage || null
- this.tagASIN = metadata.tagASIN || null
- }
-
- // Data parsed in prober.js
- setData(payload) {
- this.tagAlbum = payload.file_tag_album || null
- this.tagArtist = payload.file_tag_artist || null
- this.tagGenre = payload.file_tag_genre || null
- this.tagTitle = payload.file_tag_title || null
- this.tagSeries = payload.file_tag_series || null
- this.tagSeriesPart = payload.file_tag_seriespart || null
- this.tagTrack = payload.file_tag_track || null
- this.tagDisc = payload.file_tag_disc || null
- this.tagSubtitle = payload.file_tag_subtitle || null
- this.tagAlbumArtist = payload.file_tag_albumartist || null
- this.tagDate = payload.file_tag_date || null
- this.tagComposer = payload.file_tag_composer || null
- this.tagPublisher = payload.file_tag_publisher || null
- this.tagComment = payload.file_tag_comment || null
- this.tagDescription = payload.file_tag_description || null
- this.tagEncoder = payload.file_tag_encoder || null
- this.tagEncodedBy = payload.file_tag_encodedby || null
- this.tagIsbn = payload.file_tag_isbn || null
- this.tagLanguage = payload.file_tag_language || null
- this.tagASIN = payload.file_tag_asin || null
- }
-
- updateData(payload) {
- const dataMap = {
- tagAlbum: payload.file_tag_album || null,
- tagArtist: payload.file_tag_artist || null,
- tagGenre: payload.file_tag_genre || null,
- tagTitle: payload.file_tag_title || null,
- tagSeries: payload.file_tag_series || null,
- tagSeriesPart: payload.file_tag_seriespart || null,
- tagTrack: payload.file_tag_track || null,
- tagDisc: payload.file_tag_disc || null,
- tagSubtitle: payload.file_tag_subtitle || null,
- tagAlbumArtist: payload.file_tag_albumartist || null,
- tagDate: payload.file_tag_date || null,
- tagComposer: payload.file_tag_composer || null,
- tagPublisher: payload.file_tag_publisher || null,
- tagComment: payload.file_tag_comment || null,
- tagDescription: payload.file_tag_description || null,
- tagEncoder: payload.file_tag_encoder || null,
- tagEncodedBy: payload.file_tag_encodedby || null,
- tagIsbn: payload.file_tag_isbn || null,
- tagLanguage: payload.file_tag_language || null,
- tagASIN: payload.file_tag_asin || null
- }
-
- var hasUpdates = false
- for (const key in dataMap) {
- if (dataMap[key] !== this[key]) {
- this[key] = dataMap[key]
- hasUpdates = true
- }
- }
- return hasUpdates
- }
-
- isEqual(audioFileMetadata) {
- if (!audioFileMetadata || !audioFileMetadata.toJSON) return false
- for (const key in audioFileMetadata.toJSON()) {
- if (audioFileMetadata[key] !== this[key]) return false
- }
- return true
- }
-}
-module.exports = AudioFileMetadata
\ No newline at end of file
diff --git a/server/objects/LibraryItem.js b/server/objects/LibraryItem.js
index 971b3545..a01e5cc5 100644
--- a/server/objects/LibraryItem.js
+++ b/server/objects/LibraryItem.js
@@ -17,15 +17,15 @@ class LibraryItem {
this.ctimeMs = null
this.birthtimeMs = null
this.addedAt = null
- this.lastUpdate = null
+ this.updatedAt = null
this.lastScan = null
this.scanVersion = null
// Entity was scanned and not found
this.isMissing = false
- this.entityType = null
- this.entity = null
+ this.mediaType = null
+ this.media = null
this.libraryFiles = []
@@ -45,17 +45,17 @@ class LibraryItem {
this.ctimeMs = libraryItem.ctimeMs || 0
this.birthtimeMs = libraryItem.birthtimeMs || 0
this.addedAt = libraryItem.addedAt
- this.lastUpdate = libraryItem.lastUpdate || this.addedAt
+ this.updatedAt = libraryItem.updatedAt || this.addedAt
this.lastScan = libraryItem.lastScan || null
this.scanVersion = libraryItem.scanVersion || null
this.isMissing = !!libraryItem.isMissing
- this.entityType = libraryItem.entityType
- if (this.entityType === 'book') {
- this.entity = new Book(libraryItem.entity)
- } else if (this.entityType === 'podcast') {
- this.entity = new Podcast(libraryItem.entity)
+ this.mediaType = libraryItem.mediaType
+ if (this.mediaType === 'book') {
+ this.media = new Book(libraryItem.media)
+ } else if (this.mediaType === 'podcast') {
+ this.media = new Podcast(libraryItem.media)
}
this.libraryFiles = libraryItem.libraryFiles.map(f => new LibraryFile(f))
@@ -73,14 +73,64 @@ class LibraryItem {
ctimeMs: this.ctimeMs,
birthtimeMs: this.birthtimeMs,
addedAt: this.addedAt,
- lastUpdate: this.lastUpdate,
+ updatedAt: this.updatedAt,
lastScan: this.lastScan,
scanVersion: this.scanVersion,
isMissing: !!this.isMissing,
- entityType: this.entityType,
- entity: this.entity.toJSON(),
+ mediaType: this.mediaType,
+ media: this.media.toJSON(),
libraryFiles: this.libraryFiles.map(f => f.toJSON())
}
}
+
+ toJSONMinified() {
+ return {
+ id: this.id,
+ ino: this.ino,
+ libraryId: this.libraryId,
+ folderId: this.folderId,
+ path: this.path,
+ relPath: this.relPath,
+ mtimeMs: this.mtimeMs,
+ ctimeMs: this.ctimeMs,
+ birthtimeMs: this.birthtimeMs,
+ addedAt: this.addedAt,
+ updatedAt: this.updatedAt,
+ isMissing: !!this.isMissing,
+ mediaType: this.mediaType,
+ media: this.media.toJSONMinified(),
+ numFiles: this.libraryFiles.length
+ }
+ }
+
+ // Adds additional helpful fields like media duration, tracks, etc.
+ toJSONExpanded() {
+ return {
+ id: this.id,
+ ino: this.ino,
+ libraryId: this.libraryId,
+ folderId: this.folderId,
+ path: this.path,
+ relPath: this.relPath,
+ mtimeMs: this.mtimeMs,
+ ctimeMs: this.ctimeMs,
+ birthtimeMs: this.birthtimeMs,
+ addedAt: this.addedAt,
+ updatedAt: this.updatedAt,
+ lastScan: this.lastScan,
+ scanVersion: this.scanVersion,
+ isMissing: !!this.isMissing,
+ mediaType: this.mediaType,
+ media: this.media.toJSONExpanded(),
+ libraryFiles: this.libraryFiles.map(f => f.toJSON()),
+ size: this.size
+ }
+ }
+
+ get size() {
+ var total = 0
+ this.libraryFiles.forEach((lf) => total += lf.metadata.size)
+ return total
+ }
}
module.exports = LibraryItem
\ No newline at end of file
diff --git a/server/objects/entities/Book.js b/server/objects/entities/Book.js
index 37c497e1..c76b92d7 100644
--- a/server/objects/entities/Book.js
+++ b/server/objects/entities/Book.js
@@ -7,7 +7,6 @@ class Book {
this.metadata = null
this.coverPath = null
- this.relCoverPath = null
this.tags = []
this.audioFiles = []
this.ebookFiles = []
@@ -21,7 +20,6 @@ class Book {
construct(book) {
this.metadata = new BookMetadata(book.metadata)
this.coverPath = book.coverPath
- this.relCoverPath = book.relCoverPath
this.tags = [...book.tags]
this.audioFiles = book.audioFiles.map(f => new AudioFile(f))
this.ebookFiles = book.ebookFiles.map(f => new EBookFile(f))
@@ -32,12 +30,53 @@ class Book {
return {
metadata: this.metadata.toJSON(),
coverPath: this.coverPath,
- relCoverPath: this.relCoverPath,
tags: [...this.tags],
audioFiles: this.audioFiles.map(f => f.toJSON()),
ebookFiles: this.ebookFiles.map(f => f.toJSON()),
chapters: this.chapters.map(c => ({ ...c }))
}
}
+
+ toJSONMinified() {
+ return {
+ metadata: this.metadata.toJSON(),
+ coverPath: this.coverPath,
+ tags: [...this.tags],
+ numTracks: this.tracks.length,
+ numAudioFiles: this.audioFiles.length,
+ numEbooks: this.ebookFiles.length,
+ numChapters: this.chapters.length,
+ duration: this.duration,
+ size: this.size
+ }
+ }
+
+ toJSONExpanded() {
+ return {
+ metadata: this.metadata.toJSONExpanded(),
+ coverPath: this.coverPath,
+ tags: [...this.tags],
+ audioFiles: this.audioFiles.map(f => f.toJSON()),
+ ebookFiles: this.ebookFiles.map(f => f.toJSON()),
+ chapters: this.chapters.map(c => ({ ...c })),
+ duration: this.duration,
+ size: this.size,
+ tracks: this.tracks.map(t => t.toJSON())
+ }
+ }
+
+ get tracks() {
+ return this.audioFiles.filter(af => !af.exclude && !af.invalid)
+ }
+ get duration() {
+ var total = 0
+ this.tracks.forEach((track) => total += track.duration)
+ return total
+ }
+ get size() {
+ var total = 0
+ this.audioFiles.forEach((af) => total += af.metadata.size)
+ return total
+ }
}
module.exports = Book
\ No newline at end of file
diff --git a/server/objects/entities/Podcast.js b/server/objects/entities/Podcast.js
index a8913cf8..9e3cc6b8 100644
--- a/server/objects/entities/Podcast.js
+++ b/server/objects/entities/Podcast.js
@@ -6,8 +6,8 @@ class Podcast {
this.id = null
this.metadata = null
- this.cover = null
- this.coverFullPath = null
+ this.coverPath = null
+ this.tags = []
this.episodes = []
this.createdAt = null
@@ -21,8 +21,8 @@ class Podcast {
construct(podcast) {
this.id = podcast.id
this.metadata = new PodcastMetadata(podcast.metadata)
- this.cover = podcast.cover
- this.coverFullPath = podcast.coverFullPath
+ this.coverPath = podcast.coverPath
+ this.tags = [...podcast.tags]
this.episodes = podcast.episodes.map((e) => new PodcastEpisode(e))
this.createdAt = podcast.createdAt
this.lastUpdate = podcast.lastUpdate
@@ -32,8 +32,32 @@ class Podcast {
return {
id: this.id,
metadata: this.metadata.toJSON(),
- cover: this.cover,
- coverFullPath: this.coverFullPath,
+ coverPath: this.coverPath,
+ tags: [...this.tags],
+ episodes: this.episodes.map(e => e.toJSON()),
+ createdAt: this.createdAt,
+ lastUpdate: this.lastUpdate
+ }
+ }
+
+ toJSONMinified() {
+ return {
+ id: this.id,
+ metadata: this.metadata.toJSON(),
+ coverPath: this.coverPath,
+ tags: [...this.tags],
+ episodes: this.episodes.map(e => e.toJSON()),
+ createdAt: this.createdAt,
+ lastUpdate: this.lastUpdate
+ }
+ }
+
+ toJSONExpanded() {
+ return {
+ id: this.id,
+ metadata: this.metadata.toJSONExpanded(),
+ coverPath: this.coverPath,
+ tags: [...this.tags],
episodes: this.episodes.map(e => e.toJSON()),
createdAt: this.createdAt,
lastUpdate: this.lastUpdate
diff --git a/server/objects/files/LibraryFile.js b/server/objects/files/LibraryFile.js
index 5d444a78..dfbf002d 100644
--- a/server/objects/files/LibraryFile.js
+++ b/server/objects/files/LibraryFile.js
@@ -1,3 +1,4 @@
+const globals = require('../../utils/globals')
const FileMetadata = require('../metadata/FileMetadata')
class LibraryFile {
@@ -24,8 +25,18 @@ class LibraryFile {
ino: this.ino,
metadata: this.metadata.toJSON(),
addedAt: this.addedAt,
- updatedAt: this.updatedAt
+ updatedAt: this.updatedAt,
+ fileType: this.fileType
}
}
+
+ get fileType() {
+ if (globals.SupportedImageTypes.includes(this.metadata.format)) return 'image'
+ if (globals.SupportedAudioTypes.includes(this.metadata.format)) return 'audio'
+ if (globals.SupportedEbookTypes.includes(this.metadata.format)) return 'ebook'
+ if (globals.TextFileTypes.includes(this.metadata.format)) return 'text'
+ if (globals.MetadataFileTypes.includes(this.metadata.format)) return 'metadata'
+ return 'unknown'
+ }
}
module.exports = LibraryFile
\ No newline at end of file
diff --git a/server/objects/AudioFile.js b/server/objects/legacy/AudioFile.js
similarity index 98%
rename from server/objects/AudioFile.js
rename to server/objects/legacy/AudioFile.js
index 629a25af..25dbf495 100644
--- a/server/objects/AudioFile.js
+++ b/server/objects/legacy/AudioFile.js
@@ -1,5 +1,5 @@
-const { isNullOrNaN } = require('../utils/index')
-const AudioFileMetadata = require('./metadata/AudioMetaTags')
+const { isNullOrNaN } = require('../../utils/index')
+const AudioFileMetadata = require('../metadata/AudioMetaTags')
class AudioFile {
constructor(data) {
diff --git a/server/objects/AudioTrack.js b/server/objects/legacy/AudioTrack.js
similarity index 98%
rename from server/objects/AudioTrack.js
rename to server/objects/legacy/AudioTrack.js
index 18f8044e..656d5eed 100644
--- a/server/objects/AudioTrack.js
+++ b/server/objects/legacy/AudioTrack.js
@@ -1,4 +1,4 @@
-var { bytesPretty } = require('../utils/fileUtils')
+var { bytesPretty } = require('../../utils/fileUtils')
class AudioTrack {
constructor(audioTrack = null) {
diff --git a/server/objects/Audiobook.js b/server/objects/legacy/Audiobook.js
similarity index 98%
rename from server/objects/Audiobook.js
rename to server/objects/legacy/Audiobook.js
index 1cbe8cda..05aa70bc 100644
--- a/server/objects/Audiobook.js
+++ b/server/objects/legacy/Audiobook.js
@@ -1,12 +1,12 @@
const Path = require('path')
const fs = require('fs-extra')
-const { bytesPretty, readTextFile, getIno } = require('../utils/fileUtils')
-const { comparePaths, getId, elapsedPretty } = require('../utils/index')
-const { parseOpfMetadataXML } = require('../utils/parseOpfMetadata')
-const { extractCoverArt } = require('../utils/ffmpegHelpers')
-const nfoGenerator = require('../utils/nfoGenerator')
-const abmetadataGenerator = require('../utils/abmetadataGenerator')
-const Logger = require('../Logger')
+const { bytesPretty, readTextFile, getIno } = require('../../utils/fileUtils')
+const { comparePaths, getId, elapsedPretty } = require('../../utils/index')
+const { parseOpfMetadataXML } = require('../../utils/parseOpfMetadata')
+const { extractCoverArt } = require('../../utils/ffmpegHelpers')
+const nfoGenerator = require('../../utils/nfoGenerator')
+const abmetadataGenerator = require('../../utils/abmetadataGenerator')
+const Logger = require('../../Logger')
const Book = require('./Book')
const AudioTrack = require('./AudioTrack')
const AudioFile = require('./AudioFile')
diff --git a/server/objects/AudiobookFile.js b/server/objects/legacy/AudiobookFile.js
similarity index 100%
rename from server/objects/AudiobookFile.js
rename to server/objects/legacy/AudiobookFile.js
diff --git a/server/objects/Author.js b/server/objects/legacy/Author.js
similarity index 94%
rename from server/objects/Author.js
rename to server/objects/legacy/Author.js
index b0aca398..04d70e4e 100644
--- a/server/objects/Author.js
+++ b/server/objects/legacy/Author.js
@@ -1,5 +1,5 @@
-const { getId } = require('../utils/index')
-const Logger = require('../Logger')
+const { getId } = require('../../utils/index')
+const Logger = require('../../Logger')
class Author {
constructor(author = null) {
diff --git a/server/objects/Book.js b/server/objects/legacy/Book.js
similarity index 99%
rename from server/objects/Book.js
rename to server/objects/legacy/Book.js
index f932cc17..39a5992c 100644
--- a/server/objects/Book.js
+++ b/server/objects/legacy/Book.js
@@ -1,6 +1,6 @@
const Path = require('path')
-const Logger = require('../Logger')
-const parseAuthors = require('../utils/parseAuthors')
+const Logger = require('../../Logger')
+const parseAuthors = require('../../utils/parseAuthors')
class Book {
constructor(book = null) {
diff --git a/server/objects/metadata/BookMetadata.js b/server/objects/metadata/BookMetadata.js
index 171e77ef..bae3c565 100644
--- a/server/objects/metadata/BookMetadata.js
+++ b/server/objects/metadata/BookMetadata.js
@@ -13,6 +13,7 @@ class BookMetadata {
this.isbn = null
this.asin = null
this.language = null
+ this.explicit = false
if (metadata) {
this.construct(metadata)
@@ -33,6 +34,7 @@ class BookMetadata {
this.isbn = metadata.isbn
this.asin = metadata.asin
this.language = metadata.language
+ this.explicit = metadata.explicit
}
toJSON() {
@@ -49,8 +51,54 @@ class BookMetadata {
description: this.description,
isbn: this.isbn,
asin: this.asin,
- language: this.language
+ language: this.language,
+ explicit: this.explicit
}
}
+
+ toJSONExpanded() {
+ return {
+ title: this.title,
+ subtitle: this.subtitle,
+ authors: this.authors.map(a => ({ ...a })), // Author JSONMinimal with name and id
+ narrators: [...this.narrators],
+ series: this.series.map(s => ({ ...s })),
+ genres: [...this.genres],
+ publishedYear: this.publishedYear,
+ publishedDate: this.publishedDate,
+ publisher: this.publisher,
+ description: this.description,
+ isbn: this.isbn,
+ asin: this.asin,
+ language: this.language,
+ explicit: this.explicit,
+ authorName: this.authorName,
+ narratorName: this.narratorName
+ }
+ }
+
+ get titleIgnorePrefix() {
+ if (!this.title) return ''
+ if (this.title.toLowerCase().startsWith('the ')) {
+ return this.title.substr(4) + ', The'
+ }
+ return this.title
+ }
+ get authorName() {
+ return this.authors.map(au => au.name).join(', ')
+ }
+ get narratorName() {
+ return this.narrators.join(', ')
+ }
+
+ hasAuthor(authorName) {
+ return !!this.authors.find(au => au.name == authorName)
+ }
+ hasSeries(seriesName) {
+ return !!this.series.find(se => se.name == seriesName)
+ }
+ hasNarrator(narratorName) {
+ return this.narrators.includes(narratorName)
+ }
}
module.exports = BookMetadata
\ No newline at end of file
diff --git a/server/objects/metadata/FileMetadata.js b/server/objects/metadata/FileMetadata.js
index 1cd55eb3..4cdd7f6f 100644
--- a/server/objects/metadata/FileMetadata.js
+++ b/server/objects/metadata/FileMetadata.js
@@ -41,5 +41,10 @@ class FileMetadata {
clone() {
return new FileMetadata(this.toJSON())
}
+
+ get format() {
+ if (!this.ext) return ''
+ return this.ext.slice(1)
+ }
}
module.exports = FileMetadata
\ No newline at end of file
diff --git a/server/objects/metadata/PodcastMetadata.js b/server/objects/metadata/PodcastMetadata.js
index 54580729..6d0b31af 100644
--- a/server/objects/metadata/PodcastMetadata.js
+++ b/server/objects/metadata/PodcastMetadata.js
@@ -9,6 +9,7 @@ class PodcastMetadata {
this.itunesPageUrl = null
this.itunesId = null
this.itunesArtistId = null
+ this.explicit = false
if (metadata) {
this.construct(metadata)
@@ -25,6 +26,7 @@ class PodcastMetadata {
this.itunesPageUrl = metadata.itunesPageUrl
this.itunesId = metadata.itunesId
this.itunesArtistId = metadata.itunesArtistId
+ this.explicit = metadata.explicit
}
toJSON() {
@@ -38,7 +40,12 @@ class PodcastMetadata {
itunesPageUrl: this.itunesPageUrl,
itunesId: this.itunesId,
itunesArtistId: this.itunesArtistId,
+ explicit: this.explicit
}
}
+
+ toJSONExpanded() {
+ return this.toJSON()
+ }
}
module.exports = PodcastMetadata
\ No newline at end of file
diff --git a/server/scanner/AudioProbeData.js b/server/scanner/AudioProbeData.js
index 05e09891..af7f063b 100644
--- a/server/scanner/AudioProbeData.js
+++ b/server/scanner/AudioProbeData.js
@@ -1,4 +1,4 @@
-const AudioFileMetadata = require('../objects/AudioFileMetadata')
+const AudioFileMetadata = require('../objects/metadata/AudioMetaTags')
class AudioProbeData {
constructor() {
diff --git a/server/scanner/Scanner.js b/server/scanner/Scanner.js
index 142440e9..5145da2d 100644
--- a/server/scanner/Scanner.js
+++ b/server/scanner/Scanner.js
@@ -10,7 +10,7 @@ const { ScanResult, LogLevel } = require('../utils/constants')
const AudioFileScanner = require('./AudioFileScanner')
const BookFinder = require('../finders/BookFinder')
-const Audiobook = require('../objects/Audiobook')
+const Audiobook = require('../objects/legacy/Audiobook')
const LibraryScan = require('./LibraryScan')
const ScanOptions = require('./ScanOptions')
diff --git a/server/utils/dbMigration.js b/server/utils/dbMigration.js
index 72a39108..c409a2b6 100644
--- a/server/utils/dbMigration.js
+++ b/server/utils/dbMigration.js
@@ -3,7 +3,7 @@ const fs = require('fs-extra')
const njodb = require("njodb")
const { SupportedEbookTypes } = require('./globals')
-const Audiobook = require('../objects/Audiobook')
+const Audiobook = require('../objects/legacy/Audiobook')
const LibraryItem = require('../objects/LibraryItem')
const Logger = require('../Logger')
@@ -142,7 +142,7 @@ function makeLibraryItemFromOldAb(audiobook) {
libraryItem.lastScan = audiobook.lastScan
libraryItem.scanVersion = audiobook.scanVersion
libraryItem.isMissing = audiobook.isMissing
- libraryItem.entityType = 'book'
+ libraryItem.mediaType = 'book'
var bookEntity = new Book()
var bookMetadata = new BookMetadata(audiobook.book)
@@ -159,8 +159,6 @@ function makeLibraryItemFromOldAb(audiobook) {
bookEntity.metadata = bookMetadata
bookEntity.coverPath = audiobook.book.coverFullPath
- // Path relative to library item
- bookEntity.relCoverPath = getRelativePath(audiobook.book.coverFullPath, audiobook.fullPath)
bookEntity.tags = [...audiobook.tags]
var payload = makeFilesFromOldAb(audiobook)
@@ -171,7 +169,7 @@ function makeLibraryItemFromOldAb(audiobook) {
bookEntity.chapters = audiobook.chapters.map(c => ({ ...c }))
}
- libraryItem.entity = bookEntity
+ libraryItem.media = bookEntity
libraryItem.libraryFiles = payload.libraryFiles
return libraryItem
}
diff --git a/server/utils/globals.js b/server/utils/globals.js
index 6c4f5a73..6c0a4d44 100644
--- a/server/utils/globals.js
+++ b/server/utils/globals.js
@@ -1,7 +1,9 @@
const globals = {
SupportedImageTypes: ['png', 'jpg', 'jpeg', 'webp'],
SupportedAudioTypes: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'mp4', 'aac'],
- SupportedEbookTypes: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz']
+ SupportedEbookTypes: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'],
+ TextFileTypes: ['txt', 'nfo'],
+ MetadataFileTypes: ['opf', 'abs']
}
module.exports = globals
diff --git a/server/utils/libraryHelpers.js b/server/utils/libraryHelpers.js
index 68eb361c..64843528 100644
--- a/server/utils/libraryHelpers.js
+++ b/server/utils/libraryHelpers.js
@@ -8,6 +8,45 @@ module.exports = {
return Buffer.from(decodeURIComponent(text), 'base64').toString()
},
+ getFilteredLibraryItems(libraryItems, filterBy, user) {
+ var filtered = libraryItems
+
+ var searchGroups = ['genres', 'tags', 'series', 'authors', 'progress', 'narrators', 'languages']
+ var group = searchGroups.find(_group => filterBy.startsWith(_group + '.'))
+ if (group) {
+ var filterVal = filterBy.replace(`${group}.`, '')
+ var filter = this.decode(filterVal)
+ if (group === 'genres') filtered = filtered.filter(li => li.media.metadata && li.media.metadata.genres.includes(filter))
+ else if (group === 'tags') filtered = filtered.filter(li => li.media.tags.includes(filter))
+ else if (group === 'series') {
+ if (filter === 'No Series') filtered = filtered.filter(li => li.media.metadata && !li.media.metadata.series.length)
+ else filtered = filtered.filter(li => li.media.metadata && li.media.metadata.hasSeries(filter))
+ }
+ else if (group === 'authors') filtered = filtered.filter(li => li.media.metadata && li.media.metadata.hasAuthor(filter))
+ else if (group === 'narrators') filtered = filtered.filter(li => li.media.metadata && li.media.metadata.hasNarrator(filter))
+ else if (group === 'progress') {
+ filtered = filtered.filter(li => {
+ var userAudiobook = user.getAudiobookJSON(li.id)
+ var isRead = userAudiobook && userAudiobook.isRead
+ if (filter === 'Read' && isRead) return true
+ if (filter === 'Unread' && !isRead) return true
+ if (filter === 'In Progress' && (userAudiobook && !userAudiobook.isRead && userAudiobook.progress > 0)) return true
+ return false
+ })
+ } else if (group === 'languages') {
+ filtered = filtered.filter(li => li.media.metadata && li.media.metadata.language === filter)
+ }
+ } else if (filterBy === 'issues') {
+ filtered = filtered.filter(ab => {
+ // TODO: Update filter for issues
+ return ab.isMissing
+ // return ab.numMissingParts || ab.numInvalidParts || ab.isMissing || ab.isInvalid
+ })
+ }
+
+ return filtered
+ },
+
getFiltered(audiobooks, filterBy, user) {
var filtered = audiobooks
@@ -45,6 +84,55 @@ module.exports = {
return filtered
},
+ getDistinctFilterDataNew(libraryItems) {
+ var data = {
+ authors: [],
+ genres: [],
+ tags: [],
+ series: [],
+ narrators: [],
+ languages: []
+ }
+ libraryItems.forEach((li) => {
+ var mediaMetadata = li.media.metadata
+ if (mediaMetadata.authors.length) {
+ mediaMetadata.authors.forEach((author) => {
+ if (author && !data.authors.includes(author.name)) data.authors.push(author.name)
+ })
+ }
+ if (mediaMetadata.series.length) {
+ mediaMetadata.series.forEach((series) => {
+ if (series && !data.series.includes(series.name)) data.series.push(series.name)
+ })
+ }
+ if (mediaMetadata.genres.length) {
+ mediaMetadata.genres.forEach((genre) => {
+ if (genre && !data.genres.includes(genre)) data.genres.push(genre)
+ })
+ }
+ if (li.media.tags.length) {
+ li.media.tags.forEach((tag) => {
+ if (tag && !data.tags.includes(tag)) data.tags.push(tag)
+ })
+ }
+ if (mediaMetadata.narrators.length) {
+ mediaMetadata.narrators.forEach((narrator) => {
+ if (narrator && !data.narrators.includes(narrator)) data.narrators.push(narrator)
+ })
+ }
+ if (mediaMetadata.language && !data.languages.includes(mediaMetadata.language)) data.languages.push(mediaMetadata.language)
+ })
+ data.authors = naturalSort(data.authors).asc()
+ data.genres = naturalSort(data.genres).asc()
+ data.tags = naturalSort(data.tags).asc()
+ data.series = naturalSort(data.series).asc()
+ data.narrators = naturalSort(data.narrators).asc()
+ data.languages = naturalSort(data.languages).asc()
+ return data
+ },
+
+
+ // TODO: Remove legacy
getDistinctFilterData(audiobooks) {
var data = {
authors: [],
@@ -246,9 +334,11 @@ module.exports = {
return totalSize
},
- getNumIssues(books) {
- return books.filter(ab => {
- return ab.numMissingParts || ab.numInvalidParts || ab.isMissing || ab.isInvalid
- }).length
+ getNumIssues(libraryItems) {
+ // TODO: Implement issues
+ return libraryItems.filter(li => li.isMissing).length
+ // return books.filter(ab => {
+ // return ab.numMissingParts || ab.numInvalidParts || ab.isMissing || ab.isInvalid
+ // }).length
}
}
\ No newline at end of file