Compare commits

...

31 Commits

Author SHA1 Message Date
advplyr 8abda14e0f Version bump v2.0.7 2022-04-29 13:16:29 -05:00
advplyr 619e5c0895 Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2022-04-29 13:14:19 -05:00
advplyr 3a2594cde9 Version bump v2.0.6 2022-04-29 13:13:54 -05:00
advplyr 5cca2d0155 Update docker-build.yml 2022-04-29 13:01:12 -05:00
advplyr a467637cb5 Version bump v2.0.5 2022-04-29 12:59:35 -05:00
advplyr 1a23001955 Update version check to use releases from gh api instead of tags, add 5 minute buffer between checking for new releases 2022-04-29 12:20:51 -05:00
advplyr 8942dca31d Update docker-build workflow 2022-04-29 09:48:00 -05:00
advplyr 2a919012b6 Version bump 2.0.4 2022-04-28 18:43:00 -05:00
advplyr 40b342498f Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2022-04-28 18:40:34 -05:00
advplyr e220b2818a Add docker-build workflow 2022-04-28 18:40:29 -05:00
advplyr 0df36d2609 Merge pull request #523 from mediacowboy/patch-1
Update readme.md
2022-04-28 17:43:50 -05:00
MediaCowboy adfe50a841 Update readme.md
Updated the pull command to reflect the new docker repo.
2022-04-27 22:26:44 -05:00
advplyr 35925ddc1b Merge pull request #522 from selfhost-alt/skip-matching-identified-media
Add options to skip matching media items if they already have an ASIN/ISBN
2022-04-27 20:14:04 -05:00
advplyr 33dfb764fa Add:Support for openaudible folder structure (subject to change), add support for treating single audio files in the root directory as library items #401 2022-04-27 19:42:34 -05:00
advplyr 49bef2c641 Fix:Uploader removing single item from parsed upload items #530 2022-04-27 18:08:07 -05:00
advplyr ac58536501 Fix:Drag n drop folder upload 2022-04-27 18:03:00 -05:00
advplyr c344555be3 Fix:default user settings for orderBy and default to sort ascending for titles and authors #515 2022-04-27 17:20:44 -05:00
MediaCowboy 645bcc53c6 Update readme.md
Removed the --rm from the docker install command and added Docker Update section
2022-04-26 21:28:24 -05:00
Selfhost Alt 84dd06dfc4 Add options to skip matching media items if they already have an ASIN/ISBN 2022-04-26 17:36:29 -07:00
advplyr 0a73dd6437 Add:Ability to ignore directories by putting a file named .ignore inside dir #516 2022-04-26 19:11:32 -05:00
advplyr 2cc055a1ad Fix:checkbox default check color add to tailwind safelist #521 2022-04-26 18:14:11 -05:00
advplyr d8ec3bd218 Merge pull request #512 from selfhost-alt/log-empty-folder-path-on-scan
Log full path when warning about empty root
2022-04-25 19:14:54 -05:00
advplyr d189ec74c9 Update item api endpoint to include user media progress with item if using query string include=progress and optionally episode=episodeid - for mobile app downloads 2022-04-25 19:03:26 -05:00
advplyr 4291769b93 Fix:Filter checks on server to check for mediaType 2022-04-25 17:36:18 -05:00
Selfhost Alt 22900a3f67 Log full path when warning about empty root 2022-04-25 15:28:03 -07:00
advplyr 7fa08449de Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2022-04-25 16:39:02 -05:00
advplyr 4f7203fccb Update docker template 2022-04-25 16:38:57 -05:00
advplyr 0eea766931 Merge pull request #509 from jflattery/patch-1
Change default to ghcr
2022-04-25 14:58:18 -05:00
advplyr 5c054aef90 Merge pull request #508 from jflattery/patch-2
Change default to ghcr
2022-04-25 14:57:54 -05:00
Jim Flattery a1674d5da1 Change default to ghcr 2022-04-25 15:45:08 -04:00
Jim Flattery 91597a5454 Change default to ghcr 2022-04-25 15:43:58 -04:00
32 changed files with 339 additions and 111 deletions
+78
View File
@@ -0,0 +1,78 @@
---
name: Build and Push Docker Image
on:
push:
branches: [master]
tags:
- 'v*.*.*'
# Only build when files in these directories have been changed
paths:
- client/**
- server/**
- index.js
- package.json
release:
types: [published, edited]
# Allows you to run workflow manually from Actions tab
workflow_dispatch:
jobs:
build:
if: "!contains(github.event.head_commit.message, 'skip ci')"
runs-on: ubuntu-20.04
steps:
- name: Check out
uses: actions/checkout@v2
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
images: advplyr/audiobookshelf,ghcr.io/${{ github.repository_owner }}/audiobookshelf
tags: |
type=edge,branch=master
type=semver,pattern={{version}}
- name: Setup QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Login to Dockerhub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Login to ghcr
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GHCR_PASSWORD }}
- name: Build image
uses: docker/build-push-action@v2
with:
tags: ${{ steps.meta.outputs.tags }}
context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
- name: Move cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
+5 -1
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'
@@ -131,6 +131,9 @@ export default {
this.selectedDesc = !this.selectedDesc this.selectedDesc = !this.selectedDesc
} else { } else {
this.selected = val this.selected = val
if (val == 'media.metadata.title' || val == 'media.metadata.author' || val == 'media.metadata.authorName' || val == 'media.metadata.authorNameLF') {
this.selectedDesc = false
}
} }
this.showMenu = false this.showMenu = false
this.$nextTick(() => this.$emit('change', val)) this.$nextTick(() => this.$emit('change', val))
@@ -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']
}, },
@@ -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: {
@@ -93,7 +93,9 @@ export default {
icon: 'database', icon: 'database',
mediaType: 'book', mediaType: 'book',
settings: { settings: {
disableWatcher: false disableWatcher: false,
skipMatchingMediaWithAsin: false,
skipMatchingMediaWithIsbn: false,
} }
} }
}, },
@@ -8,6 +8,18 @@
</div> </div>
<p v-if="globalWatcherDisabled" class="text-xs text-warning">*Watcher is disabled globally in server settings</p> <p v-if="globalWatcherDisabled" class="text-xs text-warning">*Watcher is disabled globally in server settings</p>
</div> </div>
<div class="py-3">
<div class="flex items-center">
<ui-toggle-switch v-model="skipMatchingMediaWithAsin" @input="formUpdated" />
<p class="pl-4 text-lg">Skip matching books that already have an ASIN</p>
</div>
</div>
<div class="py-3">
<div class="flex items-center">
<ui-toggle-switch v-model="skipMatchingMediaWithIsbn" @input="formUpdated" />
<p class="pl-4 text-lg">Skip matching books that already have an ISBN</p>
</div>
</div>
</div> </div>
</template> </template>
@@ -23,7 +35,9 @@ export default {
data() { data() {
return { return {
provider: null, provider: null,
disableWatcher: false disableWatcher: false,
skipMatchingMediaWithAsin: false,
skipMatchingMediaWithIsbn: false,
} }
}, },
computed: { computed: {
@@ -45,7 +59,9 @@ export default {
getLibraryData() { getLibraryData() {
return { return {
settings: { settings: {
disableWatcher: !!this.disableWatcher disableWatcher: !!this.disableWatcher,
skipMatchingMediaWithAsin: !!this.skipMatchingMediaWithAsin,
skipMatchingMediaWithIsbn: !!this.skipMatchingMediaWithIsbn
} }
} }
}, },
@@ -54,6 +70,8 @@ export default {
}, },
init() { init() {
this.disableWatcher = !!this.librarySettings.disableWatcher this.disableWatcher = !!this.librarySettings.disableWatcher
this.skipMatchingMediaWithAsin = !!this.librarySettings.skipMatchingMediaWithAsin
this.skipMatchingMediaWithIsbn = !!this.librarySettings.skipMatchingMediaWithIsbn
} }
}, },
mounted() { mounted() {
+3 -2
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 {
+3 -2
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 {}
+20 -11
View File
@@ -485,6 +485,25 @@ export default {
}, },
resize() { resize() {
this.$store.commit('globals/updateWindowSize', { width: window.innerWidth, height: window.innerHeight }) this.$store.commit('globals/updateWindowSize', { width: window.innerWidth, height: window.innerHeight })
},
checkVersionUpdate() {
// Version check is only run if time since last check was 5 minutes
const VERSION_CHECK_BUFF = 1000 * 60 * 5 // 5 minutes
var lastVerCheck = localStorage.getItem('lastVerCheck') || 0
if (Date.now() - Number(lastVerCheck) > VERSION_CHECK_BUFF) {
this.$store
.dispatch('checkForUpdate')
.then((res) => {
localStorage.setItem('lastVerCheck', Date.now())
if (res && res.hasUpdate) this.showUpdateToast(res)
})
.catch((err) => console.error(err))
if (this.$route.query.error) {
this.$toast.error(this.$route.query.error)
this.$router.replace(this.$route.path)
}
}
} }
}, },
beforeMount() { beforeMount() {
@@ -503,17 +522,7 @@ export default {
this.$store.commit('setExperimentalFeatures', true) this.$store.commit('setExperimentalFeatures', true)
} }
this.$store this.checkVersionUpdate()
.dispatch('checkForUpdate')
.then((res) => {
if (res && res.hasUpdate) this.showUpdateToast(res)
})
.catch((err) => console.error(err))
if (this.$route.query.error) {
this.$toast.error(this.$route.query.error)
this.$router.replace(this.$route.path)
}
}, },
beforeDestroy() { beforeDestroy() {
window.removeEventListener('resize', this.resize) window.removeEventListener('resize', this.resize)
+3 -3
View File
@@ -174,8 +174,8 @@ export default {
if (mediaType === 'podcast') return this.cleanPodcast(item, index) if (mediaType === 'podcast') return this.cleanPodcast(item, index)
return this.cleanBook(item, index) return this.cleanBook(item, index)
}, },
async getItemsFromDataTransferItems(items, mediaType) { async getItemsFromDataTransferItems(dataTransferItems, mediaType) {
var files = await this.getFilesDropped(items) var files = await this.getFilesDropped(dataTransferItems)
if (!files || !files.length) return { error: 'No files found ' } if (!files || !files.length) return { error: 'No files found ' }
var itemData = this.fileTreeToItems(files, mediaType) var itemData = this.fileTreeToItems(files, mediaType)
if (!itemData.items.length && !itemData.ignoredFiles.length) { if (!itemData.items.length && !itemData.ignoredFiles.length) {
@@ -189,7 +189,7 @@ export default {
if (ab.ignoredFiles.length) ignoredFiles = ignoredFiles.concat(ab.ignoredFiles) if (ab.ignoredFiles.length) ignoredFiles = ignoredFiles.concat(ab.ignoredFiles)
} }
return ab.itemFiles.length return ab.itemFiles.length
}).map(ab => this.cleanItem(ab, index++)) }).map(ab => this.cleanItem(ab, mediaType, index++))
return { return {
items, items,
ignoredFiles ignoredFiles
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.0.3", "version": "2.0.7",
"description": "Audiobook manager and player", "description": "Audiobook manager and player",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
+4
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 })) : []
+4 -1
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')
}, },
+2 -1
View File
@@ -195,7 +195,8 @@ export default {
e.preventDefault() e.preventDefault()
this.isDragging = false this.isDragging = false
var items = e.dataTransfer.items || [] var items = e.dataTransfer.items || []
var itemResults = await this.uploadHelpers.getItemsFromDrop(items)
var itemResults = await this.uploadHelpers.getItemsFromDrop(items, this.selectedLibraryMediaType)
this.setResults(itemResults) this.setResults(itemResults)
}, },
inputChanged(e) { inputChanged(e) {
+7 -5
View File
@@ -33,11 +33,12 @@ export async function checkForUpdate() {
return return
} }
var largestVer = null var largestVer = null
await axios.get(`https://api.github.com/repos/advplyr/audiobookshelf/tags`).then((res) => { await axios.get(`https://api.github.com/repos/advplyr/audiobookshelf/releases`).then((res) => {
var tags = res.data var releases = res.data
if (tags && tags.length) { if (releases && releases.length) {
tags.forEach((tag) => { releases.forEach((release) => {
var verObj = parseSemver(tag.name) var tagName = release.tag_name
var verObj = parseSemver(tagName)
if (verObj) { if (verObj) {
if (!largestVer || largestVer.total < verObj.total) { if (!largestVer || largestVer.total < verObj.total) {
largestVer = verObj largestVer = verObj
@@ -50,6 +51,7 @@ export async function checkForUpdate() {
console.error('No valid version tags to compare with') console.error('No valid version tags to compare with')
return return
} }
return { return {
hasUpdate: largestVer.total > currVerObj.total, hasUpdate: largestVer.total > currVerObj.total,
latestVersion: largestVer.version, latestVersion: largestVer.version,
+1
View File
@@ -6,6 +6,7 @@ module.exports = {
safelist: [ safelist: [
'bg-success', 'bg-success',
'bg-red-600', 'bg-red-600',
'text-green-500',
'py-1.5', 'py-1.5',
'bg-info' 'bg-info'
] ]
+2 -2
View File
@@ -3,10 +3,10 @@ version: "3.7"
services: services:
audiobookshelf: audiobookshelf:
image: advplyr/audiobookshelf image: ghcr.io/advplyr/audiobookshelf
ports: ports:
- 13378:80 - 13378:80
volumes: volumes:
- /audiobooks:/audiobooks - /audiobooks:/audiobooks
- /metadata:/metadata - /metadata:/metadata
- /config:/config - /config:/config
+5 -5
View File
@@ -1,7 +1,7 @@
<?xml version="1.0"?> <?xml version="1.0"?>
<Container version="2"> <Container version="2">
<Name>audiobookshelf</Name> <Name>audiobookshelf</Name>
<Repository>advplyr/audiobookshelf</Repository> <Repository>ghcr.io/advplyr/audiobookshelf</Repository>
<Registry>https://hub.docker.com/r/advplyr/audiobookshelf/</Registry> <Registry>https://hub.docker.com/r/advplyr/audiobookshelf/</Registry>
<Network>bridge</Network> <Network>bridge</Network>
<MyIP/> <MyIP/>
@@ -9,8 +9,8 @@
<Privileged>false</Privileged> <Privileged>false</Privileged>
<Support>https://forums.unraid.net/topic/112698-support-audiobookshelf/</Support> <Support>https://forums.unraid.net/topic/112698-support-audiobookshelf/</Support>
<Project>https://github.com/advplyr/audiobookshelf</Project> <Project>https://github.com/advplyr/audiobookshelf</Project>
<Overview>**(Android app is live)** Audiobook manager and player. Saves your progress, supports multiple accounts, stream all audio formats on the fly. No more switching between dozens of audio files for a single audiobook, Audiobookshelf shows you one audio track with skipping, seeking and adjustable playback speed. Free &amp; open source mobile apps under construction, consider contributing by posting feedback, suggestions, feature requests on github or the forums.</Overview> <Overview>Self-hosted audiobook and podcast server and web app. Supports multi-user w/ permissions and keeps progress in sync across devices. Free &amp; open source mobile apps. Consider contributing by posting feedback, suggestions, feature requests on github or the forums.</Overview>
<Category>MediaApp:Books MediaServer:Books</Category> <Category>MediaApp:Books MediaServer:Books MediaApp:Other MediaServer:Other</Category>
<WebUI>http://[IP]:[PORT:80]</WebUI> <WebUI>http://[IP]:[PORT:80]</WebUI>
<TemplateURL>https://raw.githubusercontent.com/advplyr/docker-templates/master/audiobookshelf.xml</TemplateURL> <TemplateURL>https://raw.githubusercontent.com/advplyr/docker-templates/master/audiobookshelf.xml</TemplateURL>
<Icon>https://github.com/advplyr/audiobookshelf/raw/master/client/static/Logo.png</Icon> <Icon>https://github.com/advplyr/audiobookshelf/raw/master/client/static/Logo.png</Icon>
@@ -20,7 +20,7 @@
<DateInstalled>1629238508</DateInstalled> <DateInstalled>1629238508</DateInstalled>
<DonateText/> <DonateText/>
<DonateLink/> <DonateLink/>
<Description>Audiobook manager and player. Saves your progress, supports multiple accounts, stream all audio formats on the fly. No more switching between dozens of audio files for a single audiobook, Audiobookshelf shows you one audio track with skipping, seeking and adjustable playback speed. Free &amp; open source mobile apps under construction, consider contributing by posting feedback, suggestions, feature requests on github or the forums.</Description> <Description>Self-hosted audiobook and podcast server and web app. Supports multi-user w/ permissions and keeps progress in sync across devices. Free &amp; open source mobile apps. Consider contributing by posting feedback, suggestions, feature requests on github or the forums.</Description>
<Networking> <Networking>
<Mode>bridge</Mode> <Mode>bridge</Mode>
<Publish> <Publish>
@@ -65,4 +65,4 @@
<Config Name="Config" Target="/config" Default="/mnt/user/appdata/audiobookshelf/config/" Mode="rw" Description="Container Path: /config" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/audiobookshelf/config/</Config> <Config Name="Config" Target="/config" Default="/mnt/user/appdata/audiobookshelf/config/" Mode="rw" Description="Container Path: /config" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/audiobookshelf/config/</Config>
<Config Name="Metadata" Target="/metadata" Default="/mnt/user/appdata/audiobookshelf/metadata/" Mode="rw" Description="Container Path: /metadata" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/audiobookshelf/metadata/</Config> <Config Name="Metadata" Target="/metadata" Default="/mnt/user/appdata/audiobookshelf/metadata/" Mode="rw" Description="Container Path: /metadata" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/audiobookshelf/metadata/</Config>
<Config Name="Web UI Port" Target="80" Default="13378" Mode="tcp" Description="Container Port: 80" Type="Port" Display="always" Required="false" Mask="false">13378</Config> <Config Name="Web UI Port" Target="80" Default="13378" Mode="tcp" Description="Container Port: 80" Type="Port" Display="always" Required="false" Mask="false">13378</Config>
</Container> </Container>
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf", "name": "audiobookshelf",
"version": "2.0.3", "version": "2.0.7",
"description": "Self-hosted audiobook server for managing and playing audiobooks", "description": "Self-hosted audiobook server for managing and playing audiobooks",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
+10 -2
View File
@@ -74,7 +74,15 @@ docker run -d \
-v </path/to/config>:/config \ -v </path/to/config>:/config \
-v </path/to/metadata>:/metadata \ -v </path/to/metadata>:/metadata \
--name audiobookshelf \ --name audiobookshelf \
--rm advplyr/audiobookshelf ghcr.io/advplyr/audiobookshelf
```
### Docker Update
```bash
docker stop audiobookshelf
docker pull ghcr.io/advplyr/audiobookshelf
docker start audiobookshelf
``` ```
### Running with Docker Compose ### Running with Docker Compose
@@ -83,7 +91,7 @@ docker run -d \
### docker-compose.yml ### ### docker-compose.yml ###
services: services:
audiobookshelf: audiobookshelf:
image: advplyr/audiobookshelf image: ghcr.io/advplyr/audiobookshelf
ports: ports:
- 13378:80 - 13378:80
volumes: volumes:
+3 -1
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]) {
@@ -11,6 +11,12 @@ class LibraryItemController {
if (req.query.expanded == 1) { if (req.query.expanded == 1) {
var item = req.libraryItem.toJSONExpanded() var item = req.libraryItem.toJSONExpanded()
// Include users media progress
if (includeEntities.includes('progress')) {
var episodeId = req.query.episode || null
item.userMediaProgress = req.user.getMediaProgress(item.id, episodeId)
}
if (item.mediaType == 'book') { if (item.mediaType == 'book') {
if (includeEntities.includes('authors')) { if (includeEntities.includes('authors')) {
item.media.metadata.authors = item.media.metadata.authors.map(au => { item.media.metadata.authors = item.media.metadata.authors.map(au => {
@@ -336,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)
+1 -1
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)
+6 -1
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
+7 -1
View File
@@ -4,6 +4,8 @@ const Logger = require('../../Logger')
class LibrarySettings { class LibrarySettings {
constructor(settings) { constructor(settings) {
this.disableWatcher = false this.disableWatcher = false
this.skipMatchingMediaWithAsin = false
this.skipMatchingMediaWithIsbn = false
if (settings) { if (settings) {
this.construct(settings) this.construct(settings)
@@ -12,11 +14,15 @@ class LibrarySettings {
construct(settings) { construct(settings) {
this.disableWatcher = !!settings.disableWatcher this.disableWatcher = !!settings.disableWatcher
this.skipMatchingMediaWithAsin = !!settings.skipMatchingMediaWithAsin
this.skipMatchingMediaWithIsbn = !!settings.skipMatchingMediaWithIsbn
} }
toJSON() { toJSON() {
return { return {
disableWatcher: this.disableWatcher disableWatcher: this.disableWatcher,
skipMatchingMediaWithAsin: this.skipMatchingMediaWithAsin,
skipMatchingMediaWithIsbn: this.skipMatchingMediaWithIsbn
} }
} }
+1 -1
View File
@@ -2,7 +2,7 @@ const Logger = require('../../Logger')
class MediaProgress { class MediaProgress {
constructor(progress) { constructor(progress) {
this.id = null // Same as library item id this.id = null
this.libraryItemId = null this.libraryItemId = null
this.episodeId = null // For podcasts this.episodeId = null // For podcasts
+1 -1
View File
@@ -57,7 +57,7 @@ class User {
mobileOrderBy: 'recent', mobileOrderBy: 'recent',
mobileOrderDesc: true, mobileOrderDesc: true,
mobileFilterBy: 'all', mobileFilterBy: 'all',
orderBy: 'book.title', orderBy: 'media.metadata.title',
orderDesc: false, orderDesc: false,
filterBy: 'all', filterBy: 'all',
playbackRate: 1, playbackRate: 1,
+16 -1
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)
@@ -726,6 +726,21 @@ class Scanner {
for (let i = 0; i < itemsInLibrary.length; i++) { for (let i = 0; i < itemsInLibrary.length; i++) {
var libraryItem = itemsInLibrary[i] var libraryItem = itemsInLibrary[i]
if (libraryItem.media.metadata.asin && library.settings.skipMatchingMediaWithAsin) {
Logger.debug(`[Scanner] matchLibraryItems: Skipping "${
libraryItem.media.metadata.title
}" because it already has an ASIN (${i + 1} of ${itemsInLibrary.length})`)
continue;
}
if (libraryItem.media.metadata.isbn && library.settings.skipMatchingMediaWithIsbn) {
Logger.debug(`[Scanner] matchLibraryItems: Skipping "${
libraryItem.media.metadata.title
}" because it already has an ISBN (${i + 1} of ${itemsInLibrary.length})`)
continue;
}
Logger.debug(`[Scanner] matchLibraryItems: Quick matching "${libraryItem.media.metadata.title}" (${i + 1} of ${itemsInLibrary.length})`) Logger.debug(`[Scanner] matchLibraryItems: Quick matching "${libraryItem.media.metadata.title}" (${i + 1} of ${itemsInLibrary.length})`)
var result = await this.quickMatchLibraryItem(libraryItem, { provider }) var result = await this.quickMatchLibraryItem(libraryItem, { provider })
if (result.warning) { if (result.warning) {
+35 -11
View File
@@ -1,6 +1,7 @@
const fs = require('fs-extra') const fs = require('fs-extra')
const rra = require('recursive-readdir-async') const rra = require('recursive-readdir-async')
const axios = require('axios') const axios = require('axios')
const Path = require('path')
const Logger = require('../Logger') const Logger = require('../Logger')
async function getFileStat(path) { async function getFileStat(path) {
@@ -104,14 +105,27 @@ async function recurseFiles(path, relPathToReplace = null) {
return [] return []
} }
const directoriesToIgnore = []
list = list.filter((item) => { list = list.filter((item) => {
if (item.error) { if (item.error) {
Logger.error(`[fileUtils] Recurse files file "${item.fullName}" has error`, item.error) Logger.error(`[fileUtils] Recurse files file "${item.fullname}" has error`, item.error)
return false
}
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
if (item.name === '.ignore' && reldirname && reldirname !== '.' && !directoriesToIgnore.includes(dirname)) {
Logger.debug(`[fileUtils] .ignore found - ignoring directory "${reldirname}"`)
directoriesToIgnore.push(dirname)
return false return false
} }
// Ignore any file if a directory or the filename starts with "." // Ignore any file if a directory or the filename starts with "."
var relpath = item.fullname.replace(relPathToReplace, '')
var pathStartsWithPeriod = relpath.split('/').find(p => p.startsWith('.')) var pathStartsWithPeriod = relpath.split('/').find(p => p.startsWith('.'))
if (pathStartsWithPeriod) { if (pathStartsWithPeriod) {
Logger.debug(`[fileUtils] Ignoring path has . "${relpath}"`) Logger.debug(`[fileUtils] Ignoring path has . "${relpath}"`)
@@ -119,15 +133,25 @@ async function recurseFiles(path, relPathToReplace = null) {
} }
return true return true
}).map((item) => ({ }).filter(item => {
name: item.name, // Filter out items in ignore directories
path: item.fullname.replace(relPathToReplace, ''), if (directoriesToIgnore.includes(Path.dirname(item.fullname))) {
dirpath: item.path, Logger.debug(`[fileUtils] Ignoring path in dir with .ignore "${item.fullname}"`)
reldirpath: item.path.replace(relPathToReplace, ''), return false
fullpath: item.fullname, }
extension: item.extension, return true
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 // Sort from least deep to most
list.sort((a, b) => a.deep - b.deep) list.sort((a, b) => a.deep - b.deep)
+20 -16
View File
@@ -20,13 +20,13 @@ module.exports = {
if (group === 'genres') filtered = filtered.filter(li => li.media.metadata && li.media.metadata.genres.includes(filter)) if (group === 'genres') filtered = filtered.filter(li => li.media.metadata && li.media.metadata.genres.includes(filter))
else if (group === 'tags') filtered = filtered.filter(li => li.media.tags.includes(filter)) else if (group === 'tags') filtered = filtered.filter(li => li.media.tags.includes(filter))
else if (group === 'series') { else if (group === 'series') {
if (filter === 'No Series') filtered = filtered.filter(li => li.media.metadata && !li.media.metadata.series.length) if (filter === 'No Series') filtered = filtered.filter(li => li.mediaType === 'book' && !li.media.metadata.series.length)
else { else {
filtered = filtered.filter(li => li.media.metadata && li.media.metadata.hasSeries(filter)) filtered = filtered.filter(li => li.mediaType === 'book' && li.media.metadata.hasSeries(filter))
} }
} }
else if (group === 'authors') filtered = filtered.filter(li => li.media.metadata && li.media.metadata.hasAuthor(filter)) else if (group === 'authors') filtered = filtered.filter(li => li.mediaType === 'book' && li.media.metadata.hasAuthor(filter))
else if (group === 'narrators') filtered = filtered.filter(li => li.media.metadata && li.media.metadata.hasNarrator(filter)) else if (group === 'narrators') filtered = filtered.filter(li => li.mediaType === 'book' && li.media.metadata.hasNarrator(filter))
else if (group === 'progress') { else if (group === 'progress') {
filtered = filtered.filter(li => { filtered = filtered.filter(li => {
var itemProgress = user.getMediaProgress(li.id) var itemProgress = user.getMediaProgress(li.id)
@@ -37,18 +37,22 @@ module.exports = {
}) })
} else if (group == 'missing') { } else if (group == 'missing') {
filtered = filtered.filter(li => { filtered = filtered.filter(li => {
if (filter === 'ASIN' && li.media.metadata.asin === null) return true; if (li.mediaType === 'book') {
if (filter === 'ISBN' && li.media.metadata.isbn === null) return true; if (filter === 'ASIN' && li.media.metadata.asin === null) return true;
if (filter === 'Subtitle' && li.media.metadata.subtitle === null) return true; if (filter === 'ISBN' && li.media.metadata.isbn === null) return true;
if (filter === 'Author' && li.media.metadata.authors.length === 0) return true; if (filter === 'Subtitle' && li.media.metadata.subtitle === null) return true;
if (filter === 'Publish Year' && li.media.metadata.publishedYear === null) return true; if (filter === 'Author' && li.media.metadata.authors.length === 0) return true;
if (filter === 'Series' && li.media.metadata.series.length === 0) return true; if (filter === 'Publish Year' && li.media.metadata.publishedYear === null) return true;
if (filter === 'Description' && li.media.metadata.description === null) return true; if (filter === 'Series' && li.media.metadata.series.length === 0) return true;
if (filter === 'Genres' && li.media.metadata.genres.length === 0) return true; if (filter === 'Description' && li.media.metadata.description === null) return true;
if (filter === 'Tags' && li.media.tags.length === 0) return true; if (filter === 'Genres' && li.media.metadata.genres.length === 0) return true;
if (filter === 'Narrator' && li.media.metadata.narrators.length === 0) return true; if (filter === 'Tags' && li.media.tags.length === 0) return true;
if (filter === 'Publisher' && li.media.metadata.publisher === null) return true; if (filter === 'Narrator' && li.media.metadata.narrators.length === 0) return true;
if (filter === 'Language' && li.media.metadata.language === null) return true; if (filter === 'Publisher' && li.media.metadata.publisher === null) return true;
if (filter === 'Language' && li.media.metadata.language === null) return true;
} else {
return false
}
}) })
} else if (group === 'languages') { } else if (group === 'languages') {
filtered = filtered.filter(li => li.media.metadata && li.media.metadata.language === filter) filtered = filtered.filter(li => li.media.metadata && li.media.metadata.language === filter)
+58 -26
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,19 +147,44 @@ 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)
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: ${folderPath}`)
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)