diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue
index 3bebacce..a642df71 100644
--- a/client/components/cards/LazyBookCard.vue
+++ b/client/components/cards/LazyBookCard.vue
@@ -150,6 +150,10 @@ export default {
_libraryItem() {
return this.libraryItem || {}
},
+ isFile() {
+ // Library item is not in a folder
+ return this._libraryItem.isFile
+ },
media() {
return this._libraryItem.media || {}
},
@@ -365,7 +369,7 @@ export default {
text: 'Match'
})
}
- if (this.userIsRoot) {
+ if (this.userIsRoot && !this.isFile) {
items.push({
func: 'rescan',
text: 'Re-Scan'
diff --git a/client/components/modals/item/tabs/Details.vue b/client/components/modals/item/tabs/Details.vue
index 6aeb84a4..e5c3a3fe 100644
--- a/client/components/modals/item/tabs/Details.vue
+++ b/client/components/modals/item/tabs/Details.vue
@@ -14,7 +14,7 @@
- Re-Scan
+ Re-Scan
Submit
@@ -49,6 +49,9 @@ export default {
this.$emit('update:processing', val)
}
},
+ isFile() {
+ return !!this.libraryItem && this.libraryItem.isFile
+ },
isRootUser() {
return this.$store.getters['user/getIsRoot']
},
diff --git a/client/components/modals/item/tabs/Files.vue b/client/components/modals/item/tabs/Files.vue
index f74df8d7..386aa643 100644
--- a/client/components/modals/item/tabs/Files.vue
+++ b/client/components/modals/item/tabs/Files.vue
@@ -1,9 +1,5 @@
@@ -51,12 +47,6 @@ export default {
},
showDownload() {
return this.userCanDownload && !this.isMissing
- },
- audiobooks() {
- return this.media.audiobooks || []
- },
- ebooks() {
- return this.media.ebooks || []
}
},
methods: {
diff --git a/client/components/tables/TracksTable.vue b/client/components/tables/TracksTable.vue
index 4df9050f..89f366fd 100644
--- a/client/components/tables/TracksTable.vue
+++ b/client/components/tables/TracksTable.vue
@@ -8,7 +8,7 @@
Full Path
-
+
Manage Tracks
@@ -59,7 +59,8 @@ export default {
type: Array,
default: () => []
},
- libraryItemId: String
+ libraryItemId: String,
+ isFile: Boolean
},
data() {
return {
diff --git a/client/components/widgets/AudiobookData.vue b/client/components/widgets/AudiobookData.vue
index a218c58e..b909c378 100644
--- a/client/components/widgets/AudiobookData.vue
+++ b/client/components/widgets/AudiobookData.vue
@@ -16,7 +16,7 @@
-
+
@@ -27,7 +27,8 @@ export default {
media: {
type: Object,
default: () => {}
- }
+ },
+ isFile: Boolean
},
data() {
return {}
diff --git a/client/pages/audiobook/_id/edit.vue b/client/pages/audiobook/_id/edit.vue
index 8f2b8b97..7969932a 100644
--- a/client/pages/audiobook/_id/edit.vue
+++ b/client/pages/audiobook/_id/edit.vue
@@ -107,6 +107,10 @@ export default {
console.error('Invalid media type')
return redirect('/')
}
+ if (libraryItem.isFile) {
+ console.error('No need to edit library item that is 1 file...')
+ return redirect('/')
+ }
return {
libraryItem,
files: libraryItem.media.audioFiles ? libraryItem.media.audioFiles.map((af) => ({ ...af, include: !af.exclude })) : []
diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue
index 9e6f8be9..cd08df99 100644
--- a/client/pages/item/_id/index.vue
+++ b/client/pages/item/_id/index.vue
@@ -165,7 +165,7 @@
- {{ audioFile.metadata.filename }} ({{ audioFile.error }})
-
+
@@ -210,6 +210,9 @@ export default {
}
},
computed: {
+ isFile() {
+ return this.libraryItem.isFile
+ },
coverAspectRatio() {
return this.$store.getters['getServerSetting']('coverAspectRatio')
},
diff --git a/server/Db.js b/server/Db.js
index b43594b8..2addf69b 100644
--- a/server/Db.js
+++ b/server/Db.js
@@ -411,7 +411,9 @@ class Db {
removeEntity(entityName, entityId) {
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}`)
var arrayKey = this.getEntityArrayKey(entityName)
if (this[arrayKey]) {
diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js
index b494c754..2189a2b4 100644
--- a/server/controllers/LibraryItemController.js
+++ b/server/controllers/LibraryItemController.js
@@ -342,6 +342,12 @@ class LibraryItemController {
Logger.error(`[LibraryItemController] Non-root user attempted to scan library item`, req.user)
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)
res.json({
result: Object.keys(ScanResult).find(key => ScanResult[key] == result)
diff --git a/server/managers/CoverManager.js b/server/managers/CoverManager.js
index 0dfdfdce..20a53d89 100644
--- a/server/managers/CoverManager.js
+++ b/server/managers/CoverManager.js
@@ -19,7 +19,7 @@ class CoverManager {
}
getCoverDirectory(libraryItem) {
- if (this.db.serverSettings.storeCoverWithItem) {
+ if (this.db.serverSettings.storeCoverWithItem && !libraryItem.isFile) {
return libraryItem.path
} else {
return Path.posix.join(this.ItemMetadataPath, libraryItem.id)
diff --git a/server/objects/LibraryItem.js b/server/objects/LibraryItem.js
index ed956757..74d13792 100644
--- a/server/objects/LibraryItem.js
+++ b/server/objects/LibraryItem.js
@@ -18,6 +18,7 @@ class LibraryItem {
this.path = null
this.relPath = null
+ this.isFile = false
this.mtimeMs = null
this.ctimeMs = null
this.birthtimeMs = null
@@ -51,6 +52,7 @@ class LibraryItem {
this.folderId = libraryItem.folderId
this.path = libraryItem.path
this.relPath = libraryItem.relPath
+ this.isFile = !!libraryItem.isFile
this.mtimeMs = libraryItem.mtimeMs || 0
this.ctimeMs = libraryItem.ctimeMs || 0
this.birthtimeMs = libraryItem.birthtimeMs || 0
@@ -82,6 +84,7 @@ class LibraryItem {
folderId: this.folderId,
path: this.path,
relPath: this.relPath,
+ isFile: this.isFile,
mtimeMs: this.mtimeMs,
ctimeMs: this.ctimeMs,
birthtimeMs: this.birthtimeMs,
@@ -105,6 +108,7 @@ class LibraryItem {
folderId: this.folderId,
path: this.path,
relPath: this.relPath,
+ isFile: this.isFile,
mtimeMs: this.mtimeMs,
ctimeMs: this.ctimeMs,
birthtimeMs: this.birthtimeMs,
@@ -128,6 +132,7 @@ class LibraryItem {
folderId: this.folderId,
path: this.path,
relPath: this.relPath,
+ isFile: this.isFile,
mtimeMs: this.mtimeMs,
ctimeMs: this.ctimeMs,
birthtimeMs: this.birthtimeMs,
@@ -460,7 +465,7 @@ class LibraryItem {
this.isSavingMetadata = true
var metadataPath = Path.join(global.MetadataPath, 'items', this.id)
- if (global.ServerSettings.storeMetadataWithItem) {
+ if (global.ServerSettings.storeMetadataWithItem && !this.isFile) {
metadataPath = this.path
} else {
// Make sure metadata book dir exists
diff --git a/server/scanner/Scanner.js b/server/scanner/Scanner.js
index 19fd3c99..d7c62091 100644
--- a/server/scanner/Scanner.js
+++ b/server/scanner/Scanner.js
@@ -235,7 +235,7 @@ class Scanner {
var hasMediaFile = dataFound.libraryFiles.some(lf => lf.isMediaFile)
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 {
var audioFileSize = 0
dataFound.libraryFiles.filter(lf => lf.fileType == 'audio').forEach(lf => audioFileSize += lf.metadata.size)
diff --git a/server/utils/fileUtils.js b/server/utils/fileUtils.js
index 763a579c..3cf98f06 100644
--- a/server/utils/fileUtils.js
+++ b/server/utils/fileUtils.js
@@ -115,6 +115,7 @@ async function recurseFiles(path, relPathToReplace = null) {
var relpath = item.fullname.replace(relPathToReplace, '')
var reldirname = Path.dirname(relpath)
+ if (reldirname === '.') reldirname = ''
var dirname = Path.dirname(item.fullname)
// Directory has a file named ".ignore" flag directory and ignore
@@ -139,15 +140,18 @@ async function recurseFiles(path, relPathToReplace = null) {
return false
}
return true
- }).map((item) => ({
- name: item.name,
- path: item.fullname.replace(relPathToReplace, ''),
- dirpath: item.path,
- reldirpath: item.path.replace(relPathToReplace, ''),
- fullpath: item.fullname,
- extension: item.extension,
- deep: item.deep
- }))
+ }).map((item) => {
+ var isInRoot = (item.path + '/' === relPathToReplace)
+ return {
+ name: item.name,
+ path: item.fullname.replace(relPathToReplace, ''),
+ dirpath: item.path,
+ reldirpath: isInRoot ? '' : item.path.replace(relPathToReplace, ''),
+ fullpath: item.fullname,
+ extension: item.extension,
+ deep: item.deep
+ }
+ })
// Sort from least deep to most
list.sort((a, b) => a.deep - b.deep)
diff --git a/server/utils/scandir.js b/server/utils/scandir.js
index b7672c1e..3bcfbc35 100644
--- a/server/utils/scandir.js
+++ b/server/utils/scandir.js
@@ -5,9 +5,9 @@ const { recurseFiles, getFileTimestampsWithIno } = require('./fileUtils')
const globals = require('./globals')
const LibraryFile = require('../objects/files/LibraryFile')
-function isMediaFile(mediaType, path) {
- if (!path) return false
- var ext = Path.extname(path)
+function isMediaFile(mediaType, ext) {
+ // 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)
@@ -62,40 +62,47 @@ 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) {
- // Step 1: Filter out files in root dir (with depth of 0)
- var itemsFiltered = fileItems.filter(i => i.deep > 0)
+ // Step 1: Filter out non-media files in root dir (with depth of 0)
+ var itemsFiltered = fileItems.filter(i => {
+ return i.deep > 0 || 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 = []
itemsFiltered.forEach(item => {
- if (isMediaFile(mediaType, item.fullpath)) mediaFileItems.push(item)
+ if (isMediaFile(mediaType, item.extension)) mediaFileItems.push(item)
else otherFileItems.push(item)
})
// Step 3: Group audio files in library items
var libraryItemGroup = {}
mediaFileItems.forEach((item) => {
- var dirparts = item.reldirpath.split('/')
+ var dirparts = item.reldirpath.split('/').filter(p => !!p)
var numparts = dirparts.length
var _path = ''
- // Iterate over directories in path
- for (let i = 0; i < numparts; i++) {
- var dirpart = dirparts.shift()
- _path = Path.posix.join(_path, dirpart)
+ if (!dirparts.length) {
+ // Media file in root
+ libraryItemGroup[item.name] = item.name
+ } 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
- var relpath = Path.posix.join(dirparts.join('/'), item.name)
- libraryItemGroup[_path].push(relpath)
- return
- } else if (!dirparts.length) { // This is the last directory, create group
- libraryItemGroup[_path] = [item.name]
- 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
- libraryItemGroup[_path] = [Path.posix.join(dirparts[0], item.name)]
- return
+ if (libraryItemGroup[_path]) { // Directory already has files, add file
+ var relpath = Path.posix.join(dirparts.join('/'), item.name)
+ libraryItemGroup[_path].push(relpath)
+ return
+ } else if (!dirparts.length) { // This is the last directory, create group
+ libraryItemGroup[_path] = [item.name]
+ 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
+ libraryItemGroup[_path] = [Path.posix.join(dirparts[0], item.name)]
+ return
+ }
}
}
})
@@ -140,6 +147,15 @@ async function scanFolder(libraryMediaType, folder, serverSettings = {}) {
}
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)
@@ -148,11 +164,27 @@ async function scanFolder(libraryMediaType, folder, serverSettings = {}) {
return []
}
+ var isFile = false // item is not in a folder
var items = []
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)
items.push({
folderId: folder.id,
@@ -163,6 +195,7 @@ async function scanFolder(libraryMediaType, folder, serverSettings = {}) {
birthtimeMs: libraryItemFolderStats.birthtimeMs || 0,
path: libraryItemData.path,
relPath: libraryItemData.relPath,
+ isFile,
media: {
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 is everything after " - "
var subtitle = null
@@ -290,7 +322,7 @@ function getDataFromMediaDir(libraryMediaType, folderPath, relPath, serverSettin
}
}
-
+// Called from Scanner.js
async function getLibraryItemFileData(libraryMediaType, folder, libraryItemPath, serverSettings = {}) {
var fileItems = await recurseFiles(libraryItemPath)