play_circle_filled
@@ -65,7 +66,7 @@
-
+
@@ -115,7 +116,7 @@ export default {
isHovering: false,
isMoreMenuOpen: false,
isProcessingReadUpdate: false,
- audiobook: null,
+ libraryItem: null,
imageReady: false,
rescanning: false,
selected: false,
@@ -127,7 +128,7 @@ export default {
bookMount: {
handler(newVal) {
if (newVal) {
- this.audiobook = newVal
+ this.libraryItem = newVal
}
}
}
@@ -137,7 +138,7 @@ export default {
return this.store.state.showExperimentalFeatures
},
_libraryItem() {
- return this.audiobook || {}
+ return this.libraryItem || {}
},
media() {
return this._libraryItem.media || {}
@@ -161,18 +162,17 @@ export default {
return this._libraryItem.libraryId
},
hasEbook() {
- if (!this.media.ebooks) return 0
- return this.media.ebooks.length
+ return this.media.ebookFile
},
- hasAudiobook() {
- if (!this.media.audiobooks) return 0
- return this.media.audiobooks.length
+ numTracks() {
+ if (this.media.tracks) return this.media.tracks.length
+ return this.media.numTracks || 0 // toJSONMinified
},
processingBatch() {
return this.store.state.processingBatch
},
booksInSeries() {
- // Only added to audiobook object when collapseSeries is enabled
+ // Only added to item object when collapseSeries is enabled
return this._libraryItem.booksInSeries
},
hasCover() {
@@ -228,7 +228,7 @@ export default {
return null
},
userProgress() {
- return this.store.getters['user/getUserLibraryItemProgress'](this.libraryItemId)
+ return this.store.getters['user/getUserMediaProgress'](this.libraryItemId)
},
userProgressPercent() {
return this.userProgress ? this.userProgress.progress || 0 : 0
@@ -246,7 +246,7 @@ export default {
return !this.isSelectionMode && this.showExperimentalFeatures && !this.showPlayButton && this.hasEbook
},
showPlayButton() {
- return !this.isSelectionMode && !this.isMissing && !this.isInvalid && this.hasAudiobook && !this.isStreaming
+ return !this.isSelectionMode && !this.isMissing && !this.isInvalid && this.numTracks && !this.isStreaming
},
showSmallEBookIcon() {
return !this.isSelectionMode && this.showExperimentalFeatures && this.hasEbook
@@ -264,8 +264,8 @@ export default {
return this._libraryItem.hasInvalidParts
},
errorText() {
- if (this.isMissing) return 'Audiobook directory is missing!'
- else if (this.isInvalid) return 'Audiobook has no audio tracks & ebook'
+ if (this.isMissing) return 'Item directory is missing!'
+ else if (this.isInvalid) return 'Item has no audio tracks & ebook'
var txt = ''
if (this.hasMissingParts) {
txt = `${this.hasMissingParts} missing parts.`
@@ -312,7 +312,7 @@ export default {
}
]
if (this.userCanUpdate) {
- if (this.hasAudiobook) {
+ if (this.numTracks) {
items.push({
func: 'showEditModalTracks',
text: 'Tracks'
@@ -382,7 +382,7 @@ export default {
if (!val) this.selected = false
},
setEntity(libraryItem) {
- this.audiobook = libraryItem
+ this.libraryItem = libraryItem
},
clickCard(e) {
if (this.isSelectionMode) {
@@ -398,7 +398,7 @@ export default {
}
},
editClick() {
- this.$emit('edit', this.audiobook)
+ this.$emit('edit', this.libraryItem)
},
toggleFinished() {
var updatePayload = {
@@ -444,18 +444,18 @@ export default {
},
showEditModalTracks() {
// More menu func
- this.store.commit('showEditModalOnTab', { libraryItem: this.audiobook, tab: 'tracks' })
+ this.store.commit('showEditModalOnTab', { libraryItem: this.libraryItem, tab: 'tracks' })
},
showEditModalMatch() {
// More menu func
- this.store.commit('showEditModalOnTab', { libraryItem: this.audiobook, tab: 'match' })
+ this.store.commit('showEditModalOnTab', { libraryItem: this.libraryItem, tab: 'match' })
},
showEditModalDownload() {
// More menu func
- this.store.commit('showEditModalOnTab', { libraryItem: this.audiobook, tab: 'download' })
+ this.store.commit('showEditModalOnTab', { libraryItem: this.libraryItem, tab: 'download' })
},
openCollections() {
- this.store.commit('setSelectedLibraryItem', this.audiobook)
+ this.store.commit('setSelectedLibraryItem', this.libraryItem)
this.store.commit('globals/setShowUserCollectionsModal', true)
},
createMoreMenu() {
@@ -509,12 +509,12 @@ export default {
this.createMoreMenu()
},
clickReadEBook() {
- this.store.commit('showEReader', this.audiobook)
+ this.store.commit('showEReader', this.media.ebookFile)
},
selectBtnClick() {
if (this.processingBatch) return
this.selected = !this.selected
- this.$emit('select', this.audiobook)
+ this.$emit('select', this.libraryItem)
},
play() {
var eventBus = this.$eventBus || this.$nuxt.$eventBus
diff --git a/client/components/covers/BookCover.vue b/client/components/covers/BookCover.vue
index 38449b3c..069650ef 100644
--- a/client/components/covers/BookCover.vue
+++ b/client/components/covers/BookCover.vue
@@ -138,6 +138,7 @@ export default {
this.$nextTick(() => {
this.imageReady = true
})
+
if (this.$refs.cover && this.cover !== this.placeholderUrl) {
var { naturalWidth, naturalHeight } = this.$refs.cover
var aspectRatio = naturalHeight / naturalWidth
diff --git a/client/components/modals/item/tabs/Chapters.vue b/client/components/modals/item/tabs/Chapters.vue
index d9470847..fd1fd371 100644
--- a/client/components/modals/item/tabs/Chapters.vue
+++ b/client/components/modals/item/tabs/Chapters.vue
@@ -1,36 +1,33 @@
-
No Audiobooks
-
-
-
-
Audiobook Chapters ({{ audiobook.name }})
-
-
No Chapters
-
-
- Id |
- Title |
- Start |
- End |
-
-
-
- {{ chapter.id }}
- |
-
- {{ chapter.title }}
- |
-
- {{ $secondsToTimestamp(chapter.start) }}
- |
-
- {{ $secondsToTimestamp(chapter.end) }}
- |
-
-
+
+
-
+
No Chapters
+
+
+ Id |
+ Title |
+ Start |
+ End |
+
+
+
+ {{ chapter.id }}
+ |
+
+ {{ chapter.title }}
+ |
+
+ {{ $secondsToTimestamp(chapter.start) }}
+ |
+
+ {{ $secondsToTimestamp(chapter.end) }}
+ |
+
+
+
@@ -49,8 +46,8 @@ export default {
media() {
return this.libraryItem ? this.libraryItem.media || {} : {}
},
- audiobooks() {
- return this.media.audiobooks || []
+ chapters() {
+ return this.media.chapters || []
}
},
methods: {}
diff --git a/client/components/tables/TracksTable.vue b/client/components/tables/TracksTable.vue
index f05e4415..fb888619 100644
--- a/client/components/tables/TracksTable.vue
+++ b/client/components/tables/TracksTable.vue
@@ -8,7 +8,7 @@
Full Path
-
+
Manage Tracks
@@ -38,7 +38,7 @@
{{ $secondsToTimestamp(track.duration) }}
- download
+ download
|
@@ -59,7 +59,7 @@ export default {
type: Array,
default: () => []
},
- audiobookId: String
+ libraryItemId: String
},
data() {
return {
diff --git a/client/components/tables/collection/BookTableRow.vue b/client/components/tables/collection/BookTableRow.vue
index e689467f..1e26b335 100644
--- a/client/components/tables/collection/BookTableRow.vue
+++ b/client/components/tables/collection/BookTableRow.vue
@@ -82,18 +82,17 @@ export default {
mediaMetadata() {
return this.media.metadata || {}
},
+ tracks() {
+ return this.media.tracks || []
+ },
bookTitle() {
return this.mediaMetadata.title || ''
},
bookAuthor() {
return (this.mediaMetadata.authors || []).map((au) => au.name).join(', ')
},
- defaultAudiobook() {
- if (!this.media.audiobooks.length) return null
- return this.media.audiobooks[0]
- },
bookDuration() {
- return this.$secondsToTimestamp(this.defaultAudiobook.duration)
+ return this.$secondsToTimestamp(this.media.duration)
},
isMissing() {
return this.book.isMissing
@@ -105,10 +104,10 @@ export default {
return this.$store.getters['getLibraryItemIdStreaming'] === this.book.id
},
showPlayBtn() {
- return !this.isMissing && !this.isInvalid && !this.isStreaming && this.defaultAudiobook
+ return !this.isMissing && !this.isInvalid && !this.isStreaming && this.tracks.length
},
itemProgress() {
- return this.$store.getters['user/getUserLibraryItemProgress'](this.book.id)
+ return this.$store.getters['user/getUserMediaProgress'](this.book.id)
},
userIsFinished() {
return this.itemProgress ? !!this.itemProgress.isFinished : false
diff --git a/client/components/ui/TextareaInput.vue b/client/components/ui/TextareaInput.vue
index 5f5080a9..55c007e2 100644
--- a/client/components/ui/TextareaInput.vue
+++ b/client/components/ui/TextareaInput.vue
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/client/components/widgets/AudiobookData.vue b/client/components/widgets/AudiobookData.vue
index 15166416..a218c58e 100644
--- a/client/components/widgets/AudiobookData.vue
+++ b/client/components/widgets/AudiobookData.vue
@@ -16,14 +16,15 @@
-
+
diff --git a/client/players/PlayerHandler.js b/client/players/PlayerHandler.js
index 321951cd..9ee78e07 100644
--- a/client/players/PlayerHandler.js
+++ b/client/players/PlayerHandler.js
@@ -11,7 +11,6 @@ export default class PlayerHandler {
this.playerState = 'IDLE'
this.isHlsTranscode = false
this.currentSessionId = null
- this.mediaEntityId = null
this.startTime = 0
this.lastSyncTime = 0
@@ -150,7 +149,6 @@ export default class PlayerHandler {
prepareSession(session) {
this.startTime = session.currentTime
this.currentSessionId = session.id
- this.mediaEntityId = session.mediaEntityId
console.log('[PlayerHandler] Preparing Session', session)
var audioTracks = session.audioTracks.map(at => new AudioTrack(at, this.userToken))
@@ -210,7 +208,6 @@ export default class PlayerHandler {
syncData = {
timeListened: listeningTimeToAdd,
duration: this.getDuration(),
- mediaEntityId: this.mediaEntityId,
currentTime: this.getCurrentTime()
}
}
@@ -229,7 +226,6 @@ export default class PlayerHandler {
var syncData = {
timeListened: listeningTimeToAdd,
duration: this.getDuration(),
- mediaEntityId: this.mediaEntityId,
currentTime
}
this.listeningTimeSinceSync = 0
diff --git a/client/store/user.js b/client/store/user.js
index da3826ae..d53d9e3c 100644
--- a/client/store/user.js
+++ b/client/store/user.js
@@ -22,9 +22,9 @@ export const getters = {
getToken: (state) => {
return state.user ? state.user.token : null
},
- getUserLibraryItemProgress: (state) => (libraryItemId) => {
- if (!state.user.libraryItemProgress) return null
- return state.user.libraryItemProgress.find(li => li.id == libraryItemId)
+ getUserMediaProgress: (state) => (libraryItemId) => {
+ if (!state.user.mediaProgress) return null
+ return state.user.mediaProgress.find(li => li.id == libraryItemId)
},
getUserBookmarksForItem: (state) => (libraryItemId) => {
if (!state.user.bookmarks) return []
@@ -107,16 +107,16 @@ export const mutations = {
localStorage.removeItem('token')
}
},
- updateItemProgress(state, { id, data }) {
+ updateMediaProgress(state, { id, data }) {
if (!state.user) return
if (!data) {
- state.user.libraryItemProgress = state.user.libraryItemProgress.filter(lip => lip.id != id)
+ state.user.mediaProgress = state.user.mediaProgress.filter(lip => lip.id != id)
} else {
- var indexOf = state.user.libraryItemProgress.findIndex(lip => lip.id == id)
+ var indexOf = state.user.mediaProgress.findIndex(lip => lip.id == id)
if (indexOf >= 0) {
- state.user.libraryItemProgress.splice(indexOf, 1, data)
+ state.user.mediaProgress.splice(indexOf, 1, data)
} else {
- state.user.libraryItemProgress.push(data)
+ state.user.mediaProgress.push(data)
}
}
},
diff --git a/docs/SampleBookLibraryItem.js b/docs/SampleBookLibraryItem.js
index eca73ab0..385107dc 100644
--- a/docs/SampleBookLibraryItem.js
+++ b/docs/SampleBookLibraryItem.js
@@ -53,106 +53,88 @@ new LibraryItem({
language: 'english',
explicit: false
},
- audiobooks: [
- { // Audiobook.js
- id: 'au_289374asf0a98',
+ audioFiles: [
+ { // AudioFile.js
+ ino: "55450570412017066",
index: 1,
- name: 'default',
- audioFiles: [
- { // AudioFile.js
- ino: "55450570412017066",
- index: 1,
- metadata: { // FileMetadata.js
- filename: 'audiofile.mp3',
- ext: '.mp3',
- path: '/audiobooks/Terry Goodkind/Sword of Truth/1 - Wizards First Rule/CD01/audiofile.mp3',
- relPath: '/CD01/audiofile.mp3',
- mtimeMs: 1646784672127,
- ctimeMs: 1646784672127,
- birthtimeMs: 1646784672127,
- size: 1197449516
- },
- trackNumFromMeta: 1,
- discNumFromMeta: null,
- trackNumFromFilename: null,
- discNumFromFilename: 1,
- manuallyVerified: false,
- exclude: false,
- invalid: false,
- format: "MP2/3 (MPEG audio layer 2/3)",
- duration: 2342342,
- bitRate: 324234,
- language: null,
- codec: 'mp3',
- timeBase: "1/14112000",
- channels: 1,
- channelLayout: "mono",
- chapters: [],
- embeddedCoverArt: 'jpeg', // Video stream codec ['mjpeg', 'jpeg', 'png'] or null
- metaTags: { // AudioMetaTags.js
- tagAlbum: '',
- tagArtist: '',
- tagGenre: '',
- tagTitle: '',
- tagSeries: '',
- tagSeriesPart: '',
- tagTrack: '',
- tagDisc: '',
- tagSubtitle: '',
- tagAlbumArtist: '',
- tagDate: '',
- tagComposer: '',
- tagPublisher: '',
- tagComment: '',
- tagDescription: '',
- tagEncoder: '',
- tagEncodedBy: '',
- tagIsbn: '',
- tagLanguage: '',
- tagASIN: ''
- },
- addedAt: 1646784672127,
- updatedAt: 1646784672127
- }
- ],
- chapters: [
- {
- id: 0,
- title: 'Chapter 01',
- start: 0,
- end: 2467.753
- }
- ],
- missingParts: [4, 10], // Array of missing parts in tracklist
- addedAt: 1646784672127,
- updatedAt: 1646784672127
- }
- ],
- ebooks: [
- { // EBook.js
- id: 'eb_289374asf0a98',
- index: 1,
- name: 'default',
- ebookFile: { // EBookFile.js
- ino: "55450570412017066",
- metadata: { // FileMetadata.js
- filename: 'ebookfile.mobi',
- ext: '.mobi',
- path: '/audiobooks/Terry Goodkind/Sword of Truth/1 - Wizards First Rule/ebookfile.mobi',
- relPath: '/ebookfile.mobi',
- mtimeMs: 1646784672127,
- ctimeMs: 1646784672127,
- birthtimeMs: 1646784672127,
- size: 1197449516
- },
- ebookFormat: 'mobi',
- addedAt: 1646784672127,
- updatedAt: 1646784672127
+ metadata: { // FileMetadata.js
+ filename: 'audiofile.mp3',
+ ext: '.mp3',
+ path: '/audiobooks/Terry Goodkind/Sword of Truth/1 - Wizards First Rule/CD01/audiofile.mp3',
+ relPath: '/CD01/audiofile.mp3',
+ mtimeMs: 1646784672127,
+ ctimeMs: 1646784672127,
+ birthtimeMs: 1646784672127,
+ size: 1197449516
+ },
+ trackNumFromMeta: 1,
+ discNumFromMeta: null,
+ trackNumFromFilename: null,
+ discNumFromFilename: 1,
+ manuallyVerified: false,
+ exclude: false,
+ invalid: false,
+ format: "MP2/3 (MPEG audio layer 2/3)",
+ duration: 2342342,
+ bitRate: 324234,
+ language: null,
+ codec: 'mp3',
+ timeBase: "1/14112000",
+ channels: 1,
+ channelLayout: "mono",
+ chapters: [],
+ embeddedCoverArt: 'jpeg', // Video stream codec ['mjpeg', 'jpeg', 'png'] or null
+ metaTags: { // AudioMetaTags.js
+ tagAlbum: '',
+ tagArtist: '',
+ tagGenre: '',
+ tagTitle: '',
+ tagSeries: '',
+ tagSeriesPart: '',
+ tagTrack: '',
+ tagDisc: '',
+ tagSubtitle: '',
+ tagAlbumArtist: '',
+ tagDate: '',
+ tagComposer: '',
+ tagPublisher: '',
+ tagComment: '',
+ tagDescription: '',
+ tagEncoder: '',
+ tagEncodedBy: '',
+ tagIsbn: '',
+ tagLanguage: '',
+ tagASIN: ''
},
addedAt: 1646784672127,
updatedAt: 1646784672127
}
- ]
+ ],
+ chapters: [
+ {
+ id: 0,
+ title: 'Chapter 01',
+ start: 0,
+ end: 2467.753
+ }
+ ],
+ missingParts: [4, 10], // Array of missing parts in tracklist
+ ebookFile: { // EBookFile.js
+ ino: "55450570412017066",
+ metadata: { // FileMetadata.js
+ filename: 'ebookfile.mobi',
+ ext: '.mobi',
+ path: '/audiobooks/Terry Goodkind/Sword of Truth/1 - Wizards First Rule/ebookfile.mobi',
+ relPath: '/ebookfile.mobi',
+ mtimeMs: 1646784672127,
+ ctimeMs: 1646784672127,
+ birthtimeMs: 1646784672127,
+ size: 1197449516
+ },
+ ebookFormat: 'mobi',
+ addedAt: 1646784672127,
+ updatedAt: 1646784672127
+ }
},
libraryFiles: [
{ // LibraryFile.js
diff --git a/server/Server.js b/server/Server.js
index 5880d46a..a9369a0b 100644
--- a/server/Server.js
+++ b/server/Server.js
@@ -125,7 +125,7 @@ class Server {
this.auth.init()
- await this.checkUserLibraryItemProgress() // Remove invalid user item progress
+ await this.checkUserMediaProgress() // Remove invalid user item progress
await this.purgeMetadata() // Remove metadata folders without library item
await this.backupManager.init()
@@ -299,16 +299,17 @@ class Server {
return purged
}
- // Remove user library item progress entries that dont have a library item
- async checkUserLibraryItemProgress() {
+ // Remove user media progress entries that dont have a library item
+ // TODO: Check podcast episode exists still
+ async checkUserMediaProgress() {
for (let i = 0; i < this.db.users.length; i++) {
var _user = this.db.users[i]
- if (_user.libraryItemProgress) {
- var itemProgressIdsToRemove = _user.libraryItemProgress.map(lip => lip.id).filter(lipId => !this.db.libraryItems.find(_li => _li.id == lipId))
+ if (_user.mediaProgress) {
+ var itemProgressIdsToRemove = _user.mediaProgress.map(lip => lip.id).filter(lipId => !this.db.libraryItems.find(_li => _li.id == lipId))
if (itemProgressIdsToRemove.length) {
- Logger.debug(`[Server] Found ${itemProgressIdsToRemove.length} library item progress data to remove from user ${_user.username}`)
+ Logger.debug(`[Server] Found ${itemProgressIdsToRemove.length} media progress data to remove from user ${_user.username}`)
for (const lipId of itemProgressIdsToRemove) {
- _user.removeLibraryItemProgress(lipId)
+ _user.removeMediaProgress(lipId)
}
await this.db.updateEntity('user', _user)
}
@@ -378,14 +379,14 @@ class Server {
var session = this.playbackSessionManager.getUserSession(user.id)
if (session) {
Logger.debug(`[Server] User Online "${client.user.username}" with session open "${session.id}"`)
- session = session.toJSONForClient()
var sessionLibraryItem = this.db.libraryItems.find(li => li.id === session.libraryItemId)
if (!sessionLibraryItem) {
Logger.error(`[Server] Library Item for session "${session.id}" does not exist "${session.libraryItemId}"`)
this.playbackSessionManager.removeSession(session.id)
session = null
- } else {
- session.libraryItem = sessionLibraryItem.toJSONExpanded()
+ }
+ if (session) {
+ session = session.toJSONForClient(sessionLibraryItem)
}
} else {
Logger.debug(`[Server] User Online ${client.user.username}`)
diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js
index 09708619..635f8557 100644
--- a/server/controllers/LibraryController.js
+++ b/server/controllers/LibraryController.js
@@ -138,11 +138,7 @@ class LibraryController {
// api/libraries/:id/items
// TODO: Optimize this method, items are iterated through several times but can be combined
getLibraryItems(req, res) {
- var media = req.query.media || 'all'
- var libraryItems = req.libraryItems.filter(li => {
- if (media != 'all') return li.mediaType == media
- return true
- })
+ var libraryItems = req.libraryItems
var payload = {
results: [],
total: libraryItems.length,
@@ -151,7 +147,7 @@ class LibraryController {
sortBy: req.query.sort,
sortDesc: req.query.desc === '1',
filterBy: req.query.filter,
- media,
+ mediaType: req.library.mediaType,
minified: req.query.minified === '1',
collapseseries: req.query.collapseseries === '1'
}
diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js
index 483a8bbb..c6cac9a9 100644
--- a/server/controllers/LibraryItemController.js
+++ b/server/controllers/LibraryItemController.js
@@ -164,13 +164,26 @@ class LibraryItemController {
// POST: api/items/:id/play
startPlaybackSession(req, res) {
- var playbackMediaEntity = req.libraryItem.getPlaybackMediaEntity()
- if (!playbackMediaEntity) {
- Logger.error(`[LibraryItemController] startPlaybackSession no playback media entity ${req.libraryItem.id}`)
+ if (!req.libraryItem.media.numTracks) {
+ Logger.error(`[LibraryItemController] startPlaybackSession cannot playback ${req.libraryItem.id}`)
return res.sendStatus(404)
}
const options = req.body || {}
- this.playbackSessionManager.startSessionRequest(req.user, req.libraryItem, playbackMediaEntity, options, res)
+ this.playbackSessionManager.startSessionRequest(req.user, req.libraryItem, options, res)
+ }
+
+ // PATCH: api/items/:id/tracks
+ async updateTracks(req, res) {
+ var libraryItem = req.libraryItem
+ var orderedFileData = req.body.orderedFileData
+ if (!libraryItem.media.updateAudioTracks) {
+ Logger.error(`[LibraryItemController] updateTracks invalid media type ${libraryItem.id}`)
+ return res.sendStatus(500)
+ }
+ libraryItem.media.updateAudioTracks(orderedFileData)
+ await this.db.updateLibraryItem(libraryItem)
+ this.emitter('item_updated', libraryItem.toJSONExpanded())
+ res.json(libraryItem.toJSON())
}
// POST api/items/:id/match
diff --git a/server/controllers/MeController.js b/server/controllers/MeController.js
index 8f88ed4e..acace5b0 100644
--- a/server/controllers/MeController.js
+++ b/server/controllers/MeController.js
@@ -17,8 +17,8 @@ class MeController {
}
// DELETE: api/me/progress/:id
- async removeLibraryItemProgress(req, res) {
- var wasRemoved = req.user.removeLibraryItemProgress(req.params.id)
+ async removeMediaProgress(req, res) {
+ var wasRemoved = req.user.removeMediaProgress(req.params.id)
if (!wasRemoved) {
return res.sendStatus(200)
}
@@ -30,12 +30,12 @@ class MeController {
}
// PATCH: api/me/progress/:id
- async createUpdateLibraryItemProgress(req, res) {
+ async createUpdateMediaProgress(req, res) {
var libraryItem = this.db.libraryItems.find(ab => ab.id === req.params.id)
if (!libraryItem) {
return res.status(404).send('Item not found')
}
- var wasUpdated = req.user.createUpdateLibraryItemProgress(libraryItem, req.body)
+ var wasUpdated = req.user.createUpdateMediaProgress(libraryItem, req.body)
if (wasUpdated) {
await this.db.updateEntity('user', req.user)
this.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
@@ -44,7 +44,7 @@ class MeController {
}
// PATCH: api/me/progress/batch/update
- async batchUpdateLibraryItemProgress(req, res) {
+ async batchUpdateMediaProgress(req, res) {
var itemProgressPayloads = req.body
if (!itemProgressPayloads || !itemProgressPayloads.length) {
return res.sendStatus(500)
@@ -54,10 +54,10 @@ class MeController {
itemProgressPayloads.forEach((itemProgress) => {
var libraryItem = this.db.libraryItems.find(li => li.id === itemProgress.id) // Make sure this library item exists
if (libraryItem) {
- var wasUpdated = req.user.createUpdateLibraryItemProgress(libraryItem, itemProgress)
+ var wasUpdated = req.user.createUpdateMediaProgress(libraryItem, itemProgress)
if (wasUpdated) shouldUpdate = true
} else {
- Logger.error(`[MeController] batchUpdateLibraryItemProgress: Library Item does not exist ${itemProgress.id}`)
+ Logger.error(`[MeController] batchUpdateMediaProgress: Library Item does not exist ${itemProgress.id}`)
}
})
diff --git a/server/controllers/MediaEntityController.js b/server/controllers/MediaEntityController.js
deleted file mode 100644
index 9bbf271a..00000000
--- a/server/controllers/MediaEntityController.js
+++ /dev/null
@@ -1,71 +0,0 @@
-const Logger = require('../Logger')
-
-class MediaEntityController {
- constructor() { }
-
- async findOne(req, res) {
- if (req.query.expanded == 1) return res.json(req.mediaEntity.toJSONExpanded())
- return res.json(req.mediaEntity)
- }
-
- async findWithItem(req, res) {
- if (req.query.expanded == 1) {
- return res.json({
- libraryItem: req.libraryItem.toJSONExpanded(),
- mediaEntity: req.mediaEntity.toJSONExpanded()
- })
- }
- res.json({
- libraryItem: req.libraryItem.toJSON(),
- mediaEntity: req.mediaEntity.toJSON()
- })
- }
-
- // PATCH: api/entities/:id/tracks
- async updateTracks(req, res) {
- var libraryItem = req.libraryItem
- var mediaEntity = req.mediaEntity
- var orderedFileData = req.body.orderedFileData
- if (!mediaEntity.updateAudioTracks) {
- Logger.error(`[MediaEntityController] updateTracks invalid media entity ${mediaEntity.id}`)
- return res.sendStatus(500)
- }
- mediaEntity.updateAudioTracks(orderedFileData)
- await this.db.updateLibraryItem(libraryItem)
- this.emitter('item_updated', libraryItem.toJSONExpanded())
- res.json(libraryItem.toJSON())
- }
-
- // POST: api/entities/:id/play
- startPlaybackSession(req, res) {
- if (!req.mediaEntity.isPlaybackMediaEntity) {
- Logger.error(`[MediaEntityController] startPlaybackSession invalid media entity ${req.mediaEntity.id}`)
- return res.sendStatus(500)
- }
- const options = req.body || {}
- this.playbackSessionManager.startSessionRequest(req.user, req.libraryItem, req.mediaEntity, options, res)
- }
-
- middleware(req, res, next) {
- var mediaEntity = null
- var libraryItem = this.db.libraryItems.find(li => {
- if (li.mediaType != 'book') return false
- mediaEntity = li.media.getMediaEntityById(req.params.id)
- return !!mediaEntity
- })
- if (!mediaEntity) return res.sendStatus(404)
-
- if (req.method == 'DELETE' && !req.user.canDelete) {
- Logger.warn(`[MediaEntityController] User attempted to delete without permission`, req.user)
- return res.sendStatus(403)
- } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) {
- Logger.warn('[MediaEntityController] User attempted to update without permission', req.user)
- return res.sendStatus(403)
- }
-
- req.mediaEntity = mediaEntity
- req.libraryItem = libraryItem
- next()
- }
-}
-module.exports = new MediaEntityController()
\ No newline at end of file
diff --git a/server/managers/PlaybackSessionManager.js b/server/managers/PlaybackSessionManager.js
index 4a3866d9..e319b2ee 100644
--- a/server/managers/PlaybackSessionManager.js
+++ b/server/managers/PlaybackSessionManager.js
@@ -25,14 +25,16 @@ class PlaybackSessionManager {
return session ? session.stream : null
}
- async startSessionRequest(user, libraryItem, mediaEntity, options, res) {
- const session = await this.startSession(user, libraryItem, mediaEntity, options)
- res.json(session.toJSONForClient())
+ async startSessionRequest(user, libraryItem, options, res) {
+ const session = await this.startSession(user, libraryItem, options)
+ res.json(session.toJSONForClient(libraryItem))
}
async syncSessionRequest(user, session, payload, res) {
- await this.syncSession(user, session, payload)
- res.json(session.toJSONForClient())
+ var result = await this.syncSession(user, session, payload)
+ if (result) {
+ res.json(session.toJSONForClient(result.libraryItem))
+ }
}
async closeSessionRequest(user, session, syncData, res) {
@@ -40,23 +42,23 @@ class PlaybackSessionManager {
res.sendStatus(200)
}
- async startSession(user, libraryItem, mediaEntity, options) {
- var shouldDirectPlay = options.forceDirectPlay || (!options.forceTranscode && mediaEntity.checkCanDirectPlay(options))
+ async startSession(user, libraryItem, options) {
+ var shouldDirectPlay = options.forceDirectPlay || (!options.forceTranscode && libraryItem.media.checkCanDirectPlay(options))
- const userProgress = user.getLibraryItemProgress(libraryItem.id)
+ const userProgress = user.getMediaProgress(libraryItem.id)
var userStartTime = 0
if (userProgress) userStartTime = userProgress.currentTime || 0
const newPlaybackSession = new PlaybackSession()
- newPlaybackSession.setData(libraryItem, mediaEntity, user)
+ newPlaybackSession.setData(libraryItem, user)
var audioTracks = []
if (shouldDirectPlay) {
- Logger.debug(`[PlaybackSessionManager] "${user.username}" starting direct play session for media entity "${mediaEntity.id}"`)
- audioTracks = mediaEntity.getDirectPlayTracklist(libraryItem.id)
+ Logger.debug(`[PlaybackSessionManager] "${user.username}" starting direct play session for item "${libraryItem.id}"`)
+ audioTracks = libraryItem.getDirectPlayTracklist(libraryItem.id)
newPlaybackSession.playMethod = PlayMethod.DIRECTPLAY
} else {
- Logger.debug(`[PlaybackSessionManager] "${user.username}" starting stream session for media entity "${mediaEntity.id}"`)
- var stream = new Stream(newPlaybackSession.id, this.StreamsPath, user, libraryItem, mediaEntity, userStartTime, this.clientEmitter.bind(this))
+ Logger.debug(`[PlaybackSessionManager] "${user.username}" starting stream session for item "${libraryItem.id}"`)
+ var stream = new Stream(newPlaybackSession.id, this.StreamsPath, user, libraryItem, userStartTime, this.clientEmitter.bind(this))
await stream.generatePlaylist()
audioTracks = [stream.getAudioTrack()]
newPlaybackSession.stream = stream
@@ -83,7 +85,7 @@ class PlaybackSessionManager {
var libraryItem = this.db.libraryItems.find(li => li.id === session.libraryItemId)
if (!libraryItem) {
Logger.error(`[PlaybackSessionManager] syncSession Library Item not found "${sessino.libraryItemId}"`)
- return
+ return null
}
session.currentTime = syncData.currentTime
@@ -91,21 +93,23 @@ class PlaybackSessionManager {
Logger.debug(`[PlaybackSessionManager] syncSession "${session.id}" | Total Time Listened: ${session.timeListening}`)
const itemProgressUpdate = {
- mediaEntityId: syncData.mediaEntityId || null,
duration: syncData.duration,
currentTime: syncData.currentTime,
progress: session.progress
}
- var wasUpdated = user.createUpdateLibraryItemProgress(libraryItem, itemProgressUpdate)
+ var wasUpdated = user.createUpdateMediaProgress(libraryItem, itemProgressUpdate)
if (wasUpdated) {
await this.db.updateEntity('user', user)
- var itemProgress = user.getLibraryItemProgress(session.libraryItemId)
+ var itemProgress = user.getMediaProgress(session.libraryItemId)
this.clientEmitter(user.id, 'user_item_progress_updated', {
id: itemProgress.id,
data: itemProgress.toJSON()
})
}
this.saveSession(session)
+ return {
+ libraryItem
+ }
}
async closeSession(user, session, syncData = null) {
diff --git a/server/managers/PodcastManager.js b/server/managers/PodcastManager.js
index 1ef55df8..60fad0d9 100644
--- a/server/managers/PodcastManager.js
+++ b/server/managers/PodcastManager.js
@@ -1,4 +1,8 @@
const fs = require('fs-extra')
+const cron = require('node-cron')
+const axios = require('axios')
+
+const { parsePodcastRssFeedXml } = require('../utils/podcastUtils')
const Logger = require('../Logger')
const { downloadFile } = require('../utils/fileUtils')
@@ -16,6 +20,15 @@ class PodcastManager {
this.downloadQueue = []
this.currentDownload = null
+
+ this.episodeScheduleTask = null
+ }
+
+ init() {
+ var podcastsWithAutoDownload = this.db.libraryItems.find(li => li.mediaType === 'podcast' && li.media.autoDownloadEpisodes)
+ if (podcastsWithAutoDownload.length) {
+ this.schedulePodcastEpisodeCron()
+ }
}
async downloadPodcastEpisodes(libraryItem, episodesToDownload) {
@@ -97,5 +110,37 @@ class PodcastManager {
newAudioFile.setDataFromProbe(libraryFile, audioProbeData)
return newAudioFile
}
+
+ schedulePodcastEpisodeCron() {
+ try {
+ this.episodeScheduleTask = cron.schedule(this.serverSettings.podcastEpisodeSchedule, this.checkForNewEpisodes.bind(this))
+ } catch (error) {
+ Logger.error(`[PodcastManager] Failed to schedule podcast cron ${this.serverSettings.backupSchedule}`, error)
+ }
+ }
+
+ checkForNewEpisodes() {
+ var podcastsWithAutoDownload = this.db.libraryItems.find(li => li.mediaType === 'podcast' && li.media.autoDownloadEpisodes)
+ for (const libraryItem of podcastsWithAutoDownload) {
+
+ }
+ }
+
+ getPodcastFeed(podcastMedia) {
+ axios.get(podcastMedia.feedUrl).then(async (data) => {
+ if (!data || !data.data) {
+ Logger.error('Invalid podcast feed request response')
+ return res.status(500).send('Bad response from feed request')
+ }
+ var podcast = await parsePodcastRssFeedXml(data.data)
+ if (!podcast) {
+ return res.status(500).send('Invalid podcast RSS feed')
+ }
+ res.json(podcast)
+ }).catch((error) => {
+ console.error('Failed', error)
+ res.status(500).send(error)
+ })
+ }
}
module.exports = PodcastManager
\ No newline at end of file
diff --git a/server/objects/LibraryItem.js b/server/objects/LibraryItem.js
index e39b1a1d..0c7f9b1c 100644
--- a/server/objects/LibraryItem.js
+++ b/server/objects/LibraryItem.js
@@ -358,8 +358,8 @@ class LibraryItem {
return true
})
if (filesRemoved.length) {
- if (this.media.audiobooks && this.media.audiobooks.length) {
- this.media.audiobooks.forEach(ab => ab.checkUpdateMissingTracks())
+ if (this.media.mediaType === 'book') {
+ this.media.checkUpdateMissingTracks()
}
hasUpdated = true
}
@@ -404,17 +404,14 @@ class LibraryItem {
var hasUpdated = false
if (this.mediaType === 'book') {
- // Add/update ebook files (ebooks that were removed are removed in checkScanData)
+ // Add/update ebook file (ebooks that were removed are removed in checkScanData)
this.libraryFiles.forEach((lf) => {
if (lf.fileType === 'ebook') {
- var existingFile = this.media.findFileWithInode(lf.ino)
- if (!existingFile) {
- this.media.addEbookFile(lf)
+ if (!this.media.ebookFile) {
+ this.media.setEbookFile(lf)
+ hasUpdated = true
+ } else if (this.media.ebookFile.ino == lf.ino && this.media.ebookFile.updateFromLibraryFile(lf)) { // Update existing ebookFile
hasUpdated = true
- } else if (existingFile.ebookFormat) {
- if (existingFile.updateFromLibraryFile(lf)) {// EBookFile.js
- hasUpdated = true
- }
}
}
})
@@ -447,8 +444,8 @@ class LibraryItem {
return this.media.searchQuery(query)
}
- getPlaybackMediaEntity() {
- return this.media.getPlaybackMediaEntity()
+ getDirectPlayTracklist(libraryItemId) {
+ return this.media.getDirectPlayTracklist(libraryItemId)
}
}
module.exports = LibraryItem
\ No newline at end of file
diff --git a/server/objects/PlaybackSession.js b/server/objects/PlaybackSession.js
index 6f191e13..e1b0e8dc 100644
--- a/server/objects/PlaybackSession.js
+++ b/server/objects/PlaybackSession.js
@@ -9,10 +9,10 @@ class PlaybackSession {
this.id = null
this.userId = null
this.libraryItemId = null
- this.mediaEntityId = null
this.mediaType = null
this.mediaMetadata = null
+ this.coverPath = null
this.duration = null
this.playMethod = null
@@ -41,9 +41,9 @@ class PlaybackSession {
sessionType: this.sessionType,
userId: this.userId,
libraryItemId: this.libraryItemId,
- mediaEntityId: this.mediaEntityId,
mediaType: this.mediaType,
mediaMetadata: this.mediaMetadata ? this.mediaMetadata.toJSON() : null,
+ coverPath: this.coverPath,
duration: this.duration,
playMethod: this.playMethod,
date: this.date,
@@ -54,15 +54,15 @@ class PlaybackSession {
}
}
- toJSONForClient() {
+ toJSONForClient(libraryItem) {
return {
id: this.id,
sessionType: this.sessionType,
userId: this.userId,
libraryItemId: this.libraryItemId,
- mediaEntityId: this.mediaEntityId,
mediaType: this.mediaType,
mediaMetadata: this.mediaMetadata ? this.mediaMetadata.toJSON() : null,
+ coverPath: this.coverPath,
duration: this.duration,
playMethod: this.playMethod,
date: this.date,
@@ -71,7 +71,8 @@ class PlaybackSession {
lastUpdate: this.lastUpdate,
updatedAt: this.updatedAt,
audioTracks: this.audioTracks.map(at => at.toJSON()),
- currentTime: this.currentTime
+ currentTime: this.currentTime,
+ libraryItem: libraryItem.toJSONExpanded()
}
}
@@ -80,7 +81,6 @@ class PlaybackSession {
this.sessionType = session.sessionType
this.userId = session.userId
this.libraryItemId = session.libraryItemId
- this.mediaEntityId = session.mediaEntityId
this.mediaType = session.mediaType
this.duration = session.duration
this.playMethod = session.playMethod
@@ -93,7 +93,7 @@ class PlaybackSession {
this.mediaMetadata = new PodcastMetadata(session.mediaMetadata)
}
}
-
+ this.coverPath = session.coverPath
this.date = session.date
this.dayOfWeek = session.dayOfWeek
@@ -107,14 +107,14 @@ class PlaybackSession {
return Math.max(0, Math.min(this.currentTime / this.duration, 1))
}
- setData(libraryItem, mediaEntity, user) {
+ setData(libraryItem, user) {
this.id = getId('play')
this.userId = user.id
this.libraryItemId = libraryItem.id
- this.mediaEntityId = mediaEntity.id
this.mediaType = libraryItem.mediaType
this.mediaMetadata = libraryItem.media.metadata.clone()
- this.duration = mediaEntity.duration
+ this.coverPath = libraryItem.media.coverPath
+ this.duration = libraryItem.media.duration
this.timeListening = 0
this.date = date.format(new Date(), 'YYYY-MM-DD')
diff --git a/server/objects/ServerSettings.js b/server/objects/ServerSettings.js
index 09e3eb7f..d76344ef 100644
--- a/server/objects/ServerSettings.js
+++ b/server/objects/ServerSettings.js
@@ -39,6 +39,9 @@ class ServerSettings {
this.coverAspectRatio = BookCoverAspectRatio.SQUARE
this.bookshelfView = BookshelfView.STANDARD
+ // Podcasts
+ this.podcastEpisodeSchedule = '0 * * * *' // Every hour
+
this.sortingIgnorePrefix = false
this.chromecastEnabled = false
this.logLevel = Logger.logLevel
diff --git a/server/objects/Stream.js b/server/objects/Stream.js
index 9f1a39fe..1abb592c 100644
--- a/server/objects/Stream.js
+++ b/server/objects/Stream.js
@@ -9,13 +9,12 @@ const hlsPlaylistGenerator = require('../utils/hlsPlaylistGenerator')
const AudioTrack = require('./files/AudioTrack')
class Stream extends EventEmitter {
- constructor(sessionId, streamPath, user, libraryItem, mediaEntity, startTime, clientEmitter, transcodeOptions = {}) {
+ constructor(sessionId, streamPath, user, libraryItem, startTime, clientEmitter, transcodeOptions = {}) {
super()
this.id = sessionId
this.user = user
this.libraryItem = libraryItem
- this.mediaEntity = mediaEntity
this.clientEmitter = clientEmitter
this.transcodeOptions = transcodeOptions
@@ -46,17 +45,12 @@ class Stream extends EventEmitter {
get mediaTitle() {
return this.libraryItem.media.metadata.title || ''
}
- get mediaEntityName() {
- return this.mediaEntity.name
- }
- get itemTitle() {
- return `${this.mediaTitle} (${this.mediaEntityName})`
- }
get totalDuration() {
- return this.mediaEntity.duration
+ return this.libraryItem.media.duration
}
get tracks() {
- return this.mediaEntity.tracks
+ // TODO: Podcast episode tracks
+ return this.libraryItem.media.tracks
}
get tracksAudioFileType() {
if (!this.tracks.length) return null
@@ -226,7 +220,7 @@ class Stream extends EventEmitter {
if (!this.isTranscodeComplete) {
this.checkFiles()
} else {
- Logger.info(`[Stream] ${this.itemTitle} sending stream_ready`)
+ Logger.info(`[Stream] ${this.mediaTitle} sending stream_ready`)
this.clientEmit('stream_ready')
clearInterval(intervalId)
}
@@ -414,7 +408,7 @@ class Stream extends EventEmitter {
getAudioTrack() {
var newAudioTrack = new AudioTrack()
- newAudioTrack.setFromStream(this.itemTitle, this.totalDuration, this.clientPlaylistUri)
+ newAudioTrack.setFromStream(this.mediaTitle, this.totalDuration, this.clientPlaylistUri)
return newAudioTrack
}
}
diff --git a/server/objects/entities/PodcastEpisode.js b/server/objects/entities/PodcastEpisode.js
index df22ac39..08a794e6 100644
--- a/server/objects/entities/PodcastEpisode.js
+++ b/server/objects/entities/PodcastEpisode.js
@@ -59,7 +59,6 @@ class PodcastEpisode {
}
}
- get isPlaybackMediaEntity() { return true }
get tracks() {
return [this.audioFile]
}
diff --git a/server/objects/mediaTypes/Book.js b/server/objects/mediaTypes/Book.js
index 966c59ed..00a5090d 100644
--- a/server/objects/mediaTypes/Book.js
+++ b/server/objects/mediaTypes/Book.js
@@ -5,10 +5,9 @@ const abmetadataGenerator = require('../../utils/abmetadataGenerator')
const { areEquivalent, copyValue } = require('../../utils/index')
const { parseOpfMetadataXML } = require('../../utils/parseOpfMetadata')
const { readTextFile } = require('../../utils/fileUtils')
-
+const AudioFile = require('../files/AudioFile')
+const AudioTrack = require('../files/AudioTrack')
const EBookFile = require('../files/EBookFile')
-const Audiobook = require('../entities/Audiobook')
-const EBook = require('../entities/EBook')
class Book {
constructor(book) {
@@ -17,8 +16,10 @@ class Book {
this.coverPath = null
this.tags = []
- this.audiobooks = []
- this.ebooks = []
+ this.audioFiles = []
+ this.chapters = []
+ this.missingParts = []
+ this.ebookFile = null
this.lastCoverSearch = null
this.lastCoverSearchQuery = null
@@ -32,8 +33,10 @@ class Book {
this.metadata = new BookMetadata(book.metadata)
this.coverPath = book.coverPath
this.tags = [...book.tags]
- this.audiobooks = book.audiobooks.map(ab => new Audiobook(ab))
- this.ebooks = book.ebooks.map(eb => new EBook(eb))
+ this.audioFiles = book.audioFiles.map(f => new AudioFile(f))
+ this.chapters = book.chapters.map(c => ({ ...c }))
+ this.missingParts = book.missingParts ? [...book.missingParts] : []
+ this.ebookFile = book.ebookFile ? new EBookFile(book.ebookFile) : null
this.lastCoverSearch = book.lastCoverSearch || null
this.lastCoverSearchQuery = book.lastCoverSearchQuery || null
}
@@ -43,8 +46,10 @@ class Book {
metadata: this.metadata.toJSON(),
coverPath: this.coverPath,
tags: [...this.tags],
- audiobooks: this.audiobooks.map(ab => ab.toJSON()),
- ebooks: this.ebooks.map(eb => eb.toJSON())
+ audioFiles: this.audioFiles.map(f => f.toJSON()),
+ chapters: this.chapters.map(c => ({ ...c })),
+ missingParts: [...this.missingParts],
+ ebookFile: this.ebookFile ? this.ebookFile.toJSON() : null
}
}
@@ -53,9 +58,13 @@ class Book {
metadata: this.metadata.toJSON(),
coverPath: this.coverPath,
tags: [...this.tags],
- audiobooks: this.audiobooks.map(ab => ab.toJSONMinified()),
- ebooks: this.ebooks.map(eb => eb.toJSONMinified()),
- size: this.size
+ numTracks: this.tracks.length,
+ numAudioFiles: this.audioFiles.length,
+ numChapters: this.chapters.length,
+ numMissingParts: this.missingParts.length,
+ duration: this.duration,
+ size: this.size,
+ ebookFormat: this.ebookFile ? this.ebookFile.ebookFormat : null
}
}
@@ -64,24 +73,26 @@ class Book {
metadata: this.metadata.toJSONExpanded(),
coverPath: this.coverPath,
tags: [...this.tags],
- audiobooks: this.audiobooks.map(ab => ab.toJSONExpanded()),
- ebooks: this.ebooks.map(eb => eb.toJSONExpanded()),
+ audioFiles: this.audioFiles.map(f => f.toJSON()),
+ chapters: this.chapters.map(c => ({ ...c })),
+ duration: this.duration,
size: this.size,
+ tracks: this.tracks.map(t => t.toJSON()),
+ missingParts: [...this.missingParts],
+ ebookFile: this.ebookFile ? this.ebookFile.toJSON() : null
}
}
get size() {
var total = 0
- this.audiobooks.forEach((ab) => {
- total += ab.size
- })
- this.ebooks.forEach((eb) => {
- total += eb.size
- })
+ this.audioFiles.forEach((af) => total += af.metadata.size)
+ if (this.ebookFile) {
+ total += this.ebookFile.metadata.size
+ }
return total
}
get hasMediaEntities() {
- return !!(this.audiobooks.length + this.ebooks.length)
+ return !!this.tracks.length || this.ebookFile
}
get shouldSearchForCover() {
if (this.coverPath) return false
@@ -89,10 +100,22 @@ class Book {
return (Date.now() - this.lastCoverSearch) > 1000 * 60 * 60 * 24 * 7 // 7 day
}
get hasEmbeddedCoverArt() {
- return this.audiobooks.some(ab => ab.hasEmbeddedCoverArt)
+ return this.audioFiles.some(af => af.embeddedCoverArt)
}
get hasIssues() {
- return this.audiobooks.some(ab => ab.missingParts.length)
+ return this.missingParts.length || this.audioFiles.some(af => af.invalid)
+ }
+ 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 numTracks() {
+ return this.tracks.length
}
update(payload) {
@@ -123,42 +146,22 @@ class Book {
this.coverPath = coverPath
return true
}
-
- getAudiobookById(audiobookId) {
- return this.audiobooks.find(ab => ab.id === audiobookId)
- }
- getMediaEntityById(entityId) {
- var ent = this.audiobooks.find(ab => ab.id === entityId)
- if (ent) return ent
- return this.ebooks.find(eb => eb.id === entityId)
- }
- getPlaybackMediaEntity() { // Get first playback media entity
- if (!this.audiobooks.length) return null
- return this.audiobooks[0]
- }
-
removeFileWithInode(inode) {
- var audiobookWithIno = this.audiobooks.find(ab => ab.findFileWithInode(inode))
- if (audiobookWithIno) {
- audiobookWithIno.removeFileWithInode(inode)
- if (!audiobookWithIno.audioFiles.length) { // All audio files removed = remove audiobook
- this.audiobooks = this.audiobooks.filter(ab => ab.id !== audiobookWithIno.id)
- }
+ if (this.audioFiles.some(af => af.ino === inode)) {
+ this.audioFiles = this.audioFiles.filter(af => af.ino !== inode)
return true
}
- var ebookWithIno = this.ebooks.find(eb => eb.findFileWithInode(inode))
- if (ebookWithIno) {
- this.ebooks = this.ebooks.filter(eb => eb.id !== ebookWithIno.id) // Remove ebook
+ if (this.ebookFile && this.ebookFile.ino === inode) {
+ this.ebookFile = null
return true
}
return false
}
findFileWithInode(inode) {
- var audioFile = this.audiobooks.find(ab => ab.findFileWithInode(inode))
+ var audioFile = this.audioFiles.find(af => af.ino === inode)
if (audioFile) return audioFile
- var ebookFile = this.ebooks.find(eb => eb.findFileWithInode(inode))
- if (ebookFile) return ebookFile
+ if (this.ebookFile && this.ebookFile.ino === inode) return this.ebookFile
return null
}
@@ -169,9 +172,8 @@ class Book {
// Audio file metadata tags map to book details (will not overwrite)
setMetadataFromAudioFile(overrideExistingDetails = false) {
- if (!this.audiobooks.length) return false
- var audiobook = this.audiobooks[0]
- var audioFile = audiobook.audioFiles[0]
+ if (!this.audioFiles.length) return false
+ var audioFile = this.audioFiles[0]
if (!audioFile.metaTags) return false
return this.metadata.setDataFromAudioMetaTags(audioFile.metaTags, overrideExistingDetails)
}
@@ -276,50 +278,135 @@ class Book {
return payload
}
- addEbookFile(libraryFile) {
+ setEbookFile(libraryFile) {
var ebookFile = new EBookFile()
ebookFile.setData(libraryFile)
-
- var ebookIndex = this.ebooks.length + 1
- var newEBook = new EBook()
- newEBook.setData(ebookFile, ebookIndex)
- this.ebooks.push(newEBook)
+ this.ebookFile = ebookFile
}
- getCreateAudiobookVariant(variant) {
- if (this.audiobooks.length) {
- var ab = this.audiobooks.find(ab => ab.name == variantName)
- if (ab) return ab
- }
- var abIndex = this.audiobooks.length + 1
- var newAb = new Audiobook()
- newAb.setData(variant, abIndex)
- this.audiobooks.push(newAb)
- return newAb
+ addAudioFile(audioFile) {
+ this.audioFiles.push(audioFile)
}
- addAudioFileToAudiobook(audioFile, variant = 'default') { // Create if none
- var audiobook = this.getCreateAudiobookVariant(variant)
- audiobook.audioFiles.push(audioFile)
- }
-
- getLongestDuration() {
- if (!this.audiobooks.length) return 0
- var longest = 0
- this.audiobooks.forEach((ab) => {
- if (ab.duration > longest) longest = ab.duration
+ updateAudioTracks(orderedFileData) {
+ var index = 1
+ this.audioFiles = orderedFileData.map((fileData) => {
+ var audioFile = this.audioFiles.find(af => af.ino === fileData.ino)
+ audioFile.manuallyVerified = true
+ audioFile.invalid = false
+ audioFile.error = null
+ if (fileData.exclude !== undefined) {
+ audioFile.exclude = !!fileData.exclude
+ }
+ if (audioFile.exclude) {
+ audioFile.index = -1
+ } else {
+ audioFile.index = index++
+ }
+ return audioFile
})
- return longest
+
+ this.rebuildTracks()
}
- getTotalAudioTracks() {
- var total = 0
- this.audiobooks.forEach((ab) => total += ab.tracks.length)
- return total
+
+ rebuildTracks() {
+ this.audioFiles.sort((a, b) => a.index - b.index)
+ this.missingParts = []
+ this.setChapters()
+ this.checkUpdateMissingTracks()
}
- getTotalDuration() {
- var total = 0
- this.audiobooks.forEach((ab) => total += ab.duration)
- return total
+
+ checkUpdateMissingTracks() {
+ var currMissingParts = (this.missingParts || []).join(',') || ''
+
+ var current_index = 1
+ var missingParts = []
+
+ for (let i = 0; i < this.tracks.length; i++) {
+ var _track = this.tracks[i]
+ if (_track.index > current_index) {
+ var num_parts_missing = _track.index - current_index
+ for (let x = 0; x < num_parts_missing && x < 9999; x++) {
+ missingParts.push(current_index + x)
+ }
+ }
+ current_index = _track.index + 1
+ }
+
+ this.missingParts = missingParts
+
+ var newMissingParts = (this.missingParts || []).join(',') || ''
+ var wasUpdated = newMissingParts !== currMissingParts
+ if (wasUpdated && this.missingParts.length) {
+ Logger.info(`[Audiobook] "${this.name}" has ${missingParts.length} missing parts`)
+ }
+
+ return wasUpdated
+ }
+
+ setChapters() {
+ // If 1 audio file without chapters, then no chapters will be set
+ var includedAudioFiles = this.audioFiles.filter(af => !af.exclude)
+ if (includedAudioFiles.length === 1) {
+ // 1 audio file with chapters
+ if (includedAudioFiles[0].chapters) {
+ this.chapters = includedAudioFiles[0].chapters.map(c => ({ ...c }))
+ }
+ } else {
+ this.chapters = []
+ var currChapterId = 0
+ var currStartTime = 0
+ includedAudioFiles.forEach((file) => {
+ // If audio file has chapters use chapters
+ if (file.chapters && file.chapters.length) {
+ file.chapters.forEach((chapter) => {
+ var chapterDuration = chapter.end - chapter.start
+ if (chapterDuration > 0) {
+ var title = `Chapter ${currChapterId}`
+ if (chapter.title) {
+ title += ` (${chapter.title})`
+ }
+ this.chapters.push({
+ id: currChapterId++,
+ start: currStartTime,
+ end: currStartTime + chapterDuration,
+ title
+ })
+ currStartTime += chapterDuration
+ }
+ })
+ } else if (file.duration) {
+ // Otherwise just use track has chapter
+ this.chapters.push({
+ id: currChapterId++,
+ start: currStartTime,
+ end: currStartTime + file.duration,
+ title: file.metadata.filename ? Path.basename(file.metadata.filename, Path.extname(file.metadata.filename)) : `Chapter ${currChapterId}`
+ })
+ currStartTime += file.duration
+ }
+ })
+ }
+ }
+
+ // Only checks container format
+ checkCanDirectPlay(payload) {
+ var supportedMimeTypes = payload.supportedMimeTypes || []
+ return !this.tracks.some((t) => !supportedMimeTypes.includes(t.mimeType))
+ }
+
+ getDirectPlayTracklist(libraryItemId) {
+ var tracklist = []
+
+ var startOffset = 0
+ this.tracks.forEach((audioFile) => {
+ var audioTrack = new AudioTrack()
+ audioTrack.setData(libraryItemId, audioFile, startOffset)
+ startOffset += audioTrack.duration
+ tracklist.push(audioTrack)
+ })
+
+ return tracklist
}
}
module.exports = Book
\ No newline at end of file
diff --git a/server/objects/mediaTypes/Podcast.js b/server/objects/mediaTypes/Podcast.js
index aed58131..54259567 100644
--- a/server/objects/mediaTypes/Podcast.js
+++ b/server/objects/mediaTypes/Podcast.js
@@ -74,6 +74,14 @@ class Podcast {
get hasIssues() {
return false
}
+ get duration() {
+ var total = 0
+ this.episodes.forEach((ep) => total += ep.duration)
+ return total
+ }
+ get numTracks() {
+ return this.episodes.length
+ }
update(payload) {
var json = this.toJSON()
@@ -110,14 +118,6 @@ class Podcast {
return null
}
- getMediaEntityById(entityId) {
- return this.episodes.find(ep => ep.id === entityId)
- }
- getPlaybackMediaEntity() { // Get first playback media entity
- if (!this.episodes.length) return null
- return this.episodes[0]
- }
-
setData(mediaMetadata) {
this.metadata = new PodcastMetadata()
if (mediaMetadata.metadata) {
@@ -137,26 +137,19 @@ class Podcast {
return payload || {}
}
- getLongestDuration() {
- if (!this.episodes.length) return 0
- var longest = 0
- this.episodes.forEach((ab) => {
- if (ab.duration > longest) longest = ab.duration
- })
- return longest
- }
-
- getTotalAudioTracks() {
- return this.episodes.length
- }
- getTotalDuration() {
- var total = 0
- this.episodes.forEach((ep) => total += ep.duration)
- return total
- }
-
addPodcastEpisode(podcastEpisode) {
this.episodes.push(podcastEpisode)
}
+
+ // Only checks container format
+ checkCanDirectPlay(payload, epsiodeIndex = 0) {
+ var episode = this.episodes[epsiodeIndex]
+ return episode.checkCanDirectPlay(payload)
+ }
+
+ getDirectPlayTracklist(libraryItemId, episodeIndex = 0) {
+ var episode = this.episodes[episodeIndex]
+ return episode.getDirectPlayTracklist(libraryItemId)
+ }
}
module.exports = Podcast
\ No newline at end of file
diff --git a/server/objects/metadata/BookMetadata.js b/server/objects/metadata/BookMetadata.js
index 5d235b88..ab01a02a 100644
--- a/server/objects/metadata/BookMetadata.js
+++ b/server/objects/metadata/BookMetadata.js
@@ -77,7 +77,8 @@ class BookMetadata {
explicit: this.explicit,
authorName: this.authorName,
authorNameLF: this.authorNameLF,
- narratorName: this.narratorName
+ narratorName: this.narratorName,
+ seriesName: this.seriesName
}
}
diff --git a/server/objects/user/LibraryItemProgress.js b/server/objects/user/MediaProgress.js
similarity index 89%
rename from server/objects/user/LibraryItemProgress.js
rename to server/objects/user/MediaProgress.js
index cd3be7ca..056dfe06 100644
--- a/server/objects/user/LibraryItemProgress.js
+++ b/server/objects/user/MediaProgress.js
@@ -1,10 +1,10 @@
const Logger = require('../../Logger')
-class LibraryItemProgress {
+class MediaProgress {
constructor(progress) {
this.id = null // Same as library item id
this.libraryItemId = null
- this.mediaEntityId = null
+ this.episodeId = null // For podcasts
this.duration = null
this.progress = null // 0 to 1
@@ -24,7 +24,7 @@ class LibraryItemProgress {
return {
id: this.id,
libraryItemId: this.libraryItemId,
- mediaEntityId: this.mediaEntityId,
+ episodeId: this.episodeId,
duration: this.duration,
progress: this.progress,
currentTime: this.currentTime,
@@ -38,7 +38,7 @@ class LibraryItemProgress {
construct(progress) {
this.id = progress.id
this.libraryItemId = progress.libraryItemId
- this.mediaEntityId = progress.mediaEntityId || null
+ this.episodeId = progress.episodeId
this.duration = progress.duration || 0
this.progress = progress.progress
this.currentTime = progress.currentTime
@@ -52,10 +52,10 @@ class LibraryItemProgress {
return !this.isFinished && this.progress > 0
}
- setData(libraryItemId, mediaEntityId, progress) {
+ setData(libraryItemId, progress) {
this.id = libraryItemId
this.libraryItemId = libraryItemId
- this.mediaEntityId = mediaEntityId
+ this.episodeId = progress.episodeId || null
this.duration = progress.duration || 0
this.progress = Math.min(1, (progress.progress || 0))
this.currentTime = progress.currentTime || 0
@@ -97,4 +97,4 @@ class LibraryItemProgress {
return hasUpdates
}
}
-module.exports = LibraryItemProgress
\ No newline at end of file
+module.exports = MediaProgress
\ No newline at end of file
diff --git a/server/objects/user/User.js b/server/objects/user/User.js
index 1457a909..bf0948ea 100644
--- a/server/objects/user/User.js
+++ b/server/objects/user/User.js
@@ -1,6 +1,6 @@
const Logger = require('../../Logger')
const AudioBookmark = require('./AudioBookmark')
-const LibraryItemProgress = require('./LibraryItemProgress')
+const MediaProgress = require('./MediaProgress')
class User {
constructor(user) {
@@ -14,7 +14,7 @@ class User {
this.lastSeen = null
this.createdAt = null
- this.libraryItemProgress = []
+ this.mediaProgress = []
this.bookmarks = []
this.settings = {}
@@ -84,7 +84,7 @@ class User {
pash: this.pash,
type: this.type,
token: this.token,
- libraryItemProgress: this.libraryItemProgress ? this.libraryItemProgress.map(li => li.toJSON()) : [],
+ mediaProgress: this.mediaProgress ? this.mediaProgress.map(li => li.toJSON()) : [],
bookmarks: this.bookmarks ? this.bookmarks.map(b => b.toJSON()) : [],
isActive: this.isActive,
isLocked: this.isLocked,
@@ -103,7 +103,7 @@ class User {
username: this.username,
type: this.type,
token: this.token,
- libraryItemProgress: this.libraryItemProgress ? this.libraryItemProgress.map(li => li.toJSON()) : [],
+ mediaProgress: this.mediaProgress ? this.mediaProgress.map(li => li.toJSON()) : [],
bookmarks: this.bookmarks ? this.bookmarks.map(b => b.toJSON()) : [],
isActive: this.isActive,
isLocked: this.isLocked,
@@ -118,12 +118,19 @@ class User {
// Data broadcasted
toJSONForPublic(sessions, libraryItems) {
- var session = sessions ? sessions.find(s => s.userId === this.id) : null
+ var userSession = sessions ? sessions.find(s => s.userId === this.id) : null
+ var session = null
+ if (session) {
+ var libraryItem = libraryItems.find(li => li.id === session.libraryItemId)
+ if (libraryItem) {
+ session = userSession.toJSONForClient(libraryItem)
+ }
+ }
return {
id: this.id,
username: this.username,
type: this.type,
- session: session ? session.toJSONForClient() : null,
+ session,
mostRecent: this.getMostRecentItemProgress(libraryItems),
lastSeen: this.lastSeen,
createdAt: this.createdAt
@@ -137,9 +144,9 @@ class User {
this.type = user.type
this.token = user.token
- this.libraryItemProgress = []
- if (user.libraryItemProgress) {
- this.libraryItemProgress = user.libraryItemProgress.map(li => new LibraryItemProgress(li)).filter(lip => lip.id)
+ this.mediaProgress = []
+ if (user.mediaProgress) {
+ this.mediaProgress = user.mediaProgress.map(li => new MediaProgress(li)).filter(lip => lip.id)
}
this.bookmarks = []
@@ -217,8 +224,8 @@ class User {
}
getMostRecentItemProgress(libraryItems) {
- if (!this.libraryItemProgress.length) return null
- var lip = this.libraryItemProgress.map(lip => lip.toJSON())
+ if (!this.mediaProgress.length) return null
+ var lip = this.mediaProgress.map(lip => lip.toJSON())
lip.sort((a, b) => b.lastUpdate - a.lastUpdate)
var mostRecentWithLip = lip.find(li => libraryItems.find(_li => _li.id === li.id))
if (!mostRecentWithLip) return null
@@ -229,35 +236,27 @@ class User {
}
}
- getLibraryItemProgress(libraryItemId) {
- if (!this.libraryItemProgress) return null
- return this.libraryItemProgress.find(lip => lip.id === libraryItemId)
+ getMediaProgress(libraryItemId) {
+ if (!this.mediaProgress) return null
+ return this.mediaProgress.find(lip => lip.id === libraryItemId)
}
- createUpdateLibraryItemProgress(libraryItem, updatePayload) {
- var itemProgress = this.libraryItemProgress.find(li => li.id === libraryItem.id)
+ createUpdateMediaProgress(libraryItem, updatePayload) {
+ var itemProgress = this.mediaProgress.find(li => li.id === libraryItem.id)
if (!itemProgress) {
- var newItemProgress = new LibraryItemProgress()
+ var newItemProgress = new MediaProgress()
- var mediaEntity = null
- if (updatePayload.mediaEntityId) mediaEntity = libraryItem.media.getMediaEntityById(updatePayload.mediaEntityId)
- if (!mediaEntity) mediaEntity = libraryItem.media.getPlaybackMediaEntity()
- if (!mediaEntity) {
- Logger.error(`[User] createUpdateLibraryItemProgress invalid library item has no playback media entity "${libraryItem.id}"`)
- return false
- }
-
- newItemProgress.setData(libraryItem.id, mediaEntity.id, updatePayload)
- this.libraryItemProgress.push(newItemProgress)
+ newItemProgress.setData(libraryItem.id, updatePayload)
+ this.mediaProgress.push(newItemProgress)
return true
}
var wasUpdated = itemProgress.update(updatePayload)
return wasUpdated
}
- removeLibraryItemProgress(libraryItemId) {
- if (!this.libraryItemProgress.some(lip => lip.id == libraryItemId)) return false
- this.libraryItemProgress = this.libraryItemProgress.filter(lip => lip.id != libraryItemId)
+ removeMediaProgress(libraryItemId) {
+ if (!this.mediaProgress.some(lip => lip.id == libraryItemId)) return false
+ this.mediaProgress = this.mediaProgress.filter(lip => lip.id != libraryItemId)
return true
}
@@ -329,30 +328,31 @@ class User {
this.bookmarks = this.bookmarks.filter(bm => (bm.libraryItemId !== libraryItemId || bm.time !== time))
}
+ // TODO: re-do mobile sync
syncLocalUserAudiobookData(localUserAudiobookData, audiobook) {
- if (!localUserAudiobookData || !localUserAudiobookData.audiobookId) {
- Logger.error(`[User] Invalid local user audiobook data`, localUserAudiobookData)
- return false
- }
- if (!this.audiobooks) this.audiobooks = {}
+ // if (!localUserAudiobookData || !localUserAudiobookData.audiobookId) {
+ // Logger.error(`[User] Invalid local user audiobook data`, localUserAudiobookData)
+ // return false
+ // }
+ // if (!this.audiobooks) this.audiobooks = {}
- if (!this.audiobooks[localUserAudiobookData.audiobookId]) {
- this.audiobooks[localUserAudiobookData.audiobookId] = new UserAudiobookData(localUserAudiobookData)
- return true
- }
+ // if (!this.audiobooks[localUserAudiobookData.audiobookId]) {
+ // this.audiobooks[localUserAudiobookData.audiobookId] = new UserAudiobookData(localUserAudiobookData)
+ // return true
+ // }
- var userAbD = this.audiobooks[localUserAudiobookData.audiobookId]
- if (userAbD.lastUpdate >= localUserAudiobookData.lastUpdate) {
- // Server audiobook data is more recent
- return false
- }
+ // var userAbD = this.audiobooks[localUserAudiobookData.audiobookId]
+ // if (userAbD.lastUpdate >= localUserAudiobookData.lastUpdate) {
+ // // Server audiobook data is more recent
+ // return false
+ // }
- // Local Data More recent
- var wasUpdated = this.audiobooks[localUserAudiobookData.audiobookId].update(localUserAudiobookData)
- if (wasUpdated) {
- Logger.debug(`[User] syncLocalUserAudiobookData local data was more recent for "${audiobook.title}"`)
- }
- return wasUpdated
+ // // Local Data More recent
+ // var wasUpdated = this.audiobooks[localUserAudiobookData.audiobookId].update(localUserAudiobookData)
+ // if (wasUpdated) {
+ // Logger.debug(`[User] syncLocalUserAudiobookData local data was more recent for "${audiobook.title}"`)
+ // }
+ // return wasUpdated
}
}
module.exports = User
\ No newline at end of file
diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js
index f7261ad3..4891691e 100644
--- a/server/routers/ApiRouter.js
+++ b/server/routers/ApiRouter.js
@@ -12,7 +12,6 @@ const BackupController = require('../controllers/BackupController')
const LibraryItemController = require('../controllers/LibraryItemController')
const SeriesController = require('../controllers/SeriesController')
const AuthorController = require('../controllers/AuthorController')
-const MediaEntityController = require('../controllers/MediaEntityController')
const SessionController = require('../controllers/SessionController')
const PodcastController = require('../controllers/PodcastController')
const MiscController = require('../controllers/MiscController')
@@ -72,14 +71,6 @@ class ApiRouter {
this.router.post('/libraries/order', LibraryController.reorder.bind(this))
- //
- // Media Entity Routes
- //
- this.router.get('/entities/:id', MediaEntityController.middleware.bind(this), MediaEntityController.findOne.bind(this))
- this.router.get('/entities/:id/item', MediaEntityController.middleware.bind(this), MediaEntityController.findWithItem.bind(this))
- this.router.patch('/entities/:id/tracks', MediaEntityController.middleware.bind(this), MediaEntityController.updateTracks.bind(this))
- this.router.post('/entities/:id/play', MediaEntityController.middleware.bind(this), MediaEntityController.startPlaybackSession.bind(this))
-
//
// Item Routes
//
@@ -95,6 +86,7 @@ class ApiRouter {
this.router.delete('/items/:id/cover', LibraryItemController.middleware.bind(this), LibraryItemController.removeCover.bind(this))
this.router.post('/items/:id/match', LibraryItemController.middleware.bind(this), LibraryItemController.match.bind(this))
this.router.post('/items/:id/play', LibraryItemController.middleware.bind(this), LibraryItemController.startPlaybackSession.bind(this))
+ this.router.patch('/items/:id/tracks', LibraryItemController.middleware.bind(this), LibraryItemController.updateTracks.bind(this))
this.router.get('/items/:id/scan', LibraryItemController.middleware.bind(this), LibraryItemController.scan.bind(this)) // Root only
this.router.post('/items/batch/delete', LibraryItemController.batchDelete.bind(this))
@@ -132,9 +124,9 @@ class ApiRouter {
//
this.router.get('/me/listening-sessions', MeController.getListeningSessions.bind(this))
this.router.get('/me/listening-stats', MeController.getListeningStats.bind(this))
- this.router.patch('/me/progress/:id', MeController.createUpdateLibraryItemProgress.bind(this))
- this.router.delete('/me/progress/:id', MeController.removeLibraryItemProgress.bind(this))
- this.router.patch('/me/progress/batch/update', MeController.batchUpdateLibraryItemProgress.bind(this))
+ this.router.patch('/me/progress/:id', MeController.createUpdateMediaProgress.bind(this))
+ this.router.delete('/me/progress/:id', MeController.removeMediaProgress.bind(this))
+ this.router.patch('/me/progress/batch/update', MeController.batchUpdateMediaProgress.bind(this))
this.router.post('/me/item/:id/bookmark', MeController.createBookmark.bind(this))
this.router.patch('/me/item/:id/bookmark', MeController.updateBookmark.bind(this))
this.router.delete('/me/item/:id/bookmark/:time', MeController.removeBookmark.bind(this))
@@ -266,7 +258,7 @@ class ApiRouter {
userJsonWithItemProgressDetails(user) {
var json = user.toJSONForBrowser()
- json.libraryItemProgress = json.libraryItemProgress.map(lip => {
+ json.mediaProgress = json.mediaProgress.map(lip => {
var libraryItem = this.db.libraryItems.find(li => li.id === lip.id)
if (!libraryItem) {
Logger.warn('[ApiRouter] Library item not found for users progress ' + lip.id)
@@ -283,7 +275,7 @@ class ApiRouter {
// Remove libraryItem from users
for (let i = 0; i < this.db.users.length; i++) {
var user = this.db.users[i]
- var madeUpdates = user.removeLibraryItemProgress(libraryItem.id)
+ var madeUpdates = user.removeMediaProgress(libraryItem.id)
if (madeUpdates) {
await this.db.updateEntity('user', user)
}
diff --git a/server/scanner/AudioFileScanner.js b/server/scanner/AudioFileScanner.js
index 33e89baf..f4819dc9 100644
--- a/server/scanner/AudioFileScanner.js
+++ b/server/scanner/AudioFileScanner.js
@@ -169,12 +169,11 @@ class AudioFileScanner {
if (existingAF) {
if (existingAF.updateFromScan) existingAF.updateFromScan(audioFiles[i])
} else {
- libraryItem.media.addAudioFileToAudiobook(audioFiles[i])
+ libraryItem.media.addAudioFile(audioFiles[i])
}
}
}
- // TODO: support for multiple audiobooks in a book item will need to pass an audiobook variant name here
async scanAudioFiles(audioLibraryFiles, scanData, libraryItem, preferAudioMetadata, libraryScan = null) {
var hasUpdated = false
@@ -189,14 +188,14 @@ class AudioFileScanner {
return !libraryItem.media.findFileWithInode(af.ino)
})
- // Adding audio files to book media
+ // Book: Adding audio files to book media
if (libraryItem.mediaType === 'book') {
if (newAudioFiles.length) {
// Single Track Audiobooks
if (totalAudioFilesToInclude === 1) {
var af = audioScanResult.audioFiles[0]
af.index = 1
- libraryItem.media.addAudioFileToAudiobook(af)
+ libraryItem.media.addAudioFile(af)
hasUpdated = true
} else {
this.runSmartTrackOrder(libraryItem, audioScanResult.audioFiles)
@@ -221,12 +220,7 @@ class AudioFileScanner {
}
if (hasUpdated) {
- if (!libraryItem.media.audiobooks.length) {
- Logger.error(`[AudioFileScanner] Updates were made but library item has no audiobooks`, libraryItem)
- } else {
- var audiobook = libraryItem.media.audiobooks[0]
- audiobook.rebuildTracks()
- }
+ libraryItem.media.rebuildTracks()
}
} // End Book media type
}
diff --git a/server/scanner/AuthorScanner.js b/server/scanner/AuthorScanner.js
deleted file mode 100644
index 09ce2478..00000000
--- a/server/scanner/AuthorScanner.js
+++ /dev/null
@@ -1,32 +0,0 @@
-const AuthorFinder = require('../finders/AuthorFinder')
-
-class AuthorScanner {
- constructor(db) {
- this.db = db
- this.authorFinder = new AuthorFinder()
- }
-
- getUniqueAuthors() {
- var authorFls = this.db.audiobooks.map(b => b.book.authorFL)
- var authors = []
- authorFls.forEach((auth) => {
- authors = authors.concat(auth.split(', ').map(a => a.trim()))
- })
- return [...new Set(authors)]
- }
-
- async scanAuthors() {
- var authors = this.getUniqueAuthors()
- for (let i = 0; i < authors.length; i++) {
- var authorName = authors[i]
- var author = await this.authorFinder.getAuthorByName(authorName)
- if (!author) {
- return res.status(500).send('Failed to create author')
- }
-
- await this.db.insertEntity('author', author)
- this.emitter('author_added', author.toJSON())
- }
- }
-}
-module.exports = AuthorScanner
\ No newline at end of file
diff --git a/server/scanner/Scanner.js b/server/scanner/Scanner.js
index 163b1efb..10f0ab71 100644
--- a/server/scanner/Scanner.js
+++ b/server/scanner/Scanner.js
@@ -613,37 +613,38 @@ class Scanner {
return false
}
+ // TODO: Redo metadata
async saveMetadata(audiobookId) {
- if (audiobookId) {
- var audiobook = this.db.audiobooks.find(ab => ab.id === audiobookId)
- if (!audiobook) {
- return {
- error: 'Audiobook not found'
- }
- }
- var savedPath = await audiobook.writeNfoFile()
- return {
- audiobookId,
- audiobookTitle: audiobook.title,
- savedPath
- }
- } else {
- var response = {
- success: 0,
- failed: 0
- }
- for (let i = 0; i < this.db.audiobooks.length; i++) {
- var audiobook = this.db.audiobooks[i]
- var savedPath = await audiobook.writeNfoFile()
- if (savedPath) {
- Logger.info(`[Scanner] Saved metadata nfo ${savedPath}`)
- response.success++
- } else {
- response.failed++
- }
- }
- return response
- }
+ // if (audiobookId) {
+ // var audiobook = this.db.audiobooks.find(ab => ab.id === audiobookId)
+ // if (!audiobook) {
+ // return {
+ // error: 'Audiobook not found'
+ // }
+ // }
+ // var savedPath = await audiobook.writeNfoFile()
+ // return {
+ // audiobookId,
+ // audiobookTitle: audiobook.title,
+ // savedPath
+ // }
+ // } else {
+ // var response = {
+ // success: 0,
+ // failed: 0
+ // }
+ // for (let i = 0; i < this.db.audiobooks.length; i++) {
+ // var audiobook = this.db.audiobooks[i]
+ // var savedPath = await audiobook.writeNfoFile()
+ // if (savedPath) {
+ // Logger.info(`[Scanner] Saved metadata nfo ${savedPath}`)
+ // response.success++
+ // } else {
+ // response.failed++
+ // }
+ // }
+ // return response
+ // }
}
async quickMatchBook(libraryItem, options = {}) {
@@ -728,48 +729,49 @@ class Scanner {
}
}
+ // TODO: Redo quick match full library
async matchLibraryBooks(library) {
- if (this.isLibraryScanning(library.id)) {
- Logger.error(`[Scanner] Already scanning ${library.id}`)
- return
- }
+ // if (this.isLibraryScanning(library.id)) {
+ // Logger.error(`[Scanner] Already scanning ${library.id}`)
+ // return
+ // }
- const provider = library.provider || 'google'
- var audiobooksInLibrary = this.db.audiobooks.filter(ab => ab.libraryId === library.id)
- if (!audiobooksInLibrary.length) {
- return
- }
+ // const provider = library.provider || 'google'
+ // var audiobooksInLibrary = this.db.audiobooks.filter(ab => ab.libraryId === library.id)
+ // if (!audiobooksInLibrary.length) {
+ // return
+ // }
- var libraryScan = new LibraryScan()
- libraryScan.setData(library, null, 'match')
- this.librariesScanning.push(libraryScan.getScanEmitData)
- this.emitter('scan_start', libraryScan.getScanEmitData)
+ // var libraryScan = new LibraryScan()
+ // libraryScan.setData(library, null, 'match')
+ // this.librariesScanning.push(libraryScan.getScanEmitData)
+ // this.emitter('scan_start', libraryScan.getScanEmitData)
- Logger.info(`[Scanner] Starting library match books scan ${libraryScan.id} for ${libraryScan.libraryName}`)
+ // Logger.info(`[Scanner] Starting library match books scan ${libraryScan.id} for ${libraryScan.libraryName}`)
- for (let i = 0; i < audiobooksInLibrary.length; i++) {
- var audiobook = audiobooksInLibrary[i]
- Logger.debug(`[Scanner] Quick matching "${audiobook.title}" (${i + 1} of ${audiobooksInLibrary.length})`)
- var result = await this.quickMatchBook(audiobook, { provider })
- if (result.warning) {
- Logger.warn(`[Scanner] Match warning ${result.warning} for audiobook "${audiobook.title}"`)
- } else if (result.updated) {
- libraryScan.resultsUpdated++
- }
+ // for (let i = 0; i < audiobooksInLibrary.length; i++) {
+ // var audiobook = audiobooksInLibrary[i]
+ // Logger.debug(`[Scanner] Quick matching "${audiobook.title}" (${i + 1} of ${audiobooksInLibrary.length})`)
+ // var result = await this.quickMatchBook(audiobook, { provider })
+ // if (result.warning) {
+ // Logger.warn(`[Scanner] Match warning ${result.warning} for audiobook "${audiobook.title}"`)
+ // } else if (result.updated) {
+ // libraryScan.resultsUpdated++
+ // }
- if (this.cancelLibraryScan[libraryScan.libraryId]) {
- Logger.info(`[Scanner] Library match scan canceled for "${libraryScan.libraryName}"`)
- delete this.cancelLibraryScan[libraryScan.libraryId]
- var scanData = libraryScan.getScanEmitData
- scanData.results = false
- this.emitter('scan_complete', scanData)
- this.librariesScanning = this.librariesScanning.filter(ls => ls.id !== library.id)
- return
- }
- }
+ // if (this.cancelLibraryScan[libraryScan.libraryId]) {
+ // Logger.info(`[Scanner] Library match scan canceled for "${libraryScan.libraryName}"`)
+ // delete this.cancelLibraryScan[libraryScan.libraryId]
+ // var scanData = libraryScan.getScanEmitData
+ // scanData.results = false
+ // this.emitter('scan_complete', scanData)
+ // this.librariesScanning = this.librariesScanning.filter(ls => ls.id !== library.id)
+ // return
+ // }
+ // }
- this.librariesScanning = this.librariesScanning.filter(ls => ls.id !== library.id)
- this.emitter('scan_complete', libraryScan.getScanEmitData)
+ // this.librariesScanning = this.librariesScanning.filter(ls => ls.id !== library.id)
+ // this.emitter('scan_complete', libraryScan.getScanEmitData)
}
}
module.exports = Scanner
\ No newline at end of file
diff --git a/server/utils/dbMigration.js b/server/utils/dbMigration.js
index e57505b8..7c9e2469 100644
--- a/server/utils/dbMigration.js
+++ b/server/utils/dbMigration.js
@@ -27,7 +27,7 @@ const Series = require('../objects/entities/Series')
const Audiobook = require('../objects/entities/Audiobook')
const EBook = require('../objects/entities/EBook')
-const LibraryItemProgress = require('../objects/user/LibraryItemProgress')
+const MediaProgress = require('../objects/user/MediaProgress')
const PlaybackSession = require('../objects/PlaybackSession')
const { isObject } = require('.')
@@ -164,7 +164,7 @@ function cleanOldCoverPath(coverPath) {
function makeLibraryItemFromOldAb(audiobook) {
var libraryItem = new LibraryItem()
- libraryItem.id = getId('li')
+ libraryItem.id = audiobook.id
libraryItem.ino = audiobook.ino
libraryItem.libraryId = audiobook.libraryId
libraryItem.folderId = audiobook.folderId
@@ -199,34 +199,16 @@ function makeLibraryItemFromOldAb(audiobook) {
bookEntity.tags = [...audiobook.tags]
var payload = makeFilesFromOldAb(audiobook)
- if (payload.audioFiles.length) {
- var newAudiobook = new Audiobook()
- newAudiobook.id = audiobook.id
- newAudiobook.index = 1
- newAudiobook.name = 'default'
- newAudiobook.audioFiles = payload.audioFiles
- if (audiobook.chapters && audiobook.chapters.length) {
- newAudiobook.chapters = audiobook.chapters.map(c => ({ ...c }))
- }
- newAudiobook.missingParts = audiobook.missingParts || []
- newAudiobook.addedAt = audiobook.addedAt
- newAudiobook.updatedAt = audiobook.lastUpdate
-
- bookEntity.audiobooks.push(newAudiobook)
+ bookEntity.audioFiles = payload.audioFiles
+ bookEntity.chapters = []
+ if (audiobook.chapters && audiobook.chapters.length) {
+ bookEntity.chapters = audiobook.chapters.map(c => ({ ...c }))
}
+ bookEntity.missingParts = audiobook.missingParts || []
- var ebookIndex = 1
- payload.ebookFiles.forEach(ebookFile => {
- var newEBook = new EBook()
- newEBook.id = getId('eb')
- newEBook.index = ebookIndex++
- newEBook.name = ebookFile.metadata.filenameNoExt
- newEBook.ebookFile = ebookFile
- newEBook.addedAt = audiobook.addedAt
- newEBook.updatedAt = audiobook.lastUpdate
-
- bookEntity.ebooks.push(newEBook)
- })
+ if (payload.ebookFiles.length) {
+ bookEntity.ebookFile = payload.ebookFiles[0]
+ }
libraryItem.media = bookEntity
libraryItem.libraryFiles = payload.libraryFiles
@@ -258,55 +240,6 @@ async function migrateLibraryItems(db) {
var libraryItems = audiobooks.map((ab) => makeLibraryItemFromOldAb(ab))
- // User library item progress was using the auidobook ID when migrated
- // now that library items are created the LibraryItemProgress objects
- // need the library item id to be set
- for (const user of db.users) {
- if (user.libraryItemProgress.length) {
- user.libraryItemProgress = user.libraryItemProgress.map(lip => {
- var audiobookId = lip.id
- var libraryItemWithAudiobook = libraryItems.find(li => li.media.getAudiobookById && !!li.media.getAudiobookById(audiobookId))
- if (!libraryItemWithAudiobook) {
- Logger.error('[dbMigration] Failed to find library item with audiobook id', audiobookId)
- return null
- }
- lip.id = libraryItemWithAudiobook.id
- lip.libraryItemId = libraryItemWithAudiobook.id
- return lip
- }).filter(lip => !!lip)
- }
- if (user.bookmarks.length) {
- user.bookmarks = user.bookmarks.map((bookmark) => {
- var audiobookId = bookmark.libraryItemId
- var libraryItemWithAudiobook = libraryItems.find(li => li.media.getAudiobookById && !!li.media.getAudiobookById(audiobookId))
- if (!libraryItemWithAudiobook) {
- Logger.error('[dbMigration] Failed to find library item with audiobook id', audiobookId)
- return null
- }
- bookmark.libraryItemId = libraryItemWithAudiobook.id
- return bookmark
- }).filter(bm => !!bm)
- }
- if (user.libraryItemProgress.length || user.bookmarks.length) {
- await db.updateEntity('user', user)
- }
- }
-
- // Update session LibraryItemId's
- var sessions = await db.sessionsDb.select(() => true).then((results) => results.data)
- if (sessions.length) {
- sessions = sessions.map(se => {
- var libraryItemWithAudiobook = libraryItems.find(li => li.media.getAudiobookById && !!li.media.getAudiobookById(se.mediaEntityId))
- if (!libraryItemWithAudiobook) {
- Logger.error('[dbMigration] Failed to find library item with audiobook id', se.mediaEntityId)
- return null
- }
- se.libraryItemId = libraryItemWithAudiobook.id
- return se
- }).filter(se => !!se)
- await db.updateEntities('session', sessions)
- }
-
Logger.info(`>>> ${libraryItems.length} Library Items made`)
await db.insertEntities('libraryItem', libraryItems)
if (authorsToAdd.length) {
@@ -327,28 +260,27 @@ async function migrateLibraryItems(db) {
function cleanUserObject(db, userObj) {
var cleanedUserPayload = {
...userObj,
- libraryItemProgress: [],
+ mediaProgress: [],
bookmarks: []
}
- // UserAudiobookData is now LibraryItemProgress and AudioBookmarks separated
+ // UserAudiobookData is now MediaProgress and AudioBookmarks separated
if (userObj.audiobooks) {
for (const audiobookId in userObj.audiobooks) {
if (isObject(userObj.audiobooks[audiobookId])) {
// Bookmarks now live on User.js object instead of inside UserAudiobookData
if (userObj.audiobooks[audiobookId].bookmarks) {
const cleanedBookmarks = userObj.audiobooks[audiobookId].bookmarks.map((bm) => {
- bm.libraryItemId = audiobookId // Temp placeholder replace with libraryItemId when created
+ bm.libraryItemId = audiobookId
return bm
})
cleanedUserPayload.bookmarks = cleanedUserPayload.bookmarks.concat(cleanedBookmarks)
}
var userAudiobookData = new UserAudiobookData(userObj.audiobooks[audiobookId]) // Legacy object
- var liProgress = new LibraryItemProgress() // New Progress Object
- liProgress.id = userAudiobookData.audiobookId // This ID will be updated when library item is created
+ var liProgress = new MediaProgress() // New Progress Object
+ liProgress.id = userAudiobookData.audiobookId
liProgress.libraryItemId = userAudiobookData.audiobookId
- liProgress.mediaEntityId = userAudiobookData.audiobookId
liProgress.duration = userAudiobookData.totalDuration
liProgress.isFinished = !!userAudiobookData.isRead
Object.keys(liProgress.toJSON()).forEach((key) => {
@@ -356,7 +288,7 @@ function cleanUserObject(db, userObj) {
liProgress[key] = userAudiobookData[key]
}
})
- cleanedUserPayload.libraryItemProgress.push(liProgress.toJSON())
+ cleanedUserPayload.mediaProgress.push(liProgress.toJSON())
}
}
}
@@ -376,8 +308,7 @@ function cleanSessionObj(db, userListeningSession) {
newPlaybackSession.id = getId('play')
newPlaybackSession.mediaType = 'book'
newPlaybackSession.updatedAt = userListeningSession.lastUpdate
- newPlaybackSession.libraryItemId = userListeningSession.audiobookId // Temp
- newPlaybackSession.mediaEntityId = userListeningSession.audiobookId
+ newPlaybackSession.libraryItemId = userListeningSession.audiobookId
newPlaybackSession.playMethod = PlayMethod.TRANSCODE
// We only have title to transfer over nicely
diff --git a/server/utils/libraryHelpers.js b/server/utils/libraryHelpers.js
index 5ca190f6..42ca72e1 100644
--- a/server/utils/libraryHelpers.js
+++ b/server/utils/libraryHelpers.js
@@ -28,7 +28,7 @@ module.exports = {
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 itemProgress = user.getLibraryItemProgress(li.id)
+ var itemProgress = user.getMediaProgress(li.id)
if (filter === 'Finished' && (itemProgress && itemProgress.isFinished)) return true
if (filter === 'Not Started' && !itemProgress) return true
if (filter === 'In Progress' && (itemProgress && itemProgress.inProgress)) return true
@@ -126,7 +126,7 @@ module.exports = {
// var _series = {}
// books.forEach((audiobook) => {
// if (audiobook.book.series) {
- // var bookWithUserAb = { userAudiobook: user.getLibraryItemProgress(audiobook.id), book: audiobook }
+ // var bookWithUserAb = { userAudiobook: user.getMediaProgress(audiobook.id), book: audiobook }
// if (!_series[audiobook.book.series]) {
// _series[audiobook.book.series] = {
// id: audiobook.book.series,
@@ -159,7 +159,7 @@ module.exports = {
getItemsWithUserProgress(user, libraryItems) {
return libraryItems.map(li => {
- var itemProgress = user.getLibraryItemProgress(li.id)
+ var itemProgress = user.getMediaProgress(li.id)
return {
userProgress: itemProgress ? itemProgress.toJSON() : null,
libraryItem: li
@@ -241,13 +241,13 @@ module.exports = {
},
getItemDurationStats(libraryItems) {
- var sorted = sort(libraryItems).desc(li => li.media.getLongestDuration())
- var top10 = sorted.slice(0, 10).map(li => ({ title: li.media.metadata.title, duration: li.media.getLongestDuration() })).filter(i => i.duration > 0)
+ var sorted = sort(libraryItems).desc(li => li.media.duration)
+ var top10 = sorted.slice(0, 10).map(li => ({ title: li.media.metadata.title, duration: li.media.duration })).filter(i => i.duration > 0)
var totalDuration = 0
var numAudioTracks = 0
libraryItems.forEach((li) => {
- totalDuration += li.media.getTotalDuration()
- numAudioTracks += li.media.getTotalAudioTracks()
+ totalDuration += li.media.duration
+ numAudioTracks += li.media.numTracks
})
return {
totalDuration,