Add Scanner support for podcasts

This commit is contained in:
advplyr 2022-03-26 14:29:49 -05:00
parent 86e7c7fc33
commit 5446aea910
8 changed files with 84 additions and 28 deletions

View File

@ -3,8 +3,8 @@ const fs = require('fs-extra')
const filePerms = require('../utils/filePerms') const filePerms = require('../utils/filePerms')
const Logger = require('../Logger') const Logger = require('../Logger')
const Library = require('../objects/Library') const Library = require('../objects/Library')
const { sort, createNewSortInstance } = require('fast-sort')
const libraryHelpers = require('../utils/libraryHelpers') const libraryHelpers = require('../utils/libraryHelpers')
const { sort, createNewSortInstance } = require('fast-sort')
const naturalSort = createNewSortInstance({ const naturalSort = createNewSortInstance({
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
}) })

View File

@ -226,7 +226,9 @@ class CoverManager {
} }
async saveEmbeddedCoverArt(libraryItem) { async saveEmbeddedCoverArt(libraryItem) {
var audioFileWithCover = libraryItem.media.audioFiles.find(af => af.embeddedCoverArt) var audioFileWithCover = null
if (libraryItem.mediaType === 'book') audioFileWithCover = libraryItem.media.audioFiles.find(af => af.embeddedCoverArt)
else audioFileWithCover = libraryItem.media.episodes.find(ep => ep.audioFile.embeddedCoverArt)
if (!audioFileWithCover) return false if (!audioFileWithCover) return false
var coverDirPath = this.getCoverDirectory(libraryItem) var coverDirPath = this.getCoverDirectory(libraryItem)

View File

@ -166,7 +166,6 @@ class LibraryItem {
} else { } else {
this.mediaType = 'book' this.mediaType = 'book'
this.media = new Book() this.media = new Book()
} }
@ -235,6 +234,7 @@ class LibraryItem {
saveMetadata() { } saveMetadata() { }
// Returns null if file not found, true if file was updated, false if up to date // Returns null if file not found, true if file was updated, false if up to date
// updates existing LibraryFile, AudioFile, EBookFile's
checkFileFound(fileFound) { checkFileFound(fileFound) {
var hasUpdated = false var hasUpdated = false
@ -270,8 +270,8 @@ class LibraryItem {
hasUpdated = true hasUpdated = true
} }
var keysToCheck = ['filename', 'ext', 'mtimeMs', 'ctimeMs', 'birthtimeMs', 'size'] // FileMetadata keys
keysToCheck.forEach((key) => { ['filename', 'ext', 'mtimeMs', 'ctimeMs', 'birthtimeMs', 'size'].forEach((key) => {
if (existingFile.metadata[key] !== fileFound.metadata[key]) { if (existingFile.metadata[key] !== fileFound.metadata[key]) {
// Add modified flag on file data object if exists and was changed // Add modified flag on file data object if exists and was changed
@ -319,8 +319,7 @@ class LibraryItem {
hasUpdated = true hasUpdated = true
} }
var keysToCheck = ['mtimeMs', 'ctimeMs', 'birthtimeMs'] ['mtimeMs', 'ctimeMs', 'birthtimeMs'].forEach((key) => {
keysToCheck.forEach((key) => {
if (dataFound[key] != this[key]) { if (dataFound[key] != this[key]) {
this[key] = dataFound[key] || 0 this[key] = dataFound[key] || 0
hasUpdated = true hasUpdated = true
@ -347,6 +346,7 @@ class LibraryItem {
// Remove files not found (inodes will all be up to date at this point) // Remove files not found (inodes will all be up to date at this point)
this.libraryFiles = this.libraryFiles.filter(lf => { this.libraryFiles = this.libraryFiles.filter(lf => {
if (!dataFound.libraryFiles.find(_lf => _lf.ino === lf.ino)) { if (!dataFound.libraryFiles.find(_lf => _lf.ino === lf.ino)) {
// Check if removing cover path
if (lf.metadata.path === this.media.coverPath) { if (lf.metadata.path === this.media.coverPath) {
Logger.debug(`[LibraryItem] "${this.media.metadata.title}" check scan cover removed`) Logger.debug(`[LibraryItem] "${this.media.metadata.title}" check scan cover removed`)
this.media.updateCover('') this.media.updateCover('')
@ -395,10 +395,6 @@ class LibraryItem {
} }
} }
findLibraryFileWithIno(inode) {
return this.libraryFiles.find(lf => lf.ino === inode)
}
// Set metadata from files // Set metadata from files
async syncFiles(preferOpfMetadata) { async syncFiles(preferOpfMetadata) {
var hasUpdated = false var hasUpdated = false

View File

@ -86,6 +86,15 @@ class PodcastEpisode {
this.updatedAt = Date.now() this.updatedAt = Date.now()
} }
setDataFromAudioFile(audioFile, index) {
this.id = getId('ep')
this.audioFile = audioFile
this.title = audioFile.metadata.filename
this.index = index
this.addedAt = Date.now()
this.updatedAt = Date.now()
}
// Only checks container format // Only checks container format
checkCanDirectPlay(payload) { checkCanDirectPlay(payload) {
var supportedMimeTypes = payload.supportedMimeTypes || [] var supportedMimeTypes = payload.supportedMimeTypes || []

View File

@ -1,6 +1,10 @@
const PodcastEpisode = require('../entities/PodcastEpisode') const PodcastEpisode = require('../entities/PodcastEpisode')
const PodcastMetadata = require('../metadata/PodcastMetadata') const PodcastMetadata = require('../metadata/PodcastMetadata')
const { areEquivalent, copyValue } = require('../../utils/index') const { areEquivalent, copyValue } = require('../../utils/index')
const { createNewSortInstance } = require('fast-sort')
const naturalSort = createNewSortInstance({
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
})
class Podcast { class Podcast {
constructor(podcast) { constructor(podcast) {
@ -69,7 +73,7 @@ class Podcast {
return false return false
} }
get hasEmbeddedCoverArt() { get hasEmbeddedCoverArt() {
return false return this.episodes.some(ep => ep.audioFile.embeddedCoverArt)
} }
get hasIssues() { get hasIssues() {
return false return false
@ -111,11 +115,11 @@ class Podcast {
} }
removeFileWithInode(inode) { removeFileWithInode(inode) {
return false this.episodes = this.episodes.filter(ep => ep.ino !== inode)
} }
findFileWithInode(inode) { findFileWithInode(inode) {
return null return this.episodes.find(ep => ep.audioFile.ino === inode)
} }
setData(mediaMetadata) { setData(mediaMetadata) {
@ -137,10 +141,6 @@ class Podcast {
return payload || {} return payload || {}
} }
addPodcastEpisode(podcastEpisode) {
this.episodes.push(podcastEpisode)
}
// Only checks container format // Only checks container format
checkCanDirectPlay(payload, epsiodeIndex = 0) { checkCanDirectPlay(payload, epsiodeIndex = 0) {
var episode = this.episodes[epsiodeIndex] var episode = this.episodes[epsiodeIndex]
@ -151,5 +151,27 @@ class Podcast {
var episode = this.episodes[episodeIndex] var episode = this.episodes[episodeIndex]
return episode.getDirectPlayTracklist(libraryItemId) return episode.getDirectPlayTracklist(libraryItemId)
} }
addPodcastEpisode(podcastEpisode) {
this.episodes.push(podcastEpisode)
}
addNewEpisodeFromAudioFile(audioFile, index) {
var pe = new PodcastEpisode()
pe.setDataFromAudioFile(audioFile, index)
this.episodes.push(pe)
}
reorderEpisodes() {
var hasUpdates = false
this.episodes = naturalSort(this.episodes).asc((ep) => ep.bestFilename)
for (let i = 0; i < this.episodes.length; i++) {
if (this.episodes[i].index !== (i + 1)) {
this.episodes[i].index = i + 1
hasUpdates = true
}
}
return hasUpdates
}
} }
module.exports = Podcast module.exports = Podcast

View File

@ -80,7 +80,7 @@ class AudioFileScanner {
// Returns array of { AudioFile, elapsed, averageScanDuration } from audio file scan objects // Returns array of { AudioFile, elapsed, averageScanDuration } from audio file scan objects
async executeAudioFileScans(mediaType, audioLibraryFiles, scanData) { async executeAudioFileScans(mediaType, audioLibraryFiles, scanData) {
var mediaMetadataFromScan = scanData.mediaMetadata || null var mediaMetadataFromScan = scanData.media.metadata || null
var proms = [] var proms = []
for (let i = 0; i < audioLibraryFiles.length; i++) { for (let i = 0; i < audioLibraryFiles.length; i++) {
proms.push(this.scan(mediaType, audioLibraryFiles[i], mediaMetadataFromScan)) proms.push(this.scan(mediaType, audioLibraryFiles[i], mediaMetadataFromScan))
@ -150,7 +150,6 @@ class AudioFileScanner {
trackKey = 'trackNumFromMeta' trackKey = 'trackNumFromMeta'
} }
if (discKey !== null) { if (discKey !== null) {
Logger.debug(`[AudioFileScanner] Smart track order for "${libraryItem.media.metadata.title}" using disc key ${discKey} and track key ${trackKey}`) Logger.debug(`[AudioFileScanner] Smart track order for "${libraryItem.media.metadata.title}" using disc key ${discKey} and track key ${trackKey}`)
audioFiles.sort((a, b) => { audioFiles.sort((a, b) => {
@ -222,8 +221,28 @@ class AudioFileScanner {
if (hasUpdated) { if (hasUpdated) {
libraryItem.media.rebuildTracks() libraryItem.media.rebuildTracks()
} }
} // End Book media type } else { // Podcast Media Type
var existingAudioFiles = audioScanResult.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 return hasUpdated
} }
} }

View File

@ -172,7 +172,7 @@ class Scanner {
if (this.cancelLibraryScan[libraryScan.libraryId]) return true if (this.cancelLibraryScan[libraryScan.libraryId]) return true
// Remove audiobooks with no inode // Remove items with no inode
libraryItemDataFound = libraryItemDataFound.filter(lid => lid.ino) libraryItemDataFound = libraryItemDataFound.filter(lid => lid.ino)
var libraryItemsInLibrary = this.db.libraryItems.filter(li => li.libraryId === libraryScan.libraryId) var libraryItemsInLibrary = this.db.libraryItems.filter(li => li.libraryId === libraryScan.libraryId)

View File

@ -5,11 +5,12 @@ const { recurseFiles, getFileTimestampsWithIno } = require('./fileUtils')
const globals = require('./globals') const globals = require('./globals')
const LibraryFile = require('../objects/files/LibraryFile') const LibraryFile = require('../objects/files/LibraryFile')
function isMediaFile(path) { function isMediaFile(mediaType, path) {
if (!path) return false if (!path) return false
var ext = Path.extname(path) var ext = Path.extname(path)
if (!ext) return false if (!ext) return false
var extclean = ext.slice(1).toLowerCase() var extclean = ext.slice(1).toLowerCase()
if (mediaType === 'podcast') return globals.SupportedAudioTypes.includes(extclean)
return globals.SupportedAudioTypes.includes(extclean) || globals.SupportedEbookTypes.includes(extclean) return globals.SupportedAudioTypes.includes(extclean) || globals.SupportedEbookTypes.includes(extclean)
} }
@ -60,7 +61,7 @@ module.exports.groupFilesIntoLibraryItemPaths = groupFilesIntoLibraryItemPaths
// Input: array of relative file items (see recurseFiles) // Input: array of relative file items (see recurseFiles)
// Output: map of files grouped into potential libarary item dirs // Output: map of files grouped into potential libarary item dirs
function groupFileItemsIntoLibraryItemDirs(fileItems) { function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems) {
// Step 1: Filter out files in root dir (with depth of 0) // Step 1: Filter out files in root dir (with depth of 0)
var itemsFiltered = fileItems.filter(i => i.deep > 0) var itemsFiltered = fileItems.filter(i => i.deep > 0)
@ -69,7 +70,7 @@ function groupFileItemsIntoLibraryItemDirs(fileItems) {
var mediaFileItems = [] var mediaFileItems = []
var otherFileItems = [] var otherFileItems = []
itemsFiltered.forEach(item => { itemsFiltered.forEach(item => {
if (isMediaFile(item.fullpath)) mediaFileItems.push(item) if (isMediaFile(mediaType, item.fullpath)) mediaFileItems.push(item)
else otherFileItems.push(item) else otherFileItems.push(item)
}) })
@ -141,7 +142,7 @@ async function scanFolder(libraryMediaType, folder, serverSettings = {}) {
var fileItems = await recurseFiles(folderPath) var fileItems = await recurseFiles(folderPath)
var libraryItemGrouping = groupFileItemsIntoLibraryItemDirs(fileItems) var libraryItemGrouping = groupFileItemsIntoLibraryItemDirs(libraryMediaType, fileItems)
if (!Object.keys(libraryItemGrouping).length) { if (!Object.keys(libraryItemGrouping).length) {
Logger.error('Root path has no media folders', fileItems.length) Logger.error('Root path has no media folders', fileItems.length)
@ -268,17 +269,24 @@ function getBookDataFromDir(folderPath, relPath, parseSubtitle = false) {
function getPodcastDataFromDir(folderPath, relPath) { function getPodcastDataFromDir(folderPath, relPath) {
relPath = relPath.replace(/\\/g, '/') relPath = relPath.replace(/\\/g, '/')
var splitDir = relPath.split('/')
// Audio files will always be in the directory named for the title
var title = splitDir.pop()
return { return {
mediaMetadata: {
title
},
relPath: relPath, // relative audiobook path i.e. /Author Name/Book Name/.. relPath: relPath, // relative audiobook path i.e. /Author Name/Book Name/..
path: Path.posix.join(folderPath, relPath) // i.e. /audiobook/Author Name/Book Name/.. path: Path.posix.join(folderPath, relPath) // i.e. /audiobook/Author Name/Book Name/..
} }
} }
function getDataFromMediaDir(libraryMediaType, folderPath, relPath, serverSettings) { function getDataFromMediaDir(libraryMediaType, folderPath, relPath, serverSettings) {
var parseSubtitle = !!serverSettings.scannerParseSubtitle
if (libraryMediaType === 'podcast') { if (libraryMediaType === 'podcast') {
return getPodcastDataFromDir(folderPath, relPath, parseSubtitle) return getPodcastDataFromDir(folderPath, relPath)
} else { } else {
var parseSubtitle = !!serverSettings.scannerParseSubtitle
return getBookDataFromDir(folderPath, relPath, parseSubtitle) return getBookDataFromDir(folderPath, relPath, parseSubtitle)
} }
} }