mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-03 17:30:39 +02:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8abda14e0f | |||
| 619e5c0895 | |||
| 3a2594cde9 | |||
| 5cca2d0155 | |||
| a467637cb5 | |||
| 1a23001955 | |||
| 8942dca31d | |||
| 2a919012b6 | |||
| 40b342498f | |||
| e220b2818a | |||
| 0df36d2609 | |||
| adfe50a841 | |||
| 35925ddc1b | |||
| 33dfb764fa | |||
| 49bef2c641 | |||
| ac58536501 | |||
| c344555be3 | |||
| 645bcc53c6 | |||
| 84dd06dfc4 | |||
| 0a73dd6437 | |||
| 2cc055a1ad | |||
| d8ec3bd218 | |||
| d189ec74c9 | |||
| 4291769b93 | |||
| 22900a3f67 | |||
| 7fa08449de | |||
| 4f7203fccb | |||
| 0eea766931 | |||
| 5c054aef90 | |||
| a1674d5da1 | |||
| 91597a5454 |
@@ -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
|
||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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": {
|
||||||
|
|||||||
@@ -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 })) : []
|
||||||
|
|||||||
@@ -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')
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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'
|
||||||
]
|
]
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@ version: "3.7"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
audiobookshelf:
|
audiobookshelf:
|
||||||
image: advplyr/audiobookshelf
|
image: ghcr.io/advplyr/audiobookshelf
|
||||||
ports:
|
ports:
|
||||||
- 13378:80
|
- 13378:80
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
+4
-4
@@ -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 & 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 & 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 & 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 & 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>
|
||||||
|
|||||||
+1
-1
@@ -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": {
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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,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
@@ -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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user