diff --git a/client/components/app/Appbar.vue b/client/components/app/Appbar.vue
index 5bff062e..90689ae9 100644
--- a/client/components/app/Appbar.vue
+++ b/client/components/app/Appbar.vue
@@ -175,8 +175,8 @@ export default {
})
},
batchDeleteClick() {
- var audiobookText = this.numLibraryItemsSelected > 1 ? `these ${this.numLibraryItemsSelected} audiobooks` : 'this audiobook'
- var confirmMsg = `Are you sure you want to remove ${audiobookText}?\n\n*Does not delete your files, only removes the audiobooks from AudioBookshelf`
+ var audiobookText = this.numLibraryItemsSelected > 1 ? `these ${this.numLibraryItemsSelected} items` : 'this item'
+ var confirmMsg = `Are you sure you want to remove ${audiobookText}?\n\n*Does not delete your files, only removes the items from Audiobookshelf`
if (confirm(confirmMsg)) {
this.processingBatchDelete = true
this.$store.commit('setProcessingBatch', true)
diff --git a/client/components/app/LazyBookshelf.vue b/client/components/app/LazyBookshelf.vue
index f4d00acd..920758f0 100644
--- a/client/components/app/LazyBookshelf.vue
+++ b/client/components/app/LazyBookshelf.vue
@@ -300,11 +300,11 @@ export default {
var firstBookPage = Math.floor(firstBookIndex / this.booksPerFetch)
var lastBookPage = Math.floor(lastBookIndex / this.booksPerFetch)
if (!this.pagesLoaded[firstBookPage]) {
- console.log('Must load next batch', firstBookPage, 'book index', firstBookIndex)
+ // console.log('Must load next batch', firstBookPage, 'book index', firstBookIndex)
this.loadPage(firstBookPage)
}
if (!this.pagesLoaded[lastBookPage]) {
- console.log('Must load last next batch', lastBookPage, 'book index', lastBookIndex)
+ // console.log('Must load last next batch', lastBookPage, 'book index', lastBookIndex)
this.loadPage(lastBookPage)
}
diff --git a/client/components/app/StreamContainer.vue b/client/components/app/StreamContainer.vue
index 45fd7c56..6398234f 100644
--- a/client/components/app/StreamContainer.vue
+++ b/client/components/app/StreamContainer.vue
@@ -95,16 +95,17 @@ export default {
user() {
return this.$store.state.user.user
},
- userAudiobook() {
+ userLibraryItemProgress() {
if (!this.libraryItemId) return
return this.$store.getters['user/getUserLibraryItemProgress'](this.libraryItemId)
},
- userAudiobookCurrentTime() {
- return this.userAudiobook ? this.userAudiobook.currentTime || 0 : 0
+ userItemCurrentTime() {
+ return this.userLibraryItemProgress ? this.userLibraryItemProgress.currentTime || 0 : 0
},
bookmarks() {
- if (!this.userAudiobook) return []
- return (this.userAudiobook.bookmarks || []).map((bm) => ({ ...bm })).sort((a, b) => a.time - b.time)
+ return []
+ // if (!this.userAudiobook) return []
+ // return (this.userAudiobook.bookmarks || []).map((bm) => ({ ...bm })).sort((a, b) => a.time - b.time)
},
streamLibraryItem() {
return this.$store.state.streamLibraryItem
@@ -236,9 +237,9 @@ export default {
console.error('No Audio Ref')
}
},
- streamOpen(stream) {
- this.$store.commit('setLibraryItemStream', stream.libraryItem)
- this.playerHandler.prepareStream(stream)
+ sessionOpen(session) {
+ this.$store.commit('setLibraryItemStream', session.libraryItem)
+ this.playerHandler.prepareOpenSession(session)
},
streamClosed(streamId) {
// Stream was closed from the server
@@ -282,7 +283,7 @@ export default {
if (!libraryItem) return
this.$store.commit('setLibraryItemStream', libraryItem)
- this.playerHandler.load(libraryItem, true, this.userAudiobookCurrentTime)
+ this.playerHandler.load(libraryItem, true)
}
},
mounted() {
diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue
index b479a307..0b4a3565 100644
--- a/client/components/cards/LazyBookCard.vue
+++ b/client/components/cards/LazyBookCard.vue
@@ -161,10 +161,12 @@ export default {
return this._libraryItem.libraryId
},
hasEbook() {
- return this.media.numEbooks
+ if (!this.media.ebooks) return 0
+ return this.media.ebooks.length
},
- hasTracks() {
- return this.media.numTracks
+ hasAudiobook() {
+ if (!this.media.audiobooks) return 0
+ return this.media.audiobooks.length
},
processingBatch() {
return this.store.state.processingBatch
@@ -244,7 +246,7 @@ export default {
return !this.isSelectionMode && this.showExperimentalFeatures && !this.showPlayButton && this.hasEbook
},
showPlayButton() {
- return !this.isSelectionMode && !this.isMissing && !this.isInvalid && this.hasTracks && !this.isStreaming
+ return !this.isSelectionMode && !this.isMissing && !this.isInvalid && this.hasAudiobook && !this.isStreaming
},
showSmallEBookIcon() {
return !this.isSelectionMode && this.showExperimentalFeatures && this.hasEbook
@@ -310,7 +312,7 @@ export default {
}
]
if (this.userCanUpdate) {
- if (this.hasTracks) {
+ if (this.hasAudiobook) {
items.push({
func: 'showEditModalTracks',
text: 'Tracks'
diff --git a/client/components/controls/FilterSelect.vue b/client/components/controls/FilterSelect.vue
index 5908f388..2e45f450 100644
--- a/client/components/controls/FilterSelect.vue
+++ b/client/components/controls/FilterSelect.vue
@@ -175,7 +175,7 @@ export default {
return this.filterData.languages || []
},
progress() {
- return ['Read', 'Unread', 'In Progress']
+ return ['Finished', 'In Progress', 'Not Started']
},
sublistItems() {
return (this[this.sublist] || []).map((item) => {
diff --git a/client/components/modals/item/tabs/Details.vue b/client/components/modals/item/tabs/Details.vue
index f30bd583..5e149457 100644
--- a/client/components/modals/item/tabs/Details.vue
+++ b/client/components/modals/item/tabs/Details.vue
@@ -168,7 +168,6 @@ export default {
},
async updateDetails(updatedDetails) {
this.isProcessing = true
- console.log('Sending update', updatedDetails.updatePayload)
var updateResult = await this.$axios.$patch(`/api/items/${this.libraryItemId}/media`, updatedDetails.updatePayload).catch((error) => {
console.error('Failed to update', error)
return false
diff --git a/client/components/modals/libraries/FolderChooser.vue b/client/components/modals/libraries/FolderChooser.vue
index fca170ca..d41d16e9 100644
--- a/client/components/modals/libraries/FolderChooser.vue
+++ b/client/components/modals/libraries/FolderChooser.vue
@@ -64,7 +64,6 @@ export default {
computed: {
_directories() {
return this.directories.map((d) => {
- console.log('Directories', d)
var isUsed = !!this.paths.find((path) => path.endsWith(d.path))
var isSelected = d.path === this.selectedPath
var classes = []
diff --git a/client/components/tables/AllFilesTable.vue b/client/components/tables/AllFilesTable.vue
deleted file mode 100644
index 57b000b9..00000000
--- a/client/components/tables/AllFilesTable.vue
+++ /dev/null
@@ -1,105 +0,0 @@
-
-
-
-
All Files
-
{{ allFiles.length }}
-
-
-
Full Path
-
-
-
-
- Path |
- Filetype |
- Download |
-
-
-
-
- {{ showFullPath ? file.fullPath : file.path }}
- |
-
- {{ file.filetype }}
- |
-
- download
- |
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/client/components/tables/UsersTable.vue b/client/components/tables/UsersTable.vue
index 89b3232e..735f8432 100644
--- a/client/components/tables/UsersTable.vue
+++ b/client/components/tables/UsersTable.vue
@@ -26,11 +26,11 @@
{{ user.type }} |
-
- Reading: {{ usersOnline[user.id].stream.libraryItem.media.metadata.title || '' }}
+
+ Listening: {{ usersOnline[user.id].session.libraryItem.media.metadata.title || '' }}
-
- Last: {{ getLastRead(user.audiobooks) }}
+
+ Last: {{ user.mostRecent.metadata.title }}
|
@@ -78,23 +78,11 @@ export default {
},
usersOnline() {
var usermap = {}
- this.$store.state.users.users.forEach((u) => (usermap[u.id] = { online: true, stream: u.stream }))
+ this.$store.state.users.users.forEach((u) => (usermap[u.id] = { online: true, session: u.session }))
return usermap
}
},
methods: {
- getLastRead(audiobooks) {
- var abs = Object.values(audiobooks).filter((ab) => {
- return ab.progress > 0
- })
- if (abs.length) {
- abs = abs.sort((a, b) => b.lastUpdate - a.lastUpdate)
- // Book object is attached on request
- if (abs[0].book) return abs[0].book.title
- return abs[0].audiobookTitle ? abs[0].audiobookTitle : null
- }
- return null
- },
deleteUserClick(user) {
if (this.isDeletingUser) return
if (confirm(`Are you sure you want to permanently delete user "${user.username}"?`)) {
diff --git a/client/components/ui/FileInput.vue b/client/components/ui/FileInput.vue
index bdd3a266..291f8e1a 100644
--- a/client/components/ui/FileInput.vue
+++ b/client/components/ui/FileInput.vue
@@ -33,7 +33,6 @@ export default {
var _files = Array.from(e.target.files)
if (_files && _files.length) {
var file = _files[0]
- console.log('File', file)
this.$emit('change', file)
}
}
diff --git a/client/components/ui/QueryInput.vue b/client/components/ui/QueryInput.vue
index c9bc337b..6eb6275a 100644
--- a/client/components/ui/QueryInput.vue
+++ b/client/components/ui/QueryInput.vue
@@ -78,7 +78,7 @@ export default {
console.error('Failed to get search results', error)
return []
})
- console.log('Search results', results)
+ // console.log('Search results', results)
this.items = results || []
this.searching = false
},
diff --git a/client/components/widgets/ItemDetailsEdit.vue b/client/components/widgets/ItemDetailsEdit.vue
index 959a7012..12c9f2e4 100644
--- a/client/components/widgets/ItemDetailsEdit.vue
+++ b/client/components/widgets/ItemDetailsEdit.vue
@@ -276,7 +276,7 @@ export default {
if (!matchingItem) return false
for (var key in item) {
if (item[key] !== matchingItem[key]) {
- console.log('Object array item keys changed', key, item[key], matchingItem[key])
+ // console.log('Object array item keys changed', key, item[key], matchingItem[key])
return false
}
}
diff --git a/client/layouts/default.vue b/client/layouts/default.vue
index 25a211bb..53d096f4 100644
--- a/client/layouts/default.vue
+++ b/client/layouts/default.vue
@@ -97,9 +97,9 @@ export default {
return
}
console.log('Init Payload', payload)
- if (payload.stream) {
+ if (payload.session) {
if (this.$refs.streamContainer) {
- this.$refs.streamContainer.streamOpen(payload.stream)
+ this.$refs.streamContainer.sessionOpen(payload.session)
} else {
console.warn('Stream Container not mounted')
}
diff --git a/client/nuxt.config.js b/client/nuxt.config.js
index 63519131..2a583388 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/': '' } },
- '/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' : '/' },
'/metadata/': { target: process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '/' }
diff --git a/client/pages/audiobook/_id/edit.vue b/client/pages/audiobook/_id/edit.vue
index 322b7237..074293b5 100644
--- a/client/pages/audiobook/_id/edit.vue
+++ b/client/pages/audiobook/_id/edit.vue
@@ -95,7 +95,7 @@ export default {
if (!store.getters['user/getUserCanUpdate']) {
return redirect('/?error=unauthorized')
}
- var payload = await app.$axios.$get(`/api/audiobooks/${params.id}/item?expanded=1`).catch((error) => {
+ var payload = await app.$axios.$get(`/api/entities/${params.id}/item?expanded=1`).catch((error) => {
console.error('Failed', error)
return false
})
@@ -103,7 +103,7 @@ export default {
console.error('Not found...', params.id)
return redirect('/')
}
- const audiobook = payload.audiobook
+ const audiobook = payload.mediaEntity
return {
audiobook,
libraryItem: payload.libraryItem,
@@ -218,7 +218,7 @@ export default {
this.saving = true
this.$axios
- .$patch(`/api/audiobooks/${this.audiobook.id}/tracks`, { orderedFileData })
+ .$patch(`/api/entities/${this.audiobook.id}/tracks`, { orderedFileData })
.then((data) => {
console.log('Finished patching files', data)
this.saving = false
diff --git a/client/pages/config/stats.vue b/client/pages/config/stats.vue
index 2d05fcfd..d39a2e79 100644
--- a/client/pages/config/stats.vue
+++ b/client/pages/config/stats.vue
@@ -9,8 +9,8 @@
/>
- {{ userAudiobooksRead.length }}
- Books Read
+ {{ userItemsFinished.length }}
+ Items Finished
@@ -35,17 +35,17 @@
Recent Listening Sessions
No Listening Sessions
-
-
+
+
{{ index + 1 }}.
- {{ book.audiobookTitle }}
- {{ $dateDistanceFromNow(book.lastUpdate) }}
+ {{ item.mediaMetadata.title }}
+ {{ $dateDistanceFromNow(item.lastUpdate) }}
- {{ $elapsedPretty(book.timeListening) }}
+ {{ $elapsedPretty(item.timeListening) }}
@@ -76,16 +76,11 @@ export default {
currentLibraryId() {
return this.$store.state.libraries.currentLibraryId
},
- userAudiobooks() {
- return Object.values(this.user.audiobooks || {})
+ userItemProgress() {
+ return this.user.libraryItemProgress || []
},
- userAudiobooksRead() {
- return this.userAudiobooks.filter((ab) => !!ab.isRead)
- },
- mostRecentBooksListened() {
- if (!this.listeningStats) return []
- var sorted = Object.values(this.listeningStats.books || {}).sort((a, b) => b.lastUpdate - a.lastUpdate)
- return sorted.slice(0, 10)
+ userItemsFinished() {
+ return this.userItemProgress.filter((lip) => !!lip.isFinished)
},
mostRecentListeningSessions() {
if (!this.listeningStats) return []
diff --git a/client/pages/config/users/_id.vue b/client/pages/config/users/_id.vue
index 196c10d2..0a823388 100644
--- a/client/pages/config/users/_id.vue
+++ b/client/pages/config/users/_id.vue
@@ -14,7 +14,10 @@
{{ username }}
- API Token: {{ userToken }}content_copy
+
+ API Token: {{ userToken }}content_copy
+
@@ -35,32 +38,32 @@
- Reading Progress
-
+ Item Progress
+
- Book |
+ Item |
|
Progress |
Started At |
Last Update |
-
+
-
+
|
- {{ ab.media && ab.media.metadata ? ab.media.metadata.title : ab.audiobookTitle || 'Unknown' }}
- by {{ ab.media.metadata.authorName }}
+ {{ item.media && item.media.metadata ? item.media.metadata.title : 'Unknown' }}
+ by {{ item.media.metadata.authorName }}
|
- {{ Math.floor(ab.progress * 100) }}% |
+ {{ Math.floor(item.progress * 100) }}% |
-
- {{ $dateDistanceFromNow(ab.startedAt) }}
+
+ {{ $dateDistanceFromNow(item.startedAt) }}
|
-
- {{ $dateDistanceFromNow(ab.lastUpdate) }}
+
+ {{ $dateDistanceFromNow(item.lastUpdate) }}
|
@@ -108,15 +111,8 @@ export default {
userOnline() {
return this.$store.getters['users/getIsUserOnline'](this.user.id)
},
- userAudiobooks() {
- return Object.values(this.user.audiobooks || {})
- .map((uab) => {
- return {
- id: uab.audiobookId,
- ...uab
- }
- })
- .sort((a, b) => b.lastUpdate - a.lastUpdate)
+ libraryItemProgress() {
+ return this.user.libraryItemProgress.sort((a, b) => b.lastUpdate - a.lastUpdate)
},
totalListeningTime() {
return this.listeningStats.totalTime || 0
@@ -169,7 +165,7 @@ export default {
.userAudiobooksTable tr:hover:not(:first-child) {
background-color: #474747;
}
-.userAudiobooksTable tr.isRead {
+.userAudiobooksTable tr.isFinished {
background-color: rgba(76, 175, 80, 0.1);
}
.userAudiobooksTable td {
diff --git a/client/players/AudioTrack.js b/client/players/AudioTrack.js
index 0b932741..529d5a73 100644
--- a/client/players/AudioTrack.js
+++ b/client/players/AudioTrack.js
@@ -1,29 +1,31 @@
export default class AudioTrack {
- constructor(track) {
+ constructor(track, userToken) {
this.index = track.index || 0
this.startOffset = track.startOffset || 0 // Total time of all previous tracks
this.duration = track.duration || 0
- this.title = track.metadata ? track.metadata.filename || '' : ''
+ this.title = track.title || ''
this.contentUrl = track.contentUrl || null
this.mimeType = track.mimeType
+
+ this.userToken = userToken
}
get fullContentUrl() {
if (!this.contentUrl || this.contentUrl.startsWith('http')) return this.contentUrl
if (process.env.NODE_ENV === 'development') {
- return `${process.env.serverUrl}${this.contentUrl}`
+ return `${process.env.serverUrl}${this.contentUrl}?token=${this.userToken}`
}
- return `${window.location.origin}${this.contentUrl}`
+ return `${window.location.origin}${this.contentUrl}?token=${this.userToken}`
}
get relativeContentUrl() {
if (!this.contentUrl || this.contentUrl.startsWith('http')) return this.contentUrl
if (process.env.NODE_ENV === 'development') {
- return `${process.env.serverUrl}${this.contentUrl}`
+ return `${process.env.serverUrl}${this.contentUrl}?token=${this.userToken}`
}
- return this.contentUrl
+ return this.contentUrl + '?token=${this.userToken}'
}
}
\ No newline at end of file
diff --git a/client/players/CastPlayer.js b/client/players/CastPlayer.js
index 7e7da44a..4a724173 100644
--- a/client/players/CastPlayer.js
+++ b/client/players/CastPlayer.js
@@ -12,17 +12,19 @@ export default class CastPlayer extends EventEmitter {
this.audiobook = null
this.audioTracks = []
this.currentTrackIndex = 0
- this.hlsStreamId = null
+ this.isHlsTranscode = null
this.currentTime = 0
this.playWhenReady = false
this.defaultPlaybackRate = 1
- this.playableMimetypes = {}
+ // TODO: Use canDisplayType on receiver to check mime types
+ this.playableMimeTypes = {}
this.coverUrl = ''
this.castPlayerState = 'IDLE'
// Supported audio codecs for chromecast
+
this.supportedAudioCodecs = ['opus', 'mp3', 'aac', 'flac', 'webma', 'wav']
this.initialize()
@@ -68,10 +70,10 @@ export default class CastPlayer extends EventEmitter {
}
}
- async set(audiobook, tracks, hlsStreamId, startTime, playWhenReady = false) {
+ async set(audiobook, tracks, isHlsTranscode, startTime, playWhenReady = false) {
this.audiobook = audiobook
this.audioTracks = tracks
- this.hlsStreamId = hlsStreamId
+ this.isHlsTranscode = isHlsTranscode
this.playWhenReady = playWhenReady
this.currentTime = startTime
diff --git a/client/players/LocalPlayer.js b/client/players/LocalPlayer.js
index 007b22b7..7c9086c4 100644
--- a/client/players/LocalPlayer.js
+++ b/client/players/LocalPlayer.js
@@ -11,7 +11,7 @@ export default class LocalPlayer extends EventEmitter {
this.libraryItem = null
this.audioTracks = []
this.currentTrackIndex = 0
- this.hlsStreamId = null
+ this.isHlsTranscode = null
this.hlsInstance = null
this.usingNativeplayer = false
this.startTime = 0
@@ -19,7 +19,7 @@ export default class LocalPlayer extends EventEmitter {
this.playWhenReady = false
this.defaultPlaybackRate = 1
- this.playableMimetypes = {}
+ this.playableMimeTypes = {}
this.initialize()
}
@@ -48,9 +48,9 @@ export default class LocalPlayer extends EventEmitter {
var mimeTypes = ['audio/flac', 'audio/mpeg', 'audio/mp4', 'audio/ogg', 'audio/aac']
mimeTypes.forEach((mt) => {
- this.playableMimetypes[mt] = this.player.canPlayType(mt)
+ this.playableMimeTypes[mt] = this.player.canPlayType(mt)
})
- console.log(`[LocalPlayer] Supported mime types`, this.playableMimetypes)
+ console.log(`[LocalPlayer] Supported mime types`, this.playableMimeTypes)
}
evtPlay() {
@@ -80,7 +80,7 @@ export default class LocalPlayer extends EventEmitter {
this.emit('error', error)
}
evtLoadedMetadata(data) {
- if (!this.hlsStreamId) {
+ if (!this.isHlsTranscode) {
this.player.currentTime = this.trackStartTime
}
@@ -97,23 +97,16 @@ export default class LocalPlayer extends EventEmitter {
}
destroy() {
- if (this.hlsStreamId) {
- // Close HLS Stream
- console.log('Closing HLS Streams', this.hlsStreamId)
- this.ctx.$axios.$post(`/api/streams/${this.hlsStreamId}/close`).catch((error) => {
- console.error('Failed to request close hls stream', this.hlsStreamId, error)
- })
- }
this.destroyHlsInstance()
if (this.player) {
this.player.remove()
}
}
- set(libraryItem, tracks, hlsStreamId, startTime, playWhenReady = false) {
+ set(libraryItem, tracks, isHlsTranscode, startTime, playWhenReady = false) {
this.libraryItem = libraryItem
this.audioTracks = tracks
- this.hlsStreamId = hlsStreamId
+ this.isHlsTranscode = isHlsTranscode
this.playWhenReady = playWhenReady
this.startTime = startTime
@@ -121,7 +114,7 @@ export default class LocalPlayer extends EventEmitter {
this.destroyHlsInstance()
}
- if (this.hlsStreamId) {
+ if (this.isHlsTranscode) {
this.setHlsStream()
} else {
this.setDirectPlay()
@@ -198,7 +191,7 @@ export default class LocalPlayer extends EventEmitter {
async resetStream(startTime) {
this.destroyHlsInstance()
await new Promise((resolve) => setTimeout(resolve, 1000))
- this.set(this.libraryItem, this.audioTracks, this.hlsStreamId, startTime, true)
+ this.set(this.libraryItem, this.audioTracks, this.isHlsTranscode, startTime, true)
}
playPause() {
@@ -234,7 +227,7 @@ export default class LocalPlayer extends EventEmitter {
seek(time) {
if (!this.player) return
- if (this.hlsStreamId) {
+ if (this.isHlsTranscode) {
// Seeking HLS stream
var offsetTime = time - (this.currentTrack.startOffset || 0)
this.player.currentTime = Math.max(0, offsetTime)
diff --git a/client/players/PlayerHandler.js b/client/players/PlayerHandler.js
index 444b9320..e5fae273 100644
--- a/client/players/PlayerHandler.js
+++ b/client/players/PlayerHandler.js
@@ -9,7 +9,8 @@ export default class PlayerHandler {
this.playWhenReady = false
this.player = null
this.playerState = 'IDLE'
- this.currentStreamId = null
+ this.isHlsTranscode = false
+ this.currentSessionId = null
this.startTime = 0
this.lastSyncTime = 0
@@ -35,11 +36,10 @@ export default class PlayerHandler {
return this.playerState === 'PLAYING'
}
- load(libraryItem, playWhenReady, startTime = 0) {
+ load(libraryItem, playWhenReady) {
if (!this.player) this.switchPlayer()
this.libraryItem = libraryItem
- this.startTime = startTime
this.playWhenReady = playWhenReady
this.prepare()
}
@@ -125,118 +125,61 @@ export default class PlayerHandler {
this.ctx.setBufferTime(buffertime)
}
- async prepare(forceHls = false) {
- var useHls = false
-
- var runningTotal = 0
-
- var audioTracks = (this.libraryItem.media.tracks || []).map((track) => {
- if (!track.metadata) {
- console.error('INVALID TRACK', track)
- return null
- }
- var audioTrack = new AudioTrack(track)
- audioTrack.startOffset = runningTotal
- audioTrack.contentUrl = `/s/item/${this.libraryItem.id}/${this.ctx.$encodeUriPath(track.metadata.relPath.replace(/^\//, ''))}?token=${this.userToken}`
- audioTrack.mimeType = this.getMimeTypeForTrack(track)
- audioTrack.canDirectPlay = !!this.player.playableMimetypes[audioTrack.mimeType]
-
- runningTotal += audioTrack.duration
- return audioTrack
+ async prepare(forceTranscode = false) {
+ var payload = {
+ supportedMimeTypes: Object.keys(this.player.playableMimeTypes),
+ mediaPlayer: this.isCasting ? 'chromecast' : 'html5',
+ forceTranscode,
+ forceDirectPlay: this.isCasting // TODO: add transcode support for chromecast
+ }
+ var session = await this.ctx.$axios.$post(`/api/items/${this.libraryItem.id}/play`, payload).catch((error) => {
+ console.error('Failed to start stream', error)
})
-
- // All html5 audio player plays use HLS unless experimental features is on
- if (!this.isCasting) {
- if (forceHls || !this.ctx.showExperimentalFeatures) {
- useHls = true
- } else {
- // Use HLS if any audio track cannot be direct played
- useHls = !!audioTracks.find(at => !at.canDirectPlay)
-
- if (useHls) {
- console.warn(`[PlayerHandler] An audio track cannot be direct played`, audioTracks.find(at => !at.canDirectPlay))
- }
- }
- }
-
-
- if (useHls) {
- var stream = await this.ctx.$axios.$get(`/api/items/${this.libraryItem.id}/stream`).catch((error) => {
- console.error('Failed to start stream', error)
- })
- if (stream) {
- console.log(`[PlayerHandler] prepare hls stream`, stream)
- this.setHlsStream(stream)
- } else {
- console.error(`[PlayerHandler] Failed to start HLS stream`)
- }
- } else {
- this.setDirectPlay(audioTracks)
- }
+ this.prepareSession(session)
}
- getMimeTypeForTrack(track) {
- var ext = track.metadata.ext
- if (ext === '.mp3' || ext === '.m4b' || ext === '.m4a') {
- return 'audio/mpeg'
- } else if (ext === '.mp4') {
- return 'audio/mp4'
- } else if (ext === '.ogg') {
- return 'audio/ogg'
- } else if (ext === '.aac' || ext === '.m4p') {
- return 'audio/aac'
- } else if (ext === '.flac') {
- return 'audio/flac'
+ prepareOpenSession(session) { // Session opened on init socket
+ if (!this.player) this.switchPlayer()
+
+ this.libraryItem = session.libraryItem
+ this.playWhenReady = false
+ this.prepareSession(session)
+ }
+
+ prepareSession(session) {
+ this.startTime = session.currentTime
+ this.currentSessionId = session.id
+
+ console.log('[PlayerHandler] Preparing Session', session)
+ var audioTracks = session.audioTracks.map(at => new AudioTrack(at, this.userToken))
+
+ this.ctx.playerLoading = true
+ this.isHlsTranscode = true
+ if (session.playMethod === this.ctx.$constants.PlayMethod.DIRECTPLAY) {
+ this.isHlsTranscode = false
}
- return 'audio/mpeg'
+
+ this.player.set(this.libraryItem, audioTracks, this.isHlsTranscode, this.startTime, this.playWhenReady)
}
closePlayer() {
console.log('[PlayerHandler] Close Player')
+ this.sendCloseSession()
if (this.player) {
this.player.destroy()
}
this.player = null
this.playerState = 'IDLE'
this.libraryItem = null
- this.currentStreamId = null
this.startTime = 0
this.stopPlayInterval()
}
- prepareStream(stream) {
- if (!this.player) this.switchPlayer()
- this.libraryItem = stream.libraryItem
- this.setHlsStream({
- streamId: stream.id,
- streamUrl: stream.clientPlaylistUri,
- startTime: stream.clientCurrentTime
- })
- }
-
- setHlsStream(stream) {
- this.currentStreamId = stream.streamId
- var audioTrack = new AudioTrack({
- duration: this.libraryItem.media.duration,
- contentUrl: stream.streamUrl + '?token=' + this.userToken,
- mimeType: 'application/vnd.apple.mpegurl'
- })
- this.startTime = stream.startTime
- this.ctx.playerLoading = true
- this.player.set(this.libraryItem, [audioTrack], this.currentStreamId, stream.startTime, this.playWhenReady)
- }
-
- setDirectPlay(audioTracks) {
- this.currentStreamId = null
- this.ctx.playerLoading = true
- this.player.set(this.libraryItem, audioTracks, null, this.startTime, this.playWhenReady)
- }
-
resetStream(startTime, streamId) {
- if (this.currentStreamId === streamId) {
+ if (this.isHlsTranscode && this.currentSessionId === streamId) {
this.player.resetStream(startTime)
} else {
- console.warn('resetStream mismatch streamId', this.currentStreamId, streamId)
+ console.warn('resetStream mismatch streamId', this.currentSessionId, streamId)
}
}
@@ -254,43 +197,39 @@ export default class PlayerHandler {
this.listeningTimeSinceSync += exactTimeElapsed
if (this.listeningTimeSinceSync >= 5) {
this.sendProgressSync(currentTime)
- this.listeningTimeSinceSync = 0
}
}, 1000)
}
+ sendCloseSession() {
+ var syncData = null
+ if (this.player) {
+ var listeningTimeToAdd = Math.max(0, Math.floor(this.listeningTimeSinceSync))
+ syncData = {
+ timeListened: listeningTimeToAdd,
+ currentTime: this.player.getCurrentTime()
+ }
+ }
+ this.listeningTimeSinceSync = 0
+ return this.ctx.$axios.$post(`/api/session/${this.currentSessionId}/close`, syncData, { timeout: 1000 }).catch((error) => {
+ console.error('Failed to close session', error)
+ })
+ }
+
sendProgressSync(currentTime) {
var diffSinceLastSync = Math.abs(this.lastSyncTime - currentTime)
if (diffSinceLastSync < 1) return
this.lastSyncTime = currentTime
- if (this.currentStreamId) { // Updating stream progress (HLS stream)
- var listeningTimeToAdd = Math.max(0, Math.floor(this.listeningTimeSinceSync))
- var syncData = {
- timeListened: listeningTimeToAdd,
- currentTime,
- streamId: this.currentStreamId,
- audiobookId: this.libraryItem.id
- }
- this.ctx.$axios.$post('/api/syncStream', syncData, { timeout: 1000 }).catch((error) => {
- console.error('Failed to update stream progress', error)
- })
- } else {
- // Direct play via chromecast does not yet have backend stream session model
- // so the progress update for the libraryItem is updated this way (instead of through the stream)
- var duration = this.getDuration()
- var syncData = {
- totalDuration: duration,
- currentTime,
- progress: duration > 0 ? currentTime / duration : 0,
- isRead: false,
- audiobookId: this.libraryItem.id,
- lastUpdate: Date.now()
- }
- this.ctx.$axios.$post('/api/syncLocal', syncData, { timeout: 1000 }).catch((error) => {
- console.error('Failed to update local progress', error)
- })
+ var listeningTimeToAdd = Math.max(0, Math.floor(this.listeningTimeSinceSync))
+ var syncData = {
+ timeListened: listeningTimeToAdd,
+ currentTime
}
+ this.listeningTimeSinceSync = 0
+ this.ctx.$axios.$post(`/api/session/${this.currentSessionId}/sync`, syncData, { timeout: 1000 }).catch((error) => {
+ console.error('Failed to update session progress', error)
+ })
}
stopPlayInterval() {
diff --git a/client/plugins/constants.js b/client/plugins/constants.js
index 83122781..32fe35f5 100644
--- a/client/plugins/constants.js
+++ b/client/plugins/constants.js
@@ -24,11 +24,18 @@ const BookshelfView = {
TITLES: 1
}
+const PlayMethod = {
+ DIRECTPLAY: 0,
+ DIRECTSTREAM: 1,
+ TRANSCODE: 2
+}
+
const Constants = {
SupportedFileTypes,
DownloadStatus,
BookCoverAspectRatio,
- BookshelfView
+ BookshelfView,
+ PlayMethod
}
const KeyNames = {
diff --git a/server/Db.js b/server/Db.js
index a3fdfe40..b13ad056 100644
--- a/server/Db.js
+++ b/server/Db.js
@@ -35,7 +35,6 @@ class Db {
this.libraryItems = []
this.users = []
- this.sessions = []
this.libraries = []
this.settings = []
this.collections = []
@@ -263,7 +262,7 @@ class Db {
Logger.debug(`[DB] Inserted ${results.inserted} ${entityName}`)
var arrayKey = this.getEntityArrayKey(entityName)
- this[arrayKey] = this[arrayKey].concat(entities)
+ if (this[arrayKey]) this[arrayKey] = this[arrayKey].concat(entities)
return true
}).catch((error) => {
Logger.error(`[DB] Failed to insert ${entityName}`, error)
@@ -277,7 +276,7 @@ class Db {
Logger.debug(`[DB] Inserted ${results.inserted} ${entityName}`)
var arrayKey = this.getEntityArrayKey(entityName)
- this[arrayKey].push(entity)
+ if (this[arrayKey]) this[arrayKey].push(entity)
return true
}).catch((error) => {
Logger.error(`[DB] Failed to insert ${entityName}`, error)
@@ -294,10 +293,12 @@ class Db {
}).then((results) => {
Logger.debug(`[DB] Updated ${entityName}: ${results.updated}`)
var arrayKey = this.getEntityArrayKey(entityName)
- this[arrayKey] = this[arrayKey].map(e => {
- if (entityIds.includes(e.id)) return entities.find(_e => _e.id === e.id)
- return e
- })
+ if (this[arrayKey]) {
+ this[arrayKey] = this[arrayKey].map(e => {
+ if (entityIds.includes(e.id)) return entities.find(_e => _e.id === e.id)
+ return e
+ })
+ }
return true
}).catch((error) => {
Logger.error(`[DB] Update ${entityName} Failed: ${error}`)
@@ -321,9 +322,11 @@ class Db {
}
var arrayKey = this.getEntityArrayKey(entityName)
- this[arrayKey] = this[arrayKey].map(e => {
- return e.id === entity.id ? entity : e
- })
+ if (this[arrayKey]) {
+ this[arrayKey] = this[arrayKey].map(e => {
+ return e.id === entity.id ? entity : e
+ })
+ }
return true
}).catch((error) => {
Logger.error(`[DB] Update entity ${entityName} Failed: ${error}`)
@@ -336,9 +339,11 @@ class Db {
return entityDb.delete((record) => record.id === entityId).then((results) => {
Logger.debug(`[DB] Deleted entity ${entityName}: ${results.deleted}`)
var arrayKey = this.getEntityArrayKey(entityName)
- this[arrayKey] = this[arrayKey].filter(e => {
- return e.id !== entityId
- })
+ if (this[arrayKey]) {
+ this[arrayKey] = this[arrayKey].filter(e => {
+ return e.id !== entityId
+ })
+ }
}).catch((error) => {
Logger.error(`[DB] Remove entity ${entityName} Failed: ${error}`)
})
diff --git a/server/PlaybackSessionManager.js b/server/PlaybackSessionManager.js
index 30303eef..6ca92d71 100644
--- a/server/PlaybackSessionManager.js
+++ b/server/PlaybackSessionManager.js
@@ -1,5 +1,8 @@
const Path = require('path')
+const { PlayMethod } = require('./utils/constants')
const PlaybackSession = require('./objects/PlaybackSession')
+const Stream = require('./objects/Stream')
+const Logger = require('./Logger')
class PlaybackSessionManager {
constructor(db, emitter, clientEmitter) {
@@ -11,25 +14,120 @@ class PlaybackSessionManager {
this.sessions = []
}
- async startSessionRequest(req, res) {
- var user = req.user
- var libraryItem = req.libraryItem
- var options = req.query || {}
- const session = await this.startSession(user, libraryItem, options)
- res.json(session)
+ getSession(sessionId) {
+ return this.sessions.find(s => s.id === sessionId)
+ }
+ getUserSession(userId) {
+ return this.sessions.find(s => s.userId === userId)
+ }
+ getStream(sessionId) {
+ var session = this.getSession(sessionId)
+ return session ? session.stream : null
}
- async startSession(user, libraryItem, options) {
- // TODO: Determine what play method to use and setup playback session
- // temporary client can pass direct=1 in query string for direct play
- if (options.direct) {
- var tracks = libraryItem.media.getDirectPlayTracklist(options)
+ async startSessionRequest(user, libraryItem, mediaEntity, options, res) {
+ const session = await this.startSession(user, libraryItem, mediaEntity, options)
+ res.json(session.toJSONForClient())
+ }
+
+ async syncSessionRequest(user, session, payload, res) {
+ await this.syncSession(user, session, payload)
+ res.json(session.toJSONForClient())
+ }
+
+ async closeSessionRequest(user, session, syncData, res) {
+ await this.closeSession(user, session, syncData)
+ res.sendStatus(200)
+ }
+
+ async startSession(user, libraryItem, mediaEntity, options) {
+ var shouldDirectPlay = options.forceDirectPlay || (!options.forceTranscode && mediaEntity.checkCanDirectPlay(options))
+
+ const userProgress = user.getLibraryItemProgress(libraryItem.id)
+ var userStartTime = 0
+ if (userProgress) userStartTime = userProgress.currentTime || 0
+ const newPlaybackSession = new PlaybackSession()
+ newPlaybackSession.setData(libraryItem, mediaEntity, user)
+
+ var audioTracks = []
+ if (shouldDirectPlay) {
+ Logger.debug(`[PlaybackSessionManager] "${user.username}" starting direct play session for media entity "${mediaEntity.id}"`)
+ audioTracks = mediaEntity.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))
+ await stream.generatePlaylist()
+ audioTracks = [stream.getAudioTrack()]
+ newPlaybackSession.stream = stream
+ newPlaybackSession.playMethod = PlayMethod.TRANSCODE
+ stream.on('closed', () => {
+ Logger.debug(`[PlaybackSessionManager] Stream closed for session "${newPlaybackSession.id}"`)
+ newPlaybackSession.stream = null
+ })
}
- const newPlaybackSession = new PlaybackSession()
- newPlaybackSession.setData(libraryItem, user)
+ newPlaybackSession.currentTime = userStartTime
+ newPlaybackSession.audioTracks = audioTracks
+
+ // Will save on the first sync
+ user.currentSessionId = newPlaybackSession.id
+
this.sessions.push(newPlaybackSession)
+ this.emitter('user_stream_update', user.toJSONForPublic(this.sessions, this.db.libraryItems))
+
return newPlaybackSession
}
+
+ async syncSession(user, session, syncData) {
+ session.currentTime = syncData.currentTime
+ session.addListeningTime(syncData.timeListened)
+ Logger.debug(`[PlaybackSessionManager] syncSession "${session.id}" | Total Time Listened: ${session.timeListening}`)
+
+ const itemProgressUpdate = {
+ currentTime: syncData.currentTime,
+ progress: session.progress
+ }
+ var wasUpdated = user.createUpdateLibraryItemProgress(session.libraryItemId, itemProgressUpdate)
+ if (wasUpdated) {
+ await this.db.updateEntity('user', user)
+ var itemProgress = user.getLibraryItemProgress(session.libraryItemId)
+ this.clientEmitter(user.id, 'user_item_progress_updated', {
+ id: itemProgress.id,
+ data: itemProgress.toJSON()
+ })
+ }
+ this.saveSession(session)
+ }
+
+ async closeSession(user, session, syncData = null) {
+ if (syncData) {
+ await this.syncSession(user, session, syncData)
+ } else {
+ await this.saveSession(session)
+ }
+ Logger.debug(`[PlaybackSessionManager] closeSession "${session.id}"`)
+ this.emitter('user_stream_update', user.toJSONForPublic(this.sessions, this.db.libraryItems))
+ return this.removeSession(session.id)
+ }
+
+ saveSession(session) {
+ if (session.lastSave) {
+ return this.db.updateEntity('session', session)
+ } else {
+ session.lastSave = Date.now()
+ return this.db.insertEntity('session', session)
+ }
+ }
+
+ async removeSession(sessionId) {
+ var session = this.sessions.find(s => s.id === sessionId)
+ if (!session) return
+ if (session.stream) {
+ await session.stream.close()
+ }
+ this.sessions = this.sessions.filter(s => s.id !== sessionId)
+ Logger.debug(`[PlaybackSessionManager] Removed session "${sessionId}"`)
+ }
}
module.exports = PlaybackSessionManager
\ No newline at end of file
diff --git a/server/Server.js b/server/Server.js
index 40d8e197..cf051b82 100644
--- a/server/Server.js
+++ b/server/Server.js
@@ -11,7 +11,6 @@ const { version } = require('../package.json')
// Utils
const { ScanResult } = require('./utils/constants')
const filePerms = require('./utils/filePerms')
-const { secondsToTimestamp } = require('./utils/index')
const dbMigration = require('./utils/dbMigration')
const Logger = require('./Logger')
@@ -22,9 +21,9 @@ const Scanner = require('./scanner/Scanner')
const Db = require('./Db')
const BackupManager = require('./BackupManager')
const LogManager = require('./LogManager')
-const ApiController = require('./ApiController')
-const HlsController = require('./HlsController')
-// const StreamManager = require('./objects/legacy/StreamManager')
+const ApiRouter = require('./routers/ApiRouter')
+const HlsRouter = require('./routers/HlsRouter')
+const StaticRouter = require('./routers/StaticRouter')
const PlaybackSessionManager = require('./PlaybackSessionManager')
const DownloadManager = require('./DownloadManager')
const CoverController = require('./CoverController')
@@ -58,12 +57,13 @@ class Server {
this.watcher = new Watcher()
this.coverController = new CoverController(this.db, this.cacheManager)
this.scanner = new Scanner(this.db, this.coverController, this.emitter.bind(this))
-
this.playbackSessionManager = new PlaybackSessionManager(this.db, this.emitter.bind(this), this.clientEmitter.bind(this))
- // this.streamManager = new StreamManager(this.db, this.emitter.bind(this), this.clientEmitter.bind(this))
this.downloadManager = new DownloadManager(this.db)
- this.apiController = new ApiController(this.db, this.auth, this.scanner, this.playbackSessionManager, this.downloadManager, this.coverController, this.backupManager, this.watcher, this.cacheManager, this.emitter.bind(this), this.clientEmitter.bind(this))
- this.hlsController = new HlsController(this.db, this.auth, this.playbackSessionManager, this.emitter.bind(this))
+
+ // Routers
+ this.apiRouter = new ApiRouter(this.db, this.auth, this.scanner, this.playbackSessionManager, this.downloadManager, this.coverController, this.backupManager, this.watcher, this.cacheManager, this.emitter.bind(this), this.clientEmitter.bind(this))
+ this.hlsRouter = new HlsRouter(this.db, this.auth, this.playbackSessionManager, this.emitter.bind(this))
+ this.staticRouter = new StaticRouter(this.db)
Logger.logManager = this.logManager
@@ -76,7 +76,7 @@ class Server {
get usersOnline() {
// TODO: Map open user sessions
return Object.values(this.clients).filter(c => c.user).map(client => {
- return client.user.toJSONForPublic([])
+ return client.user.toJSONForPublic(this.playbackSessionManager.sessions, this.db.libraryItems)
})
}
@@ -169,41 +169,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)
- if (!library) return res.sendStatus(404)
- var folder = library.folders.find(fol => fol.id === req.params.folder)
- if (!folder) return res.status(404).send('Folder not found')
-
- var remainingPath = req.params['0']
- var fullPath = Path.join(folder.fullPath, remainingPath)
- res.sendFile(fullPath)
- })
-
- // 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)
-
- var remainingPath = req.params['0']
- var fullPath = Path.join(audiobook.fullPath, remainingPath)
- 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)
- })
+ app.use('/api', this.authMiddleware.bind(this), this.apiRouter.router)
+ app.use('/hls', this.authMiddleware.bind(this), this.hlsRouter.router)
+ app.use('/s', this.authMiddleware.bind(this), this.staticRouter.router)
// EBook static file routes
app.get('/ebook/:library/:folder/*', (req, res) => {
@@ -267,14 +235,6 @@ class Server {
socket.on('scan_item', (libraryItemId) => this.scanLibraryItem(socket, libraryItemId))
socket.on('save_metadata', (libraryItemId) => this.saveMetadata(socket, libraryItemId))
- // 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
socket.on('download', (payload) => this.downloadManager.downloadSocketRequest(socket, payload))
socket.on('remove_download', (downloadId) => this.downloadManager.removeSocketRequest(socket, downloadId))
@@ -303,7 +263,7 @@ class Server {
delete this.clients[socket.id]
} else {
Logger.debug('[Server] User Offline ' + _client.user.username)
- this.io.emit('user_offline', _client.user.toJSONForPublic([]))
+ this.io.emit('user_offline', _client.user.toJSONForPublic(this.playbackSessionManager.sessions, this.db.libraryItems))
const disconnectTime = Date.now() - _client.connected_at
Logger.info(`[Server] Socket ${socket.id} disconnected from client "${_client.user.username}" after ${disconnectTime}ms`)
@@ -487,11 +447,10 @@ class Server {
if (client.user) {
Logger.debug('[Server] User Offline ' + client.user.username)
- this.io.emit('user_offline', client.user.toJSONForPublic(null))
+ this.io.emit('user_offline', client.user.toJSONForPublic(null, this.db.libraryItems))
}
delete this.clients[socketId].user
- delete this.clients[socketId].stream
if (clientSocket && clientSocket.sheepClient) delete this.clients[socketId].socket.sheepClient
} else if (socketId) {
Logger.warn(`[Server] No client for socket ${socketId}`)
@@ -604,19 +563,23 @@ class Server {
return
}
- // Check if user has stream open
- if (client.user.stream) {
- Logger.info('User has stream open already', client.user.stream)
- // client.stream = this.streamManager.getStream(client.user.stream)
- // if (!client.stream) {
- // Logger.error('Invalid user stream id', client.user.stream)
- // this.streamManager.removeOrphanStreamFiles(client.user.stream)
- // await this.db.updateUserStream(client.user.id, null)
- // }
+ // Check if user has session open
+ 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()
+ }
+ } else {
+ Logger.debug(`[Server] User Online ${client.user.username}`)
}
-
- Logger.debug(`[Server] User Online ${client.user.username}`)
- this.io.emit('user_online', client.user.toJSONForPublic([]))
+ this.io.emit('user_online', client.user.toJSONForPublic(this.playbackSessionManager.sessions, this.db.libraryItems))
user.lastSeen = Date.now()
await this.db.updateEntity('user', user)
@@ -627,7 +590,7 @@ class Server {
metadataPath: global.MetadataPath,
configPath: global.ConfigPath,
user: client.user.toJSONForBrowser(),
- stream: client.stream || null,
+ session,
librariesScanning: this.scanner.librariesScanning,
backups: (this.backupManager.backups || []).map(b => b.toJSON())
}
diff --git a/server/controllers/AudiobookController.js b/server/controllers/AudiobookController.js
deleted file mode 100644
index 71e3a481..00000000
--- a/server/controllers/AudiobookController.js
+++ /dev/null
@@ -1,57 +0,0 @@
-const Logger = require('../Logger')
-
-class AudiobookController {
- constructor() { }
-
- async findOne(req, res) {
- if (req.query.expanded == 1) return res.json(req.audiobook.toJSONExpanded())
- return res.json(req.audiobook)
- }
-
- async findWithItem(req, res) {
- if (req.query.expanded == 1) {
- return res.json({
- libraryItem: req.libraryItem.toJSONExpanded(),
- audiobook: req.audiobook.toJSONExpanded()
- })
- }
- res.json({
- libraryItem: req.libraryItem.toJSON(),
- audiobook: req.audiobook.toJSON()
- })
- }
-
- // PATCH: api/audiobooks/:id/tracks
- async updateTracks(req, res) {
- var libraryItem = req.libraryItem
- var audiobook = req.audiobook
- var orderedFileData = req.body.orderedFileData
- audiobook.updateAudioTracks(orderedFileData)
- await this.db.updateLibraryItem(libraryItem)
- this.emitter('item_updated', libraryItem.toJSONExpanded())
- res.json(libraryItem.toJSON())
- }
-
- middleware(req, res, next) {
- var audiobook = null
- var libraryItem = this.db.libraryItems.find(li => {
- if (li.mediaType != 'book') return false
- audiobook = li.media.getAudiobookById(req.params.id)
- return !!audiobook
- })
- if (!audiobook) return res.sendStatus(404)
-
- if (req.method == 'DELETE' && !req.user.canDelete) {
- Logger.warn(`[AudiobookController] 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('[AudiobookController] User attempted to update without permission', req.user)
- return res.sendStatus(403)
- }
-
- req.libraryItem = libraryItem
- req.audiobook = audiobook
- next()
- }
-}
-module.exports = new AudiobookController()
\ No newline at end of file
diff --git a/server/controllers/BackupController.js b/server/controllers/BackupController.js
index 7a47f7be..12445c61 100644
--- a/server/controllers/BackupController.js
+++ b/server/controllers/BackupController.js
@@ -5,7 +5,7 @@ class BackupController {
async delete(req, res) {
if (!req.user.isRoot) {
- Logger.error(`[ApiController] Non-Root user attempting to delete backup`, req.user)
+ Logger.error(`[BackupController] Non-Root user attempting to delete backup`, req.user)
return res.sendStatus(403)
}
var backup = this.backupManager.backups.find(b => b.id === req.params.id)
@@ -18,11 +18,11 @@ class BackupController {
async upload(req, res) {
if (!req.user.isRoot) {
- Logger.error(`[ApiController] Non-Root user attempting to upload backup`, req.user)
+ Logger.error(`[BackupController] Non-Root user attempting to upload backup`, req.user)
return res.sendStatus(403)
}
if (!req.files.file) {
- Logger.error('[ApiController] Upload backup invalid')
+ Logger.error('[BackupController] Upload backup invalid')
return res.sendStatus(500)
}
this.backupManager.uploadBackup(req, res)
diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js
index 02694e52..3aafc8d1 100644
--- a/server/controllers/LibraryController.js
+++ b/server/controllers/LibraryController.js
@@ -344,7 +344,7 @@ class LibraryController {
// PATCH: Change the order of libraries
async reorder(req, res) {
if (!req.user.isRoot) {
- Logger.error('[ApiController] ReorderLibraries invalid user', req.user)
+ Logger.error('[LibraryController] ReorderLibraries invalid user', req.user)
return res.sendStatus(403)
}
@@ -353,7 +353,7 @@ class LibraryController {
for (let i = 0; i < orderdata.length; i++) {
var library = this.db.libraries.find(lib => lib.id === orderdata[i].id)
if (!library) {
- Logger.error(`[ApiController] Invalid library not found in reorder ${orderdata[i].id}`)
+ Logger.error(`[LibraryController] Invalid library not found in reorder ${orderdata[i].id}`)
return res.sendStatus(500)
}
if (library.update({ displayOrder: orderdata[i].newOrder })) {
@@ -363,9 +363,9 @@ class LibraryController {
}
if (hasUpdates) {
- Logger.info(`[ApiController] Updated library display orders`)
+ Logger.info(`[LibraryController] Updated library display orders`)
} else {
- Logger.info(`[ApiController] Library orders were up to date`)
+ Logger.info(`[LibraryController] Library orders were up to date`)
}
var libraries = this.db.libraries.map(lib => lib.toJSON())
diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js
index 5ec64a4c..bc403f77 100644
--- a/server/controllers/LibraryItemController.js
+++ b/server/controllers/LibraryItemController.js
@@ -142,9 +142,16 @@ class LibraryItemController {
res.sendStatus(500)
}
- // GET: api/items/:id/play
+
+ // POST: api/items/:id/play
startPlaybackSession(req, res) {
- res.sendStatus(200)
+ var playbackMediaEntity = req.libraryItem.getPlaybackMediaEntity()
+ if (!playbackMediaEntity) {
+ Logger.error(`[LibraryItemController] startPlaybackSession no playback media entity ${req.libraryItem.id}`)
+ return res.sendStatus(404)
+ }
+ const options = req.body || {}
+ this.playbackSessionManager.startSessionRequest(req.user, req.libraryItem, playbackMediaEntity, options, res)
}
// POST api/items/:id/match
diff --git a/server/controllers/MediaEntityController.js b/server/controllers/MediaEntityController.js
new file mode 100644
index 00000000..9bbf271a
--- /dev/null
+++ b/server/controllers/MediaEntityController.js
@@ -0,0 +1,71 @@
+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/controllers/SessionController.js b/server/controllers/SessionController.js
new file mode 100644
index 00000000..544bf3dc
--- /dev/null
+++ b/server/controllers/SessionController.js
@@ -0,0 +1,33 @@
+const Logger = require('../Logger')
+
+class SessionController {
+ constructor() { }
+
+ async findOne(req, res) {
+ return res.json(req.session)
+ }
+
+ // POST: api/session/:id/sync
+ sync(req, res) {
+ this.playbackSessionManager.syncSessionRequest(req.user, req.session, req.body, res)
+ }
+
+ // POST: api/session/:id/close
+ close(req, res) {
+ this.playbackSessionManager.closeSessionRequest(req.user, req.session, req.body, res)
+ }
+
+ middleware(req, res, next) {
+ var playbackSession = this.playbackSessionManager.getSession(req.params.id)
+ if (!playbackSession) return res.sendStatus(404)
+
+ if (playbackSession.userId !== req.user.id) {
+ Logger.error(`[SessionController] User "${req.user.username}" attempting to access session belonging to another user "${req.params.id}"`)
+ return res.sendStatus(404)
+ }
+
+ req.session = playbackSession
+ next()
+ }
+}
+module.exports = new SessionController()
\ No newline at end of file
diff --git a/server/controllers/UserController.js b/server/controllers/UserController.js
index b8a82e89..334a3204 100644
--- a/server/controllers/UserController.js
+++ b/server/controllers/UserController.js
@@ -6,6 +6,26 @@ const { getId } = require('../utils/index')
class UserController {
constructor() { }
+ findAll(req, res) {
+ if (!req.user.isRoot) return res.sendStatus(403)
+ var users = this.db.users.map(u => this.userJsonWithItemProgressDetails(u))
+ res.json(users)
+ }
+
+ findOne(req, res) {
+ if (!req.user.isRoot) {
+ Logger.error('User other than root attempting to get user', req.user)
+ return res.sendStatus(403)
+ }
+
+ var user = this.db.users.find(u => u.id === req.params.id)
+ if (!user) {
+ return res.sendStatus(404)
+ }
+
+ res.json(this.userJsonWithItemProgressDetails(user))
+ }
+
async create(req, res) {
if (!req.user.isRoot) {
Logger.warn('Non-root user attempted to create user', req.user)
@@ -36,26 +56,6 @@ class UserController {
}
}
- findAll(req, res) {
- if (!req.user.isRoot) return res.sendStatus(403)
- var users = this.db.users.map(u => this.userJsonWithBookProgressDetails(u))
- res.json(users)
- }
-
- findOne(req, res) {
- if (!req.user.isRoot) {
- Logger.error('User other than root attempting to get user', req.user)
- return res.sendStatus(403)
- }
-
- var user = this.db.users.find(u => u.id === req.params.id)
- if (!user) {
- return res.sendStatus(404)
- }
-
- res.json(this.userJsonWithBookProgressDetails(user))
- }
-
async update(req, res) {
if (!req.user.isRoot) {
Logger.error('User other than root attempting to update user', req.user)
diff --git a/server/objects/LibraryItem.js b/server/objects/LibraryItem.js
index f763feae..88895236 100644
--- a/server/objects/LibraryItem.js
+++ b/server/objects/LibraryItem.js
@@ -434,8 +434,8 @@ class LibraryItem {
return this.media.searchQuery(query)
}
- getDirectPlayTracklist(options) {
- return this.media.getDirectPlayTracklist(options)
+ getPlaybackMediaEntity() {
+ return this.media.getPlaybackMediaEntity()
}
}
module.exports = LibraryItem
\ No newline at end of file
diff --git a/server/objects/PlaybackSession.js b/server/objects/PlaybackSession.js
index f6034920..6f191e13 100644
--- a/server/objects/PlaybackSession.js
+++ b/server/objects/PlaybackSession.js
@@ -9,8 +9,11 @@ class PlaybackSession {
this.id = null
this.userId = null
this.libraryItemId = null
+ this.mediaEntityId = null
+
this.mediaType = null
this.mediaMetadata = null
+ this.duration = null
this.playMethod = null
@@ -21,6 +24,12 @@ class PlaybackSession {
this.startedAt = null
this.updatedAt = null
+ // Not saved in DB
+ this.lastSave = 0
+ this.audioTracks = []
+ this.currentTime = 0
+ this.stream = null
+
if (session) {
this.construct(session)
}
@@ -32,8 +41,10 @@ class PlaybackSession {
sessionType: this.sessionType,
userId: this.userId,
libraryItemId: this.libraryItemId,
+ mediaEntityId: this.mediaEntityId,
mediaType: this.mediaType,
mediaMetadata: this.mediaMetadata ? this.mediaMetadata.toJSON() : null,
+ duration: this.duration,
playMethod: this.playMethod,
date: this.date,
dayOfWeek: this.dayOfWeek,
@@ -43,12 +54,35 @@ class PlaybackSession {
}
}
+ toJSONForClient() {
+ 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,
+ duration: this.duration,
+ playMethod: this.playMethod,
+ date: this.date,
+ dayOfWeek: this.dayOfWeek,
+ timeListening: this.timeListening,
+ lastUpdate: this.lastUpdate,
+ updatedAt: this.updatedAt,
+ audioTracks: this.audioTracks.map(at => at.toJSON()),
+ currentTime: this.currentTime
+ }
+ }
+
construct(session) {
this.id = session.id
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
this.mediaMetadata = null
@@ -68,30 +102,38 @@ class PlaybackSession {
this.updatedAt = session.updatedAt || null
}
- setData(libraryItem, user) {
- this.id = getId('ls')
+ get progress() { // Value between 0 and 1
+ if (!this.duration) return 0
+ return Math.max(0, Math.min(this.currentTime / this.duration, 1))
+ }
+
+ setData(libraryItem, mediaEntity, 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.playMethod = PlayMethod.TRANSCODE
+ this.duration = mediaEntity.duration
this.timeListening = 0
+ this.date = date.format(new Date(), 'YYYY-MM-DD')
+ this.dayOfWeek = date.format(new Date(), 'dddd')
this.startedAt = Date.now()
this.updatedAt = Date.now()
}
addListeningTime(timeListened) {
- if (timeListened && !isNaN(timeListened)) {
- if (!this.date) {
- // Set date info on first listening update
- this.date = date.format(new Date(), 'YYYY-MM-DD')
- this.dayOfWeek = date.format(new Date(), 'dddd')
- }
+ if (!timeListened || isNaN(timeListened)) return
- this.timeListening += timeListened
- this.updatedAt = Date.now()
+ if (!this.date) {
+ // Set date info on first listening update
+ this.date = date.format(new Date(), 'YYYY-MM-DD')
+ this.dayOfWeek = date.format(new Date(), 'dddd')
}
+
+ this.timeListening += timeListened
+ this.updatedAt = Date.now()
}
// New date since start of listening session
diff --git a/server/objects/Stream.js b/server/objects/Stream.js
index ac38e5a0..9f1a39fe 100644
--- a/server/objects/Stream.js
+++ b/server/objects/Stream.js
@@ -6,16 +6,17 @@ const Logger = require('../Logger')
const { getId, secondsToTimestamp } = require('../utils/index')
const { writeConcatFile } = require('../utils/ffmpegHelpers')
const hlsPlaylistGenerator = require('../utils/hlsPlaylistGenerator')
-
-const UserListeningSession = require('./legacy/UserListeningSession')
+const AudioTrack = require('./files/AudioTrack')
class Stream extends EventEmitter {
- constructor(streamPath, client, libraryItem, transcodeOptions = {}) {
+ constructor(sessionId, streamPath, user, libraryItem, mediaEntity, startTime, clientEmitter, transcodeOptions = {}) {
super()
- this.id = getId('str')
- this.client = client
+ this.id = sessionId
+ this.user = user
this.libraryItem = libraryItem
+ this.mediaEntity = mediaEntity
+ this.clientEmitter = clientEmitter
this.transcodeOptions = transcodeOptions
@@ -25,7 +26,7 @@ class Stream extends EventEmitter {
this.concatFilesPath = Path.join(this.streamPath, 'files.txt')
this.playlistPath = Path.join(this.streamPath, 'output.m3u8')
this.finalPlaylistPath = Path.join(this.streamPath, 'final-output.m3u8')
- this.startTime = 0
+ this.startTime = startTime
this.ffmpeg = null
this.loop = null
@@ -34,53 +35,49 @@ class Stream extends EventEmitter {
this.isTranscodeComplete = false
this.segmentsCreated = new Set()
this.furthestSegmentCreated = 0
- this.clientCurrentTime = 0
-
- this.listeningSession = new UserListeningSession()
- this.listeningSession.setData(libraryItem, client.user)
+ // this.clientCurrentTime = 0
this.init()
}
- get socket() {
- return this.client ? this.client.socket || null : null
- }
-
get libraryItemId() {
return this.libraryItem.id
}
-
+ get mediaTitle() {
+ return this.libraryItem.media.metadata.title || ''
+ }
+ get mediaEntityName() {
+ return this.mediaEntity.name
+ }
get itemTitle() {
- return this.libraryItem ? this.libraryItem.media.metadata.title : null
+ return `${this.mediaTitle} (${this.mediaEntityName})`
}
-
get totalDuration() {
- return this.libraryItem.media.duration
+ return this.mediaEntity.duration
+ }
+ get tracks() {
+ return this.mediaEntity.tracks
}
-
get tracksAudioFileType() {
if (!this.tracks.length) return null
- return this.tracks[0].metadata.ext.toLowerCase().slice(1)
+ return this.tracks[0].metadata.format
+ }
+ get userToken() {
+ return this.user.token
}
-
// Fmp4 does not work on iOS devices: https://github.com/advplyr/audiobookshelf-app/issues/85
// Workaround: Force AAC transcode for FLAC
get hlsSegmentType() {
return 'mpegts'
- // var hasFlac = this.tracks.find(t => t.ext.toLowerCase() === '.flac')
- // return hasFlac ? 'fmp4' : 'mpegts'
}
-
get segmentBasename() {
if (this.hlsSegmentType === 'fmp4') return 'output-%d.m4s'
return 'output-%d.ts'
}
-
get segmentStartNumber() {
if (!this.startTime) return 0
return Math.floor(Math.max(this.startTime - this.maxSeekBackTime, 0) / this.segmentLength)
}
-
get numSegments() {
var numSegs = Math.floor(this.totalDuration / this.segmentLength)
if (this.totalDuration - (numSegs * this.segmentLength) > 0) {
@@ -88,41 +85,17 @@ class Stream extends EventEmitter {
}
return numSegs
}
-
- get tracks() {
- return this.libraryItem.media.tracks
- }
-
- get clientUser() {
- return this.client ? this.client.user || {} : null
- }
-
- get userToken() {
- return this.clientUser ? this.clientUser.token : null
- }
-
- get clientUserAudiobooks() {
- return this.client ? this.clientUser.audiobooks || {} : null
- }
-
- get clientUserAudiobookData() {
- return this.client ? this.clientUserAudiobooks[this.libraryItemId] : null
- }
-
get clientPlaylistUri() {
return `/hls/${this.id}/output.m3u8`
}
-
- get clientProgress() {
- if (!this.clientCurrentTime) return 0
- var prog = Math.min(1, this.clientCurrentTime / this.totalDuration)
- return Number(prog.toFixed(3))
- }
-
+ // get clientProgress() {
+ // if (!this.clientCurrentTime) return 0
+ // var prog = Math.min(1, this.clientCurrentTime / this.totalDuration)
+ // return Number(prog.toFixed(3))
+ // }
get isAACEncodable() {
return ['mp4', 'm4a', 'm4b'].includes(this.tracksAudioFileType)
}
-
get transcodeForceAAC() {
return !!this.transcodeOptions.forceAAC
}
@@ -130,29 +103,28 @@ class Stream extends EventEmitter {
toJSON() {
return {
id: this.id,
- clientId: this.client.id,
- userId: this.client.user.id,
+ userId: this.user.id,
libraryItem: this.libraryItem.toJSONExpanded(),
segmentLength: this.segmentLength,
playlistPath: this.playlistPath,
clientPlaylistUri: this.clientPlaylistUri,
- clientCurrentTime: this.clientCurrentTime,
+ // clientCurrentTime: this.clientCurrentTime,
startTime: this.startTime,
segmentStartNumber: this.segmentStartNumber,
isTranscodeComplete: this.isTranscodeComplete,
- lastUpdate: this.clientUserAudiobookData ? this.clientUserAudiobookData.lastUpdate : 0
+ // lastUpdate: this.clientUserAudiobookData ? this.clientUserAudiobookData.lastUpdate : 0
}
}
init() {
- if (this.clientUserAudiobookData) {
- var timeRemaining = this.totalDuration - this.clientUserAudiobookData.currentTime
- Logger.info('[STREAM] User has progress for item', this.clientUserAudiobookData.progress, `Time Remaining: ${timeRemaining}s`)
- if (timeRemaining > 15) {
- this.startTime = this.clientUserAudiobookData.currentTime
- this.clientCurrentTime = this.startTime
- }
- }
+ // if (this.clientUserAudiobookData) {
+ // var timeRemaining = this.totalDuration - this.clientUserAudiobookData.currentTime
+ // Logger.info('[STREAM] User has progress for item', this.clientUserAudiobookData.progress, `Time Remaining: ${timeRemaining}s`)
+ // if (timeRemaining > 15) {
+ // this.startTime = this.clientUserAudiobookData.currentTime
+ // this.clientCurrentTime = this.startTime
+ // }
+ // }
}
async checkSegmentNumberRequest(segNum) {
@@ -175,39 +147,6 @@ class Stream extends EventEmitter {
return false
}
- syncStream({ timeListened, currentTime }) {
- var syncLog = ''
- // Set user current time
- if (currentTime !== null && !isNaN(currentTime)) {
- syncLog = `Update client current time ${secondsToTimestamp(currentTime)}`
- this.clientCurrentTime = currentTime
- }
-
- // Update user listening session
- var saveListeningSession = false
- if (timeListened && !isNaN(timeListened)) {
-
- // Check if listening session should roll to next day
- if (this.listeningSession.checkDateRollover()) {
- if (!this.clientUser) {
- Logger.error(`[Stream] Sync stream invalid client user`)
- return null
- }
- this.listeningSession = new UserListeningSession()
- this.listeningSession.setData(this.libraryItem, this.clientUser)
- Logger.debug(`[Stream] Listening session rolled to next day`)
- }
-
- this.listeningSession.addListeningTime(timeListened)
- if (syncLog) syncLog += ' | '
- syncLog += `Add listening time ${timeListened}s, Total time listened ${this.listeningSession.timeListening}s`
- saveListeningSession = true
- }
-
- Logger.debug('[Stream]', syncLog)
- return saveListeningSession ? this.listeningSession : null
- }
-
async generatePlaylist() {
fs.ensureDirSync(this.streamPath)
await hlsPlaylistGenerator(this.playlistPath, 'output', this.totalDuration, this.segmentLength, this.hlsSegmentType, this.userToken)
@@ -234,10 +173,8 @@ class Stream extends EventEmitter {
if (this.segmentsCreated.size > 6 && !this.isClientInitialized) {
this.isClientInitialized = true
- if (this.socket) {
- Logger.info(`[STREAM] ${this.id} notifying client that stream is ready`)
- this.socket.emit('stream_open', this.toJSON())
- }
+ Logger.info(`[STREAM] ${this.id} notifying client that stream is ready`)
+ this.clientEmit('stream_open', this.toJSON())
}
var chunks = []
@@ -270,33 +207,27 @@ class Stream extends EventEmitter {
Logger.info('[STREAM-CHECK] Check Files', this.segmentsCreated.size, 'of', this.numSegments, perc, `Furthest Segment: ${this.furthestSegmentCreated}`)
// Logger.debug('[STREAM-CHECK] Chunks', chunks.join(', '))
- if (this.socket) {
- this.socket.emit('stream_progress', {
- stream: this.id,
- percent: perc,
- chunks,
- numSegments: this.numSegments
- })
- }
+ this.clientEmit('stream_progress', {
+ stream: this.id,
+ percent: perc,
+ chunks,
+ numSegments: this.numSegments
+ })
} catch (error) {
Logger.error('Failed checking files', error)
}
}
startLoop() {
- if (this.socket) {
- this.socket.emit('stream_progress', { stream: this.id, chunks: [], numSegments: 0, percent: '0%' })
- }
+ this.clientEmit('stream_progress', { stream: this.id, chunks: [], numSegments: 0, percent: '0%' })
clearInterval(this.loop)
var intervalId = setInterval(() => {
if (!this.isTranscodeComplete) {
this.checkFiles()
} else {
- if (this.socket) {
- Logger.info(`[Stream] ${this.itemTitle} sending stream_ready`)
- this.socket.emit('stream_ready')
- }
+ Logger.info(`[Stream] ${this.itemTitle} sending stream_ready`)
+ this.clientEmit('stream_ready')
clearInterval(intervalId)
}
}, 2000)
@@ -409,10 +340,10 @@ class Stream extends EventEmitter {
// For very small fast load
if (!this.isClientInitialized) {
this.isClientInitialized = true
- if (this.socket) {
- Logger.info(`[STREAM] ${this.id} notifying client that stream is ready`)
- this.socket.emit('stream_open', this.toJSON())
- }
+
+ Logger.info(`[STREAM] ${this.id} notifying client that stream is ready`)
+ this.clientEmit('stream_open', this.toJSON())
+
}
this.isTranscodeComplete = true
this.ffmpeg = null
@@ -436,10 +367,8 @@ class Stream extends EventEmitter {
Logger.error('Failed to delete session data', err)
})
- if (this.socket) {
- if (errorMessage) this.socket.emit('stream_error', { id: this.id, error: (errorMessage || '').trim() })
- else this.socket.emit('stream_closed', this.id)
- }
+ if (errorMessage) this.clientEmit('stream_error', { id: this.id, error: (errorMessage || '').trim() })
+ else this.clientEmit('stream_closed', this.id)
this.emit('closed')
}
@@ -474,9 +403,19 @@ class Stream extends EventEmitter {
this.isTranscodeComplete = false
this.startTime = time
- this.clientCurrentTime = this.startTime
+ // this.clientCurrentTime = this.startTime
Logger.info(`Stream Reset New Start Time ${secondsToTimestamp(this.startTime)}`)
this.start()
}
+
+ clientEmit(evtName, data) {
+ if (this.clientEmitter) this.clientEmitter(this.user.id, evtName, data)
+ }
+
+ getAudioTrack() {
+ var newAudioTrack = new AudioTrack()
+ newAudioTrack.setFromStream(this.itemTitle, this.totalDuration, this.clientPlaylistUri)
+ return newAudioTrack
+ }
}
module.exports = Stream
\ No newline at end of file
diff --git a/server/objects/entities/Audiobook.js b/server/objects/entities/Audiobook.js
index 86cbf9e5..575e2bc6 100644
--- a/server/objects/entities/Audiobook.js
+++ b/server/objects/entities/Audiobook.js
@@ -1,6 +1,7 @@
const Path = require('path')
const AudioFile = require('../files/AudioFile')
const { areEquivalent, copyValue } = require('../../utils/index')
+const AudioTrack = require('../files/AudioTrack')
class Audiobook {
constructor(audiobook) {
@@ -74,6 +75,7 @@ class Audiobook {
}
}
+ get isPlaybackMediaEntity() { return true }
get tracks() {
return this.audioFiles.filter(af => !af.exclude && !af.invalid)
}
@@ -214,5 +216,25 @@ class Audiobook {
removeFileWithInode(inode) {
this.audioFiles = this.audioFiles.filter(af => af.ino !== inode)
}
+
+ // 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 = Audiobook
\ No newline at end of file
diff --git a/server/objects/entities/EBook.js b/server/objects/entities/EBook.js
index f91c2511..74d5ca22 100644
--- a/server/objects/entities/EBook.js
+++ b/server/objects/entities/EBook.js
@@ -47,7 +47,7 @@ class EBook {
}
}
- toJSONMinified() {
+ toJSONExpanded() {
return {
id: this.id,
index: this.index,
@@ -59,6 +59,7 @@ class EBook {
}
}
+ get isPlaybackMediaEntity() { return false }
get size() {
return this.ebookFile.metadata.size
}
diff --git a/server/objects/entities/PodcastEpisode.js b/server/objects/entities/PodcastEpisode.js
index 505a6b4a..cc961920 100644
--- a/server/objects/entities/PodcastEpisode.js
+++ b/server/objects/entities/PodcastEpisode.js
@@ -1,4 +1,5 @@
const AudioFile = require('../files/AudioFile')
+const AudioTrack = require('../files/AudioTrack')
class PodcastEpisode {
constructor(episode) {
@@ -37,5 +38,22 @@ class PodcastEpisode {
updatedAt: this.updatedAt
}
}
+
+ get isPlaybackMediaEntity() { return true }
+ get tracks() {
+ return [this.audioFile]
+ }
+
+ // Only checks container format
+ checkCanDirectPlay(payload) {
+ var supportedMimeTypes = payload.supportedMimeTypes || []
+ return supportedMimeTypes.includes(this.audioFile.mimeType)
+ }
+
+ getDirectPlayTracklist(libraryItemId) {
+ var audioTrack = new AudioTrack()
+ audioTrack.setData(libraryItemId, this.audioFile, 0)
+ return [audioTrack]
+ }
}
module.exports = PodcastEpisode
\ No newline at end of file
diff --git a/server/objects/files/AudioFile.js b/server/objects/files/AudioFile.js
index 92d3ab8e..0ecc5fb1 100644
--- a/server/objects/files/AudioFile.js
+++ b/server/objects/files/AudioFile.js
@@ -64,7 +64,8 @@ class AudioFile {
channelLayout: this.channelLayout,
chapters: this.chapters,
embeddedCoverArt: this.embeddedCoverArt,
- metaTags: this.metaTags ? this.metaTags.toJSON() : {}
+ metaTags: this.metaTags ? this.metaTags.toJSON() : {},
+ mimeType: this.mimeType
}
}
@@ -72,9 +73,6 @@ class AudioFile {
this.index = data.index
this.ino = data.ino
this.metadata = new FileMetadata(data.metadata || {})
- if (!this.metadata.toJSON) {
- console.error('No metadata tojosnm\n\n\n\n\n\n', this)
- }
this.addedAt = data.addedAt
this.updatedAt = data.updatedAt
this.manuallyVerified = !!data.manuallyVerified
@@ -103,6 +101,22 @@ class AudioFile {
this.metaTags = new AudioMetaTags(data.metaTags || {})
}
+ get mimeType() {
+ var ext = this.metadata.ext
+ if (ext === '.mp3' || ext === '.m4b' || ext === '.m4a') {
+ return 'audio/mpeg'
+ } else if (ext === '.mp4') {
+ return 'audio/mp4'
+ } else if (ext === '.ogg') {
+ return 'audio/ogg'
+ } else if (ext === '.aac' || ext === '.m4p') {
+ return 'audio/aac'
+ } else if (ext === '.flac') {
+ return 'audio/flac'
+ }
+ return 'audio/mpeg'
+ }
+
// New scanner creates AudioFile from AudioFileScanner
setDataFromProbe(libraryFile, probeData) {
this.ino = libraryFile.ino || null
diff --git a/server/objects/files/AudioTrack.js b/server/objects/files/AudioTrack.js
new file mode 100644
index 00000000..52711b64
--- /dev/null
+++ b/server/objects/files/AudioTrack.js
@@ -0,0 +1,42 @@
+const Path = require('path')
+
+class AudioTrack {
+ constructor() {
+ this.index = null
+ this.startOffset = null
+ this.duration = null
+ this.title = null
+ this.contentUrl = null
+ this.mimeType = null
+ }
+
+ toJSON() {
+ return {
+ index: this.index,
+ startOffset: this.startOffset,
+ duration: this.duration,
+ title: this.title,
+ contentUrl: this.contentUrl,
+ mimeType: this.mimeType
+ }
+ }
+
+ setData(itemId, audioFile, startOffset) {
+ this.index = audioFile.index
+ this.startOffset = startOffset
+ this.duration = audioFile.duration
+ this.title = audioFile.metadata.filename || ''
+ this.contentUrl = Path.join(`/s/item/${itemId}`, audioFile.metadata.relPath)
+ this.mimeType = audioFile.mimeType
+ }
+
+ setFromStream(title, duration, contentUrl) {
+ this.index = 1
+ this.startOffset = 0
+ this.duration = duration
+ this.title = title
+ this.contentUrl = contentUrl
+ this.mimeType = 'application/vnd.apple.mpegurl'
+ }
+}
+module.exports = AudioTrack
\ No newline at end of file
diff --git a/server/objects/legacy/StreamManager.js b/server/objects/legacy/StreamManager.js
index 7ae6c3f4..89dfc0e3 100644
--- a/server/objects/legacy/StreamManager.js
+++ b/server/objects/legacy/StreamManager.js
@@ -108,8 +108,6 @@ class StreamManager {
var stream = await this.openStream(client, libraryItem)
this.db.updateUserStream(client.user.id, stream.id)
-
- this.emitter('user_stream_update', client.user.toJSONForPublic(this.streams))
}
async closeStreamRequest(socket) {
@@ -125,8 +123,6 @@ class StreamManager {
client.user.stream = null
client.stream = null
this.db.updateUserStream(client.user.id, null)
-
- this.emitter('user_stream_update', client.user.toJSONForPublic(this.streams))
}
async closeStreamApiRequest(userId, streamId) {
diff --git a/server/objects/mediaTypes/Book.js b/server/objects/mediaTypes/Book.js
index 127b53fb..f7f7ae22 100644
--- a/server/objects/mediaTypes/Book.js
+++ b/server/objects/mediaTypes/Book.js
@@ -119,6 +119,15 @@ class Book {
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))
@@ -262,9 +271,5 @@ class Book {
// newEbook.setData(libraryFile)
// this.ebookFiles.push(newEbook)
}
-
- getDirectPlayTracklist(options) {
-
- }
}
module.exports = Book
\ No newline at end of file
diff --git a/server/objects/mediaTypes/Podcast.js b/server/objects/mediaTypes/Podcast.js
index 9d4d7a6e..3107eb7e 100644
--- a/server/objects/mediaTypes/Podcast.js
+++ b/server/objects/mediaTypes/Podcast.js
@@ -117,6 +117,14 @@ 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(scanMediaMetadata) {
this.metadata = new PodcastMetadata()
this.metadata.setData(scanMediaMetadata)
@@ -130,9 +138,5 @@ class Podcast {
var payload = this.metadata.searchQuery(query)
return payload || {}
}
-
- getDirectPlayTracklist(options) {
-
- }
}
module.exports = Podcast
\ No newline at end of file
diff --git a/server/objects/metadata/BookMetadata.js b/server/objects/metadata/BookMetadata.js
index c2988395..89dcd334 100644
--- a/server/objects/metadata/BookMetadata.js
+++ b/server/objects/metadata/BookMetadata.js
@@ -37,7 +37,7 @@ class BookMetadata {
this.isbn = metadata.isbn
this.asin = metadata.asin
this.language = metadata.language
- this.explicit = metadata.explicit
+ this.explicit = !!metadata.explicit
}
toJSON() {
@@ -150,6 +150,7 @@ class BookMetadata {
this.asin = scanMediaData.asin || null
this.language = scanMediaData.language || null
this.genres = []
+ this.explicit = !!scanMediaData.explicit
if (scanMediaData.author) {
this.authors = this.parseAuthorsTag(scanMediaData.author)
diff --git a/server/objects/metadata/FileMetadata.js b/server/objects/metadata/FileMetadata.js
index d2226cdd..9120fea6 100644
--- a/server/objects/metadata/FileMetadata.js
+++ b/server/objects/metadata/FileMetadata.js
@@ -47,7 +47,7 @@ class FileMetadata {
get format() {
if (!this.ext) return ''
- return this.ext.slice(1)
+ return this.ext.slice(1).toLowerCase()
}
get filenameNoExt() {
return this.filename.replace(this.ext, '')
diff --git a/server/objects/user/LibraryItemProgress.js b/server/objects/user/LibraryItemProgress.js
index b57fda81..127268ea 100644
--- a/server/objects/user/LibraryItemProgress.js
+++ b/server/objects/user/LibraryItemProgress.js
@@ -42,27 +42,8 @@ class LibraryItemProgress {
this.finishedAt = progress.finishedAt || null
}
- updateProgressFromStream(stream) {
- // this.audiobookId = stream.libraryItemId
- // this.totalDuration = stream.totalDuration
- // this.progress = stream.clientProgress
- // this.currentTime = stream.clientCurrentTime
- // this.lastUpdate = Date.now()
-
- // if (!this.startedAt) {
- // this.startedAt = Date.now()
- // }
-
- // // If has < 10 seconds remaining mark as read
- // var timeRemaining = this.totalDuration - this.currentTime
- // if (timeRemaining < 10) {
- // this.isFinished = true
- // this.progress = 1
- // this.finishedAt = Date.now()
- // } else {
- // this.isFinished = false
- // this.finishedAt = null
- // }
+ get inProgress() {
+ return !this.isFinished && this.progress > 0
}
setData(libraryItemId, progress) {
diff --git a/server/objects/user/User.js b/server/objects/user/User.js
index 5886935f..8d2b05fb 100644
--- a/server/objects/user/User.js
+++ b/server/objects/user/User.js
@@ -8,7 +8,6 @@ class User {
this.username = null
this.pash = null
this.type = null
- this.stream = null
this.token = null
this.isActive = true
this.isLocked = false
@@ -79,7 +78,6 @@ class User {
username: this.username,
pash: this.pash,
type: this.type,
- stream: this.stream,
token: this.token,
libraryItemProgress: this.libraryItemProgress ? this.libraryItemProgress.map(li => li.toJSON()) : [],
bookmarks: this.bookmarks ? this.bookmarks.map(b => b.toJSON()) : [],
@@ -98,7 +96,6 @@ class User {
id: this.id,
username: this.username,
type: this.type,
- stream: this.stream,
token: this.token,
libraryItemProgress: this.libraryItemProgress ? this.libraryItemProgress.map(li => li.toJSON()) : [],
isActive: this.isActive,
@@ -112,13 +109,14 @@ class User {
}
// Data broadcasted
- toJSONForPublic(streams) {
- var stream = this.stream && streams ? streams.find(s => s.id === this.stream) : null
+ toJSONForPublic(sessions, libraryItems) {
+ var session = sessions ? sessions.find(s => s.userId === this.id) : null
return {
id: this.id,
username: this.username,
type: this.type,
- stream: stream ? stream.toJSON() : null,
+ session: session ? session.toJSONForClient() : null,
+ mostRecent: this.getMostRecentItemProgress(libraryItems),
lastSeen: this.lastSeen,
createdAt: this.createdAt
}
@@ -129,12 +127,11 @@ class User {
this.username = user.username
this.pash = user.pash
this.type = user.type
- this.stream = user.stream || null
this.token = user.token
this.libraryItemProgress = []
if (user.libraryItemProgress) {
- this.libraryItemProgress = user.libraryItemProgress.map(li => new LibraryItemProgress(li))
+ this.libraryItemProgress = user.libraryItemProgress.map(li => new LibraryItemProgress(li)).filter(lip => lip.id)
}
this.bookmarks = []
@@ -195,13 +192,22 @@ class User {
return hasUpdates
}
- updateAudiobookProgressFromStream(stream) {
- // if (!this.audiobooks) this.audiobooks = {}
- // if (!this.audiobooks[stream.audiobookId]) {
- // this.audiobooks[stream.audiobookId] = new UserAudiobookData()
- // }
- // this.audiobooks[stream.audiobookId].updateProgressFromStream(stream)
- // return this.audiobooks[stream.audiobookId]
+ getMostRecentItemProgress(libraryItems) {
+ if (!this.libraryItemProgress.length) return null
+ var lip = this.libraryItemProgress.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
+ var libraryItem = libraryItems.find(li => li.id === mostRecentWithLip.id)
+ return {
+ ...mostRecentWithLip,
+ media: libraryItem.media.toJSONExpanded()
+ }
+ }
+
+ getLibraryItemProgress(libraryItemId) {
+ if (!this.libraryItemProgress) return null
+ return this.libraryItemProgress.find(lip => lip.id === libraryItemId)
}
createUpdateLibraryItemProgress(libraryItemId, updatePayload) {
@@ -254,12 +260,6 @@ class User {
return this.librariesAccessible.includes(libraryId)
}
- getLibraryItemProgress(libraryItemId) {
- if (!this.libraryItemProgress) return null
- var progress = this.libraryItemProgress.find(lip => lip.id === libraryItemId)
- return progress ? progress.toJSON() : null
- }
-
createBookmark({ libraryItemId, time, title }) {
// if (!this.audiobooks) this.audiobooks = {}
// if (!this.audiobooks[audiobookId]) {
diff --git a/server/ApiController.js b/server/routers/ApiRouter.js
similarity index 80%
rename from server/ApiController.js
rename to server/routers/ApiRouter.js
index bc70a154..5c48bb79 100644
--- a/server/ApiController.js
+++ b/server/routers/ApiRouter.js
@@ -4,29 +4,30 @@ const fs = require('fs-extra')
const date = require('date-and-time')
const axios = require('axios')
-const Logger = require('./Logger')
-const { isObject } = require('./utils/index')
-const { parsePodcastRssFeedXml } = require('./utils/podcastUtils')
+const Logger = require('../Logger')
+const { isObject } = require('../utils/index')
+const { parsePodcastRssFeedXml } = require('../utils/podcastUtils')
-const LibraryController = require('./controllers/LibraryController')
-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 SeriesController = require('./controllers/SeriesController')
-const AuthorController = require('./controllers/AuthorController')
-const AudiobookController = require('./controllers/AudiobookController')
+const LibraryController = require('../controllers/LibraryController')
+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 SeriesController = require('../controllers/SeriesController')
+const AuthorController = require('../controllers/AuthorController')
+const MediaEntityController = require('../controllers/MediaEntityController')
+const SessionController = require('../controllers/SessionController')
-const BookFinder = require('./finders/BookFinder')
-const AuthorFinder = require('./finders/AuthorFinder')
-const PodcastFinder = require('./finders/PodcastFinder')
+const BookFinder = require('../finders/BookFinder')
+const AuthorFinder = require('../finders/AuthorFinder')
+const PodcastFinder = require('../finders/PodcastFinder')
-const Author = require('./objects/entities/Author')
-const Series = require('./objects/entities/Series')
-const FileSystemController = require('./controllers/FileSystemController')
+const Author = require('../objects/entities/Author')
+const Series = require('../objects/entities/Series')
+const FileSystemController = require('../controllers/FileSystemController')
-class ApiController {
+class ApiRouter {
constructor(db, auth, scanner, playbackSessionManager, downloadManager, coverController, backupManager, watcher, cacheManager, emitter, clientEmitter) {
this.db = db
this.auth = auth
@@ -72,11 +73,12 @@ class ApiController {
this.router.post('/libraries/order', LibraryController.reorder.bind(this))
//
- // Audiobook Routes
+ // Media Entity Routes
//
- this.router.get('/audiobooks/:id', AudiobookController.middleware.bind(this), AudiobookController.findOne.bind(this))
- this.router.get('/audiobooks/:id/item', AudiobookController.middleware.bind(this), AudiobookController.findWithItem.bind(this))
- this.router.patch('/audiobooks/:id/tracks', AudiobookController.middleware.bind(this), AudiobookController.updateTracks.bind(this))
+ 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
@@ -92,13 +94,11 @@ class ApiController {
this.router.patch('/items/:id/cover', LibraryItemController.middleware.bind(this), LibraryItemController.updateCover.bind(this))
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.get('/items/:id/play', LibraryItemController.middleware.bind(this), LibraryItemController.startPlaybackSession.bind(this))
+ this.router.post('/items/:id/play', LibraryItemController.middleware.bind(this), LibraryItemController.startPlaybackSession.bind(this))
this.router.post('/items/batch/delete', LibraryItemController.batchDelete.bind(this))
this.router.post('/items/batch/update', LibraryItemController.batchUpdate.bind(this))
this.router.post('/items/batch/get', LibraryItemController.batchGet.bind(this))
- // Legacy
- this.router.get('/items/:id/stream', LibraryItemController.middleware.bind(this), LibraryItemController.openStream.bind(this))
//
// User Routes
@@ -171,6 +171,12 @@ class ApiController {
this.router.get('/series/search', SeriesController.search.bind(this))
this.router.get('/series/:id', SeriesController.middleware.bind(this), SeriesController.findOne.bind(this))
+ //
+ // Playback Session Routes
+ //
+ this.router.post('/session/:id/sync', SessionController.middleware.bind(this), SessionController.sync.bind(this))
+ this.router.post('/session/:id/close', SessionController.middleware.bind(this), SessionController.close.bind(this))
+
//
// Misc Routes
//
@@ -180,14 +186,10 @@ class ApiController {
this.router.get('/download/:id', this.download.bind(this))
- this.router.post('/syncUserAudiobookData', this.syncUserAudiobookData.bind(this))
-
this.router.post('/purgecache', this.purgeCache.bind(this))
- this.router.post('/syncStream', this.syncStream.bind(this))
- this.router.post('/syncLocal', this.syncLocal.bind(this))
-
- this.router.post('/streams/:id/close', this.closeStream.bind(this))
+ // OLD
+ // this.router.post('/syncUserAudiobookData', this.syncUserAudiobookData.bind(this))
this.router.post('/getPodcastFeed', this.getPodcastFeed.bind(this))
}
@@ -337,45 +339,21 @@ class ApiController {
// res.json(allUserAudiobookData)
}
- // Sync audiobook stream progress
- async syncStream(req, res) {
- Logger.debug(`[ApiController] syncStream for ${req.user.username} - ${req.body.streamId}`)
- // this.streamManager.streamSyncFromApi(req, res)
- res.sendStatus(500)
- }
-
- // Sync local downloaded audiobook progress
- async syncLocal(req, res) {
- // Logger.debug(`[ApiController] syncLocal for ${req.user.username}`)
- // var progressPayload = req.body
- // var itemProgress = req.user.updateLibraryItemProgress(progressPayload.libraryItemId, progressPayload)
- // if (itemProgress) {
- // await this.db.updateEntity('user', req.user)
- // this.clientEmitter(req.user.id, 'current_user_audiobook_update', {
- // id: progressPayload.libraryItemId,
- // data: itemProgress || null
- // })
- // }
- res.sendStatus(200)
- }
-
//
// Helper Methods
//
- userJsonWithBookProgressDetails(user) {
+ userJsonWithItemProgressDetails(user) {
var json = user.toJSONForBrowser()
- // User audiobook progress attach book details
- if (json.audiobooks && Object.keys(json.audiobooks).length) {
- for (const audiobookId in json.audiobooks) {
- var libraryItem = this.db.libraryItems.find(li => li.id === audiobookId)
- if (!libraryItem) {
- Logger.error('[ApiController] Library item not found for users progress ' + audiobookId)
- } else {
- json.audiobooks[audiobookId].media = libraryItem.media.toJSONExpanded()
- }
+ json.libraryItemProgress = json.libraryItemProgress.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)
+ return null
}
- }
+ lip.media = libraryItem.media.toJSONExpanded()
+ return lip
+ }).filter(lip => !!lip)
return json
}
@@ -425,8 +403,7 @@ class ApiController {
async getUserListeningSessionsHelper(userId) {
var userSessions = await this.db.selectUserSessions(userId)
- var listeningSessions = userSessions.filter(us => us.sessionType === 'listeningSession')
- return listeningSessions.sort((a, b) => b.lastUpdate - a.lastUpdate)
+ return userSessions.sort((a, b) => b.updatedAt - a.updatedAt)
}
async getUserListeningStatsHelpers(userId) {
@@ -435,7 +412,7 @@ class ApiController {
var listeningSessions = await this.getUserListeningSessionsHelper(userId)
var listeningStats = {
totalTime: 0,
- books: {},
+ items: {},
days: {},
dayOfWeek: {},
today: 0,
@@ -454,16 +431,15 @@ class ApiController {
listeningStats.today += s.timeListening
}
}
- if (!listeningStats.books[s.audiobookId]) {
- listeningStats.books[s.audiobookId] = {
- id: s.audiobookId,
+ if (!listeningStats.items[s.libraryItemId]) {
+ listeningStats.items[s.libraryItemId] = {
+ id: s.libraryItemId,
timeListening: s.timeListening,
- title: s.audiobookTitle,
- author: s.audiobookAuthor,
+ mediaMetadata: s.mediaMetadata,
lastUpdate: s.lastUpdate
}
} else {
- listeningStats.books[s.audiobookId].timeListening += s.timeListening
+ listeningStats.items[s.libraryItemId].timeListening += s.timeListening
}
listeningStats.totalTime += s.timeListening
@@ -475,18 +451,11 @@ class ApiController {
if (!req.user.isRoot) {
return res.sendStatus(403)
}
- Logger.info(`[ApiController] Purging all cache`)
+ Logger.info(`[ApiRouter] Purging all cache`)
await this.cacheManager.purgeAll()
res.sendStatus(200)
}
- async closeStream(req, res) {
- const streamId = req.params.id
- const userId = req.user.id
- // this.streamManager.closeStreamApiRequest(userId, streamId)
- res.sendStatus(200)
- }
-
async createAuthorsAndSeriesForItemUpdate(mediaPayload) {
if (mediaPayload.metadata) {
var mediaMetadata = mediaPayload.metadata
@@ -501,7 +470,7 @@ class ApiController {
if (!author) {
author = new Author()
author.setData(mediaMetadata.authors[i])
- Logger.debug(`[ApiController] Created new author "${author.name}"`)
+ Logger.debug(`[ApiRouter] Created new author "${author.name}"`)
newAuthors.push(author)
}
@@ -525,7 +494,7 @@ class ApiController {
if (!seriesItem) {
seriesItem = new Series()
seriesItem.setData(mediaMetadata.series[i])
- Logger.debug(`[ApiController] Created new series "${seriesItem.name}"`)
+ Logger.debug(`[ApiRouter] Created new series "${seriesItem.name}"`)
newSeries.push(seriesItem)
}
@@ -563,4 +532,4 @@ class ApiController {
})
}
}
-module.exports = ApiController
\ No newline at end of file
+module.exports = ApiRouter
\ No newline at end of file
diff --git a/server/HlsController.js b/server/routers/HlsRouter.js
similarity index 67%
rename from server/HlsController.js
rename to server/routers/HlsRouter.js
index eb18963d..9cf9b281 100644
--- a/server/HlsController.js
+++ b/server/routers/HlsRouter.js
@@ -1,13 +1,13 @@
const express = require('express')
const Path = require('path')
const fs = require('fs-extra')
-const Logger = require('./Logger')
+const Logger = require('../Logger')
-class HlsController {
+class HlsRouter {
constructor(db, auth, playbackSessionManager, emitter) {
this.db = db
this.auth = auth
- this.streamManager = playbackSessionManager
+ this.playbackSessionManager = playbackSessionManager
this.emitter = emitter
this.router = express()
@@ -26,13 +26,7 @@ class HlsController {
async streamFileRequest(req, res) {
var streamId = req.params.stream
- var fullFilePath = Path.join(this.streamManager.StreamsPath, streamId, req.params.file)
-
- // development test stream - ignore
- if (streamId === 'test') {
- Logger.debug('Test Stream Request', streamId, req.headers, fullFilePath)
- return res.sendFile(fullFilePath)
- }
+ var fullFilePath = Path.join(this.playbackSessionManager.StreamsPath, streamId, req.params.file)
var exists = await fs.pathExists(fullFilePath)
if (!exists) {
@@ -41,20 +35,20 @@ class HlsController {
var fileExt = Path.extname(req.params.file)
if (fileExt === '.ts' || fileExt === '.m4s') {
var segNum = this.parseSegmentFilename(req.params.file)
- var stream = this.streamManager.getStream(streamId)
+ var stream = this.playbackSessionManager.getStream(streamId)
if (!stream) {
- Logger.error(`[HLS-CONTROLLER] Stream ${streamId} does not exist`)
+ Logger.error(`[HlsRouter] Stream ${streamId} does not exist`)
return res.sendStatus(500)
}
if (stream.isResetting) {
- Logger.info(`[HLS-CONTROLLER] Stream ${streamId} is currently resetting`)
+ Logger.info(`[HlsRouter] Stream ${streamId} is currently resetting`)
return res.sendStatus(404)
} else {
var startTimeForReset = await stream.checkSegmentNumberRequest(segNum)
if (startTimeForReset) {
// HLS.js will restart the stream at the new time
- Logger.info(`[HLS-CONTROLLER] Resetting Stream - notify client @${startTimeForReset}s`)
+ Logger.info(`[HlsRouter] Resetting Stream - notify client @${startTimeForReset}s`)
this.emitter('stream_reset', {
startTime: startTimeForReset,
streamId: stream.id
@@ -69,4 +63,4 @@ class HlsController {
res.sendFile(fullFilePath)
}
}
-module.exports = HlsController
\ No newline at end of file
+module.exports = HlsRouter
\ No newline at end of file
diff --git a/server/routers/StaticRouter.js b/server/routers/StaticRouter.js
new file mode 100644
index 00000000..b571869f
--- /dev/null
+++ b/server/routers/StaticRouter.js
@@ -0,0 +1,25 @@
+const express = require('express')
+const Path = require('path')
+const Logger = require('../Logger')
+
+class StaticRouter {
+ constructor(db) {
+ this.db = db
+
+ this.router = express()
+ this.init()
+ }
+
+ init() {
+ // Library Item static file routes
+ this.router.get('/item/:id/*', (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)
+ })
+ }
+}
+module.exports = StaticRouter
\ No newline at end of file
diff --git a/server/scanner/Scanner.js b/server/scanner/Scanner.js
index 37e431f1..1e5221cb 100644
--- a/server/scanner/Scanner.js
+++ b/server/scanner/Scanner.js
@@ -33,20 +33,6 @@ class Scanner {
this.bookFinder = new BookFinder()
}
- getCoverDirectory(audiobook) {
- if (this.db.serverSettings.storeCoverWithBook) {
- return {
- fullPath: audiobook.fullPath,
- relPath: '/s/book/' + audiobook.id
- }
- } else {
- return {
- fullPath: Path.posix.join(this.BookMetadataPath, audiobook.id),
- relPath: Path.posix.join('/metadata', 'books', audiobook.id)
- }
- }
- }
-
isLibraryScanning(libraryId) {
return this.librariesScanning.find(ls => ls.id === libraryId)
}
diff --git a/server/utils/dbMigration.js b/server/utils/dbMigration.js
index 965487db..549bb3b4 100644
--- a/server/utils/dbMigration.js
+++ b/server/utils/dbMigration.js
@@ -3,6 +3,7 @@ const fs = require('fs-extra')
const njodb = require("njodb")
const { SupportedEbookTypes } = require('./globals')
+const { PlayMethod } = require('./constants')
const { getId } = require('./index')
const Logger = require('../Logger')
@@ -335,6 +336,8 @@ function cleanSessionObj(db, userListeningSession) {
newPlaybackSession.mediaType = 'book'
newPlaybackSession.updatedAt = userListeningSession.lastUpdate
newPlaybackSession.libraryItemId = userListeningSession.audiobookId
+ newPlaybackSession.mediaEntityId = userListeningSession.audiobookId
+ newPlaybackSession.playMethod = PlayMethod.TRANSCODE
// We only have title to transfer over nicely
var bookMetadata = new BookMetadata()
diff --git a/server/utils/fileUtils.js b/server/utils/fileUtils.js
index b9c08bd9..8789875b 100644
--- a/server/utils/fileUtils.js
+++ b/server/utils/fileUtils.js
@@ -132,9 +132,6 @@ async function recurseFiles(path, relPathToReplace = null) {
// Sort from least deep to most
list.sort((a, b) => a.deep - b.deep)
- // list.forEach((l) => {
- // console.log(`${l.deep}: ${l.path}`)
- // })
return list
}
module.exports.recurseFiles = recurseFiles
diff --git a/server/utils/libraryHelpers.js b/server/utils/libraryHelpers.js
index 4bbc2843..211b8600 100644
--- a/server/utils/libraryHelpers.js
+++ b/server/utils/libraryHelpers.js
@@ -28,11 +28,10 @@ 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 userAudiobook = user.getLibraryItemProgress(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
+ var itemProgress = user.getLibraryItemProgress(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
return false
})
} else if (group === 'languages') {
@@ -49,43 +48,6 @@ module.exports = {
return filtered
},
- getFiltered(audiobooks, filterBy, user) {
- var filtered = audiobooks
-
- 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(ab => ab.book && ab.book.genres.includes(filter))
- else if (group === 'tags') filtered = filtered.filter(ab => ab.tags.includes(filter))
- else if (group === 'series') {
- if (filter === 'No Series') filtered = filtered.filter(ab => ab.book && !ab.book.series)
- else filtered = filtered.filter(ab => ab.book && ab.book.series === filter)
- }
- else if (group === 'authors') filtered = filtered.filter(ab => ab.book && ab.book.authorFL && ab.book.authorFL.split(', ').includes(filter))
- else if (group === 'narrators') filtered = filtered.filter(ab => ab.book && ab.book.narratorFL && ab.book.narratorFL.split(', ').includes(filter))
- else if (group === 'progress') {
- filtered = filtered.filter(ab => {
- var userAudiobook = user.getLibraryItemProgress(ab.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(ab => ab.book && ab.book.language === filter)
- }
- } else if (filterBy === 'issues') {
- filtered = filtered.filter(ab => {
- return ab.numMissingParts || ab.numInvalidParts || ab.isMissing || ab.isInvalid
- })
- }
-
- return filtered
- },
-
getDistinctFilterDataNew(libraryItems) {
var data = {
authors: [],
@@ -160,26 +122,27 @@ module.exports = {
},
getSeriesWithProgressFromBooks(user, books) {
- var _series = {}
- books.forEach((audiobook) => {
- if (audiobook.book.series) {
- var bookWithUserAb = { userAudiobook: user.getLibraryItemProgress(audiobook.id), book: audiobook }
- if (!_series[audiobook.book.series]) {
- _series[audiobook.book.series] = {
- id: audiobook.book.series,
- name: audiobook.book.series,
- type: 'series',
- books: [bookWithUserAb]
- }
- } else {
- _series[audiobook.book.series].books.push(bookWithUserAb)
- }
- }
- })
- return Object.values(_series).map((series) => {
- series.books = naturalSort(series.books).asc(ab => ab.book.book.volumeNumber)
- return series
- }).filter((series) => series.books.some((book) => book.userAudiobook && book.userAudiobook.isRead))
+ return []
+ // var _series = {}
+ // books.forEach((audiobook) => {
+ // if (audiobook.book.series) {
+ // var bookWithUserAb = { userAudiobook: user.getLibraryItemProgress(audiobook.id), book: audiobook }
+ // if (!_series[audiobook.book.series]) {
+ // _series[audiobook.book.series] = {
+ // id: audiobook.book.series,
+ // name: audiobook.book.series,
+ // type: 'series',
+ // books: [bookWithUserAb]
+ // }
+ // } else {
+ // _series[audiobook.book.series].books.push(bookWithUserAb)
+ // }
+ // }
+ // })
+ // return Object.values(_series).map((series) => {
+ // series.books = naturalSort(series.books).asc(ab => ab.book.book.volumeNumber)
+ // return series
+ // }).filter((series) => series.books.some((book) => book.userAudiobook && book.userAudiobook.isRead))
},
sortSeriesBooks(books, seriesId, minified = false) {
@@ -196,8 +159,9 @@ module.exports = {
getItemsWithUserProgress(user, libraryItems) {
return libraryItems.map(li => {
+ var itemProgress = user.getLibraryItemProgress(li.id)
return {
- userProgress: user.getLibraryItemProgress(li.id),
+ userProgress: itemProgress ? itemProgress.toJSON() : null,
libraryItem: li
}
}).filter(b => !!b.userProgress)
|