diff --git a/client/components/app/Appbar.vue b/client/components/app/Appbar.vue index 2d557e32..19b8fe3c 100644 --- a/client/components/app/Appbar.vue +++ b/client/components/app/Appbar.vue @@ -264,7 +264,6 @@ export default { libraryItems.forEach((item) => { let subtitle = '' if (item.mediaType === 'book') subtitle = item.media.metadata.authors.map((au) => au.name).join(', ') - else if (item.mediaType === 'music') subtitle = item.media.metadata.artists.join(', ') queueItems.push({ libraryItemId: item.id, libraryId: item.libraryId, diff --git a/client/components/app/BookShelfToolbar.vue b/client/components/app/BookShelfToolbar.vue index ff337428..00b7ee34 100644 --- a/client/components/app/BookShelfToolbar.vue +++ b/client/components/app/BookShelfToolbar.vue @@ -246,9 +246,6 @@ export default { isPodcastLibrary() { return this.currentLibraryMediaType === 'podcast' }, - isMusicLibrary() { - return this.currentLibraryMediaType === 'music' - }, isLibraryPage() { return this.page === '' }, @@ -281,7 +278,6 @@ export default { }, entityName() { if (this.isAlbumsPage) return 'Albums' - if (this.isMusicLibrary) return 'Tracks' if (this.isPodcastLibrary) return this.$strings.LabelPodcasts if (!this.page) return this.$strings.LabelBooks diff --git a/client/components/app/MediaPlayerContainer.vue b/client/components/app/MediaPlayerContainer.vue index 61508d7e..259e0c98 100644 --- a/client/components/app/MediaPlayerContainer.vue +++ b/client/components/app/MediaPlayerContainer.vue @@ -1,10 +1,9 @@ - +

{{ $getString('LabelByAuthor', [podcastAuthor]) }}

+

+ by {{ author.name }} +

+

by Unknown

@@ -109,7 +104,7 @@ - + @@ -220,12 +215,6 @@ export default { isPodcast() { return this.libraryItem.mediaType === 'podcast' }, - isVideo() { - return this.libraryItem.mediaType === 'video' - }, - isMusic() { - return this.libraryItem.mediaType === 'music' - }, isMissing() { return this.libraryItem.isMissing }, @@ -240,8 +229,6 @@ export default { }, showPlayButton() { if (this.isMissing || this.isInvalid) return false - if (this.isMusic) return !!this.audioFile - if (this.isVideo) return !!this.videoFile if (this.isPodcast) return this.podcastEpisodes.length return this.tracks.length }, @@ -292,9 +279,6 @@ export default { authors() { return this.mediaMetadata.authors || [] }, - musicArtists() { - return this.mediaMetadata.artists || [] - }, series() { return this.mediaMetadata.series || [] }, @@ -309,7 +293,7 @@ export default { }) }, duration() { - if (!this.tracks.length && !this.audioFile) return 0 + if (!this.tracks.length) return 0 return this.media.duration }, libraryFiles() { @@ -321,18 +305,10 @@ export default { ebookFile() { return this.media.ebookFile }, - videoFile() { - return this.media.videoFile - }, - audioFile() { - // Music track - return this.media.audioFile - }, description() { return this.mediaMetadata.description || '' }, userMediaProgress() { - if (this.isMusic) return null return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId) }, userIsFinished() { diff --git a/client/players/LocalVideoPlayer.js b/client/players/LocalVideoPlayer.js deleted file mode 100644 index 128f070b..00000000 --- a/client/players/LocalVideoPlayer.js +++ /dev/null @@ -1,260 +0,0 @@ -import Hls from 'hls.js' -import EventEmitter from 'events' - -export default class LocalVideoPlayer extends EventEmitter { - constructor(ctx) { - super() - - this.ctx = ctx - this.player = null - - this.libraryItem = null - this.videoTrack = null - this.isHlsTranscode = null - this.hlsInstance = null - this.usingNativeplayer = false - this.startTime = 0 - this.playWhenReady = false - this.defaultPlaybackRate = 1 - - this.playableMimeTypes = [] - - this.initialize() - } - - initialize() { - if (document.getElementById('video-player')) { - document.getElementById('video-player').remove() - } - var videoEl = document.createElement('video') - videoEl.id = 'video-player' - // videoEl.style.display = 'none' - videoEl.className = 'absolute bg-black z-50' - videoEl.style.height = '216px' - videoEl.style.width = '384px' - videoEl.style.bottom = '80px' - videoEl.style.left = '16px' - document.body.appendChild(videoEl) - this.player = videoEl - - this.player.addEventListener('play', this.evtPlay.bind(this)) - this.player.addEventListener('pause', this.evtPause.bind(this)) - this.player.addEventListener('progress', this.evtProgress.bind(this)) - this.player.addEventListener('ended', this.evtEnded.bind(this)) - this.player.addEventListener('error', this.evtError.bind(this)) - this.player.addEventListener('loadedmetadata', this.evtLoadedMetadata.bind(this)) - this.player.addEventListener('timeupdate', this.evtTimeupdate.bind(this)) - - var mimeTypes = ['video/mp4'] - var mimeTypeCanPlayMap = {} - mimeTypes.forEach((mt) => { - var canPlay = this.player.canPlayType(mt) - mimeTypeCanPlayMap[mt] = canPlay - if (canPlay) this.playableMimeTypes.push(mt) - }) - console.log(`[LocalVideoPlayer] Supported mime types`, mimeTypeCanPlayMap, this.playableMimeTypes) - } - - evtPlay() { - this.emit('stateChange', 'PLAYING') - } - evtPause() { - this.emit('stateChange', 'PAUSED') - } - evtProgress() { - var lastBufferTime = this.getLastBufferedTime() - this.emit('buffertimeUpdate', lastBufferTime) - } - evtEnded() { - console.log(`[LocalVideoPlayer] Ended`) - this.emit('finished') - } - evtError(error) { - console.error('Player error', error) - this.emit('error', error) - } - evtLoadedMetadata(data) { - if (!this.isHlsTranscode) { - this.player.currentTime = this.startTime - } - - this.emit('stateChange', 'LOADED') - if (this.playWhenReady) { - this.playWhenReady = false - this.play() - } - } - evtTimeupdate() { - if (this.player.paused) { - this.emit('timeupdate', this.getCurrentTime()) - } - } - - destroy() { - this.destroyHlsInstance() - if (this.player) { - this.player.remove() - } - } - - set(libraryItem, videoTrack, isHlsTranscode, startTime, playWhenReady = false) { - this.libraryItem = libraryItem - this.videoTrack = videoTrack - this.isHlsTranscode = isHlsTranscode - this.playWhenReady = playWhenReady - this.startTime = startTime - - if (this.hlsInstance) { - this.destroyHlsInstance() - } - - if (this.isHlsTranscode) { - this.setHlsStream() - } else { - this.setDirectPlay() - } - } - - setHlsStream() { - // iOS does not support Media Elements but allows for HLS in the native video player - if (!Hls.isSupported()) { - console.warn('HLS is not supported - fallback to using video element') - this.usingNativeplayer = true - this.player.src = this.videoTrack.relativeContentUrl - this.player.currentTime = this.startTime - return - } - - var hlsOptions = { - startPosition: this.startTime || -1 - // No longer needed because token is put in a query string - // xhrSetup: (xhr) => { - // xhr.setRequestHeader('Authorization', `Bearer ${this.token}`) - // } - } - this.hlsInstance = new Hls(hlsOptions) - - this.hlsInstance.attachMedia(this.player) - this.hlsInstance.on(Hls.Events.MEDIA_ATTACHED, () => { - this.hlsInstance.loadSource(this.videoTrack.relativeContentUrl) - - this.hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => { - console.log('[HLS] Manifest Parsed') - }) - - this.hlsInstance.on(Hls.Events.ERROR, (e, data) => { - console.error('[HLS] Error', data.type, data.details, data) - if (data.details === Hls.ErrorDetails.BUFFER_STALLED_ERROR) { - console.error('[HLS] BUFFER STALLED ERROR') - } - }) - this.hlsInstance.on(Hls.Events.DESTROYING, () => { - console.log('[HLS] Destroying HLS Instance') - }) - }) - } - - setDirectPlay() { - this.player.src = this.videoTrack.relativeContentUrl - console.log(`[LocalVideoPlayer] Loading track src ${this.videoTrack.relativeContentUrl}`) - this.player.load() - } - - destroyHlsInstance() { - if (!this.hlsInstance) return - if (this.hlsInstance.destroy) { - var temp = this.hlsInstance - temp.destroy() - } - this.hlsInstance = null - } - - async resetStream(startTime) { - this.destroyHlsInstance() - await new Promise((resolve) => setTimeout(resolve, 1000)) - this.set(this.libraryItem, this.videoTrack, this.isHlsTranscode, startTime, true) - } - - playPause() { - if (!this.player) return - if (this.player.paused) this.play() - else this.pause() - } - - play() { - if (this.player) this.player.play() - } - - pause() { - if (this.player) this.player.pause() - } - - getCurrentTime() { - return this.player ? this.player.currentTime : 0 - } - - getDuration() { - return this.videoTrack.duration - } - - setPlaybackRate(playbackRate) { - if (!this.player) return - this.defaultPlaybackRate = playbackRate - this.player.playbackRate = playbackRate - } - - seek(time) { - if (!this.player) return - this.player.currentTime = Math.max(0, time) - } - - setVolume(volume) { - if (!this.player) return - this.player.volume = volume - } - - // Utils - isValidDuration(duration) { - if (duration && !isNaN(duration) && duration !== Number.POSITIVE_INFINITY && duration !== Number.NEGATIVE_INFINITY) { - return true - } - return false - } - - getBufferedRanges() { - if (!this.player) return [] - const ranges = [] - const seekable = this.player.buffered || [] - - let offset = 0 - - for (let i = 0, length = seekable.length; i < length; i++) { - let start = seekable.start(i) - let end = seekable.end(i) - if (!this.isValidDuration(start)) { - start = 0 - } - if (!this.isValidDuration(end)) { - end = 0 - continue - } - - ranges.push({ - start: start + offset, - end: end + offset - }) - } - return ranges - } - - getLastBufferedTime() { - var bufferedRanges = this.getBufferedRanges() - if (!bufferedRanges.length) return 0 - - var buff = bufferedRanges.find((buff) => buff.start < this.player.currentTime && buff.end > this.player.currentTime) - if (buff) return buff.end - - var last = bufferedRanges[bufferedRanges.length - 1] - return last.end - } -} \ No newline at end of file diff --git a/client/players/PlayerHandler.js b/client/players/PlayerHandler.js index 42d76bd0..a327f831 100644 --- a/client/players/PlayerHandler.js +++ b/client/players/PlayerHandler.js @@ -1,8 +1,6 @@ import LocalAudioPlayer from './LocalAudioPlayer' -import LocalVideoPlayer from './LocalVideoPlayer' import CastPlayer from './CastPlayer' import AudioTrack from './AudioTrack' -import VideoTrack from './VideoTrack' export default class PlayerHandler { constructor(ctx) { @@ -16,8 +14,6 @@ export default class PlayerHandler { this.player = null this.playerState = 'IDLE' this.isHlsTranscode = false - this.isVideo = false - this.isMusic = false this.currentSessionId = null this.startTimeOverride = undefined // Used for starting playback at a specific time (i.e. clicking bookmark from library item page) this.startTime = 0 @@ -65,12 +61,10 @@ export default class PlayerHandler { load(libraryItem, episodeId, playWhenReady, playbackRate, startTimeOverride = undefined) { this.libraryItem = libraryItem - this.isVideo = libraryItem.mediaType === 'video' - this.isMusic = libraryItem.mediaType === 'music' this.episodeId = episodeId this.playWhenReady = playWhenReady - this.initialPlaybackRate = this.isMusic ? 1 : playbackRate + this.initialPlaybackRate = playbackRate this.startTimeOverride = startTimeOverride == null || isNaN(startTimeOverride) ? undefined : Number(startTimeOverride) @@ -97,7 +91,7 @@ export default class PlayerHandler { this.playWhenReady = playWhenReady this.prepare() } - } else if (!this.isCasting && !(this.player instanceof LocalAudioPlayer) && !(this.player instanceof LocalVideoPlayer)) { + } else if (!this.isCasting && !(this.player instanceof LocalAudioPlayer)) { console.log('[PlayerHandler] Switching to local player') this.stopPlayInterval() @@ -107,11 +101,7 @@ export default class PlayerHandler { this.player.destroy() } - if (this.isVideo) { - this.player = new LocalVideoPlayer(this.ctx) - } else { - this.player = new LocalAudioPlayer(this.ctx) - } + this.player = new LocalAudioPlayer(this.ctx) this.setPlayerListeners() @@ -203,7 +193,7 @@ export default class PlayerHandler { supportedMimeTypes: this.player.playableMimeTypes, mediaPlayer: this.isCasting ? 'chromecast' : 'html5', forceTranscode, - forceDirectPlay: this.isCasting || this.isVideo // TODO: add transcode support for chromecast + forceDirectPlay: this.isCasting // TODO: add transcode support for chromecast } const path = this.episodeId ? `/api/items/${this.libraryItem.id}/play/${this.episodeId}` : `/api/items/${this.libraryItem.id}/play` @@ -218,7 +208,6 @@ export default class PlayerHandler { if (!this.player) this.switchPlayer() // Must set player first for open sessions this.libraryItem = session.libraryItem - this.isVideo = session.libraryItem.mediaType === 'video' this.playWhenReady = false this.initialPlaybackRate = playbackRate this.startTimeOverride = undefined @@ -237,28 +226,16 @@ export default class PlayerHandler { console.log('[PlayerHandler] Preparing Session', session) - if (session.videoTrack) { - var videoTrack = new VideoTrack(session.videoTrack, this.userToken) + 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 - } - - this.player.set(this.libraryItem, videoTrack, this.isHlsTranscode, this.startTime, this.playWhenReady) - } else { - 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 - } - - this.player.set(this.libraryItem, audioTracks, this.isHlsTranscode, this.startTime, this.playWhenReady) + this.ctx.playerLoading = true + this.isHlsTranscode = true + if (session.playMethod === this.ctx.$constants.PlayMethod.DIRECTPLAY) { + this.isHlsTranscode = false } + this.player.set(this.libraryItem, audioTracks, this.isHlsTranscode, this.startTime, this.playWhenReady) + // browser media session api this.ctx.setMediaSession() } @@ -333,8 +310,6 @@ export default class PlayerHandler { } sendProgressSync(currentTime) { - if (this.isMusic) return - const diffSinceLastSync = Math.abs(this.lastSyncTime - currentTime) if (diffSinceLastSync < 1) return diff --git a/client/players/VideoTrack.js b/client/players/VideoTrack.js deleted file mode 100644 index 92bec5eb..00000000 --- a/client/players/VideoTrack.js +++ /dev/null @@ -1,32 +0,0 @@ -export default class VideoTrack { - 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.title || '' - this.contentUrl = track.contentUrl || null - this.mimeType = track.mimeType - this.metadata = track.metadata || {} - - 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}?token=${this.userToken}` - } - 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}?token=${this.userToken}` - } - - return this.contentUrl + `?token=${this.userToken}` - } -} \ No newline at end of file diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index 472f5678..c77e1d3a 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -384,7 +384,7 @@ class LibraryItemController { * @param {Response} res */ startPlaybackSession(req, res) { - if (!req.libraryItem.media.numTracks && req.libraryItem.mediaType !== 'video') { + if (!req.libraryItem.media.numTracks) { Logger.error(`[LibraryItemController] startPlaybackSession cannot playback ${req.libraryItem.id}`) return res.sendStatus(404) } diff --git a/server/controllers/SearchController.js b/server/controllers/SearchController.js index cfe4e6d3..a19ff876 100644 --- a/server/controllers/SearchController.js +++ b/server/controllers/SearchController.js @@ -3,7 +3,6 @@ const Logger = require('../Logger') const BookFinder = require('../finders/BookFinder') const PodcastFinder = require('../finders/PodcastFinder') const AuthorFinder = require('../finders/AuthorFinder') -const MusicFinder = require('../finders/MusicFinder') const Database = require('../Database') const { isValidASIN } = require('../utils') diff --git a/server/finders/MusicFinder.js b/server/finders/MusicFinder.js deleted file mode 100644 index 3569576f..00000000 --- a/server/finders/MusicFinder.js +++ /dev/null @@ -1,12 +0,0 @@ -const MusicBrainz = require('../providers/MusicBrainz') - -class MusicFinder { - constructor() { - this.musicBrainz = new MusicBrainz() - } - - searchTrack(options) { - return this.musicBrainz.searchTrack(options) - } -} -module.exports = new MusicFinder() \ No newline at end of file diff --git a/server/managers/PlaybackSessionManager.js b/server/managers/PlaybackSessionManager.js index cafd6ff4..ccdf6c7a 100644 --- a/server/managers/PlaybackSessionManager.js +++ b/server/managers/PlaybackSessionManager.js @@ -293,37 +293,27 @@ class PlaybackSessionManager { const newPlaybackSession = new PlaybackSession() newPlaybackSession.setData(libraryItem, user.id, mediaPlayer, deviceInfo, userStartTime, episodeId) - if (libraryItem.mediaType === 'video') { - if (shouldDirectPlay) { - Logger.debug(`[PlaybackSessionManager] "${user.username}" starting direct play session for item "${libraryItem.id}" with id ${newPlaybackSession.id}`) - newPlaybackSession.videoTrack = libraryItem.media.getVideoTrack() - newPlaybackSession.playMethod = PlayMethod.DIRECTPLAY - } else { - // HLS not supported for video yet - } + let audioTracks = [] + if (shouldDirectPlay) { + Logger.debug(`[PlaybackSessionManager] "${user.username}" starting direct play session for item "${libraryItem.id}" with id ${newPlaybackSession.id} (Device: ${newPlaybackSession.deviceDescription})`) + audioTracks = libraryItem.getDirectPlayTracklist(episodeId) + newPlaybackSession.playMethod = PlayMethod.DIRECTPLAY } else { - let audioTracks = [] - if (shouldDirectPlay) { - Logger.debug(`[PlaybackSessionManager] "${user.username}" starting direct play session for item "${libraryItem.id}" with id ${newPlaybackSession.id} (Device: ${newPlaybackSession.deviceDescription})`) - audioTracks = libraryItem.getDirectPlayTracklist(episodeId) - newPlaybackSession.playMethod = PlayMethod.DIRECTPLAY - } else { - Logger.debug(`[PlaybackSessionManager] "${user.username}" starting stream session for item "${libraryItem.id}" (Device: ${newPlaybackSession.deviceDescription})`) - const stream = new Stream(newPlaybackSession.id, this.StreamsPath, user, libraryItem, episodeId, userStartTime) - await stream.generatePlaylist() - stream.start() // Start transcode + Logger.debug(`[PlaybackSessionManager] "${user.username}" starting stream session for item "${libraryItem.id}" (Device: ${newPlaybackSession.deviceDescription})`) + const stream = new Stream(newPlaybackSession.id, this.StreamsPath, user, libraryItem, episodeId, userStartTime) + await stream.generatePlaylist() + stream.start() // Start transcode - audioTracks = [stream.getAudioTrack()] - newPlaybackSession.stream = stream - newPlaybackSession.playMethod = PlayMethod.TRANSCODE + audioTracks = [stream.getAudioTrack()] + newPlaybackSession.stream = stream + newPlaybackSession.playMethod = PlayMethod.TRANSCODE - stream.on('closed', () => { - Logger.debug(`[PlaybackSessionManager] Stream closed for session "${newPlaybackSession.id}" (Device: ${newPlaybackSession.deviceDescription})`) - newPlaybackSession.stream = null - }) - } - newPlaybackSession.audioTracks = audioTracks + stream.on('closed', () => { + Logger.debug(`[PlaybackSessionManager] Stream closed for session "${newPlaybackSession.id}" (Device: ${newPlaybackSession.deviceDescription})`) + newPlaybackSession.stream = null + }) } + newPlaybackSession.audioTracks = audioTracks this.sessions.push(newPlaybackSession) SocketAuthority.adminEmitter('user_stream_update', user.toJSONForPublic(this.sessions)) diff --git a/server/objects/LibraryItem.js b/server/objects/LibraryItem.js index 3b92bdcc..0259ee4c 100644 --- a/server/objects/LibraryItem.js +++ b/server/objects/LibraryItem.js @@ -1,12 +1,10 @@ -const uuidv4 = require("uuid").v4 +const uuidv4 = require('uuid').v4 const fs = require('../libs/fsExtra') const Path = require('path') const Logger = require('../Logger') const LibraryFile = require('./files/LibraryFile') const Book = require('./mediaTypes/Book') const Podcast = require('./mediaTypes/Podcast') -const Video = require('./mediaTypes/Video') -const Music = require('./mediaTypes/Music') const { areEquivalent, copyValue } = require('../utils/index') const { filePathToPOSIX, getFileTimestampsWithIno } = require('../utils/fileUtils') @@ -74,14 +72,10 @@ class LibraryItem { this.media = new Book(libraryItem.media) } else if (this.mediaType === 'podcast') { this.media = new Podcast(libraryItem.media) - } else if (this.mediaType === 'video') { - this.media = new Video(libraryItem.media) - } else if (this.mediaType === 'music') { - this.media = new Music(libraryItem.media) } this.media.libraryItemId = this.id - this.libraryFiles = libraryItem.libraryFiles.map(f => new LibraryFile(f)) + this.libraryFiles = libraryItem.libraryFiles.map((f) => new LibraryFile(f)) // Migration for v2.2.23 to set ebook library files as supplementary if (this.isBook && this.media.ebookFile) { @@ -91,7 +85,6 @@ class LibraryItem { } } } - } toJSON() { @@ -115,7 +108,7 @@ class LibraryItem { isInvalid: !!this.isInvalid, mediaType: this.mediaType, media: this.media.toJSON(), - libraryFiles: this.libraryFiles.map(f => f.toJSON()) + libraryFiles: this.libraryFiles.map((f) => f.toJSON()) } } @@ -165,21 +158,24 @@ class LibraryItem { isInvalid: !!this.isInvalid, mediaType: this.mediaType, media: this.media.toJSONExpanded(), - libraryFiles: this.libraryFiles.map(f => f.toJSON()), + libraryFiles: this.libraryFiles.map((f) => f.toJSON()), size: this.size } } - get isPodcast() { return this.mediaType === 'podcast' } - get isBook() { return this.mediaType === 'book' } - get isMusic() { return this.mediaType === 'music' } + get isPodcast() { + return this.mediaType === 'podcast' + } + get isBook() { + return this.mediaType === 'book' + } get size() { let total = 0 - this.libraryFiles.forEach((lf) => total += lf.metadata.size) + this.libraryFiles.forEach((lf) => (total += lf.metadata.size)) return total } get hasAudioFiles() { - return this.libraryFiles.some(lf => lf.fileType === 'audio') + return this.libraryFiles.some((lf) => lf.fileType === 'audio') } get hasMediaEntities() { return this.media.hasMediaEntities @@ -201,17 +197,16 @@ class LibraryItem { for (const key in payload) { if (key === 'libraryFiles') { - this.libraryFiles = payload.libraryFiles.map(lf => lf.clone()) + this.libraryFiles = payload.libraryFiles.map((lf) => lf.clone()) // Set cover image - const imageFiles = this.libraryFiles.filter(lf => lf.fileType === 'image') - const coverMatch = imageFiles.find(iFile => /\/cover\.[^.\/]*$/.test(iFile.metadata.path)) + const imageFiles = this.libraryFiles.filter((lf) => lf.fileType === 'image') + const coverMatch = imageFiles.find((iFile) => /\/cover\.[^.\/]*$/.test(iFile.metadata.path)) if (coverMatch) { this.media.coverPath = coverMatch.metadata.path } else if (imageFiles.length) { this.media.coverPath = imageFiles[0].metadata.path } - } else if (this[key] !== undefined && key !== 'media') { this[key] = payload[key] } @@ -283,46 +278,50 @@ class LibraryItem { const metadataFilePath = Path.join(metadataPath, `metadata.${global.ServerSettings.metadataFileFormat}`) - return fs.writeFile(metadataFilePath, JSON.stringify(this.media.toJSONForMetadataFile(), null, 2)).then(async () => { - // Add metadata.json to libraryFiles array if it is new - let metadataLibraryFile = this.libraryFiles.find(lf => lf.metadata.path === filePathToPOSIX(metadataFilePath)) - if (storeMetadataWithItem) { - if (!metadataLibraryFile) { - metadataLibraryFile = new LibraryFile() - await metadataLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`) - this.libraryFiles.push(metadataLibraryFile) - } else { - const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath) - if (fileTimestamps) { - metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs - metadataLibraryFile.metadata.ctimeMs = fileTimestamps.ctimeMs - metadataLibraryFile.metadata.size = fileTimestamps.size - metadataLibraryFile.ino = fileTimestamps.ino + return fs + .writeFile(metadataFilePath, JSON.stringify(this.media.toJSONForMetadataFile(), null, 2)) + .then(async () => { + // Add metadata.json to libraryFiles array if it is new + let metadataLibraryFile = this.libraryFiles.find((lf) => lf.metadata.path === filePathToPOSIX(metadataFilePath)) + if (storeMetadataWithItem) { + if (!metadataLibraryFile) { + metadataLibraryFile = new LibraryFile() + await metadataLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`) + this.libraryFiles.push(metadataLibraryFile) + } else { + const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath) + if (fileTimestamps) { + metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs + metadataLibraryFile.metadata.ctimeMs = fileTimestamps.ctimeMs + metadataLibraryFile.metadata.size = fileTimestamps.size + metadataLibraryFile.ino = fileTimestamps.ino + } + } + const libraryItemDirTimestamps = await getFileTimestampsWithIno(this.path) + if (libraryItemDirTimestamps) { + this.mtimeMs = libraryItemDirTimestamps.mtimeMs + this.ctimeMs = libraryItemDirTimestamps.ctimeMs } } - const libraryItemDirTimestamps = await getFileTimestampsWithIno(this.path) - if (libraryItemDirTimestamps) { - this.mtimeMs = libraryItemDirTimestamps.mtimeMs - this.ctimeMs = libraryItemDirTimestamps.ctimeMs - } - } - Logger.debug(`[LibraryItem] Success saving abmetadata to "${metadataFilePath}"`) + Logger.debug(`[LibraryItem] Success saving abmetadata to "${metadataFilePath}"`) - return metadataLibraryFile - }).catch((error) => { - Logger.error(`[LibraryItem] Failed to save json file at "${metadataFilePath}"`, error) - return null - }).finally(() => { - this.isSavingMetadata = false - }) + return metadataLibraryFile + }) + .catch((error) => { + Logger.error(`[LibraryItem] Failed to save json file at "${metadataFilePath}"`, error) + return null + }) + .finally(() => { + this.isSavingMetadata = false + }) } removeLibraryFile(ino) { if (!ino) return false - const libraryFile = this.libraryFiles.find(lf => lf.ino === ino) + const libraryFile = this.libraryFiles.find((lf) => lf.ino === ino) if (libraryFile) { - this.libraryFiles = this.libraryFiles.filter(lf => lf.ino !== ino) + this.libraryFiles = this.libraryFiles.filter((lf) => lf.ino !== ino) this.updatedAt = Date.now() return true } @@ -333,15 +332,15 @@ class LibraryItem { * Set the EBookFile from a LibraryFile * If null then ebookFile will be removed from the book * all ebook library files that are not primary are marked as supplementary - * - * @param {LibraryFile} [libraryFile] + * + * @param {LibraryFile} [libraryFile] */ setPrimaryEbook(ebookLibraryFile = null) { - const ebookLibraryFiles = this.libraryFiles.filter(lf => lf.isEBookFile) + const ebookLibraryFiles = this.libraryFiles.filter((lf) => lf.isEBookFile) for (const libraryFile of ebookLibraryFiles) { libraryFile.isSupplementary = ebookLibraryFile?.ino !== libraryFile.ino } this.media.setEbookFile(ebookLibraryFile) } } -module.exports = LibraryItem \ No newline at end of file +module.exports = LibraryItem diff --git a/server/objects/PlaybackSession.js b/server/objects/PlaybackSession.js index 4a5b7d1e..cd74089a 100644 --- a/server/objects/PlaybackSession.js +++ b/server/objects/PlaybackSession.js @@ -4,7 +4,6 @@ const serverVersion = require('../../package.json').version const BookMetadata = require('./metadata/BookMetadata') const PodcastMetadata = require('./metadata/PodcastMetadata') const DeviceInfo = require('./DeviceInfo') -const VideoMetadata = require('./metadata/VideoMetadata') class PlaybackSession { constructor(session) { @@ -41,7 +40,6 @@ class PlaybackSession { // Not saved in DB this.lastSave = 0 this.audioTracks = [] - this.videoTrack = null this.stream = null // Used for share sessions this.shareSessionId = null @@ -114,7 +112,6 @@ class PlaybackSession { startedAt: this.startedAt, updatedAt: this.updatedAt, audioTracks: this.audioTracks.map((at) => at.toJSON?.() || { ...at }), - videoTrack: this.videoTrack?.toJSON() || null, libraryItem: libraryItem?.toJSONExpanded() || null } } @@ -157,8 +154,6 @@ class PlaybackSession { this.mediaMetadata = new BookMetadata(session.mediaMetadata) } else if (this.mediaType === 'podcast') { this.mediaMetadata = new PodcastMetadata(session.mediaMetadata) - } else if (this.mediaType === 'video') { - this.mediaMetadata = new VideoMetadata(session.mediaMetadata) } } this.displayTitle = session.displayTitle || '' diff --git a/server/objects/files/LibraryFile.js b/server/objects/files/LibraryFile.js index 395e11cc..8669e387 100644 --- a/server/objects/files/LibraryFile.js +++ b/server/objects/files/LibraryFile.js @@ -43,14 +43,13 @@ class LibraryFile { if (globals.SupportedImageTypes.includes(this.metadata.format)) return 'image' if (globals.SupportedAudioTypes.includes(this.metadata.format)) return 'audio' if (globals.SupportedEbookTypes.includes(this.metadata.format)) return 'ebook' - if (globals.SupportedVideoTypes.includes(this.metadata.format)) return 'video' if (globals.TextFileTypes.includes(this.metadata.format)) return 'text' if (globals.MetadataFileTypes.includes(this.metadata.format)) return 'metadata' return 'unknown' } get isMediaFile() { - return this.fileType === 'audio' || this.fileType === 'ebook' || this.fileType === 'video' + return this.fileType === 'audio' || this.fileType === 'ebook' } get isEBookFile() { @@ -75,4 +74,4 @@ class LibraryFile { this.updatedAt = Date.now() } } -module.exports = LibraryFile \ No newline at end of file +module.exports = LibraryFile diff --git a/server/objects/files/VideoFile.js b/server/objects/files/VideoFile.js deleted file mode 100644 index 51fc382b..00000000 --- a/server/objects/files/VideoFile.js +++ /dev/null @@ -1,109 +0,0 @@ -const { VideoMimeType } = require('../../utils/constants') -const FileMetadata = require('../metadata/FileMetadata') - -class VideoFile { - constructor(data) { - this.index = null - this.ino = null - this.metadata = null - this.addedAt = null - this.updatedAt = null - - this.format = null - this.duration = null - this.bitRate = null - this.language = null - this.codec = null - this.timeBase = null - this.frameRate = null - this.width = null - this.height = null - this.embeddedCoverArt = null - - this.invalid = false - this.error = null - - if (data) { - this.construct(data) - } - } - - toJSON() { - return { - index: this.index, - ino: this.ino, - metadata: this.metadata.toJSON(), - addedAt: this.addedAt, - updatedAt: this.updatedAt, - invalid: !!this.invalid, - error: this.error || null, - format: this.format, - duration: this.duration, - bitRate: this.bitRate, - language: this.language, - codec: this.codec, - timeBase: this.timeBase, - frameRate: this.frameRate, - width: this.width, - height: this.height, - embeddedCoverArt: this.embeddedCoverArt, - mimeType: this.mimeType - } - } - - construct(data) { - this.index = data.index - this.ino = data.ino - this.metadata = new FileMetadata(data.metadata || {}) - this.addedAt = data.addedAt - this.updatedAt = data.updatedAt - this.invalid = !!data.invalid - this.error = data.error || null - - this.format = data.format - this.duration = data.duration - this.bitRate = data.bitRate - this.language = data.language - this.codec = data.codec || null - this.timeBase = data.timeBase - this.frameRate = data.frameRate - this.width = data.width - this.height = data.height - this.embeddedCoverArt = data.embeddedCoverArt || null - } - - get mimeType() { - var format = this.metadata.format.toUpperCase() - if (VideoMimeType[format]) { - return VideoMimeType[format] - } else { - return VideoMimeType.MP4 - } - } - - clone() { - return new VideoFile(this.toJSON()) - } - - setDataFromProbe(libraryFile, probeData) { - this.ino = libraryFile.ino || null - - this.metadata = libraryFile.metadata.clone() - this.addedAt = Date.now() - this.updatedAt = Date.now() - - const videoStream = probeData.videoStream - - this.format = probeData.format - this.duration = probeData.duration - this.bitRate = videoStream.bit_rate || probeData.bitRate || null - this.language = probeData.language - this.codec = videoStream.codec || null - this.timeBase = videoStream.time_base - this.frameRate = videoStream.frame_rate || null - this.width = videoStream.width || null - this.height = videoStream.height || null - this.embeddedCoverArt = probeData.embeddedCoverArt - } -} -module.exports = VideoFile \ No newline at end of file diff --git a/server/objects/files/VideoTrack.js b/server/objects/files/VideoTrack.js deleted file mode 100644 index b1f1e354..00000000 --- a/server/objects/files/VideoTrack.js +++ /dev/null @@ -1,45 +0,0 @@ -const Path = require('path') -const { encodeUriPath } = require('../../utils/fileUtils') - -class VideoTrack { - constructor() { - this.index = null - this.duration = null - this.title = null - this.contentUrl = null - this.mimeType = null - this.codec = null - this.metadata = null - } - - toJSON() { - return { - index: this.index, - duration: this.duration, - title: this.title, - contentUrl: this.contentUrl, - mimeType: this.mimeType, - codec: this.codec, - metadata: this.metadata ? this.metadata.toJSON() : null - } - } - - setData(itemId, videoFile) { - this.index = videoFile.index - this.duration = videoFile.duration - this.title = videoFile.metadata.filename || '' - this.contentUrl = Path.join(`${global.RouterBasePath}/api/items/${itemId}/file/${videoFile.ino}`, encodeUriPath(videoFile.metadata.relPath)) - this.mimeType = videoFile.mimeType - this.codec = videoFile.codec - this.metadata = videoFile.metadata.clone() - } - - setFromStream(title, duration, contentUrl) { - this.index = 1 - this.duration = duration - this.title = title - this.contentUrl = contentUrl - this.mimeType = 'application/vnd.apple.mpegurl' - } -} -module.exports = VideoTrack \ No newline at end of file diff --git a/server/objects/mediaTypes/Music.js b/server/objects/mediaTypes/Music.js deleted file mode 100644 index d4b8a518..00000000 --- a/server/objects/mediaTypes/Music.js +++ /dev/null @@ -1,145 +0,0 @@ -const Logger = require('../../Logger') -const AudioFile = require('../files/AudioFile') -const AudioTrack = require('../files/AudioTrack') -const MusicMetadata = require('../metadata/MusicMetadata') -const { areEquivalent, copyValue } = require('../../utils/index') -const { filePathToPOSIX } = require('../../utils/fileUtils') - -class Music { - constructor(music) { - this.libraryItemId = null - this.metadata = null - this.coverPath = null - this.tags = [] - this.audioFile = null - - if (music) { - this.construct(music) - } - } - - construct(music) { - this.libraryItemId = music.libraryItemId - this.metadata = new MusicMetadata(music.metadata) - this.coverPath = music.coverPath - this.tags = [...music.tags] - this.audioFile = new AudioFile(music.audioFile) - } - - toJSON() { - return { - libraryItemId: this.libraryItemId, - metadata: this.metadata.toJSON(), - coverPath: this.coverPath, - tags: [...this.tags], - audioFile: this.audioFile.toJSON(), - } - } - - toJSONMinified() { - return { - metadata: this.metadata.toJSONMinified(), - coverPath: this.coverPath, - tags: [...this.tags], - audioFile: this.audioFile.toJSON(), - duration: this.duration, - size: this.size - } - } - - toJSONExpanded() { - return { - libraryItemId: this.libraryItemId, - metadata: this.metadata.toJSONExpanded(), - coverPath: this.coverPath, - tags: [...this.tags], - audioFile: this.audioFile.toJSON(), - duration: this.duration, - size: this.size - } - } - - get size() { - return this.audioFile.metadata.size - } - get hasMediaEntities() { - return !!this.audioFile - } - get duration() { - return this.audioFile.duration || 0 - } - get audioTrack() { - const audioTrack = new AudioTrack() - audioTrack.setData(this.libraryItemId, this.audioFile, 0) - return audioTrack - } - get numTracks() { - return 1 - } - - update(payload) { - const json = this.toJSON() - delete json.episodes // do not update media entities here - let hasUpdates = false - for (const key in json) { - if (payload[key] !== undefined) { - if (key === 'metadata') { - if (this.metadata.update(payload.metadata)) { - hasUpdates = true - } - } else if (!areEquivalent(payload[key], json[key])) { - this[key] = copyValue(payload[key]) - Logger.debug('[Podcast] Key updated', key, this[key]) - hasUpdates = true - } - } - } - return hasUpdates - } - - updateCover(coverPath) { - coverPath = filePathToPOSIX(coverPath) - if (this.coverPath === coverPath) return false - this.coverPath = coverPath - return true - } - - removeFileWithInode(inode) { - return false - } - - findFileWithInode(inode) { - return (this.audioFile && this.audioFile.ino === inode) ? this.audioFile : null - } - - setData(mediaData) { - this.metadata = new MusicMetadata() - if (mediaData.metadata) { - this.metadata.setData(mediaData.metadata) - } - - this.coverPath = mediaData.coverPath || null - } - - setAudioFile(audioFile) { - this.audioFile = audioFile - } - - // Only checks container format - checkCanDirectPlay(payload) { - return true - } - - getDirectPlayTracklist() { - return [this.audioTrack] - } - - getPlaybackTitle() { - return this.metadata.title - } - - getPlaybackAuthor() { - return this.metadata.artist - } -} -module.exports = Music \ No newline at end of file diff --git a/server/objects/mediaTypes/Video.js b/server/objects/mediaTypes/Video.js deleted file mode 100644 index 940eab0b..00000000 --- a/server/objects/mediaTypes/Video.js +++ /dev/null @@ -1,137 +0,0 @@ -const Logger = require('../../Logger') -const VideoFile = require('../files/VideoFile') -const VideoTrack = require('../files/VideoTrack') -const VideoMetadata = require('../metadata/VideoMetadata') -const { areEquivalent, copyValue } = require('../../utils/index') -const { filePathToPOSIX } = require('../../utils/fileUtils') - -class Video { - constructor(video) { - this.libraryItemId = null - this.metadata = null - this.coverPath = null - this.tags = [] - this.episodes = [] - - this.autoDownloadEpisodes = false - this.lastEpisodeCheck = 0 - - this.lastCoverSearch = null - this.lastCoverSearchQuery = null - - if (video) { - this.construct(video) - } - } - - construct(video) { - this.libraryItemId = video.libraryItemId - this.metadata = new VideoMetadata(video.metadata) - this.coverPath = video.coverPath - this.tags = [...video.tags] - this.videoFile = new VideoFile(video.videoFile) - } - - toJSON() { - return { - libraryItemId: this.libraryItemId, - metadata: this.metadata.toJSONExpanded(), - coverPath: this.coverPath, - tags: [...this.tags], - videoFile: this.videoFile.toJSON() - } - } - - toJSONMinified() { - return { - metadata: this.metadata.toJSONMinified(), - coverPath: this.coverPath, - tags: [...this.tags], - videoFile: this.videoFile.toJSON(), - size: this.size - } - } - - toJSONExpanded() { - return { - libraryItemId: this.libraryItemId, - metadata: this.metadata.toJSONExpanded(), - coverPath: this.coverPath, - tags: [...this.tags], - videoFile: this.videoFile.toJSON(), - size: this.size - } - } - - get size() { - return this.videoFile.metadata.size - } - get hasMediaEntities() { - return true - } - get duration() { - return 0 - } - - update(payload) { - var json = this.toJSON() - var hasUpdates = false - for (const key in json) { - if (payload[key] !== undefined) { - if (key === 'metadata') { - if (this.metadata.update(payload.metadata)) { - hasUpdates = true - } - } else if (!areEquivalent(payload[key], json[key])) { - this[key] = copyValue(payload[key]) - Logger.debug('[Video] Key updated', key, this[key]) - hasUpdates = true - } - } - } - return hasUpdates - } - - updateCover(coverPath) { - coverPath = filePathToPOSIX(coverPath) - if (this.coverPath === coverPath) return false - this.coverPath = coverPath - return true - } - - removeFileWithInode(inode) { - - } - - findFileWithInode(inode) { - return null - } - - setVideoFile(videoFile) { - this.videoFile = videoFile - } - - setData(mediaMetadata) { - this.metadata = new VideoMetadata() - if (mediaMetadata.metadata) { - this.metadata.setData(mediaMetadata.metadata) - } - - this.coverPath = mediaMetadata.coverPath || null - } - - getPlaybackTitle() { - return this.metadata.title - } - - getPlaybackAuthor() { - return '' - } - - getVideoTrack() { - var track = new VideoTrack() - track.setData(this.libraryItemId, this.videoFile) - return track - } -} -module.exports = Video \ No newline at end of file diff --git a/server/objects/metadata/MusicMetadata.js b/server/objects/metadata/MusicMetadata.js deleted file mode 100644 index 90a887e0..00000000 --- a/server/objects/metadata/MusicMetadata.js +++ /dev/null @@ -1,307 +0,0 @@ -const Logger = require('../../Logger') -const { areEquivalent, copyValue, getTitleIgnorePrefix, getTitlePrefixAtEnd } = require('../../utils/index') - -class MusicMetadata { - constructor(metadata) { - this.title = null - this.artists = [] // Array of strings - this.album = null - this.albumArtist = null - this.genres = [] // Array of strings - this.composer = null - this.originalYear = null - this.releaseDate = null - this.releaseCountry = null - this.releaseType = null - this.releaseStatus = null - this.recordLabel = null - this.language = null - this.explicit = false - - this.discNumber = null - this.discTotal = null - this.trackNumber = null - this.trackTotal = null - - this.isrc = null - this.musicBrainzTrackId = null - this.musicBrainzAlbumId = null - this.musicBrainzAlbumArtistId = null - this.musicBrainzArtistId = null - - if (metadata) { - this.construct(metadata) - } - } - - construct(metadata) { - this.title = metadata.title - this.artists = metadata.artists ? [...metadata.artists] : [] - this.album = metadata.album - this.albumArtist = metadata.albumArtist - this.genres = metadata.genres ? [...metadata.genres] : [] - this.composer = metadata.composer || null - this.originalYear = metadata.originalYear || null - this.releaseDate = metadata.releaseDate || null - this.releaseCountry = metadata.releaseCountry || null - this.releaseType = metadata.releaseType || null - this.releaseStatus = metadata.releaseStatus || null - this.recordLabel = metadata.recordLabel || null - this.language = metadata.language || null - this.explicit = !!metadata.explicit - this.discNumber = metadata.discNumber || null - this.discTotal = metadata.discTotal || null - this.trackNumber = metadata.trackNumber || null - this.trackTotal = metadata.trackTotal || null - this.isrc = metadata.isrc || null - this.musicBrainzTrackId = metadata.musicBrainzTrackId || null - this.musicBrainzAlbumId = metadata.musicBrainzAlbumId || null - this.musicBrainzAlbumArtistId = metadata.musicBrainzAlbumArtistId || null - this.musicBrainzArtistId = metadata.musicBrainzArtistId || null - } - - toJSON() { - return { - title: this.title, - artists: [...this.artists], - album: this.album, - albumArtist: this.albumArtist, - genres: [...this.genres], - composer: this.composer, - originalYear: this.originalYear, - releaseDate: this.releaseDate, - releaseCountry: this.releaseCountry, - releaseType: this.releaseType, - releaseStatus: this.releaseStatus, - recordLabel: this.recordLabel, - language: this.language, - explicit: this.explicit, - discNumber: this.discNumber, - discTotal: this.discTotal, - trackNumber: this.trackNumber, - trackTotal: this.trackTotal, - isrc: this.isrc, - musicBrainzTrackId: this.musicBrainzTrackId, - musicBrainzAlbumId: this.musicBrainzAlbumId, - musicBrainzAlbumArtistId: this.musicBrainzAlbumArtistId, - musicBrainzArtistId: this.musicBrainzArtistId - } - } - - toJSONMinified() { - return { - title: this.title, - titleIgnorePrefix: this.titlePrefixAtEnd, - artists: [...this.artists], - album: this.album, - albumArtist: this.albumArtist, - genres: [...this.genres], - composer: this.composer, - originalYear: this.originalYear, - releaseDate: this.releaseDate, - releaseCountry: this.releaseCountry, - releaseType: this.releaseType, - releaseStatus: this.releaseStatus, - recordLabel: this.recordLabel, - language: this.language, - explicit: this.explicit, - discNumber: this.discNumber, - discTotal: this.discTotal, - trackNumber: this.trackNumber, - trackTotal: this.trackTotal, - isrc: this.isrc, - musicBrainzTrackId: this.musicBrainzTrackId, - musicBrainzAlbumId: this.musicBrainzAlbumId, - musicBrainzAlbumArtistId: this.musicBrainzAlbumArtistId, - musicBrainzArtistId: this.musicBrainzArtistId - } - } - - toJSONExpanded() { - return this.toJSONMinified() - } - - clone() { - return new MusicMetadata(this.toJSON()) - } - - get titleIgnorePrefix() { - return getTitleIgnorePrefix(this.title) - } - - get titlePrefixAtEnd() { - return getTitlePrefixAtEnd(this.title) - } - - setData(mediaMetadata = {}) { - this.title = mediaMetadata.title || null - this.artist = mediaMetadata.artist || null - this.album = mediaMetadata.album || null - } - - update(payload) { - const json = this.toJSON() - let hasUpdates = false - for (const key in json) { - if (payload[key] !== undefined) { - if (!areEquivalent(payload[key], json[key])) { - this[key] = copyValue(payload[key]) - Logger.debug('[MusicMetadata] Key updated', key, this[key]) - hasUpdates = true - } - } - } - return hasUpdates - } - - parseArtistsTag(artistsTag) { - if (!artistsTag || !artistsTag.length) return [] - const separators = ['/', '//', ';'] - for (let i = 0; i < separators.length; i++) { - if (artistsTag.includes(separators[i])) { - return artistsTag.split(separators[i]).map(artist => artist.trim()).filter(a => !!a) - } - } - return [artistsTag] - } - - parseGenresTag(genreTag) { - if (!genreTag || !genreTag.length) return [] - const separators = ['/', '//', ';'] - for (let i = 0; i < separators.length; i++) { - if (genreTag.includes(separators[i])) { - return genreTag.split(separators[i]).map(genre => genre.trim()).filter(g => !!g) - } - } - return [genreTag] - } - - setDataFromAudioMetaTags(audioFileMetaTags, overrideExistingDetails = false) { - const MetadataMapArray = [ - { - tag: 'tagTitle', - key: 'title', - }, - { - tag: 'tagArtist', - key: 'artists' - }, - { - tag: 'tagAlbumArtist', - key: 'albumArtist' - }, - { - tag: 'tagAlbum', - key: 'album', - }, - { - tag: 'tagPublisher', - key: 'recordLabel' - }, - { - tag: 'tagComposer', - key: 'composer' - }, - { - tag: 'tagDate', - key: 'releaseDate' - }, - { - tag: 'tagReleaseCountry', - key: 'releaseCountry' - }, - { - tag: 'tagReleaseType', - key: 'releaseType' - }, - { - tag: 'tagReleaseStatus', - key: 'releaseStatus' - }, - { - tag: 'tagOriginalYear', - key: 'originalYear' - }, - { - tag: 'tagGenre', - key: 'genres' - }, - { - tag: 'tagLanguage', - key: 'language' - }, - { - tag: 'tagLanguage', - key: 'language' - }, - { - tag: 'tagISRC', - key: 'isrc' - }, - { - tag: 'tagMusicBrainzTrackId', - key: 'musicBrainzTrackId' - }, - { - tag: 'tagMusicBrainzAlbumId', - key: 'musicBrainzAlbumId' - }, - { - tag: 'tagMusicBrainzAlbumArtistId', - key: 'musicBrainzAlbumArtistId' - }, - { - tag: 'tagMusicBrainzArtistId', - key: 'musicBrainzArtistId' - }, - { - tag: 'trackNumber', - key: 'trackNumber' - }, - { - tag: 'trackTotal', - key: 'trackTotal' - }, - { - tag: 'discNumber', - key: 'discNumber' - }, - { - tag: 'discTotal', - key: 'discTotal' - } - ] - - const updatePayload = {} - - // Metadata is only mapped to the music track if it is empty - MetadataMapArray.forEach((mapping) => { - let value = audioFileMetaTags[mapping.tag] - - // let tagToUse = mapping.tag - if (!value && mapping.altTag) { - value = audioFileMetaTags[mapping.altTag] - // tagToUse = mapping.altTag - } - - if (value && (typeof value === 'string' || typeof value === 'number')) { - value = value.toString().trim() // Trim whitespace - - if (mapping.key === 'artists' && (!this.artists.length || overrideExistingDetails)) { - updatePayload.artists = this.parseArtistsTag(value) - } else if (mapping.key === 'genres' && (!this.genres.length || overrideExistingDetails)) { - updatePayload.genres = this.parseGenresTag(value) - } else if (!this[mapping.key] || overrideExistingDetails) { - updatePayload[mapping.key] = value - // Logger.debug(`[Book] Mapping metadata to key ${tagToUse} => ${mapping.key}: ${updatePayload[mapping.key]}`) - } - } - }) - - if (Object.keys(updatePayload).length) { - return this.update(updatePayload) - } - return false - } -} -module.exports = MusicMetadata \ No newline at end of file diff --git a/server/objects/metadata/VideoMetadata.js b/server/objects/metadata/VideoMetadata.js deleted file mode 100644 index a2194d15..00000000 --- a/server/objects/metadata/VideoMetadata.js +++ /dev/null @@ -1,80 +0,0 @@ -const Logger = require('../../Logger') -const { areEquivalent, copyValue, getTitleIgnorePrefix, getTitlePrefixAtEnd } = require('../../utils/index') - -class VideoMetadata { - constructor(metadata) { - this.title = null - this.description = null - this.explicit = false - this.language = null - - if (metadata) { - this.construct(metadata) - } - } - - construct(metadata) { - this.title = metadata.title - this.description = metadata.description - this.explicit = metadata.explicit - this.language = metadata.language || null - } - - toJSON() { - return { - title: this.title, - description: this.description, - explicit: this.explicit, - language: this.language - } - } - - toJSONMinified() { - return { - title: this.title, - titleIgnorePrefix: this.titlePrefixAtEnd, - description: this.description, - explicit: this.explicit, - language: this.language - } - } - - toJSONExpanded() { - return this.toJSONMinified() - } - - clone() { - return new VideoMetadata(this.toJSON()) - } - - get titleIgnorePrefix() { - return getTitleIgnorePrefix(this.title) - } - - get titlePrefixAtEnd() { - return getTitlePrefixAtEnd(this.title) - } - - setData(mediaMetadata = {}) { - this.title = mediaMetadata.title || null - this.description = mediaMetadata.description || null - this.explicit = !!mediaMetadata.explicit - this.language = mediaMetadata.language || null - } - - update(payload) { - var json = this.toJSON() - var hasUpdates = false - for (const key in json) { - if (payload[key] !== undefined) { - if (!areEquivalent(payload[key], json[key])) { - this[key] = copyValue(payload[key]) - Logger.debug('[VideoMetadata] Key updated', key, this[key]) - hasUpdates = true - } - } - } - return hasUpdates - } -} -module.exports = VideoMetadata \ No newline at end of file diff --git a/server/utils/constants.js b/server/utils/constants.js index 7a21d2dd..cbfe65f2 100644 --- a/server/utils/constants.js +++ b/server/utils/constants.js @@ -51,7 +51,3 @@ module.exports.AudioMimeType = { AWB: 'audio/amr-wb', CAF: 'audio/x-caf' } - -module.exports.VideoMimeType = { - MP4: 'video/mp4' -} \ No newline at end of file diff --git a/server/utils/globals.js b/server/utils/globals.js index b24fc76d..877cf07a 100644 --- a/server/utils/globals.js +++ b/server/utils/globals.js @@ -2,7 +2,6 @@ const globals = { SupportedImageTypes: ['png', 'jpg', 'jpeg', 'webp'], SupportedAudioTypes: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'ogg', 'oga', 'mp4', 'aac', 'wma', 'aiff', 'wav', 'webm', 'webma', 'mka', 'awb', 'caf'], SupportedEbookTypes: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'], - SupportedVideoTypes: ['mp4'], TextFileTypes: ['txt', 'nfo'], MetadataFileTypes: ['opf', 'abs', 'xml', 'json'] } diff --git a/server/utils/scandir.js b/server/utils/scandir.js index 21c28b8c..ff21e814 100644 --- a/server/utils/scandir.js +++ b/server/utils/scandir.js @@ -19,8 +19,7 @@ const parseNameString = require('./parsers/parseNameString') function isMediaFile(mediaType, ext, audiobooksOnly = false) { if (!ext) return false const extclean = ext.slice(1).toLowerCase() - if (mediaType === 'podcast' || mediaType === 'music') return globals.SupportedAudioTypes.includes(extclean) - else if (mediaType === 'video') return globals.SupportedVideoTypes.includes(extclean) + if (mediaType === 'podcast') return globals.SupportedAudioTypes.includes(extclean) else if (audiobooksOnly) return globals.SupportedAudioTypes.includes(extclean) return globals.SupportedAudioTypes.includes(extclean) || globals.SupportedEbookTypes.includes(extclean) } @@ -35,29 +34,33 @@ module.exports.checkFilepathIsAudioFile = checkFilepathIsAudioFile /** * TODO: Function needs to be re-done - * @param {string} mediaType + * @param {string} mediaType * @param {string[]} paths array of relative file paths * @returns {Record} map of files grouped into potential libarary item dirs */ function groupFilesIntoLibraryItemPaths(mediaType, paths) { // Step 1: Clean path, Remove leading "/", Filter out non-media files in root dir var nonMediaFilePaths = [] - var pathsFiltered = paths.map(path => { - return path.startsWith('/') ? path.slice(1) : path - }).filter(path => { - let parsedPath = Path.parse(path) - // Is not in root dir OR is a book media file - if (parsedPath.dir) { - if (!isMediaFile(mediaType, parsedPath.ext, false)) { // Seperate out non-media files - nonMediaFilePaths.push(path) - return false + var pathsFiltered = paths + .map((path) => { + return path.startsWith('/') ? path.slice(1) : path + }) + .filter((path) => { + let parsedPath = Path.parse(path) + // Is not in root dir OR is a book media file + if (parsedPath.dir) { + if (!isMediaFile(mediaType, parsedPath.ext, false)) { + // Seperate out non-media files + nonMediaFilePaths.push(path) + return false + } + return true + } else if (mediaType === 'book' && isMediaFile(mediaType, parsedPath.ext, false)) { + // (book media type supports single file audiobooks/ebooks in root dir) + return true } - return true - } else if (mediaType === 'book' && isMediaFile(mediaType, parsedPath.ext, false)) { // (book media type supports single file audiobooks/ebooks in root dir) - return true - } - return false - }) + return false + }) // Step 2: Sort by least number of directories pathsFiltered.sort((a, b) => { @@ -69,7 +72,9 @@ function groupFilesIntoLibraryItemPaths(mediaType, paths) { // Step 3: Group files in dirs var itemGroup = {} pathsFiltered.forEach((path) => { - var dirparts = Path.dirname(path).split('/').filter(p => !!p && p !== '.') // dirname returns . if no directory + var dirparts = Path.dirname(path) + .split('/') + .filter((p) => !!p && p !== '.') // dirname returns . if no directory var numparts = dirparts.length var _path = '' @@ -82,14 +87,17 @@ function groupFilesIntoLibraryItemPaths(mediaType, paths) { var dirpart = dirparts.shift() _path = Path.posix.join(_path, dirpart) - if (itemGroup[_path]) { // Directory already has files, add file + if (itemGroup[_path]) { + // Directory already has files, add file var relpath = Path.posix.join(dirparts.join('/'), Path.basename(path)) itemGroup[_path].push(relpath) return - } else if (!dirparts.length) { // This is the last directory, create group + } else if (!dirparts.length) { + // This is the last directory, create group itemGroup[_path] = [Path.basename(path)] return - } else if (dirparts.length === 1 && /^cd\d{1,3}$/i.test(dirparts[0])) { // Next directory is the last and is a CD dir, create group + } else if (dirparts.length === 1 && /^cd\d{1,3}$/i.test(dirparts[0])) { + // Next directory is the last and is a CD dir, create group itemGroup[_path] = [Path.posix.join(dirparts[0], Path.basename(path))] return } @@ -99,7 +107,6 @@ function groupFilesIntoLibraryItemPaths(mediaType, paths) { // Step 4: Add in non-media files if they fit into item group if (nonMediaFilePaths.length) { - for (const nonMediaFilePath of nonMediaFilePaths) { const pathDir = Path.dirname(nonMediaFilePath) const filename = Path.basename(nonMediaFilePath) @@ -111,7 +118,8 @@ function groupFilesIntoLibraryItemPaths(mediaType, paths) { for (let i = 0; i < numparts; i++) { const dirpart = dirparts.shift() _path = Path.posix.join(_path, dirpart) - if (itemGroup[_path]) { // Directory is a group + if (itemGroup[_path]) { + // Directory is a group const relpath = Path.posix.join(dirparts.join('/'), filename) itemGroup[_path].push(relpath) } else if (!dirparts.length) { @@ -126,31 +134,22 @@ function groupFilesIntoLibraryItemPaths(mediaType, paths) { module.exports.groupFilesIntoLibraryItemPaths = groupFilesIntoLibraryItemPaths /** - * @param {string} mediaType + * @param {string} mediaType * @param {{name:string, path:string, dirpath:string, reldirpath:string, fullpath:string, extension:string, deep:number}[]} fileItems (see recurseFiles) - * @param {boolean} [audiobooksOnly=false] + * @param {boolean} [audiobooksOnly=false] * @returns {Record} map of files grouped into potential libarary item dirs */ function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems, audiobooksOnly = false) { - // Handle music where every audio file is a library item - if (mediaType === 'music') { - const audioFileGroup = {} - fileItems.filter(i => isMediaFile(mediaType, i.extension, audiobooksOnly)).forEach((item) => { - audioFileGroup[item.path] = item.path - }) - return audioFileGroup - } - // Step 1: Filter out non-book-media files in root dir (with depth of 0) - const itemsFiltered = fileItems.filter(i => { - return i.deep > 0 || ((mediaType === 'book' || mediaType === 'video' || mediaType === 'music') && isMediaFile(mediaType, i.extension, audiobooksOnly)) + const itemsFiltered = fileItems.filter((i) => { + return i.deep > 0 || (mediaType === 'book' && isMediaFile(mediaType, i.extension, audiobooksOnly)) }) // Step 2: Seperate media files and other files // - Directories without a media file will not be included const mediaFileItems = [] const otherFileItems = [] - itemsFiltered.forEach(item => { + itemsFiltered.forEach((item) => { if (isMediaFile(mediaType, item.extension, audiobooksOnly)) mediaFileItems.push(item) else otherFileItems.push(item) }) @@ -158,7 +157,7 @@ function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems, audiobooksOnly // Step 3: Group audio files in library items const libraryItemGroup = {} mediaFileItems.forEach((item) => { - const dirparts = item.reldirpath.split('/').filter(p => !!p) + const dirparts = item.reldirpath.split('/').filter((p) => !!p) const numparts = dirparts.length let _path = '' @@ -171,14 +170,17 @@ function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems, audiobooksOnly const dirpart = dirparts.shift() _path = Path.posix.join(_path, dirpart) - if (libraryItemGroup[_path]) { // Directory already has files, add file + if (libraryItemGroup[_path]) { + // Directory already has files, add file const relpath = Path.posix.join(dirparts.join('/'), item.name) libraryItemGroup[_path].push(relpath) return - } else if (!dirparts.length) { // This is the last directory, create group + } else if (!dirparts.length) { + // This is the last directory, create group libraryItemGroup[_path] = [item.name] return - } else if (dirparts.length === 1 && /^cd\d{1,3}$/i.test(dirparts[0])) { // Next directory is the last and is a CD dir, create group + } else if (dirparts.length === 1 && /^cd\d{1,3}$/i.test(dirparts[0])) { + // Next directory is the last and is a CD dir, create group libraryItemGroup[_path] = [Path.posix.join(dirparts[0], item.name)] return } @@ -196,7 +198,8 @@ function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems, audiobooksOnly for (let i = 0; i < numparts; i++) { const dirpart = dirparts.shift() _path = Path.posix.join(_path, dirpart) - if (libraryItemGroup[_path]) { // Directory is audiobook group + if (libraryItemGroup[_path]) { + // Directory is audiobook group const relpath = Path.posix.join(dirparts.join('/'), item.name) libraryItemGroup[_path].push(relpath) return @@ -209,33 +212,35 @@ module.exports.groupFileItemsIntoLibraryItemDirs = groupFileItemsIntoLibraryItem /** * Get LibraryFile from filepath - * @param {string} libraryItemPath - * @param {string[]} files + * @param {string} libraryItemPath + * @param {string[]} files * @returns {import('../objects/files/LibraryFile')} */ function buildLibraryFile(libraryItemPath, files) { - return Promise.all(files.map(async (file) => { - const filePath = Path.posix.join(libraryItemPath, file) - const newLibraryFile = new LibraryFile() - await newLibraryFile.setDataFromPath(filePath, file) - return newLibraryFile - })) + return Promise.all( + files.map(async (file) => { + const filePath = Path.posix.join(libraryItemPath, file) + const newLibraryFile = new LibraryFile() + await newLibraryFile.setDataFromPath(filePath, file) + return newLibraryFile + }) + ) } module.exports.buildLibraryFile = buildLibraryFile /** * Get details parsed from filenames - * - * @param {string} relPath - * @param {boolean} parseSubtitle + * + * @param {string} relPath + * @param {boolean} parseSubtitle * @returns {LibraryItemFilenameMetadata} */ function getBookDataFromDir(relPath, parseSubtitle = false) { const splitDir = relPath.split('/') var folder = splitDir.pop() // Audio files will always be in the directory named for the title - series = (splitDir.length > 1) ? splitDir.pop() : null // If there are at least 2 more directories, next furthest will be the series - author = (splitDir.length > 0) ? splitDir.pop() : null // There could be many more directories, but only the top 3 are used for naming /author/series/title/ + series = splitDir.length > 1 ? splitDir.pop() : null // If there are at least 2 more directories, next furthest will be the series + author = splitDir.length > 0 ? splitDir.pop() : null // There could be many more directories, but only the top 3 are used for naming /author/series/title/ // The may contain various other pieces of metadata, these functions extract it. var [folder, asin] = getASIN(folder) @@ -244,7 +249,6 @@ function getBookDataFromDir(relPath, parseSubtitle = false) { var [folder, publishedYear] = getPublishedYear(folder) var [title, subtitle] = parseSubtitle ? getSubtitle(folder) : [folder, null] - return { title, subtitle, @@ -260,8 +264,8 @@ module.exports.getBookDataFromDir = getBookDataFromDir /** * Extract narrator from folder name - * - * @param {string} folder + * + * @param {string} folder * @returns {[string, string]} [folder, narrator] */ function getNarrator(folder) { @@ -272,7 +276,7 @@ function getNarrator(folder) { /** * Extract series sequence from folder name - * + * * @example * 'Book 2 - Title - Subtitle' * 'Title - Subtitle - Vol 12' @@ -283,8 +287,8 @@ function getNarrator(folder) { * '100 - Book Title' * '6. Title' * '0.5 - Book Title' - * - * @param {string} folder + * + * @param {string} folder * @returns {[string, string]} [folder, sequence] */ function getSequence(folder) { @@ -299,7 +303,9 @@ function getSequence(folder) { if (match && !(match.groups.suffix && !(match.groups.volumeLabel || match.groups.trailingDot))) { volumeNumber = isNaN(match.groups.sequence) ? match.groups.sequence : Number(match.groups.sequence).toString() parts[i] = match.groups.suffix - if (!parts[i]) { parts.splice(i, 1) } + if (!parts[i]) { + parts.splice(i, 1) + } break } } @@ -310,8 +316,8 @@ function getSequence(folder) { /** * Extract published year from folder name - * - * @param {string} folder + * + * @param {string} folder * @returns {[string, string]} [folder, publishedYear] */ function getPublishedYear(folder) { @@ -329,8 +335,8 @@ function getPublishedYear(folder) { /** * Extract subtitle from folder name - * - * @param {string} folder + * + * @param {string} folder * @returns {[string, string]} [folder, subtitle] */ function getSubtitle(folder) { @@ -341,8 +347,8 @@ function getSubtitle(folder) { /** * Extract asin from folder name - * - * @param {string} folder + * + * @param {string} folder * @returns {[string, string]} [folder, asin] */ function getASIN(folder) { @@ -358,8 +364,8 @@ function getASIN(folder) { } /** - * - * @param {string} relPath + * + * @param {string} relPath * @returns {LibraryItemFilenameMetadata} */ function getPodcastDataFromDir(relPath) { @@ -373,10 +379,10 @@ function getPodcastDataFromDir(relPath) { } /** - * - * @param {string} libraryMediaType - * @param {string} folderPath - * @param {string} relPath + * + * @param {string} libraryMediaType + * @param {string} folderPath + * @param {string} relPath * @returns {{ mediaMetadata: LibraryItemFilenameMetadata, relPath: string, path: string}} */ function getDataFromMediaDir(libraryMediaType, folderPath, relPath) { @@ -386,7 +392,8 @@ function getDataFromMediaDir(libraryMediaType, folderPath, relPath) { if (libraryMediaType === 'podcast') { mediaMetadata = getPodcastDataFromDir(relPath) - } else { // book + } else { + // book mediaMetadata = getBookDataFromDir(relPath, !!global.ServerSettings.scannerParseSubtitle) }