Laying the groundwork for music media type #964

This commit is contained in:
advplyr 2022-12-22 16:38:55 -06:00
parent c3717f6979
commit b884f8fe11
18 changed files with 491 additions and 134 deletions

View file

@ -230,27 +230,30 @@ class CoverManager {
}
async saveEmbeddedCoverArt(libraryItem) {
var audioFileWithCover = null
if (libraryItem.mediaType === 'book') audioFileWithCover = libraryItem.media.audioFiles.find(af => af.embeddedCoverArt)
else {
var episodeWithCover = libraryItem.media.episodes.find(ep => ep.audioFile.embeddedCoverArt)
const audioFileWithCover = null
if (libraryItem.mediaType === 'book') {
audioFileWithCover = libraryItem.media.audioFiles.find(af => af.embeddedCoverArt)
} else if (libraryItem.mediaType == 'podcast') {
const episodeWithCover = libraryItem.media.episodes.find(ep => ep.audioFile.embeddedCoverArt)
if (episodeWithCover) audioFileWithCover = episodeWithCover.audioFile
} else if (libraryItem.mediaType === 'music') {
audioFileWithCover = libraryItem.media.audioFile
}
if (!audioFileWithCover) return false
var coverDirPath = this.getCoverDirectory(libraryItem)
const coverDirPath = this.getCoverDirectory(libraryItem)
await fs.ensureDir(coverDirPath)
var coverFilename = audioFileWithCover.embeddedCoverArt === 'png' ? 'cover.png' : 'cover.jpg'
var coverFilePath = Path.join(coverDirPath, coverFilename)
const coverFilename = audioFileWithCover.embeddedCoverArt === 'png' ? 'cover.png' : 'cover.jpg'
const coverFilePath = Path.join(coverDirPath, coverFilename)
var coverAlreadyExists = await fs.pathExists(coverFilePath)
const coverAlreadyExists = await fs.pathExists(coverFilePath)
if (coverAlreadyExists) {
Logger.warn(`[CoverManager] Extract embedded cover art but cover already exists for "${libraryItem.media.metadata.title}" - bail`)
return false
}
var success = await extractCoverArt(audioFileWithCover.metadata.path, coverFilePath)
const success = await extractCoverArt(audioFileWithCover.metadata.path, coverFilePath)
if (success) {
await filePerms.setDefault(coverFilePath)

View file

@ -127,7 +127,7 @@ class PlaybackSessionManager {
const shouldDirectPlay = options.forceDirectPlay || (!options.forceTranscode && libraryItem.media.checkCanDirectPlay(options, episodeId))
const mediaPlayer = options.mediaPlayer || 'unknown'
const userProgress = user.getMediaProgress(libraryItem.id, episodeId)
const userProgress = libraryItem.isMusic ? null : user.getMediaProgress(libraryItem.id, episodeId)
let userStartTime = 0
if (userProgress) {
if (userProgress.isFinished) {

View file

@ -57,7 +57,9 @@ class Library {
else if (this.icon === 'comic') this.icon = 'file-picture'
else this.icon = 'database'
}
if (!this.mediaType || (this.mediaType !== 'podcast' && this.mediaType !== 'book' && this.mediaType !== 'video')) {
const mediaTypes = ['podcast', 'book', 'video', 'music']
if (!this.mediaType || !mediaTypes.includes(this.mediaType)) {
this.mediaType = 'book'
}
}

View file

@ -7,6 +7,7 @@ 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, getId, cleanStringForSearch } = require('../utils/index')
class LibraryItem {
@ -72,6 +73,8 @@ class LibraryItem {
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
@ -153,13 +156,14 @@ class LibraryItem {
get isPodcast() { return this.mediaType === 'podcast' }
get isBook() { return this.mediaType === 'book' }
get isMusic() { return this.mediaType === 'music' }
get size() {
var total = 0
let total = 0
this.libraryFiles.forEach((lf) => total += lf.metadata.size)
return total
}
get audioFileTotalSize() {
var total = 0
let total = 0
this.libraryFiles.filter(lf => lf.fileType == 'audio').forEach((lf) => total += lf.metadata.size)
return total
}
@ -182,8 +186,10 @@ class LibraryItem {
this.media = new Video()
} else if (libraryMediaType === 'podcast') {
this.media = new Podcast()
} else {
} else if (libraryMediaType === 'book') {
this.media = new Book()
} else if (libraryMediaType === 'music') {
this.media = new Music()
}
this.media.libraryItemId = this.id
@ -348,11 +354,11 @@ class LibraryItem {
}
})
var newLibraryFiles = []
var existingLibraryFiles = []
const newLibraryFiles = []
const existingLibraryFiles = []
dataFound.libraryFiles.forEach((lf) => {
var fileFoundCheck = this.checkFileFound(lf, true)
const fileFoundCheck = this.checkFileFound(lf, true)
if (fileFoundCheck === null) {
newLibraryFiles.push(lf)
} else if (fileFoundCheck && lf.metadata.format !== 'abs') { // Ignore abs file updates
@ -397,7 +403,7 @@ class LibraryItem {
// If cover path is in item folder, make sure libraryFile exists for it
if (this.media.coverPath && this.media.coverPath.startsWith(this.path)) {
var lf = this.libraryFiles.find(lf => lf.metadata.path === this.media.coverPath)
const lf = this.libraryFiles.find(lf => lf.metadata.path === this.media.coverPath)
if (!lf) {
Logger.warn(`[LibraryItem] Invalid cover path - library file dne "${this.media.coverPath}"`)
this.media.updateCover('')
@ -419,7 +425,7 @@ class LibraryItem {
// Set metadata from files
async syncFiles(preferOpfMetadata) {
var hasUpdated = false
let hasUpdated = false
if (this.mediaType === 'book') {
// Add/update ebook file (ebooks that were removed are removed in checkScanData)
@ -436,7 +442,7 @@ class LibraryItem {
}
// Set cover image if not set
var imageFiles = this.libraryFiles.filter(lf => lf.fileType === 'image')
const imageFiles = this.libraryFiles.filter(lf => lf.fileType === 'image')
if (imageFiles.length && !this.media.coverPath) {
this.media.coverPath = imageFiles[0].metadata.path
Logger.debug('[LibraryItem] Set media cover path', this.media.coverPath)
@ -444,7 +450,7 @@ class LibraryItem {
}
// Parse metadata files
var textMetadataFiles = this.libraryFiles.filter(lf => lf.fileType === 'metadata' || lf.fileType === 'text')
const textMetadataFiles = this.libraryFiles.filter(lf => lf.fileType === 'metadata' || lf.fileType === 'text')
if (textMetadataFiles.length) {
if (await this.media.syncMetadataFiles(textMetadataFiles, preferOpfMetadata)) {
hasUpdated = true
@ -468,12 +474,12 @@ class LibraryItem {
// Saves metadata.abs file
async saveMetadata() {
if (this.mediaType === 'video') return
if (this.mediaType === 'video' || this.mediaType === 'music') return
if (this.isSavingMetadata) return
this.isSavingMetadata = true
var metadataPath = Path.join(global.MetadataPath, 'items', this.id)
let metadataPath = Path.join(global.MetadataPath, 'items', this.id)
if (global.ServerSettings.storeMetadataWithItem && !this.isFile) {
metadataPath = this.path
} else {

View file

@ -148,7 +148,7 @@ class PodcastEpisode {
// Only checks container format
checkCanDirectPlay(payload) {
var supportedMimeTypes = payload.supportedMimeTypes || []
const supportedMimeTypes = payload.supportedMimeTypes || []
return supportedMimeTypes.includes(this.audioFile.mimeType)
}

View file

@ -118,9 +118,9 @@ class Book {
return this.missingParts.length || this.invalidAudioFiles.length
}
get tracks() {
var startOffset = 0
let startOffset = 0
return this.includedAudioFiles.map((af) => {
var audioTrack = new AudioTrack()
const audioTrack = new AudioTrack()
audioTrack.setData(this.libraryItemId, af, startOffset)
startOffset += audioTrack.duration
return audioTrack

View file

@ -0,0 +1,159 @@
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')
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(),
size: this.size
}
}
toJSONExpanded() {
return {
libraryItemId: this.libraryItemId,
metadata: this.metadata.toJSONExpanded(),
coverPath: this.coverPath,
tags: [...this.tags],
audioFile: this.audioFile.toJSON(),
size: this.size
}
}
get size() {
return this.audioFile.metadata.size
}
get hasMediaEntities() {
return !!this.audioFile
}
get shouldSearchForCover() {
return false
}
get hasEmbeddedCoverArt() {
return this.audioFile.embeddedCoverArt
}
get hasIssues() {
return false
}
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 = coverPath.replace(/\\/g, '/')
if (this.coverPath === coverPath) return false
this.coverPath = coverPath
return true
}
removeFileWithInode(inode) {
return false
}
findFileWithInode(inode) {
return this.audioFile && this.audioFile.ino === inode
}
setData(mediaData) {
this.metadata = new MusicMetadata()
if (mediaData.metadata) {
this.metadata.setData(mediaData.metadata)
}
this.coverPath = mediaData.coverPath || null
}
setAudioFile(audioFile) {
this.audioFile = audioFile
}
syncMetadataFiles(textMetadataFiles, opfMetadataOverrideDetails) {
return false
}
searchQuery(query) {
return {}
}
// 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

View file

@ -0,0 +1,104 @@
const Logger = require('../../Logger')
const { areEquivalent, copyValue, cleanStringForSearch, getTitleIgnorePrefix, getTitlePrefixAtEnd } = require('../../utils/index')
class MusicMetadata {
constructor(metadata) {
this.title = null
this.artist = null
this.album = null
this.genres = [] // Array of strings
this.releaseDate = null
this.language = null
this.explicit = false
if (metadata) {
this.construct(metadata)
}
}
construct(metadata) {
this.title = metadata.title
this.artist = metadata.artist
this.album = metadata.album
this.genres = metadata.genres ? [...metadata.genres] : []
this.releaseDate = metadata.releaseDate || null
this.language = metadata.language
this.explicit = !!metadata.explicit
}
toJSON() {
return {
title: this.title,
artist: this.artist,
album: this.album,
genres: [...this.genres],
releaseDate: this.releaseDate,
language: this.language,
explicit: this.explicit
}
}
toJSONMinified() {
return {
title: this.title,
titleIgnorePrefix: this.titlePrefixAtEnd,
artist: this.artist,
album: this.album,
genres: [...this.genres],
releaseDate: this.releaseDate,
language: this.language,
explicit: this.explicit
}
}
toJSONExpanded() {
return this.toJSONMinified()
}
clone() {
return new MusicMetadata(this.toJSON())
}
get titleIgnorePrefix() {
return getTitleIgnorePrefix(this.title)
}
get titlePrefixAtEnd() {
return getTitlePrefixAtEnd(this.title)
}
searchQuery(query) { // Returns key if match is found
const keysToCheck = ['title', 'artist', 'album']
for (const key of keysToCheck) {
if (this[key] && cleanStringForSearch(String(this[key])).includes(query)) {
return {
matchKey: key,
matchText: this[key]
}
}
}
return null
}
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
}
}
module.exports = MusicMetadata

View file

@ -57,9 +57,9 @@ class MediaFileScanner {
}
async scan(mediaType, libraryFile, mediaMetadataFromScan, verbose = false) {
var probeStart = Date.now()
const probeStart = Date.now()
var probeData = null
let probeData = null
// TODO: Temp not using tone for probing until more testing can be done
// if (global.ServerSettings.scannerUseTone) {
// Logger.debug(`[MediaFileScanner] using tone to probe audio file "${libraryFile.metadata.path}"`)
@ -79,7 +79,7 @@ class MediaFileScanner {
return null
}
var videoFile = new VideoFile()
const videoFile = new VideoFile()
videoFile.setDataFromProbe(libraryFile, probeData)
return {
@ -92,7 +92,7 @@ class MediaFileScanner {
return null
}
var audioFile = new AudioFile()
const audioFile = new AudioFile()
audioFile.trackNumFromMeta = probeData.trackNumber
audioFile.discNumFromMeta = probeData.discNumber
if (mediaType === 'book') {
@ -113,13 +113,13 @@ class MediaFileScanner {
async executeMediaFileScans(libraryItem, mediaLibraryFiles, scanData) {
const mediaType = libraryItem.mediaType
var scanStart = Date.now()
var mediaMetadataFromScan = scanData.media.metadata || null
var proms = []
const scanStart = Date.now()
const mediaMetadataFromScan = scanData.media.metadata || null
const proms = []
for (let i = 0; i < mediaLibraryFiles.length; i++) {
proms.push(this.scan(mediaType, mediaLibraryFiles[i], mediaMetadataFromScan))
}
var results = await Promise.all(proms).then((scanResults) => scanResults.filter(sr => sr))
const 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),
@ -131,7 +131,7 @@ class MediaFileScanner {
isSequential(nums) {
if (!nums || !nums.length) return false
if (nums.length === 1) return true
var prev = nums[0]
let prev = nums[0]
for (let i = 1; i < nums.length; i++) {
if (nums[i] - prev > 1) return false
prev = nums[i]
@ -207,9 +207,9 @@ class MediaFileScanner {
}
async scanMediaFiles(mediaLibraryFiles, scanData, libraryItem, preferAudioMetadata, preferOverdriveMediaMarker, libraryScan = null) {
var hasUpdated = false
let hasUpdated = false
var mediaScanResult = await this.executeMediaFileScans(libraryItem, mediaLibraryFiles, scanData)
const mediaScanResult = await this.executeMediaFileScans(libraryItem, mediaLibraryFiles, scanData)
if (libraryItem.mediaType === 'video') {
if (mediaScanResult.videoFiles.length) {
@ -223,32 +223,32 @@ class MediaFileScanner {
}
Logger.debug(`Library Item "${scanData.path}" Media file scan took ${mediaScanResult.elapsed}ms with ${mediaScanResult.audioFiles.length} audio files averaging ${mediaScanResult.averageScanDuration}ms per MB`)
var newAudioFiles = mediaScanResult.audioFiles.filter(af => {
const newAudioFiles = mediaScanResult.audioFiles.filter(af => {
return !libraryItem.media.findFileWithInode(af.ino)
})
// Book: Adding audio files to book media
if (libraryItem.mediaType === 'book') {
var mediaScanFileInodes = mediaScanResult.audioFiles.map(af => af.ino)
const mediaScanFileInodes = mediaScanResult.audioFiles.map(af => af.ino)
// Filter for existing valid track audio files not included in the audio files scanned
var existingAudioFiles = libraryItem.media.audioFiles.filter(af => af.isValidTrack && !mediaScanFileInodes.includes(af.ino))
const existingAudioFiles = libraryItem.media.audioFiles.filter(af => af.isValidTrack && !mediaScanFileInodes.includes(af.ino))
if (newAudioFiles.length) {
// Single Track Audiobooks
if (mediaScanFileInodes.length + existingAudioFiles.length === 1) {
var af = mediaScanResult.audioFiles[0]
const af = mediaScanResult.audioFiles[0]
af.index = 1
libraryItem.media.addAudioFile(af)
hasUpdated = true
} else {
var allAudioFiles = existingAudioFiles.concat(mediaScanResult.audioFiles)
const allAudioFiles = existingAudioFiles.concat(mediaScanResult.audioFiles)
this.runSmartTrackOrder(libraryItem, allAudioFiles)
hasUpdated = true
}
} else {
// Only update metadata not index
mediaScanResult.audioFiles.forEach((af) => {
var existingAF = libraryItem.media.findFileWithInode(af.ino)
const existingAF = libraryItem.media.findFileWithInode(af.ino)
if (existingAF) {
af.index = existingAF.index
if (existingAF.updateFromScan && existingAF.updateFromScan(af)) {
@ -266,11 +266,11 @@ class MediaFileScanner {
if (hasUpdated) {
libraryItem.media.rebuildTracks(preferOverdriveMediaMarker)
}
} else { // Podcast Media Type
var existingAudioFiles = mediaScanResult.audioFiles.filter(af => libraryItem.media.findFileWithInode(af.ino))
} else if (libraryItem.mediaType === 'podcast') { // Podcast Media Type
const existingAudioFiles = mediaScanResult.audioFiles.filter(af => libraryItem.media.findFileWithInode(af.ino))
if (newAudioFiles.length) {
var newIndex = libraryItem.media.episodes.length + 1
let newIndex = libraryItem.media.episodes.length + 1
newAudioFiles.forEach((newAudioFile) => {
libraryItem.media.addNewEpisodeFromAudioFile(newAudioFile, newIndex++)
})
@ -280,11 +280,19 @@ class MediaFileScanner {
// Update audio file metadata for audio files already there
existingAudioFiles.forEach((af) => {
var peAudioFile = libraryItem.media.findFileWithInode(af.ino)
const peAudioFile = libraryItem.media.findFileWithInode(af.ino)
if (peAudioFile.updateFromScan && peAudioFile.updateFromScan(af)) {
hasUpdated = true
}
})
} else if (libraryItem.mediaType === 'music') { // Music
// Only one audio file in library item
if (newAudioFiles.length) { // New audio file
libraryItem.media.setAudioFile(newAudioFiles[0])
hasUpdated = true
} else if (libraryItem.media.audioFile && libraryItem.media.audioFile.updateFromScan(mediaScanResult.audioFiles[0])) {
hasUpdated = true
}
}
}

View file

@ -47,7 +47,7 @@ class Scanner {
}
async scanLibraryItemById(libraryItemId) {
var libraryItem = this.db.libraryItems.find(li => li.id === libraryItemId)
const libraryItem = this.db.libraryItems.find(li => li.id === libraryItemId)
if (!libraryItem) {
Logger.error(`[Scanner] Scan libraryItem by id not found ${libraryItemId}`)
return ScanResult.NOTHING
@ -68,13 +68,13 @@ class Scanner {
async scanLibraryItem(libraryMediaType, folder, libraryItem) {
// TODO: Support for single media item
var libraryItemData = await getLibraryItemFileData(libraryMediaType, folder, libraryItem.path, false, this.db.serverSettings)
const libraryItemData = await getLibraryItemFileData(libraryMediaType, folder, libraryItem.path, false, this.db.serverSettings)
if (!libraryItemData) {
return ScanResult.NOTHING
}
var hasUpdated = false
let hasUpdated = false
var checkRes = libraryItem.checkScanData(libraryItemData)
const checkRes = libraryItem.checkScanData(libraryItemData)
if (checkRes.updated) hasUpdated = true
// Sync other files first so that local images are used as cover art
@ -84,14 +84,14 @@ class Scanner {
// Scan all audio files
if (libraryItem.hasAudioFiles) {
var libraryAudioFiles = libraryItem.libraryFiles.filter(lf => lf.fileType === 'audio')
const libraryAudioFiles = libraryItem.libraryFiles.filter(lf => lf.fileType === 'audio')
if (await MediaFileScanner.scanMediaFiles(libraryAudioFiles, libraryItemData, libraryItem, this.db.serverSettings.scannerPreferAudioMetadata, this.db.serverSettings.scannerPreferOverdriveMediaMarker)) {
hasUpdated = true
}
// Extract embedded cover art if cover is not already in directory
if (libraryItem.media.hasEmbeddedCoverArt && !libraryItem.media.coverPath) {
var coverPath = await this.coverManager.saveEmbeddedCoverArt(libraryItem)
const coverPath = await this.coverManager.saveEmbeddedCoverArt(libraryItem)
if (coverPath) {
Logger.debug(`[Scanner] Saved embedded cover art "${coverPath}"`)
hasUpdated = true
@ -172,8 +172,8 @@ class Scanner {
// Scan each library
for (let i = 0; i < libraryScan.folders.length; i++) {
var folder = libraryScan.folders[i]
var itemDataFoundInFolder = await scanFolder(libraryScan.libraryMediaType, folder, this.db.serverSettings)
const folder = libraryScan.folders[i]
const itemDataFoundInFolder = await scanFolder(libraryScan.libraryMediaType, folder, this.db.serverSettings)
libraryScan.addLog(LogLevel.INFO, `${itemDataFoundInFolder.length} item data found in folder "${folder.fullPath}"`)
libraryItemDataFound = libraryItemDataFound.concat(itemDataFoundInFolder)
}
@ -196,16 +196,16 @@ class Scanner {
// Check for existing & removed library items
for (let i = 0; i < libraryItemsInLibrary.length; i++) {
var libraryItem = libraryItemsInLibrary[i]
const libraryItem = libraryItemsInLibrary[i]
// Find library item folder with matching inode or matching path
var dataFound = libraryItemDataFound.find(lid => lid.ino === libraryItem.ino || comparePaths(lid.relPath, libraryItem.relPath))
const dataFound = libraryItemDataFound.find(lid => lid.ino === libraryItem.ino || comparePaths(lid.relPath, libraryItem.relPath))
if (!dataFound) {
libraryScan.addLog(LogLevel.WARN, `Library Item "${libraryItem.media.metadata.title}" is missing`)
libraryScan.resultsMissing++
libraryItem.setMissing()
itemsToUpdate.push(libraryItem)
} else {
var checkRes = libraryItem.checkScanData(dataFound)
const checkRes = libraryItem.checkScanData(dataFound)
if (checkRes.newLibraryFiles.length || libraryScan.scanOptions.forceRescan) { // Item has new files
checkRes.libraryItem = libraryItem
checkRes.scanData = dataFound
@ -244,15 +244,15 @@ class Scanner {
// Potential NEW Library Items
for (let i = 0; i < libraryItemDataFound.length; i++) {
var dataFound = libraryItemDataFound[i]
const dataFound = libraryItemDataFound[i]
var hasMediaFile = dataFound.libraryFiles.some(lf => lf.isMediaFile)
const hasMediaFile = dataFound.libraryFiles.some(lf => lf.isMediaFile)
if (!hasMediaFile) {
libraryScan.addLog(LogLevel.WARN, `Item found "${libraryItemDataFound.path}" has no media files`)
} else {
if (global.ServerSettings.scannerUseSingleThreadedProber) {
// If this item will go over max size then push current chunk
var mediaFileSize = 0
let mediaFileSize = 0
dataFound.libraryFiles.filter(lf => lf.fileType === 'audio' || lf.fileType === 'video').forEach(lf => mediaFileSize += lf.metadata.size)
if (mediaFileSize + newItemDataToScanSize > MaxSizePerChunk && newItemDataToScan.length > 0) {
newItemDataToScanChunks.push(newItemDataToScan)
@ -277,8 +277,8 @@ class Scanner {
// Library Items not requiring a scan but require a search for cover
for (let i = 0; i < itemsToFindCovers.length; i++) {
var libraryItem = itemsToFindCovers[i]
var updatedCover = await this.searchForCover(libraryItem, libraryScan)
const libraryItem = itemsToFindCovers[i]
const updatedCover = await this.searchForCover(libraryItem, libraryScan)
libraryItem.media.updateLastCoverSearch(updatedCover)
}
@ -397,10 +397,10 @@ class Scanner {
if (libraryScan) libraryScan.addLog(LogLevel.DEBUG, `Scanning new library item "${libraryItemData.path}"`)
else Logger.debug(`[Scanner] Scanning new item "${libraryItemData.path}"`)
var libraryItem = new LibraryItem()
const libraryItem = new LibraryItem()
libraryItem.setData(libraryMediaType, libraryItemData)
var mediaFiles = libraryItemData.libraryFiles.filter(lf => lf.fileType === 'audio' || lf.fileType === 'video')
const mediaFiles = libraryItemData.libraryFiles.filter(lf => lf.fileType === 'audio' || lf.fileType === 'video')
if (mediaFiles.length) {
await MediaFileScanner.scanMediaFiles(mediaFiles, libraryItemData, libraryItem, preferAudioMetadata, preferOverdriveMediaMarker, libraryScan)
}
@ -414,7 +414,7 @@ class Scanner {
// Extract embedded cover art if cover is not already in directory
if (libraryItem.media.hasEmbeddedCoverArt && !libraryItem.media.coverPath) {
var coverPath = await this.coverManager.saveEmbeddedCoverArt(libraryItem)
const coverPath = await this.coverManager.saveEmbeddedCoverArt(libraryItem)
if (coverPath) {
if (libraryScan) libraryScan.addLog(LogLevel.DEBUG, `Saved embedded cover art "${coverPath}"`)
else Logger.debug(`[Scanner] Saved embedded cover art "${coverPath}"`)
@ -424,7 +424,7 @@ class Scanner {
// Scan for cover if enabled and has no cover
if (libraryMediaType === 'book') {
if (libraryItem && findCovers && !libraryItem.media.coverPath && libraryItem.media.shouldSearchForCover) {
var updatedCover = await this.searchForCover(libraryItem, libraryScan)
const updatedCover = await this.searchForCover(libraryItem, libraryScan)
libraryItem.media.updateLastCoverSearch(updatedCover)
}
}
@ -636,19 +636,19 @@ class Scanner {
}
async scanPotentialNewLibraryItem(libraryMediaType, folder, fullPath, isSingleMediaItem = false) {
var libraryItemData = await getLibraryItemFileData(libraryMediaType, folder, fullPath, isSingleMediaItem, this.db.serverSettings)
const libraryItemData = await getLibraryItemFileData(libraryMediaType, folder, fullPath, isSingleMediaItem, this.db.serverSettings)
if (!libraryItemData) return null
var serverSettings = this.db.serverSettings
const serverSettings = this.db.serverSettings
return this.scanNewLibraryItem(libraryItemData, libraryMediaType, serverSettings.scannerPreferAudioMetadata, serverSettings.scannerPreferOpfMetadata, serverSettings.scannerFindCovers, serverSettings.scannerPreferOverdriveMediaMarker)
}
async searchForCover(libraryItem, libraryScan = null) {
var options = {
const options = {
titleDistance: 2,
authorDistance: 2
}
var scannerCoverProvider = this.db.serverSettings.scannerCoverProvider
var results = await this.bookFinder.findCovers(scannerCoverProvider, libraryItem.media.metadata.title, libraryItem.media.metadata.authorName, options)
const scannerCoverProvider = this.db.serverSettings.scannerCoverProvider
const results = await this.bookFinder.findCovers(scannerCoverProvider, libraryItem.media.metadata.title, libraryItem.media.metadata.authorName, options)
if (results.length) {
if (libraryScan) libraryScan.addLog(LogLevel.DEBUG, `Found best cover for "${libraryItem.media.metadata.title}"`)
else Logger.debug(`[Scanner] Found best cover for "${libraryItem.media.metadata.title}"`)
@ -657,7 +657,7 @@ class Scanner {
for (let i = 0; i < results.length && i < 2; i++) {
// Downloads and updates the book cover
var result = await this.coverManager.downloadCoverFromUrl(libraryItem, results[i])
const result = await this.coverManager.downloadCoverFromUrl(libraryItem, results[i])
if (result.error) {
Logger.error(`[Scanner] Failed to download cover from url "${results[i]}" | Attempt ${i + 1}`, result.error)

View file

@ -8,7 +8,7 @@ const LibraryFile = require('../objects/files/LibraryFile')
function isMediaFile(mediaType, ext) {
if (!ext) return false
var extclean = ext.slice(1).toLowerCase()
if (mediaType === 'podcast') return globals.SupportedAudioTypes.includes(extclean)
if (mediaType === 'podcast' || mediaType === 'music') return globals.SupportedAudioTypes.includes(extclean)
else if (mediaType === 'video') return globals.SupportedVideoTypes.includes(extclean)
return globals.SupportedAudioTypes.includes(extclean) || globals.SupportedEbookTypes.includes(extclean)
}
@ -91,26 +91,39 @@ module.exports.groupFilesIntoLibraryItemPaths = groupFilesIntoLibraryItemPaths
// Input: array of relative file items (see recurseFiles)
// Output: map of files grouped into potential libarary item dirs
function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems) {
// Handle music where every audio file is a library item
if (mediaType === 'music') {
const audioFileGroup = {}
fileItems.filter(i => isMediaFile(mediaType, i.extension)).forEach((item) => {
if (!item.reldirpath) {
audioFileGroup[item.name] = item.name
} else {
audioFileGroup[item.reldirpath] = [item.name]
}
})
return audioFileGroup
}
// 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' || mediaType === 'video') && isMediaFile(mediaType, i.extension))
const itemsFiltered = fileItems.filter(i => {
return i.deep > 0 || ((mediaType === 'book' || mediaType === 'video' || mediaType === 'music') && isMediaFile(mediaType, i.extension))
})
// Step 2: Seperate media files and other files
// - Directories without a media file will not be included
var mediaFileItems = []
var otherFileItems = []
const mediaFileItems = []
const otherFileItems = []
itemsFiltered.forEach(item => {
if (isMediaFile(mediaType, item.extension)) mediaFileItems.push(item)
else otherFileItems.push(item)
})
// Step 3: Group audio files in library items
var libraryItemGroup = {}
const libraryItemGroup = {}
mediaFileItems.forEach((item) => {
var dirparts = item.reldirpath.split('/').filter(p => !!p)
var numparts = dirparts.length
var _path = ''
const dirparts = item.reldirpath.split('/').filter(p => !!p)
const numparts = dirparts.length
let _path = ''
if (!dirparts.length) {
// Media file in root
@ -118,11 +131,11 @@ function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems) {
} else {
// Iterate over directories in path
for (let i = 0; i < numparts; i++) {
var dirpart = dirparts.shift()
const dirpart = dirparts.shift()
_path = Path.posix.join(_path, dirpart)
if (libraryItemGroup[_path]) { // Directory already has files, add file
var relpath = Path.posix.join(dirparts.join('/'), item.name)
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
@ -138,16 +151,16 @@ function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems) {
// Step 4: Add other files into library item groups
otherFileItems.forEach((item) => {
var dirparts = item.reldirpath.split('/')
var numparts = dirparts.length
var _path = ''
const dirparts = item.reldirpath.split('/')
const numparts = dirparts.length
let _path = ''
// Iterate over directories in path
for (let i = 0; i < numparts; i++) {
var dirpart = dirparts.shift()
const dirpart = dirparts.shift()
_path = Path.posix.join(_path, dirpart)
if (libraryItemGroup[_path]) { // Directory is audiobook group
var relpath = Path.posix.join(dirparts.join('/'), item.name)
const relpath = Path.posix.join(dirparts.join('/'), item.name)
libraryItemGroup[_path].push(relpath)
return
}
@ -158,8 +171,8 @@ function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems) {
function cleanFileObjects(libraryItemPath, files) {
return Promise.all(files.map(async (file) => {
var filePath = Path.posix.join(libraryItemPath, file)
var newLibraryFile = new LibraryFile()
const filePath = Path.posix.join(libraryItemPath, file)
const newLibraryFile = new LibraryFile()
await newLibraryFile.setDataFromPath(filePath, file)
return newLibraryFile
}))
@ -167,27 +180,27 @@ function cleanFileObjects(libraryItemPath, files) {
// Scan folder
async function scanFolder(libraryMediaType, folder, serverSettings = {}) {
var folderPath = folder.fullPath.replace(/\\/g, '/')
const folderPath = folder.fullPath.replace(/\\/g, '/')
var pathExists = await fs.pathExists(folderPath)
const pathExists = await fs.pathExists(folderPath)
if (!pathExists) {
Logger.error(`[scandir] Invalid folder path does not exist "${folderPath}"`)
return []
}
var fileItems = await recurseFiles(folderPath)
var libraryItemGrouping = groupFileItemsIntoLibraryItemDirs(libraryMediaType, fileItems)
const fileItems = await recurseFiles(folderPath)
const libraryItemGrouping = groupFileItemsIntoLibraryItemDirs(libraryMediaType, fileItems)
if (!Object.keys(libraryItemGrouping).length) {
Logger.error(`Root path has no media folders: ${folderPath}`)
return []
}
var items = []
const items = []
for (const libraryItemPath in libraryItemGrouping) {
var isFile = false // item is not in a folder
var libraryItemData = null
var fileObjs = []
let isFile = false // item is not in a folder
let libraryItemData = null
let fileObjs = []
if (libraryItemPath === libraryItemGrouping[libraryItemPath]) {
// Media file in root only get title
libraryItemData = {
@ -200,11 +213,11 @@ async function scanFolder(libraryMediaType, folder, serverSettings = {}) {
fileObjs = await cleanFileObjects(folderPath, [libraryItemPath])
isFile = true
} else {
libraryItemData = getDataFromMediaDir(libraryMediaType, folderPath, libraryItemPath, serverSettings)
libraryItemData = getDataFromMediaDir(libraryMediaType, folderPath, libraryItemPath, serverSettings, libraryItemGrouping[libraryItemPath])
fileObjs = await cleanFileObjects(libraryItemData.path, libraryItemGrouping[libraryItemPath])
}
var libraryItemFolderStats = await getFileTimestampsWithIno(libraryItemData.path)
const libraryItemFolderStats = await getFileTimestampsWithIno(libraryItemData.path)
items.push({
folderId: folder.id,
libraryId: folder.libraryId,
@ -318,21 +331,36 @@ function getSubtitle(folder) {
function getPodcastDataFromDir(folderPath, relPath) {
relPath = relPath.replace(/\\/g, '/')
var splitDir = relPath.split('/')
const splitDir = relPath.split('/')
// Audio files will always be in the directory named for the title
var title = splitDir.pop()
const title = splitDir.pop()
return {
mediaMetadata: {
title
},
relPath: relPath, // relative audiobook path i.e. /Author Name/Book Name/..
path: Path.posix.join(folderPath, relPath) // i.e. /audiobook/Author Name/Book Name/..
relPath: relPath, // relative podcast path i.e. /Podcast Name/..
path: Path.posix.join(folderPath, relPath) // i.e. /podcasts/Podcast Name/..
}
}
function getDataFromMediaDir(libraryMediaType, folderPath, relPath, serverSettings) {
if (libraryMediaType === 'podcast') {
function getMusicDataFromDir(folderPath, relPath, fileNames) {
relPath = relPath.replace(/\\/g, '/')
const firstFileName = fileNames.length ? fileNames[0] : ''
return {
mediaMetadata: {
title: Path.basename(firstFileName, Path.extname(firstFileName))
},
relPath: relPath, // relative music audio file path i.e. /Some Folder/..
path: Path.posix.join(folderPath, relPath) // i.e. /music/Some Folder/..
}
}
function getDataFromMediaDir(libraryMediaType, folderPath, relPath, serverSettings, fileNames) {
if (libraryMediaType === 'music') {
return getMusicDataFromDir(folderPath, relPath, fileNames)
} else if (libraryMediaType === 'podcast') {
return getPodcastDataFromDir(folderPath, relPath)
} else if (libraryMediaType === 'book') {
var parseSubtitle = !!serverSettings.scannerParseSubtitle
@ -368,7 +396,8 @@ async function getLibraryItemFileData(libraryMediaType, folder, libraryItemPath,
}
} else {
fileItems = await recurseFiles(libraryItemPath)
libraryItemData = getDataFromMediaDir(libraryMediaType, folderFullPath, libraryItemDir, serverSettings)
const fileNames = fileItems.map(i => i.name)
libraryItemData = getDataFromMediaDir(libraryMediaType, folderFullPath, libraryItemDir, serverSettings, fileNames)
}
var libraryItemDirStats = await getFileTimestampsWithIno(libraryItemData.path)
@ -389,8 +418,8 @@ async function getLibraryItemFileData(libraryMediaType, folder, libraryItemPath,
}
for (let i = 0; i < fileItems.length; i++) {
var fileItem = fileItems[i]
var newLibraryFile = new LibraryFile()
const fileItem = fileItems[i]
const newLibraryFile = new LibraryFile()
// fileItem.path is the relative path
await newLibraryFile.setDataFromPath(fileItem.fullpath, fileItem.path)
libraryItem.libraryFiles.push(newLibraryFile)