mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-01-03 00:06:46 +01:00
Add Scanner support for podcasts
This commit is contained in:
parent
86e7c7fc33
commit
5446aea910
@ -3,8 +3,8 @@ const fs = require('fs-extra')
|
||||
const filePerms = require('../utils/filePerms')
|
||||
const Logger = require('../Logger')
|
||||
const Library = require('../objects/Library')
|
||||
const { sort, createNewSortInstance } = require('fast-sort')
|
||||
const libraryHelpers = require('../utils/libraryHelpers')
|
||||
const { sort, createNewSortInstance } = require('fast-sort')
|
||||
const naturalSort = createNewSortInstance({
|
||||
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
|
||||
})
|
||||
|
@ -226,7 +226,9 @@ class CoverManager {
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
var coverDirPath = this.getCoverDirectory(libraryItem)
|
||||
|
@ -166,7 +166,6 @@ class LibraryItem {
|
||||
} else {
|
||||
this.mediaType = 'book'
|
||||
this.media = new Book()
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -235,6 +234,7 @@ class LibraryItem {
|
||||
saveMetadata() { }
|
||||
|
||||
// Returns null if file not found, true if file was updated, false if up to date
|
||||
// updates existing LibraryFile, AudioFile, EBookFile's
|
||||
checkFileFound(fileFound) {
|
||||
var hasUpdated = false
|
||||
|
||||
@ -270,8 +270,8 @@ class LibraryItem {
|
||||
hasUpdated = true
|
||||
}
|
||||
|
||||
var keysToCheck = ['filename', 'ext', 'mtimeMs', 'ctimeMs', 'birthtimeMs', 'size']
|
||||
keysToCheck.forEach((key) => {
|
||||
// FileMetadata keys
|
||||
['filename', 'ext', 'mtimeMs', 'ctimeMs', 'birthtimeMs', 'size'].forEach((key) => {
|
||||
if (existingFile.metadata[key] !== fileFound.metadata[key]) {
|
||||
|
||||
// Add modified flag on file data object if exists and was changed
|
||||
@ -319,8 +319,7 @@ class LibraryItem {
|
||||
hasUpdated = true
|
||||
}
|
||||
|
||||
var keysToCheck = ['mtimeMs', 'ctimeMs', 'birthtimeMs']
|
||||
keysToCheck.forEach((key) => {
|
||||
['mtimeMs', 'ctimeMs', 'birthtimeMs'].forEach((key) => {
|
||||
if (dataFound[key] != this[key]) {
|
||||
this[key] = dataFound[key] || 0
|
||||
hasUpdated = true
|
||||
@ -347,6 +346,7 @@ class LibraryItem {
|
||||
// Remove files not found (inodes will all be up to date at this point)
|
||||
this.libraryFiles = this.libraryFiles.filter(lf => {
|
||||
if (!dataFound.libraryFiles.find(_lf => _lf.ino === lf.ino)) {
|
||||
// Check if removing cover path
|
||||
if (lf.metadata.path === this.media.coverPath) {
|
||||
Logger.debug(`[LibraryItem] "${this.media.metadata.title}" check scan cover removed`)
|
||||
this.media.updateCover('')
|
||||
@ -395,10 +395,6 @@ class LibraryItem {
|
||||
}
|
||||
}
|
||||
|
||||
findLibraryFileWithIno(inode) {
|
||||
return this.libraryFiles.find(lf => lf.ino === inode)
|
||||
}
|
||||
|
||||
// Set metadata from files
|
||||
async syncFiles(preferOpfMetadata) {
|
||||
var hasUpdated = false
|
||||
|
@ -86,6 +86,15 @@ class PodcastEpisode {
|
||||
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
|
||||
checkCanDirectPlay(payload) {
|
||||
var supportedMimeTypes = payload.supportedMimeTypes || []
|
||||
|
@ -1,6 +1,10 @@
|
||||
const PodcastEpisode = require('../entities/PodcastEpisode')
|
||||
const PodcastMetadata = require('../metadata/PodcastMetadata')
|
||||
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 {
|
||||
constructor(podcast) {
|
||||
@ -69,7 +73,7 @@ class Podcast {
|
||||
return false
|
||||
}
|
||||
get hasEmbeddedCoverArt() {
|
||||
return false
|
||||
return this.episodes.some(ep => ep.audioFile.embeddedCoverArt)
|
||||
}
|
||||
get hasIssues() {
|
||||
return false
|
||||
@ -111,11 +115,11 @@ class Podcast {
|
||||
}
|
||||
|
||||
removeFileWithInode(inode) {
|
||||
return false
|
||||
this.episodes = this.episodes.filter(ep => ep.ino !== inode)
|
||||
}
|
||||
|
||||
findFileWithInode(inode) {
|
||||
return null
|
||||
return this.episodes.find(ep => ep.audioFile.ino === inode)
|
||||
}
|
||||
|
||||
setData(mediaMetadata) {
|
||||
@ -137,10 +141,6 @@ class Podcast {
|
||||
return payload || {}
|
||||
}
|
||||
|
||||
addPodcastEpisode(podcastEpisode) {
|
||||
this.episodes.push(podcastEpisode)
|
||||
}
|
||||
|
||||
// Only checks container format
|
||||
checkCanDirectPlay(payload, epsiodeIndex = 0) {
|
||||
var episode = this.episodes[epsiodeIndex]
|
||||
@ -151,5 +151,27 @@ class Podcast {
|
||||
var episode = this.episodes[episodeIndex]
|
||||
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
|
@ -80,7 +80,7 @@ class AudioFileScanner {
|
||||
|
||||
// Returns array of { AudioFile, elapsed, averageScanDuration } from audio file scan objects
|
||||
async executeAudioFileScans(mediaType, audioLibraryFiles, scanData) {
|
||||
var mediaMetadataFromScan = scanData.mediaMetadata || null
|
||||
var mediaMetadataFromScan = scanData.media.metadata || null
|
||||
var proms = []
|
||||
for (let i = 0; i < audioLibraryFiles.length; i++) {
|
||||
proms.push(this.scan(mediaType, audioLibraryFiles[i], mediaMetadataFromScan))
|
||||
@ -150,7 +150,6 @@ class AudioFileScanner {
|
||||
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) => {
|
||||
@ -222,8 +221,28 @@ class AudioFileScanner {
|
||||
if (hasUpdated) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -172,7 +172,7 @@ class Scanner {
|
||||
|
||||
if (this.cancelLibraryScan[libraryScan.libraryId]) return true
|
||||
|
||||
// Remove audiobooks with no inode
|
||||
// Remove items with no inode
|
||||
libraryItemDataFound = libraryItemDataFound.filter(lid => lid.ino)
|
||||
var libraryItemsInLibrary = this.db.libraryItems.filter(li => li.libraryId === libraryScan.libraryId)
|
||||
|
||||
|
@ -5,11 +5,12 @@ const { recurseFiles, getFileTimestampsWithIno } = require('./fileUtils')
|
||||
const globals = require('./globals')
|
||||
const LibraryFile = require('../objects/files/LibraryFile')
|
||||
|
||||
function isMediaFile(path) {
|
||||
function isMediaFile(mediaType, path) {
|
||||
if (!path) return false
|
||||
var ext = Path.extname(path)
|
||||
if (!ext) return false
|
||||
var extclean = ext.slice(1).toLowerCase()
|
||||
if (mediaType === 'podcast') return globals.SupportedAudioTypes.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)
|
||||
// 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)
|
||||
var itemsFiltered = fileItems.filter(i => i.deep > 0)
|
||||
|
||||
@ -69,7 +70,7 @@ function groupFileItemsIntoLibraryItemDirs(fileItems) {
|
||||
var mediaFileItems = []
|
||||
var otherFileItems = []
|
||||
itemsFiltered.forEach(item => {
|
||||
if (isMediaFile(item.fullpath)) mediaFileItems.push(item)
|
||||
if (isMediaFile(mediaType, item.fullpath)) mediaFileItems.push(item)
|
||||
else otherFileItems.push(item)
|
||||
})
|
||||
|
||||
@ -141,7 +142,7 @@ async function scanFolder(libraryMediaType, folder, serverSettings = {}) {
|
||||
|
||||
var fileItems = await recurseFiles(folderPath)
|
||||
|
||||
var libraryItemGrouping = groupFileItemsIntoLibraryItemDirs(fileItems)
|
||||
var libraryItemGrouping = groupFileItemsIntoLibraryItemDirs(libraryMediaType, fileItems)
|
||||
|
||||
if (!Object.keys(libraryItemGrouping).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) {
|
||||
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 {
|
||||
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/..
|
||||
}
|
||||
}
|
||||
|
||||
function getDataFromMediaDir(libraryMediaType, folderPath, relPath, serverSettings) {
|
||||
var parseSubtitle = !!serverSettings.scannerParseSubtitle
|
||||
if (libraryMediaType === 'podcast') {
|
||||
return getPodcastDataFromDir(folderPath, relPath, parseSubtitle)
|
||||
return getPodcastDataFromDir(folderPath, relPath)
|
||||
} else {
|
||||
var parseSubtitle = !!serverSettings.scannerParseSubtitle
|
||||
return getBookDataFromDir(folderPath, relPath, parseSubtitle)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user