mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-02-03 16:59:41 +00:00
Clean out old unused objects
This commit is contained in:
parent
24923c0009
commit
0344a63b48
29 changed files with 180 additions and 1540 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
module.exports = LibraryItem
|
||||
|
|
|
|||
|
|
@ -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 || ''
|
||||
|
|
|
|||
|
|
@ -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
|
||||
module.exports = LibraryFile
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -51,7 +51,3 @@ module.exports.AudioMimeType = {
|
|||
AWB: 'audio/amr-wb',
|
||||
CAF: 'audio/x-caf'
|
||||
}
|
||||
|
||||
module.exports.VideoMimeType = {
|
||||
MP4: 'video/mp4'
|
||||
}
|
||||
|
|
@ -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']
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string,string[]>} 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<string,string[]>} 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)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue