mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-12-15 00:09:38 +00:00
Testing video media type
This commit is contained in:
parent
705aac40d7
commit
acf22ca4fa
27 changed files with 1124 additions and 92 deletions
|
|
@ -184,7 +184,7 @@ class LibraryItemController {
|
|||
|
||||
// POST: api/items/:id/play
|
||||
startPlaybackSession(req, res) {
|
||||
if (!req.libraryItem.media.numTracks) {
|
||||
if (!req.libraryItem.media.numTracks && req.libraryItem.mediaType !== 'video') {
|
||||
Logger.error(`[LibraryItemController] startPlaybackSession cannot playback ${req.libraryItem.id}`)
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,29 +119,38 @@ class PlaybackSessionManager {
|
|||
const newPlaybackSession = new PlaybackSession()
|
||||
newPlaybackSession.setData(libraryItem, user, mediaPlayer, deviceInfo, userStartTime, episodeId)
|
||||
|
||||
var audioTracks = []
|
||||
if (shouldDirectPlay) {
|
||||
Logger.debug(`[PlaybackSessionManager] "${user.username}" starting direct play session for item "${libraryItem.id}"`)
|
||||
audioTracks = libraryItem.getDirectPlayTracklist(episodeId)
|
||||
newPlaybackSession.playMethod = PlayMethod.DIRECTPLAY
|
||||
if (libraryItem.mediaType === 'video') {
|
||||
if (shouldDirectPlay) {
|
||||
Logger.debug(`[PlaybackSessionManager] "${user.username}" starting direct play session for item "${libraryItem.id}"`)
|
||||
newPlaybackSession.videoTrack = libraryItem.media.getVideoTrack()
|
||||
newPlaybackSession.playMethod = PlayMethod.DIRECTPLAY
|
||||
} else {
|
||||
// HLS not supported for video yet
|
||||
}
|
||||
} else {
|
||||
Logger.debug(`[PlaybackSessionManager] "${user.username}" starting stream session for item "${libraryItem.id}"`)
|
||||
var stream = new Stream(newPlaybackSession.id, this.StreamsPath, user, libraryItem, episodeId, userStartTime, this.clientEmitter.bind(this))
|
||||
await stream.generatePlaylist()
|
||||
stream.start() // Start transcode
|
||||
var audioTracks = []
|
||||
if (shouldDirectPlay) {
|
||||
Logger.debug(`[PlaybackSessionManager] "${user.username}" starting direct play session for item "${libraryItem.id}"`)
|
||||
audioTracks = libraryItem.getDirectPlayTracklist(episodeId)
|
||||
newPlaybackSession.playMethod = PlayMethod.DIRECTPLAY
|
||||
} else {
|
||||
Logger.debug(`[PlaybackSessionManager] "${user.username}" starting stream session for item "${libraryItem.id}"`)
|
||||
var stream = new Stream(newPlaybackSession.id, this.StreamsPath, user, libraryItem, episodeId, userStartTime, this.clientEmitter.bind(this))
|
||||
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}"`)
|
||||
newPlaybackSession.stream = null
|
||||
})
|
||||
stream.on('closed', () => {
|
||||
Logger.debug(`[PlaybackSessionManager] Stream closed for session "${newPlaybackSession.id}"`)
|
||||
newPlaybackSession.stream = null
|
||||
})
|
||||
}
|
||||
newPlaybackSession.audioTracks = audioTracks
|
||||
}
|
||||
|
||||
newPlaybackSession.audioTracks = audioTracks
|
||||
|
||||
// Will save on the first sync
|
||||
user.currentSessionId = newPlaybackSession.id
|
||||
|
||||
|
|
|
|||
|
|
@ -143,13 +143,13 @@ class PodcastManager {
|
|||
|
||||
async probeAudioFile(libraryFile) {
|
||||
var path = libraryFile.metadata.path
|
||||
var audioProbeData = await prober.probe(path)
|
||||
if (audioProbeData.error) {
|
||||
Logger.error(`[PodcastManager] Podcast Episode downloaded but failed to probe "${path}"`, audioProbeData.error)
|
||||
var mediaProbeData = await prober.probe(path)
|
||||
if (mediaProbeData.error) {
|
||||
Logger.error(`[PodcastManager] Podcast Episode downloaded but failed to probe "${path}"`, mediaProbeData.error)
|
||||
return false
|
||||
}
|
||||
var newAudioFile = new AudioFile()
|
||||
newAudioFile.setDataFromProbe(libraryFile, audioProbeData)
|
||||
newAudioFile.setDataFromProbe(libraryFile, mediaProbeData)
|
||||
return newAudioFile
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ class Library {
|
|||
else if (this.icon.endsWith('s') && availableIcons.includes(this.icon.slice(0, -1))) this.icon = this.icon.slice(0, -1)
|
||||
else this.icon = 'database'
|
||||
}
|
||||
if (!this.mediaType || (this.mediaType !== 'podcast' && this.mediaType !== 'book')) {
|
||||
if (!this.mediaType || (this.mediaType !== 'podcast' && this.mediaType !== 'book' && this.mediaType !== 'video')) {
|
||||
this.mediaType = 'book'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ const abmetadataGenerator = require('../utils/abmetadataGenerator')
|
|||
const LibraryFile = require('./files/LibraryFile')
|
||||
const Book = require('./mediaTypes/Book')
|
||||
const Podcast = require('./mediaTypes/Podcast')
|
||||
const Video = require('./mediatypes/Video')
|
||||
const { areEquivalent, copyValue, getId } = require('../utils/index')
|
||||
|
||||
class LibraryItem {
|
||||
|
|
@ -67,11 +68,12 @@ class LibraryItem {
|
|||
this.mediaType = libraryItem.mediaType
|
||||
if (this.mediaType === 'book') {
|
||||
this.media = new Book(libraryItem.media)
|
||||
this.media.libraryItemId = this.id
|
||||
} else if (this.mediaType === 'podcast') {
|
||||
this.media = new Podcast(libraryItem.media)
|
||||
this.media.libraryItemId = this.id
|
||||
} else if (this.mediaType === 'video') {
|
||||
this.media = new Video(libraryItem.media)
|
||||
}
|
||||
this.media.libraryItemId = this.id
|
||||
|
||||
this.libraryFiles = libraryItem.libraryFiles.map(f => new LibraryFile(f))
|
||||
}
|
||||
|
|
@ -175,16 +177,15 @@ class LibraryItem {
|
|||
// Data comes from scandir library item data
|
||||
setData(libraryMediaType, payload) {
|
||||
this.id = getId('li')
|
||||
if (libraryMediaType === 'podcast') {
|
||||
this.mediaType = 'podcast'
|
||||
this.mediaType = libraryMediaType
|
||||
if (libraryMediaType === 'video') {
|
||||
this.media = new Video()
|
||||
} else if (libraryMediaType === 'podcast') {
|
||||
this.media = new Podcast()
|
||||
this.media.libraryItemId = this.id
|
||||
} else {
|
||||
this.mediaType = 'book'
|
||||
this.media = new Book()
|
||||
this.media.libraryItemId = this.id
|
||||
}
|
||||
|
||||
this.media.libraryItemId = this.id
|
||||
|
||||
for (const key in payload) {
|
||||
if (key === 'libraryFiles') {
|
||||
|
|
@ -460,6 +461,8 @@ class LibraryItem {
|
|||
|
||||
// Saves metadata.abs file
|
||||
async saveMetadata() {
|
||||
if (this.mediaType === 'video') return
|
||||
|
||||
if (this.isSavingMetadata) return
|
||||
this.isSavingMetadata = true
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ const { PlayMethod } = require('../utils/constants')
|
|||
const BookMetadata = require('./metadata/BookMetadata')
|
||||
const PodcastMetadata = require('./metadata/PodcastMetadata')
|
||||
const DeviceInfo = require('./DeviceInfo')
|
||||
const VideoMetadata = require('./metadata/VideoMetadata')
|
||||
|
||||
class PlaybackSession {
|
||||
constructor(session) {
|
||||
|
|
@ -38,6 +39,7 @@ class PlaybackSession {
|
|||
// Not saved in DB
|
||||
this.lastSave = 0
|
||||
this.audioTracks = []
|
||||
this.videoTrack = null
|
||||
this.stream = null
|
||||
|
||||
if (session) {
|
||||
|
|
@ -97,6 +99,7 @@ class PlaybackSession {
|
|||
startedAt: this.startedAt,
|
||||
updatedAt: this.updatedAt,
|
||||
audioTracks: this.audioTracks.map(at => at.toJSON()),
|
||||
videoTrack: this.videoTrack ? this.videoTrack.toJSON() : null,
|
||||
libraryItem: libraryItem.toJSONExpanded()
|
||||
}
|
||||
}
|
||||
|
|
@ -120,6 +123,8 @@ 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 || ''
|
||||
|
|
|
|||
|
|
@ -40,13 +40,14 @@ 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'
|
||||
return this.fileType === 'audio' || this.fileType === 'ebook' || this.fileType === 'video'
|
||||
}
|
||||
|
||||
get isOPFFile() {
|
||||
|
|
|
|||
109
server/objects/files/VideoFile.js
Normal file
109
server/objects/files/VideoFile.js
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
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
|
||||
42
server/objects/files/VideoTrack.js
Normal file
42
server/objects/files/VideoTrack.js
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
const Path = require('path')
|
||||
const { encodeUriPath } = require('../../utils/index')
|
||||
|
||||
class VideoTrack {
|
||||
constructor() {
|
||||
this.index = null
|
||||
this.duration = null
|
||||
this.title = null
|
||||
this.contentUrl = null
|
||||
this.mimeType = null
|
||||
this.metadata = null
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
index: this.index,
|
||||
duration: this.duration,
|
||||
title: this.title,
|
||||
contentUrl: this.contentUrl,
|
||||
mimeType: this.mimeType,
|
||||
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(`/s/item/${itemId}`, encodeUriPath(videoFile.metadata.relPath))
|
||||
this.mimeType = videoFile.mimeType
|
||||
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
|
||||
145
server/objects/mediaTypes/Video.js
Normal file
145
server/objects/mediaTypes/Video.js
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
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')
|
||||
|
||||
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 shouldSearchForCover() {
|
||||
return false
|
||||
}
|
||||
get hasEmbeddedCoverArt() {
|
||||
return false
|
||||
}
|
||||
get hasIssues() {
|
||||
return false
|
||||
}
|
||||
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 = coverPath.replace(/\\/g, '/')
|
||||
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
|
||||
97
server/objects/metadata/VideoMetadata.js
Normal file
97
server/objects/metadata/VideoMetadata.js
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
const Logger = require('../../Logger')
|
||||
const { areEquivalent, copyValue } = 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.titleIgnorePrefix,
|
||||
description: this.description,
|
||||
explicit: this.explicit,
|
||||
language: this.language
|
||||
}
|
||||
}
|
||||
|
||||
toJSONExpanded() {
|
||||
return this.toJSONMinified()
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new VideoMetadata(this.toJSON())
|
||||
}
|
||||
|
||||
get titleIgnorePrefix() {
|
||||
if (!this.title) return ''
|
||||
var prefixesToIgnore = global.ServerSettings.sortingPrefixes || []
|
||||
for (const prefix of prefixesToIgnore) {
|
||||
// e.g. for prefix "the". If title is "The Book Title" return "Book Title, The"
|
||||
if (this.title.toLowerCase().startsWith(`${prefix} `)) {
|
||||
return this.title.substr(prefix.length + 1) + `, ${prefix.substr(0, 1).toUpperCase() + prefix.substr(1)}`
|
||||
}
|
||||
}
|
||||
return this.title
|
||||
}
|
||||
|
||||
searchQuery(query) { // Returns key if match is found
|
||||
var keysToCheck = ['title']
|
||||
for (var key of keysToCheck) {
|
||||
if (this[key] && String(this[key]).toLowerCase().includes(query)) {
|
||||
return {
|
||||
matchKey: key,
|
||||
matchText: this[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
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
|
||||
|
|
@ -343,6 +343,7 @@ class User {
|
|||
|
||||
checkCanAccessLibraryItem(libraryItem) {
|
||||
if (!this.checkCanAccessLibrary(libraryItem.libraryId)) return false
|
||||
|
||||
if (libraryItem.media.metadata.explicit && !this.canAccessExplicitContent) return false
|
||||
return this.checkCanAccessLibraryItemWithTags(libraryItem.media.tags)
|
||||
}
|
||||
|
|
|
|||
277
server/scanner/MediaFileScanner.js
Normal file
277
server/scanner/MediaFileScanner.js
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
const Path = require('path')
|
||||
|
||||
const AudioFile = require('../objects/files/AudioFile')
|
||||
const VideoFile = require('../objects/files/VideoFile')
|
||||
|
||||
const prober = require('../utils/prober')
|
||||
const Logger = require('../Logger')
|
||||
const { LogLevel } = require('../utils/constants')
|
||||
|
||||
class MediaFileScanner {
|
||||
constructor() { }
|
||||
|
||||
getTrackAndDiscNumberFromFilename(mediaMetadataFromScan, audioLibraryFile) {
|
||||
const { title, author, series, publishedYear } = mediaMetadataFromScan
|
||||
const { filename, path } = audioLibraryFile.metadata
|
||||
var partbasename = Path.basename(filename, Path.extname(filename))
|
||||
|
||||
// Remove title, author, series, and publishedYear from filename if there
|
||||
if (title) partbasename = partbasename.replace(title, '')
|
||||
if (author) partbasename = partbasename.replace(author, '')
|
||||
if (series) partbasename = partbasename.replace(series, '')
|
||||
if (publishedYear) partbasename = partbasename.replace(publishedYear)
|
||||
|
||||
// Look for disc number
|
||||
var discNumber = null
|
||||
var discMatch = partbasename.match(/\b(disc|cd) ?(\d\d?)\b/i)
|
||||
if (discMatch && discMatch.length > 2 && discMatch[2]) {
|
||||
if (!isNaN(discMatch[2])) {
|
||||
discNumber = Number(discMatch[2])
|
||||
}
|
||||
|
||||
// Remove disc number from filename
|
||||
partbasename = partbasename.replace(/\b(disc|cd) ?(\d\d?)\b/i, '')
|
||||
}
|
||||
|
||||
// Look for disc number in folder path e.g. /Book Title/CD01/audiofile.mp3
|
||||
var pathdir = Path.dirname(path).split('/').pop()
|
||||
if (pathdir && /^cd\d{1,3}$/i.test(pathdir)) {
|
||||
var discFromFolder = Number(pathdir.replace(/cd/i, ''))
|
||||
if (!isNaN(discFromFolder) && discFromFolder !== null) discNumber = discFromFolder
|
||||
}
|
||||
|
||||
var numbersinpath = partbasename.match(/\d{1,4}/g)
|
||||
var trackNumber = numbersinpath && numbersinpath.length ? parseInt(numbersinpath[0]) : null
|
||||
return {
|
||||
trackNumber,
|
||||
discNumber
|
||||
}
|
||||
}
|
||||
|
||||
getAverageScanDurationMs(results) {
|
||||
if (!results.length) return 0
|
||||
var total = 0
|
||||
for (let i = 0; i < results.length; i++) total += results[i].elapsed
|
||||
return Math.floor(total / results.length)
|
||||
}
|
||||
|
||||
async scan(mediaType, libraryFile, mediaMetadataFromScan, verbose = false) {
|
||||
var probeStart = Date.now()
|
||||
var probeData = await prober.probe(libraryFile.metadata.path, verbose)
|
||||
if (probeData.error) {
|
||||
Logger.error(`[MediaFileScanner] ${probeData.error} : "${libraryFile.metadata.path}"`)
|
||||
return null
|
||||
}
|
||||
|
||||
if (mediaType === 'video') {
|
||||
if (!probeData.videoStream) {
|
||||
Logger.error('[MediaFileScanner] Invalid video file no video stream')
|
||||
return null
|
||||
}
|
||||
|
||||
var videoFile = new VideoFile()
|
||||
videoFile.setDataFromProbe(libraryFile, probeData)
|
||||
|
||||
return {
|
||||
videoFile,
|
||||
elapsed: Date.now() - probeStart
|
||||
}
|
||||
} else {
|
||||
if (!probeData.audioStream) {
|
||||
Logger.error('[MediaFileScanner] Invalid audio file no audio stream')
|
||||
return null
|
||||
}
|
||||
|
||||
var audioFile = new AudioFile()
|
||||
audioFile.trackNumFromMeta = probeData.trackNumber
|
||||
audioFile.discNumFromMeta = probeData.discNumber
|
||||
if (mediaType === 'book') {
|
||||
const { trackNumber, discNumber } = this.getTrackAndDiscNumberFromFilename(mediaMetadataFromScan, libraryFile)
|
||||
audioFile.trackNumFromFilename = trackNumber
|
||||
audioFile.discNumFromFilename = discNumber
|
||||
}
|
||||
audioFile.setDataFromProbe(libraryFile, probeData)
|
||||
|
||||
return {
|
||||
audioFile,
|
||||
elapsed: Date.now() - probeStart
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Returns array of { MediaFile, elapsed, averageScanDuration } from audio file scan objects
|
||||
async executeMediaFileScans(mediaType, mediaLibraryFiles, scanData) {
|
||||
var mediaMetadataFromScan = scanData.media.metadata || null
|
||||
var proms = []
|
||||
for (let i = 0; i < mediaLibraryFiles.length; i++) {
|
||||
proms.push(this.scan(mediaType, mediaLibraryFiles[i], mediaMetadataFromScan))
|
||||
}
|
||||
var scanStart = Date.now()
|
||||
var results = await Promise.all(proms).then((scanResults) => scanResults.filter(sr => sr))
|
||||
return {
|
||||
audioFiles: results.filter(r => r.audioFile).map(r => r.audioFile),
|
||||
videoFiles: results.filter(r => r.videoFile).map(r => r.videoFile),
|
||||
elapsed: Date.now() - scanStart,
|
||||
averageScanDuration: this.getAverageScanDurationMs(results)
|
||||
}
|
||||
}
|
||||
|
||||
isSequential(nums) {
|
||||
if (!nums || !nums.length) return false
|
||||
if (nums.length === 1) return true
|
||||
var prev = nums[0]
|
||||
for (let i = 1; i < nums.length; i++) {
|
||||
if (nums[i] - prev > 1) return false
|
||||
prev = nums[i]
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
removeDupes(nums) {
|
||||
if (!nums || !nums.length) return []
|
||||
if (nums.length === 1) return nums
|
||||
|
||||
var nodupes = [nums[0]]
|
||||
nums.forEach((num) => {
|
||||
if (num > nodupes[nodupes.length - 1]) nodupes.push(num)
|
||||
})
|
||||
return nodupes
|
||||
}
|
||||
|
||||
runSmartTrackOrder(libraryItem, audioFiles) {
|
||||
var discsFromFilename = []
|
||||
var tracksFromFilename = []
|
||||
var discsFromMeta = []
|
||||
var tracksFromMeta = []
|
||||
|
||||
audioFiles.forEach((af) => {
|
||||
if (af.discNumFromFilename !== null) discsFromFilename.push(af.discNumFromFilename)
|
||||
if (af.discNumFromMeta !== null) discsFromMeta.push(af.discNumFromMeta)
|
||||
if (af.trackNumFromFilename !== null) tracksFromFilename.push(af.trackNumFromFilename)
|
||||
if (af.trackNumFromMeta !== null) tracksFromMeta.push(af.trackNumFromMeta)
|
||||
af.validateTrackIndex() // Sets error if no valid track number
|
||||
})
|
||||
discsFromFilename.sort((a, b) => a - b)
|
||||
discsFromMeta.sort((a, b) => a - b)
|
||||
tracksFromFilename.sort((a, b) => a - b)
|
||||
tracksFromMeta.sort((a, b) => a - b)
|
||||
|
||||
var discKey = null
|
||||
if (discsFromMeta.length === audioFiles.length && this.isSequential(discsFromMeta)) {
|
||||
discKey = 'discNumFromMeta'
|
||||
} else if (discsFromFilename.length === audioFiles.length && this.isSequential(discsFromFilename)) {
|
||||
discKey = 'discNumFromFilename'
|
||||
}
|
||||
|
||||
var trackKey = null
|
||||
tracksFromFilename = this.removeDupes(tracksFromFilename)
|
||||
tracksFromMeta = this.removeDupes(tracksFromMeta)
|
||||
if (tracksFromFilename.length > tracksFromMeta.length) {
|
||||
trackKey = 'trackNumFromFilename'
|
||||
} else {
|
||||
trackKey = 'trackNumFromMeta'
|
||||
}
|
||||
|
||||
if (discKey !== null) {
|
||||
Logger.debug(`[AudioFileScanner] Smart track order for "${libraryItem.media.metadata.title}" using disc key ${discKey} and track key ${trackKey}`)
|
||||
audioFiles.sort((a, b) => {
|
||||
let Dx = a[discKey] - b[discKey]
|
||||
if (Dx === 0) Dx = a[trackKey] - b[trackKey]
|
||||
return Dx
|
||||
})
|
||||
} else {
|
||||
Logger.debug(`[AudioFileScanner] Smart track order for "${libraryItem.media.metadata.title}" using track key ${trackKey}`)
|
||||
audioFiles.sort((a, b) => a[trackKey] - b[trackKey])
|
||||
}
|
||||
|
||||
for (let i = 0; i < audioFiles.length; i++) {
|
||||
audioFiles[i].index = i + 1
|
||||
var existingAF = libraryItem.media.findFileWithInode(audioFiles[i].ino)
|
||||
if (existingAF) {
|
||||
if (existingAF.updateFromScan) existingAF.updateFromScan(audioFiles[i])
|
||||
} else {
|
||||
libraryItem.media.addAudioFile(audioFiles[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async scanMediaFiles(mediaLibraryFiles, scanData, libraryItem, preferAudioMetadata, libraryScan = null) {
|
||||
var hasUpdated = false
|
||||
|
||||
var mediaScanResult = await this.executeMediaFileScans(libraryItem.mediaType, mediaLibraryFiles, scanData)
|
||||
if (libraryItem.mediaType === 'video') {
|
||||
if (mediaScanResult.videoFiles.length) {
|
||||
// TODO: Check for updates etc
|
||||
hasUpdated = true
|
||||
libraryItem.media.setVideoFile(mediaScanResult.videoFiles[0])
|
||||
}
|
||||
} else if (mediaScanResult.audioFiles.length) {
|
||||
if (libraryScan) {
|
||||
libraryScan.addLog(LogLevel.DEBUG, `Library Item "${scanData.path}" Audio file scan took ${mediaScanResult.elapsed}ms for ${mediaScanResult.audioFiles.length} with average time of ${mediaScanResult.averageScanDuration}ms`)
|
||||
}
|
||||
|
||||
var totalAudioFilesToInclude = mediaScanResult.audioFiles.length
|
||||
var newAudioFiles = mediaScanResult.audioFiles.filter(af => {
|
||||
return !libraryItem.media.findFileWithInode(af.ino)
|
||||
})
|
||||
|
||||
// Book: Adding audio files to book media
|
||||
if (libraryItem.mediaType === 'book') {
|
||||
if (newAudioFiles.length) {
|
||||
// Single Track Audiobooks
|
||||
if (totalAudioFilesToInclude === 1) {
|
||||
var af = mediaScanResult.audioFiles[0]
|
||||
af.index = 1
|
||||
libraryItem.media.addAudioFile(af)
|
||||
hasUpdated = true
|
||||
} else {
|
||||
this.runSmartTrackOrder(libraryItem, mediaScanResult.audioFiles)
|
||||
hasUpdated = true
|
||||
}
|
||||
} else {
|
||||
// Only update metadata not index
|
||||
mediaScanResult.audioFiles.forEach((af) => {
|
||||
var existingAF = libraryItem.media.findFileWithInode(af.ino)
|
||||
if (existingAF) {
|
||||
af.index = existingAF.index
|
||||
if (existingAF.updateFromScan && existingAF.updateFromScan(af)) {
|
||||
hasUpdated = true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Set book details from audio file ID3 tags, optional prefer
|
||||
if (libraryItem.media.setMetadataFromAudioFile(preferAudioMetadata)) {
|
||||
hasUpdated = true
|
||||
}
|
||||
|
||||
if (hasUpdated) {
|
||||
libraryItem.media.rebuildTracks()
|
||||
}
|
||||
} else { // Podcast Media Type
|
||||
var existingAudioFiles = mediaScanResult.audioFiles.filter(af => libraryItem.media.findFileWithInode(af.ino))
|
||||
|
||||
if (newAudioFiles.length) {
|
||||
var newIndex = libraryItem.media.episodes.length + 1
|
||||
newAudioFiles.forEach((newAudioFile) => {
|
||||
libraryItem.media.addNewEpisodeFromAudioFile(newAudioFile, newIndex++)
|
||||
})
|
||||
libraryItem.media.reorderEpisodes()
|
||||
hasUpdated = true
|
||||
}
|
||||
|
||||
// Update audio file metadata for audio files already there
|
||||
existingAudioFiles.forEach((af) => {
|
||||
var peAudioFile = libraryItem.media.findFileWithInode(af.ino)
|
||||
if (peAudioFile.updateFromScan && peAudioFile.updateFromScan(af)) {
|
||||
hasUpdated = true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return hasUpdated
|
||||
}
|
||||
}
|
||||
module.exports = new MediaFileScanner()
|
||||
|
|
@ -1,11 +1,15 @@
|
|||
const AudioFileMetadata = require('../objects/metadata/AudioMetaTags')
|
||||
|
||||
class AudioProbeData {
|
||||
class MediaProbeData {
|
||||
constructor() {
|
||||
this.embeddedCoverArt = null
|
||||
this.format = null
|
||||
this.duration = null
|
||||
this.size = null
|
||||
|
||||
this.audioStream = null
|
||||
this.videoStream = null
|
||||
|
||||
this.bitRate = null
|
||||
this.codec = null
|
||||
this.timeBase = null
|
||||
|
|
@ -35,6 +39,10 @@ class AudioProbeData {
|
|||
this.format = data.format
|
||||
this.duration = data.duration
|
||||
this.size = data.size
|
||||
|
||||
this.audioStream = audioStream
|
||||
this.videoStream = this.embeddedCoverArt ? null : data.video_stream || null
|
||||
|
||||
this.bitRate = audioStream.bit_rate || data.bit_rate
|
||||
this.codec = audioStream.codec
|
||||
this.timeBase = audioStream.time_base
|
||||
|
|
@ -78,4 +86,4 @@ class AudioProbeData {
|
|||
}
|
||||
}
|
||||
}
|
||||
module.exports = AudioProbeData
|
||||
module.exports = MediaProbeData
|
||||
|
|
@ -8,7 +8,7 @@ const { comparePaths } = require('../utils/index')
|
|||
const { getIno } = require('../utils/fileUtils')
|
||||
const { ScanResult, LogLevel } = require('../utils/constants')
|
||||
|
||||
const AudioFileScanner = require('./AudioFileScanner')
|
||||
const MediaFileScanner = require('./MediaFileScanner')
|
||||
const BookFinder = require('../finders/BookFinder')
|
||||
const LibraryItem = require('../objects/LibraryItem')
|
||||
const LibraryScan = require('./LibraryScan')
|
||||
|
|
@ -80,7 +80,7 @@ class Scanner {
|
|||
// Scan all audio files
|
||||
if (libraryItem.hasAudioFiles) {
|
||||
var libraryAudioFiles = libraryItem.libraryFiles.filter(lf => lf.fileType === 'audio')
|
||||
if (await AudioFileScanner.scanAudioFiles(libraryAudioFiles, libraryItemData, libraryItem, this.db.serverSettings.scannerPreferAudioMetadata)) {
|
||||
if (await MediaFileScanner.scanMediaFiles(libraryAudioFiles, libraryItemData, libraryItem, this.db.serverSettings.scannerPreferAudioMetadata)) {
|
||||
hasUpdated = true
|
||||
}
|
||||
|
||||
|
|
@ -240,18 +240,18 @@ class Scanner {
|
|||
if (!hasMediaFile) {
|
||||
libraryScan.addLog(LogLevel.WARN, `Item found "${libraryItemDataFound.path}" has no media files`)
|
||||
} else {
|
||||
var audioFileSize = 0
|
||||
dataFound.libraryFiles.filter(lf => lf.fileType == 'audio').forEach(lf => audioFileSize += lf.metadata.size)
|
||||
var mediaFileSize = 0
|
||||
dataFound.libraryFiles.filter(lf => lf.fileType === 'audio' || lf.fileType === 'video').forEach(lf => mediaFileSize += lf.metadata.size)
|
||||
|
||||
// If this item will go over max size then push current chunk
|
||||
if (audioFileSize + newItemDataToScanSize > MaxSizePerChunk && newItemDataToScan.length > 0) {
|
||||
if (mediaFileSize + newItemDataToScanSize > MaxSizePerChunk && newItemDataToScan.length > 0) {
|
||||
newItemDataToScanChunks.push(newItemDataToScan)
|
||||
newItemDataToScanSize = 0
|
||||
newItemDataToScan = []
|
||||
}
|
||||
|
||||
newItemDataToScan.push(dataFound)
|
||||
newItemDataToScanSize += audioFileSize
|
||||
newItemDataToScanSize += mediaFileSize
|
||||
if (newItemDataToScanSize >= MaxSizePerChunk) {
|
||||
newItemDataToScanChunks.push(newItemDataToScan)
|
||||
newItemDataToScanSize = 0
|
||||
|
|
@ -337,7 +337,7 @@ class Scanner {
|
|||
// forceRescan all existing audio files - will probe and update ID3 tag metadata
|
||||
var existingAudioFiles = existingLibraryFiles.filter(lf => lf.fileType === 'audio')
|
||||
if (libraryScan.scanOptions.forceRescan && existingAudioFiles.length) {
|
||||
if (await AudioFileScanner.scanAudioFiles(existingAudioFiles, scanData, libraryItem, libraryScan.preferAudioMetadata, libraryScan)) {
|
||||
if (await MediaFileScanner.scanMediaFiles(existingAudioFiles, scanData, libraryItem, libraryScan.preferAudioMetadata, libraryScan)) {
|
||||
hasUpdated = true
|
||||
}
|
||||
}
|
||||
|
|
@ -345,7 +345,7 @@ class Scanner {
|
|||
var newAudioFiles = newLibraryFiles.filter(lf => lf.fileType === 'audio')
|
||||
var removedAudioFiles = filesRemoved.filter(lf => lf.fileType === 'audio')
|
||||
if (newAudioFiles.length || removedAudioFiles.length) {
|
||||
if (await AudioFileScanner.scanAudioFiles(newAudioFiles, scanData, libraryItem, libraryScan.preferAudioMetadata, libraryScan)) {
|
||||
if (await MediaFileScanner.scanMediaFiles(newAudioFiles, scanData, libraryItem, libraryScan.preferAudioMetadata, libraryScan)) {
|
||||
hasUpdated = true
|
||||
}
|
||||
}
|
||||
|
|
@ -386,9 +386,9 @@ class Scanner {
|
|||
var libraryItem = new LibraryItem()
|
||||
libraryItem.setData(libraryMediaType, libraryItemData)
|
||||
|
||||
var audioFiles = libraryItemData.libraryFiles.filter(lf => lf.fileType === 'audio')
|
||||
if (audioFiles.length) {
|
||||
await AudioFileScanner.scanAudioFiles(audioFiles, libraryItemData, libraryItem, preferAudioMetadata, libraryScan)
|
||||
var mediaFiles = libraryItemData.libraryFiles.filter(lf => lf.fileType === 'audio' || lf.fileType === 'video')
|
||||
if (mediaFiles.length) {
|
||||
await MediaFileScanner.scanMediaFiles(mediaFiles, libraryItemData, libraryItem, preferAudioMetadata, libraryScan)
|
||||
}
|
||||
|
||||
await libraryItem.syncFiles(preferOpfMetadata)
|
||||
|
|
@ -408,7 +408,7 @@ class Scanner {
|
|||
}
|
||||
|
||||
// Scan for cover if enabled and has no cover
|
||||
if (libraryMediaType !== 'podcast') {
|
||||
if (libraryMediaType === 'book') {
|
||||
if (libraryItem && findCovers && !libraryItem.media.coverPath && libraryItem.media.shouldSearchForCover) {
|
||||
var updatedCover = await this.searchForCover(libraryItem, libraryScan)
|
||||
libraryItem.media.updateLastCoverSearch(updatedCover)
|
||||
|
|
|
|||
|
|
@ -44,4 +44,8 @@ module.exports.AudioMimeType = {
|
|||
FLAC: 'audio/flac',
|
||||
WMA: 'audio/x-ms-wma',
|
||||
AIFF: 'audio/x-aiff'
|
||||
}
|
||||
|
||||
module.exports.VideoMimeType = {
|
||||
MP4: 'video/mp4'
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ const globals = {
|
|||
SupportedImageTypes: ['png', 'jpg', 'jpeg', 'webp'],
|
||||
SupportedAudioTypes: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'ogg', 'mp4', 'aac', 'wma', 'aiff'],
|
||||
SupportedEbookTypes: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'],
|
||||
SupportedVideoTypes: ['mp4'],
|
||||
TextFileTypes: ['txt', 'nfo'],
|
||||
MetadataFileTypes: ['opf', 'abs', 'xml']
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ module.exports = {
|
|||
if (series && !data.series.find(se => se.id === series.id)) data.series.push({ id: series.id, name: series.name })
|
||||
})
|
||||
}
|
||||
if (mediaMetadata.genres.length) {
|
||||
if (mediaMetadata.genres && mediaMetadata.genres.length) {
|
||||
mediaMetadata.genres.forEach((genre) => {
|
||||
if (genre && !data.genres.includes(genre)) data.genres.push(genre)
|
||||
})
|
||||
|
|
@ -399,7 +399,7 @@ module.exports = {
|
|||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
} else if (libraryItem.isBook) {
|
||||
// Book categories
|
||||
|
||||
// Newest series
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
const ffprobe = require('node-ffprobe')
|
||||
const AudioProbeData = require('../scanner/AudioProbeData')
|
||||
const MediaProbeData = require('../scanner/MediaProbeData')
|
||||
|
||||
const Logger = require('../Logger')
|
||||
|
||||
|
|
@ -274,7 +274,7 @@ function parseProbeData(data, verbose = false) {
|
|||
}
|
||||
}
|
||||
|
||||
// Updated probe returns AudioProbeData object
|
||||
// Updated probe returns MediaProbeData object
|
||||
function probe(filepath, verbose = false) {
|
||||
if (process.env.FFPROBE_PATH) {
|
||||
ffprobe.FFPROBE_PATH = process.env.FFPROBE_PATH
|
||||
|
|
@ -283,12 +283,12 @@ function probe(filepath, verbose = false) {
|
|||
return ffprobe(filepath)
|
||||
.then(raw => {
|
||||
var rawProbeData = parseProbeData(raw, verbose)
|
||||
if (!rawProbeData || !rawProbeData.audio_stream) {
|
||||
if (!rawProbeData || (!rawProbeData.audio_stream && !rawProbeData.video_stream)) {
|
||||
return {
|
||||
error: rawProbeData ? 'Invalid audio file: no audio streams found' : 'Probe Failed'
|
||||
error: rawProbeData ? 'Invalid media file: no audio or video streams found' : 'Probe Failed'
|
||||
}
|
||||
} else {
|
||||
var probeData = new AudioProbeData()
|
||||
var probeData = new MediaProbeData()
|
||||
probeData.setData(rawProbeData)
|
||||
return probeData
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ function isMediaFile(mediaType, ext) {
|
|||
if (!ext) return false
|
||||
var extclean = ext.slice(1).toLowerCase()
|
||||
if (mediaType === 'podcast') return globals.SupportedAudioTypes.includes(extclean)
|
||||
else if (mediaType === 'video') return globals.SupportedVideoTypes.includes(extclean)
|
||||
return globals.SupportedAudioTypes.includes(extclean) || globals.SupportedEbookTypes.includes(extclean)
|
||||
}
|
||||
|
||||
|
|
@ -72,7 +73,7 @@ module.exports.groupFilesIntoLibraryItemPaths = groupFilesIntoLibraryItemPaths
|
|||
function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems) {
|
||||
// Step 1: Filter out non-book-media files in root dir (with depth of 0)
|
||||
var itemsFiltered = fileItems.filter(i => {
|
||||
return i.deep > 0 || (mediaType === 'book' && isMediaFile(mediaType, i.extension))
|
||||
return i.deep > 0 || ((mediaType === 'book' || mediaType === 'video') && isMediaFile(mediaType, i.extension))
|
||||
})
|
||||
|
||||
// Step 2: Seperate media files and other files
|
||||
|
|
@ -136,7 +137,7 @@ function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems) {
|
|||
}
|
||||
|
||||
function cleanFileObjects(libraryItemPath, files) {
|
||||
return Promise.all(files.map(async(file) => {
|
||||
return Promise.all(files.map(async (file) => {
|
||||
var filePath = Path.posix.join(libraryItemPath, file)
|
||||
var newLibraryFile = new LibraryFile()
|
||||
await newLibraryFile.setDataFromPath(filePath, file)
|
||||
|
|
@ -314,9 +315,11 @@ function getPodcastDataFromDir(folderPath, relPath) {
|
|||
function getDataFromMediaDir(libraryMediaType, folderPath, relPath, serverSettings) {
|
||||
if (libraryMediaType === 'podcast') {
|
||||
return getPodcastDataFromDir(folderPath, relPath)
|
||||
} else {
|
||||
} else if (libraryMediaType === 'book') {
|
||||
var parseSubtitle = !!serverSettings.scannerParseSubtitle
|
||||
return getBookDataFromDir(folderPath, relPath, parseSubtitle)
|
||||
} else {
|
||||
return this.getPodcastDataFromDir(folderPath, relPath)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -333,10 +336,10 @@ async function getLibraryItemFileData(libraryMediaType, folder, libraryItemPath,
|
|||
if (isSingleMediaItem) { // Single media item in root of folder
|
||||
fileItems = [
|
||||
{
|
||||
fullpath: libraryItemPath,
|
||||
path: libraryItemDir // actually the relPath (only filename here)
|
||||
}
|
||||
]
|
||||
fullpath: libraryItemPath,
|
||||
path: libraryItemDir // actually the relPath (only filename here)
|
||||
}
|
||||
]
|
||||
libraryItemData = {
|
||||
path: libraryItemPath, // full path
|
||||
relPath: libraryItemDir, // only filename
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue