mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-04 09:50:42 +02:00
Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 54389e3c25 | |||
| bf0da1c6ec | |||
| 591a866f8c | |||
| fc8473ed84 | |||
| b19442e440 | |||
| 7a51e0693d | |||
| 21785c8e72 | |||
| bdf6ccbd2d | |||
| ceb163570f | |||
| 049ae73d74 | |||
| 729fdd5c9f | |||
| 4dac8ac16c | |||
| 220bbc3d2d | |||
| c2a4b32192 | |||
| 09d0d47549 | |||
| 4185807da4 | |||
| 8abda14e0f | |||
| 619e5c0895 | |||
| 3a2594cde9 | |||
| 5cca2d0155 | |||
| a467637cb5 | |||
| 1a23001955 | |||
| 8942dca31d | |||
| 2a919012b6 | |||
| 40b342498f | |||
| e220b2818a | |||
| 620bf7990f | |||
| 0df36d2609 | |||
| adfe50a841 | |||
| 35925ddc1b | |||
| 33dfb764fa | |||
| 49bef2c641 | |||
| ac58536501 | |||
| c344555be3 | |||
| 645bcc53c6 | |||
| 84dd06dfc4 | |||
| 0a73dd6437 | |||
| 2cc055a1ad | |||
| d8ec3bd218 | |||
| d189ec74c9 | |||
| 4291769b93 | |||
| 22900a3f67 | |||
| 7fa08449de | |||
| 4f7203fccb | |||
| 0eea766931 | |||
| 5c054aef90 | |||
| a1674d5da1 | |||
| 91597a5454 | |||
| 11354a3e3f | |||
| dcd4f69383 | |||
| e253939c1e | |||
| f25ce1c0e7 | |||
| 7717e57c16 | |||
| 2e28c9b06d | |||
| 4bc7cd2045 | |||
| 5389115120 | |||
| 6e99cf6570 | |||
| 21bdd9f9ec | |||
| e3ae3f7e6a | |||
| 74bf917150 | |||
| 5666b263f5 |
@@ -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
|
||||||
@@ -54,10 +54,16 @@
|
|||||||
<ui-icon-btn :disabled="processingBatch" icon="collections_bookmark" @click="batchAddToCollectionClick" class="mx-1.5" />
|
<ui-icon-btn :disabled="processingBatch" icon="collections_bookmark" @click="batchAddToCollectionClick" class="mx-1.5" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
<template v-if="userCanUpdate && numLibraryItemsSelected < 50">
|
<template v-if="userCanUpdate && numLibraryItemsSelected < 50">
|
||||||
<ui-icon-btn v-show="!processingBatchDelete" icon="edit" bg-color="warning" class="mx-1.5" @click="batchEditClick" />
|
<ui-tooltip text="Edit" direction="bottom">
|
||||||
|
<ui-icon-btn v-show="!processingBatchDelete" icon="edit" bg-color="warning" class="mx-1.5" @click="batchEditClick" />
|
||||||
|
</ui-tooltip>
|
||||||
</template>
|
</template>
|
||||||
<ui-icon-btn v-show="userCanDelete" :disabled="processingBatchDelete" icon="delete" bg-color="error" class="mx-1.5" @click="batchDeleteClick" />
|
<ui-tooltip v-if="userCanDelete" text="Delete" direction="bottom">
|
||||||
<span class="material-icons text-4xl px-4 hover:text-gray-100 cursor-pointer" :class="processingBatchDelete ? 'text-gray-400' : ''" @click="cancelSelectionMode">close</span>
|
<ui-icon-btn :disabled="processingBatchDelete" icon="delete" bg-color="error" class="mx-1.5" @click="batchDeleteClick" />
|
||||||
|
</ui-tooltip>
|
||||||
|
<ui-tooltip text="Deselect All" direction="bottom">
|
||||||
|
<span class="material-icons text-4xl px-4 hover:text-gray-100 cursor-pointer" :class="processingBatchDelete ? 'text-gray-400' : ''" @click="cancelSelectionMode">close</span>
|
||||||
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -229,4 +235,4 @@ export default {
|
|||||||
#appbar {
|
#appbar {
|
||||||
box-shadow: 0px 5px 5px #11111155;
|
box-shadow: 0px 5px 5px #11111155;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export default {
|
|||||||
},
|
},
|
||||||
async fetchCategories() {
|
async fetchCategories() {
|
||||||
var categories = await this.$axios
|
var categories = await this.$axios
|
||||||
.$get(`/api/libraries/${this.currentLibraryId}/personalized?minified=1`)
|
.$get(`/api/libraries/${this.currentLibraryId}/personalized`)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
return data
|
return data
|
||||||
})
|
})
|
||||||
@@ -128,8 +128,7 @@ export default {
|
|||||||
type: 'series',
|
type: 'series',
|
||||||
entities: this.results.series.map((seriesObj) => {
|
entities: this.results.series.map((seriesObj) => {
|
||||||
return {
|
return {
|
||||||
name: seriesObj.series.name,
|
...seriesObj.series,
|
||||||
series: seriesObj.series,
|
|
||||||
books: seriesObj.books,
|
books: seriesObj.books,
|
||||||
type: 'series'
|
type: 'series'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,16 +14,28 @@
|
|||||||
<div id="toolbar" class="absolute top-10 md:top-0 left-0 w-full h-10 md:h-full z-30 flex items-center justify-end md:justify-start px-2 md:px-8">
|
<div id="toolbar" class="absolute top-10 md:top-0 left-0 w-full h-10 md:h-full z-30 flex items-center justify-end md:justify-start px-2 md:px-8">
|
||||||
<template v-if="page !== 'search' && page !== 'podcast-search' && !isHome">
|
<template v-if="page !== 'search' && page !== 'podcast-search' && !isHome">
|
||||||
<p v-if="!selectedSeries" class="font-book hidden md:block">{{ numShowing }} {{ entityName }}</p>
|
<p v-if="!selectedSeries" class="font-book hidden md:block">{{ numShowing }} {{ entityName }}</p>
|
||||||
<div v-else class="items-center hidden md:flex">
|
<div v-else class="items-center hidden md:flex w-full">
|
||||||
<div @click="seriesBackArrow" class="rounded-full h-9 w-9 flex items-center justify-center hover:bg-white hover:bg-opacity-10 cursor-pointer">
|
<div @click="seriesBackArrow" class="rounded-full h-9 w-9 flex items-center justify-center hover:bg-white hover:bg-opacity-10 cursor-pointer">
|
||||||
<span class="material-icons text-2xl text-white">west</span>
|
<span class="material-icons text-2xl text-white">west</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="pl-4 font-book text-lg">
|
<p class="pl-4 font-book text-lg">
|
||||||
{{ selectedSeries }}
|
{{ seriesName }}
|
||||||
</p>
|
</p>
|
||||||
<div class="w-6 h-6 rounded-full bg-black bg-opacity-30 flex items-center justify-center ml-3">
|
<div class="w-6 h-6 rounded-full bg-black bg-opacity-30 flex items-center justify-center ml-3">
|
||||||
<span class="font-mono">{{ numShowing }}</span>
|
<span class="font-mono">{{ numShowing }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<ui-btn color="primary" small :loading="processingSeries" class="flex items-center" @click="markSeriesFinished">
|
||||||
|
<div class="h-5 w-5">
|
||||||
|
<svg v-if="isSeriesFinished" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="rgb(63, 181, 68)">
|
||||||
|
<path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z" />
|
||||||
|
</svg>
|
||||||
|
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-7 19.6l-7-4.66V3h14v12.93l-7 4.67zm-2.01-7.42l-2.58-2.59L6 12l4 4 8-8-1.42-1.42z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span class="pl-2"> Mark Series {{ isSeriesFinished ? 'Not Finished' : 'Finished' }}</span></ui-btn
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow hidden sm:inline-block" />
|
<div class="flex-grow hidden sm:inline-block" />
|
||||||
|
|
||||||
@@ -38,6 +50,8 @@
|
|||||||
<span class="material-icons" style="font-size: 1.4rem">view_list</span>
|
<span class="material-icons" style="font-size: 1.4rem">view_list</span>
|
||||||
</div>
|
</div>
|
||||||
</div> -->
|
</div> -->
|
||||||
|
|
||||||
|
<ui-btn v-if="isIssuesFilter && userCanDelete" :loading="processingIssues" color="error" small class="ml-4" @click="removeAllIssues">Remove All {{ numShowing }} {{ entityName }}</ui-btn>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="page === 'search'">
|
<template v-else-if="page === 'search'">
|
||||||
<div @click="searchBackArrow" class="rounded-full h-10 w-10 flex items-center justify-center hover:bg-white hover:bg-opacity-10 cursor-pointer">
|
<div @click="searchBackArrow" class="rounded-full h-10 w-10 flex items-center justify-center hover:bg-white hover:bg-opacity-10 cursor-pointer">
|
||||||
@@ -56,7 +70,10 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
page: String,
|
page: String,
|
||||||
isHome: Boolean,
|
isHome: Boolean,
|
||||||
selectedSeries: String,
|
selectedSeries: {
|
||||||
|
type: Object,
|
||||||
|
default: () => null
|
||||||
|
},
|
||||||
searchQuery: String,
|
searchQuery: String,
|
||||||
viewMode: String
|
viewMode: String
|
||||||
},
|
},
|
||||||
@@ -66,10 +83,15 @@ export default {
|
|||||||
hasInit: false,
|
hasInit: false,
|
||||||
totalEntities: 0,
|
totalEntities: 0,
|
||||||
keywordFilter: null,
|
keywordFilter: null,
|
||||||
keywordTimeout: null
|
keywordTimeout: null,
|
||||||
|
processingSeries: false,
|
||||||
|
processingIssues: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
userCanDelete() {
|
||||||
|
return this.$store.getters['user/getUserCanDelete']
|
||||||
|
},
|
||||||
isPodcast() {
|
isPodcast() {
|
||||||
return this.$store.getters['libraries/getCurrentLibraryMediaType'] == 'podcast'
|
return this.$store.getters['libraries/getCurrentLibraryMediaType'] == 'podcast'
|
||||||
},
|
},
|
||||||
@@ -103,9 +125,68 @@ export default {
|
|||||||
},
|
},
|
||||||
showLibrary() {
|
showLibrary() {
|
||||||
return this.libraryBookshelfPage && this.paramId === '' && !this.showingIssues
|
return this.libraryBookshelfPage && this.paramId === '' && !this.showingIssues
|
||||||
|
},
|
||||||
|
seriesName() {
|
||||||
|
return this.selectedSeries ? this.selectedSeries.name : null
|
||||||
|
},
|
||||||
|
seriesProgress() {
|
||||||
|
return this.selectedSeries ? this.selectedSeries.progress : null
|
||||||
|
},
|
||||||
|
seriesLibraryItemIds() {
|
||||||
|
if (!this.seriesProgress) return []
|
||||||
|
return this.seriesProgress.libraryItemIds || []
|
||||||
|
},
|
||||||
|
isSeriesFinished() {
|
||||||
|
return this.seriesProgress && !!this.seriesProgress.isFinished
|
||||||
|
},
|
||||||
|
filterBy() {
|
||||||
|
return this.$store.getters['user/getUserSetting']('filterBy')
|
||||||
|
},
|
||||||
|
isIssuesFilter() {
|
||||||
|
return this.filterBy === 'issues'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
removeAllIssues() {
|
||||||
|
if (confirm(`Are you sure you want to remove all library items with issues?\n\nNote: This will not delete any files`)) {
|
||||||
|
this.processingIssues = true
|
||||||
|
this.$axios
|
||||||
|
.$delete(`/api/libraries/${this.currentLibraryId}/issues`)
|
||||||
|
.then(() => {
|
||||||
|
this.$toast.success('Removed library items with issues')
|
||||||
|
this.$router.push(`/library/${this.currentLibraryId}/bookshelf`)
|
||||||
|
this.processingIssues = false
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to remove library items with issues', error)
|
||||||
|
this.$toast.error('Failed to remove library items with issues')
|
||||||
|
this.processingIssues = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
markSeriesFinished() {
|
||||||
|
var newIsFinished = !this.isSeriesFinished
|
||||||
|
this.processingSeries = true
|
||||||
|
var updateProgressPayloads = this.seriesLibraryItemIds.map((lid) => {
|
||||||
|
return {
|
||||||
|
id: lid,
|
||||||
|
isFinished: newIsFinished
|
||||||
|
}
|
||||||
|
})
|
||||||
|
console.log('Progress payloads', updateProgressPayloads)
|
||||||
|
this.$axios
|
||||||
|
.patch(`/api/me/progress/batch/update`, updateProgressPayloads)
|
||||||
|
.then(() => {
|
||||||
|
this.$toast.success('Series update success')
|
||||||
|
this.selectedSeries.progress.isFinished = newIsFinished
|
||||||
|
this.processingSeries = false
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.$toast.error('Series update failed')
|
||||||
|
console.error('Failed to batch update read/not read', error)
|
||||||
|
this.processingSeries = false
|
||||||
|
})
|
||||||
|
},
|
||||||
searchBackArrow() {
|
searchBackArrow() {
|
||||||
this.$router.replace(`/library/${this.currentLibraryId}/bookshelf`)
|
this.$router.replace(`/library/${this.currentLibraryId}/bookshelf`)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
<div v-show="isAuthorsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="isAuthorsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/search`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
<icons-podcast-svg class="w-6 h-6" />
|
<icons-podcast-svg class="w-6 h-6" />
|
||||||
|
|
||||||
<p class="font-book pt-1.5" style="font-size: 0.9rem">Search</p>
|
<p class="font-book pt-1.5" style="font-size: 0.9rem">Search</p>
|
||||||
@@ -82,6 +82,9 @@ export default {
|
|||||||
showExperimentalFeatures() {
|
showExperimentalFeatures() {
|
||||||
return this.$store.state.showExperimentalFeatures
|
return this.$store.state.showExperimentalFeatures
|
||||||
},
|
},
|
||||||
|
userIsAdminOrUp() {
|
||||||
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
|
},
|
||||||
paramId() {
|
paramId() {
|
||||||
return this.$route.params ? this.$route.params.id || '' : ''
|
return this.$route.params ? this.$route.params.id || '' : ''
|
||||||
},
|
},
|
||||||
@@ -112,6 +115,9 @@ export default {
|
|||||||
showLibrary() {
|
showLibrary() {
|
||||||
return this.libraryBookshelfPage && this.paramId === '' && !this.showingIssues
|
return this.libraryBookshelfPage && this.paramId === '' && !this.showingIssues
|
||||||
},
|
},
|
||||||
|
filterBy() {
|
||||||
|
return this.$store.getters['user/getUserSetting']('filterBy')
|
||||||
|
},
|
||||||
showingIssues() {
|
showingIssues() {
|
||||||
if (!this.$route.query) return false
|
if (!this.$route.query) return false
|
||||||
return this.libraryBookshelfPage && this.$route.query.filter === 'issues'
|
return this.libraryBookshelfPage && this.$route.query.filter === 'issues'
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<!-- Series sequence -->
|
<!-- Series sequence -->
|
||||||
<div v-if="seriesSequence && showSequence && !isHovering && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
|
<div v-if="seriesSequence && !isHovering && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
|
||||||
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">#{{ seriesSequence }}</p>
|
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">#{{ seriesSequence }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -110,7 +110,6 @@ export default {
|
|||||||
default: 192
|
default: 192
|
||||||
},
|
},
|
||||||
bookCoverAspectRatio: Number,
|
bookCoverAspectRatio: Number,
|
||||||
showSequence: Boolean,
|
|
||||||
bookshelfView: Number,
|
bookshelfView: Number,
|
||||||
bookMount: {
|
bookMount: {
|
||||||
// Book can be passed as prop or set with setEntity()
|
// Book can be passed as prop or set with setEntity()
|
||||||
@@ -150,6 +149,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 || {}
|
||||||
},
|
},
|
||||||
@@ -172,7 +175,7 @@ export default {
|
|||||||
return this._libraryItem.id
|
return this._libraryItem.id
|
||||||
},
|
},
|
||||||
series() {
|
series() {
|
||||||
// Only included when filtering by series or collapse series
|
// Only included when filtering by series or collapse series or Continue Series shelf on home page
|
||||||
return this.mediaMetadata.series
|
return this.mediaMetadata.series
|
||||||
},
|
},
|
||||||
seriesSequence() {
|
seriesSequence() {
|
||||||
@@ -272,7 +275,8 @@ export default {
|
|||||||
return this.userProgress ? !!this.userProgress.isFinished : false
|
return this.userProgress ? !!this.userProgress.isFinished : false
|
||||||
},
|
},
|
||||||
showError() {
|
showError() {
|
||||||
return this.numMissingParts || this.isMissing || this.isInvalid
|
if (this.recentEpisode) return false // Dont show podcast error on episode card
|
||||||
|
return this.numInvalidAudioFiles || this.numMissingParts || this.isMissing || this.isInvalid
|
||||||
},
|
},
|
||||||
isStreaming() {
|
isStreaming() {
|
||||||
return this.store.getters['getlibraryItemIdStreaming'] === this.libraryItemId
|
return this.store.getters['getlibraryItemIdStreaming'] === this.libraryItemId
|
||||||
@@ -296,6 +300,10 @@ export default {
|
|||||||
if (this.isPodcast) return 0
|
if (this.isPodcast) return 0
|
||||||
return this.media.numMissingParts
|
return this.media.numMissingParts
|
||||||
},
|
},
|
||||||
|
numInvalidAudioFiles() {
|
||||||
|
if (this.isPodcast) return 0
|
||||||
|
return this.media.numInvalidAudioFiles
|
||||||
|
},
|
||||||
errorText() {
|
errorText() {
|
||||||
if (this.isMissing) return 'Item directory is missing!'
|
if (this.isMissing) return 'Item directory is missing!'
|
||||||
else if (this.isInvalid) {
|
else if (this.isInvalid) {
|
||||||
@@ -304,7 +312,11 @@ export default {
|
|||||||
}
|
}
|
||||||
var txt = ''
|
var txt = ''
|
||||||
if (this.numMissingParts) {
|
if (this.numMissingParts) {
|
||||||
txt = `${this.numMissingParts} missing parts.`
|
txt += `${this.numMissingParts} missing parts.`
|
||||||
|
}
|
||||||
|
if (this.numInvalidAudioFiles) {
|
||||||
|
if (txt) txt += ' '
|
||||||
|
txt += `${this.numInvalidAudioFiles} invalid audio files.`
|
||||||
}
|
}
|
||||||
return txt || 'Unknown Error'
|
return txt || 'Unknown Error'
|
||||||
},
|
},
|
||||||
@@ -356,7 +368,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'
|
||||||
|
|||||||
@@ -61,6 +61,9 @@ export default {
|
|||||||
books() {
|
books() {
|
||||||
return this.series ? this.series.books || [] : []
|
return this.series ? this.series.books || [] : []
|
||||||
},
|
},
|
||||||
|
addedAt() {
|
||||||
|
return this.series ? this.series.addedAt : 0
|
||||||
|
},
|
||||||
seriesBookProgress() {
|
seriesBookProgress() {
|
||||||
return this.books
|
return this.books
|
||||||
.map((libraryItem) => {
|
.map((libraryItem) => {
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ export default {
|
|||||||
return ['Finished', 'In Progress', 'Not Started']
|
return ['Finished', 'In Progress', 'Not Started']
|
||||||
},
|
},
|
||||||
missing() {
|
missing() {
|
||||||
return ['ASIN', 'ISBN', 'Subtitle', 'Author', 'Publish Year', 'Series', 'Volume Number', 'Description', 'Genres', 'Tags', 'Narrator', 'Publisher', 'Language']
|
return ['ASIN', 'ISBN', 'Subtitle', 'Author', 'Publish Year', 'Series', 'Description', 'Genres', 'Tags', 'Narrator', 'Publisher', 'Language']
|
||||||
},
|
},
|
||||||
sublistItems() {
|
sublistItems() {
|
||||||
return (this[this.sublist] || []).map((item) => {
|
return (this[this.sublist] || []).map((item) => {
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -44,6 +44,14 @@ export default {
|
|||||||
this.$nextTick(this.init)
|
this.$nextTick(this.init)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
handler(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.isInit = false
|
||||||
|
this.$nextTick(this.init)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<modals-modal v-model="show" name="account" :width="800" :height="'unset'" :processing="processing">
|
<modals-modal ref="modal" v-model="show" name="account" :width="800" :height="'unset'" :processing="processing">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
|
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
|
||||||
@@ -8,20 +8,20 @@
|
|||||||
<form @submit.prevent="submitForm">
|
<form @submit.prevent="submitForm">
|
||||||
<div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300">
|
<div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300">
|
||||||
<div class="w-full p-8">
|
<div class="w-full p-8">
|
||||||
<div class="flex py-2 -mx-2">
|
<div class="flex py-2">
|
||||||
<div class="w-1/2 px-2">
|
<div class="w-1/2 px-2">
|
||||||
<ui-text-input-with-label v-model="newUser.username" label="Username" class="mx-2" />
|
<ui-text-input-with-label v-model="newUser.username" label="Username" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/2 px-2">
|
<div class="w-1/2 px-2">
|
||||||
<ui-text-input-with-label v-if="!isEditingRoot" v-model="newUser.password" :label="isNew ? 'Password' : 'Change Password'" type="password" class="mx-2" />
|
<ui-text-input-with-label v-if="!isEditingRoot" v-model="newUser.password" :label="isNew ? 'Password' : 'Change Password'" type="password" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex py-2">
|
<div v-show="!isEditingRoot" class="flex py-2">
|
||||||
<div class="px-2">
|
<div class="px-2 w-52">
|
||||||
<ui-input-dropdown v-model="newUser.type" label="Account Type" :disabled="isEditingRoot" :editable="false" :items="accountTypes" @input="userTypeUpdated" />
|
<ui-dropdown v-model="newUser.type" label="Account Type" :disabled="isEditingRoot" :items="accountTypes" @input="userTypeUpdated" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<div v-show="!isEditingRoot" class="flex items-center pt-4 px-2">
|
<div class="flex items-center pt-4 px-2">
|
||||||
<p class="px-3 font-semibold" :class="isEditingRoot ? 'text-gray-300' : ''">Is Active</p>
|
<p class="px-3 font-semibold" :class="isEditingRoot ? 'text-gray-300' : ''">Is Active</p>
|
||||||
<ui-toggle-switch v-model="newUser.isActive" :disabled="isEditingRoot" />
|
<ui-toggle-switch v-model="newUser.isActive" :disabled="isEditingRoot" />
|
||||||
</div>
|
</div>
|
||||||
@@ -92,7 +92,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex pt-4">
|
<div class="flex pt-4 px-2">
|
||||||
|
<ui-btn v-if="isEditingRoot" to="/account">Change Root Password</ui-btn>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn color="success" type="submit">Submit</ui-btn>
|
<ui-btn color="success" type="submit">Submit</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
@@ -116,7 +117,20 @@ export default {
|
|||||||
processing: false,
|
processing: false,
|
||||||
newUser: {},
|
newUser: {},
|
||||||
isNew: true,
|
isNew: true,
|
||||||
accountTypes: ['guest', 'user', 'admin'],
|
accountTypes: [
|
||||||
|
{
|
||||||
|
text: 'Guest',
|
||||||
|
value: 'guest'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'User',
|
||||||
|
value: 'user'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Admin',
|
||||||
|
value: 'admin'
|
||||||
|
}
|
||||||
|
],
|
||||||
tags: [],
|
tags: [],
|
||||||
loadingTags: false
|
loadingTags: false
|
||||||
}
|
}
|
||||||
@@ -124,6 +138,7 @@ export default {
|
|||||||
watch: {
|
watch: {
|
||||||
show: {
|
show: {
|
||||||
handler(newVal) {
|
handler(newVal) {
|
||||||
|
console.log('accoutn modal show change', newVal)
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
this.init()
|
this.init()
|
||||||
}
|
}
|
||||||
@@ -140,7 +155,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
title() {
|
title() {
|
||||||
return this.isNew ? 'Add New Account' : `Update Account: ${(this.account || {}).username}`
|
return this.isNew ? 'Add New Account' : `Update ${(this.account || {}).username}`
|
||||||
},
|
},
|
||||||
isEditingRoot() {
|
isEditingRoot() {
|
||||||
return this.account && this.account.type === 'root'
|
return this.account && this.account.type === 'root'
|
||||||
@@ -161,6 +176,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
close() {
|
||||||
|
// Force close when navigating - used in UsersTable
|
||||||
|
if (this.$refs.modal) this.$refs.modal.setHide()
|
||||||
|
},
|
||||||
accessAllTagsToggled(val) {
|
accessAllTagsToggled(val) {
|
||||||
if (!val && !this.newUser.itemTagsAccessible.length) {
|
if (!val && !this.newUser.itemTagsAccessible.length) {
|
||||||
this.newUser.itemTagsAccessible = this.libraries.map((l) => l.id)
|
this.newUser.itemTagsAccessible = this.libraries.map((l) => l.id)
|
||||||
|
|||||||
@@ -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,11 +1,11 @@
|
|||||||
<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">
|
||||||
<div class="w-full mb-4">
|
<div class="w-full mb-4">
|
||||||
<!-- <div class="flex items-center mb-4">
|
<div v-if="userIsAdminOrUp" class="flex items-end justify-end mb-4">
|
||||||
<p v-if="autoDownloadEpisodes">Last new episode check {{ $formatDate(lastEpisodeCheck) }}</p>
|
<!-- <p v-if="autoDownloadEpisodes">Last new episode check {{ $formatDate(lastEpisodeCheck) }}</p> -->
|
||||||
<div class="flex-grow" />
|
<ui-text-input-with-label ref="lastCheckInput" v-model="lastEpisodeCheckInput" :disabled="checkingNewEpisodes" type="datetime-local" label="Look for new episodes after this date" class="max-w-xs mr-2" />
|
||||||
<ui-btn :loading="checkingNewEpisodes" @click="checkForNewEpisodes">Check for new episodes</ui-btn>
|
<ui-btn :loading="checkingNewEpisodes" @click="checkForNewEpisodes">Check & Download New Episodes</ui-btn>
|
||||||
</div> -->
|
</div>
|
||||||
|
|
||||||
<div v-if="episodes.length" class="w-full p-4 bg-primary">
|
<div v-if="episodes.length" class="w-full p-4 bg-primary">
|
||||||
<p>Podcast Episodes</p>
|
<p>Podcast Episodes</p>
|
||||||
@@ -51,10 +51,23 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
checkingNewEpisodes: false
|
checkingNewEpisodes: false,
|
||||||
|
lastEpisodeCheckInput: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
lastEpisodeCheck: {
|
||||||
|
handler(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.setLastEpisodeCheckInput()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
userIsAdminOrUp() {
|
||||||
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
|
},
|
||||||
autoDownloadEpisodes() {
|
autoDownloadEpisodes() {
|
||||||
return !!this.media.autoDownloadEpisodes
|
return !!this.media.autoDownloadEpisodes
|
||||||
},
|
},
|
||||||
@@ -72,8 +85,22 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
checkForNewEpisodes() {
|
async checkForNewEpisodes() {
|
||||||
|
if (this.$refs.lastCheckInput) {
|
||||||
|
this.$refs.lastCheckInput.blur()
|
||||||
|
}
|
||||||
this.checkingNewEpisodes = true
|
this.checkingNewEpisodes = true
|
||||||
|
const lastEpisodeCheck = new Date(this.lastEpisodeCheckInput).valueOf()
|
||||||
|
|
||||||
|
// If last episode check changed then update it first
|
||||||
|
if (lastEpisodeCheck && lastEpisodeCheck !== this.lastEpisodeCheck) {
|
||||||
|
var updateResult = await this.$axios.$patch(`/api/items/${this.libraryItemId}/media`, { lastEpisodeCheck }).catch((error) => {
|
||||||
|
console.error('Failed to update', error)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
console.log('updateResult', updateResult)
|
||||||
|
}
|
||||||
|
|
||||||
this.$axios
|
this.$axios
|
||||||
.$get(`/api/podcasts/${this.libraryItemId}/checknew`)
|
.$get(`/api/podcasts/${this.libraryItemId}/checknew`)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@@ -91,7 +118,13 @@ export default {
|
|||||||
this.$toast.error(errorMsg)
|
this.$toast.error(errorMsg)
|
||||||
this.checkingNewEpisodes = false
|
this.checkingNewEpisodes = false
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
setLastEpisodeCheckInput() {
|
||||||
|
this.lastEpisodeCheckInput = this.lastEpisodeCheck ? this.$formatDate(this.lastEpisodeCheck, "yyyy-MM-dd'T'HH:mm") : null
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.setLastEpisodeCheckInput()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full my-4" @mousedown.prevent @mouseup.prevent>
|
<div class="w-full my-4">
|
||||||
<div class="w-full bg-primary px-6 py-1 flex items-center cursor-pointer" @click.stop="clickBar">
|
<div class="w-full bg-primary px-6 py-1 flex items-center cursor-pointer" @click.stop="clickBar">
|
||||||
<p class="pr-4">{{ title }}</p>
|
<p class="pr-4">{{ title }}</p>
|
||||||
<span class="bg-black-400 rounded-xl py-0.5 px-2 text-sm font-mono">{{ files.length }}</span>
|
<span class="bg-black-400 rounded-xl py-0.5 px-2 text-sm font-mono">{{ files.length }}</span>
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<modals-account-modal v-model="showAccountModal" :account="selectedAccount" />
|
<modals-account-modal ref="accountModal" v-model="showAccountModal" :account="selectedAccount" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -156,6 +156,10 @@ export default {
|
|||||||
this.init()
|
this.init()
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
|
if (this.$refs.accountModal) {
|
||||||
|
this.$refs.accountModal.close()
|
||||||
|
}
|
||||||
|
|
||||||
if (this.$root.socket) {
|
if (this.$root.socket) {
|
||||||
this.$root.socket.off('user_added', this.newUserAdded)
|
this.$root.socket.off('user_added', this.newUserAdded)
|
||||||
this.$root.socket.off('user_updated', this.userUpdated)
|
this.$root.socket.off('user_updated', this.userUpdated)
|
||||||
|
|||||||
@@ -14,12 +14,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-80 h-full px-2 flex items-center">
|
<div class="flex-grow max-w-md h-full px-2 flex items-center">
|
||||||
<div>
|
<div class="truncate px-1">
|
||||||
<nuxt-link :to="`/item/${book.id}`" class="truncate hover:underline">{{ bookTitle }}</nuxt-link>
|
<nuxt-link :to="`/item/${book.id}`" class="truncate hover:underline">{{ bookTitle }}</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow flex items-center">
|
<div class="w-20 flex items-center">
|
||||||
<p class="font-mono text-sm">{{ bookDuration }}</p>
|
<p class="font-mono text-sm">{{ bookDuration }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative w-full" v-click-outside="clickOutsideObj">
|
<div class="relative w-full" v-click-outside="clickOutsideObj">
|
||||||
<p class="text-sm font-semibold" :class="disabled ? 'text-gray-300' : ''">{{ label }}</p>
|
<p class="text-sm font-semibold px-1" :class="disabled ? 'text-gray-300' : ''">{{ label }}</p>
|
||||||
<button type="button" :disabled="disabled" class="relative w-full border rounded shadow-sm pl-3 pr-8 py-2 text-left focus:outline-none sm:text-sm" :class="buttonClass" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
|
<button type="button" :disabled="disabled" class="relative w-full border rounded shadow-sm pl-3 pr-8 py-2 text-left focus:outline-none sm:text-sm" :class="buttonClass" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
|
||||||
<span class="flex items-center">
|
<span class="flex items-center">
|
||||||
<span class="block truncate" :class="small ? 'text-sm' : ''">{{ selectedText }}</span>
|
<span class="block truncate" :class="small ? 'text-sm' : ''">{{ selectedText }}</span>
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
+41
-18
@@ -106,12 +106,6 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (payload.serverSettings) {
|
if (payload.serverSettings) {
|
||||||
this.$store.commit('setServerSettings', payload.serverSettings)
|
|
||||||
|
|
||||||
if (payload.serverSettings.chromecastEnabled) {
|
|
||||||
console.log('Chromecast enabled import script')
|
|
||||||
require('@/plugins/chromecast.js').default(this)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start scans currently running
|
// Start scans currently running
|
||||||
@@ -167,8 +161,28 @@ export default {
|
|||||||
libraryUpdated(library) {
|
libraryUpdated(library) {
|
||||||
this.$store.commit('libraries/addUpdate', library)
|
this.$store.commit('libraries/addUpdate', library)
|
||||||
},
|
},
|
||||||
libraryRemoved(library) {
|
async libraryRemoved(library) {
|
||||||
this.$store.commit('libraries/remove', library)
|
this.$store.commit('libraries/remove', library)
|
||||||
|
|
||||||
|
// When removed currently selected library then set next accessible library
|
||||||
|
const currLibraryId = this.$store.state.libraries.currentLibraryId
|
||||||
|
if (currLibraryId === library.id) {
|
||||||
|
var nextLibrary = this.$store.getters['libraries/getNextAccessibleLibrary']
|
||||||
|
if (nextLibrary) {
|
||||||
|
await this.$store.dispatch('libraries/fetch', nextLibrary.id)
|
||||||
|
|
||||||
|
if (this.$route.name.startsWith('config')) {
|
||||||
|
// No need to refresh
|
||||||
|
} else if (this.$route.name.startsWith('library')) {
|
||||||
|
var newRoute = this.$route.path.replace(currLibraryId, nextLibrary.id)
|
||||||
|
this.$router.push(newRoute)
|
||||||
|
} else {
|
||||||
|
this.$router.push(`/library/${nextLibrary.id}`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('User has no accessible libraries')
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
libraryItemAdded(libraryItem) {
|
libraryItemAdded(libraryItem) {
|
||||||
// this.$store.commit('libraries/updateFilterDataWithAudiobook', libraryItem)
|
// this.$store.commit('libraries/updateFilterDataWithAudiobook', libraryItem)
|
||||||
@@ -485,6 +499,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 +536,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)
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export default {
|
|||||||
bookCoverAspectRatio: this.bookCoverAspectRatio,
|
bookCoverAspectRatio: this.bookCoverAspectRatio,
|
||||||
bookshelfView: this.bookshelfView
|
bookshelfView: this.bookshelfView
|
||||||
}
|
}
|
||||||
if (this.entityName === 'series-books') props.showSequence = true
|
|
||||||
if (this.entityName === 'books') {
|
if (this.entityName === 'books') {
|
||||||
props.filterBy = this.filterBy
|
props.filterBy = this.filterBy
|
||||||
props.orderBy = this.orderBy
|
props.orderBy = this.orderBy
|
||||||
|
|||||||
@@ -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.2",
|
"version": "2.0.8",
|
||||||
"description": "Audiobook manager and player",
|
"description": "Audiobook manager and player",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -15,8 +15,8 @@
|
|||||||
|
|
||||||
<div class="w-full h-px bg-primary my-4" />
|
<div class="w-full h-px bg-primary my-4" />
|
||||||
|
|
||||||
<p class="mb-4 text-lg">Change Password</p>
|
<p v-if="!isGuest" class="mb-4 text-lg">Change Password</p>
|
||||||
<form @submit.prevent="submitChangePassword">
|
<form v-if="!isGuest" @submit.prevent="submitChangePassword">
|
||||||
<ui-text-input-with-label v-model="password" :disabled="changingPassword" type="password" label="Password" class="my-2" />
|
<ui-text-input-with-label v-model="password" :disabled="changingPassword" type="password" label="Password" class="my-2" />
|
||||||
<ui-text-input-with-label v-model="newPassword" :disabled="changingPassword" type="password" label="New Password" class="my-2" />
|
<ui-text-input-with-label v-model="newPassword" :disabled="changingPassword" type="password" label="New Password" class="my-2" />
|
||||||
<ui-text-input-with-label v-model="confirmPassword" :disabled="changingPassword" type="password" label="Confirm Password" class="my-2" />
|
<ui-text-input-with-label v-model="confirmPassword" :disabled="changingPassword" type="password" label="Confirm Password" class="my-2" />
|
||||||
@@ -60,6 +60,9 @@ export default {
|
|||||||
},
|
},
|
||||||
isRoot() {
|
isRoot() {
|
||||||
return this.usertype === 'root'
|
return this.usertype === 'root'
|
||||||
|
},
|
||||||
|
isGuest() {
|
||||||
|
return this.usertype === 'guest'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -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 })) : []
|
||||||
|
|||||||
@@ -95,14 +95,16 @@
|
|||||||
<p class="ml-4">Book has no audio tracks but has valid ebook files. The e-reader is experimental and can be turned on in config.</p>
|
<p class="ml-4">Book has no audio tracks but has valid ebook files. The e-reader is experimental and can be turned on in config.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Podcast episode downloads queue -->
|
||||||
<div v-if="episodeDownloadsQueued.length" class="px-4 py-2 mt-4 bg-info bg-opacity-40 text-sm font-semibold rounded-md text-gray-100 relative max-w-max mx-auto md:mx-0">
|
<div v-if="episodeDownloadsQueued.length" class="px-4 py-2 mt-4 bg-info bg-opacity-40 text-sm font-semibold rounded-md text-gray-100 relative max-w-max mx-auto md:mx-0">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<p class="text-sm py-1">{{ episodeDownloadsQueued.length }} Episode{{ episodeDownloadsQueued.length === 1 ? '' : 's' }} queued for download</p>
|
<p class="text-sm py-1">{{ episodeDownloadsQueued.length }} Episode{{ episodeDownloadsQueued.length === 1 ? '' : 's' }} queued for download</p>
|
||||||
|
|
||||||
<span class="material-icons hover:text-error text-xl ml-3 cursor-pointer" @click="clearDownloadQueue">close</span>
|
<span v-if="userIsAdminOrUp" class="material-icons hover:text-error text-xl ml-3 cursor-pointer" @click="clearDownloadQueue">close</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Podcast episodes currently downloading -->
|
||||||
<div v-if="episodesDownloading.length" class="px-4 py-2 mt-4 bg-success bg-opacity-20 text-sm font-semibold rounded-md text-gray-100 relative max-w-max mx-auto md:mx-0">
|
<div v-if="episodesDownloading.length" class="px-4 py-2 mt-4 bg-success bg-opacity-20 text-sm font-semibold rounded-md text-gray-100 relative max-w-max mx-auto md:mx-0">
|
||||||
<div v-for="episode in episodesDownloading" :key="episode.id" class="flex items-center">
|
<div v-for="episode in episodesDownloading" :key="episode.id" class="flex items-center">
|
||||||
<widgets-loading-spinner />
|
<widgets-loading-spinner />
|
||||||
@@ -150,7 +152,8 @@
|
|||||||
<ui-icon-btn icon="collections_bookmark" class="mx-0.5" outlined @click="collectionsClick" />
|
<ui-icon-btn icon="collections_bookmark" class="mx-0.5" outlined @click="collectionsClick" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-tooltip v-if="isPodcast" text="Find Episodes" direction="top">
|
<!-- Only admin or root user can download new episodes -->
|
||||||
|
<ui-tooltip v-if="isPodcast && userIsAdminOrUp" text="Find Episodes" direction="top">
|
||||||
<ui-icon-btn icon="search" class="mx-0.5" :loading="fetchingRSSFeed" outlined @click="findEpisodesClick" />
|
<ui-icon-btn icon="search" class="mx-0.5" :loading="fetchingRSSFeed" outlined @click="findEpisodesClick" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -159,7 +162,13 @@
|
|||||||
<p class="text-base text-gray-100 whitespace-pre-line">{{ description }}</p>
|
<p class="text-base text-gray-100 whitespace-pre-line">{{ description }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<widgets-audiobook-data v-if="tracks.length" :library-item-id="libraryItemId" :media="media" />
|
<div v-if="invalidAudioFiles.length" class="bg-error border-red-800 shadow-md p-4">
|
||||||
|
<p class="text-sm mb-2">Invalid audio files</p>
|
||||||
|
|
||||||
|
<p v-for="audioFile in invalidAudioFiles" :key="audioFile.id" class="text-xs pl-2">- {{ audioFile.metadata.filename }} ({{ audioFile.error }})</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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" />
|
||||||
|
|
||||||
@@ -204,6 +213,12 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
userIsAdminOrUp() {
|
||||||
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
|
},
|
||||||
|
isFile() {
|
||||||
|
return this.libraryItem.isFile
|
||||||
|
},
|
||||||
coverAspectRatio() {
|
coverAspectRatio() {
|
||||||
return this.$store.getters['getServerSetting']('coverAspectRatio')
|
return this.$store.getters['getServerSetting']('coverAspectRatio')
|
||||||
},
|
},
|
||||||
@@ -228,6 +243,10 @@ export default {
|
|||||||
isInvalid() {
|
isInvalid() {
|
||||||
return this.libraryItem.isInvalid
|
return this.libraryItem.isInvalid
|
||||||
},
|
},
|
||||||
|
invalidAudioFiles() {
|
||||||
|
if (this.isPodcast) return []
|
||||||
|
return this.libraryItem.media.audioFiles.filter((af) => af.invalid)
|
||||||
|
},
|
||||||
showPlayButton() {
|
showPlayButton() {
|
||||||
if (this.isMissing || this.isInvalid) return false
|
if (this.isMissing || this.isInvalid) return false
|
||||||
if (this.isPodcast) return this.podcastEpisodes.length
|
if (this.isPodcast) return this.podcastEpisodes.length
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export default {
|
|||||||
return redirect(`/library/${libraryId}`)
|
return redirect(`/library/${libraryId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
var series = await app.$axios.$get(`/api/series/${params.id}`).catch((error) => {
|
var series = await app.$axios.$get(`/api/series/${params.id}?include=progress`).catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
@@ -33,7 +33,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
series: series.name,
|
series,
|
||||||
seriesId: params.id
|
seriesId: params.id
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+11
-4
@@ -48,8 +48,15 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
setUser(user, defaultLibraryId) {
|
setUser({ user, userDefaultLibraryId, serverSettings }) {
|
||||||
this.$store.commit('libraries/setCurrentLibrary', defaultLibraryId)
|
this.$store.commit('setServerSettings', serverSettings)
|
||||||
|
|
||||||
|
if (serverSettings.chromecastEnabled) {
|
||||||
|
console.log('Chromecast enabled import script')
|
||||||
|
require('@/plugins/chromecast.js').default(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$store.commit('libraries/setCurrentLibrary', userDefaultLibraryId)
|
||||||
this.$store.commit('user/setUser', user)
|
this.$store.commit('user/setUser', user)
|
||||||
},
|
},
|
||||||
async submitForm() {
|
async submitForm() {
|
||||||
@@ -69,7 +76,7 @@ export default {
|
|||||||
if (authRes && authRes.error) {
|
if (authRes && authRes.error) {
|
||||||
this.error = authRes.error
|
this.error = authRes.error
|
||||||
} else if (authRes) {
|
} else if (authRes) {
|
||||||
this.setUser(authRes.user, authRes.userDefaultLibraryId)
|
this.setUser(authRes)
|
||||||
}
|
}
|
||||||
this.processing = false
|
this.processing = false
|
||||||
},
|
},
|
||||||
@@ -87,7 +94,7 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
this.setUser(res.user, res.userDefaultLibraryId)
|
this.setUser(res)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
|||||||
@@ -53,8 +53,8 @@
|
|||||||
</widgets-alert>
|
</widgets-alert>
|
||||||
|
|
||||||
<!-- Item Upload cards -->
|
<!-- Item Upload cards -->
|
||||||
<template v-for="(item, index) in items">
|
<template v-for="item in items">
|
||||||
<cards-item-upload-card :ref="`itemCard-${item.index}`" :key="index" :media-type="selectedLibraryMediaType" :item="item" :processing="processing" @remove="removeItem(item)" />
|
<cards-item-upload-card :ref="`itemCard-${item.index}`" :key="item.index" :media-type="selectedLibraryMediaType" :item="item" :processing="processing" @remove="removeItem(item)" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Upload/Reset btns -->
|
<!-- Upload/Reset btns -->
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -29,6 +29,19 @@ export const getters = {
|
|||||||
var library = state.libraries.find(l => l.id === libraryId)
|
var library = state.libraries.find(l => l.id === libraryId)
|
||||||
if (!library) return null
|
if (!library) return null
|
||||||
return library.provider
|
return library.provider
|
||||||
|
},
|
||||||
|
getNextAccessibleLibrary: (state, getters, rootState, rootGetters) => {
|
||||||
|
var librariesSorted = getters['getSortedLibraries']()
|
||||||
|
if (!librariesSorted.length) return null
|
||||||
|
|
||||||
|
var canAccessAllLibraries = rootGetters['user/getUserCanAccessAllLibraries']
|
||||||
|
var userAccessibleLibraries = rootGetters['user/getLibrariesAccessible']
|
||||||
|
if (canAccessAllLibraries) return librariesSorted[0]
|
||||||
|
librariesSorted = librariesSorted.filter((lib) => {
|
||||||
|
return userAccessibleLibraries.includes(lib.id)
|
||||||
|
})
|
||||||
|
if (!librariesSorted.length) return null
|
||||||
|
return librariesSorted[0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export const state = () => ({
|
|||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
getIsRoot: (state) => state.user && state.user.type === 'root',
|
getIsRoot: (state) => state.user && state.user.type === 'root',
|
||||||
|
getIsAdminOrUp: (state) => state.user && (state.user.type === 'admin' || state.user.type === 'root'),
|
||||||
getToken: (state) => {
|
getToken: (state) => {
|
||||||
return state.user ? state.user.token : null
|
return state.user ? state.user.token : null
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|||||||
+49
-1
@@ -1 +1,49 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 6702.73 1277.37"><defs><style>.cls-1{fill:#fff;}.cls-2{fill:url(#linear-gradient);}.cls-3{font-size:800px;fill:#c9c9c9;font-family:GentiumBookBasic, Gentium Book Basic;}.cls-4{font-size:420px;fill:#474747;font-family:GentiumBasic, Gentium Basic;}</style><linearGradient id="linear-gradient" x1="617.37" y1="20.7" x2="617.37" y2="1216.56" gradientUnits="userSpaceOnUse"><stop offset="0.32" stop-color="#cd9d49"/><stop offset="0.99" stop-color="#875d27"/></linearGradient></defs><title>bgAsset 6</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_2-2" data-name="Layer 2"><g id="Layer_4" data-name="Layer 4"><g id="Layer_5" data-name="Layer 5"><circle class="cls-1" cx="618.63" cy="618.63" r="618.63"/></g><circle class="cls-2" cx="617.37" cy="618.63" r="597.93"/></g><path class="cls-1" d="M1005.57,574.08c-4.84-4-12.37-10-22.58-17v-79.2c0-201.93-163.69-365.63-365.62-365.63h0c-201.93,0-365.63,163.7-365.63,365.63v79.2c-10.21,7-17.74,13-22.58,17A18.15,18.15,0,0,0,222.63,588v94.89a18.15,18.15,0,0,0,6.53,14c11.29,9.4,37.19,29.1,77.52,49.31v9.22c0,24.88,16,45,35.84,45h0c19.79,0,35.84-20.16,35.84-45V527.83c0-24.87-16.05-45-35.84-45h0c-19,0-34.48,18.51-35.75,41.94l-.09,0v-46.9c0-171.59,139.1-310.69,310.69-310.69h0c171.58,0,310.68,139.1,310.68,310.69v46.9l-.08,0c-1.27-23.43-16.79-41.94-35.76-41.94h0c-19.79,0-35.83,20.17-35.83,45V755.4c0,24.88,16,45,35.83,45h0c19.8,0,35.84-20.16,35.84-45v-9.22c40.33-20.21,66.24-39.91,77.52-49.31a18.15,18.15,0,0,0,6.53-14V588A18.15,18.15,0,0,0,1005.57,574.08Z"/><path class="cls-1" d="M489.87,969.71a43.31,43.31,0,0,0,43.3-43.3V441.64a43.3,43.3,0,0,0-43.3-43.29H445.15a43.3,43.3,0,0,0-43.3,43.29V926.41a43.31,43.31,0,0,0,43.3,43.3Zm-71.69-455.1h98.67v10.31H418.18Z"/><path class="cls-1" d="M639.73,969.71A43.3,43.3,0,0,0,683,926.41V441.64a43.29,43.29,0,0,0-43.29-43.29H595a43.29,43.29,0,0,0-43.29,43.29V926.41A43.3,43.3,0,0,0,595,969.71ZM568,514.61H666.7v10.31H568Z"/><path class="cls-1" d="M789.59,969.71a43.3,43.3,0,0,0,43.29-43.3V441.64a43.29,43.29,0,0,0-43.29-43.29H744.86a43.3,43.3,0,0,0-43.3,43.29V926.41a43.31,43.31,0,0,0,43.3,43.3Zm-71.7-455.1h98.67v10.31H717.89Z"/><rect class="cls-1" x="294.5" y="984.69" width="645.74" height="65.25" rx="32.63"/></g><g id="Layer_6" data-name="Layer 6"><text class="cls-3" transform="translate(1492.27 670.42)">audiobookshelf</text><text class="cls-4" transform="translate(1492.27 1128.69)">self-hosted audiobook server</text></g></g></svg>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 24.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 6702.7 1277.4" style="enable-background:new 0 0 6702.7 1277.4;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#FFFFFF;}
|
||||||
|
.st1{fill:url(#SVGID_1_);}
|
||||||
|
.st2{fill:#C9C9C9;}
|
||||||
|
.st3{font-family:'GentiumBookBasic';}
|
||||||
|
.st4{font-size:800px;}
|
||||||
|
.st5{fill:#474747;}
|
||||||
|
.st6{font-family:'GentiumBasic';}
|
||||||
|
.st7{font-size:305px;}
|
||||||
|
</style>
|
||||||
|
<title>bgAsset 6</title>
|
||||||
|
<g id="Layer_2_1_">
|
||||||
|
<g id="Layer_2-2">
|
||||||
|
<g id="Layer_4">
|
||||||
|
<g id="Layer_5">
|
||||||
|
<circle class="st0" cx="618.6" cy="618.6" r="618.6"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="617.37" y1="1257.3" x2="617.37" y2="61.4399" gradientTransform="matrix(1 0 0 -1 0 1278)">
|
||||||
|
<stop offset="0.32" style="stop-color:#CD9D49"/>
|
||||||
|
<stop offset="0.99" style="stop-color:#875D27"/>
|
||||||
|
</linearGradient>
|
||||||
|
<circle class="st1" cx="617.4" cy="618.6" r="597.9"/>
|
||||||
|
</g>
|
||||||
|
<path class="st0" d="M1005.6,574.1c-4.8-4-12.4-10-22.6-17v-79.2c0-201.9-163.7-365.6-365.6-365.6l0,0
|
||||||
|
c-201.9,0-365.6,163.7-365.6,365.6v79.2c-10.2,7-17.7,13-22.6,17c-4.1,3.4-6.5,8.5-6.5,13.9v94.9c0,5.4,2.4,10.5,6.5,14
|
||||||
|
c11.3,9.4,37.2,29.1,77.5,49.3v9.2c0,24.9,16,45,35.8,45l0,0c19.8,0,35.8-20.2,35.8-45V527.8c0-24.9-16-45-35.8-45l0,0
|
||||||
|
c-19,0-34.5,18.5-35.8,41.9h-0.1v-46.9c0-171.6,139.1-310.7,310.7-310.7l0,0C789,167.2,928,306.3,928,477.9v46.9H928
|
||||||
|
c-1.3-23.4-16.8-41.9-35.8-41.9l0,0c-19.8,0-35.8,20.2-35.8,45v227.6c0,24.9,16,45,35.8,45l0,0c19.8,0,35.8-20.2,35.8-45v-9.2
|
||||||
|
c40.3-20.2,66.2-39.9,77.5-49.3c4.2-3.5,6.5-8.6,6.5-14V588C1012.1,582.6,1009.7,577.5,1005.6,574.1z"/>
|
||||||
|
<path class="st0" d="M489.9,969.7c23.9,0,43.3-19.4,43.3-43.3V441.6c0-23.9-19.4-43.3-43.3-43.3h-44.7
|
||||||
|
c-23.9,0-43.3,19.4-43.3,43.3v484.8c0,23.9,19.4,43.3,43.3,43.3L489.9,969.7z M418.2,514.6h98.7v10.3h-98.7V514.6z"/>
|
||||||
|
<path class="st0" d="M639.7,969.7c23.9,0,43.3-19.4,43.3-43.3V441.6c0-23.9-19.4-43.3-43.3-43.3H595c-23.9,0-43.3,19.4-43.3,43.3
|
||||||
|
v484.8c0,23.9,19.4,43.3,43.3,43.3H639.7z M568,514.6h98.7v10.3H568V514.6z"/>
|
||||||
|
<path class="st0" d="M789.6,969.7c23.9,0,43.3-19.4,43.3-43.3V441.6c0-23.9-19.4-43.3-43.3-43.3h-44.7
|
||||||
|
c-23.9,0-43.3,19.4-43.3,43.3v484.8c0,23.9,19.4,43.3,43.3,43.3L789.6,969.7z M717.9,514.6h98.7v10.3h-98.7V514.6z"/>
|
||||||
|
<path class="st0" d="M327.1,984.7h580.5c18,0,32.6,14.6,32.6,32.6v0c0,18-14.6,32.6-32.6,32.6H327.1c-18,0-32.6-14.6-32.6-32.6v0
|
||||||
|
C294.5,999.3,309.1,984.7,327.1,984.7z"/>
|
||||||
|
</g>
|
||||||
|
<g id="Layer_6">
|
||||||
|
<text transform="matrix(1 0 0 1 1492.27 735.42)" class="st2 st3 st4">audiobookshelf</text>
|
||||||
|
<text id="self-hosted_audiobook_and_podcast_server" transform="matrix(1 0 0 1 1492.27 1103.6899)" class="st5 st6 st7">self-hosted audiobook and podcast server</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.9 KiB |
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.0.2",
|
"version": "2.0.8",
|
||||||
"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": {
|
||||||
|
|||||||
@@ -14,20 +14,23 @@
|
|||||||
|
|
||||||
# About
|
# About
|
||||||
|
|
||||||
Audiobookshelf is a self-hosted audiobook server for managing and playing your audiobooks.
|
Audiobookshelf is a self-hosted audiobook and podcast server.
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
* Fully **open-source**, including the [android & iOS app](https://github.com/advplyr/audiobookshelf-app) *(in beta)*
|
* Fully **open-source**, including the [android & iOS app](https://github.com/advplyr/audiobookshelf-app) *(in beta)*
|
||||||
* Stream all audiobook formats on the fly
|
* Stream all audio formats on the fly
|
||||||
|
* Search and add podcasts to download episodes w/ auto-download
|
||||||
* Multi-user support w/ custom permissions
|
* Multi-user support w/ custom permissions
|
||||||
* Keeps progress per user and syncs across devices
|
* Keeps progress per user and syncs across devices
|
||||||
* Auto-detects library updates, no need to re-scan
|
* Auto-detects library updates, no need to re-scan
|
||||||
* Upload audiobooks w/ bulk upload drag and drop folders
|
* Upload books and podcasts w/ bulk upload drag and drop folders
|
||||||
* Backup your metadata + automated daily backups
|
* Backup your metadata + automated daily backups
|
||||||
* Progressive Web App (PWA)
|
* Progressive Web App (PWA)
|
||||||
* Chromecast support on the web app
|
* Chromecast support on the web app and android app
|
||||||
* Fetch metadata and cover art from several sources
|
* Fetch metadata and cover art from several sources
|
||||||
|
* Basic ebook support and e-reader *(experimental)*
|
||||||
|
* Merge your audio files into a single m4b w/ metadata and embedded cover *(experimental)*
|
||||||
|
|
||||||
Is there a feature you are looking for? [Suggest it](https://github.com/advplyr/audiobookshelf/issues/new/choose)
|
Is there a feature you are looking for? [Suggest it](https://github.com/advplyr/audiobookshelf/issues/new/choose)
|
||||||
|
|
||||||
@@ -71,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
|
||||||
@@ -80,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:
|
||||||
|
|||||||
+10
-5
@@ -100,6 +100,14 @@ class Auth {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getUserLoginResponsePayload(user) {
|
||||||
|
return {
|
||||||
|
user: user.toJSONForBrowser(),
|
||||||
|
userDefaultLibraryId: user.getDefaultLibraryId(this.db.libraries),
|
||||||
|
serverSettings: this.db.serverSettings.toJSON()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async login(req, res) {
|
async login(req, res) {
|
||||||
var username = (req.body.username || '').toLowerCase()
|
var username = (req.body.username || '').toLowerCase()
|
||||||
var password = req.body.password || ''
|
var password = req.body.password || ''
|
||||||
@@ -120,17 +128,14 @@ class Auth {
|
|||||||
if (password) {
|
if (password) {
|
||||||
return res.status(401).send('Invalid root password (hint: there is none)')
|
return res.status(401).send('Invalid root password (hint: there is none)')
|
||||||
} else {
|
} else {
|
||||||
return res.json({ user: user.toJSONForBrowser(), userDefaultLibraryId: user.getDefaultLibraryId(this.db.libraries) })
|
return res.json(this.getUserLoginResponsePayload(user))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check password match
|
// Check password match
|
||||||
var compare = await bcrypt.compare(password, user.pash)
|
var compare = await bcrypt.compare(password, user.pash)
|
||||||
if (compare) {
|
if (compare) {
|
||||||
res.json({
|
res.json(this.getUserLoginResponsePayload(user))
|
||||||
user: user.toJSONForBrowser(),
|
|
||||||
userDefaultLibraryId: user.getDefaultLibraryId(this.db.libraries)
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
Logger.debug(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit}`)
|
Logger.debug(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit}`)
|
||||||
if (req.rateLimit.remaining <= 2) {
|
if (req.rateLimit.remaining <= 2) {
|
||||||
|
|||||||
+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]) {
|
||||||
|
|||||||
+14
-3
@@ -10,6 +10,7 @@ const { version } = require('../package.json')
|
|||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
const dbMigration = require('./utils/dbMigration')
|
const dbMigration = require('./utils/dbMigration')
|
||||||
|
const filePerms = require('./utils/filePerms')
|
||||||
const Logger = require('./Logger')
|
const Logger = require('./Logger')
|
||||||
|
|
||||||
// Classes
|
// Classes
|
||||||
@@ -46,9 +47,18 @@ class Server {
|
|||||||
global.MetadataPath = global.MetadataPath.replace(/\\/g, '/')
|
global.MetadataPath = global.MetadataPath.replace(/\\/g, '/')
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.ensureDirSync(global.ConfigPath, 0o774)
|
if (!fs.pathExistsSync(global.ConfigPath)) {
|
||||||
fs.ensureDirSync(global.MetadataPath, 0o774)
|
fs.mkdirSync(global.ConfigPath)
|
||||||
fs.ensureDirSync(global.AudiobookPath, 0o774)
|
filePerms.setDefaultDirSync(global.ConfigPath, false)
|
||||||
|
}
|
||||||
|
if (!fs.pathExistsSync(global.MetadataPath)) {
|
||||||
|
fs.mkdirSync(global.MetadataPath)
|
||||||
|
filePerms.setDefaultDirSync(global.MetadataPath, false)
|
||||||
|
}
|
||||||
|
if (!fs.pathExistsSync(global.AudiobookPath)) {
|
||||||
|
fs.mkdirSync(global.AudiobookPath)
|
||||||
|
filePerms.setDefaultDirSync(global.AudiobookPath, false)
|
||||||
|
}
|
||||||
|
|
||||||
this.db = new Db()
|
this.db = new Db()
|
||||||
this.watcher = new Watcher()
|
this.watcher = new Watcher()
|
||||||
@@ -399,6 +409,7 @@ class Server {
|
|||||||
await this.db.updateEntity('user', user)
|
await this.db.updateEntity('user', user)
|
||||||
|
|
||||||
const initialPayload = {
|
const initialPayload = {
|
||||||
|
// TODO: this is sent with user auth now, update mobile app to use that then remove this
|
||||||
serverSettings: this.db.serverSettings.toJSON(),
|
serverSettings: this.db.serverSettings.toJSON(),
|
||||||
audiobookPath: global.AudiobookPath,
|
audiobookPath: global.AudiobookPath,
|
||||||
metadataPath: global.MetadataPath,
|
metadataPath: global.MetadataPath,
|
||||||
|
|||||||
@@ -201,7 +201,6 @@ class LibraryController {
|
|||||||
libraryItems = naturalSort(libraryItems).by(sortArray)
|
libraryItems = naturalSort(libraryItems).by(sortArray)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Potentially implement collapse series again
|
|
||||||
if (payload.collapseseries) {
|
if (payload.collapseseries) {
|
||||||
libraryItems = libraryHelpers.collapseBookSeries(libraryItems)
|
libraryItems = libraryHelpers.collapseBookSeries(libraryItems)
|
||||||
payload.total = libraryItems.length
|
payload.total = libraryItems.length
|
||||||
@@ -226,6 +225,22 @@ class LibraryController {
|
|||||||
res.json(payload)
|
res.json(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async removeLibraryItemsWithIssues(req, res) {
|
||||||
|
var libraryItemsWithIssues = req.libraryItems.filter(li => li.hasIssues)
|
||||||
|
if (!libraryItemsWithIssues.length) {
|
||||||
|
Logger.warn(`[LibraryController] No library items have issues`)
|
||||||
|
return res.sendStatus(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.info(`[LibraryController] Removing ${libraryItemsWithIssues.length} items with issues`)
|
||||||
|
for (const libraryItem of libraryItemsWithIssues) {
|
||||||
|
Logger.info(`[LibraryController] Removing library item "${libraryItem.media.metadata.title}"`)
|
||||||
|
await this.handleDeleteLibraryItem(libraryItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.sendStatus(200)
|
||||||
|
}
|
||||||
|
|
||||||
// api/libraries/:id/series
|
// api/libraries/:id/series
|
||||||
async getAllSeriesForLibrary(req, res) {
|
async getAllSeriesForLibrary(req, res) {
|
||||||
var libraryItems = req.libraryItems
|
var libraryItems = req.libraryItems
|
||||||
@@ -293,97 +308,13 @@ class LibraryController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// api/libraries/:id/personalized
|
// api/libraries/:id/personalized
|
||||||
async getLibraryUserPersonalized(req, res) {
|
// New and improved personalized call only loops through library items once
|
||||||
var mediaType = req.library.mediaType
|
async getLibraryUserPersonalizedOptimal(req, res) {
|
||||||
var isPodcastLibrary = mediaType == 'podcast'
|
const mediaType = req.library.mediaType
|
||||||
var libraryItems = req.libraryItems
|
const libraryItems = req.libraryItems
|
||||||
var limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12
|
const limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 10
|
||||||
var minified = req.query.minified === '1'
|
|
||||||
|
|
||||||
var itemsWithUserProgress = libraryHelpers.getMediaProgressWithItems(req.user, libraryItems)
|
|
||||||
var categories = [
|
|
||||||
{
|
|
||||||
id: 'continue-listening',
|
|
||||||
label: 'Continue Listening',
|
|
||||||
type: isPodcastLibrary ? 'episode' : req.library.mediaType,
|
|
||||||
entities: libraryHelpers.getItemsMostRecentlyListened(itemsWithUserProgress, limitPerShelf, minified)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'recently-added',
|
|
||||||
label: 'Recently Added',
|
|
||||||
type: req.library.mediaType,
|
|
||||||
entities: libraryHelpers.getItemsMostRecentlyAdded(libraryItems, limitPerShelf, minified)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'listen-again',
|
|
||||||
label: 'Listen Again',
|
|
||||||
type: isPodcastLibrary ? 'episode' : req.library.mediaType,
|
|
||||||
entities: libraryHelpers.getItemsMostRecentlyFinished(itemsWithUserProgress, limitPerShelf, minified)
|
|
||||||
}
|
|
||||||
].filter(cats => { // Remove categories with no items
|
|
||||||
return cats.entities.length
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
// New Series section
|
|
||||||
// TODO: optimize and move to libraryHelpers
|
|
||||||
if (!isPodcastLibrary) {
|
|
||||||
var series = this.db.series.map(se => {
|
|
||||||
var books = libraryItems.filter(li => li.media.metadata.hasSeries(se.id))
|
|
||||||
if (!books.length) return null
|
|
||||||
books = books.map(b => {
|
|
||||||
var json = b.toJSONMinified()
|
|
||||||
json.sequence = b.media.metadata.getSeriesSequence(se.id)
|
|
||||||
return json
|
|
||||||
})
|
|
||||||
books = naturalSort(books).asc(b => b.sequence)
|
|
||||||
return {
|
|
||||||
id: se.id,
|
|
||||||
name: se.name,
|
|
||||||
type: 'series',
|
|
||||||
addedAt: se.addedAt,
|
|
||||||
books
|
|
||||||
}
|
|
||||||
}).filter(se => se).sort((a, b) => a.addedAt - b.addedAt).slice(0, 5)
|
|
||||||
|
|
||||||
if (series.length) {
|
|
||||||
categories.push({
|
|
||||||
id: 'recent-series',
|
|
||||||
label: 'Recent Series',
|
|
||||||
type: 'series',
|
|
||||||
entities: series
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var authors = this.db.authors.map(author => {
|
|
||||||
var books = libraryItems.filter(li => li.media.metadata.hasAuthor(author.id))
|
|
||||||
if (!books.length) return null
|
|
||||||
// books = books.map(b => b.toJSONMinified())
|
|
||||||
return {
|
|
||||||
...author.toJSON(),
|
|
||||||
numBooks: books.length
|
|
||||||
}
|
|
||||||
}).filter(au => au).sort((a, b) => a.addedAt - b.addedAt).slice(0, 10)
|
|
||||||
if (authors.length) {
|
|
||||||
categories.push({
|
|
||||||
id: 'newest-authors',
|
|
||||||
label: 'Newest Authors',
|
|
||||||
type: 'authors',
|
|
||||||
entities: authors
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
var episodesRecentlyAdded = libraryHelpers.getEpisodesRecentlyAdded(libraryItems, limitPerShelf, minified)
|
|
||||||
if (episodesRecentlyAdded.length) {
|
|
||||||
categories.splice(1, 0, {
|
|
||||||
id: 'episodes-recently-added',
|
|
||||||
label: 'Newest Episodes',
|
|
||||||
type: 'episode',
|
|
||||||
entities: episodesRecentlyAdded
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const categories = libraryHelpers.buildPersonalizedShelves(req.user, libraryItems, mediaType, this.db.series, this.db.authors, limitPerShelf)
|
||||||
res.json(categories)
|
res.json(categories)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -521,7 +452,8 @@ class LibraryController {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
res.json(Object.values(authors))
|
|
||||||
|
res.json(naturalSort(Object.values(authors)).asc(au => au.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
async matchAll(req, res) {
|
async matchAll(req, res) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -133,6 +133,10 @@ class MeController {
|
|||||||
|
|
||||||
// PATCH: api/me/password
|
// PATCH: api/me/password
|
||||||
updatePassword(req, res) {
|
updatePassword(req, res) {
|
||||||
|
if (req.user.isGuest) {
|
||||||
|
Logger.error(`[MeController] Guest user attempted to change password`, req.user.username)
|
||||||
|
return res.sendStatus(500)
|
||||||
|
}
|
||||||
this.auth.userChangePassword(req, res)
|
this.auth.userChangePassword(req, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -230,7 +230,12 @@ class MiscController {
|
|||||||
Logger.error('Invalid user in authorize')
|
Logger.error('Invalid user in authorize')
|
||||||
return res.sendStatus(401)
|
return res.sendStatus(401)
|
||||||
}
|
}
|
||||||
res.json({ user: req.user, userDefaultLibraryId: req.user.getDefaultLibraryId(this.db.libraries) })
|
const userResponse = {
|
||||||
|
user: req.user,
|
||||||
|
userDefaultLibraryId: req.user.getDefaultLibraryId(this.db.libraries),
|
||||||
|
serverSettings: this.db.serverSettings.toJSON()
|
||||||
|
}
|
||||||
|
res.json(userResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllTags(req, res) {
|
getAllTags(req, res) {
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ const filePerms = require('../utils/filePerms')
|
|||||||
class PodcastController {
|
class PodcastController {
|
||||||
|
|
||||||
async create(req, res) {
|
async create(req, res) {
|
||||||
if (!req.user.isRoot) {
|
if (!req.user.isAdminOrUp) {
|
||||||
Logger.error(`[PodcastController] Non-root user attempted to create podcast`, req.user)
|
Logger.error(`[PodcastController] Non-admin user attempted to create podcast`, req.user)
|
||||||
return res.sendStatus(500)
|
return res.sendStatus(500)
|
||||||
}
|
}
|
||||||
const payload = req.body
|
const payload = req.body
|
||||||
@@ -115,24 +115,33 @@ class PodcastController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async checkNewEpisodes(req, res) {
|
async checkNewEpisodes(req, res) {
|
||||||
|
if (!req.user.isAdminOrUp) {
|
||||||
|
Logger.error(`[PodcastController] Non-admin user attempted to check/download episodes`, req.user)
|
||||||
|
return res.sendStatus(500)
|
||||||
|
}
|
||||||
|
|
||||||
var libraryItem = this.db.getLibraryItem(req.params.id)
|
var libraryItem = this.db.getLibraryItem(req.params.id)
|
||||||
if (!libraryItem || libraryItem.mediaType !== 'podcast') {
|
if (!libraryItem || libraryItem.mediaType !== 'podcast') {
|
||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
}
|
}
|
||||||
|
if (!req.user.checkCanAccessLibrary(libraryItem.libraryId)) {
|
||||||
|
Logger.error(`[PodcastController] User attempted to check/download episodes for a library without permission`, req.user)
|
||||||
|
return res.sendStatus(500)
|
||||||
|
}
|
||||||
if (!libraryItem.media.metadata.feedUrl) {
|
if (!libraryItem.media.metadata.feedUrl) {
|
||||||
Logger.error(`[PodcastController] checkNewEpisodes no feed url for item ${libraryItem.id}`)
|
Logger.error(`[PodcastController] checkNewEpisodes no feed url for item ${libraryItem.id}`)
|
||||||
return res.status(500).send('Podcast has no rss feed url')
|
return res.status(500).send('Podcast has no rss feed url')
|
||||||
}
|
}
|
||||||
|
|
||||||
var newEpisodes = await this.podcastManager.checkPodcastForNewEpisodes(libraryItem)
|
var newEpisodes = await this.podcastManager.checkAndDownloadNewEpisodes(libraryItem)
|
||||||
res.json({
|
res.json({
|
||||||
episodes: newEpisodes || []
|
episodes: newEpisodes || []
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
clearEpisodeDownloadQueue(req, res) {
|
clearEpisodeDownloadQueue(req, res) {
|
||||||
if (!req.user.canUpdate) {
|
if (!req.user.isAdminOrUp) {
|
||||||
Logger.error(`[PodcastController] User attempting to clear download queue without permission "${req.user.username}"`)
|
Logger.error(`[PodcastController] Non-admin user attempting to clear download queue "${req.user.username}"`)
|
||||||
return res.sendStatus(500)
|
return res.sendStatus(500)
|
||||||
}
|
}
|
||||||
this.podcastManager.clearDownloadQueue(req.params.id)
|
this.podcastManager.clearDownloadQueue(req.params.id)
|
||||||
@@ -151,11 +160,17 @@ class PodcastController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async downloadEpisodes(req, res) {
|
async downloadEpisodes(req, res) {
|
||||||
|
if (!req.user.isAdminOrUp) {
|
||||||
|
Logger.error(`[PodcastController] Non-admin user attempted to download episodes`, req.user)
|
||||||
|
return res.sendStatus(500)
|
||||||
|
}
|
||||||
|
|
||||||
var libraryItem = this.db.getLibraryItem(req.params.id)
|
var libraryItem = this.db.getLibraryItem(req.params.id)
|
||||||
if (!libraryItem || libraryItem.mediaType !== 'podcast') {
|
if (!libraryItem || libraryItem.mediaType !== 'podcast') {
|
||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
}
|
}
|
||||||
if (!req.user.canUpload || !req.user.checkCanAccessLibrary(libraryItem.libraryId)) {
|
if (!req.user.checkCanAccessLibrary(libraryItem.libraryId)) {
|
||||||
|
Logger.error(`[PodcastController] User attempted to download episodes for library without permission`, req.user)
|
||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,25 @@ class SeriesController {
|
|||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
async findOne(req, res) {
|
async findOne(req, res) {
|
||||||
return res.json(req.series)
|
var include = (req.query.include || '').split(',')
|
||||||
|
|
||||||
|
var seriesJson = req.series.toJSON()
|
||||||
|
|
||||||
|
// Add progress map with isFinished flag
|
||||||
|
if (include.includes('progress')) {
|
||||||
|
var libraryItemsInSeries = this.db.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasSeries(seriesJson.id))
|
||||||
|
var libraryItemsFinished = libraryItemsInSeries.filter(li => {
|
||||||
|
var mediaProgress = req.user.getMediaProgress(li.id)
|
||||||
|
return mediaProgress && mediaProgress.isFinished
|
||||||
|
})
|
||||||
|
seriesJson.progress = {
|
||||||
|
libraryItemIds: libraryItemsInSeries.map(li => li.id),
|
||||||
|
libraryItemIdsFinished: libraryItemsFinished.map(li => li.id),
|
||||||
|
isFinished: libraryItemsFinished.length === libraryItemsInSeries.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json(seriesJson)
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(req, res) {
|
async search(req, res) {
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -113,6 +113,7 @@ class CoverManager {
|
|||||||
|
|
||||||
Logger.info(`[CoverManager] Uploaded libraryItem cover "${coverFullPath}" for "${libraryItem.media.metadata.title}"`)
|
Logger.info(`[CoverManager] Uploaded libraryItem cover "${coverFullPath}" for "${libraryItem.media.metadata.title}"`)
|
||||||
|
|
||||||
|
await filePerms.setDefault(coverFullPath)
|
||||||
libraryItem.updateMediaCover(coverFullPath)
|
libraryItem.updateMediaCover(coverFullPath)
|
||||||
return {
|
return {
|
||||||
cover: coverFullPath
|
cover: coverFullPath
|
||||||
@@ -151,6 +152,7 @@ class CoverManager {
|
|||||||
|
|
||||||
Logger.info(`[CoverManager] Downloaded libraryItem cover "${coverFullPath}" from url "${url}" for "${libraryItem.media.metadata.title}"`)
|
Logger.info(`[CoverManager] Downloaded libraryItem cover "${coverFullPath}" from url "${url}" for "${libraryItem.media.metadata.title}"`)
|
||||||
|
|
||||||
|
await filePerms.setDefault(coverFullPath)
|
||||||
libraryItem.updateMediaCover(coverFullPath)
|
libraryItem.updateMediaCover(coverFullPath)
|
||||||
return {
|
return {
|
||||||
cover: coverFullPath
|
cover: coverFullPath
|
||||||
@@ -250,6 +252,8 @@ class CoverManager {
|
|||||||
|
|
||||||
var success = await extractCoverArt(audioFileWithCover.metadata.path, coverFilePath)
|
var success = await extractCoverArt(audioFileWithCover.metadata.path, coverFilePath)
|
||||||
if (success) {
|
if (success) {
|
||||||
|
await filePerms.setDefault(coverFilePath)
|
||||||
|
|
||||||
libraryItem.updateMediaCover(coverFilePath)
|
libraryItem.updateMediaCover(coverFilePath)
|
||||||
return coverFilePath
|
return coverFilePath
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,6 +122,10 @@ class PodcastManager {
|
|||||||
var podcastEpisode = this.currentDownload.podcastEpisode
|
var podcastEpisode = this.currentDownload.podcastEpisode
|
||||||
podcastEpisode.audioFile = audioFile
|
podcastEpisode.audioFile = audioFile
|
||||||
libraryItem.media.addPodcastEpisode(podcastEpisode)
|
libraryItem.media.addPodcastEpisode(podcastEpisode)
|
||||||
|
if (libraryItem.isInvalid) {
|
||||||
|
// First episode added to an empty podcast
|
||||||
|
libraryItem.isInvalid = false
|
||||||
|
}
|
||||||
libraryItem.libraryFiles.push(libraryFile)
|
libraryItem.libraryFiles.push(libraryFile)
|
||||||
libraryItem.updatedAt = Date.now()
|
libraryItem.updatedAt = Date.now()
|
||||||
await this.db.updateLibraryItem(libraryItem)
|
await this.db.updateLibraryItem(libraryItem)
|
||||||
@@ -204,8 +208,27 @@ class PodcastManager {
|
|||||||
}
|
}
|
||||||
// Filter new and not already has
|
// Filter new and not already has
|
||||||
var newEpisodes = feed.episodes.filter(ep => ep.publishedAt > podcastLibraryItem.media.lastEpisodeCheck && !podcastLibraryItem.media.checkHasEpisodeByFeedUrl(ep.enclosure.url))
|
var newEpisodes = feed.episodes.filter(ep => ep.publishedAt > podcastLibraryItem.media.lastEpisodeCheck && !podcastLibraryItem.media.checkHasEpisodeByFeedUrl(ep.enclosure.url))
|
||||||
// Max new episodes for safety = 2
|
// Max new episodes for safety = 3
|
||||||
newEpisodes = newEpisodes.slice(0, 2)
|
newEpisodes = newEpisodes.slice(0, 3)
|
||||||
|
return newEpisodes
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkAndDownloadNewEpisodes(libraryItem) {
|
||||||
|
const lastEpisodeCheckDate = new Date(libraryItem.media.lastEpisodeCheck || 0)
|
||||||
|
Logger.info(`[PodcastManager] checkAndDownloadNewEpisodes for "${libraryItem.media.metadata.title}" - Last episode check: ${lastEpisodeCheckDate}`)
|
||||||
|
var newEpisodes = await this.checkPodcastForNewEpisodes(libraryItem)
|
||||||
|
if (newEpisodes.length) {
|
||||||
|
Logger.info(`[PodcastManager] Found ${newEpisodes.length} new episodes for podcast "${libraryItem.media.metadata.title}" - starting download`)
|
||||||
|
this.downloadPodcastEpisodes(libraryItem, newEpisodes)
|
||||||
|
} else {
|
||||||
|
Logger.info(`[PodcastManager] No new episodes found for podcast "${libraryItem.media.metadata.title}"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
libraryItem.media.lastEpisodeCheck = Date.now()
|
||||||
|
libraryItem.updatedAt = Date.now()
|
||||||
|
await this.db.updateLibraryItem(libraryItem)
|
||||||
|
this.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||||
|
|
||||||
return newEpisodes
|
return newEpisodes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
const Logger = require('../../Logger')
|
||||||
const { getId } = require('../../utils/index')
|
const { getId } = require('../../utils/index')
|
||||||
|
|
||||||
class Author {
|
class Author {
|
||||||
@@ -19,7 +20,7 @@ class Author {
|
|||||||
construct(author) {
|
construct(author) {
|
||||||
this.id = author.id
|
this.id = author.id
|
||||||
this.asin = author.asin
|
this.asin = author.asin
|
||||||
this.name = author.name
|
this.name = author.name || ''
|
||||||
this.description = author.description || null
|
this.description = author.description || null
|
||||||
this.imagePath = author.imagePath
|
this.imagePath = author.imagePath
|
||||||
this.relImagePath = author.relImagePath
|
this.relImagePath = author.relImagePath
|
||||||
@@ -81,6 +82,10 @@ class Author {
|
|||||||
|
|
||||||
checkNameEquals(name) {
|
checkNameEquals(name) {
|
||||||
if (!name) return false
|
if (!name) return false
|
||||||
|
if (this.name === null) {
|
||||||
|
Logger.error(`[Author] Author name is null (${this.id})`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
return this.name.toLowerCase() == name.toLowerCase().trim()
|
return this.name.toLowerCase() == name.toLowerCase().trim()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ class Book {
|
|||||||
numAudioFiles: this.audioFiles.length,
|
numAudioFiles: this.audioFiles.length,
|
||||||
numChapters: this.chapters.length,
|
numChapters: this.chapters.length,
|
||||||
numMissingParts: this.missingParts.length,
|
numMissingParts: this.missingParts.length,
|
||||||
|
numInvalidAudioFiles: this.invalidAudioFiles.length,
|
||||||
duration: this.duration,
|
duration: this.duration,
|
||||||
size: this.size,
|
size: this.size,
|
||||||
ebookFormat: this.ebookFile ? this.ebookFile.ebookFormat : null
|
ebookFormat: this.ebookFile ? this.ebookFile.ebookFormat : null
|
||||||
@@ -106,8 +107,11 @@ class Book {
|
|||||||
get hasEmbeddedCoverArt() {
|
get hasEmbeddedCoverArt() {
|
||||||
return this.audioFiles.some(af => af.embeddedCoverArt)
|
return this.audioFiles.some(af => af.embeddedCoverArt)
|
||||||
}
|
}
|
||||||
|
get invalidAudioFiles() {
|
||||||
|
return this.audioFiles.filter(af => af.invalid)
|
||||||
|
}
|
||||||
get hasIssues() {
|
get hasIssues() {
|
||||||
return this.missingParts.length || this.audioFiles.some(af => af.invalid)
|
return this.missingParts.length || this.invalidAudioFiles.length
|
||||||
}
|
}
|
||||||
get tracks() {
|
get tracks() {
|
||||||
var startOffset = 0
|
var startOffset = 0
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ class ServerSettings {
|
|||||||
|
|
||||||
this.backupSchedule = settings.backupSchedule || false
|
this.backupSchedule = settings.backupSchedule || false
|
||||||
this.backupsToKeep = settings.backupsToKeep || 2
|
this.backupsToKeep = settings.backupsToKeep || 2
|
||||||
this.maxBackupSize = settings.maxBackupSize || 1
|
this.maxBackupSize = settings.maxBackupSize || 1
|
||||||
this.backupMetadataCovers = settings.backupMetadataCovers !== false
|
this.backupMetadataCovers = settings.backupMetadataCovers !== false
|
||||||
|
|
||||||
this.loggerDailyLogsToKeep = settings.loggerDailyLogsToKeep || 7
|
this.loggerDailyLogsToKeep = settings.loggerDailyLogsToKeep || 7
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,15 @@ class User {
|
|||||||
get isRoot() {
|
get isRoot() {
|
||||||
return this.type === 'root'
|
return this.type === 'root'
|
||||||
}
|
}
|
||||||
|
get isAdmin() {
|
||||||
|
return this.type === 'admin'
|
||||||
|
}
|
||||||
|
get isGuest() {
|
||||||
|
return this.type === 'guest'
|
||||||
|
}
|
||||||
|
get isAdminOrUp() {
|
||||||
|
return this.isAdmin || this.isRoot
|
||||||
|
}
|
||||||
get canDelete() {
|
get canDelete() {
|
||||||
return !!this.permissions.delete && this.isActive
|
return !!this.permissions.delete && this.isActive
|
||||||
}
|
}
|
||||||
@@ -57,7 +66,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,
|
||||||
@@ -186,6 +195,7 @@ class User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// And update permissions
|
// And update permissions
|
||||||
if (payload.permissions) {
|
if (payload.permissions) {
|
||||||
for (const key in payload.permissions) {
|
for (const key in payload.permissions) {
|
||||||
@@ -195,8 +205,15 @@ class User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update accessible libraries
|
// Update accessible libraries
|
||||||
if (payload.librariesAccessible !== undefined) {
|
if (this.permissions.accessAllLibraries) {
|
||||||
|
// Access all libraries
|
||||||
|
if (this.librariesAccessible.length) {
|
||||||
|
this.librariesAccessible = []
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
} else if (payload.librariesAccessible !== undefined) {
|
||||||
if (payload.librariesAccessible.length) {
|
if (payload.librariesAccessible.length) {
|
||||||
if (payload.librariesAccessible.join(',') !== this.librariesAccessible.join(',')) {
|
if (payload.librariesAccessible.join(',') !== this.librariesAccessible.join(',')) {
|
||||||
hasUpdates = true
|
hasUpdates = true
|
||||||
@@ -208,8 +225,14 @@ class User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update accessible libraries
|
// Update accessible tags
|
||||||
if (payload.itemTagsAccessible !== undefined) {
|
if (this.permissions.accessAllTags) {
|
||||||
|
// Access all tags
|
||||||
|
if (this.itemTagsAccessible.length) {
|
||||||
|
this.itemTagsAccessible = []
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
} else if (payload.itemTagsAccessible !== undefined) {
|
||||||
if (payload.itemTagsAccessible.length) {
|
if (payload.itemTagsAccessible.length) {
|
||||||
if (payload.itemTagsAccessible.join(',') !== this.itemTagsAccessible.join(',')) {
|
if (payload.itemTagsAccessible.join(',') !== this.itemTagsAccessible.join(',')) {
|
||||||
hasUpdates = true
|
hasUpdates = true
|
||||||
|
|||||||
@@ -58,9 +58,10 @@ class ApiRouter {
|
|||||||
this.router.delete('/libraries/:id', LibraryController.middleware.bind(this), LibraryController.delete.bind(this))
|
this.router.delete('/libraries/:id', LibraryController.middleware.bind(this), LibraryController.delete.bind(this))
|
||||||
|
|
||||||
this.router.get('/libraries/:id/items', LibraryController.middleware.bind(this), LibraryController.getLibraryItems.bind(this))
|
this.router.get('/libraries/:id/items', LibraryController.middleware.bind(this), LibraryController.getLibraryItems.bind(this))
|
||||||
|
this.router.delete('/libraries/:id/issues', LibraryController.middleware.bind(this), LibraryController.removeLibraryItemsWithIssues.bind(this))
|
||||||
this.router.get('/libraries/:id/series', LibraryController.middleware.bind(this), LibraryController.getAllSeriesForLibrary.bind(this))
|
this.router.get('/libraries/:id/series', LibraryController.middleware.bind(this), LibraryController.getAllSeriesForLibrary.bind(this))
|
||||||
this.router.get('/libraries/:id/collections', LibraryController.middleware.bind(this), LibraryController.getCollectionsForLibrary.bind(this))
|
this.router.get('/libraries/:id/collections', LibraryController.middleware.bind(this), LibraryController.getCollectionsForLibrary.bind(this))
|
||||||
this.router.get('/libraries/:id/personalized', LibraryController.middleware.bind(this), LibraryController.getLibraryUserPersonalized.bind(this))
|
this.router.get('/libraries/:id/personalized', LibraryController.middleware.bind(this), LibraryController.getLibraryUserPersonalizedOptimal.bind(this))
|
||||||
this.router.get('/libraries/:id/filterdata', LibraryController.middleware.bind(this), LibraryController.getLibraryFilterData.bind(this))
|
this.router.get('/libraries/:id/filterdata', LibraryController.middleware.bind(this), LibraryController.getLibraryFilterData.bind(this))
|
||||||
this.router.get('/libraries/:id/search', LibraryController.middleware.bind(this), LibraryController.search.bind(this))
|
this.router.get('/libraries/:id/search', LibraryController.middleware.bind(this), LibraryController.search.bind(this))
|
||||||
this.router.get('/libraries/:id/stats', LibraryController.middleware.bind(this), LibraryController.stats.bind(this))
|
this.router.get('/libraries/:id/stats', LibraryController.middleware.bind(this), LibraryController.stats.bind(this))
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -94,4 +94,21 @@ module.exports.setDefault = (path, silent = false) => {
|
|||||||
if (!silent) Logger.debug(`[FilePerms] Setting permission "${mode}" for uid ${uid} and gid ${gid} | "${path}"`)
|
if (!silent) Logger.debug(`[FilePerms] Setting permission "${mode}" for uid ${uid} and gid ${gid} | "${path}"`)
|
||||||
chmodr(path, mode, uid, gid, resolve)
|
chmodr(path, mode, uid, gid, resolve)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default permissions 0o744 and global Uid/Gid
|
||||||
|
// Used for setting default permission to initial config/metadata directories
|
||||||
|
module.exports.setDefaultDirSync = (path, silent = false) => {
|
||||||
|
const mode = 0o744
|
||||||
|
const uid = global.Uid
|
||||||
|
const gid = global.Gid
|
||||||
|
if (!silent) Logger.debug(`[FilePerms] Setting dir permission "${mode}" for uid ${uid} and gid ${gid} | "${path}"`)
|
||||||
|
try {
|
||||||
|
fs.chmodSync(path, mode)
|
||||||
|
fs.chownSync(path, uid, gid)
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(`[FilePerms] Error setting dir permissions for path "${path}"`, error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+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)
|
||||||
|
|||||||
+377
-141
@@ -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,29 +37,28 @@ 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 === 'Volume Number' && (li.media.metadata.series.length === 0 || li.media.metadata.series[0].sequence === null)) return true;
|
if (filter === 'Series' && li.media.metadata.series.length === 0) return true;
|
||||||
if (filter === 'Description' && li.media.metadata.description === null) return true;
|
if (filter === 'Description' && li.media.metadata.description === null) return true;
|
||||||
if (filter === 'Genres' && li.media.metadata.genres.length === 0) return true;
|
if (filter === 'Genres' && li.media.metadata.genres.length === 0) return true;
|
||||||
if (filter === 'Tags' && li.media.tags.length === 0) return true;
|
if (filter === 'Tags' && li.media.tags.length === 0) return true;
|
||||||
if (filter === 'Narrator' && li.media.metadata.narrators.length === 0) return true;
|
if (filter === 'Narrator' && li.media.metadata.narrators.length === 0) return true;
|
||||||
if (filter === 'Publisher' && li.media.metadata.publisher === null) return true;
|
if (filter === 'Publisher' && li.media.metadata.publisher === null) return true;
|
||||||
if (filter === 'Language' && li.media.metadata.language === 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)
|
||||||
}
|
}
|
||||||
} else if (filterBy === 'issues') {
|
} else if (filterBy === 'issues') {
|
||||||
filtered = filtered.filter(ab => {
|
filtered = filtered.filter(li => li.hasIssues)
|
||||||
// TODO: Update filter for issues
|
|
||||||
return ab.isMissing || ab.isInvalid
|
|
||||||
// return ab.numMissingParts || ab.numInvalidParts || ab.isMissing || ab.isInvalid
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return filtered
|
return filtered
|
||||||
@@ -103,10 +102,10 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
if (mediaMetadata.language && !data.languages.includes(mediaMetadata.language)) data.languages.push(mediaMetadata.language)
|
if (mediaMetadata.language && !data.languages.includes(mediaMetadata.language)) data.languages.push(mediaMetadata.language)
|
||||||
})
|
})
|
||||||
data.authors = naturalSort(data.authors).asc()
|
data.authors = naturalSort(data.authors).asc(au => au.name)
|
||||||
data.genres = naturalSort(data.genres).asc()
|
data.genres = naturalSort(data.genres).asc()
|
||||||
data.tags = naturalSort(data.tags).asc()
|
data.tags = naturalSort(data.tags).asc()
|
||||||
data.series = naturalSort(data.series).asc()
|
data.series = naturalSort(data.series).asc(se => se.name)
|
||||||
data.narrators = naturalSort(data.narrators).asc()
|
data.narrators = naturalSort(data.narrators).asc()
|
||||||
data.languages = naturalSort(data.languages).asc()
|
data.languages = naturalSort(data.languages).asc()
|
||||||
return data
|
return data
|
||||||
@@ -137,80 +136,6 @@ module.exports = {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
getSeriesWithProgressFromBooks(user, books) {
|
|
||||||
return []
|
|
||||||
// var _series = {}
|
|
||||||
// books.forEach((audiobook) => {
|
|
||||||
// if (audiobook.book.series) {
|
|
||||||
// var bookWithUserAb = { userAudiobook: user.getMediaProgress(audiobook.id), book: audiobook }
|
|
||||||
// if (!_series[audiobook.book.series]) {
|
|
||||||
// _series[audiobook.book.series] = {
|
|
||||||
// id: audiobook.book.series,
|
|
||||||
// name: audiobook.book.series,
|
|
||||||
// type: 'series',
|
|
||||||
// books: [bookWithUserAb]
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// _series[audiobook.book.series].books.push(bookWithUserAb)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// return Object.values(_series).map((series) => {
|
|
||||||
// series.books = naturalSort(series.books).asc(ab => ab.book.book.volumeNumber)
|
|
||||||
// return series
|
|
||||||
// }).filter((series) => series.books.some((book) => book.userAudiobook && book.userAudiobook.isRead))
|
|
||||||
},
|
|
||||||
|
|
||||||
sortSeriesBooks(books, seriesId, minified = false) {
|
|
||||||
return naturalSort(books).asc(li => {
|
|
||||||
if (!li.media.metadata.series) return null
|
|
||||||
var series = li.media.metadata.series.find(se => se.id === seriesId)
|
|
||||||
if (!series) return null
|
|
||||||
return series.sequence
|
|
||||||
}).map(li => {
|
|
||||||
if (minified) return li.toJSONMinified()
|
|
||||||
return li.toJSONExpanded()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
getMediaProgressWithItems(user, libraryItems) {
|
|
||||||
var mediaProgress = []
|
|
||||||
libraryItems.forEach(li => {
|
|
||||||
var itemProgress = user.getAllMediaProgressForLibraryItem(li.id).map(mp => {
|
|
||||||
var episode = null
|
|
||||||
if (mp.episodeId) {
|
|
||||||
episode = li.media.getEpisode(mp.episodeId)
|
|
||||||
if (!episode) {
|
|
||||||
// Episode not found for library item
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
userProgress: mp.toJSON(),
|
|
||||||
libraryItem: li,
|
|
||||||
episode
|
|
||||||
}
|
|
||||||
}).filter(mp => !!mp)
|
|
||||||
|
|
||||||
mediaProgress = mediaProgress.concat(itemProgress)
|
|
||||||
})
|
|
||||||
return mediaProgress
|
|
||||||
},
|
|
||||||
|
|
||||||
getItemsMostRecentlyListened(itemsWithUserProgress, limit, minified = false) {
|
|
||||||
var itemsInProgress = itemsWithUserProgress.filter((data) => data.userProgress && data.userProgress.progress > 0 && !data.userProgress.isFinished)
|
|
||||||
itemsInProgress.sort((a, b) => {
|
|
||||||
return b.userProgress.lastUpdate - a.userProgress.lastUpdate
|
|
||||||
})
|
|
||||||
return itemsInProgress.map(b => {
|
|
||||||
var libjson = minified ? b.libraryItem.toJSONMinified() : b.libraryItem.toJSONExpanded()
|
|
||||||
if (b.episode) {
|
|
||||||
libjson.recentEpisode = b.episode
|
|
||||||
}
|
|
||||||
return libjson
|
|
||||||
}).slice(0, limit)
|
|
||||||
},
|
|
||||||
|
|
||||||
getBooksNextInSeries(seriesWithUserAb, limit, minified = false) {
|
getBooksNextInSeries(seriesWithUserAb, limit, minified = false) {
|
||||||
var incompleteSeires = seriesWithUserAb.filter((series) => series.books.some((book) => !book.userAudiobook || (!book.userAudiobook.isRead && book.userAudiobook.progress == 0)))
|
var incompleteSeires = seriesWithUserAb.filter((series) => series.books.some((book) => !book.userAudiobook || (!book.userAudiobook.isRead && book.userAudiobook.progress == 0)))
|
||||||
var booksNextInSeries = []
|
var booksNextInSeries = []
|
||||||
@@ -223,49 +148,6 @@ module.exports = {
|
|||||||
return booksNextInSeries.sort((a, b) => { return b.DateLastReadSeries - a.DateLastReadSeries }).map(b => minified ? b.book.toJSONMinified() : b.book.toJSONExpanded()).slice(0, limit)
|
return booksNextInSeries.sort((a, b) => { return b.DateLastReadSeries - a.DateLastReadSeries }).map(b => minified ? b.book.toJSONMinified() : b.book.toJSONExpanded()).slice(0, limit)
|
||||||
},
|
},
|
||||||
|
|
||||||
getItemsMostRecentlyFinished(itemsWithUserProgress, limit, minified = false) {
|
|
||||||
var itemsFinished = itemsWithUserProgress.filter((data) => data.userProgress && data.userProgress.isFinished)
|
|
||||||
itemsFinished.sort((a, b) => {
|
|
||||||
return b.userProgress.finishedAt - a.userProgress.finishedAt
|
|
||||||
})
|
|
||||||
return itemsFinished.map(i => {
|
|
||||||
var libjson = minified ? i.libraryItem.toJSONMinified() : i.libraryItem.toJSONExpanded()
|
|
||||||
if (i.episode) {
|
|
||||||
libjson.recentEpisode = i.episode
|
|
||||||
}
|
|
||||||
return libjson
|
|
||||||
}).slice(0, limit)
|
|
||||||
},
|
|
||||||
|
|
||||||
getItemsMostRecentlyAdded(libraryItems, limit, minified = false) {
|
|
||||||
var itemsSortedByAddedAt = sort(libraryItems).desc(li => li.addedAt)
|
|
||||||
return itemsSortedByAddedAt.map(b => minified ? b.toJSONMinified() : b.toJSONExpanded()).slice(0, limit)
|
|
||||||
},
|
|
||||||
|
|
||||||
getEpisodesRecentlyAdded(libraryItems, limit, minified = false) {
|
|
||||||
var libraryItemsWithEpisode = []
|
|
||||||
libraryItems.forEach((li) => {
|
|
||||||
if (li.mediaType !== 'podcast' || !li.media.hasMediaEntities) return
|
|
||||||
var libjson = minified ? li.toJSONMinified() : li.toJSONExpanded()
|
|
||||||
var episodes = sort(li.media.episodes || []).desc(ep => ep.addedAt)
|
|
||||||
episodes.forEach((ep) => {
|
|
||||||
var lie = { ...libjson }
|
|
||||||
lie.recentEpisode = ep
|
|
||||||
libraryItemsWithEpisode.push(lie)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
libraryItemsWithEpisode = sort(libraryItemsWithEpisode).desc(lie => lie.recentEpisode.addedAt)
|
|
||||||
return libraryItemsWithEpisode.slice(0, limit)
|
|
||||||
},
|
|
||||||
|
|
||||||
getSeriesMostRecentlyAdded(series, limit) {
|
|
||||||
var seriesSortedByAddedAt = sort(series).desc(_series => {
|
|
||||||
var booksSortedByMostRecent = sort(_series.books).desc(b => b.addedAt)
|
|
||||||
return booksSortedByMostRecent[0].addedAt
|
|
||||||
})
|
|
||||||
return seriesSortedByAddedAt.slice(0, limit)
|
|
||||||
},
|
|
||||||
|
|
||||||
getGenresWithCount(libraryItems) {
|
getGenresWithCount(libraryItems) {
|
||||||
var genresMap = {}
|
var genresMap = {}
|
||||||
libraryItems.forEach((li) => {
|
libraryItems.forEach((li) => {
|
||||||
@@ -350,5 +232,359 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
return libraryItemJson
|
return libraryItemJson
|
||||||
}).filter(li => li)
|
}).filter(li => li)
|
||||||
|
},
|
||||||
|
|
||||||
|
buildPersonalizedShelves(user, libraryItems, mediaType, allSeries, allAuthors, maxEntitiesPerShelf = 10) {
|
||||||
|
const isPodcastLibrary = mediaType === 'podcast'
|
||||||
|
|
||||||
|
const shelves = [
|
||||||
|
{
|
||||||
|
id: 'continue-listening',
|
||||||
|
label: 'Continue Listening',
|
||||||
|
type: isPodcastLibrary ? 'episode' : mediaType,
|
||||||
|
entities: [],
|
||||||
|
category: 'recentlyListened'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'continue-series',
|
||||||
|
label: 'Continue Series',
|
||||||
|
type: mediaType,
|
||||||
|
entities: [],
|
||||||
|
category: 'continueSeries'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'recently-added',
|
||||||
|
label: 'Recently Added',
|
||||||
|
type: mediaType,
|
||||||
|
entities: [],
|
||||||
|
category: 'newestItems'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'listen-again',
|
||||||
|
label: 'Listen Again',
|
||||||
|
type: isPodcastLibrary ? 'episode' : mediaType,
|
||||||
|
entities: [],
|
||||||
|
category: 'recentlyFinished'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'recent-series',
|
||||||
|
label: 'Recent Series',
|
||||||
|
type: 'series',
|
||||||
|
entities: [],
|
||||||
|
category: 'newestSeries'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'newest-authors',
|
||||||
|
label: 'Newest Authors',
|
||||||
|
type: 'authors',
|
||||||
|
entities: [],
|
||||||
|
category: 'newestAuthors'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'episodes-recently-added',
|
||||||
|
label: 'Newest Episodes',
|
||||||
|
type: 'episode',
|
||||||
|
entities: [],
|
||||||
|
category: 'newestEpisodes'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const categories = ['recentlyListened', 'continueSeries', 'newestEpisodes', 'newestItems', 'newestSeries', 'recentlyFinished', 'newestAuthors']
|
||||||
|
const categoryMap = {}
|
||||||
|
categories.forEach((cat) => {
|
||||||
|
categoryMap[cat] = {
|
||||||
|
category: cat,
|
||||||
|
biggest: 0,
|
||||||
|
smallest: 0,
|
||||||
|
items: []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const seriesMap = {}
|
||||||
|
const authorMap = {}
|
||||||
|
|
||||||
|
for (const libraryItem of libraryItems) {
|
||||||
|
if (libraryItem.addedAt > categoryMap.newestItems.smallest) {
|
||||||
|
|
||||||
|
var indexToPut = categoryMap.newestItems.items.findIndex(i => libraryItem.addedAt > i.addedAt)
|
||||||
|
if (indexToPut >= 0) {
|
||||||
|
categoryMap.newestItems.items.splice(indexToPut, 0, libraryItem.toJSONMinified())
|
||||||
|
} else {
|
||||||
|
categoryMap.newestItems.items.push(libraryItem.toJSONMinified())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (categoryMap.newestItems.items.length > maxEntitiesPerShelf) {
|
||||||
|
// Remove last item
|
||||||
|
categoryMap.newestItems.items.pop()
|
||||||
|
categoryMap.newestItems.smallest = categoryMap.newestItems.items[categoryMap.newestItems.items.length - 1].addedAt
|
||||||
|
}
|
||||||
|
categoryMap.newestItems.biggest = categoryMap.newestItems.items[0].addedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
var allItemProgress = user.getAllMediaProgressForLibraryItem(libraryItem.id)
|
||||||
|
if (libraryItem.isPodcast) {
|
||||||
|
// Podcast categories
|
||||||
|
const podcastEpisodes = libraryItem.media.episodes || []
|
||||||
|
for (const episode of podcastEpisodes) {
|
||||||
|
// Newest episodes
|
||||||
|
if (episode.addedAt > categoryMap.newestEpisodes.smallest) {
|
||||||
|
const libraryItemWithEpisode = {
|
||||||
|
...libraryItem.toJSONMinified(),
|
||||||
|
recentEpisode: episode.toJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
var indexToPut = categoryMap.newestEpisodes.items.findIndex(i => episode.addedAt > i.recentEpisode.addedAt)
|
||||||
|
if (indexToPut >= 0) {
|
||||||
|
categoryMap.newestEpisodes.items.splice(indexToPut, 0, libraryItemWithEpisode)
|
||||||
|
} else {
|
||||||
|
categoryMap.newestEpisodes.items.push(libraryItemWithEpisode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (categoryMap.newestEpisodes.items.length > maxEntitiesPerShelf) {
|
||||||
|
// Remove last item
|
||||||
|
categoryMap.newestEpisodes.items.pop()
|
||||||
|
categoryMap.newestEpisodes.smallest = categoryMap.newestEpisodes.items[categoryMap.newestEpisodes.items.length - 1].recentEpisode.addedAt
|
||||||
|
}
|
||||||
|
categoryMap.newestEpisodes.biggest = categoryMap.newestEpisodes.items[0].recentEpisode.addedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Episode recently listened and finished
|
||||||
|
var mediaProgress = allItemProgress.find(mp => mp.episodeId === episode.id)
|
||||||
|
if (mediaProgress) {
|
||||||
|
if (mediaProgress.isFinished) {
|
||||||
|
if (mediaProgress.finishedAt > categoryMap.recentlyFinished.smallest) { // Item belongs on shelf
|
||||||
|
const libraryItemWithEpisode = {
|
||||||
|
...libraryItem.toJSONMinified(),
|
||||||
|
recentEpisode: episode.toJSON(),
|
||||||
|
finishedAt: mediaProgress.finishedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
var indexToPut = categoryMap.recentlyFinished.items.findIndex(i => mediaProgress.finishedAt > i.finishedAt)
|
||||||
|
if (indexToPut >= 0) {
|
||||||
|
categoryMap.recentlyFinished.items.splice(indexToPut, 0, libraryItemWithEpisode)
|
||||||
|
} else {
|
||||||
|
categoryMap.recentlyFinished.items.push(libraryItemWithEpisode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (categoryMap.recentlyFinished.items.length > maxEntitiesPerShelf) {
|
||||||
|
// Remove last item
|
||||||
|
categoryMap.recentlyFinished.items.pop()
|
||||||
|
categoryMap.recentlyFinished.smallest = categoryMap.recentlyFinished.items[categoryMap.recentlyFinished.items.length - 1].finishedAt
|
||||||
|
}
|
||||||
|
categoryMap.recentlyFinished.biggest = categoryMap.recentlyFinished.items[0].finishedAt
|
||||||
|
}
|
||||||
|
} else if (mediaProgress.progress > 0) { // Handle most recently listened
|
||||||
|
if (mediaProgress.lastUpdate > categoryMap.recentlyListened.smallest) { // Item belongs on shelf
|
||||||
|
const libraryItemWithEpisode = {
|
||||||
|
...libraryItem.toJSONMinified(),
|
||||||
|
recentEpisode: episode.toJSON(),
|
||||||
|
progressLastUpdate: mediaProgress.lastUpdate
|
||||||
|
}
|
||||||
|
|
||||||
|
var indexToPut = categoryMap.recentlyListened.items.findIndex(i => mediaProgress.lastUpdate > i.progressLastUpdate)
|
||||||
|
if (indexToPut >= 0) {
|
||||||
|
categoryMap.recentlyListened.items.splice(indexToPut, 0, libraryItemWithEpisode)
|
||||||
|
} else {
|
||||||
|
categoryMap.recentlyListened.items.push(libraryItemWithEpisode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (categoryMap.recentlyListened.items.length > maxEntitiesPerShelf) {
|
||||||
|
// Remove last item
|
||||||
|
categoryMap.recentlyListened.items.pop()
|
||||||
|
categoryMap.recentlyListened.smallest = categoryMap.recentlyListened.items[categoryMap.recentlyListened.items.length - 1].progressLastUpdate
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryMap.recentlyListened.biggest = categoryMap.recentlyListened.items[0].progressLastUpdate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Book categories
|
||||||
|
|
||||||
|
// Newest series
|
||||||
|
if (libraryItem.media.metadata.series.length) {
|
||||||
|
for (const librarySeries of libraryItem.media.metadata.series) {
|
||||||
|
const mediaProgress = allItemProgress.length ? allItemProgress[0] : null
|
||||||
|
const bookInProgress = mediaProgress && mediaProgress.inProgress
|
||||||
|
const libraryItemJson = libraryItem.toJSONMinified()
|
||||||
|
libraryItemJson.seriesSequence = librarySeries.sequence
|
||||||
|
|
||||||
|
if (!seriesMap[librarySeries.id]) {
|
||||||
|
const seriesObj = allSeries.find(se => se.id === librarySeries.id)
|
||||||
|
if (seriesObj) {
|
||||||
|
var series = {
|
||||||
|
...seriesObj.toJSON(),
|
||||||
|
books: [libraryItemJson],
|
||||||
|
inProgress: bookInProgress,
|
||||||
|
bookInProgressLastUpdate: bookInProgress ? mediaProgress.lastUpdate : null,
|
||||||
|
sequenceInProgress: bookInProgress ? libraryItemJson.seriesSequence : null
|
||||||
|
}
|
||||||
|
seriesMap[librarySeries.id] = series
|
||||||
|
|
||||||
|
if (series.addedAt > categoryMap.newestSeries.smallest) {
|
||||||
|
var indexToPut = categoryMap.newestSeries.items.findIndex(i => series.addedAt > i.addedAt)
|
||||||
|
if (indexToPut >= 0) {
|
||||||
|
categoryMap.newestSeries.items.splice(indexToPut, 0, series)
|
||||||
|
} else {
|
||||||
|
categoryMap.newestSeries.items.push(series)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Max series is 5
|
||||||
|
if (categoryMap.newestSeries.items.length > 5) {
|
||||||
|
categoryMap.newestSeries.items.pop()
|
||||||
|
categoryMap.newestSeries.smallest = categoryMap.newestSeries.items[categoryMap.newestSeries.items.length - 1].addedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryMap.newestSeries.biggest = categoryMap.newestSeries.items[0].addedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// series already in map - add book
|
||||||
|
seriesMap[librarySeries.id].books.push(libraryItemJson)
|
||||||
|
|
||||||
|
if (bookInProgress) { // Update if this series is in progress
|
||||||
|
seriesMap[librarySeries.id].inProgress = true
|
||||||
|
if (!seriesMap[librarySeries.id].sequenceInProgress) {
|
||||||
|
seriesMap[librarySeries.id].sequenceInProgress = librarySeries.sequence
|
||||||
|
seriesMap[librarySeries.id].bookInProgressLastUpdate = mediaProgress.lastUpdate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Newest authors
|
||||||
|
if (libraryItem.media.metadata.authors.length) {
|
||||||
|
for (const libraryAuthor of libraryItem.media.metadata.authors) {
|
||||||
|
if (!authorMap[libraryAuthor.id]) {
|
||||||
|
const authorObj = allAuthors.find(au => au.id === libraryAuthor.id)
|
||||||
|
if (authorObj) {
|
||||||
|
var author = {
|
||||||
|
...authorObj.toJSON(),
|
||||||
|
numBooks: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (author.addedAt > categoryMap.newestAuthors.smallest) {
|
||||||
|
|
||||||
|
var indexToPut = categoryMap.newestAuthors.items.findIndex(i => author.addedAt > i.addedAt)
|
||||||
|
if (indexToPut >= 0) {
|
||||||
|
categoryMap.newestAuthors.items.splice(indexToPut, 0, author)
|
||||||
|
} else {
|
||||||
|
categoryMap.newestAuthors.items.push(author)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Max authors is 10
|
||||||
|
if (categoryMap.newestAuthors.items.length > 10) {
|
||||||
|
categoryMap.newestAuthors.items.pop()
|
||||||
|
categoryMap.newestAuthors.smallest = categoryMap.newestAuthors.items[categoryMap.newestAuthors.items.length - 1].addedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryMap.newestAuthors.biggest = categoryMap.newestAuthors.items[0].addedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
authorMap[libraryAuthor.id] = author
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
authorMap[libraryAuthor.id].numBooks++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Book listening and finished
|
||||||
|
var mediaProgress = allItemProgress.length ? allItemProgress[0] : null
|
||||||
|
if (mediaProgress) {
|
||||||
|
// Handle most recently finished
|
||||||
|
if (mediaProgress.isFinished) {
|
||||||
|
if (mediaProgress.finishedAt > categoryMap.recentlyFinished.smallest) { // Item belongs on shelf
|
||||||
|
const libraryItemObj = {
|
||||||
|
...libraryItem.toJSONMinified(),
|
||||||
|
finishedAt: mediaProgress.finishedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
var indexToPut = categoryMap.recentlyFinished.items.findIndex(i => mediaProgress.finishedAt > i.finishedAt)
|
||||||
|
if (indexToPut >= 0) {
|
||||||
|
categoryMap.recentlyFinished.items.splice(indexToPut, 0, libraryItemObj)
|
||||||
|
} else {
|
||||||
|
categoryMap.recentlyFinished.items.push(libraryItemObj)
|
||||||
|
}
|
||||||
|
if (categoryMap.recentlyFinished.items.length > maxEntitiesPerShelf) {
|
||||||
|
// Remove last item
|
||||||
|
categoryMap.recentlyFinished.items.pop()
|
||||||
|
categoryMap.recentlyFinished.smallest = categoryMap.recentlyFinished.items[categoryMap.recentlyFinished.items.length - 1].finishedAt
|
||||||
|
}
|
||||||
|
categoryMap.recentlyFinished.biggest = categoryMap.recentlyFinished.items[0].finishedAt
|
||||||
|
}
|
||||||
|
} else if (mediaProgress.inProgress) { // Handle most recently listened
|
||||||
|
if (mediaProgress.lastUpdate > categoryMap.recentlyListened.smallest) { // Item belongs on shelf
|
||||||
|
const libraryItemObj = {
|
||||||
|
...libraryItem.toJSONMinified(),
|
||||||
|
progressLastUpdate: mediaProgress.lastUpdate
|
||||||
|
}
|
||||||
|
|
||||||
|
var indexToPut = categoryMap.recentlyListened.items.findIndex(i => mediaProgress.lastUpdate > i.progressLastUpdate)
|
||||||
|
if (indexToPut >= 0) {
|
||||||
|
categoryMap.recentlyListened.items.splice(indexToPut, 0, libraryItemObj)
|
||||||
|
} else { // Should only happen when array is < max
|
||||||
|
categoryMap.recentlyListened.items.push(libraryItemObj)
|
||||||
|
}
|
||||||
|
if (categoryMap.recentlyListened.items.length > maxEntitiesPerShelf) {
|
||||||
|
// Remove last item
|
||||||
|
categoryMap.recentlyListened.items.pop()
|
||||||
|
categoryMap.recentlyListened.smallest = categoryMap.recentlyListened.items[categoryMap.recentlyListened.items.length - 1].progressLastUpdate
|
||||||
|
}
|
||||||
|
categoryMap.recentlyListened.biggest = categoryMap.recentlyListened.items[0].progressLastUpdate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Continue Series - Find next book in series for series that are in progress
|
||||||
|
for (const seriesId in seriesMap) {
|
||||||
|
if (seriesMap[seriesId].inProgress) {
|
||||||
|
seriesMap[seriesId].books = naturalSort(seriesMap[seriesId].books).asc(li => li.seriesSequence)
|
||||||
|
|
||||||
|
const nextBookInSeries = seriesMap[seriesId].books.find(li => {
|
||||||
|
if (!seriesMap[seriesId].sequenceInProgress) return true
|
||||||
|
// True if book series sequence is greater than the current book sequence in progress
|
||||||
|
return String(li.seriesSequence).localeCompare(String(seriesMap[seriesId].sequenceInProgress), undefined, { sensitivity: 'base', numeric: true }) > 0
|
||||||
|
})
|
||||||
|
|
||||||
|
if (nextBookInSeries) {
|
||||||
|
const bookForContinueSeries = {
|
||||||
|
...nextBookInSeries,
|
||||||
|
prevBookInProgressLastUpdate: seriesMap[seriesId].bookInProgressLastUpdate
|
||||||
|
}
|
||||||
|
bookForContinueSeries.media.metadata.series = {
|
||||||
|
id: seriesId,
|
||||||
|
name: seriesMap[seriesId].name,
|
||||||
|
sequence: nextBookInSeries.seriesSequence
|
||||||
|
}
|
||||||
|
|
||||||
|
const indexToPut = categoryMap.continueSeries.items.findIndex(i => i.prevBookInProgressLastUpdate < bookForContinueSeries.prevBookInProgressLastUpdate)
|
||||||
|
if (indexToPut >= 0) {
|
||||||
|
categoryMap.continueSeries.items.splice(indexToPut, 0, bookForContinueSeries)
|
||||||
|
} else if (categoryMap.continueSeries.items.length < 10) { // Max 10 books
|
||||||
|
categoryMap.continueSeries.items.push(bookForContinueSeries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort series books by sequence
|
||||||
|
if (categoryMap.newestSeries.items.length) {
|
||||||
|
for (const seriesItem of categoryMap.newestSeries.items) {
|
||||||
|
seriesItem.books = naturalSort(seriesItem.books).asc(li => li.seriesSequence)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var categoriesWithItems = Object.values(categoryMap).filter(cat => cat.items.length)
|
||||||
|
|
||||||
|
return categoriesWithItems.map(cat => {
|
||||||
|
var shelf = shelves.find(s => s.category === cat.category)
|
||||||
|
shelf.entities = cat.items
|
||||||
|
return shelf
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,6 +79,9 @@ module.exports.parse = (nameString) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter out names that have no first and last
|
||||||
|
names = names.filter(n => n.first_name || n.last_name)
|
||||||
|
|
||||||
var namesArray = names.map(a => a.first_name ? `${a.first_name} ${a.last_name}` : a.last_name)
|
var namesArray = names.map(a => a.first_name ? `${a.first_name} ${a.last_name}` : a.last_name)
|
||||||
var firstLast = names.length ? namesArray.join(', ') : ''
|
var firstLast = names.length ? namesArray.join(', ') : ''
|
||||||
var lastFirst = names.length ? names.map(a => a.first_name ? `${a.last_name}, ${a.first_name}` : a.last_name).join(', ') : ''
|
var lastFirst = names.length ? names.map(a => a.first_name ? `${a.last_name}, ${a.first_name}` : a.last_name).join(', ') : ''
|
||||||
|
|||||||
+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