mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-05 02:02:44 +02:00
Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 67f51c6de9 | |||
| 0c8fd6ab0e | |||
| 5452a57a14 | |||
| 19f020e7a6 | |||
| 825641f2a9 | |||
| 35ab4cb2fe | |||
| fd13607d89 | |||
| f79b4d44b9 | |||
| 91e30a6e84 | |||
| 8ab0f164a6 | |||
| 578bb03404 | |||
| 06582b5371 | |||
| 7f6baf35b7 | |||
| 6227d0baa1 | |||
| e334b585be | |||
| c83b3f19f7 | |||
| d31ec055f9 | |||
| 38c259a45e | |||
| b2ee24de98 | |||
| 9ba0e52bb7 | |||
| edc712e6f6 | |||
| 485888b2d9 | |||
| c2e90d4d83 | |||
| f5d89b8f52 | |||
| 378b40790a | |||
| be3d38392d | |||
| 27fef50983 | |||
| 167df85c1e | |||
| 009e16c9a4 | |||
| b40cc767b2 | |||
| 4f5f2d32be | |||
| 66be3e0281 | |||
| 987f188f00 | |||
| daca2bdf2a | |||
| 8894f52439 | |||
| 863f81e55a | |||
| d03d3735e5 | |||
| 3bb2df6e12 | |||
| 80c9efc618 | |||
| 3279901ab0 | |||
| d43d351721 | |||
| 8210eba439 | |||
| cbd7294b0b | |||
| 6064e8af87 | |||
| 8754f0c25f | |||
| f31700f668 | |||
| 9877b139f6 | |||
| 5643c846ee | |||
| 5a071babe9 | |||
| 3d85d0bce6 | |||
| 68afc2c718 | |||
| b3d9323f66 | |||
| effc63755b | |||
| b90934a72a | |||
| e01748eb2f | |||
| 430fbf5e46 | |||
| 27e6b9ce0d | |||
| fc614b9833 |
+2
-1
@@ -11,6 +11,7 @@ test/
|
|||||||
/client/.nuxt/
|
/client/.nuxt/
|
||||||
/client/dist/
|
/client/dist/
|
||||||
/dist/
|
/dist/
|
||||||
|
library/
|
||||||
|
|
||||||
sw.*
|
sw.*
|
||||||
.DS_STORE
|
.DS_STORE
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div id="appbar" class="absolute top-0 bottom-0 left-0 w-full h-full px-2 md:px-6 py-1 z-50">
|
<div id="appbar" class="absolute top-0 bottom-0 left-0 w-full h-full px-2 md:px-6 py-1 z-50">
|
||||||
<div class="flex h-full items-center">
|
<div class="flex h-full items-center">
|
||||||
<nuxt-link to="/">
|
<nuxt-link to="/">
|
||||||
<img src="/icon48.png" class="w-10 h-10 md:w-12 md:h-12 mr-4" />
|
<img src="/icon48.png" class="w-8 h-8 mr-8 sm:w-12 sm:h-12 sm:mr-4" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link to="/">
|
<nuxt-link to="/">
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
<ui-libraries-dropdown />
|
<ui-libraries-dropdown />
|
||||||
|
|
||||||
<controls-global-search v-if="currentLibrary" class="hidden md:block" />
|
<controls-global-search v-if="currentLibrary" class="" />
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|
||||||
<span v-if="showExperimentalFeatures" class="material-icons text-4xl text-warning pr-0 sm:pr-2 md:pr-4">logo_dev</span>
|
<span v-if="showExperimentalFeatures" class="material-icons text-4xl text-warning pr-0 sm:pr-2 md:pr-4">logo_dev</span>
|
||||||
|
|||||||
@@ -144,6 +144,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
dateFormat() {
|
||||||
|
return this.store.state.serverSettings.dateFormat
|
||||||
|
},
|
||||||
showExperimentalFeatures() {
|
showExperimentalFeatures() {
|
||||||
return this.store.state.showExperimentalFeatures
|
return this.store.state.showExperimentalFeatures
|
||||||
},
|
},
|
||||||
@@ -259,9 +262,9 @@ export default {
|
|||||||
return this.author
|
return this.author
|
||||||
},
|
},
|
||||||
displaySortLine() {
|
displaySortLine() {
|
||||||
if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs)
|
if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs, this.dateFormat)
|
||||||
if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs)
|
if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs, this.dateFormat)
|
||||||
if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._libraryItem.addedAt)
|
if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._libraryItem.addedAt, this.dateFormat)
|
||||||
if (this.orderBy === 'media.duration') return 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false)
|
if (this.orderBy === 'media.duration') return 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false)
|
||||||
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
|
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
|
||||||
if (this.orderBy === 'media.numTracks') return `${this.numEpisodes} Episodes`
|
if (this.orderBy === 'media.numTracks') return `${this.numEpisodes} Episodes`
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-80 ml-6 relative">
|
<div class="sm:w-80 w-full sm:ml-6 relative">
|
||||||
<form @submit.prevent="submitSearch">
|
<form @submit.prevent="submitSearch">
|
||||||
<ui-text-input ref="input" v-model="search" placeholder="Search.." @input="inputUpdate" @focus="focussed" @blur="blurred" class="w-full h-8 text-sm" />
|
<ui-text-input ref="input" v-model="search" placeholder="Search.." @input="inputUpdate" @focus="focussed" @blur="blurred" class="w-full h-8 text-sm" />
|
||||||
</form>
|
</form>
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
<span v-if="!search" class="material-icons" style="font-size: 1.2rem">search</span>
|
<span v-if="!search" class="material-icons" style="font-size: 1.2rem">search</span>
|
||||||
<span v-else class="material-icons" style="font-size: 1.2rem">close</span>
|
<span v-else class="material-icons" style="font-size: 1.2rem">close</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="showMenu && (lastSearch || isTyping)" class="absolute z-40 -mt-px w-full bg-bg border border-black-200 shadow-lg rounded-md py-1 px-2 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm globalSearchMenu">
|
<div v-show="showMenu && (lastSearch || isTyping)" class="absolute z-40 -mt-px w-40 sm:w-full bg-bg border border-black-200 shadow-lg rounded-md py-1 px-2 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm globalSearchMenu">
|
||||||
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||||
<li v-if="isTyping" class="py-2 px-2">
|
<li v-if="isTyping" class="py-2 px-2">
|
||||||
<p>Thinking...</p>
|
<p>Thinking...</p>
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
<template v-else>
|
<template v-else>
|
||||||
<p v-if="bookResults.length" class="uppercase text-xs text-gray-400 my-1 px-1 font-semibold">Books</p>
|
<p v-if="bookResults.length" class="uppercase text-xs text-gray-400 my-1 px-1 font-semibold">Books</p>
|
||||||
<template v-for="item in bookResults">
|
<template v-for="item in bookResults">
|
||||||
<li :key="item.libraryItem.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option">
|
<li :key="item.libraryItem.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
||||||
<nuxt-link :to="`/item/${item.libraryItem.id}`">
|
<nuxt-link :to="`/item/${item.libraryItem.id}`">
|
||||||
<cards-item-search-card :library-item="item.libraryItem" :match-key="item.matchKey" :match-text="item.matchText" :search="lastSearch" />
|
<cards-item-search-card :library-item="item.libraryItem" :match-key="item.matchKey" :match-text="item.matchText" :search="lastSearch" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
<p v-if="podcastResults.length" class="uppercase text-xs text-gray-400 my-1 px-1 font-semibold">Podcasts</p>
|
<p v-if="podcastResults.length" class="uppercase text-xs text-gray-400 my-1 px-1 font-semibold">Podcasts</p>
|
||||||
<template v-for="item in podcastResults">
|
<template v-for="item in podcastResults">
|
||||||
<li :key="item.libraryItem.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option">
|
<li :key="item.libraryItem.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
||||||
<nuxt-link :to="`/item/${item.libraryItem.id}`">
|
<nuxt-link :to="`/item/${item.libraryItem.id}`">
|
||||||
<cards-item-search-card :library-item="item.libraryItem" :match-key="item.matchKey" :match-text="item.matchText" :search="lastSearch" />
|
<cards-item-search-card :library-item="item.libraryItem" :match-key="item.matchKey" :match-text="item.matchText" :search="lastSearch" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
|
|
||||||
<p v-if="authorResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">Authors</p>
|
<p v-if="authorResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">Authors</p>
|
||||||
<template v-for="item in authorResults">
|
<template v-for="item in authorResults">
|
||||||
<li :key="item.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option">
|
<li :key="item.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
||||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(item.id)}`">
|
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(item.id)}`">
|
||||||
<cards-author-search-card :author="item" />
|
<cards-author-search-card :author="item" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
|
|
||||||
<p v-if="seriesResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">Series</p>
|
<p v-if="seriesResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">Series</p>
|
||||||
<template v-for="item in seriesResults">
|
<template v-for="item in seriesResults">
|
||||||
<li :key="item.series.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option">
|
<li :key="item.series.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
||||||
<nuxt-link :to="`/library/${currentLibraryId}/series/${item.series.id}`">
|
<nuxt-link :to="`/library/${currentLibraryId}/series/${item.series.id}`">
|
||||||
<cards-series-search-card :series="item.series" :book-items="item.books" />
|
<cards-series-search-card :series="item.series" :book-items="item.books" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
|
|
||||||
<p v-if="tagResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">Tags</p>
|
<p v-if="tagResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">Tags</p>
|
||||||
<template v-for="item in tagResults">
|
<template v-for="item in tagResults">
|
||||||
<li :key="item.name" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option">
|
<li :key="item.name" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
||||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=tags.${$encode(item.name)}`">
|
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=tags.${$encode(item.name)}`">
|
||||||
<cards-tag-search-card :tag="item.name" />
|
<cards-tag-search-card :tag="item.name" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
@@ -97,6 +97,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
clickOption() {
|
||||||
|
this.clearResults()
|
||||||
|
},
|
||||||
submitSearch() {
|
submitSearch() {
|
||||||
if (!this.search) return
|
if (!this.search) return
|
||||||
var search = this.search
|
var search = this.search
|
||||||
|
|||||||
@@ -25,6 +25,9 @@
|
|||||||
<ui-text-input-with-label v-model="authorCopy.asin" :disabled="processing" label="ASIN" />
|
<ui-text-input-with-label v-model="authorCopy.asin" :disabled="processing" label="ASIN" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="p-2">
|
||||||
|
<ui-text-input-with-label v-model="authorCopy.imagePath" :disabled="processing" label="Photo Path/URL" />
|
||||||
|
</div>
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
<ui-textarea-with-label v-model="authorCopy.description" :disabled="processing" label="Description" :rows="8" />
|
<ui-textarea-with-label v-model="authorCopy.description" :disabled="processing" label="Description" :rows="8" />
|
||||||
</div>
|
</div>
|
||||||
@@ -43,19 +46,13 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
// props: {
|
|
||||||
// value: Boolean,
|
|
||||||
// author: {
|
|
||||||
// type: Object,
|
|
||||||
// default: () => {}
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
authorCopy: {
|
authorCopy: {
|
||||||
name: '',
|
name: '',
|
||||||
asin: '',
|
asin: '',
|
||||||
description: ''
|
description: '',
|
||||||
|
imagePath: ''
|
||||||
},
|
},
|
||||||
processing: false
|
processing: false
|
||||||
}
|
}
|
||||||
@@ -95,9 +92,10 @@ export default {
|
|||||||
this.authorCopy.name = this.author.name
|
this.authorCopy.name = this.author.name
|
||||||
this.authorCopy.asin = this.author.asin
|
this.authorCopy.asin = this.author.asin
|
||||||
this.authorCopy.description = this.author.description
|
this.authorCopy.description = this.author.description
|
||||||
|
this.authorCopy.imagePath = this.author.imagePath
|
||||||
},
|
},
|
||||||
async submitForm() {
|
async submitForm() {
|
||||||
var keysToCheck = ['name', 'asin', 'description']
|
var keysToCheck = ['name', 'asin', 'description', 'imagePath']
|
||||||
var updatePayload = {}
|
var updatePayload = {}
|
||||||
keysToCheck.forEach((key) => {
|
keysToCheck.forEach((key) => {
|
||||||
if (this.authorCopy[key] !== this.author[key]) {
|
if (this.authorCopy[key] !== this.author[key]) {
|
||||||
@@ -167,4 +165,4 @@ export default {
|
|||||||
mounted() {},
|
mounted() {},
|
||||||
beforeDestroy() {}
|
beforeDestroy() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
|
|
||||||
<p v-if="showM4bDownload" class="text-left text-base mb-4 py-4">
|
<p v-if="showM4bDownload" class="text-left text-base mb-4 py-4">
|
||||||
<span class="text-error">* <strong>Experimental</strong></span
|
<span class="text-error">* <strong>Experimental</strong></span
|
||||||
> - M4b merge can take several minutes and will be stored in <span class="bg-primary bg-opacity-75 font-mono p-1 text-base">/metadata/downloads</span>. After the download is ready, it will remain available for 60 minutes, then be deleted. Download will timeout after 20 minutes.
|
> - M4b merge can take several minutes and will be stored in <span class="bg-primary bg-opacity-75 font-mono p-1 text-base">/metadata/downloads</span>. After the download is ready, it will remain available for 60 minutes, then be deleted. Download will timeout after 30 minutes.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- <p v-if="isSingleM4b" class="text-lg text-center my-8">Audiobook is already a single m4b!</p> -->
|
<!-- <p v-if="isSingleM4b" class="text-lg text-center my-8">Audiobook is already a single m4b!</p> -->
|
||||||
|
|||||||
@@ -428,7 +428,7 @@ export default {
|
|||||||
)
|
)
|
||||||
updatePayload.metadata.authors = authorPayload
|
updatePayload.metadata.authors = authorPayload
|
||||||
} else if (key === 'narrator') {
|
} else if (key === 'narrator') {
|
||||||
updatePayload.metadata.narrators = [this.selectedMatch[key]]
|
updatePayload.metadata.narrators = this.selectedMatch[key].split(',').map((v) => v.trim())
|
||||||
} else if (key === 'genres') {
|
} else if (key === 'genres') {
|
||||||
updatePayload.metadata.genres = this.selectedMatch[key].split(',').map((v) => v.trim())
|
updatePayload.metadata.genres = this.selectedMatch[key].split(',').map((v) => v.trim())
|
||||||
} else if (key === 'tags') {
|
} else if (key === 'tags') {
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="currentLibrary" class="relative w-36 h-8" v-click-outside="clickOutsideObj">
|
<div v-if="currentLibrary" class="relative sm:w-36 h-8 px-1.5" v-click-outside="clickOutsideObj">
|
||||||
<button type="button" :disabled="disabled" class="relative h-full w-full border border-white border-opacity-10 hover:border-opacity-20 rounded shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer bg-black bg-opacity-20 text-gray-400 hover:text-gray-200" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
|
<button type="button" :disabled="disabled" class="w-10 sm:w-36 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">
|
||||||
<span class="flex items-center">
|
<span class="flex items-center justify-center sm:justify-start">
|
||||||
<widgets-library-icon :icon="currentLibraryIcon" class="mr-2" />
|
<widgets-library-icon :icon="currentLibraryIcon" class="sm:mr-2" />
|
||||||
<span class="block truncate text-sm">{{ currentLibrary.name }}</span>
|
<span class="hidden sm:block">{{ currentLibrary.name }}</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<transition name="menu">
|
<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">
|
<ul v-show="showMenu" class="absolute z-10 -mt-px w-36 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="library in librariesFiltered">
|
<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-100 select-none relative py-2 cursor-pointer hover:bg-black-400" id="listbox-option-0" role="option" @click="selectLibrary(library)">
|
||||||
<div class="flex items-center px-3">
|
<div class="flex items-center px-3">
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<div ref="wrapper" class="relative">
|
<div ref="wrapper" class="relative">
|
||||||
<form @submit.prevent="submitForm">
|
<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 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 text-xs bg-bg flex flex-nowrap whitespace-nowrap items-center relative">
|
<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 class="w-full h-full rounded-full absolute top-0 left-0 opacity-0 hover:opacity-100 px-1 bg-bg bg-opacity-75 flex items-center justify-end cursor-pointer">
|
<div class="w-full h-full rounded-full absolute top-0 left-0 opacity-0 hover:opacity-100 px-1 bg-bg bg-opacity-75 flex items-center justify-end cursor-pointer">
|
||||||
<span v-if="showEdit" class="material-icons text-white hover:text-warning" style="font-size: 1.1rem" @click.stop="editItem(item)">edit</span>
|
<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>
|
<span class="material-icons text-white hover:text-error" style="font-size: 1.1rem" @click.stop="removeItem(item)">close</span>
|
||||||
|
|||||||
@@ -110,13 +110,7 @@ export default {
|
|||||||
}
|
}
|
||||||
console.log('Init Payload', payload)
|
console.log('Init Payload', payload)
|
||||||
if (payload.session) {
|
if (payload.session) {
|
||||||
if (this.$refs.streamContainer) {
|
this.$refs.streamContainer.sessionOpen(payload.session)
|
||||||
this.$refs.streamContainer.sessionOpen(payload.session)
|
|
||||||
} else {
|
|
||||||
console.warn('Stream Container not mounted')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (payload.serverSettings) {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start scans currently running
|
// Start scans currently running
|
||||||
|
|||||||
@@ -28,11 +28,11 @@ export default {
|
|||||||
var validOtherFiles = []
|
var validOtherFiles = []
|
||||||
var ignoredFiles = []
|
var ignoredFiles = []
|
||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
var filetype = this.checkFileType(file.name)
|
// var filetype = this.checkFileType(file.name)
|
||||||
if (!filetype) ignoredFiles.push(file)
|
if (!file.filetype) ignoredFiles.push(file)
|
||||||
else {
|
else {
|
||||||
file.filetype = filetype
|
// file.filetype = filetype
|
||||||
if (filetype === 'audio' || (filetype === 'ebook' && mediaType === 'book')) validItemFiles.push(file)
|
if (file.filetype === 'audio' || (file.filetype === 'ebook' && mediaType === 'book')) validItemFiles.push(file)
|
||||||
else validOtherFiles.push(file)
|
else validOtherFiles.push(file)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -82,12 +82,18 @@ export default {
|
|||||||
items: itemResults,
|
items: itemResults,
|
||||||
ignoredFiles: ignoredFilesInRoot
|
ignoredFiles: ignoredFilesInRoot
|
||||||
}
|
}
|
||||||
} else {
|
} else if (filetree.some((f) => f.filetype !== 'audio') || mediaType !== 'book') {
|
||||||
// Single Book drop
|
// Single Book drop
|
||||||
return {
|
return {
|
||||||
items: this.itemFromTreeItems(filetree, mediaType),
|
items: this.itemFromTreeItems(filetree, mediaType),
|
||||||
ignoredFiles: []
|
ignoredFiles: []
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Only audio files dropped so treat each one as an audiobook
|
||||||
|
return {
|
||||||
|
items: filetree.map((audioFile) => ({ itemFiles: [audioFile], otherFiles: [], ignoredFiles: [] })),
|
||||||
|
ignoredFiles: []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getFilesDropped(dataTransferItems) {
|
getFilesDropped(dataTransferItems) {
|
||||||
@@ -95,11 +101,12 @@ export default {
|
|||||||
path: '/',
|
path: '/',
|
||||||
items: []
|
items: []
|
||||||
}
|
}
|
||||||
function traverseFileTreePromise(item, currtreemap) {
|
function traverseFileTreePromise(item, currtreemap, checkFileType) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (item.isFile) {
|
if (item.isFile) {
|
||||||
item.file((file) => {
|
item.file((file) => {
|
||||||
file.filepath = currtreemap.path + file.name //save full path
|
file.filepath = currtreemap.path + file.name //save full path
|
||||||
|
file.filetype = checkFileType(file.name)
|
||||||
currtreemap.items.push(file)
|
currtreemap.items.push(file)
|
||||||
resolve(file)
|
resolve(file)
|
||||||
})
|
})
|
||||||
@@ -119,7 +126,7 @@ export default {
|
|||||||
dirReader.readEntries((entries) => {
|
dirReader.readEntries((entries) => {
|
||||||
if (entries.length > 0) {
|
if (entries.length > 0) {
|
||||||
for (let entr of entries) {
|
for (let entr of entries) {
|
||||||
entriesPromises.push(traverseFileTreePromise(entr, newtreemap))
|
entriesPromises.push(traverseFileTreePromise(entr, newtreemap, checkFileType))
|
||||||
}
|
}
|
||||||
readEntries()
|
readEntries()
|
||||||
} else {
|
} else {
|
||||||
@@ -135,7 +142,7 @@ export default {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let entriesPromises = []
|
let entriesPromises = []
|
||||||
for (let it of dataTransferItems) {
|
for (let it of dataTransferItems) {
|
||||||
var filetree = traverseFileTreePromise(it.webkitGetAsEntry(), treemap)
|
var filetree = traverseFileTreePromise(it.webkitGetAsEntry(), treemap, this.checkFileType)
|
||||||
entriesPromises.push(filetree)
|
entriesPromises.push(filetree)
|
||||||
}
|
}
|
||||||
Promise.all(entriesPromises).then(() => {
|
Promise.all(entriesPromises).then(() => {
|
||||||
@@ -152,7 +159,9 @@ export default {
|
|||||||
...book
|
...book
|
||||||
}
|
}
|
||||||
var firstBookFile = book.itemFiles[0]
|
var firstBookFile = book.itemFiles[0]
|
||||||
if (!firstBookFile.filepath) return audiobook // No path
|
if (!firstBookFile.filepath) {
|
||||||
|
return audiobook // No path
|
||||||
|
}
|
||||||
|
|
||||||
var firstBookPath = Path.dirname(firstBookFile.filepath)
|
var firstBookPath = Path.dirname(firstBookFile.filepath)
|
||||||
|
|
||||||
@@ -165,6 +174,9 @@ export default {
|
|||||||
if (dirs.length) {
|
if (dirs.length) {
|
||||||
audiobook.author = dirs.pop()
|
audiobook.author = dirs.pop()
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Use file basename as title
|
||||||
|
audiobook.title = Path.basename(firstBookFile.name, Path.extname(firstBookFile.name))
|
||||||
}
|
}
|
||||||
return audiobook
|
return audiobook
|
||||||
},
|
},
|
||||||
@@ -178,7 +190,12 @@ export default {
|
|||||||
if (!firstAudioFile.filepath) return podcast // No path
|
if (!firstAudioFile.filepath) return podcast // No path
|
||||||
var firstPath = Path.dirname(firstAudioFile.filepath)
|
var firstPath = Path.dirname(firstAudioFile.filepath)
|
||||||
var dirs = firstPath.split('/').filter(d => !!d && d !== '.')
|
var dirs = firstPath.split('/').filter(d => !!d && d !== '.')
|
||||||
podcast.title = dirs.length > 1 ? dirs[1] : dirs[0]
|
if (dirs.length) {
|
||||||
|
podcast.title = dirs.length > 1 ? dirs[1] : dirs[0]
|
||||||
|
} else {
|
||||||
|
podcast.title = Path.basename(firstAudioFile.name, Path.extname(firstAudioFile.name))
|
||||||
|
}
|
||||||
|
|
||||||
return podcast
|
return podcast
|
||||||
},
|
},
|
||||||
cleanItem(item, mediaType, index) {
|
cleanItem(item, mediaType, index) {
|
||||||
@@ -188,6 +205,7 @@ export default {
|
|||||||
async getItemsFromDataTransferItems(dataTransferItems, mediaType) {
|
async getItemsFromDataTransferItems(dataTransferItems, mediaType) {
|
||||||
var files = await this.getFilesDropped(dataTransferItems)
|
var files = await this.getFilesDropped(dataTransferItems)
|
||||||
if (!files || !files.length) return { error: 'No files found ' }
|
if (!files || !files.length) return { error: 'No files found ' }
|
||||||
|
|
||||||
var itemData = this.fileTreeToItems(files, mediaType)
|
var itemData = this.fileTreeToItems(files, mediaType)
|
||||||
if (!itemData.items.length && !itemData.ignoredFiles.length) {
|
if (!itemData.items.length && !itemData.ignoredFiles.length) {
|
||||||
return { error: 'Invalid file drop' }
|
return { error: 'Invalid file drop' }
|
||||||
@@ -218,9 +236,12 @@ export default {
|
|||||||
else {
|
else {
|
||||||
file.filetype = filetype
|
file.filetype = filetype
|
||||||
if (file.webkitRelativePath) file.filepath = file.webkitRelativePath
|
if (file.webkitRelativePath) file.filepath = file.webkitRelativePath
|
||||||
|
else file.filepath = file.name
|
||||||
|
|
||||||
if (filetype === 'audio' || (filetype === 'ebook' && mediaType === 'book')) {
|
if (filetype === 'audio' || (filetype === 'ebook' && mediaType === 'book')) {
|
||||||
var dir = file.filepath ? Path.dirname(file.filepath) : ''
|
var dir = file.filepath ? Path.dirname(file.filepath) : ''
|
||||||
|
if (dir === '.') dir = ''
|
||||||
|
|
||||||
if (!itemMap[dir]) {
|
if (!itemMap[dir]) {
|
||||||
itemMap[dir] = {
|
itemMap[dir] = {
|
||||||
path: dir,
|
path: dir,
|
||||||
@@ -246,8 +267,17 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
var items = []
|
||||||
var index = 1
|
var index = 1
|
||||||
var items = Object.values(itemMap).map(i => this.cleanItem(i, mediaType, index++))
|
// If book media type and all files are audio files then treat each one as an audiobook
|
||||||
|
if (itemMap[''] && !otherFiles.length && mediaType === 'book' && !itemMap[''].itemFiles.some(f => f.filetype !== 'audio')) {
|
||||||
|
items = itemMap[''].itemFiles.map((audioFile) => {
|
||||||
|
return this.cleanItem({ itemFiles: [audioFile], otherFiles: [], ignoredFiles: [] }, mediaType, index++)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
items = Object.values(itemMap).map(i => this.cleanItem(i, mediaType, index++))
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items,
|
items,
|
||||||
ignoredFiles: ignoredFiles
|
ignoredFiles: ignoredFiles
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.0.21",
|
"version": "2.0.22",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.0.21",
|
"version": "2.0.22",
|
||||||
"description": "Self-hosted audiobook and podcast client",
|
"description": "Self-hosted audiobook and podcast client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
|
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
|
||||||
<div class="flex items-center mb-2">
|
<div class="flex items-center mb-2">
|
||||||
<h1 class="text-xl">Settings</h1>
|
<h1 class="text-xl font-semibold">Settings</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
@@ -27,26 +27,6 @@
|
|||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
|
||||||
<ui-toggle-switch v-model="useSquareBookCovers" :disabled="updatingServerSettings" @input="updateBookCoverAspectRatio" />
|
|
||||||
<ui-tooltip :text="tooltips.coverAspectRatio">
|
|
||||||
<p class="pl-4 text-lg">
|
|
||||||
Use square book covers
|
|
||||||
<span class="material-icons icon-text">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-tooltip :text="tooltips.bookshelfView">
|
|
||||||
<p class="pl-4 text-lg">
|
|
||||||
Use alternative bookshelf view
|
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
|
||||||
</p>
|
|
||||||
</ui-tooltip>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="newServerSettings.sortingIgnorePrefix" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('sortingIgnorePrefix', val)" />
|
<ui-toggle-switch v-model="newServerSettings.sortingIgnorePrefix" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('sortingIgnorePrefix', val)" />
|
||||||
<ui-tooltip :text="tooltips.sortingIgnorePrefix">
|
<ui-tooltip :text="tooltips.sortingIgnorePrefix">
|
||||||
@@ -66,7 +46,36 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center mb-2 mt-8">
|
<div class="flex items-center mb-2 mt-8">
|
||||||
<h1 class="text-xl">Scanner Settings</h1>
|
<h1 class="text-xl font-semibold">Display Settings</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center py-2">
|
||||||
|
<ui-toggle-switch v-model="useSquareBookCovers" :disabled="updatingServerSettings" @input="updateBookCoverAspectRatio" />
|
||||||
|
<ui-tooltip :text="tooltips.coverAspectRatio">
|
||||||
|
<p class="pl-4 text-lg">
|
||||||
|
Use square book covers
|
||||||
|
<span class="material-icons icon-text">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-tooltip :text="tooltips.bookshelfView">
|
||||||
|
<p class="pl-4 text-lg">
|
||||||
|
Use alternative bookshelf view
|
||||||
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
|
</p>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center py-2">
|
||||||
|
<p class="pr-4 text-lg">Date Format</p>
|
||||||
|
<ui-dropdown v-model="newServerSettings.dateFormat" :items="dateFormats" small class="max-w-40" @input="(val) => updateSettingsKey('dateFormat', val)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center mb-2 mt-8">
|
||||||
|
<h1 class="text-xl font-semibold">Scanner Settings</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
@@ -103,6 +112,16 @@
|
|||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center py-2">
|
||||||
|
<ui-toggle-switch v-model="newServerSettings.scannerPreferOverdriveMediaMarker" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferOverdriveMediaMarker', val)" />
|
||||||
|
<ui-tooltip :text="tooltips.scannerPreferOverdriveMediaMarker">
|
||||||
|
<p class="pl-4 text-lg">
|
||||||
|
Scanner prefer Overdrive Media Markers for chapters
|
||||||
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
|
</p>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="newServerSettings.scannerPreferOpfMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferOpfMetadata', val)" />
|
<ui-toggle-switch v-model="newServerSettings.scannerPreferOpfMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferOpfMetadata', val)" />
|
||||||
<ui-tooltip :text="tooltips.scannerPreferOpfMetadata">
|
<ui-tooltip :text="tooltips.scannerPreferOpfMetadata">
|
||||||
@@ -134,7 +153,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center mb-2 mt-8">
|
<div class="flex items-center mb-2 mt-8">
|
||||||
<h1 class="text-xl">Experimental Feature Settings</h1>
|
<h1 class="text-xl font-semibold">Experimental Feature Settings</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
@@ -245,7 +264,8 @@ export default {
|
|||||||
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',
|
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',
|
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',
|
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 below just for you)'
|
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 below just for you)',
|
||||||
|
scannerPreferOverdriveMediaMarker: 'MP3 files from Overdrive come with chapter timings embedded as custom metadata. Enabling this will use these tags for chapter timings automatically'
|
||||||
},
|
},
|
||||||
showConfirmPurgeCache: false
|
showConfirmPurgeCache: false
|
||||||
}
|
}
|
||||||
@@ -271,12 +291,12 @@ export default {
|
|||||||
set(val) {
|
set(val) {
|
||||||
this.$store.commit('setExperimentalFeatures', val)
|
this.$store.commit('setExperimentalFeatures', val)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
dateFormats() {
|
||||||
|
return this.$store.state.globals.dateFormats
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateEnableChromecast(val) {
|
|
||||||
this.updateServerSettings({ enableChromecast: val })
|
|
||||||
},
|
|
||||||
updateSortingPrefixes(val) {
|
updateSortingPrefixes(val) {
|
||||||
if (!val || !val.length) {
|
if (!val || !val.length) {
|
||||||
this.$toast.error('Must have at least 1 prefix')
|
this.$toast.error('Must have at least 1 prefix')
|
||||||
@@ -314,10 +334,12 @@ export default {
|
|||||||
.then((success) => {
|
.then((success) => {
|
||||||
console.log('Updated Server Settings', success)
|
console.log('Updated Server Settings', success)
|
||||||
this.updatingServerSettings = false
|
this.updatingServerSettings = false
|
||||||
|
this.$toast.success('Server settings updated')
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to update server settings', error)
|
console.error('Failed to update server settings', error)
|
||||||
this.updatingServerSettings = false
|
this.updatingServerSettings = false
|
||||||
|
this.$toast.error('Failed to update server settings')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
initServerSettings() {
|
initServerSettings() {
|
||||||
|
|||||||
@@ -17,8 +17,8 @@
|
|||||||
<div ref="container" class="relative w-full h-full bg-primary border-bg overflow-x-hidden overflow-y-auto text-red shadow-inner rounded-md" style="max-height: 550px; min-height: 550px">
|
<div ref="container" class="relative w-full h-full bg-primary border-bg overflow-x-hidden overflow-y-auto text-red shadow-inner rounded-md" style="max-height: 550px; min-height: 550px">
|
||||||
<template v-for="(log, index) in logs">
|
<template v-for="(log, index) in logs">
|
||||||
<div :key="index" class="flex flex-nowrap px-2 py-1 items-start text-sm bg-opacity-10" :class="`bg-${logColors[log.level]}`">
|
<div :key="index" class="flex flex-nowrap px-2 py-1 items-start text-sm bg-opacity-10" :class="`bg-${logColors[log.level]}`">
|
||||||
<p class="text-gray-400 w-40 font-mono">{{ log.timestamp.split('.')[0].split('T').join(' ') }}</p>
|
<p class="text-gray-400 w-36 font-mono text-xs">{{ log.timestamp }}</p>
|
||||||
<p class="font-semibold w-12 text-right" :class="`text-${logColors[log.level]}`">{{ log.levelName }}</p>
|
<p class="font-semibold w-12 text-right text-sm" :class="`text-${logColors[log.level]}`">{{ log.levelName }}</p>
|
||||||
<p class="px-4 logmessage">{{ log.message }}</p>
|
<p class="px-4 logmessage">{{ log.message }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -118,9 +118,9 @@
|
|||||||
<!-- Progress -->
|
<!-- Progress -->
|
||||||
<div v-if="!isPodcast && progressPercent > 0" class="px-4 py-2 mt-4 bg-primary text-sm font-semibold rounded-md text-gray-100 relative max-w-max mx-auto md:mx-0" :class="resettingProgress ? 'opacity-25' : ''">
|
<div v-if="!isPodcast && progressPercent > 0" class="px-4 py-2 mt-4 bg-primary text-sm font-semibold rounded-md text-gray-100 relative max-w-max mx-auto md:mx-0" :class="resettingProgress ? 'opacity-25' : ''">
|
||||||
<p v-if="progressPercent < 1" class="leading-6">Your Progress: {{ Math.round(progressPercent * 100) }}%</p>
|
<p v-if="progressPercent < 1" class="leading-6">Your Progress: {{ Math.round(progressPercent * 100) }}%</p>
|
||||||
<p v-else class="text-xs">Finished {{ $formatDate(userProgressFinishedAt, 'MM/dd/yyyy') }}</p>
|
<p v-else class="text-xs">Finished {{ $formatDate(userProgressFinishedAt, dateFormat) }}</p>
|
||||||
<p v-if="progressPercent < 1" class="text-gray-200 text-xs">{{ $elapsedPretty(userTimeRemaining) }} remaining</p>
|
<p v-if="progressPercent < 1" class="text-gray-200 text-xs">{{ $elapsedPretty(userTimeRemaining) }} remaining</p>
|
||||||
<p class="text-gray-400 text-xs pt-1">Started {{ $formatDate(userProgressStartedAt, 'MM/dd/yyyy') }}</p>
|
<p class="text-gray-400 text-xs pt-1">Started {{ $formatDate(userProgressStartedAt, dateFormat) }}</p>
|
||||||
|
|
||||||
<div v-if="!resettingProgress" class="absolute -top-1.5 -right-1.5 p-1 w-5 h-5 rounded-full bg-bg hover:bg-error border border-primary flex items-center justify-center cursor-pointer" @click.stop="clearProgressClick">
|
<div v-if="!resettingProgress" class="absolute -top-1.5 -right-1.5 p-1 w-5 h-5 rounded-full bg-bg hover:bg-error border border-primary flex items-center justify-center cursor-pointer" @click.stop="clearProgressClick">
|
||||||
<span class="material-icons text-sm">close</span>
|
<span class="material-icons text-sm">close</span>
|
||||||
@@ -226,6 +226,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
dateFormat() {
|
||||||
|
return this.$store.state.serverSettings.dateFormat
|
||||||
|
},
|
||||||
showExperimentalFeatures() {
|
showExperimentalFeatures() {
|
||||||
return this.$store.state.showExperimentalFeatures
|
return this.$store.state.showExperimentalFeatures
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -29,7 +29,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pt-8 text-center">
|
<div class="pt-8 text-center">
|
||||||
<p class="text-xs text-white text-opacity-50 font-mono"><strong>Supported File Types: </strong>{{ inputAccept.join(', ') }}</p>
|
<p class="text-xs text-white text-opacity-50 font-mono mb-4"><strong>Supported File Types: </strong>{{ inputAccept.join(', ') }}</p>
|
||||||
|
|
||||||
|
<p class="text-sm text-white text-opacity-70">Folders with media files will be treated as separate library items. <span v-if="selectedLibraryMediaType === 'book'">If uploading only audio files then each audio file will be treated as a separate audiobook.</span></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Item list header -->
|
<!-- Item list header -->
|
||||||
|
|||||||
@@ -177,12 +177,13 @@ export default class PlayerHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
prepareOpenSession(session, playbackRate) { // Session opened on init socket
|
prepareOpenSession(session, playbackRate) { // Session opened on init socket
|
||||||
|
if (!this.player) this.switchPlayer() // Must set player first for open sessions
|
||||||
|
|
||||||
this.libraryItem = session.libraryItem
|
this.libraryItem = session.libraryItem
|
||||||
this.isVideo = session.libraryItem.mediaType === 'video'
|
this.isVideo = session.libraryItem.mediaType === 'video'
|
||||||
this.playWhenReady = false
|
this.playWhenReady = false
|
||||||
this.initialPlaybackRate = playbackRate
|
this.initialPlaybackRate = playbackRate
|
||||||
|
|
||||||
if (!this.player) this.switchPlayer()
|
|
||||||
this.prepareSession(session)
|
this.prepareSession(session)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ Vue.prototype.$sanitizeSlug = (str) => {
|
|||||||
str = str.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i))
|
str = str.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i))
|
||||||
}
|
}
|
||||||
|
|
||||||
str = str.replace('.', '-') // replace a dot by a dash
|
str = str.replace('.', '-') // replace a dot by a dash
|
||||||
.replace(/[^a-z0-9 -_]/g, '') // remove invalid chars
|
.replace(/[^a-z0-9 -_]/g, '') // remove invalid chars
|
||||||
.replace(/\s+/g, '-') // collapse whitespace and replace by a dash
|
.replace(/\s+/g, '-') // collapse whitespace and replace by a dash
|
||||||
.replace(/-+/g, '-') // collapse dashes
|
.replace(/-+/g, '-') // collapse dashes
|
||||||
|
|||||||
+15
-2
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
export const state = () => ({
|
export const state = () => ({
|
||||||
isMobile: false,
|
isMobile: false,
|
||||||
isMobileLandscape: false,
|
isMobileLandscape: false,
|
||||||
@@ -12,7 +11,21 @@ export const state = () => ({
|
|||||||
selectedCollection: null,
|
selectedCollection: null,
|
||||||
selectedAuthor: null,
|
selectedAuthor: null,
|
||||||
isCasting: false, // Actively casting
|
isCasting: false, // Actively casting
|
||||||
isChromecastInitialized: false // Script loaded
|
isChromecastInitialized: false, // Script loaded
|
||||||
|
dateFormats: [
|
||||||
|
{
|
||||||
|
text: 'MM/DD/YYYY',
|
||||||
|
value: 'MM/dd/yyyy'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'DD/MM/YYYY',
|
||||||
|
value: 'dd/MM/yyyy'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'YYYY-MM-DD',
|
||||||
|
value: 'yyyy-MM-dd'
|
||||||
|
}
|
||||||
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ module.exports = {
|
|||||||
'20': '5rem',
|
'20': '5rem',
|
||||||
'24': '6rem',
|
'24': '6rem',
|
||||||
'32': '8rem',
|
'32': '8rem',
|
||||||
|
'40': '10rem',
|
||||||
'48': '12rem',
|
'48': '12rem',
|
||||||
'64': '16rem',
|
'64': '16rem',
|
||||||
'80': '20rem'
|
'80': '20rem'
|
||||||
|
|||||||
Generated
+3
-3
@@ -1,18 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.0.21",
|
"version": "2.0.22",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"version": "2.0.20",
|
"version": "2.0.21",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"archiver": "^5.3.0",
|
"archiver": "^5.3.0",
|
||||||
"axios": "^0.26.1",
|
"axios": "^0.26.1",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"command-line-args": "^5.2.0",
|
"command-line-args": "^5.2.0",
|
||||||
"date-and-time": "^2.0.1",
|
"date-and-time": "^2.3.1",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"express-fileupload": "^1.2.1",
|
"express-fileupload": "^1.2.1",
|
||||||
"express-rate-limit": "^5.3.0",
|
"express-rate-limit": "^5.3.0",
|
||||||
|
|||||||
+5
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.0.21",
|
"version": "2.0.22",
|
||||||
"description": "Self-hosted audiobook and podcast server",
|
"description": "Self-hosted audiobook and podcast server",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -11,6 +11,9 @@
|
|||||||
"build-win": "npm run client && pkg -t node16-win-x64 -o ./dist/win/audiobookshelf -C GZip .",
|
"build-win": "npm run client && pkg -t node16-win-x64 -o ./dist/win/audiobookshelf -C GZip .",
|
||||||
"build-linux": "build/linuxpackager",
|
"build-linux": "build/linuxpackager",
|
||||||
"docker": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 --push . -t advplyr/audiobookshelf",
|
"docker": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 --push . -t advplyr/audiobookshelf",
|
||||||
|
"docker-amd64-local": "docker buildx build --platform linux/amd64 --load . -t advplyr/audiobookshelf-amd64-local",
|
||||||
|
"docker-arm64-local": "docker buildx build --platform linux/arm64 --load . -t advplyr/audiobookshelf-arm64-local",
|
||||||
|
"docker-armv7-local": "docker buildx build --platform linux/arm/v7 --load . -t advplyr/audiobookshelf-armv7-local",
|
||||||
"deploy": "node dist/autodeploy"
|
"deploy": "node dist/autodeploy"
|
||||||
},
|
},
|
||||||
"bin": "prod.js",
|
"bin": "prod.js",
|
||||||
@@ -31,7 +34,7 @@
|
|||||||
"axios": "^0.26.1",
|
"axios": "^0.26.1",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"command-line-args": "^5.2.0",
|
"command-line-args": "^5.2.0",
|
||||||
"date-and-time": "^2.0.1",
|
"date-and-time": "^2.3.1",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"express-fileupload": "^1.2.1",
|
"express-fileupload": "^1.2.1",
|
||||||
"express-rate-limit": "^5.3.0",
|
"express-rate-limit": "^5.3.0",
|
||||||
|
|||||||
+2
-2
@@ -1,16 +1,16 @@
|
|||||||
|
const date = require('date-and-time')
|
||||||
const { LogLevel } = require('./utils/constants')
|
const { LogLevel } = require('./utils/constants')
|
||||||
|
|
||||||
class Logger {
|
class Logger {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.logLevel = process.env.NODE_ENV === 'production' ? LogLevel.INFO : LogLevel.TRACE
|
this.logLevel = process.env.NODE_ENV === 'production' ? LogLevel.INFO : LogLevel.TRACE
|
||||||
// this.logFileLevel = LogLevel.INFO
|
|
||||||
this.socketListeners = []
|
this.socketListeners = []
|
||||||
|
|
||||||
this.logManager = null
|
this.logManager = null
|
||||||
}
|
}
|
||||||
|
|
||||||
get timestamp() {
|
get timestamp() {
|
||||||
return (new Date()).toISOString()
|
return date.format(new Date(), 'YYYY-MM-DD HH:mm:ss')
|
||||||
}
|
}
|
||||||
|
|
||||||
get levelString() {
|
get levelString() {
|
||||||
|
|||||||
+2
-1
@@ -140,7 +140,7 @@ class Server {
|
|||||||
await this.purgeMetadata() // Remove metadata folders without library item
|
await this.purgeMetadata() // Remove metadata folders without library item
|
||||||
await this.cacheManager.ensureCachePaths()
|
await this.cacheManager.ensureCachePaths()
|
||||||
await this.abMergeManager.ensureDownloadDirPath()
|
await this.abMergeManager.ensureDownloadDirPath()
|
||||||
|
|
||||||
await this.backupManager.init()
|
await this.backupManager.init()
|
||||||
await this.logManager.init()
|
await this.logManager.init()
|
||||||
await this.rssFeedManager.init()
|
await this.rssFeedManager.init()
|
||||||
@@ -451,6 +451,7 @@ class Server {
|
|||||||
} else {
|
} else {
|
||||||
Logger.debug(`[Server] User Online ${client.user.username}`)
|
Logger.debug(`[Server] User Online ${client.user.username}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.io.emit('user_online', client.user.toJSONForPublic(this.playbackSessionManager.sessions, this.db.libraryItems))
|
this.io.emit('user_online', client.user.toJSONForPublic(this.playbackSessionManager.sessions, this.db.libraryItems))
|
||||||
|
|
||||||
user.lastSeen = Date.now()
|
user.lastSeen = Date.now()
|
||||||
|
|||||||
@@ -63,15 +63,27 @@ class AuthorController {
|
|||||||
// If updating or removing cover image then clear cache
|
// If updating or removing cover image then clear cache
|
||||||
if (payload.imagePath !== undefined && req.author.imagePath && payload.imagePath !== req.author.imagePath) {
|
if (payload.imagePath !== undefined && req.author.imagePath && payload.imagePath !== req.author.imagePath) {
|
||||||
this.cacheManager.purgeImageCache(req.author.id)
|
this.cacheManager.purgeImageCache(req.author.id)
|
||||||
|
|
||||||
if (!payload.imagePath) { // If removing image then remove file
|
if (!payload.imagePath) { // If removing image then remove file
|
||||||
var currentImagePath = req.author.imagePath
|
var currentImagePath = req.author.imagePath
|
||||||
await this.coverManager.removeFile(currentImagePath)
|
await this.coverManager.removeFile(currentImagePath)
|
||||||
|
} else if (payload.imagePath.startsWith('http')) { // Check if image path is a url
|
||||||
|
var imageData = await this.authorFinder.saveAuthorImage(req.author.id, payload.imagePath)
|
||||||
|
if (imageData) {
|
||||||
|
req.author.imagePath = imageData.path
|
||||||
|
req.author.relImagePath = imageData.relPath
|
||||||
|
hasUpdated = hasUpdated || true;
|
||||||
|
} else {
|
||||||
|
req.author.imagePath = null
|
||||||
|
req.author.relImagePath = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var authorNameUpdate = payload.name !== undefined && payload.name !== req.author.name
|
var authorNameUpdate = payload.name !== undefined && payload.name !== req.author.name
|
||||||
|
|
||||||
var hasUpdated = req.author.update(payload)
|
var hasUpdated = req.author.update(payload)
|
||||||
|
|
||||||
if (hasUpdated) {
|
if (hasUpdated) {
|
||||||
if (authorNameUpdate) { // Update author name on all books
|
if (authorNameUpdate) { // Update author name on all books
|
||||||
var itemsWithAuthor = this.db.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasAuthor(req.author.id))
|
var itemsWithAuthor = this.db.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasAuthor(req.author.id))
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ class PodcastController {
|
|||||||
Logger.error('Invalid podcast feed request response')
|
Logger.error('Invalid podcast feed request response')
|
||||||
return res.status(500).send('Bad response from feed request')
|
return res.status(500).send('Bad response from feed request')
|
||||||
}
|
}
|
||||||
Logger.debug(`[PdocastController] Podcast feed size ${(data.data.length / 1024 / 1024).toFixed(2)}MB`)
|
Logger.debug(`[PodcastController] Podcast feed size ${(data.data.length / 1024 / 1024).toFixed(2)}MB`)
|
||||||
var payload = await parsePodcastRssFeedXml(data.data, false, includeRaw)
|
var payload = await parsePodcastRssFeedXml(data.data, false, includeRaw)
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
return res.status(500).send('Invalid podcast RSS feed')
|
return res.status(500).send('Invalid podcast RSS feed')
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const Path = require('path')
|
|||||||
const Audnexus = require('../providers/Audnexus')
|
const Audnexus = require('../providers/Audnexus')
|
||||||
|
|
||||||
const { downloadFile } = require('../utils/fileUtils')
|
const { downloadFile } = require('../utils/fileUtils')
|
||||||
|
const filePerms = require('../utils/filePerms')
|
||||||
|
|
||||||
class AuthorFinder {
|
class AuthorFinder {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -38,7 +39,11 @@ class AuthorFinder {
|
|||||||
async saveAuthorImage(authorId, url) {
|
async saveAuthorImage(authorId, url) {
|
||||||
var authorDir = this.AuthorPath
|
var authorDir = this.AuthorPath
|
||||||
var relAuthorDir = Path.posix.join('/metadata', 'authors')
|
var relAuthorDir = Path.posix.join('/metadata', 'authors')
|
||||||
await fs.ensureDir(authorDir)
|
|
||||||
|
if (!await fs.pathExists(authorDir)) {
|
||||||
|
await fs.ensureDir(authorDir)
|
||||||
|
await filePerms.setDefault(authorDir)
|
||||||
|
}
|
||||||
|
|
||||||
var imageExtension = url.toLowerCase().split('.').pop()
|
var imageExtension = url.toLowerCase().split('.').pop()
|
||||||
var ext = imageExtension === 'png' ? 'png' : 'jpg'
|
var ext = imageExtension === 'png' ? 'png' : 'jpg'
|
||||||
|
|||||||
@@ -180,11 +180,11 @@ class BookFinder {
|
|||||||
Logger.debug(`Book Search: title: "${title}", author: "${author}", provider: ${provider}`)
|
Logger.debug(`Book Search: title: "${title}", author: "${author}", provider: ${provider}`)
|
||||||
|
|
||||||
if (provider === 'google') {
|
if (provider === 'google') {
|
||||||
return this.getGoogleBooksResults(title, author)
|
books = this.getGoogleBooksResults(title, author)
|
||||||
} else if (provider === 'audible') {
|
} else if (provider === 'audible') {
|
||||||
return this.getAudibleResults(title, author, asin)
|
books = this.getAudibleResults(title, author, asin)
|
||||||
} else if (provider === 'itunes') {
|
} else if (provider === 'itunes') {
|
||||||
return this.getiTunesAudiobooksResults(title, author)
|
books = this.getiTunesAudiobooksResults(title, author)
|
||||||
} else if (provider === 'libgen') {
|
} else if (provider === 'libgen') {
|
||||||
books = await this.getLibGenResults(title, author, maxTitleDistance, maxAuthorDistance)
|
books = await this.getLibGenResults(title, author, maxTitleDistance, maxAuthorDistance)
|
||||||
} else if (provider === 'openlibrary') {
|
} else if (provider === 'openlibrary') {
|
||||||
@@ -208,6 +208,18 @@ class BookFinder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!books.length && !options.currentlyTryingCleaned) {
|
||||||
|
var cleanedTitle = this.cleanTitleForCompares(title)
|
||||||
|
var cleanedAuthor = this.cleanAuthorForCompares(author)
|
||||||
|
if (cleanedTitle == title && cleanedAuthor == author) return books
|
||||||
|
|
||||||
|
Logger.debug(`Book Search, no matches.. checking cleaned title and author`)
|
||||||
|
options.currentlyTryingCleaned = true
|
||||||
|
return this.search(provider, cleanedTitle, cleanedAuthor, isbn, asin, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (["google", "audible", "itunes"].includes(provider)) return books
|
||||||
|
|
||||||
return books.sort((a, b) => {
|
return books.sort((a, b) => {
|
||||||
return a.totalDistance - b.totalDistance
|
return a.totalDistance - b.totalDistance
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const DEFAULT_EXPIRATION = 1000 * 60 * 60 // 60 minutes
|
const DEFAULT_EXPIRATION = 1000 * 60 * 60 // 60 minutes
|
||||||
const DEFAULT_TIMEOUT = 1000 * 60 * 20 // 20 minutes
|
const DEFAULT_TIMEOUT = 1000 * 60 * 30 // 30 minutes
|
||||||
class Download {
|
class Download {
|
||||||
constructor(download) {
|
constructor(download) {
|
||||||
this.id = null
|
this.id = null
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const Logger = require('../../Logger')
|
|||||||
const BookMetadata = require('../metadata/BookMetadata')
|
const BookMetadata = require('../metadata/BookMetadata')
|
||||||
const { areEquivalent, copyValue } = require('../../utils/index')
|
const { areEquivalent, copyValue } = require('../../utils/index')
|
||||||
const { parseOpfMetadataXML } = require('../../utils/parsers/parseOpfMetadata')
|
const { parseOpfMetadataXML } = require('../../utils/parsers/parseOpfMetadata')
|
||||||
|
const { overdriveMediaMarkersExist, parseOverdriveMediaMarkersAsChapters } = require('../../utils/parsers/parseOverdriveMediaMarkers')
|
||||||
const abmetadataGenerator = require('../../utils/abmetadataGenerator')
|
const abmetadataGenerator = require('../../utils/abmetadataGenerator')
|
||||||
const { readTextFile } = require('../../utils/fileUtils')
|
const { readTextFile } = require('../../utils/fileUtils')
|
||||||
const AudioFile = require('../files/AudioFile')
|
const AudioFile = require('../files/AudioFile')
|
||||||
@@ -360,10 +361,11 @@ class Book {
|
|||||||
this.rebuildTracks()
|
this.rebuildTracks()
|
||||||
}
|
}
|
||||||
|
|
||||||
rebuildTracks() {
|
rebuildTracks(preferOverdriveMediaMarker) {
|
||||||
|
Logger.debug(`[Book] Tracks being rebuilt...!`)
|
||||||
this.audioFiles.sort((a, b) => a.index - b.index)
|
this.audioFiles.sort((a, b) => a.index - b.index)
|
||||||
this.missingParts = []
|
this.missingParts = []
|
||||||
this.setChapters()
|
this.setChapters(preferOverdriveMediaMarker)
|
||||||
this.checkUpdateMissingTracks()
|
this.checkUpdateMissingTracks()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,9 +397,16 @@ class Book {
|
|||||||
return wasUpdated
|
return wasUpdated
|
||||||
}
|
}
|
||||||
|
|
||||||
setChapters() {
|
setChapters(preferOverdriveMediaMarker = false) {
|
||||||
// If 1 audio file without chapters, then no chapters will be set
|
// If 1 audio file without chapters, then no chapters will be set
|
||||||
var includedAudioFiles = this.audioFiles.filter(af => !af.exclude)
|
var includedAudioFiles = this.audioFiles.filter(af => !af.exclude)
|
||||||
|
|
||||||
|
// If overdrive media markers are present and preferred, use those instead
|
||||||
|
if (preferOverdriveMediaMarker && overdriveMediaMarkersExist(includedAudioFiles)) {
|
||||||
|
Logger.info('[Book] Overdrive Media Markers and preference found! Using these for chapter definitions')
|
||||||
|
return this.chapters = parseOverdriveMediaMarkersAsChapters(includedAudioFiles)
|
||||||
|
}
|
||||||
|
|
||||||
if (includedAudioFiles.length === 1) {
|
if (includedAudioFiles.length === 1) {
|
||||||
// 1 audio file with chapters
|
// 1 audio file with chapters
|
||||||
if (includedAudioFiles[0].chapters) {
|
if (includedAudioFiles[0].chapters) {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class AudioMetaTags {
|
|||||||
this.tagIsbn = null
|
this.tagIsbn = null
|
||||||
this.tagLanguage = null
|
this.tagLanguage = null
|
||||||
this.tagASIN = null
|
this.tagASIN = null
|
||||||
|
this.tagOverdriveMediaMarker = null
|
||||||
|
|
||||||
if (metadata) {
|
if (metadata) {
|
||||||
this.construct(metadata)
|
this.construct(metadata)
|
||||||
@@ -58,6 +59,7 @@ class AudioMetaTags {
|
|||||||
this.tagIsbn = metadata.tagIsbn || null
|
this.tagIsbn = metadata.tagIsbn || null
|
||||||
this.tagLanguage = metadata.tagLanguage || null
|
this.tagLanguage = metadata.tagLanguage || null
|
||||||
this.tagASIN = metadata.tagASIN || null
|
this.tagASIN = metadata.tagASIN || null
|
||||||
|
this.tagOverdriveMediaMarker = metadata.tagOverdriveMediaMarker || null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Data parsed in prober.js
|
// Data parsed in prober.js
|
||||||
@@ -82,6 +84,7 @@ class AudioMetaTags {
|
|||||||
this.tagIsbn = payload.file_tag_isbn || null
|
this.tagIsbn = payload.file_tag_isbn || null
|
||||||
this.tagLanguage = payload.file_tag_language || null
|
this.tagLanguage = payload.file_tag_language || null
|
||||||
this.tagASIN = payload.file_tag_asin || null
|
this.tagASIN = payload.file_tag_asin || null
|
||||||
|
this.tagOverdriveMediaMarker = payload.file_tag_overdrive_media_marker || null
|
||||||
}
|
}
|
||||||
|
|
||||||
updateData(payload) {
|
updateData(payload) {
|
||||||
@@ -105,7 +108,8 @@ class AudioMetaTags {
|
|||||||
tagEncodedBy: payload.file_tag_encodedby || null,
|
tagEncodedBy: payload.file_tag_encodedby || null,
|
||||||
tagIsbn: payload.file_tag_isbn || null,
|
tagIsbn: payload.file_tag_isbn || null,
|
||||||
tagLanguage: payload.file_tag_language || null,
|
tagLanguage: payload.file_tag_language || null,
|
||||||
tagASIN: payload.file_tag_asin || null
|
tagASIN: payload.file_tag_asin || null,
|
||||||
|
tagOverdriveMediaMarker: payload.file_tag_overdrive_media_marker || null,
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasUpdates = false
|
var hasUpdates = false
|
||||||
|
|||||||
@@ -262,6 +262,10 @@ class BookMetadata {
|
|||||||
{
|
{
|
||||||
tag: 'tagASIN',
|
tag: 'tagASIN',
|
||||||
key: 'asin'
|
key: 'asin'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'tagOverdriveMediaMarker',
|
||||||
|
key: 'overdriveMediaMarker'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ class ServerSettings {
|
|||||||
this.scannerPreferAudioMetadata = false
|
this.scannerPreferAudioMetadata = false
|
||||||
this.scannerPreferOpfMetadata = false
|
this.scannerPreferOpfMetadata = false
|
||||||
this.scannerPreferMatchedMetadata = false
|
this.scannerPreferMatchedMetadata = false
|
||||||
this.scannerDisableWatcher = false
|
this.scannerDisableWatcher = false
|
||||||
|
this.scannerPreferOverdriveMediaMarker = false
|
||||||
|
|
||||||
// Metadata - choose to store inside users library item folder
|
// Metadata - choose to store inside users library item folder
|
||||||
this.storeCoverWithItem = false
|
this.storeCoverWithItem = false
|
||||||
@@ -47,6 +48,7 @@ class ServerSettings {
|
|||||||
// Misc Flags
|
// Misc Flags
|
||||||
this.chromecastEnabled = false
|
this.chromecastEnabled = false
|
||||||
this.enableEReader = false
|
this.enableEReader = false
|
||||||
|
this.dateFormat = 'MM/dd/yyyy'
|
||||||
|
|
||||||
this.logLevel = Logger.logLevel
|
this.logLevel = Logger.logLevel
|
||||||
|
|
||||||
@@ -65,6 +67,7 @@ class ServerSettings {
|
|||||||
this.scannerPreferOpfMetadata = !!settings.scannerPreferOpfMetadata
|
this.scannerPreferOpfMetadata = !!settings.scannerPreferOpfMetadata
|
||||||
this.scannerPreferMatchedMetadata = !!settings.scannerPreferMatchedMetadata
|
this.scannerPreferMatchedMetadata = !!settings.scannerPreferMatchedMetadata
|
||||||
this.scannerDisableWatcher = !!settings.scannerDisableWatcher
|
this.scannerDisableWatcher = !!settings.scannerDisableWatcher
|
||||||
|
this.scannerPreferOverdriveMediaMarker = !!settings.scannerPreferOverdriveMediaMarker
|
||||||
|
|
||||||
this.storeCoverWithItem = !!settings.storeCoverWithItem
|
this.storeCoverWithItem = !!settings.storeCoverWithItem
|
||||||
if (settings.storeCoverWithBook != undefined) { // storeCoverWithBook was old name of setting < v2
|
if (settings.storeCoverWithBook != undefined) { // storeCoverWithBook was old name of setting < v2
|
||||||
@@ -93,6 +96,7 @@ class ServerSettings {
|
|||||||
this.sortingPrefixes = settings.sortingPrefixes || ['the', 'a']
|
this.sortingPrefixes = settings.sortingPrefixes || ['the', 'a']
|
||||||
this.chromecastEnabled = !!settings.chromecastEnabled
|
this.chromecastEnabled = !!settings.chromecastEnabled
|
||||||
this.enableEReader = !!settings.enableEReader
|
this.enableEReader = !!settings.enableEReader
|
||||||
|
this.dateFormat = settings.dateFormat || 'MM/dd/yyyy'
|
||||||
this.logLevel = settings.logLevel || Logger.logLevel
|
this.logLevel = settings.logLevel || Logger.logLevel
|
||||||
this.version = settings.version || null
|
this.version = settings.version || null
|
||||||
|
|
||||||
@@ -111,6 +115,7 @@ class ServerSettings {
|
|||||||
scannerPreferOpfMetadata: this.scannerPreferOpfMetadata,
|
scannerPreferOpfMetadata: this.scannerPreferOpfMetadata,
|
||||||
scannerPreferMatchedMetadata: this.scannerPreferMatchedMetadata,
|
scannerPreferMatchedMetadata: this.scannerPreferMatchedMetadata,
|
||||||
scannerDisableWatcher: this.scannerDisableWatcher,
|
scannerDisableWatcher: this.scannerDisableWatcher,
|
||||||
|
scannerPreferOverdriveMediaMarker: this.scannerPreferOverdriveMediaMarker,
|
||||||
storeCoverWithItem: this.storeCoverWithItem,
|
storeCoverWithItem: this.storeCoverWithItem,
|
||||||
storeMetadataWithItem: this.storeMetadataWithItem,
|
storeMetadataWithItem: this.storeMetadataWithItem,
|
||||||
rateLimitLoginRequests: this.rateLimitLoginRequests,
|
rateLimitLoginRequests: this.rateLimitLoginRequests,
|
||||||
@@ -127,6 +132,7 @@ class ServerSettings {
|
|||||||
sortingPrefixes: [...this.sortingPrefixes],
|
sortingPrefixes: [...this.sortingPrefixes],
|
||||||
chromecastEnabled: this.chromecastEnabled,
|
chromecastEnabled: this.chromecastEnabled,
|
||||||
enableEReader: this.enableEReader,
|
enableEReader: this.enableEReader,
|
||||||
|
dateFormat: this.dateFormat,
|
||||||
logLevel: this.logLevel,
|
logLevel: this.logLevel,
|
||||||
version: this.version
|
version: this.version
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class LibraryScan {
|
|||||||
get forceRescan() { return !!this._scanOptions.forceRescan }
|
get forceRescan() { return !!this._scanOptions.forceRescan }
|
||||||
get preferAudioMetadata() { return !!this._scanOptions.preferAudioMetadata }
|
get preferAudioMetadata() { return !!this._scanOptions.preferAudioMetadata }
|
||||||
get preferOpfMetadata() { return !!this._scanOptions.preferOpfMetadata }
|
get preferOpfMetadata() { return !!this._scanOptions.preferOpfMetadata }
|
||||||
|
get preferOverdriveMediaMarker() { return !!this._scanOptions.preferOverdriveMediaMarker }
|
||||||
get findCovers() { return !!this._scanOptions.findCovers }
|
get findCovers() { return !!this._scanOptions.findCovers }
|
||||||
get timestamp() {
|
get timestamp() {
|
||||||
return (new Date()).toISOString()
|
return (new Date()).toISOString()
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ class MediaFileScanner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async scanMediaFiles(mediaLibraryFiles, scanData, libraryItem, preferAudioMetadata, libraryScan = null) {
|
async scanMediaFiles(mediaLibraryFiles, scanData, libraryItem, preferAudioMetadata, preferOverdriveMediaMarker, libraryScan = null) {
|
||||||
var hasUpdated = false
|
var hasUpdated = false
|
||||||
|
|
||||||
var mediaScanResult = await this.executeMediaFileScans(libraryItem.mediaType, mediaLibraryFiles, scanData)
|
var mediaScanResult = await this.executeMediaFileScans(libraryItem.mediaType, mediaLibraryFiles, scanData)
|
||||||
@@ -208,6 +208,7 @@ class MediaFileScanner {
|
|||||||
} else if (mediaScanResult.audioFiles.length) {
|
} else if (mediaScanResult.audioFiles.length) {
|
||||||
if (libraryScan) {
|
if (libraryScan) {
|
||||||
libraryScan.addLog(LogLevel.DEBUG, `Library Item "${scanData.path}" Audio file scan took ${mediaScanResult.elapsed}ms for ${mediaScanResult.audioFiles.length} with average time of ${mediaScanResult.averageScanDuration}ms`)
|
libraryScan.addLog(LogLevel.DEBUG, `Library Item "${scanData.path}" Audio file scan took ${mediaScanResult.elapsed}ms for ${mediaScanResult.audioFiles.length} with average time of ${mediaScanResult.averageScanDuration}ms`)
|
||||||
|
Logger.debug(`Library Item "${scanData.path}" Audio file scan took ${mediaScanResult.elapsed}ms for ${mediaScanResult.audioFiles.length} with average time of ${mediaScanResult.averageScanDuration}ms`)
|
||||||
}
|
}
|
||||||
|
|
||||||
var totalAudioFilesToInclude = mediaScanResult.audioFiles.length
|
var totalAudioFilesToInclude = mediaScanResult.audioFiles.length
|
||||||
@@ -247,7 +248,7 @@ class MediaFileScanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasUpdated) {
|
if (hasUpdated) {
|
||||||
libraryItem.media.rebuildTracks()
|
libraryItem.media.rebuildTracks(preferOverdriveMediaMarker)
|
||||||
}
|
}
|
||||||
} else { // Podcast Media Type
|
} else { // Podcast Media Type
|
||||||
var existingAudioFiles = mediaScanResult.audioFiles.filter(af => libraryItem.media.findFileWithInode(af.ino))
|
var existingAudioFiles = mediaScanResult.audioFiles.filter(af => libraryItem.media.findFileWithInode(af.ino))
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class ScanOptions {
|
|||||||
this.preferAudioMetadata = false
|
this.preferAudioMetadata = false
|
||||||
this.preferOpfMetadata = false
|
this.preferOpfMetadata = false
|
||||||
this.preferMatchedMetadata = false
|
this.preferMatchedMetadata = false
|
||||||
|
this.preferOverdriveMediaMarker = false
|
||||||
|
|
||||||
if (options) {
|
if (options) {
|
||||||
this.construct(options)
|
this.construct(options)
|
||||||
@@ -34,7 +35,8 @@ class ScanOptions {
|
|||||||
storeCoverWithItem: this.storeCoverWithItem,
|
storeCoverWithItem: this.storeCoverWithItem,
|
||||||
preferAudioMetadata: this.preferAudioMetadata,
|
preferAudioMetadata: this.preferAudioMetadata,
|
||||||
preferOpfMetadata: this.preferOpfMetadata,
|
preferOpfMetadata: this.preferOpfMetadata,
|
||||||
preferMatchedMetadata: this.preferMatchedMetadata
|
preferMatchedMetadata: this.preferMatchedMetadata,
|
||||||
|
preferOverdriveMediaMarker: this.preferOverdriveMediaMarker
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,6 +49,7 @@ class ScanOptions {
|
|||||||
this.preferAudioMetadata = serverSettings.scannerPreferAudioMetadata
|
this.preferAudioMetadata = serverSettings.scannerPreferAudioMetadata
|
||||||
this.preferOpfMetadata = serverSettings.scannerPreferOpfMetadata
|
this.preferOpfMetadata = serverSettings.scannerPreferOpfMetadata
|
||||||
this.scannerPreferMatchedMetadata = serverSettings.scannerPreferMatchedMetadata
|
this.scannerPreferMatchedMetadata = serverSettings.scannerPreferMatchedMetadata
|
||||||
|
this.preferOverdriveMediaMarker = serverSettings.scannerPreferOverdriveMediaMarker
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = ScanOptions
|
module.exports = ScanOptions
|
||||||
@@ -80,7 +80,7 @@ class Scanner {
|
|||||||
// Scan all audio files
|
// Scan all audio files
|
||||||
if (libraryItem.hasAudioFiles) {
|
if (libraryItem.hasAudioFiles) {
|
||||||
var libraryAudioFiles = libraryItem.libraryFiles.filter(lf => lf.fileType === 'audio')
|
var libraryAudioFiles = libraryItem.libraryFiles.filter(lf => lf.fileType === 'audio')
|
||||||
if (await MediaFileScanner.scanMediaFiles(libraryAudioFiles, libraryItemData, libraryItem, this.db.serverSettings.scannerPreferAudioMetadata)) {
|
if (await MediaFileScanner.scanMediaFiles(libraryAudioFiles, libraryItemData, libraryItem, this.db.serverSettings.scannerPreferAudioMetadata, this.db.serverSettings.scannerPreferOverdriveMediaMarker)) {
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,7 +310,7 @@ class Scanner {
|
|||||||
|
|
||||||
async scanNewLibraryItemDataChunk(newLibraryItemsData, libraryScan) {
|
async scanNewLibraryItemDataChunk(newLibraryItemsData, libraryScan) {
|
||||||
var newLibraryItems = await Promise.all(newLibraryItemsData.map((lid) => {
|
var newLibraryItems = await Promise.all(newLibraryItemsData.map((lid) => {
|
||||||
return this.scanNewLibraryItem(lid, libraryScan.libraryMediaType, libraryScan.preferAudioMetadata, libraryScan.preferOpfMetadata, libraryScan.findCovers, libraryScan)
|
return this.scanNewLibraryItem(lid, libraryScan.libraryMediaType, libraryScan.preferAudioMetadata, libraryScan.preferOpfMetadata, libraryScan.findCovers, libraryScan.preferOverdriveMediaMarker, libraryScan)
|
||||||
}))
|
}))
|
||||||
newLibraryItems = newLibraryItems.filter(li => li) // Filter out nulls
|
newLibraryItems = newLibraryItems.filter(li => li) // Filter out nulls
|
||||||
|
|
||||||
@@ -337,7 +337,7 @@ class Scanner {
|
|||||||
// forceRescan all existing audio files - will probe and update ID3 tag metadata
|
// forceRescan all existing audio files - will probe and update ID3 tag metadata
|
||||||
var existingAudioFiles = existingLibraryFiles.filter(lf => lf.fileType === 'audio')
|
var existingAudioFiles = existingLibraryFiles.filter(lf => lf.fileType === 'audio')
|
||||||
if (libraryScan.scanOptions.forceRescan && existingAudioFiles.length) {
|
if (libraryScan.scanOptions.forceRescan && existingAudioFiles.length) {
|
||||||
if (await MediaFileScanner.scanMediaFiles(existingAudioFiles, scanData, libraryItem, libraryScan.preferAudioMetadata, libraryScan)) {
|
if (await MediaFileScanner.scanMediaFiles(existingAudioFiles, scanData, libraryItem, libraryScan.preferAudioMetadata, libraryScan.preferOverdriveMediaMarker, libraryScan)) {
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -345,7 +345,7 @@ class Scanner {
|
|||||||
var newAudioFiles = newLibraryFiles.filter(lf => lf.fileType === 'audio')
|
var newAudioFiles = newLibraryFiles.filter(lf => lf.fileType === 'audio')
|
||||||
var removedAudioFiles = filesRemoved.filter(lf => lf.fileType === 'audio')
|
var removedAudioFiles = filesRemoved.filter(lf => lf.fileType === 'audio')
|
||||||
if (newAudioFiles.length || removedAudioFiles.length) {
|
if (newAudioFiles.length || removedAudioFiles.length) {
|
||||||
if (await MediaFileScanner.scanMediaFiles(newAudioFiles, scanData, libraryItem, libraryScan.preferAudioMetadata, libraryScan)) {
|
if (await MediaFileScanner.scanMediaFiles(newAudioFiles, scanData, libraryItem, libraryScan.preferAudioMetadata, libraryScan.preferOverdriveMediaMarker, libraryScan)) {
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -379,7 +379,7 @@ class Scanner {
|
|||||||
return hasUpdated ? libraryItem : null
|
return hasUpdated ? libraryItem : null
|
||||||
}
|
}
|
||||||
|
|
||||||
async scanNewLibraryItem(libraryItemData, libraryMediaType, preferAudioMetadata, preferOpfMetadata, findCovers, libraryScan = null) {
|
async scanNewLibraryItem(libraryItemData, libraryMediaType, preferAudioMetadata, preferOpfMetadata, findCovers, preferOverdriveMediaMarker, libraryScan = null) {
|
||||||
if (libraryScan) libraryScan.addLog(LogLevel.DEBUG, `Scanning new library item "${libraryItemData.path}"`)
|
if (libraryScan) libraryScan.addLog(LogLevel.DEBUG, `Scanning new library item "${libraryItemData.path}"`)
|
||||||
else Logger.debug(`[Scanner] Scanning new item "${libraryItemData.path}"`)
|
else Logger.debug(`[Scanner] Scanning new item "${libraryItemData.path}"`)
|
||||||
|
|
||||||
@@ -388,7 +388,7 @@ class Scanner {
|
|||||||
|
|
||||||
var mediaFiles = libraryItemData.libraryFiles.filter(lf => lf.fileType === 'audio' || lf.fileType === 'video')
|
var mediaFiles = libraryItemData.libraryFiles.filter(lf => lf.fileType === 'audio' || lf.fileType === 'video')
|
||||||
if (mediaFiles.length) {
|
if (mediaFiles.length) {
|
||||||
await MediaFileScanner.scanMediaFiles(mediaFiles, libraryItemData, libraryItem, preferAudioMetadata, libraryScan)
|
await MediaFileScanner.scanMediaFiles(mediaFiles, libraryItemData, libraryItem, preferAudioMetadata, preferOverdriveMediaMarker, libraryScan)
|
||||||
}
|
}
|
||||||
|
|
||||||
await libraryItem.syncFiles(preferOpfMetadata)
|
await libraryItem.syncFiles(preferOpfMetadata)
|
||||||
@@ -608,7 +608,7 @@ class Scanner {
|
|||||||
var libraryItemData = await getLibraryItemFileData(libraryMediaType, folder, fullPath, isSingleMediaItem, this.db.serverSettings)
|
var libraryItemData = await getLibraryItemFileData(libraryMediaType, folder, fullPath, isSingleMediaItem, this.db.serverSettings)
|
||||||
if (!libraryItemData) return null
|
if (!libraryItemData) return null
|
||||||
var serverSettings = this.db.serverSettings
|
var serverSettings = this.db.serverSettings
|
||||||
return this.scanNewLibraryItem(libraryItemData, libraryMediaType, serverSettings.scannerPreferAudioMetadata, serverSettings.scannerPreferOpfMetadata, serverSettings.scannerFindCovers)
|
return this.scanNewLibraryItem(libraryItemData, libraryMediaType, serverSettings.scannerPreferAudioMetadata, serverSettings.scannerPreferOpfMetadata, serverSettings.scannerFindCovers, serverSettings.scannerPreferOverdriveMediaMarker)
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchForCover(libraryItem, libraryScan = null) {
|
async searchForCover(libraryItem, libraryScan = null) {
|
||||||
|
|||||||
@@ -273,8 +273,8 @@ function parseAbMetadataText(text, mediaType) {
|
|||||||
} else if (!metadataMapper[keyValue[0].trim()]) {
|
} else if (!metadataMapper[keyValue[0].trim()]) {
|
||||||
Logger.warn(`abmetadata key "${keyValue[0].trim()}" is not a valid ${mediaType} metadata key`)
|
Logger.warn(`abmetadata key "${keyValue[0].trim()}" is not a valid ${mediaType} metadata key`)
|
||||||
} else {
|
} else {
|
||||||
var key = keyValue[0].trim()
|
var key = keyValue.shift().trim()
|
||||||
var value = keyValue[1].trim()
|
var value = keyValue.join('=').trim()
|
||||||
mediaMetadataDetails[key] = metadataMapper[key].from(value)
|
mediaMetadataDetails[key] = metadataMapper[key].from(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
const Logger = require('../../Logger')
|
||||||
|
|
||||||
|
// given a list of audio files, extract all of the Overdrive Media Markers metaTags, and return an array of them as XML
|
||||||
|
function extractOverdriveMediaMarkers(includedAudioFiles) {
|
||||||
|
Logger.debug('[parseOverdriveMediaMarkers] Extracting overdrive media markers')
|
||||||
|
var markers = includedAudioFiles.map((af) => af.metaTags.tagOverdriveMediaMarker).filter(notUndefined => notUndefined !== undefined).filter(elem => { return elem !== null }) || []
|
||||||
|
|
||||||
|
return markers
|
||||||
|
}
|
||||||
|
|
||||||
|
// given the array of Overdrive Media Markers from generateOverdriveMediaMarkers()
|
||||||
|
// parse and clean them in to something a bit more usable
|
||||||
|
function cleanOverdriveMediaMarkers(overdriveMediaMarkers) {
|
||||||
|
Logger.debug('[parseOverdriveMediaMarkers] Cleaning up overdrive media markers')
|
||||||
|
/*
|
||||||
|
returns an array of arrays of objects. Each inner array corresponds to an audio track, with it's objects being a chapter:
|
||||||
|
[
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"Name": "Chapter 1",
|
||||||
|
"Time": "0:00.000"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Chapter 2",
|
||||||
|
"Time": "15:51.000"
|
||||||
|
},
|
||||||
|
{ etc }
|
||||||
|
]
|
||||||
|
]
|
||||||
|
*/
|
||||||
|
|
||||||
|
var parseString = require('xml2js').parseString; // function to convert xml to JSON
|
||||||
|
var parsedOverdriveMediaMarkers = []
|
||||||
|
|
||||||
|
overdriveMediaMarkers.forEach(function (item, index) {
|
||||||
|
var parsed_result
|
||||||
|
parseString(item, function (err, result) {
|
||||||
|
/*
|
||||||
|
result.Markers.Marker is the result of parsing the XML for the MediaMarker tags for the MP3 file (Part##.mp3)
|
||||||
|
it is shaped like this, and needs further cleaning below:
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"Name": [
|
||||||
|
"Chapter 1: "
|
||||||
|
],
|
||||||
|
"Time": [
|
||||||
|
"0:00.000"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ANOTHER CHAPTER
|
||||||
|
},
|
||||||
|
]
|
||||||
|
*/
|
||||||
|
|
||||||
|
// The values for Name and Time in results.Markers.Marker are returned as Arrays from parseString and should be strings
|
||||||
|
parsed_result = objectValuesArrayToString(result.Markers.Marker)
|
||||||
|
})
|
||||||
|
|
||||||
|
parsedOverdriveMediaMarkers.push(parsed_result)
|
||||||
|
})
|
||||||
|
|
||||||
|
return removeExtraChapters(parsedOverdriveMediaMarkers)
|
||||||
|
}
|
||||||
|
|
||||||
|
// given an array of objects, convert any values that are arrays to strings
|
||||||
|
function objectValuesArrayToString(arrayOfObjects) {
|
||||||
|
Logger.debug('[parseOverdriveMediaMarkers] Converting Marker object values from arrays to strings')
|
||||||
|
arrayOfObjects.forEach((item) => {
|
||||||
|
Object.keys(item).forEach(key => {
|
||||||
|
item[key] = item[key].toString()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return arrayOfObjects
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overdrive sometimes has weird chapters and subchapters defined
|
||||||
|
// These aren't necessary, so lets remove them
|
||||||
|
function removeExtraChapters(parsedOverdriveMediaMarkers) {
|
||||||
|
Logger.debug('[parseOverdriveMediaMarkers] Removing any unnecessary chapters')
|
||||||
|
const weirdChapterFilterRegex = /([(]\d|[cC]ontinued)/
|
||||||
|
var cleaned = []
|
||||||
|
parsedOverdriveMediaMarkers.forEach(function (item) {
|
||||||
|
cleaned.push(item.filter(chapter => !weirdChapterFilterRegex.test(chapter.Name)))
|
||||||
|
})
|
||||||
|
|
||||||
|
return cleaned
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given a set of chapters from generateParsedChapters, add the end time to each one
|
||||||
|
function addChapterEndTimes(chapters, totalAudioDuration) {
|
||||||
|
Logger.debug('[parseOverdriveMediaMarkers] Adding chapter end times')
|
||||||
|
chapters.forEach((chapter, chapter_index) => {
|
||||||
|
if (chapter_index < chapters.length - 1) {
|
||||||
|
chapter.end = chapters[chapter_index + 1].start
|
||||||
|
} else {
|
||||||
|
chapter.end = totalAudioDuration
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return chapters
|
||||||
|
}
|
||||||
|
|
||||||
|
// The function that actually generates the Chapters object that we update ABS with
|
||||||
|
function generateParsedChapters(includedAudioFiles, cleanedOverdriveMediaMarkers) {
|
||||||
|
Logger.debug('[parseOverdriveMediaMarkers] Generating new chapters for ABS')
|
||||||
|
// logic ported over from benonymity's OverdriveChapterizer:
|
||||||
|
// https://github.com/benonymity/OverdriveChapterizer/blob/main/chapters.py
|
||||||
|
var parsedChapters = []
|
||||||
|
var length = 0.0
|
||||||
|
var index = 0
|
||||||
|
var time = 0.0
|
||||||
|
|
||||||
|
// cleanedOverdriveMediaMarkers is an array of array of objects, where the inner array matches to the included audio files tracks
|
||||||
|
// this allows us to leverage the individual track durations when calculating the start times of chapters in tracks after the first (using length)
|
||||||
|
includedAudioFiles.forEach((track, track_index) => {
|
||||||
|
cleanedOverdriveMediaMarkers[track_index].forEach((chapter) => {
|
||||||
|
time = chapter.Time.split(":")
|
||||||
|
time = length + parseFloat(time[0]) * 60 + parseFloat(time[1])
|
||||||
|
var newChapterData = {
|
||||||
|
id: index++,
|
||||||
|
start: time,
|
||||||
|
title: chapter.Name
|
||||||
|
}
|
||||||
|
parsedChapters.push(newChapterData)
|
||||||
|
})
|
||||||
|
length += track.duration
|
||||||
|
})
|
||||||
|
|
||||||
|
parsedChapters = addChapterEndTimes(parsedChapters, length) // we need all the start times sorted out before we can add the end times
|
||||||
|
|
||||||
|
return parsedChapters
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.overdriveMediaMarkersExist = (includedAudioFiles) => {
|
||||||
|
return extractOverdriveMediaMarkers(includedAudioFiles).length > 1
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.parseOverdriveMediaMarkersAsChapters = (includedAudioFiles) => {
|
||||||
|
Logger.info('[parseOverdriveMediaMarkers] Parsing of Overdrive Media Markers started')
|
||||||
|
|
||||||
|
var overdriveMediaMarkers = extractOverdriveMediaMarkers(includedAudioFiles)
|
||||||
|
var cleanedOverdriveMediaMarkers = cleanOverdriveMediaMarkers(overdriveMediaMarkers)
|
||||||
|
var parsedChapters = generateParsedChapters(includedAudioFiles, cleanedOverdriveMediaMarkers)
|
||||||
|
|
||||||
|
return parsedChapters
|
||||||
|
}
|
||||||
@@ -192,6 +192,7 @@ function parseTags(format, verbose) {
|
|||||||
file_tag_movement: tryGrabTags(format, 'movement', 'mvin'),
|
file_tag_movement: tryGrabTags(format, 'movement', 'mvin'),
|
||||||
file_tag_genre1: tryGrabTags(format, 'tmp_genre1', 'genre1'),
|
file_tag_genre1: tryGrabTags(format, 'tmp_genre1', 'genre1'),
|
||||||
file_tag_genre2: tryGrabTags(format, 'tmp_genre2', 'genre2'),
|
file_tag_genre2: tryGrabTags(format, 'tmp_genre2', 'genre2'),
|
||||||
|
file_tag_overdrive_media_marker: tryGrabTags(format, 'OverDrive MediaMarkers'),
|
||||||
}
|
}
|
||||||
for (const key in tags) {
|
for (const key in tags) {
|
||||||
if (!tags[key]) {
|
if (!tags[key]) {
|
||||||
|
|||||||
Reference in New Issue
Block a user