Add:Support for openaudible folder structure (subject to change), add support for treating single audio files in the root directory as library items #401

This commit is contained in:
advplyr 2022-04-27 19:42:34 -05:00
parent 49bef2c641
commit 33dfb764fa
14 changed files with 110 additions and 55 deletions

View File

@ -150,6 +150,10 @@ export default {
_libraryItem() { _libraryItem() {
return this.libraryItem || {} return this.libraryItem || {}
}, },
isFile() {
// Library item is not in a folder
return this._libraryItem.isFile
},
media() { media() {
return this._libraryItem.media || {} return this._libraryItem.media || {}
}, },
@ -365,7 +369,7 @@ export default {
text: 'Match' text: 'Match'
}) })
} }
if (this.userIsRoot) { if (this.userIsRoot && !this.isFile) {
items.push({ items.push({
func: 'rescan', func: 'rescan',
text: 'Re-Scan' text: 'Re-Scan'

View File

@ -14,7 +14,7 @@
</ui-tooltip> </ui-tooltip>
<ui-tooltip :disabled="!!libraryScan" text="(Root User Only) Rescan audiobook including metadata" direction="bottom" class="mr-4"> <ui-tooltip :disabled="!!libraryScan" text="(Root User Only) Rescan audiobook including metadata" direction="bottom" class="mr-4">
<ui-btn v-if="isRootUser" :loading="rescanning" :disabled="!!libraryScan" color="bg" type="button" class="h-full" small @click.stop.prevent="rescan">Re-Scan</ui-btn> <ui-btn v-if="isRootUser && !isFile" :loading="rescanning" :disabled="!!libraryScan" color="bg" type="button" class="h-full" small @click.stop.prevent="rescan">Re-Scan</ui-btn>
</ui-tooltip> </ui-tooltip>
<ui-btn @click="submitForm">Submit</ui-btn> <ui-btn @click="submitForm">Submit</ui-btn>
@ -49,6 +49,9 @@ export default {
this.$emit('update:processing', val) this.$emit('update:processing', val)
} }
}, },
isFile() {
return !!this.libraryItem && this.libraryItem.isFile
},
isRootUser() { isRootUser() {
return this.$store.getters['user/getIsRoot'] return this.$store.getters['user/getIsRoot']
}, },

View File

@ -1,9 +1,5 @@
<template> <template>
<div class="w-full h-full overflow-y-auto overflow-x-hidden px-4 py-6"> <div class="w-full h-full overflow-y-auto overflow-x-hidden px-4 py-6">
<template v-for="audiobook in audiobooks">
<tables-tracks-table :key="audiobook.id" :title="`Audiobook Tracks (${audiobook.name})`" :audiobook-id="audiobook.id" :tracks="audiobook.tracks" class="mb-4" />
</template>
<tables-library-files-table expanded :files="libraryFiles" :library-item-id="libraryItem.id" :is-missing="isMissing" /> <tables-library-files-table expanded :files="libraryFiles" :library-item-id="libraryItem.id" :is-missing="isMissing" />
</div> </div>
</template> </template>
@ -51,12 +47,6 @@ export default {
}, },
showDownload() { showDownload() {
return this.userCanDownload && !this.isMissing return this.userCanDownload && !this.isMissing
},
audiobooks() {
return this.media.audiobooks || []
},
ebooks() {
return this.media.ebooks || []
} }
}, },
methods: { methods: {

View File

@ -8,7 +8,7 @@
<!-- <span class="bg-black-400 rounded-xl py-1 px-2 text-sm font-mono">{{ tracks.length }}</span> --> <!-- <span class="bg-black-400 rounded-xl py-1 px-2 text-sm font-mono">{{ tracks.length }}</span> -->
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="showFullPath = !showFullPath">Full Path</ui-btn> <ui-btn small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="showFullPath = !showFullPath">Full Path</ui-btn>
<nuxt-link v-if="userCanUpdate" :to="`/audiobook/${libraryItemId}/edit`" class="mr-2 md:mr-4" @mousedown.prevent> <nuxt-link v-if="userCanUpdate && !isFile" :to="`/audiobook/${libraryItemId}/edit`" class="mr-2 md:mr-4" @mousedown.prevent>
<ui-btn small color="primary">Manage Tracks</ui-btn> <ui-btn small color="primary">Manage Tracks</ui-btn>
</nuxt-link> </nuxt-link>
<div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showTracks ? 'transform rotate-180' : ''"> <div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showTracks ? 'transform rotate-180' : ''">
@ -59,7 +59,8 @@ export default {
type: Array, type: Array,
default: () => [] default: () => []
}, },
libraryItemId: String libraryItemId: String,
isFile: Boolean
}, },
data() { data() {
return { return {

View File

@ -16,7 +16,7 @@
</div> </div>
</div> </div>
<tables-tracks-table :title="`Audiobook Tracks`" :tracks="media.tracks" :library-item-id="libraryItemId" class="mt-6" /> <tables-tracks-table :title="`Audiobook Tracks`" :tracks="media.tracks" :is-file="isFile" :library-item-id="libraryItemId" class="mt-6" />
</div> </div>
</template> </template>
@ -27,7 +27,8 @@ export default {
media: { media: {
type: Object, type: Object,
default: () => {} default: () => {}
} },
isFile: Boolean
}, },
data() { data() {
return {} return {}

View File

@ -107,6 +107,10 @@ export default {
console.error('Invalid media type') console.error('Invalid media type')
return redirect('/') return redirect('/')
} }
if (libraryItem.isFile) {
console.error('No need to edit library item that is 1 file...')
return redirect('/')
}
return { return {
libraryItem, libraryItem,
files: libraryItem.media.audioFiles ? libraryItem.media.audioFiles.map((af) => ({ ...af, include: !af.exclude })) : [] files: libraryItem.media.audioFiles ? libraryItem.media.audioFiles.map((af) => ({ ...af, include: !af.exclude })) : []

View File

@ -165,7 +165,7 @@
<p v-for="audioFile in invalidAudioFiles" :key="audioFile.id" class="text-xs pl-2">- {{ audioFile.metadata.filename }} ({{ audioFile.error }})</p> <p v-for="audioFile in invalidAudioFiles" :key="audioFile.id" class="text-xs pl-2">- {{ audioFile.metadata.filename }} ({{ audioFile.error }})</p>
</div> </div>
<widgets-audiobook-data v-if="tracks.length" :library-item-id="libraryItemId" :media="media" /> <widgets-audiobook-data v-if="tracks.length" :library-item-id="libraryItemId" :is-file="isFile" :media="media" />
<tables-podcast-episodes-table v-if="isPodcast" :library-item="libraryItem" /> <tables-podcast-episodes-table v-if="isPodcast" :library-item="libraryItem" />
@ -210,6 +210,9 @@ export default {
} }
}, },
computed: { computed: {
isFile() {
return this.libraryItem.isFile
},
coverAspectRatio() { coverAspectRatio() {
return this.$store.getters['getServerSetting']('coverAspectRatio') return this.$store.getters['getServerSetting']('coverAspectRatio')
}, },

View File

@ -411,7 +411,9 @@ class Db {
removeEntity(entityName, entityId) { removeEntity(entityName, entityId) {
var entityDb = this.getEntityDb(entityName) var entityDb = this.getEntityDb(entityName)
return entityDb.delete((record) => record.id === entityId).then((results) => { return entityDb.delete((record) => {
return record.id === entityId
}).then((results) => {
Logger.debug(`[DB] Deleted entity ${entityName}: ${results.deleted}`) Logger.debug(`[DB] Deleted entity ${entityName}: ${results.deleted}`)
var arrayKey = this.getEntityArrayKey(entityName) var arrayKey = this.getEntityArrayKey(entityName)
if (this[arrayKey]) { if (this[arrayKey]) {

View File

@ -342,6 +342,12 @@ class LibraryItemController {
Logger.error(`[LibraryItemController] Non-root user attempted to scan library item`, req.user) Logger.error(`[LibraryItemController] Non-root user attempted to scan library item`, req.user)
return res.sendStatus(403) return res.sendStatus(403)
} }
if (req.libraryItem.isFile) {
Logger.error(`[LibraryItemController] Re-scanning file library items not yet supported`)
return res.sendStatus(500)
}
var result = await this.scanner.scanLibraryItemById(req.libraryItem.id) var result = await this.scanner.scanLibraryItemById(req.libraryItem.id)
res.json({ res.json({
result: Object.keys(ScanResult).find(key => ScanResult[key] == result) result: Object.keys(ScanResult).find(key => ScanResult[key] == result)

View File

@ -19,7 +19,7 @@ class CoverManager {
} }
getCoverDirectory(libraryItem) { getCoverDirectory(libraryItem) {
if (this.db.serverSettings.storeCoverWithItem) { if (this.db.serverSettings.storeCoverWithItem && !libraryItem.isFile) {
return libraryItem.path return libraryItem.path
} else { } else {
return Path.posix.join(this.ItemMetadataPath, libraryItem.id) return Path.posix.join(this.ItemMetadataPath, libraryItem.id)

View File

@ -18,6 +18,7 @@ class LibraryItem {
this.path = null this.path = null
this.relPath = null this.relPath = null
this.isFile = false
this.mtimeMs = null this.mtimeMs = null
this.ctimeMs = null this.ctimeMs = null
this.birthtimeMs = null this.birthtimeMs = null
@ -51,6 +52,7 @@ class LibraryItem {
this.folderId = libraryItem.folderId this.folderId = libraryItem.folderId
this.path = libraryItem.path this.path = libraryItem.path
this.relPath = libraryItem.relPath this.relPath = libraryItem.relPath
this.isFile = !!libraryItem.isFile
this.mtimeMs = libraryItem.mtimeMs || 0 this.mtimeMs = libraryItem.mtimeMs || 0
this.ctimeMs = libraryItem.ctimeMs || 0 this.ctimeMs = libraryItem.ctimeMs || 0
this.birthtimeMs = libraryItem.birthtimeMs || 0 this.birthtimeMs = libraryItem.birthtimeMs || 0
@ -82,6 +84,7 @@ class LibraryItem {
folderId: this.folderId, folderId: this.folderId,
path: this.path, path: this.path,
relPath: this.relPath, relPath: this.relPath,
isFile: this.isFile,
mtimeMs: this.mtimeMs, mtimeMs: this.mtimeMs,
ctimeMs: this.ctimeMs, ctimeMs: this.ctimeMs,
birthtimeMs: this.birthtimeMs, birthtimeMs: this.birthtimeMs,
@ -105,6 +108,7 @@ class LibraryItem {
folderId: this.folderId, folderId: this.folderId,
path: this.path, path: this.path,
relPath: this.relPath, relPath: this.relPath,
isFile: this.isFile,
mtimeMs: this.mtimeMs, mtimeMs: this.mtimeMs,
ctimeMs: this.ctimeMs, ctimeMs: this.ctimeMs,
birthtimeMs: this.birthtimeMs, birthtimeMs: this.birthtimeMs,
@ -128,6 +132,7 @@ class LibraryItem {
folderId: this.folderId, folderId: this.folderId,
path: this.path, path: this.path,
relPath: this.relPath, relPath: this.relPath,
isFile: this.isFile,
mtimeMs: this.mtimeMs, mtimeMs: this.mtimeMs,
ctimeMs: this.ctimeMs, ctimeMs: this.ctimeMs,
birthtimeMs: this.birthtimeMs, birthtimeMs: this.birthtimeMs,
@ -460,7 +465,7 @@ class LibraryItem {
this.isSavingMetadata = true this.isSavingMetadata = true
var metadataPath = Path.join(global.MetadataPath, 'items', this.id) var metadataPath = Path.join(global.MetadataPath, 'items', this.id)
if (global.ServerSettings.storeMetadataWithItem) { if (global.ServerSettings.storeMetadataWithItem && !this.isFile) {
metadataPath = this.path metadataPath = this.path
} else { } else {
// Make sure metadata book dir exists // Make sure metadata book dir exists

View File

@ -235,7 +235,7 @@ class Scanner {
var hasMediaFile = dataFound.libraryFiles.some(lf => lf.isMediaFile) var hasMediaFile = dataFound.libraryFiles.some(lf => lf.isMediaFile)
if (!hasMediaFile) { if (!hasMediaFile) {
libraryScan.addLog(LogLevel.WARN, `Directory found "${libraryItemDataFound.path}" has no media files`) libraryScan.addLog(LogLevel.WARN, `Item found "${libraryItemDataFound.path}" has no media files`)
} else { } else {
var audioFileSize = 0 var audioFileSize = 0
dataFound.libraryFiles.filter(lf => lf.fileType == 'audio').forEach(lf => audioFileSize += lf.metadata.size) dataFound.libraryFiles.filter(lf => lf.fileType == 'audio').forEach(lf => audioFileSize += lf.metadata.size)

View File

@ -115,6 +115,7 @@ async function recurseFiles(path, relPathToReplace = null) {
var relpath = item.fullname.replace(relPathToReplace, '') var relpath = item.fullname.replace(relPathToReplace, '')
var reldirname = Path.dirname(relpath) var reldirname = Path.dirname(relpath)
if (reldirname === '.') reldirname = ''
var dirname = Path.dirname(item.fullname) var dirname = Path.dirname(item.fullname)
// Directory has a file named ".ignore" flag directory and ignore // Directory has a file named ".ignore" flag directory and ignore
@ -139,15 +140,18 @@ async function recurseFiles(path, relPathToReplace = null) {
return false return false
} }
return true return true
}).map((item) => ({ }).map((item) => {
name: item.name, var isInRoot = (item.path + '/' === relPathToReplace)
path: item.fullname.replace(relPathToReplace, ''), return {
dirpath: item.path, name: item.name,
reldirpath: item.path.replace(relPathToReplace, ''), path: item.fullname.replace(relPathToReplace, ''),
fullpath: item.fullname, dirpath: item.path,
extension: item.extension, reldirpath: isInRoot ? '' : item.path.replace(relPathToReplace, ''),
deep: item.deep fullpath: item.fullname,
})) extension: item.extension,
deep: item.deep
}
})
// Sort from least deep to most // Sort from least deep to most
list.sort((a, b) => a.deep - b.deep) list.sort((a, b) => a.deep - b.deep)

View File

@ -5,9 +5,9 @@ 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(mediaType, path) { function isMediaFile(mediaType, ext) {
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) if (mediaType === 'podcast') return globals.SupportedAudioTypes.includes(extclean)
@ -62,40 +62,47 @@ 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(mediaType, fileItems) { function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems) {
// Step 1: Filter out files in root dir (with depth of 0) // Step 1: Filter out non-media files in root dir (with depth of 0)
var itemsFiltered = fileItems.filter(i => i.deep > 0) var itemsFiltered = fileItems.filter(i => {
return i.deep > 0 || isMediaFile(mediaType, i.extension)
})
// Step 2: Seperate media files and other files // Step 2: Seperate media files and other files
// - Directories without a media file will not be included // - Directories without a media file will not be included
var mediaFileItems = [] var mediaFileItems = []
var otherFileItems = [] var otherFileItems = []
itemsFiltered.forEach(item => { itemsFiltered.forEach(item => {
if (isMediaFile(mediaType, item.fullpath)) mediaFileItems.push(item) if (isMediaFile(mediaType, item.extension)) mediaFileItems.push(item)
else otherFileItems.push(item) else otherFileItems.push(item)
}) })
// Step 3: Group audio files in library items // Step 3: Group audio files in library items
var libraryItemGroup = {} var libraryItemGroup = {}
mediaFileItems.forEach((item) => { mediaFileItems.forEach((item) => {
var dirparts = item.reldirpath.split('/') var dirparts = item.reldirpath.split('/').filter(p => !!p)
var numparts = dirparts.length var numparts = dirparts.length
var _path = '' var _path = ''
// Iterate over directories in path if (!dirparts.length) {
for (let i = 0; i < numparts; i++) { // Media file in root
var dirpart = dirparts.shift() libraryItemGroup[item.name] = item.name
_path = Path.posix.join(_path, dirpart) } else {
// Iterate over directories in path
for (let i = 0; i < numparts; i++) {
var dirpart = dirparts.shift()
_path = Path.posix.join(_path, dirpart)
if (libraryItemGroup[_path]) { // Directory already has files, add file if (libraryItemGroup[_path]) { // Directory already has files, add file
var relpath = Path.posix.join(dirparts.join('/'), item.name) var relpath = Path.posix.join(dirparts.join('/'), item.name)
libraryItemGroup[_path].push(relpath) libraryItemGroup[_path].push(relpath)
return return
} else if (!dirparts.length) { // This is the last directory, create group } else if (!dirparts.length) { // This is the last directory, create group
libraryItemGroup[_path] = [item.name] libraryItemGroup[_path] = [item.name]
return return
} else if (dirparts.length === 1 && /^cd\d{1,3}$/i.test(dirparts[0])) { // Next directory is the last and is a CD dir, create group } else if (dirparts.length === 1 && /^cd\d{1,3}$/i.test(dirparts[0])) { // Next directory is the last and is a CD dir, create group
libraryItemGroup[_path] = [Path.posix.join(dirparts[0], item.name)] libraryItemGroup[_path] = [Path.posix.join(dirparts[0], item.name)]
return return
}
} }
} }
}) })
@ -140,6 +147,15 @@ async function scanFolder(libraryMediaType, folder, serverSettings = {}) {
} }
var fileItems = await recurseFiles(folderPath) var fileItems = await recurseFiles(folderPath)
var basePath = folderPath
const isOpenAudibleFolder = fileItems.find(fi => fi.deep === 0 && fi.name === 'books.json')
if (isOpenAudibleFolder) {
Logger.info(`[scandir] Detected Open Audible Folder, looking in books folder`)
basePath = Path.posix.join(folderPath, 'books')
fileItems = await recurseFiles(basePath)
Logger.debug(`[scandir] ${fileItems.length} files found in books folder`)
}
var libraryItemGrouping = groupFileItemsIntoLibraryItemDirs(libraryMediaType, fileItems) var libraryItemGrouping = groupFileItemsIntoLibraryItemDirs(libraryMediaType, fileItems)
@ -148,11 +164,27 @@ async function scanFolder(libraryMediaType, folder, serverSettings = {}) {
return [] return []
} }
var isFile = false // item is not in a folder
var items = [] var items = []
for (const libraryItemPath in libraryItemGrouping) { for (const libraryItemPath in libraryItemGrouping) {
var libraryItemData = getDataFromMediaDir(libraryMediaType, folderPath, libraryItemPath, serverSettings) var libraryItemData = null
var fileObjs = []
if (libraryItemPath === libraryItemGrouping[libraryItemPath]) {
// Media file in root only get title
libraryItemData = {
mediaMetadata: {
title: Path.basename(libraryItemPath, Path.extname(libraryItemPath))
},
path: Path.posix.join(basePath, libraryItemPath),
relPath: libraryItemPath
}
fileObjs = await cleanFileObjects(basePath, [libraryItemPath])
isFile = true
} else {
libraryItemData = getDataFromMediaDir(libraryMediaType, folderPath, libraryItemPath, serverSettings)
fileObjs = await cleanFileObjects(libraryItemData.path, libraryItemGrouping[libraryItemPath])
}
var fileObjs = await cleanFileObjects(libraryItemData.path, libraryItemGrouping[libraryItemPath])
var libraryItemFolderStats = await getFileTimestampsWithIno(libraryItemData.path) var libraryItemFolderStats = await getFileTimestampsWithIno(libraryItemData.path)
items.push({ items.push({
folderId: folder.id, folderId: folder.id,
@ -163,6 +195,7 @@ async function scanFolder(libraryMediaType, folder, serverSettings = {}) {
birthtimeMs: libraryItemFolderStats.birthtimeMs || 0, birthtimeMs: libraryItemFolderStats.birthtimeMs || 0,
path: libraryItemData.path, path: libraryItemData.path,
relPath: libraryItemData.relPath, relPath: libraryItemData.relPath,
isFile,
media: { media: {
metadata: libraryItemData.mediaMetadata || null metadata: libraryItemData.mediaMetadata || null
}, },
@ -242,7 +275,6 @@ function getBookDataFromDir(folderPath, relPath, parseSubtitle = false) {
} }
} }
// Subtitle can be parsed from the title if user enabled // Subtitle can be parsed from the title if user enabled
// Subtitle is everything after " - " // Subtitle is everything after " - "
var subtitle = null var subtitle = null
@ -290,7 +322,7 @@ function getDataFromMediaDir(libraryMediaType, folderPath, relPath, serverSettin
} }
} }
// Called from Scanner.js
async function getLibraryItemFileData(libraryMediaType, folder, libraryItemPath, serverSettings = {}) { async function getLibraryItemFileData(libraryMediaType, folder, libraryItemPath, serverSettings = {}) {
var fileItems = await recurseFiles(libraryItemPath) var fileItems = await recurseFiles(libraryItemPath)