mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-12-31 11:38:47 -05:00
Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3dc79121e | ||
|
|
9627f58541 | ||
|
|
1118b8b782 | ||
|
|
36626d43a1 | ||
|
|
af9a87f8bd | ||
|
|
056de09645 | ||
|
|
f5c394c96d | ||
|
|
3824154c15 | ||
|
|
586c8a550a | ||
|
|
d57effe97c | ||
|
|
473257f65e | ||
|
|
c1938f78c2 | ||
|
|
e97171d953 | ||
|
|
c6e9fe6513 | ||
|
|
765a11f135 | ||
|
|
491bb04877 | ||
|
|
fbbcbb4af1 | ||
|
|
ce133cd6f2 | ||
|
|
dc4c30d791 | ||
|
|
e752b4071d | ||
|
|
685b4e77eb | ||
|
|
1a35def375 | ||
|
|
76d55e72df | ||
|
|
8127ee7e56 | ||
|
|
efecf7ed82 | ||
|
|
ac46548c4d | ||
|
|
40384dd442 | ||
|
|
05b4124761 | ||
|
|
e1e10dca50 | ||
|
|
0e96465d74 | ||
|
|
88e9dabaaa | ||
|
|
d65ab0e35d | ||
|
|
f55559e9a3 | ||
|
|
4ea1e4460a | ||
|
|
b16e69ee86 | ||
|
|
6b8d71c0b0 | ||
|
|
cb762c97a8 | ||
|
|
77139c7256 | ||
|
|
4cf43bc105 | ||
|
|
588b8ff209 | ||
|
|
62a8301938 | ||
|
|
ce4e48cbd7 | ||
|
|
067d90474b | ||
|
|
e0e69fb164 | ||
|
|
365610d918 | ||
|
|
fdece944f4 | ||
|
|
d7952dab04 | ||
|
|
bec599f325 | ||
|
|
affcc03c61 | ||
|
|
db18c71857 | ||
|
|
dcc223949a | ||
|
|
6a6d384d88 | ||
|
|
cd57667444 | ||
|
|
3900db14d3 | ||
|
|
1fa94cbfad | ||
|
|
793233e782 | ||
|
|
94012e5dff | ||
|
|
d440a9fd6a | ||
|
|
928c6cf5b3 | ||
|
|
23a25d420c | ||
|
|
dc779a3fc5 | ||
|
|
876badbeea | ||
|
|
8563bdde74 |
@@ -6,7 +6,7 @@ RUN npm ci && npm cache clean --force
|
||||
RUN npm run generate
|
||||
|
||||
### STAGE 1: Build server ###
|
||||
FROM sandreas/tone:v0.0.9 AS tone
|
||||
FROM sandreas/tone:v0.1.2 AS tone
|
||||
FROM node:16-alpine
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
@@ -49,8 +49,8 @@ add_group() {
|
||||
install_ffmpeg() {
|
||||
echo "Starting FFMPEG Install"
|
||||
|
||||
WGET="wget https://johnvansickle.com/ffmpeg/builds/ffmpeg-git-amd64-static.tar.xz"
|
||||
WGET_TONE="wget https://github.com/sandreas/tone/releases/download/v0.0.9/tone-0.0.9-linux-x64.tar.gz"
|
||||
WGET="wget https://johnvansickle.com/ffmpeg/builds/ffmpeg-git-amd64-static.tar.xz --output-document=ffmpeg-git-amd64-static.tar.xz"
|
||||
WGET_TONE="wget https://github.com/sandreas/tone/releases/download/v0.1.2/tone-0.1.2-linux-x64.tar.gz --output-document=tone-0.1.2-linux-x64.tar.gz"
|
||||
|
||||
if ! cd "$FFMPEG_INSTALL_DIR"; then
|
||||
echo "Creating ffmpeg install dir at $FFMPEG_INSTALL_DIR"
|
||||
@@ -66,8 +66,8 @@ install_ffmpeg() {
|
||||
# Temp downloading tone library to the ffmpeg dir
|
||||
echo "Getting tone.."
|
||||
$WGET_TONE
|
||||
tar xvf tone-0.0.9-linux-x64.tar.gz --strip-components=1
|
||||
rm tone-0.0.9-linux-x64.tar.gz
|
||||
tar xvf tone-0.1.2-linux-x64.tar.gz --strip-components=1
|
||||
rm tone-0.1.2-linux-x64.tar.gz
|
||||
|
||||
echo "Good to go on Ffmpeg (& tone)... hopefully"
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ ExecStart=/usr/share/audiobookshelf/audiobookshelf
|
||||
ExecReload=/bin/kill -HUP $MAINPID
|
||||
Restart=always
|
||||
User=audiobookshelf
|
||||
Group=audiobookshelf
|
||||
PermissionsStartOnly=true
|
||||
|
||||
[Install]
|
||||
|
||||
106
client/assets/absicons.css
Normal file
106
client/assets/absicons.css
Normal file
@@ -0,0 +1,106 @@
|
||||
@font-face {
|
||||
font-family: 'absicons';
|
||||
src: url('~static/fonts/absicons/absicons.eot?2jfq33');
|
||||
src: url('~static/fonts/absicons/absicons.eot?2jfq33#iefix') format('embedded-opentype'),
|
||||
url('~static/fonts/absicons/absicons.ttf?2jfq33') format('truetype'),
|
||||
url('~static/fonts/absicons/absicons.woff?2jfq33') format('woff'),
|
||||
url('~static/fonts/absicons/absicons.svg?2jfq33#absicons') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
.abs-icons {
|
||||
/* use !important to prevent issues with browser extensions that change fonts */
|
||||
font-family: 'absicons' !important;
|
||||
speak: never;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
|
||||
/* Better Font Rendering =========== */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-books-1:before {
|
||||
content: "\e905";
|
||||
}
|
||||
|
||||
.icon-microphone-1:before {
|
||||
content: "\e902";
|
||||
}
|
||||
|
||||
.icon-radio:before {
|
||||
content: "\e903";
|
||||
}
|
||||
|
||||
.icon-podcast:before {
|
||||
content: "\e904";
|
||||
}
|
||||
|
||||
.icon-audiobookshelf:before {
|
||||
content: "\e900";
|
||||
}
|
||||
|
||||
.icon-database:before {
|
||||
content: "\e906";
|
||||
}
|
||||
|
||||
.icon-microphone-2:before {
|
||||
content: "\e901";
|
||||
}
|
||||
|
||||
.icon-headphones:before {
|
||||
content: "\e910";
|
||||
}
|
||||
|
||||
.icon-music:before {
|
||||
content: "\e911";
|
||||
}
|
||||
|
||||
.icon-video:before {
|
||||
content: "\e914";
|
||||
}
|
||||
|
||||
.icon-microphone-3:before {
|
||||
content: "\e91e";
|
||||
}
|
||||
|
||||
.icon-book-1:before {
|
||||
content: "\e91f";
|
||||
}
|
||||
|
||||
.icon-books-2:before {
|
||||
content: "\e920";
|
||||
}
|
||||
|
||||
.icon-file-picture:before {
|
||||
content: "\e927";
|
||||
}
|
||||
|
||||
.icon-database-1:before {
|
||||
content: "\e964";
|
||||
}
|
||||
|
||||
.icon-rocket:before {
|
||||
content: "\e9a5";
|
||||
}
|
||||
|
||||
.icon-power:before {
|
||||
content: "\e9b5";
|
||||
}
|
||||
|
||||
.icon-star:before {
|
||||
content: "\e9d9";
|
||||
}
|
||||
|
||||
.icon-heart:before {
|
||||
content: "\e9da";
|
||||
}
|
||||
|
||||
.icon-rss:before {
|
||||
content: "\ea9b";
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
@import './transitions.css';
|
||||
@import './draggable.css';
|
||||
@import './defaultStyles.css';
|
||||
@import './absicons.css';
|
||||
|
||||
:root {
|
||||
--bookshelf-texture-img: url(/textures/wood_default.jpg);
|
||||
|
||||
@@ -153,7 +153,7 @@ export default {
|
||||
cancelSelectionMode() {
|
||||
if (this.processingBatchDelete) return
|
||||
this.$store.commit('setSelectedLibraryItems', [])
|
||||
this.$eventBus.$emit('bookshelf-clear-selection')
|
||||
this.$eventBus.$emit('bookshelf_clear_selection')
|
||||
this.isAllSelected = false
|
||||
},
|
||||
toggleBatchRead() {
|
||||
@@ -172,7 +172,7 @@ export default {
|
||||
this.$toast.success('Batch update success!')
|
||||
this.$store.commit('setProcessingBatch', false)
|
||||
this.$store.commit('setSelectedLibraryItems', [])
|
||||
this.$eventBus.$emit('bookshelf-clear-selection')
|
||||
this.$eventBus.$emit('bookshelf_clear_selection')
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toast.error('Batch update failed')
|
||||
@@ -195,7 +195,7 @@ export default {
|
||||
this.processingBatchDelete = false
|
||||
this.$store.commit('setProcessingBatch', false)
|
||||
this.$store.commit('setSelectedLibraryItems', [])
|
||||
this.$eventBus.$emit('bookshelf-clear-selection')
|
||||
this.$eventBus.$emit('bookshelf_clear_selection')
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toast.error('Batch delete failed')
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
<!-- Alternate plain view -->
|
||||
<div v-else-if="isAlternativeBookshelfView" class="w-full mb-24">
|
||||
<template v-for="(shelf, index) in shelves">
|
||||
<widgets-item-slider v-if="shelf.type === 'book' || shelf.type === 'podcast'" :key="index + '.'" :items="shelf.entities" :continue-listening-shelf="shelf.id === 'continue-listening'" :height="232 * sizeMultiplier" class="bookshelf-row pl-8 my-6">
|
||||
<widgets-item-slider v-if="shelf.type === 'book' || shelf.type === 'podcast'" :key="index + '.'" :items="shelf.entities" :continue-listening-shelf="shelf.id === 'continue-listening'" :height="232 * sizeMultiplier" class="bookshelf-row pl-8 my-6" @selectEntity="(payload) => selectEntity(payload, index)">
|
||||
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ shelf.label }}</p>
|
||||
</widgets-item-slider>
|
||||
<widgets-episode-slider v-else-if="shelf.type === 'episode'" :key="index + '.'" :items="shelf.entities" :continue-listening-shelf="shelf.id === 'continue-listening'" :height="232 * sizeMultiplier" class="bookshelf-row pl-8 my-6">
|
||||
<widgets-episode-slider v-else-if="shelf.type === 'episode'" :key="index + '.'" :items="shelf.entities" :continue-listening-shelf="shelf.id === 'continue-listening'" :height="232 * sizeMultiplier" class="bookshelf-row pl-8 my-6" @selectEntity="(payload) => selectEntity(payload, index)">
|
||||
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ shelf.label }}</p>
|
||||
</widgets-episode-slider>
|
||||
<widgets-series-slider v-else-if="shelf.type === 'series'" :key="index + '.'" :items="shelf.entities" :height="232 * sizeMultiplier" class="bookshelf-row pl-8 my-6">
|
||||
@@ -33,7 +33,7 @@
|
||||
<!-- Regular bookshelf view -->
|
||||
<div v-else class="w-full">
|
||||
<template v-for="(shelf, index) in shelves">
|
||||
<app-book-shelf-row :key="index" :index="index" :shelf="shelf" :size-multiplier="sizeMultiplier" :book-cover-width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" :continue-listening-shelf="shelf.id === 'continue-listening'" />
|
||||
<app-book-shelf-row :key="index" :index="index" :shelf="shelf" :size-multiplier="sizeMultiplier" :book-cover-width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" :continue-listening-shelf="shelf.id === 'continue-listening'" @selectEntity="(payload) => selectEntity(payload, index)" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -54,7 +54,8 @@ export default {
|
||||
keywordFilterTimeout: null,
|
||||
scannerParseSubtitle: false,
|
||||
wrapperClientWidth: 0,
|
||||
shelves: []
|
||||
shelves: [],
|
||||
lastItemIndexSelected: -1
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -71,7 +72,7 @@ export default {
|
||||
return this.$store.getters['libraries/getCurrentLibraryName']
|
||||
},
|
||||
isAlternativeBookshelfView() {
|
||||
return this.$store.getters['getHomeBookshelfView'] === this.$constants.BookshelfView.TITLES
|
||||
return this.$store.getters['getHomeBookshelfView'] === this.$constants.BookshelfView.DETAIL
|
||||
},
|
||||
bookCoverWidth() {
|
||||
var coverSize = this.$store.getters['user/getUserSetting']('bookshelfCoverSize')
|
||||
@@ -87,9 +88,64 @@ export default {
|
||||
sizeMultiplier() {
|
||||
var baseSize = this.isCoverSquareAspectRatio ? 192 : 120
|
||||
return this.bookCoverWidth / baseSize
|
||||
},
|
||||
selectedLibraryItems() {
|
||||
return this.$store.state.selectedLibraryItems || []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectEntity({ entity, shiftKey }, shelfIndex) {
|
||||
const shelf = this.shelves[shelfIndex]
|
||||
const entityShelfIndex = shelf.entities.findIndex((ent) => ent.id === entity.id)
|
||||
const indexOf = shelf.shelfStartIndex + entityShelfIndex
|
||||
|
||||
const lastLastItemIndexSelected = this.lastItemIndexSelected
|
||||
if (!this.selectedLibraryItems.includes(entity.id)) {
|
||||
this.lastItemIndexSelected = indexOf
|
||||
} else {
|
||||
this.lastItemIndexSelected = -1
|
||||
}
|
||||
|
||||
if (shiftKey && lastLastItemIndexSelected >= 0) {
|
||||
var loopStart = indexOf
|
||||
var loopEnd = lastLastItemIndexSelected
|
||||
if (indexOf > lastLastItemIndexSelected) {
|
||||
loopStart = lastLastItemIndexSelected
|
||||
loopEnd = indexOf
|
||||
}
|
||||
|
||||
const flattenedEntitiesArray = []
|
||||
this.shelves.map((s) => flattenedEntitiesArray.push(...s.entities))
|
||||
|
||||
var isSelecting = false
|
||||
// If any items in this range is not selected then select all otherwise unselect all
|
||||
for (let i = loopStart; i <= loopEnd; i++) {
|
||||
const thisEntity = flattenedEntitiesArray[i]
|
||||
if (thisEntity) {
|
||||
if (!this.selectedLibraryItems.includes(thisEntity.id)) {
|
||||
isSelecting = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isSelecting) this.lastItemIndexSelected = indexOf
|
||||
|
||||
for (let i = loopStart; i <= loopEnd; i++) {
|
||||
const thisEntity = flattenedEntitiesArray[i]
|
||||
if (thisEntity) {
|
||||
this.$store.commit('setLibraryItemSelected', { libraryItemId: thisEntity.id, selected: isSelecting })
|
||||
} else {
|
||||
console.error('Invalid entity index', i)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.$store.commit('toggleLibraryItemSelected', entity.id)
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.$eventBus.$emit('item-selected', entity)
|
||||
})
|
||||
},
|
||||
async init() {
|
||||
this.wrapperClientWidth = this.$refs.wrapper ? this.$refs.wrapper.clientWidth : 0
|
||||
|
||||
@@ -110,6 +166,12 @@ export default {
|
||||
console.error('Failed to fetch categories', error)
|
||||
return []
|
||||
})
|
||||
|
||||
let totalEntityCount = 0
|
||||
for (const shelf of categories) {
|
||||
shelf.shelfStartIndex = totalEntityCount
|
||||
totalEntityCount += shelf.entities.length
|
||||
}
|
||||
this.shelves = categories
|
||||
},
|
||||
async setShelvesFromSearch() {
|
||||
|
||||
@@ -138,11 +138,8 @@ export default {
|
||||
})
|
||||
}
|
||||
},
|
||||
selectItem(libraryItem) {
|
||||
this.$store.commit('toggleLibraryItemSelected', libraryItem.id)
|
||||
this.$nextTick(() => {
|
||||
this.$eventBus.$emit('item-selected', libraryItem)
|
||||
})
|
||||
selectItem(payload) {
|
||||
this.$emit('selectEntity', payload)
|
||||
},
|
||||
itemSelectedEvt() {
|
||||
this.updateSelectionMode(this.isSelectionMode)
|
||||
@@ -191,11 +188,11 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$eventBus.$on('bookshelf-clear-selection', this.clearSelectedEntities)
|
||||
this.$eventBus.$on('bookshelf_clear_selection', this.clearSelectedEntities)
|
||||
this.$eventBus.$on('item-selected', this.itemSelectedEvt)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$eventBus.$off('bookshelf-clear-selection', this.clearSelectedEntities)
|
||||
this.$eventBus.$off('bookshelf_clear_selection', this.clearSelectedEntities)
|
||||
this.$eventBus.$off('item-selected', this.itemSelectedEvt)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<template>
|
||||
<div class="w-full h-20 md:h-10 relative">
|
||||
<div class="flex md:hidden h-10 items-center">
|
||||
<nuxt-link :to="`/library/${currentLibraryId}`" class="flex-grow h-full flex justify-center items-center" :class="homePage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||
<nuxt-link :to="`/library/${currentLibraryId}`" class="flex-grow h-full flex justify-center items-center" :class="isHomePage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||
<p class="text-sm">Home</p>
|
||||
</nuxt-link>
|
||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf`" class="flex-grow h-full flex justify-center items-center" :class="showLibrary ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf`" class="flex-grow h-full flex justify-center items-center" :class="isLibraryPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||
<p class="text-sm">Library</p>
|
||||
</nuxt-link>
|
||||
<nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" class="flex-grow h-full flex justify-center items-center" :class="paramId === 'series' ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||
<nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" class="flex-grow h-full flex justify-center items-center" :class="isSeriesPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||
<p class="text-sm">Series</p>
|
||||
</nuxt-link>
|
||||
<nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="flex-grow h-full flex justify-center items-center" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||
<nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="flex-grow h-full flex justify-center items-center" :class="isCollectionsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||
<p class="text-sm">Collections</p>
|
||||
</nuxt-link>
|
||||
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="flex-grow h-full flex justify-center items-center" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||
@@ -42,17 +42,11 @@
|
||||
</div>
|
||||
<div class="flex-grow hidden sm:inline-block" />
|
||||
|
||||
<ui-checkbox v-show="showSortFilters && !isPodcast" v-model="settings.collapseSeries" label="Collapse Series" checkbox-bg="bg" check-color="white" small class="mr-2" @input="updateCollapseSeries" />
|
||||
<controls-filter-select v-show="showSortFilters" v-model="settings.filterBy" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateFilter" />
|
||||
<controls-order-select v-show="showSortFilters" v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateOrder" />
|
||||
<!-- <div v-show="showSortFilters" class="h-7 ml-4 flex border border-white border-opacity-25 rounded-md">
|
||||
<div class="h-full px-2 text-white flex items-center rounded-l-md hover:bg-primary hover:bg-opacity-75 cursor-pointer" :class="isGridMode ? 'bg-primary' : 'text-opacity-70'" @click="$emit('update:viewMode', 'grid')">
|
||||
<span class="material-icons" style="font-size: 1.4rem">view_module</span>
|
||||
</div>
|
||||
<div class="h-full px-2 text-white flex items-center rounded-r-md hover:bg-primary hover:bg-opacity-75 cursor-pointer" :class="!isGridMode ? 'bg-primary' : 'text-opacity-70'" @click="$emit('update:viewMode', 'list')">
|
||||
<span class="material-icons" style="font-size: 1.4rem">view_list</span>
|
||||
</div>
|
||||
</div> -->
|
||||
<ui-checkbox v-if="isLibraryPage && !isPodcastLibrary" v-model="settings.collapseSeries" label="Collapse Series" checkbox-bg="bg" check-color="white" small class="mr-2" @input="updateCollapseSeries" />
|
||||
<controls-library-filter-select v-if="isLibraryPage" v-model="settings.filterBy" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateFilter" />
|
||||
<controls-library-sort-select v-if="isLibraryPage" v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateOrder" />
|
||||
<controls-library-filter-select v-if="isSeriesPage" v-model="seriesFilterBy" is-series class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateSeriesFilter" />
|
||||
<controls-sort-select v-if="isSeriesPage" v-model="seriesSortBy" :descending.sync="seriesSortDesc" :items="seriesSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateSeriesSort" />
|
||||
|
||||
<ui-btn v-if="isIssuesFilter && userCanDelete" :loading="processingIssues" color="error" small class="ml-4" @click="removeAllIssues">Remove All {{ numShowing }} {{ entityName }}</ui-btn>
|
||||
</template>
|
||||
@@ -79,7 +73,6 @@ export default {
|
||||
default: () => null
|
||||
},
|
||||
searchQuery: String,
|
||||
viewMode: String,
|
||||
authors: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
@@ -90,11 +83,27 @@ export default {
|
||||
settings: {},
|
||||
hasInit: false,
|
||||
totalEntities: 0,
|
||||
keywordFilter: null,
|
||||
keywordTimeout: null,
|
||||
processingSeries: false,
|
||||
processingIssues: false,
|
||||
processingAuthors: false
|
||||
processingAuthors: false,
|
||||
seriesSortItems: [
|
||||
{
|
||||
text: 'Name',
|
||||
value: 'name'
|
||||
},
|
||||
{
|
||||
text: 'Number of Books',
|
||||
value: 'numBooks'
|
||||
},
|
||||
{
|
||||
text: 'Date Added',
|
||||
value: 'addedAt'
|
||||
},
|
||||
{
|
||||
text: 'Total Duration',
|
||||
value: 'totalDuration'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -107,28 +116,6 @@ export default {
|
||||
userCanUpdate() {
|
||||
return this.$store.getters['user/getUserCanUpdate']
|
||||
},
|
||||
isPodcast() {
|
||||
return this.$store.getters['libraries/getCurrentLibraryMediaType'] == 'podcast'
|
||||
},
|
||||
isGridMode() {
|
||||
return this.viewMode === 'grid'
|
||||
},
|
||||
showSortFilters() {
|
||||
return this.page === ''
|
||||
},
|
||||
numShowing() {
|
||||
return this.totalEntities
|
||||
},
|
||||
entityName() {
|
||||
if (this.isPodcast) return 'Podcasts'
|
||||
if (!this.page) return 'Books'
|
||||
if (this.page === 'series') return 'Series'
|
||||
if (this.page === 'collections') return 'Collections'
|
||||
return ''
|
||||
},
|
||||
paramId() {
|
||||
return this.$route.params ? this.$route.params.id || '' : ''
|
||||
},
|
||||
currentLibraryId() {
|
||||
return this.$store.state.libraries.currentLibraryId
|
||||
},
|
||||
@@ -138,14 +125,30 @@ export default {
|
||||
isPodcastLibrary() {
|
||||
return this.currentLibraryMediaType === 'podcast'
|
||||
},
|
||||
homePage() {
|
||||
isLibraryPage() {
|
||||
return this.page === ''
|
||||
},
|
||||
isSeriesPage() {
|
||||
return this.page === 'series'
|
||||
},
|
||||
isCollectionsPage() {
|
||||
return this.page === 'collections'
|
||||
},
|
||||
isHomePage() {
|
||||
return this.$route.name === 'library-library'
|
||||
},
|
||||
libraryBookshelfPage() {
|
||||
return this.$route.name === 'library-library-bookshelf-id'
|
||||
isPodcastSearchPage() {
|
||||
return this.$route.name === 'library-library-podcast-search'
|
||||
},
|
||||
showLibrary() {
|
||||
return this.libraryBookshelfPage && this.paramId === '' && !this.showingIssues
|
||||
numShowing() {
|
||||
return this.totalEntities
|
||||
},
|
||||
entityName() {
|
||||
if (this.isPodcastLibrary) return 'Podcasts'
|
||||
if (!this.page) return 'Books'
|
||||
if (this.isSeriesPage) return 'Series'
|
||||
if (this.isCollectionsPage) return 'Collections'
|
||||
return ''
|
||||
},
|
||||
seriesName() {
|
||||
return this.selectedSeries ? this.selectedSeries.name : null
|
||||
@@ -166,8 +169,29 @@ export default {
|
||||
isIssuesFilter() {
|
||||
return this.filterBy === 'issues' && this.$route.query.filter === 'issues'
|
||||
},
|
||||
isPodcastSearchPage() {
|
||||
return this.$route.name === 'library-library-podcast-search'
|
||||
seriesSortBy: {
|
||||
get() {
|
||||
return this.$store.state.libraries.seriesSortBy
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit('libraries/setSeriesSortBy', val)
|
||||
}
|
||||
},
|
||||
seriesSortDesc: {
|
||||
get() {
|
||||
return this.$store.state.libraries.seriesSortDesc
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit('libraries/setSeriesSortDesc', val)
|
||||
}
|
||||
},
|
||||
seriesFilterBy: {
|
||||
get() {
|
||||
return this.$store.state.libraries.seriesFilterBy
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit('libraries/setSeriesFilterBy', val)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -247,6 +271,12 @@ export default {
|
||||
updateFilter() {
|
||||
this.saveSettings()
|
||||
},
|
||||
updateSeriesSort() {
|
||||
this.$eventBus.$emit('series-sort-updated')
|
||||
},
|
||||
updateSeriesFilter() {
|
||||
this.$eventBus.$emit('series-sort-updated')
|
||||
},
|
||||
updateCollapseSeries() {
|
||||
this.saveSettings()
|
||||
},
|
||||
@@ -263,15 +293,6 @@ export default {
|
||||
},
|
||||
setBookshelfTotalEntities(totalEntities) {
|
||||
this.totalEntities = totalEntities
|
||||
},
|
||||
keywordFilterInput() {
|
||||
clearTimeout(this.keywordTimeout)
|
||||
this.keywordTimeout = setTimeout(() => {
|
||||
this.keywordUpdated(this.keywordFilter)
|
||||
}, 1000)
|
||||
},
|
||||
keywordUpdated() {
|
||||
this.$eventBus.$emit('bookshelf-keyword-filter', this.keywordFilter)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
@@ -61,7 +61,8 @@ export default {
|
||||
keywordFilter: null,
|
||||
currScrollTop: 0,
|
||||
resizeTimeout: null,
|
||||
mountWindowWidth: 0
|
||||
mountWindowWidth: 0,
|
||||
lastItemIndexSelected: -1
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -97,6 +98,15 @@ export default {
|
||||
if (!this.page) return 'books'
|
||||
return this.page
|
||||
},
|
||||
seriesSortBy() {
|
||||
return this.$store.state.libraries.seriesSortBy
|
||||
},
|
||||
seriesSortDesc() {
|
||||
return this.$store.state.libraries.seriesSortDesc
|
||||
},
|
||||
seriesFilterBy() {
|
||||
return this.$store.state.libraries.seriesFilterBy
|
||||
},
|
||||
orderBy() {
|
||||
return this.$store.getters['user/getUserSetting']('orderBy')
|
||||
},
|
||||
@@ -122,7 +132,7 @@ export default {
|
||||
return this.$store.getters['getBookshelfView']
|
||||
},
|
||||
isAlternativeBookshelfView() {
|
||||
return this.bookshelfView === this.$constants.BookshelfView.TITLES
|
||||
return this.bookshelfView === this.$constants.BookshelfView.DETAIL
|
||||
},
|
||||
hasFilter() {
|
||||
return this.filterBy && this.filterBy !== 'all'
|
||||
@@ -212,9 +222,55 @@ export default {
|
||||
this.updateBookSelectionMode(false)
|
||||
this.isSelectionMode = false
|
||||
},
|
||||
selectEntity(entity) {
|
||||
selectEntity(entity, shiftKey) {
|
||||
if (this.entityName === 'books' || this.entityName === 'series-books') {
|
||||
this.$store.commit('toggleLibraryItemSelected', entity.id)
|
||||
var indexOf = this.entities.findIndex((ent) => ent && ent.id === entity.id)
|
||||
const lastLastItemIndexSelected = this.lastItemIndexSelected
|
||||
if (!this.selectedLibraryItems.includes(entity.id)) {
|
||||
this.lastItemIndexSelected = indexOf
|
||||
} else {
|
||||
this.lastItemIndexSelected = -1
|
||||
}
|
||||
|
||||
if (shiftKey && lastLastItemIndexSelected >= 0) {
|
||||
var loopStart = indexOf
|
||||
var loopEnd = lastLastItemIndexSelected
|
||||
if (indexOf > lastLastItemIndexSelected) {
|
||||
loopStart = lastLastItemIndexSelected
|
||||
loopEnd = indexOf
|
||||
}
|
||||
|
||||
var isSelecting = false
|
||||
// If any items in this range is not selected then select all otherwise unselect all
|
||||
for (let i = loopStart; i <= loopEnd; i++) {
|
||||
const thisEntity = this.entities[i]
|
||||
if (thisEntity && !thisEntity.collapsedSeries) {
|
||||
if (!this.selectedLibraryItems.includes(thisEntity.id)) {
|
||||
isSelecting = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isSelecting) this.lastItemIndexSelected = indexOf
|
||||
|
||||
for (let i = loopStart; i <= loopEnd; i++) {
|
||||
const thisEntity = this.entities[i]
|
||||
if (thisEntity.collapsedSeries) {
|
||||
console.warn('Ignoring collapsed series')
|
||||
continue
|
||||
}
|
||||
|
||||
const entityComponentRef = this.entityComponentRefs[i]
|
||||
if (thisEntity && entityComponentRef) {
|
||||
entityComponentRef.selected = isSelecting
|
||||
this.$store.commit('setLibraryItemSelected', { libraryItemId: thisEntity.id, selected: isSelecting })
|
||||
} else {
|
||||
console.error('Invalid entity index', i)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.$store.commit('toggleLibraryItemSelected', entity.id)
|
||||
}
|
||||
|
||||
var newIsSelectionMode = !!this.selectedLibraryItems.length
|
||||
if (this.isSelectionMode !== newIsSelectionMode) {
|
||||
@@ -229,6 +285,9 @@ export default {
|
||||
this.entityComponentRefs[key].setSelectionMode(isSelectionMode)
|
||||
}
|
||||
}
|
||||
if (!isSelectionMode) {
|
||||
this.lastItemIndexSelected = -1
|
||||
}
|
||||
},
|
||||
async fetchEntites(page = 0) {
|
||||
var startIndex = page * this.booksPerFetch
|
||||
@@ -367,12 +426,16 @@ export default {
|
||||
this.$nextTick(this.remountEntities)
|
||||
},
|
||||
buildSearchParams() {
|
||||
if (this.page === 'search' || this.page === 'series' || this.page === 'collections') {
|
||||
if (this.page === 'search' || this.page === 'collections') {
|
||||
return ''
|
||||
}
|
||||
|
||||
let searchParams = new URLSearchParams()
|
||||
if (this.page === 'series-books') {
|
||||
if (this.page === 'series') {
|
||||
searchParams.set('sort', this.seriesSortBy)
|
||||
searchParams.set('desc', this.seriesSortDesc ? 1 : 0)
|
||||
searchParams.set('filter', this.seriesFilterBy)
|
||||
} else if (this.page === 'series-books') {
|
||||
searchParams.set('filter', `series.${this.$encode(this.seriesId)}`)
|
||||
} else {
|
||||
if (this.filterBy && this.filterBy !== 'all') {
|
||||
@@ -408,6 +471,12 @@ export default {
|
||||
|
||||
return false
|
||||
},
|
||||
seriesSortUpdated() {
|
||||
var wasUpdated = this.checkUpdateSearchParams()
|
||||
if (wasUpdated) {
|
||||
this.resetEntities()
|
||||
}
|
||||
},
|
||||
settingsUpdated(settings) {
|
||||
var wasUpdated = this.checkUpdateSearchParams()
|
||||
if (wasUpdated) {
|
||||
@@ -419,10 +488,7 @@ export default {
|
||||
scroll(e) {
|
||||
if (!e || !e.target) return
|
||||
var { scrollTop } = e.target
|
||||
// clearTimeout(this.scrollTimeout)
|
||||
// this.scrollTimeout = setTimeout(() => {
|
||||
this.handleScroll(scrollTop)
|
||||
// }, 250)
|
||||
},
|
||||
libraryItemAdded(libraryItem) {
|
||||
console.log('libraryItem added', libraryItem)
|
||||
@@ -554,7 +620,8 @@ export default {
|
||||
}
|
||||
})
|
||||
|
||||
this.$eventBus.$on('bookshelf-clear-selection', this.clearSelectedEntities)
|
||||
this.$eventBus.$on('series-sort-updated', this.seriesSortUpdated)
|
||||
this.$eventBus.$on('bookshelf_clear_selection', this.clearSelectedEntities)
|
||||
this.$eventBus.$on('socket_init', this.socketInit)
|
||||
|
||||
this.$store.commit('user/addSettingsListener', { id: 'lazy-bookshelf', meth: this.settingsUpdated })
|
||||
@@ -578,7 +645,9 @@ export default {
|
||||
if (bookshelf) {
|
||||
bookshelf.removeEventListener('scroll', this.scroll)
|
||||
}
|
||||
this.$eventBus.$off('bookshelf-clear-selection', this.clearSelectedEntities)
|
||||
|
||||
this.$eventBus.$off('series-sort-updated', this.seriesSortUpdated)
|
||||
this.$eventBus.$off('bookshelf_clear_selection', this.clearSelectedEntities)
|
||||
this.$eventBus.$off('socket_init', this.socketInit)
|
||||
|
||||
this.$store.commit('user/removeSettingsListener', 'lazy-bookshelf')
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
</nuxt-link>
|
||||
|
||||
<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" />
|
||||
<span class="abs-icons icon-podcast text-xl"></span>
|
||||
|
||||
<p class="font-book pt-1.5" style="font-size: 0.9rem">Search</p>
|
||||
|
||||
|
||||
@@ -37,6 +37,8 @@
|
||||
|
||||
<!-- No progress shown for collapsed series in library and podcasts (unless showing podcast episode) -->
|
||||
<div v-if="!booksInSeries && (!isPodcast || episodeProgress)" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div>
|
||||
<!-- Finished progress bar for collapsed series -->
|
||||
<div v-else-if="booksInSeries && seriesIsFinished" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b bg-success" :style="{ width: width * userProgressPercent + 'px' }"></div>
|
||||
|
||||
<!-- Overlay is not shown if collapsing series in library -->
|
||||
<div v-show="!booksInSeries && libraryItem && (isHovering || isSelectionMode || isMoreMenuOpen) && !processing" class="w-full h-full absolute top-0 left-0 z-10 bg-black rounded hidden md:block" :class="overlayWrapperClasslist">
|
||||
@@ -234,6 +236,10 @@ export default {
|
||||
// Only added to item object when collapseSeries is enabled
|
||||
return this.collapsedSeries ? this.collapsedSeries.numBooks : 0
|
||||
},
|
||||
libraryItemIdsInSeries() {
|
||||
// Only added to item object when collapseSeries is enabled
|
||||
return this.collapsedSeries ? this.collapsedSeries.libraryItemIds || [] : []
|
||||
},
|
||||
hasCover() {
|
||||
return !!this.media.coverPath
|
||||
},
|
||||
@@ -298,6 +304,12 @@ export default {
|
||||
itemIsFinished() {
|
||||
return this.userProgress ? !!this.userProgress.isFinished : false
|
||||
},
|
||||
seriesIsFinished() {
|
||||
return !this.libraryItemIdsInSeries.some((lid) => {
|
||||
const progress = this.store.getters['user/getUserMediaProgress'](lid)
|
||||
return !progress || !progress.isFinished
|
||||
})
|
||||
},
|
||||
showError() {
|
||||
if (this.recentEpisode) return false // Dont show podcast error on episode card
|
||||
return this.numInvalidAudioFiles || this.numMissingParts || this.isMissing || this.isInvalid
|
||||
@@ -465,7 +477,7 @@ export default {
|
||||
},
|
||||
isAlternativeBookshelfView() {
|
||||
var constants = this.$constants || this.$nuxt.$constants
|
||||
return this.bookshelfView === constants.BookshelfView.TITLES
|
||||
return this.bookshelfView === constants.BookshelfView.DETAIL
|
||||
},
|
||||
isAuthorBookshelfView() {
|
||||
var constants = this.$constants || this.$nuxt.$constants
|
||||
@@ -707,10 +719,10 @@ export default {
|
||||
console.log('Got library itemn', libraryItem)
|
||||
this.store.commit('showEReader', libraryItem)
|
||||
},
|
||||
selectBtnClick() {
|
||||
selectBtnClick(evt) {
|
||||
if (this.processingBatch) return
|
||||
this.selected = !this.selected
|
||||
this.$emit('select', this.libraryItem)
|
||||
this.$emit('select', { entity: this.libraryItem, shiftKey: evt.shiftKey })
|
||||
},
|
||||
async play() {
|
||||
var eventBus = this.$eventBus || this.$nuxt.$eventBus
|
||||
|
||||
@@ -68,7 +68,7 @@ export default {
|
||||
},
|
||||
isAlternativeBookshelfView() {
|
||||
const constants = this.$constants || this.$nuxt.$constants
|
||||
return this.bookshelfView == constants.BookshelfView.TITLES
|
||||
return this.bookshelfView == constants.BookshelfView.DETAIL
|
||||
},
|
||||
userCanUpdate() {
|
||||
return this.store.getters['user/getUserCanUpdate']
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div ref="card" :id="`series-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||
<div ref="card" :id="`series-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="rounded-sm z-10 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
||||
<div class="w-full h-full bg-primary relative rounded overflow-hidden z-0">
|
||||
<covers-group-cover v-if="series" ref="cover" :id="seriesId" :name="displayTitle" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
@@ -20,6 +20,7 @@
|
||||
</div>
|
||||
<div v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
|
||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ displayTitle }}</p>
|
||||
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -40,7 +41,8 @@ export default {
|
||||
type: Object,
|
||||
default: () => null
|
||||
},
|
||||
sortingIgnorePrefix: Boolean
|
||||
sortingIgnorePrefix: Boolean,
|
||||
orderBy: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -52,6 +54,9 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
dateFormat() {
|
||||
return this.store.state.serverSettings.dateFormat
|
||||
},
|
||||
labelFontSize() {
|
||||
if (this.width < 160) return 0.75
|
||||
return 0.875
|
||||
@@ -73,12 +78,24 @@ export default {
|
||||
if (this.sortingIgnorePrefix) return this.nameIgnorePrefix || this.title
|
||||
return this.title
|
||||
},
|
||||
displaySortLine() {
|
||||
if (this.orderBy === 'addedAt') {
|
||||
// return this.addedAt
|
||||
return 'Added ' + this.$formatDate(this.addedAt, this.dateFormat)
|
||||
} else if (this.orderBy === 'totalDuration') {
|
||||
return 'Duration: ' + this.$elapsedPrettyExtended(this.totalDuration, false)
|
||||
}
|
||||
return null
|
||||
},
|
||||
books() {
|
||||
return this.series ? this.series.books || [] : []
|
||||
},
|
||||
addedAt() {
|
||||
return this.series ? this.series.addedAt : 0
|
||||
},
|
||||
totalDuration() {
|
||||
return this.series ? this.series.totalDuration : 0
|
||||
},
|
||||
seriesBookProgress() {
|
||||
return this.books
|
||||
.map((libraryItem) => {
|
||||
@@ -107,7 +124,7 @@ export default {
|
||||
},
|
||||
isAlternativeBookshelfView() {
|
||||
const constants = this.$constants || this.$nuxt.$constants
|
||||
return this.bookshelfView == constants.BookshelfView.TITLES
|
||||
return this.bookshelfView == constants.BookshelfView.DETAIL
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div ref="wrapper" class="relative" v-click-outside="clickOutside">
|
||||
<button type="button" class="relative w-full h-full bg-fg border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu">
|
||||
<button type="button" class="relative w-full h-full border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu">
|
||||
<span class="flex items-center justify-between">
|
||||
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
|
||||
<span class="block truncate text-xs">{{ selectedText }}</span>
|
||||
</span>
|
||||
<span v-if="selected === 'all'" class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||
<svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
@@ -15,42 +15,12 @@
|
||||
</button>
|
||||
|
||||
<div v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-96 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
|
||||
<ul v-show="!sublist" class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||
<template v-for="item in selectItems">
|
||||
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||
<template v-for="item in items">
|
||||
<li :key="item.value" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" :class="item.value === selected ? 'bg-primary bg-opacity-50' : ''" role="option" @click="clickedOption(item)">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-normal ml-3 block truncate text-sm md:text-base">{{ item.text }}</span>
|
||||
</div>
|
||||
<div v-if="item.sublist" class="absolute right-1 top-0 bottom-0 h-full flex items-center">
|
||||
<span class="material-icons">arrow_right</span>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
<ul v-show="sublist" class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||
<li class="text-gray-50 select-none relative py-2 pl-9 cursor-pointer hover:bg-black-400" role="option" @click="sublist = null">
|
||||
<div class="absolute left-1 top-0 bottom-0 h-full flex items-center">
|
||||
<span class="material-icons">arrow_left</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-normal ml-3 block truncate">Back</span>
|
||||
</div>
|
||||
</li>
|
||||
<li v-if="!sublistItems.length" class="text-gray-400 select-none relative px-2" role="option">
|
||||
<div class="flex items-center justify-center">
|
||||
<span class="font-normal block truncate py-2">No {{ sublist }}</span>
|
||||
</div>
|
||||
</li>
|
||||
<li v-else-if="sublist === 'series'" class="text-gray-50 select-none relative px-2 cursor-pointer hover:bg-black-400" role="option" @click="clickedSublistOption($encode('No Series'))">
|
||||
<div class="flex items-center">
|
||||
<span class="font-normal block truncate py-2 text-xs text-white text-opacity-80">No Series</span>
|
||||
</div>
|
||||
</li>
|
||||
<template v-for="item in sublistItems">
|
||||
<li :key="item.value" class="text-gray-50 select-none relative px-2 cursor-pointer hover:bg-black-400" :class="`${sublist}.${item.value}` === selected ? 'bg-primary bg-opacity-50' : ''" role="option" @click="clickedSublistOption(item.value)">
|
||||
<div class="flex items-center">
|
||||
<span class="font-normal truncate py-2 text-xs">{{ item.text }}</span>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
@@ -61,97 +31,15 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: String
|
||||
value: String,
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showMenu: false,
|
||||
sublist: null,
|
||||
bookItems: [
|
||||
{
|
||||
text: 'All',
|
||||
value: 'all'
|
||||
},
|
||||
{
|
||||
text: 'Genre',
|
||||
value: 'genres',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Tag',
|
||||
value: 'tags',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Series',
|
||||
value: 'series',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Authors',
|
||||
value: 'authors',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Narrator',
|
||||
value: 'narrators',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Language',
|
||||
value: 'languages',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Progress',
|
||||
value: 'progress',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Missing',
|
||||
value: 'missing',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Issues',
|
||||
value: 'issues',
|
||||
sublist: false
|
||||
},
|
||||
{
|
||||
text: 'RSS Feed Open',
|
||||
value: 'feed-open',
|
||||
sublist: false
|
||||
}
|
||||
],
|
||||
podcastItems: [
|
||||
{
|
||||
text: 'All',
|
||||
value: 'all'
|
||||
},
|
||||
{
|
||||
text: 'Genre',
|
||||
value: 'genres',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Tag',
|
||||
value: 'tags',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Issues',
|
||||
value: 'issues',
|
||||
sublist: false
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
showMenu(newVal) {
|
||||
if (!newVal) {
|
||||
if (this.sublist && !this.selectedItemSublist) this.sublist = null
|
||||
if (!this.sublist && this.selectedItemSublist) this.sublist = this.selectedItemSublist
|
||||
}
|
||||
showMenu: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -163,81 +51,10 @@ export default {
|
||||
this.$emit('input', val)
|
||||
}
|
||||
},
|
||||
isPodcast() {
|
||||
return this.$store.getters['libraries/getCurrentLibraryMediaType'] == 'podcast'
|
||||
},
|
||||
selectItems() {
|
||||
if (this.isPodcast) return this.podcastItems
|
||||
return this.bookItems
|
||||
},
|
||||
selectedItemSublist() {
|
||||
return this.selected && this.selected.includes('.') ? this.selected.split('.')[0] : false
|
||||
},
|
||||
selectedText() {
|
||||
if (!this.selected) return ''
|
||||
var parts = this.selected.split('.')
|
||||
var filterName = this.selectItems.find((i) => i.value === parts[0])
|
||||
var filterValue = null
|
||||
if (parts.length > 1) {
|
||||
var decoded = this.$decode(parts[1])
|
||||
if (decoded.startsWith('aut_')) {
|
||||
var author = this.authors.find((au) => au.id == decoded)
|
||||
if (author) filterValue = author.name
|
||||
} else if (decoded.startsWith('ser_')) {
|
||||
var series = this.series.find((se) => se.id == decoded)
|
||||
if (series) filterValue = series.name
|
||||
} else {
|
||||
filterValue = decoded
|
||||
}
|
||||
}
|
||||
if (filterName && filterValue) {
|
||||
return `${filterName.text}: ${filterValue}`
|
||||
} else if (filterName) {
|
||||
return filterName.text
|
||||
} else if (filterValue) {
|
||||
return filterValue
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
},
|
||||
genres() {
|
||||
return this.filterData.genres || []
|
||||
},
|
||||
tags() {
|
||||
return this.filterData.tags || []
|
||||
},
|
||||
series() {
|
||||
return this.filterData.series || []
|
||||
},
|
||||
authors() {
|
||||
return this.filterData.authors || []
|
||||
},
|
||||
narrators() {
|
||||
return this.filterData.narrators || []
|
||||
},
|
||||
languages() {
|
||||
return this.filterData.languages || []
|
||||
},
|
||||
progress() {
|
||||
return ['Finished', 'In Progress', 'Not Started', 'Not Finished']
|
||||
},
|
||||
missing() {
|
||||
return ['ASIN', 'ISBN', 'Subtitle', 'Author', 'Publish Year', 'Series', 'Description', 'Genres', 'Tags', 'Narrator', 'Publisher', 'Language']
|
||||
},
|
||||
sublistItems() {
|
||||
return (this[this.sublist] || []).map((item) => {
|
||||
if (typeof item === 'string') {
|
||||
return {
|
||||
text: item,
|
||||
value: this.$encode(item)
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
text: item.name,
|
||||
value: this.$encode(item.id)
|
||||
}
|
||||
}
|
||||
})
|
||||
const filter = this.items.find((i) => i.value === this.selected)
|
||||
return filter ? filter.text : ''
|
||||
},
|
||||
filterData() {
|
||||
return this.$store.state.libraries.filterData || {}
|
||||
@@ -250,18 +67,9 @@ export default {
|
||||
this.$nextTick(() => this.$emit('change', 'all'))
|
||||
},
|
||||
clickOutside() {
|
||||
if (!this.selectedItemSublist) this.sublist = null
|
||||
this.showMenu = false
|
||||
},
|
||||
clickedSublistOption(item) {
|
||||
this.clickedOption({ value: `${this.sublist}.${item}` })
|
||||
},
|
||||
clickedOption(option) {
|
||||
if (option.sublist) {
|
||||
this.sublist = option.value
|
||||
return
|
||||
}
|
||||
|
||||
var val = option.value
|
||||
if (this.selected === val) {
|
||||
this.showMenu = false
|
||||
|
||||
314
client/components/controls/LibraryFilterSelect.vue
Normal file
314
client/components/controls/LibraryFilterSelect.vue
Normal file
@@ -0,0 +1,314 @@
|
||||
<template>
|
||||
<div ref="wrapper" class="relative" v-click-outside="clickOutside">
|
||||
<button type="button" class="relative w-full h-full bg-fg border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu">
|
||||
<span class="flex items-center justify-between">
|
||||
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
|
||||
</span>
|
||||
<span v-if="selected === 'all'" class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||
<svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</span>
|
||||
<div v-else class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 cursor-pointer text-gray-400 hover:text-gray-300" @mousedown.stop @mouseup.stop @click.stop.prevent="clearSelected">
|
||||
<span class="material-icons" style="font-size: 1.1rem">close</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-96 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
|
||||
<ul v-show="!sublist" class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||
<template v-for="item in selectItems">
|
||||
<li :key="item.value" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" :class="item.value === selected ? 'bg-primary bg-opacity-50' : ''" role="option" @click="clickedOption(item)">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-normal ml-3 block truncate text-sm md:text-base">{{ item.text }}</span>
|
||||
</div>
|
||||
<div v-if="item.sublist" class="absolute right-1 top-0 bottom-0 h-full flex items-center">
|
||||
<span class="material-icons">arrow_right</span>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
<ul v-show="sublist" class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||
<li class="text-gray-50 select-none relative py-2 pl-9 cursor-pointer hover:bg-black-400" role="option" @click="sublist = null">
|
||||
<div class="absolute left-1 top-0 bottom-0 h-full flex items-center">
|
||||
<span class="material-icons">arrow_left</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-normal ml-3 block truncate">Back</span>
|
||||
</div>
|
||||
</li>
|
||||
<li v-if="!sublistItems.length" class="text-gray-400 select-none relative px-2" role="option">
|
||||
<div class="flex items-center justify-center">
|
||||
<span class="font-normal block truncate py-2">No {{ sublist }}</span>
|
||||
</div>
|
||||
</li>
|
||||
<li v-else-if="sublist === 'series'" class="text-gray-50 select-none relative px-2 cursor-pointer hover:bg-black-400" role="option" @click="clickedSublistOption($encode('No Series'))">
|
||||
<div class="flex items-center">
|
||||
<span class="font-normal block truncate py-2 text-xs text-white text-opacity-80">No Series</span>
|
||||
</div>
|
||||
</li>
|
||||
<template v-for="item in sublistItems">
|
||||
<li :key="item.value" class="text-gray-50 select-none relative px-2 cursor-pointer hover:bg-black-400" :class="`${sublist}.${item.value}` === selected ? 'bg-primary bg-opacity-50' : ''" role="option" @click="clickedSublistOption(item.value)">
|
||||
<div class="flex items-center">
|
||||
<span class="font-normal truncate py-2 text-xs">{{ item.text }}</span>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: String,
|
||||
isSeries: Boolean
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showMenu: false,
|
||||
sublist: null,
|
||||
seriesItems: [
|
||||
{
|
||||
text: 'All',
|
||||
value: 'all'
|
||||
},
|
||||
{
|
||||
text: 'Genre',
|
||||
value: 'genres',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Tag',
|
||||
value: 'tags',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Authors',
|
||||
value: 'authors',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Narrator',
|
||||
value: 'narrators',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Language',
|
||||
value: 'languages',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Series Progress',
|
||||
value: 'progress',
|
||||
sublist: true
|
||||
}
|
||||
],
|
||||
bookItems: [
|
||||
{
|
||||
text: 'All',
|
||||
value: 'all'
|
||||
},
|
||||
{
|
||||
text: 'Genre',
|
||||
value: 'genres',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Tag',
|
||||
value: 'tags',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Series',
|
||||
value: 'series',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Authors',
|
||||
value: 'authors',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Narrator',
|
||||
value: 'narrators',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Language',
|
||||
value: 'languages',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Progress',
|
||||
value: 'progress',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Missing',
|
||||
value: 'missing',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Issues',
|
||||
value: 'issues',
|
||||
sublist: false
|
||||
},
|
||||
{
|
||||
text: 'RSS Feed Open',
|
||||
value: 'feed-open',
|
||||
sublist: false
|
||||
}
|
||||
],
|
||||
podcastItems: [
|
||||
{
|
||||
text: 'All',
|
||||
value: 'all'
|
||||
},
|
||||
{
|
||||
text: 'Genre',
|
||||
value: 'genres',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Tag',
|
||||
value: 'tags',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Issues',
|
||||
value: 'issues',
|
||||
sublist: false
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
showMenu(newVal) {
|
||||
if (!newVal) {
|
||||
if (this.sublist && !this.selectedItemSublist) this.sublist = null
|
||||
if (!this.sublist && this.selectedItemSublist) this.sublist = this.selectedItemSublist
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selected: {
|
||||
get() {
|
||||
return this.value
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('input', val)
|
||||
}
|
||||
},
|
||||
isPodcast() {
|
||||
return this.$store.getters['libraries/getCurrentLibraryMediaType'] == 'podcast'
|
||||
},
|
||||
selectItems() {
|
||||
if (this.isSeries) return this.seriesItems
|
||||
if (this.isPodcast) return this.podcastItems
|
||||
return this.bookItems
|
||||
},
|
||||
selectedItemSublist() {
|
||||
return this.selected && this.selected.includes('.') ? this.selected.split('.')[0] : false
|
||||
},
|
||||
selectedText() {
|
||||
if (!this.selected) return ''
|
||||
var parts = this.selected.split('.')
|
||||
var filterName = this.selectItems.find((i) => i.value === parts[0])
|
||||
var filterValue = null
|
||||
if (parts.length > 1) {
|
||||
var decoded = this.$decode(parts[1])
|
||||
if (decoded.startsWith('aut_')) {
|
||||
var author = this.authors.find((au) => au.id == decoded)
|
||||
if (author) filterValue = author.name
|
||||
} else if (decoded.startsWith('ser_')) {
|
||||
var series = this.series.find((se) => se.id == decoded)
|
||||
if (series) filterValue = series.name
|
||||
} else {
|
||||
filterValue = decoded
|
||||
}
|
||||
}
|
||||
if (filterName && filterValue) {
|
||||
return `${filterName.text}: ${filterValue}`
|
||||
} else if (filterName) {
|
||||
return filterName.text
|
||||
} else if (filterValue) {
|
||||
return filterValue
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
},
|
||||
genres() {
|
||||
return this.filterData.genres || []
|
||||
},
|
||||
tags() {
|
||||
return this.filterData.tags || []
|
||||
},
|
||||
series() {
|
||||
return this.filterData.series || []
|
||||
},
|
||||
authors() {
|
||||
return this.filterData.authors || []
|
||||
},
|
||||
narrators() {
|
||||
return this.filterData.narrators || []
|
||||
},
|
||||
languages() {
|
||||
return this.filterData.languages || []
|
||||
},
|
||||
progress() {
|
||||
return ['Finished', 'In Progress', 'Not Started', 'Not Finished']
|
||||
},
|
||||
missing() {
|
||||
return ['ASIN', 'ISBN', 'Subtitle', 'Author', 'Publish Year', 'Series', 'Description', 'Genres', 'Tags', 'Narrator', 'Publisher', 'Language']
|
||||
},
|
||||
sublistItems() {
|
||||
return (this[this.sublist] || []).map((item) => {
|
||||
if (typeof item === 'string') {
|
||||
return {
|
||||
text: item,
|
||||
value: this.$encode(item)
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
text: item.name,
|
||||
value: this.$encode(item.id)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
filterData() {
|
||||
return this.$store.state.libraries.filterData || {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clearSelected() {
|
||||
this.selected = 'all'
|
||||
this.showMenu = false
|
||||
this.$nextTick(() => this.$emit('change', 'all'))
|
||||
},
|
||||
clickOutside() {
|
||||
if (!this.selectedItemSublist) this.sublist = null
|
||||
this.showMenu = false
|
||||
},
|
||||
clickedSublistOption(item) {
|
||||
this.clickedOption({ value: `${this.sublist}.${item}` })
|
||||
},
|
||||
clickedOption(option) {
|
||||
if (option.sublist) {
|
||||
this.sublist = option.value
|
||||
return
|
||||
}
|
||||
|
||||
var val = option.value
|
||||
if (this.selected === val) {
|
||||
this.showMenu = false
|
||||
return
|
||||
}
|
||||
this.selected = val
|
||||
this.showMenu = false
|
||||
this.$nextTick(() => this.$emit('change', val))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div ref="wrapper" class="relative" v-click-outside="clickOutside">
|
||||
<button type="button" class="relative w-full h-full bg-fg border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu">
|
||||
<button type="button" class="relative w-full h-full border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu">
|
||||
<span class="flex items-center justify-between">
|
||||
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
|
||||
<span class="material-icons text-lg text-yellow-400">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
||||
@@ -26,29 +26,15 @@
|
||||
export default {
|
||||
props: {
|
||||
value: String,
|
||||
descending: Boolean
|
||||
descending: Boolean,
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showMenu: false,
|
||||
items: [
|
||||
{
|
||||
text: 'Pub Date',
|
||||
value: 'publishedAt'
|
||||
},
|
||||
{
|
||||
text: 'Title',
|
||||
value: 'title'
|
||||
},
|
||||
{
|
||||
text: 'Season',
|
||||
value: 'season'
|
||||
},
|
||||
{
|
||||
text: 'Episode',
|
||||
value: 'episode'
|
||||
}
|
||||
]
|
||||
showMenu: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -94,7 +94,8 @@ export default {
|
||||
return this.author
|
||||
},
|
||||
placeholderUrl() {
|
||||
return `${this.$config.routerBasePath}/book_placeholder.jpg`
|
||||
const config = this.$config || this.$nuxt.$config
|
||||
return `${config.routerBasePath}/book_placeholder.jpg`
|
||||
},
|
||||
fullCoverUrl() {
|
||||
if (!this.libraryItem) return null
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
<template>
|
||||
<svg fill="currentColor" class="h-full w-full p-px" viewBox="0 0 1978.03 2349.44">
|
||||
<path
|
||||
d="M2519.5,1438.39c-12.13-10.1-31-25-56.57-42.62V1197.31c0-505.94-410.15-916.09-916.1-916.09h0c-505.94,0-916.09,410.15-916.09,916.09v198.46c-25.57,17.66-44.44,32.52-56.57,42.62a45.45,45.45,0,0,0-16.35,34.95v237.74a45.45,45.45,0,0,0,16.35,35c28.28,23.54,93.18,72.92,194.22,123.55v23.11c0,62.32,40.21,112.85,89.8,112.85h0c49.59,0,89.8-50.53,89.8-112.85V1322.51c0-62.33-40.21-112.86-89.8-112.86h0c-47.51,0-86.4,46.38-89.58,105.07l-.22.11V1197.31c0-429.92,348.52-778.43,778.44-778.43h0c429.92,0,778.44,348.51,778.44,778.43v117.52l-.22-.11c-3.18-58.69-42.06-105.07-89.58-105.07h0c-49.59,0-89.79,50.53-89.79,112.86v570.18c0,62.32,40.2,112.85,89.79,112.85h0c49.6,0,89.8-50.53,89.8-112.85v-23.11c101.05-50.63,165.95-100,194.23-123.55a45.48,45.48,0,0,0,16.35-35V1473.34A45.48,45.48,0,0,0,2519.5,1438.39Z"
|
||||
transform="translate(-557.82 -281.22)"
|
||||
/>
|
||||
<path d="M1227.4,2429.63a108.47,108.47,0,0,0,108.47-108.47V1106.56A108.47,108.47,0,0,0,1227.4,998.08H1115.33a108.48,108.48,0,0,0-108.48,108.48v1214.6a108.47,108.47,0,0,0,108.48,108.47ZM1047.75,1289.38H1295v25.83H1047.75Z" transform="translate(-557.82 -281.22)" />
|
||||
<path d="M1602.87,2429.63a108.47,108.47,0,0,0,108.47-108.47V1106.56a108.47,108.47,0,0,0-108.47-108.48H1490.8a108.48,108.48,0,0,0-108.48,108.48v1214.6a108.47,108.47,0,0,0,108.48,108.47ZM1423.22,1289.38h247.22v25.83H1423.22Z" transform="translate(-557.82 -281.22)" />
|
||||
<path d="M1978.34,2429.63a108.47,108.47,0,0,0,108.47-108.47V1106.56a108.47,108.47,0,0,0-108.47-108.48H1866.27a108.48,108.48,0,0,0-108.48,108.48v1214.6a108.47,108.47,0,0,0,108.48,108.47ZM1798.69,1289.38h247.22v25.83H1798.69Z" transform="translate(-557.82 -281.22)" />
|
||||
<rect x="180.05" y="2185.95" width="1617.93" height="163.49" rx="81.74" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {},
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
@@ -1,16 +0,0 @@
|
||||
<template>
|
||||
<svg fill="currentColor" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M9 3V18H12V3H9M12 5L16 18L19 17L15 4L12 5M5 5V18H8V5H5M3 19V21H21V19H3Z" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {},
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
@@ -1,16 +0,0 @@
|
||||
<template>
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M6,19L9,15.14L11.14,17.72L14.14,13.86L18,19H6M6,4H11V12L8.5,10.5L6,12M18,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V4A2,2 0 0,0 18,2Z" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {},
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
@@ -1,16 +0,0 @@
|
||||
<template>
|
||||
<svg class="p-px" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {},
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
@@ -1,19 +0,0 @@
|
||||
<template>
|
||||
<svg class="p-px" viewBox="0 0 122.877 120.596">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M68.925,69.906v50.689H53.953V69.906c-4.918-2.662-8.259-7.867-8.259-13.854 c0-8.694,7.05-15.744,15.745-15.744c8.694,0,15.745,7.05,15.745,15.744C77.184,62.039,73.843,67.244,68.925,69.906L68.925,69.906z M39.32,11.165c2.916-1.438,4.111-4.969,2.673-7.882c-1.438-2.914-4.966-4.111-7.88-2.674C22.213,6.479,12.958,16.19,7.11,27.625 c-4.32,8.445-6.783,17.842-7.08,27.325c-0.299,9.563,1.587,19.223,5.973,28.114c5.401,10.953,14.558,20.695,28.039,27.592 c2.889,1.477,6.429,0.33,7.905-2.559c1.477-2.889,0.331-6.428-2.558-7.904c-11.037-5.645-18.486-13.525-22.833-22.334 c-3.506-7.111-5.014-14.857-4.774-22.539c0.243-7.757,2.256-15.442,5.79-22.348C22.304,23.721,29.76,15.879,39.32,11.165 L39.32,11.165z M88.765,0.608c-2.914-1.438-6.443-0.24-7.881,2.674c-1.438,2.914-0.242,6.445,2.674,7.882 c9.561,4.715,17.017,12.556,21.747,21.808c3.533,6.905,5.547,14.59,5.789,22.348c0.24,7.682-1.268,15.428-4.773,22.539 c-4.347,8.809-11.796,16.689-22.833,22.334c-2.889,1.477-4.034,5.016-2.558,7.904c1.476,2.889,5.016,4.035,7.905,2.559 c13.48-6.896,22.638-16.639,28.039-27.592c4.386-8.891,6.272-18.551,5.973-28.114c-0.297-9.483-2.76-18.88-7.079-27.325 C109.919,16.19,100.665,6.479,88.765,0.608L88.765,0.608z M82.791,26.505c-2.195-1.581-5.256-1.082-6.837,1.113 c-1.58,2.195-1.082,5.256,1.113,6.837c0.885,0.637,1.753,1.352,2.604,2.134c4.971,4.583,7.919,10.694,8.538,17.16 c0.626,6.524-1.111,13.437-5.518,19.552c-0.748,1.039-1.61,2.092-2.585,3.15c-1.835,1.992-1.708,5.098,0.287,6.932 c1.994,1.834,5.099,1.705,6.933-0.287c1.18-1.279,2.286-2.641,3.315-4.072c5.862-8.139,8.166-17.4,7.322-26.197 c-0.848-8.853-4.871-17.208-11.648-23.457C85.249,28.387,84.074,27.431,82.791,26.505L82.791,26.505z M45.81,34.458 c2.195-1.581,2.694-4.642,1.113-6.837c-1.581-2.195-4.642-2.694-6.837-1.114c-1.284,0.926-2.458,1.882-3.524,2.864 c-6.778,6.25-10.801,14.604-11.649,23.457c-0.844,8.796,1.46,18.06,7.323,26.199c1.031,1.43,2.136,2.791,3.315,4.07 c1.834,1.992,4.939,2.121,6.932,0.287c1.996-1.834,2.123-4.939,0.288-6.932c-0.975-1.059-1.837-2.111-2.585-3.15 c-4.406-6.115-6.144-13.027-5.518-19.551c0.619-6.465,3.567-12.577,8.538-17.16C44.058,35.81,44.926,35.095,45.81,34.458 L45.81,34.458z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {},
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
@@ -231,8 +231,10 @@ export default {
|
||||
}
|
||||
},
|
||||
selectTab(tab) {
|
||||
if (this.selectedTab === tab) return
|
||||
if (this.availableTabs.find((t) => t.id === tab)) {
|
||||
this.selectedTab = tab
|
||||
this.processing = false
|
||||
}
|
||||
},
|
||||
libraryItemUpdated(expandedLibraryItem) {
|
||||
|
||||
@@ -125,7 +125,7 @@ export default {
|
||||
return this.$store.state.scanners.providers
|
||||
},
|
||||
searchTitleLabel() {
|
||||
if (this.provider == 'audible') return 'Search Title or ASIN'
|
||||
if (this.provider.startsWith('audible')) return 'Search Title or ASIN'
|
||||
else if (this.provider == 'itunes') return 'Search Term'
|
||||
return 'Search Title'
|
||||
},
|
||||
|
||||
@@ -258,7 +258,7 @@ export default {
|
||||
return this.$store.state.scanners.providers
|
||||
},
|
||||
searchTitleLabel() {
|
||||
if (this.provider == 'audible') return 'Search Title or ASIN'
|
||||
if (this.provider.startsWith('audible')) return 'Search Title or ASIN'
|
||||
else if (this.provider == 'itunes') return 'Search Term'
|
||||
return 'Search Title'
|
||||
},
|
||||
@@ -312,7 +312,7 @@ export default {
|
||||
this.isProcessing = true
|
||||
this.lastSearch = searchQuery
|
||||
var searchEntity = this.isPodcast ? 'podcast' : 'books'
|
||||
var results = await this.$axios.$get(`/api/search/${searchEntity}?${searchQuery}`, { timeout: 10000 }).catch((error) => {
|
||||
var results = await this.$axios.$get(`/api/search/${searchEntity}?${searchQuery}`, { timeout: 20000 }).catch((error) => {
|
||||
console.error('Failed', error)
|
||||
return []
|
||||
})
|
||||
|
||||
@@ -19,6 +19,15 @@
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
<div v-if="enableAutoDownloadEpisodes" class="flex items-center py-2">
|
||||
<ui-text-input ref="maxEpisodesInput" type="number" v-model="newMaxNewEpisodesToDownload" no-spinner :padding-x="1" text-center class="w-10 text-base" @change="updateMaxNewEpisodesToDownload" />
|
||||
<ui-tooltip text="Value of 0 sets no max limit. When checking for new episodes this is the max number of episodes that will be downloaded.">
|
||||
<p class="pl-4 text-base">
|
||||
Max new episodes to download per check
|
||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
<widgets-cron-expression-builder ref="cronExpressionBuilder" v-if="enableAutoDownloadEpisodes" v-model="cronExpression" />
|
||||
</template>
|
||||
@@ -46,7 +55,8 @@ export default {
|
||||
return {
|
||||
enableAutoDownloadEpisodes: false,
|
||||
cronExpression: null,
|
||||
newMaxEpisodesToKeep: 0
|
||||
newMaxEpisodesToKeep: 0,
|
||||
newMaxNewEpisodesToDownload: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -82,8 +92,11 @@ export default {
|
||||
maxEpisodesToKeep() {
|
||||
return this.media.maxEpisodesToKeep
|
||||
},
|
||||
maxNewEpisodesToDownload() {
|
||||
return this.media.maxNewEpisodesToDownload
|
||||
},
|
||||
isUpdated() {
|
||||
return this.autoDownloadSchedule !== this.cronExpression || this.autoDownloadEpisodes !== this.enableAutoDownloadEpisodes || this.maxEpisodesToKeep !== Number(this.newMaxEpisodesToKeep)
|
||||
return this.autoDownloadSchedule !== this.cronExpression || this.autoDownloadEpisodes !== this.enableAutoDownloadEpisodes || this.maxEpisodesToKeep !== Number(this.newMaxEpisodesToKeep) || this.maxNewEpisodesToDownload !== Number(this.newMaxNewEpisodesToDownload)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -94,6 +107,13 @@ export default {
|
||||
this.newMaxEpisodesToKeep = Number(this.newMaxEpisodesToKeep)
|
||||
}
|
||||
},
|
||||
updateMaxNewEpisodesToDownload() {
|
||||
if (isNaN(this.newMaxNewEpisodesToDownload) || this.newMaxNewEpisodesToDownload < 0) {
|
||||
this.newMaxNewEpisodesToDownload = 0
|
||||
} else {
|
||||
this.newMaxNewEpisodesToDownload = Number(this.newMaxNewEpisodesToDownload)
|
||||
}
|
||||
},
|
||||
save() {
|
||||
// If custom expression input is focused then unfocus it instead of submitting
|
||||
if (this.$refs.cronExpressionBuilder && this.$refs.cronExpressionBuilder.checkBlurExpressionInput) {
|
||||
@@ -115,6 +135,9 @@ export default {
|
||||
if (this.newMaxEpisodesToKeep !== this.maxEpisodesToKeep) {
|
||||
updatePayload.maxEpisodesToKeep = this.newMaxEpisodesToKeep
|
||||
}
|
||||
if (this.newMaxNewEpisodesToDownload !== this.maxNewEpisodesToDownload) {
|
||||
updatePayload.maxNewEpisodesToDownload = this.newMaxNewEpisodesToDownload
|
||||
}
|
||||
|
||||
this.updateDetails(updatePayload)
|
||||
},
|
||||
@@ -139,6 +162,7 @@ export default {
|
||||
this.enableAutoDownloadEpisodes = this.autoDownloadEpisodes
|
||||
this.cronExpression = this.autoDownloadSchedule
|
||||
this.newMaxEpisodesToKeep = this.maxEpisodesToKeep
|
||||
this.newMaxNewEpisodesToDownload = this.maxNewEpisodesToDownload
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
@@ -29,9 +29,10 @@
|
||||
<div v-else-if="loadingFolders" class="py-12 text-center">
|
||||
<p>Loading folders...</p>
|
||||
</div>
|
||||
<div v-else class="py-12 text-center">
|
||||
<div v-else class="py-12 text-center max-w-sm mx-auto">
|
||||
<p class="text-lg mb-2">No Folders Available</p>
|
||||
<p class="text-gray-300">Note: folders already mapped will not be shown</p>
|
||||
<p class="text-gray-300 mb-2">Note: folders already mapped will not be shown</p>
|
||||
<p v-if="isDebian" class="text-red-400">Note: Folder picker for the debian install is not fully implemented. You should enter the path to your library directly.</p>
|
||||
</div>
|
||||
|
||||
<div class="w-full py-2">
|
||||
@@ -88,6 +89,12 @@ export default {
|
||||
...d
|
||||
}
|
||||
})
|
||||
},
|
||||
isDebian() {
|
||||
return this.Source == 'debian'
|
||||
},
|
||||
Source() {
|
||||
return this.$store.state.Source
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
<script>
|
||||
import Path from 'path'
|
||||
import { Archive } from 'libarchive.js/main.js'
|
||||
import { CompressedFile } from 'libarchive.js/src/compressed-file'
|
||||
|
||||
Archive.init({
|
||||
workerUrl: '/libarchive/worker-bundle.js'
|
||||
@@ -150,7 +151,11 @@ export default {
|
||||
responseType: 'blob'
|
||||
})
|
||||
const archive = await Archive.open(buff)
|
||||
this.filesObject = await archive.getFilesObject()
|
||||
const originalFilesObject = await archive.getFilesObject()
|
||||
// to support images in subfolders we need to flatten the object
|
||||
// ref: https://github.com/advplyr/audiobookshelf/issues/811
|
||||
this.filesObject = this.flattenFilesObject(originalFilesObject)
|
||||
console.log('Extracted files object', this.filesObject)
|
||||
var filenames = Object.keys(this.filesObject)
|
||||
this.parseFilenames(filenames)
|
||||
|
||||
@@ -168,6 +173,26 @@ export default {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
flattenFilesObject(filesObject) {
|
||||
const flattenObject = (obj, prefix = '') => {
|
||||
var _obj = {}
|
||||
for (const key in obj) {
|
||||
const newKey = prefix ? prefix + '/' + key : key
|
||||
if (obj[key] instanceof CompressedFile) {
|
||||
_obj[newKey] = obj[key]
|
||||
} else if (!key.startsWith('_') && typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
|
||||
_obj = {
|
||||
..._obj,
|
||||
...flattenObject(obj[key], newKey)
|
||||
}
|
||||
} else {
|
||||
_obj[newKey] = obj[key]
|
||||
}
|
||||
}
|
||||
return _obj
|
||||
}
|
||||
return flattenObject(filesObject)
|
||||
},
|
||||
async extractXmlFile(filename) {
|
||||
console.log('extracting xml filename', filename)
|
||||
try {
|
||||
@@ -187,7 +212,7 @@ export default {
|
||||
},
|
||||
parseImageFilename(filename) {
|
||||
var basename = Path.basename(filename, Path.extname(filename))
|
||||
var numbersinpath = basename.match(/\d{1,4}/g)
|
||||
var numbersinpath = basename.match(/\d{1,5}/g)
|
||||
if (!numbersinpath || !numbersinpath.length) {
|
||||
return {
|
||||
index: -1,
|
||||
|
||||
@@ -96,7 +96,9 @@ export default {
|
||||
if (itemRelPath.startsWith('/')) itemRelPath = itemRelPath.slice(1)
|
||||
var relPath = this.ebookFile.metadata.relPath
|
||||
if (relPath.startsWith('/')) relPath = relPath.slice(1)
|
||||
return `/ebook/${this.libraryId}/${this.folderId}/${itemRelPath}/${relPath}`
|
||||
|
||||
const relRelPath = this.$encodeUriPath(`${itemRelPath}/${relPath}`)
|
||||
return `/ebook/${this.libraryId}/${this.folderId}/${relRelPath}`
|
||||
},
|
||||
userToken() {
|
||||
return this.$store.getters['user/getToken']
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<span class="material-icons drag-handle text-lg md:text-xl">menu</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-full relative" :style="{ width: coverWidth + 'px', minWidth: coverWidth + 'px', maxWidth: coverWidth + 'px' }">
|
||||
<div class="h-full relative flex items-center" :style="{ width: coverWidth + 'px', minWidth: coverWidth + 'px', maxWidth: coverWidth + 'px' }">
|
||||
<covers-book-cover :library-item="book" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
<div class="absolute top-0 left-0 bg-black bg-opacity-50 flex items-center justify-center h-full w-full z-10" v-show="isHovering && showPlayBtn">
|
||||
<div class="w-8 h-8 bg-white bg-opacity-20 rounded-full flex items-center justify-center hover:bg-opacity-40 cursor-pointer" @click="playClick">
|
||||
@@ -14,18 +14,20 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow max-w-md h-full px-2 flex items-center">
|
||||
<div class="truncate px-1">
|
||||
<nuxt-link :to="`/item/${book.id}`" class="truncate hover:underline">{{ bookTitle }}</nuxt-link>
|
||||
<div class="flex-grow overflow-hidden max-w-48 md:max-w-md h-full flex items-center px-2 md:px-3">
|
||||
<div>
|
||||
<div class="truncate max-w-48 md:max-w-md">
|
||||
<nuxt-link :to="`/item/${book.id}`" class="truncate hover:underline text-sm md:text-base">{{ bookTitle }}</nuxt-link>
|
||||
</div>
|
||||
<div class="truncate max-w-48 md:max-w-md text-xs md:text-sm text-gray-300">
|
||||
<template v-for="(author, index) in bookAuthors">
|
||||
<nuxt-link :key="author.id" :to="`/author/${author.id}`" class="truncate hover:underline">{{ author.name }}</nuxt-link
|
||||
><span :key="author.id + '-comma'" v-if="index < bookAuthors.length - 1">, </span>
|
||||
</template>
|
||||
</div>
|
||||
<p class="text-xs md:text-sm text-gray-400">{{ bookDuration }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-20 flex items-center">
|
||||
<p class="font-mono text-sm">{{ bookDuration }}</p>
|
||||
</div>
|
||||
|
||||
<!-- <div class="w-12 flex items-center justify-center">
|
||||
<span class="material-icons text-lg text-white text-opacity-70 hover:text-opacity-100 cursor-pointer">radio_button_unchecked</span>
|
||||
</div> -->
|
||||
</div>
|
||||
<div class="w-40 absolute top-0 -right-24 h-full transform transition-transform" :class="!isHovering ? 'translate-x-0' : translateDistance">
|
||||
<div class="flex h-full items-center">
|
||||
@@ -88,11 +90,11 @@ export default {
|
||||
bookTitle() {
|
||||
return this.mediaMetadata.title || ''
|
||||
},
|
||||
bookAuthor() {
|
||||
return (this.mediaMetadata.authors || []).map((au) => au.name).join(', ')
|
||||
bookAuthors() {
|
||||
return this.mediaMetadata.authors || []
|
||||
},
|
||||
bookDuration() {
|
||||
return this.$secondsToTimestamp(this.media.duration)
|
||||
return this.$elapsedPretty(this.media.duration)
|
||||
},
|
||||
isMissing() {
|
||||
return this.book.isMissing
|
||||
@@ -113,7 +115,7 @@ export default {
|
||||
return this.itemProgress ? !!this.itemProgress.isFinished : false
|
||||
},
|
||||
coverSize() {
|
||||
return this.$store.state.globals.isMobile ? 40 : 50
|
||||
return this.$store.state.globals.isMobile ? 30 : 50
|
||||
},
|
||||
coverWidth() {
|
||||
if (this.bookCoverAspectRatio === 1) return this.coverSize * 1.6
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="w-full px-4 h-12 border border-white border-opacity-10 flex items-center relative -mt-px" :class="selected ? 'bg-primary bg-opacity-50' : 'hover:bg-primary hover:bg-opacity-25'" @mouseover="mouseover = true" @mouseleave="mouseover = false">
|
||||
<div v-show="selected" class="absolute top-0 left-0 h-full w-0.5 bg-warning z-10" />
|
||||
<widgets-library-icon v-if="!libraryScan" :icon="library.icon" :size="6" class="text-white" :class="isHovering ? 'text-opacity-90' : 'text-opacity-50'" />
|
||||
<ui-library-icon v-if="!libraryScan" :icon="library.icon" :size="6" font-size="xl" class="text-white" :class="isHovering ? 'text-opacity-90' : 'text-opacity-50'" />
|
||||
<svg v-else viewBox="0 0 24 24" class="h-6 w-6 text-white text-opacity-50 animate-spin">
|
||||
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
|
||||
</svg>
|
||||
|
||||
@@ -10,7 +10,10 @@
|
||||
<ui-btn color="error" :disabled="processing" small class="h-9" @click="removeSelectedEpisodes">Remove {{ selectedEpisodes.length }} episode{{ selectedEpisodes.length > 1 ? 's' : '' }}</ui-btn>
|
||||
<ui-btn :disabled="processing" small class="ml-2 h-9" @click="clearSelected">Cancel</ui-btn>
|
||||
</template>
|
||||
<controls-episode-sort-select v-else v-model="sortKey" :descending.sync="sortDesc" class="w-36 sm:w-44 md:w-48 h-9 ml-1 sm:ml-4" />
|
||||
<template v-else>
|
||||
<controls-filter-select v-model="filterKey" :items="filterItems" class="w-32 md:w-36 h-9 ml-1 sm:ml-4" />
|
||||
<controls-sort-select v-model="sortKey" :descending.sync="sortDesc" :items="sortItems" class="w-32 sm:w-44 md:w-48 h-9 ml-1 sm:ml-4" />
|
||||
</template>
|
||||
</div>
|
||||
<p v-if="!episodes.length" class="py-4 text-center text-lg">No Episodes</p>
|
||||
<template v-for="episode in episodesSorted">
|
||||
@@ -32,13 +35,50 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
episodesCopy: [],
|
||||
filterKey: 'incomplete',
|
||||
sortKey: 'publishedAt',
|
||||
sortDesc: true,
|
||||
selectedEpisode: null,
|
||||
showPodcastRemoveModal: false,
|
||||
selectedEpisodes: [],
|
||||
episodesToRemove: [],
|
||||
processing: false
|
||||
processing: false,
|
||||
sortItems: [
|
||||
{
|
||||
text: 'Pub Date',
|
||||
value: 'publishedAt'
|
||||
},
|
||||
{
|
||||
text: 'Title',
|
||||
value: 'title'
|
||||
},
|
||||
{
|
||||
text: 'Season',
|
||||
value: 'season'
|
||||
},
|
||||
{
|
||||
text: 'Episode',
|
||||
value: 'episode'
|
||||
}
|
||||
],
|
||||
filterItems: [
|
||||
{
|
||||
value: 'all',
|
||||
text: 'Show All'
|
||||
},
|
||||
{
|
||||
value: 'incomplete',
|
||||
text: 'Incomplete'
|
||||
},
|
||||
{
|
||||
value: 'complete',
|
||||
text: 'Complete'
|
||||
},
|
||||
{
|
||||
value: 'in_progress',
|
||||
text: 'In Progress'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -63,12 +103,20 @@ export default {
|
||||
return this.media.episodes || []
|
||||
},
|
||||
episodesSorted() {
|
||||
return this.episodesCopy.sort((a, b) => {
|
||||
if (this.sortDesc) {
|
||||
return String(b[this.sortKey]).localeCompare(String(a[this.sortKey]), undefined, { numeric: true, sensitivity: 'base' })
|
||||
}
|
||||
return String(a[this.sortKey]).localeCompare(String(b[this.sortKey]), undefined, { numeric: true, sensitivity: 'base' })
|
||||
})
|
||||
return this.episodesCopy
|
||||
.filter((ep) => {
|
||||
if (this.filterKey === 'all') return true
|
||||
const episodeProgress = this.$store.getters['user/getUserMediaProgress'](this.libraryItem.id, ep.id)
|
||||
if (this.filterKey === 'incomplete') return !episodeProgress || !episodeProgress.isFinished
|
||||
if (this.filterKey === 'complete') return episodeProgress && episodeProgress.isFinished
|
||||
return episodeProgress && !episodeProgress.isFinished
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (this.sortDesc) {
|
||||
return String(b[this.sortKey]).localeCompare(String(a[this.sortKey]), undefined, { numeric: true, sensitivity: 'base' })
|
||||
}
|
||||
return String(a[this.sortKey]).localeCompare(String(b[this.sortKey]), undefined, { numeric: true, sensitivity: 'base' })
|
||||
})
|
||||
},
|
||||
selectedIsFinished() {
|
||||
// Find an item that is not finished, if none then all items finished
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<transition name="menu">
|
||||
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-black-200 shadow-lg max-h-56 rounded-b-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" tabindex="-1" role="listbox">
|
||||
<template v-for="item in items">
|
||||
<template v-for="item in itemsToShow">
|
||||
<li :key="item.value" class="text-gray-100 select-none relative py-2 cursor-pointer hover:bg-black-400" id="listbox-option-0" role="option" @click="clickedOption(item.value)">
|
||||
<div class="flex items-center">
|
||||
<span class="ml-3 block truncate font-sans text-sm" :class="{ 'font-semibold': item.subtext }">{{ item.text }}</span>
|
||||
@@ -62,8 +62,19 @@ export default {
|
||||
this.$emit('input', val)
|
||||
}
|
||||
},
|
||||
itemsToShow() {
|
||||
return this.items.map((i) => {
|
||||
if (typeof i === 'string') {
|
||||
return {
|
||||
text: i,
|
||||
value: i
|
||||
}
|
||||
}
|
||||
return i
|
||||
})
|
||||
},
|
||||
selectedItem() {
|
||||
return this.items.find((i) => i.value === this.selected)
|
||||
return this.itemsToShow.find((i) => i.value === this.selected)
|
||||
},
|
||||
selectedText() {
|
||||
return this.selectedItem ? this.selectedItem.text : ''
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
<div v-if="currentLibrary" class="relative h-8 max-w-52 md:min-w-32" v-click-outside="clickOutsideObj">
|
||||
<button type="button" :disabled="disabled" class="w-10 sm:w-full relative h-full border border-white border-opacity-10 hover:border-opacity-20 rounded shadow-sm px-2 text-left text-sm focus:outline-none cursor-pointer bg-black bg-opacity-20 text-gray-400 hover:text-gray-200" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
|
||||
<div class="flex items-center justify-center sm:justify-start">
|
||||
<widgets-library-icon :icon="currentLibraryIcon" class="sm:mr-1.5" />
|
||||
<ui-library-icon :icon="currentLibraryIcon" class="sm:mr-1.5" />
|
||||
<span class="hidden sm:block truncate">{{ currentLibrary.name }}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<transition name="menu">
|
||||
<ul v-show="showMenu" class="absolute z-10 -mt-px min-w-48 w-full bg-primary border border-black-200 shadow-lg max-h-56 rounded-b-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" tabindex="-1" role="listbox">
|
||||
<ul v-show="showMenu" class="absolute z-10 -mt-px min-w-48 w-full bg-primary border border-black-200 shadow-lg max-h-56 rounded-b-md py-1 overflow-auto focus:outline-none sm:text-sm" tabindex="-1" role="listbox">
|
||||
<template v-for="library in librariesFiltered">
|
||||
<li :key="library.id" class="text-gray-100 select-none relative py-2 cursor-pointer hover:bg-black-400" id="listbox-option-0" role="option" @click="selectLibrary(library)">
|
||||
<li :key="library.id" class="text-gray-400 hover:text-white select-none relative py-2 cursor-pointer hover:bg-black-400" role="option" @click="selectLibrary(library)">
|
||||
<div class="flex items-center px-2">
|
||||
<widgets-library-icon :icon="library.icon" class="mr-1.5 text-gray-400" />
|
||||
<ui-library-icon :icon="library.icon" class="mr-1.5" />
|
||||
<span class="font-normal block truncate font-sans text-sm">{{ library.name }}</span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
37
client/components/ui/LibraryIcon.vue
Normal file
37
client/components/ui/LibraryIcon.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div :class="`h-${size} w-${size} min-w-${size} text-${fontSize}`" class="flex items-center justify-center">
|
||||
<span class="abs-icons" :class="`icon-${iconToUse}`"></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
icon: {
|
||||
type: String,
|
||||
default: 'audiobookshelf'
|
||||
},
|
||||
fontSize: {
|
||||
type: String,
|
||||
default: 'lg'
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 5
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
iconToUse() {
|
||||
return this.icons.includes(this.icon) ? this.icon : 'audiobookshelf'
|
||||
},
|
||||
icons() {
|
||||
return this.$store.state.globals.libraryIcons
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
@@ -3,19 +3,19 @@
|
||||
<p class="text-sm font-semibold">{{ label }}</p>
|
||||
|
||||
<button type="button" :disabled="disabled" class="relative h-full w-full border border-gray-600 rounded shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer bg-primary text-gray-100 hover:text-gray-200" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
|
||||
<span class="flex items-center">
|
||||
<widgets-library-icon :icon="selected" />
|
||||
</span>
|
||||
<ui-library-icon :icon="selectedItem" />
|
||||
</button>
|
||||
|
||||
<transition name="menu">
|
||||
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-black-200 shadow-lg max-h-56 rounded-b-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" tabindex="-1" role="listbox">
|
||||
<template v-for="type in types">
|
||||
<li :key="type.id" class="text-gray-100 select-none relative py-2 cursor-pointer hover:bg-black-400 flex justify-center" id="listbox-option-0" role="option" @click="select(type)">
|
||||
<widgets-library-icon :icon="type.id" />
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
<div v-show="showMenu" class="absolute -left-[4.5rem] z-10 -mt-px bg-primary border border-black-200 shadow-lg max-h-56 w-48 rounded-md py-1 overflow-auto focus:outline-none sm:text-sm">
|
||||
<div class="flex justify-center items-center flex-wrap">
|
||||
<template v-for="icon in icons">
|
||||
<div :key="icon" class="p-2">
|
||||
<span class="abs-icons text-xl text-white text-opacity-80 hover:text-opacity-100 cursor-pointer" :class="`icon-${icon}`" @click="select(icon)"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
@@ -37,29 +37,7 @@ export default {
|
||||
events: ['mousedown'],
|
||||
isActive: true
|
||||
},
|
||||
showMenu: false,
|
||||
types: [
|
||||
{
|
||||
id: 'database',
|
||||
name: 'Database'
|
||||
},
|
||||
{
|
||||
id: 'audiobook',
|
||||
name: 'Audiobooks'
|
||||
},
|
||||
{
|
||||
id: 'book',
|
||||
name: 'Books'
|
||||
},
|
||||
{
|
||||
id: 'podcast',
|
||||
name: 'Podcasts'
|
||||
},
|
||||
{
|
||||
id: 'comic',
|
||||
name: 'Comics'
|
||||
}
|
||||
]
|
||||
showMenu: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -71,11 +49,11 @@ export default {
|
||||
this.$emit('input', val)
|
||||
}
|
||||
},
|
||||
selectedItem() {
|
||||
return this.types.find((t) => t.id === this.selected)
|
||||
icons() {
|
||||
return this.$store.state.globals.libraryIcons
|
||||
},
|
||||
selectedName() {
|
||||
return this.selectedItem ? this.selectedItem.name : 'Database'
|
||||
selectedItem() {
|
||||
return this.icons.find((i) => i === this.selected) || 'audiobookshelf'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -86,9 +64,9 @@ export default {
|
||||
clickedOutside() {
|
||||
this.showMenu = false
|
||||
},
|
||||
select(type) {
|
||||
select(icon) {
|
||||
if (this.disabled) return
|
||||
this.selected = type.id
|
||||
this.selected = icon
|
||||
this.showMenu = false
|
||||
}
|
||||
},
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
<form @submit.prevent="submitForm">
|
||||
<div ref="inputWrapper" style="min-height: 36px" class="flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-1" :class="wrapperClass" @click.stop.prevent="clickWrapper" @mouseup.stop.prevent @mousedown.prevent>
|
||||
<div v-for="item in selected" :key="item" class="rounded-full px-2 py-1 mx-0.5 my-0.5 text-xs bg-bg flex flex-nowrap break-all items-center relative">
|
||||
<div v-if="!disabled" class="w-full h-full rounded-full absolute top-0 left-0 opacity-0 px-1 bg-bg bg-opacity-75 flex items-center justify-end">
|
||||
<span v-if="showEdit" class="material-icons text-white hover:text-warning" style="font-size: 1.1rem" @click.stop="editItem(item)">edit</span>
|
||||
<span class="material-icons text-white hover:text-error" style="font-size: 1.1rem" @click.stop="removeItem(item)">close</span>
|
||||
<div v-if="!disabled" class="w-full h-full rounded-full absolute top-0 left-0 px-1 bg-bg bg-opacity-75 flex items-center justify-end opacity-0 hover:opacity-100">
|
||||
<span v-if="showEdit" class="material-icons text-white hover:text-warning cursor-pointer" style="font-size: 1.1rem" @click.stop="editItem(item)">edit</span>
|
||||
<span class="material-icons text-white hover:text-error cursor-pointer" style="font-size: 1.1rem" @click.stop="removeItem(item)">close</span>
|
||||
</div>
|
||||
{{ item }}
|
||||
</div>
|
||||
|
||||
@@ -94,11 +94,8 @@ export default {
|
||||
this.$store.commit('setBookshelfBookIds', itemIds)
|
||||
this.$store.commit('showEditModal', libraryItem)
|
||||
},
|
||||
selectItem(libraryItem) {
|
||||
this.$store.commit('toggleLibraryItemSelected', libraryItem.id)
|
||||
this.$nextTick(() => {
|
||||
this.$eventBus.$emit('item-selected', libraryItem)
|
||||
})
|
||||
selectItem(payload) {
|
||||
this.$emit('selectEntity', payload)
|
||||
},
|
||||
itemSelectedEvt() {
|
||||
this.updateSelectionMode(this.isSelectionMode)
|
||||
@@ -153,11 +150,11 @@ export default {
|
||||
this.setScrollVars()
|
||||
},
|
||||
mounted() {
|
||||
this.$eventBus.$on('bookshelf-clear-selection', this.clearSelectedEntities)
|
||||
this.$eventBus.$on('bookshelf_clear_selection', this.clearSelectedEntities)
|
||||
this.$eventBus.$on('item-selected', this.itemSelectedEvt)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$eventBus.$off('bookshelf-clear-selection', this.clearSelectedEntities)
|
||||
this.$eventBus.$off('bookshelf_clear_selection', this.clearSelectedEntities)
|
||||
this.$eventBus.$off('item-selected', this.itemSelectedEvt)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,11 +74,8 @@ export default {
|
||||
this.$store.commit('setBookshelfBookIds', itemIds)
|
||||
this.$store.commit('showEditModal', libraryItem)
|
||||
},
|
||||
selectItem(libraryItem) {
|
||||
this.$store.commit('toggleLibraryItemSelected', libraryItem.id)
|
||||
this.$nextTick(() => {
|
||||
this.$eventBus.$emit('item-selected', libraryItem)
|
||||
})
|
||||
selectItem(payload) {
|
||||
this.$emit('selectEntity', payload)
|
||||
},
|
||||
itemSelectedEvt() {
|
||||
this.updateSelectionMode(this.isSelectionMode)
|
||||
@@ -133,11 +130,11 @@ export default {
|
||||
this.setScrollVars()
|
||||
},
|
||||
mounted() {
|
||||
this.$eventBus.$on('bookshelf-clear-selection', this.clearSelectedEntities)
|
||||
this.$eventBus.$on('bookshelf_clear_selection', this.clearSelectedEntities)
|
||||
this.$eventBus.$on('item-selected', this.itemSelectedEvt)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$eventBus.$off('bookshelf-clear-selection', this.clearSelectedEntities)
|
||||
this.$eventBus.$off('bookshelf_clear_selection', this.clearSelectedEntities)
|
||||
this.$eventBus.$off('item-selected', this.itemSelectedEvt)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
<template>
|
||||
<div :class="`h-${size} w-${size} min-w-${size}`">
|
||||
<component :is="iconComponentName" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
icon: String,
|
||||
size: {
|
||||
type: Number,
|
||||
default: 5
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
iconComponentName() {
|
||||
if (this.icon === 'default') return `icons-database-svg`
|
||||
return `icons-${this.icon}-svg`
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
@@ -13,7 +13,7 @@
|
||||
<div ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll -mx-2" style="scroll-behavior: smooth" @scroll="scrolled">
|
||||
<div class="flex" :style="{ height: height + 'px' }">
|
||||
<template v-for="(item, index) in items">
|
||||
<cards-lazy-series-card :key="item.id" :ref="`slider-item-${item.id}`" :index="index" :series-mount="item" :height="cardHeight" :width="cardWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" :bookshelf-view="$constants.BookshelfView.TITLES" class="relative mx-2" @hook:updated="setScrollVars" />
|
||||
<cards-lazy-series-card :key="item.id" :ref="`slider-item-${item.id}`" :index="index" :series-mount="item" :height="cardHeight" :width="cardWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" :bookshelf-view="$constants.BookshelfView.DETAIL" class="relative mx-2" @hook:updated="setScrollVars" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -474,7 +474,7 @@ export default {
|
||||
if (this.$store.getters['getNumLibraryItemsSelected'] && name === 'Escape') {
|
||||
// ESCAPE key cancels batch selection
|
||||
this.$store.commit('setSelectedLibraryItems', [])
|
||||
this.$eventBus.$emit('bookshelf-clear-selection')
|
||||
this.$eventBus.$emit('bookshelf_clear_selection')
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -4,6 +4,6 @@ export default function ({ store, redirect, route, app }) {
|
||||
if (route.name === 'batch' || route.name === 'index') {
|
||||
return redirect('/login')
|
||||
}
|
||||
return redirect(`/login?redirect=${route.fullPath}`)
|
||||
return redirect(`/login?redirect=${encodeURIComponent(route.fullPath)}`)
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,8 @@ export default {
|
||||
if (this.entityName === 'books') {
|
||||
props.filterBy = this.filterBy
|
||||
props.orderBy = this.orderBy
|
||||
} else if (this.entityName === 'series') {
|
||||
props.orderBy = this.seriesSortBy
|
||||
}
|
||||
|
||||
var _this = this
|
||||
@@ -68,8 +70,8 @@ export default {
|
||||
this.$on('edit', (entity) => {
|
||||
if (_this.editEntity) _this.editEntity(entity)
|
||||
})
|
||||
this.$on('select', (entity) => {
|
||||
if (_this.selectEntity) _this.selectEntity(entity)
|
||||
this.$on('select', ({ entity, shiftKey }) => {
|
||||
if (_this.selectEntity) _this.selectEntity(entity, shiftKey)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@ module.exports = {
|
||||
target: 'static',
|
||||
dev: process.env.NODE_ENV !== 'production',
|
||||
env: {
|
||||
serverUrl: process.env.NODE_ENV === 'production' ? process.env.ROUTER_BASE_PATH : 'http://localhost:3333',
|
||||
serverUrl: process.env.NODE_ENV === 'production' ? process.env.ROUTER_BASE_PATH || '' : 'http://localhost:3333',
|
||||
chromecastReceiver: 'FD1F76C5'
|
||||
},
|
||||
telemetry: false,
|
||||
|
||||
26
client/package-lock.json
generated
26
client/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.2.0",
|
||||
"version": "2.2.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.1.5",
|
||||
"version": "2.2.2",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@nuxtjs/axios": "^5.13.6",
|
||||
@@ -20,7 +20,7 @@
|
||||
"nuxt-socket-io": "^1.1.18",
|
||||
"trix": "^1.3.1",
|
||||
"v-click-outside": "^3.1.2",
|
||||
"vue-pdf": "^4.3.0",
|
||||
"vue-pdf": "^4.2.0",
|
||||
"vue-toastification": "^1.7.11",
|
||||
"vuedraggable": "^2.24.3"
|
||||
},
|
||||
@@ -15879,14 +15879,14 @@
|
||||
"integrity": "sha512-ZMjqRpWabMPqPc7gIrG0Nw6vRf1+itwf0Itft7LbMXs2g3Zs/NFmevjZGN1x7K3Q95GmIjWbQZTVerxiBxI+0g=="
|
||||
},
|
||||
"node_modules/vue-pdf": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-pdf/-/vue-pdf-4.3.0.tgz",
|
||||
"integrity": "sha512-zd3lJj6CbtrawgaaDDciTDjkJMUKiLWtbEmBg5CvFn9Noe9oAO/GNy/fc5c59qGuFCJ14ibIV1baw4S07e5bSQ==",
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-pdf/-/vue-pdf-4.2.0.tgz",
|
||||
"integrity": "sha512-GpAbZfM48Hom1R8f4XL5ZzoVBLlbyy+4z0VYmTQORVOSieVIIu+XtnNl0RY6EXg60Qni6T6nIgrmsCcCkWv39A==",
|
||||
"dependencies": {
|
||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||
"loader-utils": "^1.4.0",
|
||||
"pdfjs-dist": "2.6.347",
|
||||
"raw-loader": "^4.0.2",
|
||||
"pdfjs-dist": "^2.5.207",
|
||||
"raw-loader": "^4.0.1",
|
||||
"vue-resize-sensor": "^2.0.0",
|
||||
"worker-loader": "^2.0.0"
|
||||
}
|
||||
@@ -29629,14 +29629,14 @@
|
||||
"integrity": "sha512-ZMjqRpWabMPqPc7gIrG0Nw6vRf1+itwf0Itft7LbMXs2g3Zs/NFmevjZGN1x7K3Q95GmIjWbQZTVerxiBxI+0g=="
|
||||
},
|
||||
"vue-pdf": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-pdf/-/vue-pdf-4.3.0.tgz",
|
||||
"integrity": "sha512-zd3lJj6CbtrawgaaDDciTDjkJMUKiLWtbEmBg5CvFn9Noe9oAO/GNy/fc5c59qGuFCJ14ibIV1baw4S07e5bSQ==",
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-pdf/-/vue-pdf-4.2.0.tgz",
|
||||
"integrity": "sha512-GpAbZfM48Hom1R8f4XL5ZzoVBLlbyy+4z0VYmTQORVOSieVIIu+XtnNl0RY6EXg60Qni6T6nIgrmsCcCkWv39A==",
|
||||
"requires": {
|
||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||
"loader-utils": "^1.4.0",
|
||||
"pdfjs-dist": "2.6.347",
|
||||
"raw-loader": "^4.0.2",
|
||||
"pdfjs-dist": "^2.5.207",
|
||||
"raw-loader": "^4.0.1",
|
||||
"vue-resize-sensor": "^2.0.0",
|
||||
"worker-loader": "^2.0.0"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.2.0",
|
||||
"version": "2.2.2",
|
||||
"description": "Self-hosted audiobook and podcast client",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -24,7 +24,7 @@
|
||||
"nuxt-socket-io": "^1.1.18",
|
||||
"trix": "^1.3.1",
|
||||
"v-click-outside": "^3.1.2",
|
||||
"vue-pdf": "^4.3.0",
|
||||
"vue-pdf": "^4.2.0",
|
||||
"vue-toastification": "^1.7.11",
|
||||
"vuedraggable": "^2.24.3"
|
||||
},
|
||||
|
||||
@@ -18,11 +18,35 @@
|
||||
<p class="text-lg mb-4 font-semibold">Audiobook Chapters</p>
|
||||
<div class="flex-grow" />
|
||||
<ui-checkbox v-model="showSecondInputs" checkbox-bg="primary" small label-class="text-sm text-gray-200 pl-1" label="Show seconds" class="mx-2" />
|
||||
<div class="w-40" />
|
||||
</div>
|
||||
<div class="flex items-center mb-3 py-1">
|
||||
<div class="flex-grow" />
|
||||
<ui-btn v-if="newChapters.length > 1" :color="showShiftTimes ? 'bg' : 'primary'" small @click="showShiftTimes = !showShiftTimes">Shift Times</ui-btn>
|
||||
<ui-btn color="primary" small class="mx-2" @click="showFindChaptersModal = true">Lookup</ui-btn>
|
||||
<ui-btn color="success" small @click="saveChapters">Save</ui-btn>
|
||||
<div class="w-40" />
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden">
|
||||
<transition name="slide">
|
||||
<div v-if="showShiftTimes" class="flex mb-4">
|
||||
<div class="w-12"></div>
|
||||
<div class="flex-grow">
|
||||
<div class="flex items-center">
|
||||
<p class="text-sm mb-1 font-semibold pr-2">Time to shift in seconds</p>
|
||||
<ui-text-input v-model="shiftAmount" type="number" class="max-w-20" style="height: 30px" />
|
||||
<ui-btn color="primary" class="mx-1" small @click="shiftChapterTimes">Add</ui-btn>
|
||||
<div class="flex-grow" />
|
||||
<span class="material-icons text-gray-200 hover:text-white cursor-pointer" @click="showShiftTimes = false">close</span>
|
||||
</div>
|
||||
<p class="text-xs py-1.5 text-gray-300 max-w-md">Note: First chapter start time must remain at 0:00 and the last chapter start time cannot exceed this audiobooks duration.</p>
|
||||
</div>
|
||||
<div class="w-40"></div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<div class="flex text-xs uppercase text-gray-300 font-semibold mb-2">
|
||||
<div class="w-12"></div>
|
||||
<div class="w-32 px-2">Start</div>
|
||||
@@ -104,15 +128,20 @@
|
||||
<div class="w-full h-full max-h-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative">
|
||||
<div v-if="!chapterData" class="flex p-20">
|
||||
<ui-text-input-with-label v-model="asinInput" label="ASIN" />
|
||||
<ui-btn small color="primary" class="mt-5 ml-2" @click="findChapters">Find</ui-btn>
|
||||
<ui-dropdown v-model="regionInput" label="Region" small :items="audibleRegions" class="w-32 mx-1" />
|
||||
<ui-btn small color="primary" class="mt-5" @click="findChapters">Find</ui-btn>
|
||||
</div>
|
||||
<div v-else class="w-full p-4">
|
||||
<div class="flex justify-between mb-4">
|
||||
<p>
|
||||
Duration found: <span class="font-semibold">{{ $secondsToTimestamp(chapterData.runtimeLengthSec) }}</span>
|
||||
Duration found: <span class="font-semibold">{{ $secondsToTimestamp(chapterData.runtimeLengthSec) }}</span
|
||||
><br />
|
||||
<span class="font-semibold" :class="{ 'text-warning': chapters.length !== chapterData.chapters.length }">{{ chapterData.chapters.length }}</span> chapters found
|
||||
</p>
|
||||
<p>
|
||||
Your audiobook duration: <span class="font-semibold">{{ $secondsToTimestamp(mediaDurationRounded) }}</span>
|
||||
Your audiobook duration: <span class="font-semibold">{{ $secondsToTimestamp(mediaDurationRounded) }}</span
|
||||
><br />
|
||||
Your audiobook has <span class="font-semibold" :class="{ 'text-warning': chapters.length !== chapterData.chapters.length }">{{ chapters.length }}</span> chapters
|
||||
</p>
|
||||
</div>
|
||||
<widgets-alert v-if="chapterData.runtimeLengthSec > mediaDurationRounded" type="warning" class="mb-2"> Your audiobook duration is shorter than duration found </widgets-alert>
|
||||
@@ -186,16 +215,20 @@ export default {
|
||||
return {
|
||||
newChapters: [],
|
||||
selectedChapter: null,
|
||||
showShiftTimes: false,
|
||||
shiftAmount: 0,
|
||||
audioEl: null,
|
||||
isPlayingChapter: false,
|
||||
isLoadingChapter: false,
|
||||
currentTrackIndex: 0,
|
||||
saving: false,
|
||||
asinInput: null,
|
||||
regionInput: 'US',
|
||||
findingChapters: false,
|
||||
showFindChaptersModal: false,
|
||||
chapterData: null,
|
||||
showSecondInputs: false
|
||||
showSecondInputs: false,
|
||||
audibleRegions: ['US', 'CA', 'UK', 'AU', 'FR', 'DE', 'JP', 'IT', 'IN', 'ES']
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -237,6 +270,32 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
shiftChapterTimes() {
|
||||
if (!this.shiftAmount || isNaN(this.shiftAmount) || this.newChapters.length <= 1) {
|
||||
return
|
||||
}
|
||||
|
||||
const amount = Number(this.shiftAmount)
|
||||
|
||||
const lastChapter = this.newChapters[this.newChapters.length - 1]
|
||||
if (lastChapter.start + amount > this.mediaDurationRounded) {
|
||||
this.$toast.error('Invalid shift amount. Last chapter start time would extend beyond the duration of this audiobook.')
|
||||
return
|
||||
}
|
||||
|
||||
if (this.newChapters[0].end + amount <= 0) {
|
||||
this.$toast.error('Invalid shift amount. First chapter would have zero or negative length.')
|
||||
return
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.newChapters.length; i++) {
|
||||
const chap = this.newChapters[i]
|
||||
chap.end = Math.min(chap.end + amount, this.mediaDuration)
|
||||
if (i > 0) {
|
||||
chap.start = Math.max(0, chap.start + amount)
|
||||
}
|
||||
}
|
||||
},
|
||||
editItem() {
|
||||
this.$store.commit('showEditModal', this.libraryItem)
|
||||
},
|
||||
@@ -423,10 +482,16 @@ export default {
|
||||
this.$toast.error('Must input an ASIN')
|
||||
return
|
||||
}
|
||||
|
||||
// Update local storage region
|
||||
if (this.regionInput !== localStorage.getItem('audibleRegion')) {
|
||||
localStorage.setItem('audibleRegion', this.regionInput)
|
||||
}
|
||||
|
||||
this.findingChapters = true
|
||||
this.chapterData = null
|
||||
this.$axios
|
||||
.$get(`/api/search/chapters?asin=${this.asinInput}`)
|
||||
.$get(`/api/search/chapters?asin=${this.asinInput}®ion=${this.regionInput}`)
|
||||
.then((data) => {
|
||||
this.findingChapters = false
|
||||
|
||||
@@ -447,6 +512,7 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.regionInput = localStorage.getItem('audibleRegion') || 'US'
|
||||
this.asinInput = this.mediaMetadata.asin || null
|
||||
this.newChapters = this.chapters.map((c) => ({ ...c }))
|
||||
if (!this.newChapters.length) {
|
||||
|
||||
@@ -91,6 +91,10 @@
|
||||
A backup of your original audio files will be stored in <span class="rounded-md bg-neutral-600 text-sm text-white py-0.5 px-1 font-mono">/metadata/cache/items/{{ libraryItemId }}/</span>. Make sure to periodically purge items cache.
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="selectedTool === 'embed' && audioFiles.length > 1" class="flex items-start mb-2">
|
||||
<span class="material-icons text-base text-warning pt-1">star</span>
|
||||
<p class="text-gray-200 ml-2">Chapters are not embedded in multi-track audiobooks.</p>
|
||||
</div>
|
||||
<div v-if="selectedTool === 'm4b'" class="flex items-start mb-2">
|
||||
<span class="material-icons text-base text-warning pt-1">star</span>
|
||||
<p class="text-gray-200 ml-2">Encoding can take up to 30 minutes.</p>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
<script>
|
||||
export default {
|
||||
async asyncData({ store, app, params, redirect }) {
|
||||
const author = await app.$axios.$get(`/api/authors/${params.id}?include=items,series`).catch((error) => {
|
||||
const author = await app.$axios.$get(`/api/authors/${params.id}?library=${store.state.libraries.currentLibraryId}&include=items,series`).catch((error) => {
|
||||
console.error('Failed to get author', error)
|
||||
return null
|
||||
})
|
||||
|
||||
@@ -25,7 +25,9 @@
|
||||
<div class="flex items-center py-2">
|
||||
<ui-text-input type="number" v-model="backupsToKeep" no-spinner :disabled="updatingServerSettings" :padding-x="1" text-center class="w-10" @change="updateBackupsSettings" />
|
||||
|
||||
<p class="pl-4 text-lg">Number of backups to keep</p>
|
||||
<ui-tooltip :text="numBackupsToKeepTooltip">
|
||||
<p class="pl-4 text-lg">Number of backups to keep <span class="material-icons icon-text">info_outlined</span></p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2">
|
||||
@@ -53,7 +55,10 @@ export default {
|
||||
maxBackupSize: 1,
|
||||
cronExpression: '',
|
||||
newServerSettings: {},
|
||||
showCronBuilder: false
|
||||
showCronBuilder: false,
|
||||
backupsTooltip: 'Backups saved in /metadata/backups',
|
||||
numBackupsToKeepTooltip: 'Only 1 backup will be removed at a time so if you already have more backups than this you should manually remove them.',
|
||||
maxBackupSizeTooltip: 'As a safeguard against misconfiguration, backups will fail if they exceed the configured size.'
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -65,12 +70,6 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
backupsTooltip() {
|
||||
return 'Backups saved in /metadata/backups'
|
||||
},
|
||||
maxBackupSizeTooltip() {
|
||||
return 'As a safeguard against misconfiguration, backups will fail if they exceed the configured size.'
|
||||
},
|
||||
serverSettings() {
|
||||
return this.$store.state.serverSettings
|
||||
},
|
||||
|
||||
@@ -53,20 +53,20 @@
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2">
|
||||
<ui-toggle-switch v-model="homeUseAlternativeBookshelfView" :disabled="updatingServerSettings" @input="updateHomeAlternativeBookshelfView" />
|
||||
<ui-toggle-switch v-model="homepageUseBookshelfView" :disabled="updatingServerSettings" @input="updateHomeUseBookshelfView" />
|
||||
<ui-tooltip :text="tooltips.bookshelfView">
|
||||
<p class="pl-4">
|
||||
Alternative bookshelf view for home page
|
||||
Home page use bookshelf view
|
||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2">
|
||||
<ui-toggle-switch v-model="useAlternativeBookshelfView" :disabled="updatingServerSettings" @input="updateAlternativeBookshelfView" />
|
||||
<ui-toggle-switch v-model="useBookshelfView" :disabled="updatingServerSettings" @input="updateUseBookshelfView" />
|
||||
<ui-tooltip :text="tooltips.bookshelfView">
|
||||
<p class="pl-4">
|
||||
Alternative bookshelf view
|
||||
Library use bookshelf view
|
||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
@@ -272,8 +272,8 @@ export default {
|
||||
return {
|
||||
isResettingLibraryItems: false,
|
||||
updatingServerSettings: false,
|
||||
homeUseAlternativeBookshelfView: false,
|
||||
useAlternativeBookshelfView: false,
|
||||
homepageUseBookshelfView: false,
|
||||
useBookshelfView: false,
|
||||
isPurgingCache: false,
|
||||
newServerSettings: {},
|
||||
tooltips: {
|
||||
@@ -285,10 +285,9 @@ export default {
|
||||
scannerParseSubtitle: 'Extract subtitles from audiobook folder names.<br>Subtitle must be seperated by " - "<br>i.e. "Book Title - A Subtitle Here" has the subtitle "A Subtitle Here"',
|
||||
sortingIgnorePrefix: 'i.e. for prefix "the" book title "The Book Title" would sort as "Book Title, The"',
|
||||
scannerFindCovers: 'If your audiobook does not have an embedded cover or a cover image inside the folder, the scanner will attempt to find a cover.<br>Note: This will extend scan time',
|
||||
bookshelfView: 'Alternative view without wooden bookshelf',
|
||||
bookshelfView: 'Skeumorphic design with wooden shelves',
|
||||
storeCoverWithItem: 'By default covers are stored in /metadata/items, enabling this setting will store covers in your library item folder. Only one file named "cover" will be kept',
|
||||
storeMetadataWithItem: 'By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders. Uses .abs file extension',
|
||||
coverAspectRatio: 'Prefer to use square covers over standard 1.6:1 book covers',
|
||||
enableEReader: 'E-reader is still a work in progress, but use this setting to open it up to all your users (or use the "Experimental Features" toggle just for use by you)',
|
||||
scannerPreferOverdriveMediaMarker: 'MP3 files from Overdrive come with chapter timings embedded as custom metadata. Enabling this will use these tags for chapter timings automatically',
|
||||
scannerUseSingleThreadedProber: 'The old scanner used a single thread. Leaving it in to use as a comparison for now.',
|
||||
@@ -359,19 +358,14 @@ export default {
|
||||
scannerCoverProvider: val
|
||||
})
|
||||
},
|
||||
updateBookCoverAspectRatio(val) {
|
||||
updateHomeUseBookshelfView(val) {
|
||||
this.updateServerSettings({
|
||||
coverAspectRatio: val ? this.$constants.BookCoverAspectRatio.SQUARE : this.$constants.BookCoverAspectRatio.STANDARD
|
||||
homeBookshelfView: !val ? this.$constants.BookshelfView.DETAIL : this.$constants.BookshelfView.STANDARD
|
||||
})
|
||||
},
|
||||
updateHomeAlternativeBookshelfView(val) {
|
||||
updateUseBookshelfView(val) {
|
||||
this.updateServerSettings({
|
||||
homeBookshelfView: val ? this.$constants.BookshelfView.TITLES : this.$constants.BookshelfView.STANDARD
|
||||
})
|
||||
},
|
||||
updateAlternativeBookshelfView(val) {
|
||||
this.updateServerSettings({
|
||||
bookshelfView: val ? this.$constants.BookshelfView.TITLES : this.$constants.BookshelfView.STANDARD
|
||||
bookshelfView: !val ? this.$constants.BookshelfView.DETAIL : this.$constants.BookshelfView.STANDARD
|
||||
})
|
||||
},
|
||||
updateSettingsKey(key, val) {
|
||||
@@ -398,8 +392,8 @@ export default {
|
||||
this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
|
||||
this.newServerSettings.sortingPrefixes = [...(this.newServerSettings.sortingPrefixes || [])]
|
||||
|
||||
this.homeUseAlternativeBookshelfView = this.newServerSettings.homeBookshelfView === this.$constants.BookshelfView.TITLES
|
||||
this.useAlternativeBookshelfView = this.newServerSettings.bookshelfView === this.$constants.BookshelfView.TITLES
|
||||
this.homepageUseBookshelfView = this.newServerSettings.homeBookshelfView != this.$constants.BookshelfView.DETAIL
|
||||
this.useBookshelfView = this.newServerSettings.bookshelfView != this.$constants.BookshelfView.DETAIL
|
||||
},
|
||||
resetLibraryItems() {
|
||||
if (confirm('WARNING! This action will remove all library items from the database including any updates or matches you have made. This does not do anything to your actual files. Shall we continue?')) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="page" :class="streamLibraryItem ? 'streaming' : ''">
|
||||
<app-book-shelf-toolbar :page="id || ''" :view-mode.sync="viewMode"/>
|
||||
<app-lazy-bookshelf :page="id || ''" :view-mode="viewMode" />
|
||||
<app-book-shelf-toolbar :page="id || ''" />
|
||||
<app-lazy-bookshelf :page="id || ''" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -14,8 +14,18 @@ export default {
|
||||
return redirect('/oops?message=Library not found')
|
||||
}
|
||||
|
||||
// Set filter by
|
||||
if (query.filter) {
|
||||
// Set series sort by
|
||||
if (params.id === 'series') {
|
||||
console.log('Series page', query)
|
||||
if (query.sort) {
|
||||
store.commit('libraries/setSeriesSortBy', query.sort)
|
||||
store.commit('libraries/setSeriesSortDesc', !!query.desc)
|
||||
}
|
||||
if (query.filter) {
|
||||
console.log('has filter', query.filter)
|
||||
store.commit('libraries/setSeriesFilterBy', query.filter)
|
||||
}
|
||||
} else if (query.filter) {
|
||||
store.dispatch('user/updateUserSettings', { filterBy: query.filter })
|
||||
}
|
||||
|
||||
@@ -31,9 +41,7 @@ export default {
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
viewMode: 'grid'
|
||||
}
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
streamLibraryItem() {
|
||||
|
||||
@@ -46,7 +46,7 @@ export default class LocalAudioPlayer extends EventEmitter {
|
||||
this.player.addEventListener('loadedmetadata', this.evtLoadedMetadata.bind(this))
|
||||
this.player.addEventListener('timeupdate', this.evtTimeupdate.bind(this))
|
||||
|
||||
var mimeTypes = ['audio/flac', 'audio/mpeg', 'audio/mp4', 'audio/ogg', 'audio/aac', 'audio/x-ms-wma', 'audio/x-aiff']
|
||||
var mimeTypes = ['audio/flac', 'audio/mpeg', 'audio/mp4', 'audio/ogg', 'audio/aac', 'audio/x-ms-wma', 'audio/x-aiff', 'audio/webm']
|
||||
var mimeTypeCanPlayMap = {}
|
||||
mimeTypes.forEach((mt) => {
|
||||
var canPlay = this.player.canPlayType(mt)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const SupportedFileTypes = {
|
||||
image: ['png', 'jpg', 'jpeg', 'webp'],
|
||||
audio: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'ogg', 'oga', 'mp4', 'aac', 'wma', 'aiff', 'wav'],
|
||||
audio: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'ogg', 'oga', 'mp4', 'aac', 'wma', 'aiff', 'wav', 'webm', 'webma'],
|
||||
ebook: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'],
|
||||
info: ['nfo'],
|
||||
text: ['txt'],
|
||||
@@ -21,7 +21,7 @@ const BookCoverAspectRatio = {
|
||||
|
||||
const BookshelfView = {
|
||||
STANDARD: 0,
|
||||
TITLES: 1,
|
||||
DETAIL: 1,
|
||||
AUTHOR: 2 // Books shown on author page
|
||||
}
|
||||
|
||||
|
||||
BIN
client/static/fonts/absicons/absicons.eot
Normal file
BIN
client/static/fonts/absicons/absicons.eot
Normal file
Binary file not shown.
30
client/static/fonts/absicons/absicons.svg
Normal file
30
client/static/fonts/absicons/absicons.svg
Normal file
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<metadata>Generated by IcoMoon</metadata>
|
||||
<defs>
|
||||
<font id="absicons" horiz-adv-x="1024">
|
||||
<font-face units-per-em="1024" ascent="960" descent="-64" />
|
||||
<missing-glyph horiz-adv-x="1024" />
|
||||
<glyph unicode=" " horiz-adv-x="512" d="" />
|
||||
<glyph unicode="" glyph-name="audiobookshelf" horiz-adv-x="862" d="M854.995 455.649c-5.287 4.402-13.511 10.896-24.656 18.576v86.499c0 220.513-178.763 399.276-399.281 399.276v0c-220.513 0-399.276-178.763-399.276-399.276v-86.499c-11.145-7.697-19.369-14.174-24.656-18.576-4.368-3.659-7.126-9.116-7.126-15.217 0-0.006 0-0.011 0-0.017v0.001-103.619c0-0.011 0-0.025 0-0.038 0-6.101 2.758-11.558 7.095-15.191l0.031-0.025c12.326-10.26 40.612-31.782 84.651-53.849v-10.072c0-27.162 17.525-49.186 39.139-49.186v0c21.614 0 39.139 22.023 39.139 49.186v248.534c0 27.166-17.525 49.19-39.139 49.19v0c-20.707 0-37.657-20.215-39.043-45.795l-0.096-0.048v51.221c0 187.38 151.902 339.278 339.282 339.278v0c187.38 0 339.282-151.898 339.282-339.278v-51.221l-0.096 0.048c-1.386 25.58-18.332 45.795-39.043 45.795v0c-21.614 0-39.135-22.023-39.135-49.19v-248.512c0-27.162 17.521-49.186 39.135-49.186v0c21.618 0 39.139 22.023 39.139 49.186v10.072c44.042 22.067 72.329 43.585 84.655 53.849 4.368 3.661 7.126 9.12 7.126 15.223 0 0.011 0 0.022 0 0.034v-0.002 103.597c0 0.003 0 0.007 0 0.010 0 6.103-2.758 11.561-7.095 15.198l-0.031 0.025zM291.835 23.619c26.11 0 47.276 21.166 47.276 47.276v0 529.382c0 0.001 0 0.003 0 0.004 0 26.11-21.166 47.276-47.276 47.276h-48.846c-26.112 0-47.281-21.168-47.281-47.281v0-529.382c0-26.11 21.166-47.276 47.276-47.276 0.002 0 0.003 0 0.005 0v0zM213.535 520.595h107.764v-11.258h-107.764zM455.484 23.619c26.11 0 47.276 21.166 47.276 47.276v0 529.382c0 0.001 0 0.003 0 0.004 0 26.11-21.166 47.276-47.276 47.276h-48.846c-26.112 0-47.281-21.168-47.281-47.281v0-529.382c0-26.11 21.166-47.276 47.276-47.276 0.002 0 0.003 0 0.005 0v0zM377.183 520.595h107.75v-11.258h-107.75zM619.132 23.619c26.11 0 47.276 21.166 47.276 47.276v0 529.382c0 0.001 0 0.003 0 0.004 0 26.11-21.166 47.276-47.276 47.276h-48.846c-26.112 0-47.281-21.168-47.281-47.281v0-529.382c0-26.11 21.166-47.276 47.276-47.276 0.002 0 0.003 0 0.005 0v0zM540.831 520.595h107.75v-11.258h-107.75zM114.101 7.257h633.92c19.676 0 35.626-15.95 35.626-35.626v-0.004c0-19.676-15.95-35.626-35.626-35.626h-633.92c-19.676 0-35.626 15.95-35.626 35.626v0.004c0 19.676 15.95 35.626 35.626 35.626z" />
|
||||
<glyph unicode="" glyph-name="microphone-2" horiz-adv-x="713" d="M713.333 484.083c0 26.75-21.667 48.333-48.333 48.333-26.75 0-48.333-21.667-48.333-48.333 0-95.25-22.833-173.917-69.083-226.417-41.917-47.5-105.25-74.917-190.833-74.917s-148.917 27.333-190.833 74.833c-46.333 52.5-69.083 131.167-69.083 226.5 0 26.75-21.667 48.333-48.333 48.333-26.75 0-48.333-21.667-48.333-48.333 0-118.75 30.75-219.417 93.25-290.333 46-52.167 108-87.5 186.083-101.25v-79.333h-118.5c-21.167 0-38.5-17.333-38.5-38.5v-38.583h468.667v38.583c0 21.167-17.333 38.5-38.5 38.5h-118.75v79.333c78.083 13.833 140 49.083 186.083 101.333 62.5 70.833 93.333 171.5 93.333 290.25v0 0zM356.667 960c98.667 0 179.333-80.75 179.333-179.333v-4.667h-96.417v-114.25h96.5v-58.167h-96.5v-114.25h96.5v-41.083c0-98.667-80.75-179.333-179.333-179.333-98.667 0-179.333 80.75-179.333 179.333v41.083h96.5v114.25h-96.583v58.167h96.5v114.25h-96.5v4.667c0 98.583 80.667 179.333 179.333 179.333v0z" />
|
||||
<glyph unicode="" glyph-name="microphone-1" horiz-adv-x="768" d="M192 768v-320c0-106 86-192 192-192s192 86 192 192h-160c-17.6 0-32 14.4-32 32s14.4 32 32 32h160v64h-160c-17.6 0-32 14.4-32 32s14.4 32 32 32h160v64h-160c-17.6 0-32 14.4-32 32s14.4 32 32 32h160c0 106-86 192-192 192s-192-86-192-192zM640 480v-32c0-141.4-114.6-256-256-256s-256 114.6-256 256v80c0 26.6-21.4 48-48 48s-48-21.4-48-48v-80c0-178.2 132.4-325.4 304-348.8v-67.2h-96c-26.6 0-48-21.4-48-48s21.4-48 48-48h288c26.6 0 48 21.4 48 48s-21.4 48-48 48h-96v67.2c171.6 23.4 304 170.6 304 348.8v80c0 26.6-21.4 48-48 48s-48-21.4-48-48v-48z" />
|
||||
<glyph unicode="" glyph-name="radio" d="M989.6 866c25.4 7.4 40 34.2 32.6 59.6s-34.2 40-59.8 32.4l-859-251.8c-18.8-5.4-35.8-14.6-50.2-26.4-32.2-23.2-53.2-61-53.2-103.8v-512c0-70.6 57.4-128 128-128h768c70.6 0 128 57.4 128 128v512c0 70.6-57.4 128-128 128h-459l552.6 162zM736 160c-88.4 0-160 71.6-160 160s71.6 160 160 160 160-71.6 160-160-71.6-160-160-160zM160 448c0 17.6 14.4 32 32 32h192c17.6 0 32-14.4 32-32s-14.4-32-32-32h-192c-17.6 0-32 14.4-32 32zM128 320c0 17.6 14.4 32 32 32h256c17.6 0 32-14.4 32-32s-14.4-32-32-32h-256c-17.6 0-32 14.4-32 32zM160 192c0 17.6 14.4 32 32 32h192c17.6 0 32-14.4 32-32s-14.4-32-32-32h-192c-17.6 0-32 14.4-32 32z" />
|
||||
<glyph unicode="" glyph-name="podcast" horiz-adv-x="1043" d="M585.253 366.417v-430.408h-127.13v430.408c-41.76 22.603-70.128 66.8-70.128 117.637 0 73.822 59.863 133.685 133.693 133.685 73.822 0 133.693-59.863 133.693-133.685 0-50.837-28.369-95.033-70.128-117.637v0zM333.872 865.196c24.76 12.21 34.907 42.193 22.697 66.927-12.21 24.743-42.167 34.907-66.91 22.705-101.045-49.843-179.63-132.301-229.287-229.397-36.682-71.708-57.596-151.499-60.117-232.021-2.539-81.201 13.475-163.226 50.718-238.72 45.861-93.004 123.614-175.725 238.084-234.288 24.531-12.541 54.59-2.802 67.123 21.729 12.541 24.531 2.811 54.581-21.72 67.114-93.717 47.933-156.968 114.843-193.879 189.642-29.77 60.381-42.575 126.153-40.537 191.382 2.063 65.866 19.156 131.12 49.164 189.76 40.18 78.552 103.49 145.139 184.666 185.167v0zM753.718 954.837c-24.743 12.21-54.709 2.038-66.919-22.705s-2.055-54.726 22.705-66.927c81.184-40.036 144.494-106.615 184.657-185.175 29.999-58.631 47.1-123.886 49.155-189.76 2.038-65.229-10.767-131.002-40.528-191.382-36.911-74.799-100.162-141.709-193.879-189.642-24.531-12.541-34.253-42.592-21.72-67.114 12.533-24.531 42.592-34.262 67.123-21.729 114.461 58.555 192.223 141.284 238.084 234.288 37.242 75.495 53.257 157.52 50.718 238.72-2.522 80.522-23.436 160.313-60.109 232.021-49.665 97.096-128.242 179.554-229.287 229.406v0zM702.992 734.942c-18.638 13.425-44.63 9.187-58.054-9.451-13.416-18.638-9.187-44.63 9.451-58.054 7.515-5.409 14.885-11.48 22.111-18.12 42.21-38.915 67.241-90.804 72.498-145.708 5.315-55.396-9.434-114.096-46.854-166.019-6.351-8.822-13.671-17.764-21.95-26.747-15.581-16.914-14.503-43.288 2.437-58.861 16.931-15.573 43.296-14.477 58.869 2.437 10.020 10.86 19.411 22.425 28.148 34.576 49.775 69.11 69.339 147.746 62.172 222.443-7.201 75.172-41.36 146.116-98.905 199.177-9.052 8.347-19.029 16.464-29.923 24.327v0zM388.98 667.412c18.638 13.425 22.875 39.416 9.451 58.054s-39.416 22.875-58.054 9.459c-10.903-7.863-20.871-15.98-29.923-24.319-57.553-53.070-91.713-124.005-98.914-199.177-7.167-74.688 12.397-153.35 62.181-222.46 8.754-12.142 18.137-23.699 28.148-34.559 15.573-16.914 41.938-18.010 58.861-2.437 16.948 15.573 18.027 41.938 2.445 58.861-8.279 8.992-15.598 17.925-21.95 26.747-37.412 51.923-52.17 110.614-46.854 166.011 5.256 54.895 30.288 106.793 72.498 145.708 7.234 6.632 14.605 12.703 22.111 18.112v0z" />
|
||||
<glyph unicode="" glyph-name="books-1" d="M384 832v-640h128v640h-128zM512 746.667l170.667-554.667 128 42.667-170.667 554.667-128-42.667zM213.333 746.667v-554.667h128v554.667h-128zM128 149.333v-85.333h768v85.333h-768z" />
|
||||
<glyph unicode="" glyph-name="database-2" horiz-adv-x="876" d="M437.75 960c240.583 0 435.583-91.333 435.583-203.833 0-112.583-195.083-203.833-435.583-203.833s-435.583 91.333-435.583 203.833c0 112.5 195.083 203.833 435.583 203.833v0zM2.167 278.083v-156.5c77.5-275.25 843.167-222.083 871.25 14.083v156.333c-38.25-259.25-810-277.917-871.25-13.917v0 0zM0 685.5v-152.833c77.5-268.833 847.417-232.5 875.583-1.917v152.75c-38.333-253.25-814.333-255.833-875.583 2v0zM0 488v-156.5c77.5-275.25 847.417-238.083 875.583-1.917v156.333c-38.333-259.25-814.333-261.917-875.583 2.083v0z" />
|
||||
<glyph unicode="" glyph-name="headphones" d="M288 384h-64v-448h64c17.6 0 32 14.4 32 32v384c0 17.6-14.4 32-32 32zM736 384c-17.602 0-32-14.4-32-32v-384c0-17.6 14.398-32 32-32h64v448h-64zM1024 448c0 282.77-229.23 512-512 512s-512-229.23-512-512c0-61.412 10.83-120.29 30.656-174.848-19.478-33.206-30.656-71.87-30.656-113.152 0-112.846 83.448-206.188 192-221.716v443.418c-31.914-4.566-61.664-15.842-87.754-32.378-5.392 26.718-8.246 54.364-8.246 82.676 0 229.75 186.25 416 416 416s416-186.25 416-416c0-28.314-2.83-55.968-8.22-82.696-26.1 16.546-55.854 27.848-87.78 32.418v-443.44c108.548 15.532 192 108.874 192 221.714 0 41.274-11.178 79.934-30.648 113.138 19.828 54.566 30.648 113.452 30.648 174.866z" />
|
||||
<glyph unicode="" glyph-name="music" d="M960 960h64v-736c0-88.366-100.29-160-224-160s-224 71.634-224 160c0 88.368 100.29 160 224 160 62.684 0 119.342-18.4 160-48.040v368.040l-512-113.778v-494.222c0-88.366-100.288-160-224-160s-224 71.634-224 160c0 88.368 100.288 160 224 160 62.684 0 119.342-18.4 160-48.040v624.040l576 128z" />
|
||||
<glyph unicode="" glyph-name="video" d="M384 672c0 88.366 71.634 160 160 160s160-71.634 160-160c0-88.366-71.634-160-160-160s-160 71.634-160 160zM0 672c0 88.366 71.634 160 160 160s160-71.634 160-160c0-88.366-71.634-160-160-160s-160 71.634-160 160zM768 352v96c0 35.2-28.8 64-64 64h-640c-35.2 0-64-28.8-64-64v-320c0-35.2 28.8-64 64-64h640c35.2 0 64 28.8 64 64v96l256-160v448l-256-160zM640 192h-512v192h512v-192z" />
|
||||
<glyph unicode="" glyph-name="microphone-3" d="M480 256c88.366 0 160 71.634 160 160v384c0 88.366-71.634 160-160 160s-160-71.634-160-160v-384c0-88.366 71.636-160 160-160zM704 512v-96c0-123.71-100.29-224-224-224-123.712 0-224 100.29-224 224v96h-64v-96c0-148.238 112.004-270.3 256-286.22v-129.78h-128v-64h320v64h-128v129.78c143.994 15.92 256 137.982 256 286.22v96h-64z" />
|
||||
<glyph unicode="" glyph-name="book" d="M896 832v-832h-672c-53.026 0-96 42.98-96 96s42.974 96 96 96h608v768h-640c-70.398 0-128-57.6-128-128v-768c0-70.4 57.602-128 128-128h768v896h-64zM224.056 128v0c-0.018-0.002-0.038 0-0.056 0-17.672 0-32-14.326-32-32s14.328-32 32-32c0.018 0 0.038 0.002 0.056 0.002v-0.002h607.89v64h-607.89z" />
|
||||
<glyph unicode="" glyph-name="books-2" horiz-adv-x="1152" d="M224 832h-192c-17.6 0-32-14.4-32-32v-704c0-17.6 14.4-32 32-32h192c17.6 0 32 14.4 32 32v704c0 17.6-14.4 32-32 32zM192 640h-128v64h128v-64zM544 832h-192c-17.6 0-32-14.4-32-32v-704c0-17.6 14.4-32 32-32h192c17.6 0 32 14.4 32 32v704c0 17.6-14.4 32-32 32zM512 640h-128v64h128v-64zM765.088 782.52l-171.464-86.394c-15.716-7.918-22.096-27.258-14.178-42.976l287.978-571.548c7.918-15.718 27.258-22.098 42.976-14.178l171.464 86.392c15.716 7.92 22.096 27.26 14.178 42.974l-287.978 571.55c-7.92 15.718-27.26 22.1-42.976 14.18z" />
|
||||
<glyph unicode="" glyph-name="file-picture" d="M832 64h-640v128l192 320 263-320 185 128v-256zM832 480c0-53.020-42.98-96-96-96-53.022 0-96 42.98-96 96s42.978 96 96 96c53.020 0 96-42.98 96-96zM917.806 730.924c-22.212 30.292-53.174 65.7-87.178 99.704s-69.412 64.964-99.704 87.178c-51.574 37.82-76.592 42.194-90.924 42.194h-496c-44.112 0-80-35.888-80-80v-864c0-44.112 35.888-80 80-80h736c44.112 0 80 35.888 80 80v624c0 14.332-4.372 39.35-42.194 90.924zM785.374 785.374c30.7-30.7 54.8-58.398 72.58-81.374h-153.954v153.946c22.984-17.78 50.678-41.878 81.374-72.572zM896 16c0-8.672-7.328-16-16-16h-736c-8.672 0-16 7.328-16 16v864c0 8.672 7.328 16 16 16 0 0 495.956 0.002 496 0v-224c0-17.672 14.326-32 32-32h224v-624z" />
|
||||
<glyph unicode="" glyph-name="database-1" d="M512 960c-282.77 0-512-71.634-512-160v-128c0-88.366 229.23-160 512-160s512 71.634 512 160v128c0 88.366-229.23 160-512 160zM512 416c-282.77 0-512 71.634-512 160v-192c0-88.366 229.23-160 512-160s512 71.634 512 160v192c0-88.366-229.23-160-512-160zM512 128c-282.77 0-512 71.634-512 160v-192c0-88.366 229.23-160 512-160s512 71.634 512 160v192c0-88.366-229.23-160-512-160z" />
|
||||
<glyph unicode="" glyph-name="rocket" d="M704 896l-320-320h-192l-192-256c0 0 203.416 56.652 322.066 30.084l-322.066-414.084 421.902 328.144c58.838-134.654-37.902-328.144-37.902-328.144l256 192v192l320 320 64 320-320-64z" />
|
||||
<glyph unicode="" glyph-name="power" d="M384 960l-384-512h384l-256-512 896 640h-512l384 384z" />
|
||||
<glyph unicode="" glyph-name="star" d="M1024 562.95l-353.78 51.408-158.22 320.582-158.216-320.582-353.784-51.408 256-249.538-60.432-352.352 316.432 166.358 316.432-166.358-60.434 352.352 256.002 249.538z" />
|
||||
<glyph unicode="" glyph-name="heart" d="M755.188 896c-107.63 0-200.258-87.554-243.164-179-42.938 91.444-135.578 179-243.216 179-148.382 0-268.808-120.44-268.808-268.832 0-301.846 304.5-380.994 512.022-679.418 196.154 296.576 511.978 387.206 511.978 679.418 0 148.392-120.43 268.832-268.812 268.832z" />
|
||||
<glyph unicode="" glyph-name="rss" d="M136.294 209.070c-75.196 0-136.292-61.334-136.292-136.076 0-75.154 61.1-135.802 136.292-135.802 75.466 0 136.494 60.648 136.494 135.802-0.002 74.742-61.024 136.076-136.494 136.076zM0.156 612.070v-196.258c127.784 0 247.958-49.972 338.458-140.512 90.384-90.318 140.282-211.036 140.282-339.3h197.122c-0.002 372.82-303.282 676.070-675.862 676.070zM0.388 960v-196.356c455.782 0 826.756-371.334 826.756-827.644h196.856c0 564.47-459.254 1024-1023.612 1024z" />
|
||||
</font></defs></svg>
|
||||
|
After Width: | Height: | Size: 13 KiB |
BIN
client/static/fonts/absicons/absicons.ttf
Normal file
BIN
client/static/fonts/absicons/absicons.ttf
Normal file
Binary file not shown.
BIN
client/static/fonts/absicons/absicons.woff
Normal file
BIN
client/static/fonts/absicons/absicons.woff
Normal file
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 8.9 KiB |
@@ -28,7 +28,8 @@ export const state = () => ({
|
||||
text: 'YYYY-MM-DD',
|
||||
value: 'yyyy-MM-dd'
|
||||
}
|
||||
]
|
||||
],
|
||||
libraryIcons: ['database', 'audiobookshelf', 'books-1', 'books-2', 'book-1', 'microphone-1', 'microphone-3', 'radio', 'podcast', 'rss', 'headphones', 'music', 'file-picture', 'rocket', 'power', 'star', 'heart']
|
||||
})
|
||||
|
||||
export const getters = {
|
||||
|
||||
@@ -37,10 +37,6 @@ export const getters = {
|
||||
if (!state.serverSettings) return null
|
||||
return state.serverSettings[key]
|
||||
},
|
||||
getBookCoverAspectRatio: state => {
|
||||
if (!state.serverSettings || isNaN(state.serverSettings.coverAspectRatio)) return 1
|
||||
return state.serverSettings.coverAspectRatio === 0 ? 1.6 : 1
|
||||
},
|
||||
getNumLibraryItemsSelected: state => state.selectedLibraryItems.length,
|
||||
getLibraryItemIdStreaming: state => {
|
||||
return state.streamLibraryItem ? state.streamLibraryItem.id : null
|
||||
|
||||
@@ -8,7 +8,10 @@ export const state = () => ({
|
||||
folders: [],
|
||||
issues: 0,
|
||||
folderLastUpdate: 0,
|
||||
filterData: null
|
||||
filterData: null,
|
||||
seriesSortBy: 'name',
|
||||
seriesSortDesc: false,
|
||||
seriesFilterBy: 'all'
|
||||
})
|
||||
|
||||
export const getters = {
|
||||
@@ -289,5 +292,14 @@ export const mutations = {
|
||||
state.filterData.languages.sort((a, b) => a.localeCompare(b))
|
||||
}
|
||||
}
|
||||
},
|
||||
setSeriesSortBy(state, sortBy) {
|
||||
state.seriesSortBy = sortBy
|
||||
},
|
||||
setSeriesSortDesc(state, sortDesc) {
|
||||
state.seriesSortDesc = sortDesc
|
||||
},
|
||||
setSeriesFilterBy(state, filterBy) {
|
||||
state.seriesFilterBy = filterBy
|
||||
}
|
||||
}
|
||||
@@ -10,12 +10,48 @@ export const state = () => ({
|
||||
value: 'openlibrary'
|
||||
},
|
||||
{
|
||||
text: 'Audible',
|
||||
text: 'iTunes',
|
||||
value: 'itunes'
|
||||
},
|
||||
{
|
||||
text: 'Audible.com',
|
||||
value: 'audible'
|
||||
},
|
||||
{
|
||||
text: 'iTunes',
|
||||
value: 'itunes'
|
||||
text: 'Audible.ca',
|
||||
value: 'audible.ca'
|
||||
},
|
||||
{
|
||||
text: 'Audible.co.uk',
|
||||
value: 'audible.uk'
|
||||
},
|
||||
{
|
||||
text: 'Audible.co.au',
|
||||
value: 'audible.au'
|
||||
},
|
||||
{
|
||||
text: 'Audible.fr',
|
||||
value: 'audible.fr'
|
||||
},
|
||||
{
|
||||
text: 'Audible.de',
|
||||
value: 'audible.de'
|
||||
},
|
||||
{
|
||||
text: 'Audible.co.jp',
|
||||
value: 'audible.jp'
|
||||
},
|
||||
{
|
||||
text: 'Audible.it',
|
||||
value: 'audible.it'
|
||||
},
|
||||
{
|
||||
text: 'Audible.co.in',
|
||||
value: 'audible.in'
|
||||
},
|
||||
{
|
||||
text: 'Audible.es',
|
||||
value: 'audible.es'
|
||||
}
|
||||
],
|
||||
podcastProviders: [
|
||||
|
||||
644
package-lock.json
generated
644
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.2.0",
|
||||
"version": "2.2.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.1.5",
|
||||
"version": "2.2.2",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"axios": "^0.26.1",
|
||||
@@ -19,6 +19,9 @@
|
||||
},
|
||||
"bin": {
|
||||
"audiobookshelf": "prod.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^2.0.20"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/component-emitter": {
|
||||
@@ -41,6 +44,12 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.41.tgz",
|
||||
"integrity": "sha512-xA6drNNeqb5YyV5fO3OAEsnXLfO7uF0whiOfPTz5AeDo8KeZFmODKnvwPymMNO8qE/an8pVY/O50tig2SQCrGw=="
|
||||
},
|
||||
"node_modules/abbrev": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
@@ -53,6 +62,19 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/anymatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
|
||||
"integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"normalize-path": "^3.0.0",
|
||||
"picomatch": "^2.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
@@ -66,6 +88,12 @@
|
||||
"follow-redirects": "^1.14.8"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/base64id": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
|
||||
@@ -74,6 +102,15 @@
|
||||
"node": "^4.5.0 || >= 5.9"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.0",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz",
|
||||
@@ -97,6 +134,28 @@
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fill-range": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
@@ -117,11 +176,44 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
||||
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.10.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/component-emitter": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
|
||||
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
@@ -377,6 +469,18 @@
|
||||
"node": ">= 0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
|
||||
@@ -429,6 +533,20 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
@@ -447,6 +565,18 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.10",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
|
||||
@@ -463,6 +593,15 @@
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
||||
@@ -518,6 +657,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore-by-default": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
|
||||
"integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
@@ -531,6 +676,48 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-glob": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
@@ -582,6 +769,18 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
@@ -600,6 +799,73 @@
|
||||
"resolved": "https://registry.npmjs.org/node-tone/-/node-tone-1.0.1.tgz",
|
||||
"integrity": "sha512-wi7L0taDZMN6tM5l85TDKHsYzdhqJTtPNgvgpk2zHeZzPt6ZIUZ9vBLTJRRDpm0xzCvbsvFHjAaudeQjLHTE4w=="
|
||||
},
|
||||
"node_modules/nodemon": {
|
||||
"version": "2.0.20",
|
||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz",
|
||||
"integrity": "sha512-Km2mWHKKY5GzRg6i1j5OxOHQtuvVsgskLfigG25yTtbyfRGn/GNvIbRyOf1PSCKJ2aT/58TiuUsuOU5UToVViw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chokidar": "^3.5.2",
|
||||
"debug": "^3.2.7",
|
||||
"ignore-by-default": "^1.0.1",
|
||||
"minimatch": "^3.1.2",
|
||||
"pstree.remy": "^1.1.8",
|
||||
"semver": "^5.7.1",
|
||||
"simple-update-notifier": "^1.0.7",
|
||||
"supports-color": "^5.5.0",
|
||||
"touch": "^3.1.0",
|
||||
"undefsafe": "^2.0.5"
|
||||
},
|
||||
"bin": {
|
||||
"nodemon": "bin/nodemon.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/nodemon"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemon/node_modules/debug": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemon/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/nopt": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
|
||||
"integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"abbrev": "1"
|
||||
},
|
||||
"bin": {
|
||||
"nopt": "bin/nopt.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
@@ -640,6 +906,18 @@
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
||||
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@@ -652,6 +930,12 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/pstree.remy": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
||||
"integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.10.3",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz",
|
||||
@@ -688,6 +972,18 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
@@ -717,6 +1013,15 @@
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
|
||||
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver"
|
||||
}
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "0.18.0",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
|
||||
@@ -777,6 +1082,27 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-update-notifier": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.0.7.tgz",
|
||||
"integrity": "sha512-BBKgR84BJQJm6WjWFMHgLVuo61FBDSj1z/xSFUIozqO6wO7ii0JxCqlIud7Enr/+LhlbNI0whErq96P2qHNWew==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"semver": "~7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-update-notifier/node_modules/semver": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz",
|
||||
"integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io": {
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.1.tgz",
|
||||
@@ -861,6 +1187,30 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-flag": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
@@ -869,6 +1219,18 @@
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/touch": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz",
|
||||
"integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"nopt": "~1.0.10"
|
||||
},
|
||||
"bin": {
|
||||
"nodetouch": "bin/nodetouch.js"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
@@ -881,6 +1243,12 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/undefsafe": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
|
||||
"integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
@@ -967,6 +1335,12 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.41.tgz",
|
||||
"integrity": "sha512-xA6drNNeqb5YyV5fO3OAEsnXLfO7uF0whiOfPTz5AeDo8KeZFmODKnvwPymMNO8qE/an8pVY/O50tig2SQCrGw=="
|
||||
},
|
||||
"abbrev": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
|
||||
"dev": true
|
||||
},
|
||||
"accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
@@ -976,6 +1350,16 @@
|
||||
"negotiator": "0.6.3"
|
||||
}
|
||||
},
|
||||
"anymatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
|
||||
"integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"normalize-path": "^3.0.0",
|
||||
"picomatch": "^2.0.4"
|
||||
}
|
||||
},
|
||||
"array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
@@ -989,11 +1373,23 @@
|
||||
"follow-redirects": "^1.14.8"
|
||||
}
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true
|
||||
},
|
||||
"base64id": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
|
||||
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="
|
||||
},
|
||||
"binary-extensions": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
|
||||
"dev": true
|
||||
},
|
||||
"body-parser": {
|
||||
"version": "1.20.0",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz",
|
||||
@@ -1013,6 +1409,25 @@
|
||||
"unpipe": "1.0.0"
|
||||
}
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fill-range": "^7.0.1"
|
||||
}
|
||||
},
|
||||
"bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
@@ -1027,11 +1442,33 @@
|
||||
"get-intrinsic": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"chokidar": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
||||
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"fsevents": "~2.3.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
}
|
||||
},
|
||||
"component-emitter": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
|
||||
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"dev": true
|
||||
},
|
||||
"content-disposition": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
@@ -1220,6 +1657,15 @@
|
||||
"vary": "~1.1.2"
|
||||
}
|
||||
},
|
||||
"fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"finalhandler": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
|
||||
@@ -1249,6 +1695,13 @@
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="
|
||||
},
|
||||
"fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
@@ -1264,6 +1717,15 @@
|
||||
"has-symbols": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-glob": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"graceful-fs": {
|
||||
"version": "4.2.10",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
|
||||
@@ -1277,6 +1739,12 @@
|
||||
"function-bind": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
|
||||
"dev": true
|
||||
},
|
||||
"has-symbols": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
||||
@@ -1313,6 +1781,12 @@
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
}
|
||||
},
|
||||
"ignore-by-default": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
|
||||
"integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
|
||||
"dev": true
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
@@ -1323,6 +1797,36 @@
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
|
||||
},
|
||||
"is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true
|
||||
},
|
||||
"is-glob": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-extglob": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true
|
||||
},
|
||||
"media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
@@ -1356,6 +1860,15 @@
|
||||
"mime-db": "1.52.0"
|
||||
}
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
@@ -1371,6 +1884,56 @@
|
||||
"resolved": "https://registry.npmjs.org/node-tone/-/node-tone-1.0.1.tgz",
|
||||
"integrity": "sha512-wi7L0taDZMN6tM5l85TDKHsYzdhqJTtPNgvgpk2zHeZzPt6ZIUZ9vBLTJRRDpm0xzCvbsvFHjAaudeQjLHTE4w=="
|
||||
},
|
||||
"nodemon": {
|
||||
"version": "2.0.20",
|
||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz",
|
||||
"integrity": "sha512-Km2mWHKKY5GzRg6i1j5OxOHQtuvVsgskLfigG25yTtbyfRGn/GNvIbRyOf1PSCKJ2aT/58TiuUsuOU5UToVViw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chokidar": "^3.5.2",
|
||||
"debug": "^3.2.7",
|
||||
"ignore-by-default": "^1.0.1",
|
||||
"minimatch": "^3.1.2",
|
||||
"pstree.remy": "^1.1.8",
|
||||
"semver": "^5.7.1",
|
||||
"simple-update-notifier": "^1.0.7",
|
||||
"supports-color": "^5.5.0",
|
||||
"touch": "^3.1.0",
|
||||
"undefsafe": "^2.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"nopt": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
|
||||
"integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"abbrev": "1"
|
||||
}
|
||||
},
|
||||
"normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"dev": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
@@ -1399,6 +1962,12 @@
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
||||
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
|
||||
},
|
||||
"picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true
|
||||
},
|
||||
"proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@@ -1408,6 +1977,12 @@
|
||||
"ipaddr.js": "1.9.1"
|
||||
}
|
||||
},
|
||||
"pstree.remy": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
||||
"integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
|
||||
"dev": true
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.10.3",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz",
|
||||
@@ -1432,6 +2007,15 @@
|
||||
"unpipe": "1.0.0"
|
||||
}
|
||||
},
|
||||
"readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"picomatch": "^2.2.1"
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
@@ -1447,6 +2031,12 @@
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
|
||||
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
|
||||
},
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
|
||||
"dev": true
|
||||
},
|
||||
"send": {
|
||||
"version": "0.18.0",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
|
||||
@@ -1500,6 +2090,23 @@
|
||||
"object-inspect": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"simple-update-notifier": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.0.7.tgz",
|
||||
"integrity": "sha512-BBKgR84BJQJm6WjWFMHgLVuo61FBDSj1z/xSFUIozqO6wO7ii0JxCqlIud7Enr/+LhlbNI0whErq96P2qHNWew==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"semver": "~7.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"semver": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz",
|
||||
"integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"socket.io": {
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.1.tgz",
|
||||
@@ -1563,11 +2170,38 @@
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has-flag": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-number": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
|
||||
},
|
||||
"touch": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz",
|
||||
"integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"nopt": "~1.0.10"
|
||||
}
|
||||
},
|
||||
"type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
@@ -1577,6 +2211,12 @@
|
||||
"mime-types": "~2.1.24"
|
||||
}
|
||||
},
|
||||
"undefsafe": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
|
||||
"integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
|
||||
"dev": true
|
||||
},
|
||||
"unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.2.0",
|
||||
"version": "2.2.2",
|
||||
"description": "Self-hosted audiobook and podcast server",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "node index.js",
|
||||
"dev": "nodemon --watch server index.js",
|
||||
"start": "node index.js",
|
||||
"client": "cd client && npm ci && npm run generate",
|
||||
"prod": "npm run client && npm ci && node prod.js",
|
||||
@@ -37,5 +37,8 @@
|
||||
"node-tone": "^1.0.1",
|
||||
"socket.io": "^4.4.1",
|
||||
"xml2js": "^0.4.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^2.0.20"
|
||||
}
|
||||
}
|
||||
@@ -29,12 +29,14 @@ Audiobookshelf is a self-hosted audiobook and podcast server.
|
||||
* Progressive Web App (PWA)
|
||||
* Chromecast support on the web app and android app
|
||||
* Fetch metadata and cover art from several sources
|
||||
* Chapter editor and chapter lookup (using [Audnexus API](https://audnex.us/))
|
||||
* Merge your audio files into a single m4b
|
||||
* Embed metadata and cover image into your audio files (using [Tone](https://github.com/sandreas/tone))
|
||||
* 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)
|
||||
|
||||
Join us on [discord](https://discord.gg/pJsjuNCKRq)
|
||||
Join us on [discord](https://discord.gg/pJsjuNCKRq) or [matrix](https://matrix.to/#/#audiobookshelf:matrix.org)
|
||||
|
||||
### Android App (beta)
|
||||
Try it out on the [Google Play Store](https://play.google.com/store/apps/details?id=com.audiobookshelf.app)
|
||||
@@ -80,6 +82,7 @@ docker run -d \
|
||||
|
||||
```bash
|
||||
docker stop audiobookshelf
|
||||
docker rm audiobookshelf
|
||||
docker pull ghcr.io/advplyr/audiobookshelf:latest
|
||||
docker start audiobookshelf
|
||||
```
|
||||
@@ -125,8 +128,6 @@ docker-compose --file <path/to/config>/docker-compose.yml pull
|
||||
docker-compose --file <path/to/config>/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
** We recommend updating the the latest version of Docker Compose
|
||||
|
||||
### Linux (amd64) Install
|
||||
|
||||
Debian package will use this config file `/etc/default/audiobookshelf` if exists. The install will create a user and group named `audiobookshelf`.
|
||||
|
||||
19
server/Db.js
19
server/Db.js
@@ -24,15 +24,16 @@ class Db {
|
||||
this.SeriesPath = Path.join(global.ConfigPath, 'series')
|
||||
this.FeedsPath = Path.join(global.ConfigPath, 'feeds')
|
||||
|
||||
this.libraryItemsDb = new njodb.Database(this.LibraryItemsPath)
|
||||
this.usersDb = new njodb.Database(this.UsersPath)
|
||||
this.sessionsDb = new njodb.Database(this.SessionsPath)
|
||||
this.librariesDb = new njodb.Database(this.LibrariesPath, { datastores: 2 })
|
||||
this.settingsDb = new njodb.Database(this.SettingsPath, { datastores: 2 })
|
||||
this.collectionsDb = new njodb.Database(this.CollectionsPath, { datastores: 2 })
|
||||
this.authorsDb = new njodb.Database(this.AuthorsPath)
|
||||
this.seriesDb = new njodb.Database(this.SeriesPath, { datastores: 2 })
|
||||
this.feedsDb = new njodb.Database(this.FeedsPath, { datastores: 2 })
|
||||
const staleTime = 1000 * 60 * 2
|
||||
this.libraryItemsDb = new njodb.Database(this.LibraryItemsPath, { lockoptions: { stale: staleTime } })
|
||||
this.usersDb = new njodb.Database(this.UsersPath, { lockoptions: { stale: staleTime } })
|
||||
this.sessionsDb = new njodb.Database(this.SessionsPath, { lockoptions: { stale: staleTime } })
|
||||
this.librariesDb = new njodb.Database(this.LibrariesPath, { datastores: 2, lockoptions: { stale: staleTime } })
|
||||
this.settingsDb = new njodb.Database(this.SettingsPath, { datastores: 2, lockoptions: { stale: staleTime } })
|
||||
this.collectionsDb = new njodb.Database(this.CollectionsPath, { datastores: 2, lockoptions: { stale: staleTime } })
|
||||
this.authorsDb = new njodb.Database(this.AuthorsPath, { lockoptions: { stale: staleTime } })
|
||||
this.seriesDb = new njodb.Database(this.SeriesPath, { datastores: 2, lockoptions: { stale: staleTime } })
|
||||
this.feedsDb = new njodb.Database(this.FeedsPath, { datastores: 2, lockoptions: { stale: staleTime } })
|
||||
|
||||
this.libraryItems = []
|
||||
this.users = []
|
||||
|
||||
@@ -9,6 +9,7 @@ class AuthorController {
|
||||
constructor() { }
|
||||
|
||||
async findOne(req, res) {
|
||||
const libraryId = req.query.library
|
||||
const include = (req.query.include || '').split(',')
|
||||
|
||||
const authorJson = req.author.toJSON()
|
||||
@@ -16,6 +17,7 @@ class AuthorController {
|
||||
// Used on author landing page to include library items and items grouped in series
|
||||
if (include.includes('items')) {
|
||||
authorJson.libraryItems = this.db.libraryItems.filter(li => {
|
||||
if (libraryId && li.libraryId !== libraryId) return false
|
||||
if (!req.user.checkCanAccessLibraryItem(li)) return false // filter out library items user cannot access
|
||||
return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(req.author.id)
|
||||
})
|
||||
|
||||
@@ -222,7 +222,7 @@ class LibraryController {
|
||||
}
|
||||
|
||||
if (payload.collapseseries) {
|
||||
libraryItems = libraryHelpers.collapseBookSeries(libraryItems)
|
||||
libraryItems = libraryHelpers.collapseBookSeries(libraryItems, this.db.series)
|
||||
payload.total = libraryItems.length
|
||||
} else if (filterSeries) {
|
||||
// Book media when filtering series will include series object on media metadata
|
||||
@@ -275,10 +275,25 @@ class LibraryController {
|
||||
minified: req.query.minified === '1'
|
||||
}
|
||||
|
||||
var series = libraryHelpers.getSeriesFromBooks(libraryItems, payload.minified)
|
||||
series = sort(series).asc(s => {
|
||||
return this.db.serverSettings.sortingIgnorePrefix ? s.nameIgnorePrefix : s.name
|
||||
})
|
||||
var series = libraryHelpers.getSeriesFromBooks(libraryItems, this.db.series, payload.filterBy, req.user, payload.minified)
|
||||
|
||||
const direction = payload.sortDesc ? 'desc' : 'asc'
|
||||
series = naturalSort(series).by([
|
||||
{
|
||||
[direction]: (se) => {
|
||||
if (payload.sortBy === 'numBooks') {
|
||||
return se.books.length
|
||||
} else if (payload.sortBy === 'totalDuration') {
|
||||
return se.totalDuration
|
||||
} else if (payload.sortBy === 'addedAt') {
|
||||
return se.addedAt
|
||||
} else { // sort by name
|
||||
return this.db.serverSettings.sortingIgnorePrefix ? se.nameIgnorePrefixSort : se.name
|
||||
}
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
payload.total = series.length
|
||||
|
||||
if (payload.limit) {
|
||||
|
||||
@@ -398,7 +398,8 @@ class LibraryItemController {
|
||||
}
|
||||
|
||||
const useTone = req.query.tone === '1'
|
||||
this.audioMetadataManager.updateMetadataForItem(req.user, req.libraryItem, useTone)
|
||||
const forceEmbedChapters = req.query.forceEmbedChapters === '1'
|
||||
this.audioMetadataManager.updateMetadataForItem(req.user, req.libraryItem, useTone, forceEmbedChapters)
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
|
||||
@@ -210,7 +210,8 @@ class MiscController {
|
||||
|
||||
async findChapters(req, res) {
|
||||
var asin = req.query.asin
|
||||
var chapterData = await this.bookFinder.findChapters(asin)
|
||||
var region = (req.query.region || 'us').toLowerCase()
|
||||
var chapterData = await this.bookFinder.findChapters(asin, region)
|
||||
if (!chapterData) {
|
||||
return res.json({ error: 'Chapters not found' })
|
||||
}
|
||||
|
||||
@@ -150,8 +150,9 @@ class BookFinder {
|
||||
return this.iTunesApi.searchAudiobooks(title)
|
||||
}
|
||||
|
||||
async getAudibleResults(title, author, asin) {
|
||||
var books = await this.audible.search(title, author, asin);
|
||||
async getAudibleResults(title, author, asin, provider) {
|
||||
const region = provider.includes('.') ? provider.split('.').pop() : ''
|
||||
const books = await this.audible.search(title, author, asin, region)
|
||||
if (this.verbose) Logger.debug(`Audible Book Search Results: ${books.length || 0}`)
|
||||
if (!books) return []
|
||||
return books
|
||||
@@ -165,8 +166,8 @@ class BookFinder {
|
||||
|
||||
if (provider === 'google') {
|
||||
books = await this.getGoogleBooksResults(title, author)
|
||||
} else if (provider === 'audible') {
|
||||
books = await this.getAudibleResults(title, author, asin)
|
||||
} else if (provider.startsWith('audible')) {
|
||||
books = await this.getAudibleResults(title, author, asin, provider)
|
||||
} else if (provider === 'itunes') {
|
||||
books = await this.getiTunesAudiobooksResults(title, author)
|
||||
} else if (provider === 'openlibrary') {
|
||||
@@ -208,8 +209,8 @@ class BookFinder {
|
||||
return covers
|
||||
}
|
||||
|
||||
findChapters(asin) {
|
||||
return this.audnexus.getChaptersByASIN(asin)
|
||||
findChapters(asin, region) {
|
||||
return this.audnexus.getChaptersByASIN(asin, region)
|
||||
}
|
||||
}
|
||||
module.exports = BookFinder
|
||||
@@ -62,7 +62,7 @@ class AbMergeManager {
|
||||
targetFilename,
|
||||
targetFilepath: Path.join(libraryItem.path, targetFilename),
|
||||
itemCachePath,
|
||||
toneMetadataObject: null
|
||||
toneJsonObject: null
|
||||
}
|
||||
const taskDescription = `Encoding audiobook "${libraryItem.media.metadata.title}" into a single m4b file.`
|
||||
task.setData('encode-m4b', 'Encoding M4b', taskDescription, taskData)
|
||||
@@ -120,22 +120,19 @@ class AbMergeManager {
|
||||
}
|
||||
}
|
||||
|
||||
var chaptersFilePath = null
|
||||
if (libraryItem.media.chapters.length) {
|
||||
chaptersFilePath = Path.join(task.data.itemCachePath, 'chapters.txt')
|
||||
try {
|
||||
await toneHelpers.writeToneChaptersFile(libraryItem.media.chapters, chaptersFilePath)
|
||||
} catch (error) {
|
||||
Logger.error(`[AbMergeManager] Write chapters.txt failed`, error)
|
||||
chaptersFilePath = null
|
||||
}
|
||||
var toneJsonPath = null
|
||||
try {
|
||||
toneJsonPath = Path.join(task.data.itemCachePath, 'metadata.json')
|
||||
await toneHelpers.writeToneMetadataJsonFile(libraryItem, libraryItem.media.chapters, toneJsonPath, 1)
|
||||
} catch (error) {
|
||||
Logger.error(`[AbMergeManager] Write metadata.json failed`, error)
|
||||
toneJsonPath = null
|
||||
}
|
||||
|
||||
const toneMetadataObject = toneHelpers.getToneMetadataObject(libraryItem, chaptersFilePath)
|
||||
toneMetadataObject.TrackNumber = 1
|
||||
task.data.toneMetadataObject = toneMetadataObject
|
||||
|
||||
Logger.debug(`[AbMergeManager] Book "${libraryItem.media.metadata.title}" tone metadata object=`, toneMetadataObject)
|
||||
task.data.toneJsonObject = {
|
||||
'ToneJsonFile': toneJsonPath,
|
||||
'TrackNumber': 1,
|
||||
}
|
||||
|
||||
var workerData = {
|
||||
inputs: ffmpegInputs,
|
||||
@@ -190,7 +187,7 @@ class AbMergeManager {
|
||||
}
|
||||
|
||||
// Write metadata to merged file
|
||||
const success = await toneHelpers.tagAudioFile(task.data.tempFilepath, task.data.toneMetadataObject)
|
||||
const success = await toneHelpers.tagAudioFile(task.data.tempFilepath, task.data.toneJsonObject)
|
||||
if (!success) {
|
||||
Logger.error(`[AbMergeManager] Failed to write metadata to file "${task.data.tempFilepath}"`)
|
||||
task.setFailed('Failed to write metadata to m4b file')
|
||||
|
||||
@@ -15,9 +15,9 @@ class AudioMetadataMangaer {
|
||||
this.clientEmitter = clientEmitter
|
||||
}
|
||||
|
||||
updateMetadataForItem(user, libraryItem, useTone = true) {
|
||||
updateMetadataForItem(user, libraryItem, useTone, forceEmbedChapters) {
|
||||
if (useTone) {
|
||||
this.updateMetadataForItemWithTone(user, libraryItem)
|
||||
this.updateMetadataForItemWithTone(user, libraryItem, forceEmbedChapters)
|
||||
} else {
|
||||
this.updateMetadataForItemWithFfmpeg(user, libraryItem)
|
||||
}
|
||||
@@ -30,7 +30,7 @@ class AudioMetadataMangaer {
|
||||
return toneHelpers.getToneMetadataObject(libraryItem)
|
||||
}
|
||||
|
||||
async updateMetadataForItemWithTone(user, libraryItem) {
|
||||
async updateMetadataForItemWithTone(user, libraryItem, forceEmbedChapters) {
|
||||
var audioFiles = libraryItem.media.includedAudioFiles
|
||||
|
||||
const itemAudioMetadataPayload = {
|
||||
@@ -43,26 +43,22 @@ class AudioMetadataMangaer {
|
||||
this.emitter('audio_metadata_started', itemAudioMetadataPayload)
|
||||
|
||||
// Write chapters file
|
||||
var chaptersFilePath = null
|
||||
var toneJsonPath = null
|
||||
const itemCacheDir = Path.join(global.MetadataPath, `cache/items/${libraryItem.id}`)
|
||||
await fs.ensureDir(itemCacheDir)
|
||||
|
||||
if (libraryItem.media.chapters.length) {
|
||||
chaptersFilePath = Path.join(itemCacheDir, 'chapters.txt')
|
||||
try {
|
||||
await toneHelpers.writeToneChaptersFile(libraryItem.media.chapters, chaptersFilePath)
|
||||
} catch (error) {
|
||||
Logger.error(`[AudioMetadataManager] Write chapters.txt failed`, error)
|
||||
chaptersFilePath = null
|
||||
}
|
||||
try {
|
||||
toneJsonPath = Path.join(itemCacheDir, 'metadata.json')
|
||||
const chapters = (audioFiles.length == 1 || forceEmbedChapters) ? libraryItem.media.chapters : null
|
||||
await toneHelpers.writeToneMetadataJsonFile(libraryItem, chapters, toneJsonPath, audioFiles.length)
|
||||
} catch (error) {
|
||||
Logger.error(`[AudioMetadataManager] Write metadata.json failed`, error)
|
||||
toneJsonPath = null
|
||||
}
|
||||
|
||||
const toneMetadataObject = toneHelpers.getToneMetadataObject(libraryItem, chaptersFilePath)
|
||||
Logger.debug(`[AudioMetadataManager] Book "${libraryItem.media.metadata.title}" tone metadata object=`, toneMetadataObject)
|
||||
|
||||
const results = []
|
||||
for (const af of audioFiles) {
|
||||
const result = await this.updateAudioFileMetadataWithTone(libraryItem.id, af, toneMetadataObject, itemCacheDir)
|
||||
const result = await this.updateAudioFileMetadataWithTone(libraryItem.id, af, toneJsonPath, itemCacheDir)
|
||||
results.push(result)
|
||||
}
|
||||
|
||||
@@ -74,7 +70,7 @@ class AudioMetadataMangaer {
|
||||
this.emitter('audio_metadata_finished', itemAudioMetadataPayload)
|
||||
}
|
||||
|
||||
async updateAudioFileMetadataWithTone(libraryItemId, audioFile, toneMetadataObject, itemCacheDir) {
|
||||
async updateAudioFileMetadataWithTone(libraryItemId, audioFile, toneJsonPath, itemCacheDir) {
|
||||
const resultPayload = {
|
||||
libraryItemId,
|
||||
index: audioFile.index,
|
||||
@@ -93,8 +89,8 @@ class AudioMetadataMangaer {
|
||||
}
|
||||
|
||||
const _toneMetadataObject = {
|
||||
...toneMetadataObject,
|
||||
'TrackNumber': audioFile.index
|
||||
'ToneJsonFile': toneJsonPath,
|
||||
'TrackNumber': audioFile.index,
|
||||
}
|
||||
|
||||
resultPayload.success = await toneHelpers.tagAudioFile(audioFile.metadata.path, _toneMetadataObject)
|
||||
|
||||
@@ -204,7 +204,7 @@ class PodcastManager {
|
||||
const dateToCheckForEpisodesAfter = latestEpisodePublishedAt || lastEpisodeCheckDate
|
||||
Logger.debug(`[PodcastManager] runEpisodeCheck: "${libraryItem.media.metadata.title}" checking for episodes after ${new Date(dateToCheckForEpisodesAfter)}`)
|
||||
|
||||
var newEpisodes = await this.checkPodcastForNewEpisodes(libraryItem, dateToCheckForEpisodesAfter)
|
||||
var newEpisodes = await this.checkPodcastForNewEpisodes(libraryItem, dateToCheckForEpisodesAfter, libraryItem.media.maxNewEpisodesToDownload)
|
||||
Logger.debug(`[PodcastManager] runEpisodeCheck: ${newEpisodes ? newEpisodes.length : 'N/A'} episodes found`)
|
||||
|
||||
if (!newEpisodes) { // Failed
|
||||
|
||||
@@ -8,7 +8,7 @@ class Library {
|
||||
this.name = null
|
||||
this.folders = []
|
||||
this.displayOrder = 1
|
||||
this.icon = 'database' // database, podcast, book, audiobook, comic
|
||||
this.icon = 'database'
|
||||
this.mediaType = 'book' // book, podcast
|
||||
this.provider = 'google'
|
||||
|
||||
@@ -46,14 +46,15 @@ class Library {
|
||||
|
||||
this.createdAt = library.createdAt
|
||||
this.lastUpdate = library.lastUpdate
|
||||
this.cleanOldValues() // mediaType changed for v2
|
||||
this.cleanOldValues() // mediaType changed for v2 and icon change for v2.2.2
|
||||
}
|
||||
|
||||
cleanOldValues() {
|
||||
var availableIcons = ['database', 'audiobook', 'book', 'comic', 'podcast']
|
||||
const availableIcons = ['database', 'audiobookshelf', 'books-1', 'books-2', 'book-1', 'microphone-1', 'microphone-3', 'radio', 'podcast', 'rss', 'headphones', 'music', 'file-picture', 'rocket', 'power', 'star', 'heart']
|
||||
if (!availableIcons.includes(this.icon)) {
|
||||
if (this.icon === 'default') this.icon = 'database'
|
||||
else if (this.icon.endsWith('s') && availableIcons.includes(this.icon.slice(0, -1))) this.icon = this.icon.slice(0, -1)
|
||||
if (this.icon === 'audiobook') this.icon = 'audiobookshelf'
|
||||
else if (this.icon === 'book') this.icon = 'books-1'
|
||||
else if (this.icon === 'comic') this.icon = 'file-picture'
|
||||
else this.icon = 'database'
|
||||
}
|
||||
if (!this.mediaType || (this.mediaType !== 'podcast' && this.mediaType !== 'book' && this.mediaType !== 'video')) {
|
||||
|
||||
@@ -22,7 +22,7 @@ class PodcastEpisodeDownload {
|
||||
toJSONForClient() {
|
||||
return {
|
||||
id: this.id,
|
||||
episodeDisplayTitle: this.podcastEpisode ? this.podcastEpisode.bestFilename : null,
|
||||
episodeDisplayTitle: this.podcastEpisode ? this.podcastEpisode.title : null,
|
||||
url: this.url,
|
||||
libraryItemId: this.libraryItem ? this.libraryItem.id : null,
|
||||
isDownloading: this.isDownloading,
|
||||
@@ -35,7 +35,7 @@ class PodcastEpisodeDownload {
|
||||
}
|
||||
|
||||
get targetFilename() {
|
||||
return sanitizeFilename(`${this.podcastEpisode.bestFilename}.mp3`)
|
||||
return sanitizeFilename(`${this.podcastEpisode.title}.mp3`)
|
||||
}
|
||||
get targetPath() {
|
||||
return Path.join(this.libraryItem.path, this.targetFilename)
|
||||
|
||||
@@ -73,7 +73,9 @@ class Stream extends EventEmitter {
|
||||
AudioMimeType.FLAC,
|
||||
AudioMimeType.OPUS,
|
||||
AudioMimeType.WMA,
|
||||
AudioMimeType.AIFF
|
||||
AudioMimeType.AIFF,
|
||||
AudioMimeType.WEBM,
|
||||
AudioMimeType.WEBMA
|
||||
]
|
||||
}
|
||||
get userToken() {
|
||||
|
||||
@@ -103,10 +103,6 @@ class PodcastEpisode {
|
||||
return this.audioFile.duration
|
||||
}
|
||||
get size() { return this.audioFile.metadata.size }
|
||||
get bestFilename() {
|
||||
if (this.episode) return `${this.episode} - ${this.title}`
|
||||
return this.title
|
||||
}
|
||||
get enclosureUrl() {
|
||||
return this.enclosure ? this.enclosure.url : null
|
||||
}
|
||||
|
||||
@@ -399,7 +399,7 @@ class Book {
|
||||
var newMissingParts = (this.missingParts || []).join(',') || ''
|
||||
var wasUpdated = newMissingParts !== currMissingParts
|
||||
if (wasUpdated && this.missingParts.length) {
|
||||
Logger.info(`[Audiobook] "${this.name}" has ${missingParts.length} missing parts`)
|
||||
Logger.info(`[Audiobook] "${this.metadata.title}" has ${missingParts.length} missing parts`)
|
||||
}
|
||||
|
||||
return wasUpdated
|
||||
@@ -408,54 +408,29 @@ class Book {
|
||||
setChapters(preferOverdriveMediaMarker = false) {
|
||||
// If 1 audio file without chapters, then no chapters will be set
|
||||
var includedAudioFiles = this.audioFiles.filter(af => !af.exclude)
|
||||
if (!includedAudioFiles.length) return
|
||||
|
||||
// If overdrive media markers are present and preferred, use those instead
|
||||
if (preferOverdriveMediaMarker) {
|
||||
var overdriveChapters = parseOverdriveMediaMarkersAsChapters(includedAudioFiles)
|
||||
if (overdriveChapters) {
|
||||
Logger.info('[Book] Overdrive Media Markers and preference found! Using these for chapter definitions')
|
||||
return this.chapters = overdriveChapters
|
||||
this.chapters = overdriveChapters
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (includedAudioFiles.length === 1) {
|
||||
// 1 audio file with chapters
|
||||
if (includedAudioFiles[0].chapters) {
|
||||
this.chapters = includedAudioFiles[0].chapters.map(c => ({ ...c }))
|
||||
}
|
||||
} else {
|
||||
// IF first audio file has embedded chapters then use embedded chapters
|
||||
if (includedAudioFiles[0].chapters && includedAudioFiles[0].chapters.length) {
|
||||
Logger.debug(`[Book] setChapters: Using embedded chapters in audio file ${includedAudioFiles[0].metadata.path}`)
|
||||
this.chapters = includedAudioFiles[0].chapters.map(c => ({ ...c }))
|
||||
} else if (includedAudioFiles.length > 1) {
|
||||
// Build chapters from audio files
|
||||
this.chapters = []
|
||||
var currChapterId = 0
|
||||
var currStartTime = 0
|
||||
includedAudioFiles.forEach((file) => {
|
||||
// If audio file has chapters use chapters
|
||||
if (file.chapters && file.chapters.length) {
|
||||
file.chapters.forEach((chapter) => {
|
||||
if (currStartTime > this.duration) {
|
||||
Logger.warn(`[Book] Invalid chapter start time > duration`)
|
||||
} else {
|
||||
var chapterAlreadyExists = this.chapters.find(ch => ch.start === currStartTime)
|
||||
if (!chapterAlreadyExists) {
|
||||
var chapterDuration = chapter.end - chapter.start
|
||||
if (chapterDuration > 0) {
|
||||
var title = `Chapter ${currChapterId}`
|
||||
if (chapter.title) {
|
||||
title += ` (${chapter.title})`
|
||||
}
|
||||
var endTime = Math.min(this.duration, currStartTime + chapterDuration)
|
||||
this.chapters.push({
|
||||
id: currChapterId++,
|
||||
start: currStartTime,
|
||||
end: endTime,
|
||||
title
|
||||
})
|
||||
currStartTime += chapterDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
} else if (file.duration) {
|
||||
// Otherwise just use track has chapter
|
||||
if (file.duration) {
|
||||
this.chapters.push({
|
||||
id: currChapterId++,
|
||||
start: currStartTime,
|
||||
|
||||
@@ -21,6 +21,7 @@ class Podcast {
|
||||
this.autoDownloadSchedule = null
|
||||
this.lastEpisodeCheck = 0
|
||||
this.maxEpisodesToKeep = 0
|
||||
this.maxNewEpisodesToDownload = 3
|
||||
|
||||
this.lastCoverSearch = null
|
||||
this.lastCoverSearchQuery = null
|
||||
@@ -44,6 +45,7 @@ class Podcast {
|
||||
this.autoDownloadSchedule = podcast.autoDownloadSchedule || '0 * * * *' // Added in 2.1.3 so default to hourly
|
||||
this.lastEpisodeCheck = podcast.lastEpisodeCheck || 0
|
||||
this.maxEpisodesToKeep = podcast.maxEpisodesToKeep || 0
|
||||
this.maxNewEpisodesToDownload = podcast.maxNewEpisodesToDownload || 3
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
@@ -56,7 +58,8 @@ class Podcast {
|
||||
autoDownloadEpisodes: this.autoDownloadEpisodes,
|
||||
autoDownloadSchedule: this.autoDownloadSchedule,
|
||||
lastEpisodeCheck: this.lastEpisodeCheck,
|
||||
maxEpisodesToKeep: this.maxEpisodesToKeep
|
||||
maxEpisodesToKeep: this.maxEpisodesToKeep,
|
||||
maxNewEpisodesToDownload: this.maxNewEpisodesToDownload
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +73,7 @@ class Podcast {
|
||||
autoDownloadSchedule: this.autoDownloadSchedule,
|
||||
lastEpisodeCheck: this.lastEpisodeCheck,
|
||||
maxEpisodesToKeep: this.maxEpisodesToKeep,
|
||||
maxNewEpisodesToDownload: this.maxNewEpisodesToDownload,
|
||||
size: this.size
|
||||
}
|
||||
}
|
||||
@@ -85,6 +89,7 @@ class Podcast {
|
||||
autoDownloadSchedule: this.autoDownloadSchedule,
|
||||
lastEpisodeCheck: this.lastEpisodeCheck,
|
||||
maxEpisodesToKeep: this.maxEpisodesToKeep,
|
||||
maxNewEpisodesToDownload: this.maxNewEpisodesToDownload,
|
||||
size: this.size
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const Logger = require('../../Logger')
|
||||
const { areEquivalent, copyValue, cleanStringForSearch, getTitleIgnorePrefix } = require('../../utils/index')
|
||||
const { areEquivalent, copyValue, cleanStringForSearch, getTitleIgnorePrefix, getTitlePrefixAtEnd } = require('../../utils/index')
|
||||
const parseNameString = require('../../utils/parsers/parseNameString')
|
||||
class BookMetadata {
|
||||
constructor(metadata) {
|
||||
@@ -62,7 +62,7 @@ class BookMetadata {
|
||||
toJSONMinified() {
|
||||
return {
|
||||
title: this.title,
|
||||
titleIgnorePrefix: this.titleIgnorePrefix,
|
||||
titleIgnorePrefix: this.titlePrefixAtEnd,
|
||||
subtitle: this.subtitle,
|
||||
authorName: this.authorName,
|
||||
authorNameLF: this.authorNameLF,
|
||||
@@ -83,7 +83,7 @@ class BookMetadata {
|
||||
toJSONExpanded() {
|
||||
return {
|
||||
title: this.title,
|
||||
titleIgnorePrefix: this.titleIgnorePrefix,
|
||||
titleIgnorePrefix: this.titlePrefixAtEnd,
|
||||
subtitle: this.subtitle,
|
||||
authors: this.authors.map(a => ({ ...a })), // Author JSONMinimal with name and id
|
||||
narrators: [...this.narrators],
|
||||
@@ -111,6 +111,9 @@ class BookMetadata {
|
||||
get titleIgnorePrefix() {
|
||||
return getTitleIgnorePrefix(this.title)
|
||||
}
|
||||
get titlePrefixAtEnd() {
|
||||
return getTitlePrefixAtEnd(this.title)
|
||||
}
|
||||
get authorName() {
|
||||
if (!this.authors.length) return ''
|
||||
return this.authors.map(au => au.name).join(', ')
|
||||
@@ -133,6 +136,13 @@ class BookMetadata {
|
||||
return `${getTitleIgnorePrefix(se.name)} #${se.sequence}`
|
||||
}).join(', ')
|
||||
}
|
||||
get seriesNamePrefixAtEnd() {
|
||||
if (!this.series.length) return ''
|
||||
return this.series.map(se => {
|
||||
if (!se.sequence) return getTitlePrefixAtEnd(se.name)
|
||||
return `${getTitlePrefixAtEnd(se.name)} #${se.sequence}`
|
||||
}).join(', ')
|
||||
}
|
||||
get firstSeriesName() {
|
||||
if (!this.series.length) return ''
|
||||
return this.series[0].name
|
||||
@@ -234,6 +244,7 @@ class BookMetadata {
|
||||
},
|
||||
{
|
||||
tag: 'tagDescription',
|
||||
altTag: 'tagComment',
|
||||
key: 'description'
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const Logger = require('../../Logger')
|
||||
const { areEquivalent, copyValue, cleanStringForSearch } = require('../../utils/index')
|
||||
const { areEquivalent, copyValue, cleanStringForSearch, getTitleIgnorePrefix, getTitlePrefixAtEnd } = require('../../utils/index')
|
||||
|
||||
class PodcastMetadata {
|
||||
constructor(metadata) {
|
||||
@@ -56,7 +56,7 @@ class PodcastMetadata {
|
||||
toJSONMinified() {
|
||||
return {
|
||||
title: this.title,
|
||||
titleIgnorePrefix: this.titleIgnorePrefix,
|
||||
titleIgnorePrefix: this.titlePrefixAtEnd,
|
||||
author: this.author,
|
||||
description: this.description,
|
||||
releaseDate: this.releaseDate,
|
||||
@@ -80,15 +80,11 @@ class PodcastMetadata {
|
||||
}
|
||||
|
||||
get titleIgnorePrefix() {
|
||||
if (!this.title) return ''
|
||||
var prefixesToIgnore = global.ServerSettings.sortingPrefixes || []
|
||||
for (const prefix of prefixesToIgnore) {
|
||||
// e.g. for prefix "the". If title is "The Book Title" return "Book Title, The"
|
||||
if (this.title.toLowerCase().startsWith(`${prefix} `)) {
|
||||
return this.title.substr(prefix.length + 1) + `, ${prefix.substr(0, 1).toUpperCase() + prefix.substr(1)}`
|
||||
}
|
||||
}
|
||||
return this.title
|
||||
return getTitleIgnorePrefix(this.title)
|
||||
}
|
||||
|
||||
get titlePrefixAtEnd() {
|
||||
return getTitlePrefixAtEnd(this.title)
|
||||
}
|
||||
|
||||
searchQuery(query) { // Returns key if match is found
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const Logger = require('../../Logger')
|
||||
const { areEquivalent, copyValue } = require('../../utils/index')
|
||||
const { areEquivalent, copyValue, getTitleIgnorePrefix, getTitlePrefixAtEnd } = require('../../utils/index')
|
||||
|
||||
class VideoMetadata {
|
||||
constructor(metadata) {
|
||||
@@ -32,7 +32,7 @@ class VideoMetadata {
|
||||
toJSONMinified() {
|
||||
return {
|
||||
title: this.title,
|
||||
titleIgnorePrefix: this.titleIgnorePrefix,
|
||||
titleIgnorePrefix: this.titlePrefixAtEnd,
|
||||
description: this.description,
|
||||
explicit: this.explicit,
|
||||
language: this.language
|
||||
@@ -48,15 +48,11 @@ class VideoMetadata {
|
||||
}
|
||||
|
||||
get titleIgnorePrefix() {
|
||||
if (!this.title) return ''
|
||||
var prefixesToIgnore = global.ServerSettings.sortingPrefixes || []
|
||||
for (const prefix of prefixesToIgnore) {
|
||||
// e.g. for prefix "the". If title is "The Book Title" return "Book Title, The"
|
||||
if (this.title.toLowerCase().startsWith(`${prefix} `)) {
|
||||
return this.title.substr(prefix.length + 1) + `, ${prefix.substr(0, 1).toUpperCase() + prefix.substr(1)}`
|
||||
}
|
||||
}
|
||||
return this.title
|
||||
return getTitleIgnorePrefix(this.title)
|
||||
}
|
||||
|
||||
get titlePrefixAtEnd() {
|
||||
return getTitlePrefixAtEnd(this.title)
|
||||
}
|
||||
|
||||
searchQuery(query) { // Returns key if match is found
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { BookCoverAspectRatio, BookshelfView } = require('../../utils/constants')
|
||||
const { BookshelfView } = require('../../utils/constants')
|
||||
const { isNullOrNaN } = require('../../utils')
|
||||
const Logger = require('../../Logger')
|
||||
|
||||
@@ -29,8 +29,7 @@ class ServerSettings {
|
||||
this.rateLimitLoginWindow = 10 * 60 * 1000 // 10 Minutes
|
||||
|
||||
// Backups
|
||||
// this.backupSchedule = '30 1 * * *' // If false then auto-backups are disabled (default every day at 1:30am)
|
||||
this.backupSchedule = false
|
||||
this.backupSchedule = false // If false then auto-backups are disabled
|
||||
this.backupsToKeep = 2
|
||||
this.maxBackupSize = 1
|
||||
this.backupMetadataCovers = true
|
||||
@@ -39,20 +38,16 @@ class ServerSettings {
|
||||
this.loggerDailyLogsToKeep = 7
|
||||
this.loggerScannerLogsToKeep = 2
|
||||
|
||||
// Cover
|
||||
// TODO: Remove after mobile apps are configured to use library server settings
|
||||
this.coverAspectRatio = BookCoverAspectRatio.SQUARE
|
||||
|
||||
// Bookshelf Display
|
||||
this.homeBookshelfView = BookshelfView.STANDARD
|
||||
this.bookshelfView = BookshelfView.STANDARD
|
||||
this.bookshelfView = BookshelfView.DETAIL
|
||||
|
||||
// Podcasts
|
||||
this.podcastEpisodeSchedule = '0 * * * *' // Every hour
|
||||
|
||||
// Sorting
|
||||
this.sortingIgnorePrefix = false
|
||||
this.sortingPrefixes = ['the', 'a']
|
||||
this.sortingPrefixes = ['the']
|
||||
|
||||
// Misc Flags
|
||||
this.chromecastEnabled = false
|
||||
@@ -99,12 +94,11 @@ class ServerSettings {
|
||||
this.loggerDailyLogsToKeep = settings.loggerDailyLogsToKeep || 7
|
||||
this.loggerScannerLogsToKeep = settings.loggerScannerLogsToKeep || 2
|
||||
|
||||
this.coverAspectRatio = !isNaN(settings.coverAspectRatio) ? settings.coverAspectRatio : BookCoverAspectRatio.SQUARE
|
||||
this.homeBookshelfView = settings.homeBookshelfView || BookshelfView.STANDARD
|
||||
this.bookshelfView = settings.bookshelfView || BookshelfView.STANDARD
|
||||
|
||||
this.sortingIgnorePrefix = !!settings.sortingIgnorePrefix
|
||||
this.sortingPrefixes = settings.sortingPrefixes || ['the', 'a']
|
||||
this.sortingPrefixes = settings.sortingPrefixes || ['the']
|
||||
this.chromecastEnabled = !!settings.chromecastEnabled
|
||||
this.enableEReader = !!settings.enableEReader
|
||||
this.dateFormat = settings.dateFormat || 'MM/dd/yyyy'
|
||||
@@ -152,7 +146,6 @@ class ServerSettings {
|
||||
backupMetadataCovers: this.backupMetadataCovers,
|
||||
loggerDailyLogsToKeep: this.loggerDailyLogsToKeep,
|
||||
loggerScannerLogsToKeep: this.loggerScannerLogsToKeep,
|
||||
coverAspectRatio: this.coverAspectRatio,
|
||||
homeBookshelfView: this.homeBookshelfView,
|
||||
bookshelfView: this.bookshelfView,
|
||||
sortingIgnorePrefix: this.sortingIgnorePrefix,
|
||||
|
||||
@@ -3,7 +3,20 @@ const htmlSanitizer = require('../utils/htmlSanitizer')
|
||||
const Logger = require('../Logger')
|
||||
|
||||
class Audible {
|
||||
constructor() { }
|
||||
constructor() {
|
||||
this.regionMap = {
|
||||
'us': '.com',
|
||||
'ca': '.ca',
|
||||
'uk': '.co.uk',
|
||||
'au': '.co.au',
|
||||
'fr': '.fr',
|
||||
'de': '.de',
|
||||
'jp': '.co.jp',
|
||||
'it': '.it',
|
||||
'in': '.co.in',
|
||||
'es': '.es'
|
||||
}
|
||||
}
|
||||
|
||||
cleanResult(item) {
|
||||
var { title, subtitle, asin, authors, narrators, publisherName, summary, releaseDate, image, genres, seriesPrimary, seriesSecondary, language, runtimeLengthMin } = item
|
||||
@@ -29,7 +42,9 @@ class Audible {
|
||||
tags: tagsFiltered.length ? tagsFiltered.join(', ') : null,
|
||||
series: series != [] ? series.map(({ name, position }) => ({ series: name, sequence: position })) : null,
|
||||
language: language ? language.charAt(0).toUpperCase() + language.slice(1) : null,
|
||||
duration: runtimeLengthMin && !isNaN(runtimeLengthMin) ? Number(runtimeLengthMin) : 0
|
||||
duration: runtimeLengthMin && !isNaN(runtimeLengthMin) ? Number(runtimeLengthMin) : 0,
|
||||
region: item.region || null,
|
||||
rating: item.rating || null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,9 +52,10 @@ class Audible {
|
||||
return /^[0-9A-Z]{10}$/.test(title)
|
||||
}
|
||||
|
||||
asinSearch(asin) {
|
||||
asinSearch(asin, region) {
|
||||
asin = encodeURIComponent(asin);
|
||||
var url = `https://api.audnex.us/books/${asin}`
|
||||
var regionQuery = region ? `?region=${region}` : ''
|
||||
var url = `https://api.audnex.us/books/${asin}${regionQuery}`
|
||||
Logger.debug(`[Audible] ASIN url: ${url}`)
|
||||
return axios.get(url).then((res) => {
|
||||
if (!res || !res.data || !res.data.asin) return null
|
||||
@@ -50,14 +66,19 @@ class Audible {
|
||||
})
|
||||
}
|
||||
|
||||
async search(title, author, asin) {
|
||||
async search(title, author, asin, region) {
|
||||
if (region && !this.regionMap[region]) {
|
||||
Logger.error(`[Audible] search: Invalid region ${region}`)
|
||||
region = ''
|
||||
}
|
||||
|
||||
var items
|
||||
if (asin) {
|
||||
items = [await this.asinSearch(asin)]
|
||||
items = [await this.asinSearch(asin, region)]
|
||||
}
|
||||
|
||||
if (!items && this.isProbablyAsin(title)) {
|
||||
items = [await this.asinSearch(title)]
|
||||
items = [await this.asinSearch(title, region)]
|
||||
}
|
||||
|
||||
if (!items) {
|
||||
@@ -65,14 +86,15 @@ class Audible {
|
||||
num_results: '10',
|
||||
products_sort_by: 'Relevance',
|
||||
title: title
|
||||
};
|
||||
}
|
||||
if (author) queryObj.author = author
|
||||
var queryString = (new URLSearchParams(queryObj)).toString();
|
||||
var url = `https://api.audible.com/1.0/catalog/products?${queryString}`
|
||||
const queryString = (new URLSearchParams(queryObj)).toString()
|
||||
const tld = region ? this.regionMap[region] : '.com'
|
||||
const url = `https://api.audible${tld}/1.0/catalog/products?${queryString}`
|
||||
Logger.debug(`[Audible] Search url: ${url}`)
|
||||
items = await axios.get(url).then((res) => {
|
||||
if (!res || !res.data || !res.data.products) return null
|
||||
return Promise.all(res.data.products.map(result => this.asinSearch(result.asin)))
|
||||
return Promise.all(res.data.products.map(result => this.asinSearch(result.asin, region)))
|
||||
}).catch(error => {
|
||||
Logger.error('[Audible] query search error', error)
|
||||
return []
|
||||
|
||||
@@ -35,7 +35,7 @@ class Audnexus {
|
||||
return {
|
||||
asin: author.asin,
|
||||
description: author.description,
|
||||
image: author.image,
|
||||
image: author.image || null,
|
||||
name: author.name
|
||||
}
|
||||
}
|
||||
@@ -54,17 +54,17 @@ class Audnexus {
|
||||
return {
|
||||
asin: author.asin,
|
||||
description: author.description,
|
||||
image: author.image,
|
||||
image: author.image || null,
|
||||
name: author.name
|
||||
}
|
||||
}
|
||||
|
||||
async getChaptersByASIN(asin) {
|
||||
Logger.debug(`[Audnexus] Get chapters for ASIN ${asin}`)
|
||||
return axios.get(`${this.baseUrl}/books/${asin}/chapters`).then((res) => {
|
||||
getChaptersByASIN(asin, region) {
|
||||
Logger.debug(`[Audnexus] Get chapters for ASIN ${asin}/${region}`)
|
||||
return axios.get(`${this.baseUrl}/books/${asin}/chapters?region=${region}`).then((res) => {
|
||||
return res.data
|
||||
}).catch((error) => {
|
||||
Logger.error(`[Audnexus] Chapter ASIN request failed for ${asin}`, error)
|
||||
Logger.error(`[Audnexus] Chapter ASIN request failed for ${asin}/${region}`, error)
|
||||
return null
|
||||
})
|
||||
}
|
||||
|
||||
@@ -60,11 +60,15 @@ class iTunes {
|
||||
}
|
||||
|
||||
cleanAudiobook(data) {
|
||||
// artistName can be "Name1, Name2 & Name3" so we refactor this to "Name1, Name2, Name3"
|
||||
// see: https://github.com/advplyr/audiobookshelf/issues/1022
|
||||
const author = (data.artistName || '').split(' & ').join(', ')
|
||||
|
||||
return {
|
||||
id: data.collectionId,
|
||||
artistId: data.artistId,
|
||||
title: data.collectionName,
|
||||
author: data.artistName,
|
||||
author,
|
||||
description: htmlSanitizer.stripAllTags(data.description || ''),
|
||||
publishedYear: data.releaseDate ? data.releaseDate.split('-')[0] : null,
|
||||
genres: data.primaryGenreName ? [data.primaryGenreName] : null,
|
||||
|
||||
@@ -819,7 +819,7 @@ class Scanner {
|
||||
if ((!libraryItem.media.tags.length || options.overrideDetails)) {
|
||||
var tagsArray = []
|
||||
if (Array.isArray(matchData[key])) tagsArray = [...matchData[key]]
|
||||
else tagsArray = tagsArray[key].split(',').map(v => v.trim()).filter(v => !!v)
|
||||
else tagsArray = matchData[key].split(',').map(v => v.trim()).filter(v => !!v)
|
||||
updatePayload[key] = tagsArray
|
||||
}
|
||||
} else if ((!libraryItem.media.metadata[key] || options.overrideDetails)) {
|
||||
|
||||
@@ -13,7 +13,7 @@ module.exports.BookCoverAspectRatio = {
|
||||
|
||||
module.exports.BookshelfView = {
|
||||
STANDARD: 0,
|
||||
TITLES: 1
|
||||
DETAIL: 1
|
||||
}
|
||||
|
||||
module.exports.LogLevel = {
|
||||
@@ -44,7 +44,9 @@ module.exports.AudioMimeType = {
|
||||
AAC: 'audio/aac',
|
||||
FLAC: 'audio/flac',
|
||||
WMA: 'audio/x-ms-wma',
|
||||
AIFF: 'audio/x-aiff'
|
||||
AIFF: 'audio/x-aiff',
|
||||
WEBM: 'audio/webm',
|
||||
WEBMA: 'audio/webm'
|
||||
}
|
||||
|
||||
module.exports.VideoMimeType = {
|
||||
|
||||
@@ -168,7 +168,8 @@ module.exports.downloadFile = async (url, filepath) => {
|
||||
const response = await axios({
|
||||
url,
|
||||
method: 'GET',
|
||||
responseType: 'stream'
|
||||
responseType: 'stream',
|
||||
timeout: 30000
|
||||
})
|
||||
response.data.pipe(writer)
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const globals = {
|
||||
SupportedImageTypes: ['png', 'jpg', 'jpeg', 'webp'],
|
||||
SupportedAudioTypes: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'ogg', 'oga', 'mp4', 'aac', 'wma', 'aiff', 'wav'],
|
||||
SupportedAudioTypes: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'ogg', 'oga', 'mp4', 'aac', 'wma', 'aiff', 'wav', 'webm', 'webma'],
|
||||
SupportedEbookTypes: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'],
|
||||
SupportedVideoTypes: ['mp4'],
|
||||
TextFileTypes: ['txt', 'nfo'],
|
||||
|
||||
@@ -140,14 +140,24 @@ module.exports.cleanStringForSearch = (str) => {
|
||||
return str.toLowerCase().replace(/[\'\.\`\",]/g, '').trim()
|
||||
}
|
||||
|
||||
module.exports.getTitleIgnorePrefix = (title) => {
|
||||
if (!title) return ''
|
||||
const getTitleParts = (title) => {
|
||||
if (!title) return ['', null]
|
||||
var prefixesToIgnore = global.ServerSettings.sortingPrefixes || []
|
||||
prefixes = []
|
||||
for (const prefix of prefixesToIgnore) {
|
||||
// e.g. for prefix "the". If title is "The Book" return "Book, The"
|
||||
if (title.toLowerCase().startsWith(`${prefix} `)) {
|
||||
return title.substr(prefix.length + 1) + `, ${prefix.substr(0, 1).toUpperCase() + prefix.substr(1)}`
|
||||
return [title.substr(prefix.length + 1), `${prefix.substr(0, 1).toUpperCase() + prefix.substr(1)}`]
|
||||
}
|
||||
}
|
||||
return title
|
||||
return [title, null]
|
||||
}
|
||||
|
||||
module.exports.getTitleIgnorePrefix = (title) => {
|
||||
return getTitleParts(title)[0]
|
||||
}
|
||||
|
||||
module.exports.getTitlePrefixAtEnd = (title) => {
|
||||
let [sort, prefix] = getTitleParts(title)
|
||||
return prefix ? `${sort}, ${prefix}` : title
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
const { sort, createNewSortInstance } = require('../libs/fastSort')
|
||||
const { getTitleIgnorePrefix } = require('../utils/index')
|
||||
const { getTitlePrefixAtEnd, isNullOrNaN, getTitleIgnorePrefix } = require('../utils/index')
|
||||
const naturalSort = createNewSortInstance({
|
||||
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
|
||||
})
|
||||
@@ -67,6 +67,45 @@ module.exports = {
|
||||
return filtered
|
||||
},
|
||||
|
||||
// Returns false if should be filtered out
|
||||
checkFilterForSeriesLibraryItem(libraryItem, filterBy) {
|
||||
var searchGroups = ['genres', 'tags', 'authors', 'progress', 'narrators', 'languages']
|
||||
var group = searchGroups.find(_group => filterBy.startsWith(_group + '.'))
|
||||
if (group) {
|
||||
var filterVal = filterBy.replace(`${group}.`, '')
|
||||
var filter = this.decode(filterVal)
|
||||
|
||||
if (group === 'genres') return libraryItem.media.metadata && libraryItem.media.metadata.genres.includes(filter)
|
||||
else if (group === 'tags') return libraryItem.media.tags.includes(filter)
|
||||
else if (group === 'authors') return libraryItem.mediaType === 'book' && libraryItem.media.metadata.hasAuthor(filter)
|
||||
else if (group === 'narrators') return libraryItem.mediaType === 'book' && libraryItem.media.metadata.hasNarrator(filter)
|
||||
else if (group === 'languages') {
|
||||
return libraryItem.media.metadata && libraryItem.media.metadata.language === filter
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
|
||||
// Return false to filter out series
|
||||
checkSeriesProgressFilter(series, filterBy, user) {
|
||||
const filter = this.decode(filterBy.split('.')[1])
|
||||
|
||||
var numBooksStartedOrFinished = 0
|
||||
for (const libraryItem of series.books) {
|
||||
const itemProgress = user.getMediaProgress(libraryItem.id)
|
||||
if (filter === 'Finished' && (!itemProgress || !itemProgress.isFinished)) return false
|
||||
if (filter === 'Not Started' && itemProgress) return false
|
||||
if (itemProgress) numBooksStartedOrFinished++
|
||||
}
|
||||
|
||||
if (numBooksStartedOrFinished === series.books.length) { // Completely finished series
|
||||
if (filter === 'Not Finished') return false
|
||||
} else if (numBooksStartedOrFinished === 0 && filter === 'In Progress') { // Series not started
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
|
||||
getDistinctFilterDataNew(libraryItems) {
|
||||
var data = {
|
||||
authors: [],
|
||||
@@ -114,27 +153,59 @@ module.exports = {
|
||||
return data
|
||||
},
|
||||
|
||||
getSeriesFromBooks(books, minified = false) {
|
||||
var _series = {}
|
||||
getSeriesFromBooks(books, allSeries, filterBy, user, minified = false) {
|
||||
const _series = {}
|
||||
const seriesToFilterOut = {}
|
||||
books.forEach((libraryItem) => {
|
||||
var bookSeries = libraryItem.media.metadata.series || []
|
||||
bookSeries.forEach((series) => {
|
||||
var abJson = minified ? libraryItem.toJSONMinified() : libraryItem.toJSONExpanded()
|
||||
abJson.sequence = series.sequence
|
||||
if (!_series[series.id]) {
|
||||
_series[series.id] = {
|
||||
id: series.id,
|
||||
name: series.name,
|
||||
nameIgnorePrefix: getTitleIgnorePrefix(series.name),
|
||||
// get all book series for item that is not already filtered out
|
||||
const bookSeries = (libraryItem.media.metadata.series || []).filter(se => !seriesToFilterOut[se.id])
|
||||
if (!bookSeries.length) return
|
||||
|
||||
if (filterBy && user && !filterBy.startsWith('progress.')) { // Series progress filters are evaluated after grouping
|
||||
// If a single book in a series is filtered out then filter out the entire series
|
||||
if (!this.checkFilterForSeriesLibraryItem(libraryItem, filterBy)) {
|
||||
// filter out this library item
|
||||
bookSeries.forEach((bookSeriesObj) => {
|
||||
// flag series to filter it out
|
||||
seriesToFilterOut[bookSeriesObj.id] = true
|
||||
delete _series[bookSeriesObj.id]
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
bookSeries.forEach((bookSeriesObj) => {
|
||||
const series = allSeries.find(se => se.id === bookSeriesObj.id)
|
||||
|
||||
const abJson = minified ? libraryItem.toJSONMinified() : libraryItem.toJSONExpanded()
|
||||
abJson.sequence = bookSeriesObj.sequence
|
||||
if (!_series[bookSeriesObj.id]) {
|
||||
_series[bookSeriesObj.id] = {
|
||||
id: bookSeriesObj.id,
|
||||
name: bookSeriesObj.name,
|
||||
nameIgnorePrefix: getTitlePrefixAtEnd(bookSeriesObj.name),
|
||||
nameIgnorePrefixSort: getTitleIgnorePrefix(bookSeriesObj.name),
|
||||
type: 'series',
|
||||
books: [abJson]
|
||||
books: [abJson],
|
||||
addedAt: series ? series.addedAt : 0,
|
||||
totalDuration: isNullOrNaN(abJson.media.duration) ? 0 : Number(abJson.media.duration)
|
||||
}
|
||||
|
||||
} else {
|
||||
_series[series.id].books.push(abJson)
|
||||
_series[bookSeriesObj.id].books.push(abJson)
|
||||
_series[bookSeriesObj.id].totalDuration += isNullOrNaN(abJson.media.duration) ? 0 : Number(abJson.media.duration)
|
||||
}
|
||||
})
|
||||
})
|
||||
return Object.values(_series).map((series) => {
|
||||
|
||||
var seriesItems = Object.values(_series)
|
||||
|
||||
// check progress filter
|
||||
if (filterBy && filterBy.startsWith('progress.') && user) {
|
||||
seriesItems = seriesItems.filter(se => this.checkSeriesProgressFilter(se, filterBy, user))
|
||||
}
|
||||
|
||||
return seriesItems.map((series) => {
|
||||
series.books = naturalSort(series.books).asc(li => li.sequence)
|
||||
return series
|
||||
})
|
||||
@@ -209,8 +280,8 @@ module.exports = {
|
||||
return totalSize
|
||||
},
|
||||
|
||||
collapseBookSeries(libraryItems) {
|
||||
var seriesObjects = this.getSeriesFromBooks(libraryItems, true)
|
||||
collapseBookSeries(libraryItems, series) {
|
||||
var seriesObjects = this.getSeriesFromBooks(libraryItems, series, null, null, true)
|
||||
var seriesToUse = {}
|
||||
var libraryItemIdsToHide = []
|
||||
seriesObjects.forEach((series) => {
|
||||
@@ -232,6 +303,7 @@ module.exports = {
|
||||
id: seriesToUse[li.id].id,
|
||||
name: seriesToUse[li.id].name,
|
||||
nameIgnorePrefix: seriesToUse[li.id].nameIgnorePrefix,
|
||||
libraryItemIds: seriesToUse[li.id].books.map(b => b.id),
|
||||
numBooks: seriesToUse[li.id].books.length
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,20 @@ function extractEpisodeData(item) {
|
||||
episode.descriptionPlain = htmlSanitizer.stripAllTags(rawDescription)
|
||||
}
|
||||
|
||||
var arrayFields = ['title', 'pubDate', 'itunes:episodeType', 'itunes:season', 'itunes:episode', 'itunes:author', 'itunes:duration', 'itunes:explicit', 'itunes:subtitle']
|
||||
if (item['pubDate']) {
|
||||
if (typeof item['pubDate'] === 'string') {
|
||||
episode.pubDate = item['pubDate']
|
||||
} else if (Array.isArray(item['pubDate'])) {
|
||||
const pubDateObj = extractFirstArrayItem(item, 'pubDate')
|
||||
if (pubDateObj && typeof pubDateObj._ === 'string') {
|
||||
episode.pubDate = pubDateObj._
|
||||
}
|
||||
} else {
|
||||
Logger.error(`[podcastUtils] Invalid pubDate ${item['pubDate']} for ${episode.enclosure.url}`)
|
||||
}
|
||||
}
|
||||
|
||||
var arrayFields = ['title', 'itunes:episodeType', 'itunes:season', 'itunes:episode', 'itunes:author', 'itunes:duration', 'itunes:explicit', 'itunes:subtitle']
|
||||
arrayFields.forEach((key) => {
|
||||
var cleanKey = key.split(':').pop()
|
||||
episode[cleanKey] = extractFirstArrayItem(item, key)
|
||||
@@ -104,6 +117,8 @@ function extractEpisodeData(item) {
|
||||
}
|
||||
|
||||
function cleanEpisodeData(data) {
|
||||
const pubJsDate = data.pubDate ? new Date(data.pubDate) : null
|
||||
const publishedAt = pubJsDate && !isNaN(pubJsDate) ? pubJsDate.valueOf() : null
|
||||
return {
|
||||
title: data.title,
|
||||
subtitle: data.subtitle || '',
|
||||
@@ -116,7 +131,7 @@ function cleanEpisodeData(data) {
|
||||
author: data.author || '',
|
||||
duration: data.duration || '',
|
||||
explicit: data.explicit || '',
|
||||
publishedAt: (new Date(data.pubDate)).valueOf(),
|
||||
publishedAt,
|
||||
enclosure: data.enclosure
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,6 +254,7 @@ function getBookDataFromDir(folderPath, relPath, parseSubtitle = false) {
|
||||
path: Path.posix.join(folderPath, relPath) // i.e. /audiobook/Author Name/Book Name/..
|
||||
}
|
||||
}
|
||||
module.exports.getBookDataFromDir = getBookDataFromDir
|
||||
|
||||
function getNarrator(folder) {
|
||||
let pattern = /^(?<title>.*) \{(?<narrators>.*)\}$/
|
||||
@@ -277,16 +278,15 @@ function getSequence(folder) {
|
||||
// ]
|
||||
|
||||
// Matches a valid volume string. Also matches a book whose title starts with a 1 to 3 digit number. Will handle that later.
|
||||
let pattern = /^(?<volumeLabel>vol\.? |volume |book )?(?<sequence>\d{1,3}(?:\.\d{1,2})?)(?<trailingDot>\.?)(?: (?<suffix>.*))?$/i
|
||||
let pattern = /^(?<volumeLabel>vol\.? |volume |book )?(?<sequence>\d{0,3}(?:\.\d{1,2})?)(?<trailingDot>\.?)(?: (?<suffix>.*))?$/i
|
||||
|
||||
let volumeNumber = null
|
||||
let parts = folder.split(' - ')
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
let match = parts[i].match(pattern)
|
||||
|
||||
// This excludes '101 Dalmations' but includes '101. Dalmations'
|
||||
if (match && !(match.groups.suffix && !(match.groups.volumeLabel || match.groups.trailingDot))) {
|
||||
volumeNumber = match.groups.sequence
|
||||
volumeNumber = isNaN(match.groups.sequence) ? match.groups.sequence : Number(match.groups.sequence).toString()
|
||||
parts[i] = match.groups.suffix
|
||||
if (!parts[i]) { parts.splice(i, 1) }
|
||||
break
|
||||
@@ -338,7 +338,7 @@ function getDataFromMediaDir(libraryMediaType, folderPath, relPath, serverSettin
|
||||
var parseSubtitle = !!serverSettings.scannerParseSubtitle
|
||||
return getBookDataFromDir(folderPath, relPath, parseSubtitle)
|
||||
} else {
|
||||
return this.getPodcastDataFromDir(folderPath, relPath)
|
||||
return getPodcastDataFromDir(folderPath, relPath)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,6 +72,70 @@ module.exports.getToneMetadataObject = (libraryItem, chaptersFile) => {
|
||||
return metadataObject
|
||||
}
|
||||
|
||||
module.exports.writeToneMetadataJsonFile = (libraryItem, chapters, filePath, trackTotal) => {
|
||||
const bookMetadata = libraryItem.media.metadata
|
||||
const coverPath = libraryItem.media.coverPath
|
||||
|
||||
const metadataObject = {
|
||||
'album': bookMetadata.title || '',
|
||||
'title': bookMetadata.title || '',
|
||||
'trackTotal': trackTotal,
|
||||
'additionalFields': {}
|
||||
}
|
||||
if (bookMetadata.subtitle) {
|
||||
metadataObject['subtitle'] = bookMetadata.subtitle
|
||||
}
|
||||
if (bookMetadata.authorName) {
|
||||
metadataObject['artist'] = bookMetadata.authorName
|
||||
metadataObject['albumArtist'] = bookMetadata.authorName
|
||||
}
|
||||
if (bookMetadata.description) {
|
||||
metadataObject['comment'] = bookMetadata.description
|
||||
metadataObject['description'] = bookMetadata.description
|
||||
}
|
||||
if (bookMetadata.narratorName) {
|
||||
metadataObject['narrator'] = bookMetadata.narratorName
|
||||
metadataObject['composer'] = bookMetadata.narratorName
|
||||
}
|
||||
if (bookMetadata.firstSeriesName) {
|
||||
metadataObject['movementName'] = bookMetadata.firstSeriesName
|
||||
}
|
||||
if (bookMetadata.firstSeriesSequence) {
|
||||
metadataObject['movement'] = bookMetadata.firstSeriesSequence
|
||||
}
|
||||
if (bookMetadata.genres.length) {
|
||||
metadataObject['genre'] = bookMetadata.genres.join('/')
|
||||
}
|
||||
if (bookMetadata.publisher) {
|
||||
metadataObject['publisher'] = bookMetadata.publisher
|
||||
}
|
||||
if (bookMetadata.asin) {
|
||||
metadataObject.additionalFields['asin'] = bookMetadata.asin
|
||||
}
|
||||
if (bookMetadata.isbn) {
|
||||
metadataObject.additionalFields['isbn'] = bookMetadata.isbn
|
||||
}
|
||||
if (coverPath) {
|
||||
metadataObject['coverFile'] = coverPath
|
||||
}
|
||||
if (parsePublishedYear(bookMetadata.publishedYear)) {
|
||||
metadataObject['publishingDate'] = parsePublishedYear(bookMetadata.publishedYear)
|
||||
}
|
||||
if (chapters && chapters.length > 0) {
|
||||
let metadataChapters = []
|
||||
for (const chapter of chapters) {
|
||||
metadataChapters.push({
|
||||
start: Math.round(chapter.start * 1000),
|
||||
length: Math.round((chapter.end - chapter.start) * 1000),
|
||||
title: chapter.title,
|
||||
})
|
||||
}
|
||||
metadataObject['chapters'] = metadataChapters
|
||||
}
|
||||
|
||||
return fs.writeFile(filePath, JSON.stringify({ meta: metadataObject }, null, 2))
|
||||
}
|
||||
|
||||
module.exports.tagAudioFile = (filePath, payload) => {
|
||||
return tone.tag(filePath, payload).then((data) => {
|
||||
return true
|
||||
|
||||
Reference in New Issue
Block a user