mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-12-16 00:39:40 +00:00
New data model play media entity, PlaybackSessionManager
This commit is contained in:
parent
1cf9e85272
commit
099ae7c776
54 changed files with 841 additions and 902 deletions
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
42
server/objects/files/AudioTrack.js
Normal file
42
server/objects/files/AudioTrack.js
Normal file
|
|
@ -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
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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, '')
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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]) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue