mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-04 09:50:42 +02:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 11354a3e3f | |||
| dcd4f69383 | |||
| e253939c1e | |||
| f25ce1c0e7 | |||
| 7717e57c16 | |||
| 2e28c9b06d | |||
| 4bc7cd2045 | |||
| 5389115120 | |||
| 6e99cf6570 | |||
| 21bdd9f9ec | |||
| e3ae3f7e6a | |||
| 74bf917150 | |||
| 5666b263f5 |
@@ -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
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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`)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -112,6 +112,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'
|
||||||
|
|||||||
@@ -272,7 +272,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 +297,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 +309,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'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.0.2",
|
"version": "2.0.3",
|
||||||
"description": "Audiobook manager and player",
|
"description": "Audiobook manager and player",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -159,6 +159,12 @@
|
|||||||
<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>
|
||||||
|
|
||||||
|
<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" :media="media" />
|
<widgets-audiobook-data v-if="tracks.length" :library-item-id="libraryItemId" :media="media" />
|
||||||
|
|
||||||
<tables-podcast-episodes-table v-if="isPodcast" :library-item="libraryItem" />
|
<tables-podcast-episodes-table v-if="isPodcast" :library-item="libraryItem" />
|
||||||
@@ -228,6 +234,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
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+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.3",
|
||||||
"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)
|
||||||
|
|
||||||
|
|||||||
+13
-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()
|
||||||
|
|||||||
@@ -226,6 +226,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,12 +309,25 @@ class LibraryController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// api/libraries/:id/personalized
|
// api/libraries/:id/personalized
|
||||||
|
// New and improved personalized call only loops through library items once
|
||||||
|
async getLibraryUserPersonalizedOptimal(req, res) {
|
||||||
|
const mediaType = req.library.mediaType
|
||||||
|
const libraryItems = req.libraryItems
|
||||||
|
const limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 10
|
||||||
|
|
||||||
|
const categories = libraryHelpers.buildPersonalizedShelves(req.user, libraryItems, mediaType, this.db.series, this.db.authors, limitPerShelf)
|
||||||
|
res.json(categories)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Remove old personalized function with all its helper functions
|
||||||
|
// old personalized function looped through the library items many times
|
||||||
|
// api/libraries/:id/personalized-old
|
||||||
async getLibraryUserPersonalized(req, res) {
|
async getLibraryUserPersonalized(req, res) {
|
||||||
var mediaType = req.library.mediaType
|
var mediaType = req.library.mediaType
|
||||||
var isPodcastLibrary = mediaType == 'podcast'
|
var isPodcastLibrary = mediaType == 'podcast'
|
||||||
var libraryItems = req.libraryItems
|
var libraryItems = req.libraryItems
|
||||||
var limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12
|
var limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12
|
||||||
var minified = req.query.minified === '1'
|
var minified = req.query.minified == '1'
|
||||||
|
|
||||||
var itemsWithUserProgress = libraryHelpers.getMediaProgressWithItems(req.user, libraryItems)
|
var itemsWithUserProgress = libraryHelpers.getMediaProgressWithItems(req.user, libraryItems)
|
||||||
var categories = [
|
var categories = [
|
||||||
@@ -324,7 +353,6 @@ class LibraryController {
|
|||||||
return cats.entities.length
|
return cats.entities.length
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
// New Series section
|
// New Series section
|
||||||
// TODO: optimize and move to libraryHelpers
|
// TODO: optimize and move to libraryHelpers
|
||||||
if (!isPodcastLibrary) {
|
if (!isPodcastLibrary) {
|
||||||
@@ -521,7 +549,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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -58,9 +58,11 @@ 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-old', 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))
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -43,7 +43,6 @@ module.exports = {
|
|||||||
if (filter === 'Author' && li.media.metadata.authors.length === 0) return true;
|
if (filter === 'Author' && li.media.metadata.authors.length === 0) return true;
|
||||||
if (filter === 'Publish Year' && li.media.metadata.publishedYear === null) return true;
|
if (filter === 'Publish Year' && li.media.metadata.publishedYear === null) return true;
|
||||||
if (filter === 'Series' && li.media.metadata.series.length === 0) return true;
|
if (filter === 'Series' && li.media.metadata.series.length === 0) return true;
|
||||||
if (filter === 'Volume Number' && (li.media.metadata.series.length === 0 || li.media.metadata.series[0].sequence === null)) 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;
|
||||||
@@ -55,11 +54,7 @@ module.exports = {
|
|||||||
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 +98,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
|
||||||
@@ -350,5 +345,313 @@ 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: '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', '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) {
|
||||||
|
|
||||||
|
if (!seriesMap[librarySeries.id]) {
|
||||||
|
const seriesObj = allSeries.find(se => se.id === librarySeries.id)
|
||||||
|
if (seriesObj) {
|
||||||
|
var series = {
|
||||||
|
...seriesObj.toJSON(),
|
||||||
|
books: []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (series.addedAt > categoryMap.newestSeries.smallest) {
|
||||||
|
const libraryItemJson = libraryItem.toJSONMinified()
|
||||||
|
libraryItemJson.seriesSequence = librarySeries.sequence
|
||||||
|
series.books.push(libraryItemJson)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
seriesMap[librarySeries.id] = series
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// series already in map - add book
|
||||||
|
const libraryItemJson = libraryItem.toJSONMinified()
|
||||||
|
libraryItemJson.seriesSequence = librarySeries.sequence
|
||||||
|
seriesMap[librarySeries.id].books.push(libraryItemJson)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(', ') : ''
|
||||||
|
|||||||
Reference in New Issue
Block a user