mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-01-17 00:08:55 +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 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
|
||||||
})
|
})
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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 || []
|
||||||
|
@ -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
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user