mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-01-04 05:30:26 -05:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d38e9499db | ||
|
|
c7429efe95 | ||
|
|
b925dbcc95 | ||
|
|
2a235b8324 | ||
|
|
06cc2a1b21 | ||
|
|
4bcca97b1f | ||
|
|
313b9026f1 | ||
|
|
139ee013a7 | ||
|
|
7e5ab477b2 | ||
|
|
eba37c46cb | ||
|
|
228d9cc301 | ||
|
|
85946dd1d5 | ||
|
|
b40598593d | ||
|
|
e918a46d09 | ||
|
|
8061ee29d5 | ||
|
|
e15e04f085 | ||
|
|
958d68ffa9 |
4
.devcontainer/Dockerfile
Normal file
4
.devcontainer/Dockerfile
Normal file
@@ -0,0 +1,4 @@
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:16
|
||||
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
&& apt-get install ffmpeg gnupg2 -y
|
||||
ENV NODE_ENV=development
|
||||
12
.devcontainer/devcontainer.json
Normal file
12
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"build": { "dockerfile": "Dockerfile" },
|
||||
"mounts": [
|
||||
"source=abs-node-node_modules,target=${containerWorkspaceFolder}/node_modules,type=volume"
|
||||
],
|
||||
"features": {
|
||||
"fish": "latest"
|
||||
},
|
||||
"extensions": [
|
||||
"eamodio.gitlens"
|
||||
]
|
||||
}
|
||||
@@ -2,49 +2,11 @@
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
FFMPEG_INSTALL_DIR="/usr/lib/audiobookshelf-ffmpeg/"
|
||||
ABS_LOG_DIR="/var/log/audiobookshelf"
|
||||
|
||||
declare -r init_type='auto'
|
||||
declare -ri no_rebuild='0'
|
||||
|
||||
add_user() {
|
||||
: "${1:?'User was not defined'}"
|
||||
declare -r user="$1"
|
||||
declare -r uid="$2"
|
||||
|
||||
if [ -z "$uid" ]; then
|
||||
declare -r uid_flags=""
|
||||
else
|
||||
declare -r uid_flags="--uid $uid"
|
||||
fi
|
||||
|
||||
declare -r group="${3:-$user}"
|
||||
declare -r descr="${4:-No description}"
|
||||
declare -r shell="${5:-/bin/false}"
|
||||
|
||||
if ! getent passwd | grep -q "^$user:"; then
|
||||
echo "Creating system user: $user in $group with $descr and shell $shell"
|
||||
useradd $uid_flags --gid $group --no-create-home --system --shell $shell -c "$descr" $user
|
||||
fi
|
||||
}
|
||||
|
||||
add_group() {
|
||||
: "${1:?'Group was not defined'}"
|
||||
declare -r group="$1"
|
||||
declare -r gid="$2"
|
||||
|
||||
if [ -z "$gid" ]; then
|
||||
declare -r gid_flags=""
|
||||
else
|
||||
declare -r gid_flags="--gid $gid"
|
||||
fi
|
||||
|
||||
if ! getent group | grep -q "^$group:" ; then
|
||||
echo "Creating system group: $group"
|
||||
groupadd $gid_flags --system $group
|
||||
fi
|
||||
}
|
||||
|
||||
start_service () {
|
||||
: "${1:?'Service name was not defined'}"
|
||||
declare -r service_name="$1"
|
||||
@@ -76,13 +38,10 @@ start_service () {
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
add_group 'audiobookshelf' ''
|
||||
add_user 'audiobookshelf' '' 'audiobookshelf' 'audiobookshelf user-daemon' '/bin/false'
|
||||
|
||||
mkdir -p '/var/log/audiobookshelf'
|
||||
chown -R 'audiobookshelf:audiobookshelf' '/var/log/audiobookshelf'
|
||||
chown -R 'audiobookshelf:audiobookshelf' '/usr/share/audiobookshelf'
|
||||
chown -R 'audiobookshelf:audiobookshelf' "$FFMPEG_INSTALL_DIR"
|
||||
# Create log directory if not there and set ownership
|
||||
if [ ! -d "$ABS_LOG_DIR" ]; then
|
||||
mkdir -p "$ABS_LOG_DIR"
|
||||
chown -R 'audiobookshelf:audiobookshelf' "$ABS_LOG_DIR"
|
||||
fi
|
||||
|
||||
start_service 'audiobookshelf'
|
||||
|
||||
@@ -2,12 +2,51 @@
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
FFMPEG_INSTALL_DIR="/usr/lib/audiobookshelf-ffmpeg/"
|
||||
DEFAULT_DATA_PATH="/usr/share/audiobookshelf"
|
||||
FFMPEG_INSTALL_DIR="/usr/lib/audiobookshelf-ffmpeg"
|
||||
DEFAULT_DATA_DIR="/usr/share/audiobookshelf"
|
||||
CONFIG_PATH="/etc/default/audiobookshelf"
|
||||
DEFAULT_PORT=7331
|
||||
DEFAULT_HOST="0.0.0.0"
|
||||
|
||||
CONFIG_PATH="/etc/default/audiobookshelf"
|
||||
|
||||
|
||||
add_user() {
|
||||
: "${1:?'User was not defined'}"
|
||||
declare -r user="$1"
|
||||
declare -r uid="$2"
|
||||
|
||||
if [ -z "$uid" ]; then
|
||||
declare -r uid_flags=""
|
||||
else
|
||||
declare -r uid_flags="--uid $uid"
|
||||
fi
|
||||
|
||||
declare -r group="${3:-$user}"
|
||||
declare -r descr="${4:-No description}"
|
||||
declare -r shell="${5:-/bin/false}"
|
||||
|
||||
if ! getent passwd | grep -q "^$user:"; then
|
||||
echo "Creating system user: $user in $group with $descr and shell $shell"
|
||||
useradd $uid_flags --gid $group --no-create-home --system --shell $shell -c "$descr" $user
|
||||
fi
|
||||
}
|
||||
|
||||
add_group() {
|
||||
: "${1:?'Group was not defined'}"
|
||||
declare -r group="$1"
|
||||
declare -r gid="$2"
|
||||
|
||||
if [ -z "$gid" ]; then
|
||||
declare -r gid_flags=""
|
||||
else
|
||||
declare -r gid_flags="--gid $gid"
|
||||
fi
|
||||
|
||||
if ! getent group | grep -q "^$group:" ; then
|
||||
echo "Creating system group: $group"
|
||||
groupadd $gid_flags --system $group
|
||||
fi
|
||||
}
|
||||
|
||||
install_ffmpeg() {
|
||||
echo "Starting FFMPEG Install"
|
||||
@@ -15,8 +54,9 @@ install_ffmpeg() {
|
||||
WGET="wget https://johnvansickle.com/ffmpeg/builds/ffmpeg-git-amd64-static.tar.xz"
|
||||
|
||||
if ! cd "$FFMPEG_INSTALL_DIR"; then
|
||||
echo "WARNING: can't access working directory ($FFMPEG_INSTALL_DIR) creating it" >&2
|
||||
echo "Creating ffmpeg install dir at $FFMPEG_INSTALL_DIR"
|
||||
mkdir "$FFMPEG_INSTALL_DIR"
|
||||
chown -R 'audiobookshelf:audiobookshelf' "$FFMPEG_INSTALL_DIR"
|
||||
cd "$FFMPEG_INSTALL_DIR"
|
||||
fi
|
||||
|
||||
@@ -27,73 +67,23 @@ install_ffmpeg() {
|
||||
echo "Good to go on Ffmpeg... hopefully"
|
||||
}
|
||||
|
||||
should_build_config() {
|
||||
if [ -f "$CONFIG_PATH" ]; then
|
||||
echo "You already have a config file. Do you want to use it?"
|
||||
|
||||
options=("Yes" "No")
|
||||
select yn in "${options[@]}"
|
||||
do
|
||||
case $yn in
|
||||
"Yes")
|
||||
false; return
|
||||
;;
|
||||
"No")
|
||||
true; return
|
||||
;;
|
||||
esac
|
||||
done
|
||||
else
|
||||
echo "No existing config found in $CONFIG_PATH"
|
||||
true; return
|
||||
fi
|
||||
}
|
||||
|
||||
setup_config_interactive() {
|
||||
if should_build_config; then
|
||||
echo "Okay, let's setup a new config."
|
||||
|
||||
DATA_PATH=""
|
||||
read -p "
|
||||
Enter path for data files, i.e. streams, downloads, database [Default: $DEFAULT_DATA_PATH]:" DATA_PATH
|
||||
|
||||
if [[ -z "$DATA_PATH" ]]; then
|
||||
DATA_PATH="$DEFAULT_DATA_PATH"
|
||||
fi
|
||||
|
||||
PORT=""
|
||||
read -p "
|
||||
Port for the web ui [Default: $DEFAULT_PORT]:" PORT
|
||||
|
||||
if [[ -z "$PORT" ]]; then
|
||||
PORT="$DEFAULT_PORT"
|
||||
fi
|
||||
|
||||
config_text="METADATA_PATH=$DATA_PATH/metadata
|
||||
CONFIG_PATH=$DATA_PATH/config
|
||||
FFMPEG_PATH=/usr/lib/audiobookshelf-ffmpeg/ffmpeg
|
||||
FFPROBE_PATH=/usr/lib/audiobookshelf-ffmpeg/ffprobe
|
||||
PORT=$PORT
|
||||
HOST=$DEFAULT_HOST"
|
||||
|
||||
echo "$config_text"
|
||||
|
||||
echo "$config_text" > /etc/default/audiobookshelf;
|
||||
|
||||
echo "Config created"
|
||||
|
||||
fi
|
||||
}
|
||||
|
||||
setup_config() {
|
||||
if [ -f "$CONFIG_PATH" ]; then
|
||||
echo "Existing config found."
|
||||
cat $CONFIG_PATH
|
||||
else
|
||||
|
||||
if [ ! -d "$DEFAULT_DATA_DIR" ]; then
|
||||
# Create directory and set permissions
|
||||
echo "Creating default data dir at $DEFAULT_DATA_DIR"
|
||||
mkdir "$DEFAULT_DATA_DIR"
|
||||
chown -R 'audiobookshelf:audiobookshelf' "$DEFAULT_DATA_DIR"
|
||||
fi
|
||||
|
||||
echo "Creating default config."
|
||||
|
||||
config_text="METADATA_PATH=$DEFAULT_DATA_PATH/metadata
|
||||
CONFIG_PATH=$DEFAULT_DATA_PATH/config
|
||||
config_text="METADATA_PATH=$DEFAULT_DATA_DIR/metadata
|
||||
CONFIG_PATH=$DEFAULT_DATA_DIR/config
|
||||
FFMPEG_PATH=/usr/lib/audiobookshelf-ffmpeg/ffmpeg
|
||||
FFPROBE_PATH=/usr/lib/audiobookshelf-ffmpeg/ffprobe
|
||||
PORT=$DEFAULT_PORT
|
||||
@@ -107,6 +97,10 @@ setup_config() {
|
||||
fi
|
||||
}
|
||||
|
||||
add_group 'audiobookshelf' ''
|
||||
|
||||
add_user 'audiobookshelf' '' 'audiobookshelf' 'audiobookshelf user-daemon' '/bin/false'
|
||||
|
||||
setup_config
|
||||
|
||||
install_ffmpeg
|
||||
|
||||
@@ -190,7 +190,15 @@ export default {
|
||||
},
|
||||
settingsUpdated(settings) {},
|
||||
scan() {
|
||||
this.$store.dispatch('libraries/requestLibraryScan', { libraryId: this.$store.state.libraries.currentLibraryId })
|
||||
this.$store
|
||||
.dispatch('libraries/requestLibraryScan', { libraryId: this.$store.state.libraries.currentLibraryId })
|
||||
.then(() => {
|
||||
this.$toast.success('Library scan started')
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to start scan', error)
|
||||
this.$toast.error('Failed to start scan')
|
||||
})
|
||||
},
|
||||
libraryItemAdded(libraryItem) {
|
||||
console.log('libraryItem added', libraryItem)
|
||||
|
||||
@@ -43,6 +43,8 @@ export default {
|
||||
mixins: [bookshelfCardsHelpers],
|
||||
data() {
|
||||
return {
|
||||
routeName: null,
|
||||
routeFullPath: null,
|
||||
initialized: false,
|
||||
bookshelfHeight: 0,
|
||||
bookshelfWidth: 0,
|
||||
@@ -413,6 +415,8 @@ export default {
|
||||
if (newSearchParams !== this.currentSFQueryString || newSearchParams !== currentQueryString) {
|
||||
let newurl = window.location.protocol + '//' + window.location.host + window.location.pathname + '?' + newSearchParams
|
||||
window.history.replaceState({ path: newurl }, '', newurl)
|
||||
|
||||
this.routeFullPath = window.location.pathname + (window.location.search || '') // Update for saving scroll position
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -530,6 +534,15 @@ export default {
|
||||
await this.fetchEntites(0)
|
||||
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
|
||||
this.mountEntites(0, lastBookIndex)
|
||||
|
||||
// Set last scroll position for this bookshelf page
|
||||
if (this.$store.state.lastBookshelfScrollData[this.page] && window.bookshelf) {
|
||||
const { path, scrollTop } = this.$store.state.lastBookshelfScrollData[this.page]
|
||||
if (path === this.routeFullPath) {
|
||||
// Exact path match with query so use scroll position
|
||||
window.bookshelf.scrollTop = scrollTop
|
||||
}
|
||||
}
|
||||
},
|
||||
executeRebuild() {
|
||||
clearTimeout(this.resizeTimeout)
|
||||
@@ -605,13 +618,26 @@ export default {
|
||||
}
|
||||
},
|
||||
scan() {
|
||||
this.$store.dispatch('libraries/requestLibraryScan', { libraryId: this.currentLibraryId })
|
||||
this.$store
|
||||
.dispatch('libraries/requestLibraryScan', { libraryId: this.currentLibraryId })
|
||||
.then(() => {
|
||||
this.$toast.success('Library scan started')
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to start scan', error)
|
||||
this.$toast.error('Failed to start scan')
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initListeners()
|
||||
|
||||
this.routeName = this.$route.name // beforeDestroy will have the new route name already, so need to store this
|
||||
this.routeFullPath = window.location.pathname + (window.location.search || '')
|
||||
},
|
||||
updated() {
|
||||
this.routeFullPath = window.location.pathname + (window.location.search || '')
|
||||
|
||||
setTimeout(() => {
|
||||
if (window.innerWidth > 0 && window.innerWidth !== this.mountWindowWidth) {
|
||||
console.log('Updated window width', window.innerWidth, 'from', this.mountWindowWidth)
|
||||
@@ -622,6 +648,11 @@ export default {
|
||||
beforeDestroy() {
|
||||
this.destroyEntityComponents()
|
||||
this.removeListeners()
|
||||
|
||||
// Set bookshelf scroll position for specific bookshelf page and query
|
||||
if (window.bookshelf) {
|
||||
this.$store.commit('setLastBookshelfScrollData', { scrollTop: window.bookshelf.scrollTop || 0, path: this.routeFullPath, name: this.page })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -148,6 +148,7 @@ export default {
|
||||
setPlaying(isPlaying) {
|
||||
this.isPlaying = isPlaying
|
||||
this.$store.commit('setIsPlaying', isPlaying)
|
||||
this.updateMediaSessionPlaybackState()
|
||||
},
|
||||
setSleepTimer(seconds) {
|
||||
this.sleepTimerSet = true
|
||||
@@ -240,6 +241,71 @@ export default {
|
||||
this.playerHandler.closePlayer()
|
||||
this.$store.commit('setMediaPlaying', null)
|
||||
},
|
||||
mediaSessionPlay() {
|
||||
console.log('Media session play')
|
||||
this.playerHandler.play()
|
||||
},
|
||||
mediaSessionPause() {
|
||||
console.log('Media session pause')
|
||||
this.playerHandler.pause()
|
||||
},
|
||||
mediaSessionStop() {
|
||||
console.log('Media session stop')
|
||||
this.playerHandler.pause()
|
||||
},
|
||||
mediaSessionSeekBackward() {
|
||||
console.log('Media session seek backward')
|
||||
this.playerHandler.jumpBackward()
|
||||
},
|
||||
mediaSessionSeekForward() {
|
||||
console.log('Media session seek forward')
|
||||
this.playerHandler.jumpForward()
|
||||
},
|
||||
mediaSessionSeekTo(e) {
|
||||
console.log('Media session seek to', e)
|
||||
if (e.seekTime !== null && !isNaN(e.seekTime)) {
|
||||
this.playerHandler.seek(e.seekTime)
|
||||
}
|
||||
},
|
||||
updateMediaSessionPlaybackState() {
|
||||
if ('mediaSession' in navigator) {
|
||||
navigator.mediaSession.playbackState = this.isPlaying ? 'playing' : 'paused'
|
||||
}
|
||||
},
|
||||
setMediaSession() {
|
||||
if (!this.streamLibraryItem) {
|
||||
console.error('setMediaSession: No library item set')
|
||||
return
|
||||
}
|
||||
|
||||
if ('mediaSession' in navigator) {
|
||||
var coverImageSrc = this.$store.getters['globals/getLibraryItemCoverSrc'](this.streamLibraryItem, '/Logo.png')
|
||||
const artwork = [
|
||||
{
|
||||
src: coverImageSrc
|
||||
}
|
||||
]
|
||||
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
title: this.title,
|
||||
artist: this.playerHandler.displayAuthor || this.mediaMetadata.authorName || 'Unknown',
|
||||
album: this.mediaMetadata.seriesName || '',
|
||||
artwork
|
||||
})
|
||||
console.log('Set media session metadata', navigator.mediaSession.metadata)
|
||||
|
||||
navigator.mediaSession.setActionHandler('play', this.mediaSessionPlay)
|
||||
navigator.mediaSession.setActionHandler('pause', this.mediaSessionPause)
|
||||
navigator.mediaSession.setActionHandler('stop', this.mediaSessionStop)
|
||||
navigator.mediaSession.setActionHandler('seekbackward', this.mediaSessionSeekBackward)
|
||||
navigator.mediaSession.setActionHandler('seekforward', this.mediaSessionSeekForward)
|
||||
navigator.mediaSession.setActionHandler('seekto', this.mediaSessionSeekTo)
|
||||
// navigator.mediaSession.setActionHandler('previoustrack')
|
||||
// navigator.mediaSession.setActionHandler('nexttrack')
|
||||
} else {
|
||||
console.warn('Media session not available')
|
||||
}
|
||||
},
|
||||
streamProgress(data) {
|
||||
if (!data.numSegments) return
|
||||
var chunks = data.chunks
|
||||
@@ -312,7 +378,6 @@ export default {
|
||||
libraryItem,
|
||||
episodeId
|
||||
})
|
||||
|
||||
this.playerHandler.load(libraryItem, episodeId, true, this.initialPlaybackRate)
|
||||
},
|
||||
pauseItem() {
|
||||
|
||||
@@ -122,7 +122,7 @@ export default {
|
||||
|
||||
console.log('Payload', payload)
|
||||
this.$axios
|
||||
.$post(`/api/podcasts/${this.libraryItemId}/open-feed`, payload)
|
||||
.$post(`/api/items/${this.libraryItemId}/open-feed`, payload)
|
||||
.then((data) => {
|
||||
if (data.success) {
|
||||
console.log('Opened RSS Feed', data)
|
||||
@@ -143,7 +143,7 @@ export default {
|
||||
closeFeed() {
|
||||
this.processing = true
|
||||
this.$axios
|
||||
.$post(`/api/podcasts/${this.libraryItem.id}/close-feed`)
|
||||
.$post(`/api/items/${this.libraryItem.id}/close-feed`)
|
||||
.then(() => {
|
||||
this.$toast.success('RSS Feed Closed')
|
||||
this.show = false
|
||||
|
||||
@@ -73,10 +73,28 @@ export default {
|
||||
this.$emit('edit', this.library)
|
||||
},
|
||||
scan() {
|
||||
this.$store.dispatch('libraries/requestLibraryScan', { libraryId: this.library.id })
|
||||
this.$store
|
||||
.dispatch('libraries/requestLibraryScan', { libraryId: this.library.id })
|
||||
.then(() => {
|
||||
this.$toast.success('Library scan started')
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to start scan', error)
|
||||
this.$toast.error('Failed to start scan')
|
||||
})
|
||||
},
|
||||
forceScan() {
|
||||
this.$store.dispatch('libraries/requestLibraryScan', { libraryId: this.library.id, force: 1 })
|
||||
if (confirm(`Force Re-Scan will scan all files again like a fresh scan. Audio file ID3 tags, OPF files, and text files will be probed/parsed to be used for the library item.\n\nAre you sure you want to force re-scan?`)) {
|
||||
this.$store
|
||||
.dispatch('libraries/requestLibraryScan', { libraryId: this.library.id, force: 1 })
|
||||
.then(() => {
|
||||
this.$toast.success('Library scan started')
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to start scan', error)
|
||||
this.$toast.error('Failed to start scan')
|
||||
})
|
||||
}
|
||||
},
|
||||
deleteClick() {
|
||||
if (this.isMain) return
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="absolute w-32 bg-bg rounded-md border border-black-200 shadow-lg z-50" v-click-outside="clickOutsideObj" style="top: 0; left: 0">
|
||||
<div class="absolute w-36 bg-bg rounded-md border border-black-200 shadow-lg z-50" v-click-outside="clickOutsideObj" style="top: 0; left: 0">
|
||||
<template v-for="(item, index) in items">
|
||||
<div :key="index" class="flex h-7 items-center px-2 hover:bg-white hover:bg-opacity-5 text-white text-xs cursor-pointer" @click="clickAction(item.func)">
|
||||
<p>{{ item.text }}</p>
|
||||
|
||||
@@ -572,4 +572,11 @@ export default {
|
||||
max-width: calc(100% - 80px);
|
||||
margin-left: 80px;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
#app-content.has-siderail {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin-left: 0px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
4
client/package-lock.json
generated
4
client/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.0.13",
|
||||
"version": "2.0.14",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.0.13",
|
||||
"version": "2.0.14",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@nuxtjs/axios": "^5.13.6",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.0.14",
|
||||
"version": "2.0.15",
|
||||
"description": "Self-hosted audiobook and podcast client",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -23,13 +23,17 @@
|
||||
|
||||
<div class="py-4">
|
||||
<widgets-item-slider :items="libraryItems" :bookshelf-view="$constants.BookshelfView.AUTHOR">
|
||||
<h2 class="text-lg">{{ libraryItems.length }} Books</h2>
|
||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(author.id)}`" class="hover:underline">
|
||||
<h2 class="text-lg">{{ libraryItems.length }} Books</h2>
|
||||
</nuxt-link>
|
||||
</widgets-item-slider>
|
||||
</div>
|
||||
|
||||
<div v-for="series in authorSeries" :key="series.id" class="py-4">
|
||||
<widgets-item-slider :items="series.items" :bookshelf-view="$constants.BookshelfView.AUTHOR">
|
||||
<h2 class="text-lg">{{ series.name }}</h2>
|
||||
<nuxt-link :to="`/library/${currentLibraryId}/series/${series.id}`" class="hover:underline">
|
||||
<h2 class="text-lg">{{ series.name }}</h2>
|
||||
</nuxt-link>
|
||||
<p class="text-white text-opacity-40 text-base px-2">Series</p>
|
||||
</widgets-item-slider>
|
||||
</div>
|
||||
|
||||
@@ -383,10 +383,10 @@ export default {
|
||||
return this.$store.getters['user/getUserCanDownload']
|
||||
},
|
||||
showRssFeedBtn() {
|
||||
if (!this.rssFeedUrl && !this.podcastEpisodes.length) return false // Cannot open RSS feed with no episodes
|
||||
if (!this.rssFeedUrl && !this.podcastEpisodes.length && !this.tracks.length) return false // Cannot open RSS feed with no episodes/tracks
|
||||
|
||||
// If rss feed is open then show feed url to users otherwise just show to admins
|
||||
return this.isPodcast && (this.userIsAdminOrUp || this.rssFeedUrl)
|
||||
return this.userIsAdminOrUp || this.rssFeedUrl
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -179,6 +179,9 @@ export default class PlayerHandler {
|
||||
}
|
||||
|
||||
this.player.set(this.libraryItem, audioTracks, this.isHlsTranscode, this.startTime, this.playWhenReady)
|
||||
|
||||
// browser media session api
|
||||
this.ctx.setMediaSession()
|
||||
}
|
||||
|
||||
closePlayer() {
|
||||
|
||||
@@ -19,7 +19,8 @@ export const state = () => ({
|
||||
backups: [],
|
||||
bookshelfBookIds: [],
|
||||
openModal: null,
|
||||
selectedBookshelfTexture: '/textures/wood_default.jpg'
|
||||
selectedBookshelfTexture: '/textures/wood_default.jpg',
|
||||
lastBookshelfScrollData: {}
|
||||
})
|
||||
|
||||
export const getters = {
|
||||
@@ -80,6 +81,9 @@ export const actions = {
|
||||
}
|
||||
|
||||
export const mutations = {
|
||||
setLastBookshelfScrollData(state, { scrollTop, path, name }) {
|
||||
state.lastBookshelfScrollData[name] = { scrollTop, path }
|
||||
},
|
||||
setBookshelfBookIds(state, val) {
|
||||
state.bookshelfBookIds = val || []
|
||||
},
|
||||
|
||||
@@ -47,12 +47,7 @@ export const getters = {
|
||||
|
||||
export const actions = {
|
||||
requestLibraryScan({ state, commit }, { libraryId, force }) {
|
||||
this.$axios.$get(`/api/libraries/${libraryId}/scan`, { params: { force } }).then(() => {
|
||||
this.$toast.success('Library scan started')
|
||||
}).catch((error) => {
|
||||
console.error('Failed to start scan', error)
|
||||
this.$toast.error('Failed to start scan')
|
||||
})
|
||||
return this.$axios.$get(`/api/libraries/${libraryId}/scan`, { params: { force } })
|
||||
},
|
||||
loadFolders({ state, commit }) {
|
||||
if (state.folders.length) {
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.0.13",
|
||||
"version": "2.0.14",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.0.13",
|
||||
"version": "2.0.14",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"archiver": "^5.3.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.0.14",
|
||||
"version": "2.0.15",
|
||||
"description": "Self-hosted audiobook and podcast server",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -8,7 +8,7 @@
|
||||
"start": "node index.js",
|
||||
"client": "cd client && npm install && npm run generate",
|
||||
"prod": "npm run client && npm install && node prod.js",
|
||||
"build-win": "pkg -t node16-win-x64 -o ./dist/win/audiobookshelf .",
|
||||
"build-win": "pkg -t node16-win-x64 -o ./dist/win/audiobookshelf -C GZip .",
|
||||
"build-linux": "build/linuxpackager",
|
||||
"docker": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 --push . -t advplyr/audiobookshelf",
|
||||
"deploy": "node dist/autodeploy"
|
||||
|
||||
@@ -58,8 +58,6 @@ Available using Test Flight: https://testflight.apple.com/join/wiic7QIW - [Join
|
||||
|
||||
# Installation
|
||||
|
||||
** Default username is "root" with no password
|
||||
|
||||
### Docker Install
|
||||
Available in Unraid Community Apps
|
||||
|
||||
|
||||
@@ -86,13 +86,17 @@ class LibraryController {
|
||||
return f
|
||||
})
|
||||
for (var path of newFolderPaths) {
|
||||
var success = await fs.ensureDir(path).then(() => true).catch((error) => {
|
||||
Logger.error(`[LibraryController] Failed to ensure folder dir "${path}"`, error)
|
||||
return false
|
||||
})
|
||||
if (!success) {
|
||||
return res.status(400).send(`Invalid folder directory "${path}"`)
|
||||
} else {
|
||||
var pathExists = await fs.pathExists(path)
|
||||
if (!pathExists) {
|
||||
// Ensure dir will recursively create directories which might be preferred over mkdir
|
||||
var success = await fs.ensureDir(path).then(() => true).catch((error) => {
|
||||
Logger.error(`[LibraryController] Failed to ensure folder dir "${path}"`, error)
|
||||
return false
|
||||
})
|
||||
if (!success) {
|
||||
return res.status(400).send(`Invalid folder directory "${path}"`)
|
||||
}
|
||||
// Set permissions on newly created path
|
||||
await filePerms.setDefault(path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,6 +405,38 @@ class LibraryItemController {
|
||||
})
|
||||
}
|
||||
|
||||
// POST: api/items/:id/open-feed
|
||||
async openRSSFeed(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[LibraryItemController] Non-admin user attempted to open RSS feed`, req.user.username)
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
|
||||
const feedData = this.rssFeedManager.openFeedForItem(req.user, req.libraryItem, req.body)
|
||||
if (feedData.error) {
|
||||
return res.json({
|
||||
success: false,
|
||||
error: feedData.error
|
||||
})
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
feedUrl: feedData.feedUrl
|
||||
})
|
||||
}
|
||||
|
||||
async closeRSSFeed(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[LibraryItemController] Non-admin user attempted to close RSS feed`, req.user.username)
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
|
||||
this.rssFeedManager.closeFeedForItem(req.params.id)
|
||||
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
middleware(req, res, next) {
|
||||
var item = this.db.libraryItems.find(li => li.id === req.params.id)
|
||||
if (!item || !item.media) return res.sendStatus(404)
|
||||
|
||||
@@ -173,37 +173,6 @@ class PodcastController {
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
async openPodcastFeed(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[PodcastController] Non-admin user attempted to open podcast feed`, req.user.username)
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
|
||||
const feedData = this.rssFeedManager.openPodcastFeed(req.user, req.libraryItem, req.body)
|
||||
if (feedData.error) {
|
||||
return res.json({
|
||||
success: false,
|
||||
error: feedData.error
|
||||
})
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
feedUrl: feedData.feedUrl
|
||||
})
|
||||
}
|
||||
|
||||
async closePodcastFeed(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[PodcastController] Non-admin user attempted to close podcast feed`, req.user.username)
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
|
||||
this.rssFeedManager.closePodcastFeedForItem(req.params.id)
|
||||
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
async updateEpisode(req, res) {
|
||||
var libraryItem = req.libraryItem
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const Path = require('path')
|
||||
const fs = require('fs-extra')
|
||||
const date = require('date-and-time')
|
||||
const { Podcast } = require('podcast')
|
||||
const { getId } = require('../utils/index')
|
||||
const Logger = require('../Logger')
|
||||
|
||||
// Not functional at the moment
|
||||
@@ -60,36 +60,72 @@ class RssFeedManager {
|
||||
}
|
||||
|
||||
openFeed(userId, slug, libraryItem, serverAddress) {
|
||||
const podcast = libraryItem.media
|
||||
const media = libraryItem.media
|
||||
const mediaMetadata = media.metadata
|
||||
const isPodcast = libraryItem.mediaType === 'podcast'
|
||||
|
||||
const feedUrl = `${serverAddress}/feed/${slug}`
|
||||
// Removed Podcast npm package and ip package
|
||||
const author = isPodcast ? mediaMetadata.author : mediaMetadata.authorName
|
||||
|
||||
const feed = new Podcast({
|
||||
title: podcast.metadata.title,
|
||||
description: podcast.metadata.description,
|
||||
title: mediaMetadata.title,
|
||||
description: mediaMetadata.description,
|
||||
feedUrl,
|
||||
siteUrl: serverAddress,
|
||||
imageUrl: podcast.coverPath ? `${serverAddress}/feed/${slug}/cover` : `${serverAddress}/Logo.png`,
|
||||
author: podcast.metadata.author || 'advplyr',
|
||||
siteUrl: `${serverAddress}/items/${libraryItem.id}`,
|
||||
imageUrl: media.coverPath ? `${serverAddress}/feed/${slug}/cover` : `${serverAddress}/Logo.png`,
|
||||
author: author || 'advplyr',
|
||||
language: 'en'
|
||||
})
|
||||
podcast.episodes.forEach((episode) => {
|
||||
var contentUrl = episode.audioTrack.contentUrl.replace(/\\/g, '/')
|
||||
contentUrl = contentUrl.replace(`/s/item/${libraryItem.id}`, `/feed/${slug}/item`)
|
||||
|
||||
feed.addItem({
|
||||
title: episode.title,
|
||||
description: episode.description || '',
|
||||
enclosure: {
|
||||
if (isPodcast) { // PODCAST EPISODES
|
||||
media.episodes.forEach((episode) => {
|
||||
var contentUrl = episode.audioTrack.contentUrl.replace(/\\/g, '/')
|
||||
contentUrl = contentUrl.replace(`/s/item/${libraryItem.id}`, `/feed/${slug}/item`)
|
||||
|
||||
feed.addItem({
|
||||
title: episode.title,
|
||||
description: episode.description || '',
|
||||
enclosure: {
|
||||
url: `${serverAddress}${contentUrl}`,
|
||||
type: episode.audioTrack.mimeType,
|
||||
size: episode.size
|
||||
},
|
||||
date: episode.pubDate || '',
|
||||
url: `${serverAddress}${contentUrl}`,
|
||||
type: episode.audioTrack.mimeType,
|
||||
size: episode.size
|
||||
},
|
||||
date: episode.pubDate || '',
|
||||
url: `${serverAddress}${contentUrl}`,
|
||||
author: podcast.metadata.author || 'advplyr'
|
||||
author: author || 'advplyr'
|
||||
})
|
||||
})
|
||||
})
|
||||
} else { // AUDIOBOOK EPISODES
|
||||
|
||||
// Example: <pubDate>Fri, 04 Feb 2015 00:00:00 GMT</pubDate>
|
||||
const audiobookPubDate = date.format(new Date(libraryItem.addedAt), 'ddd, DD MMM YYYY HH:mm:ss [GMT]')
|
||||
|
||||
media.tracks.forEach((audioTrack) => {
|
||||
var contentUrl = audioTrack.contentUrl.replace(/\\/g, '/')
|
||||
contentUrl = contentUrl.replace(`/s/item/${libraryItem.id}`, `/feed/${slug}/item`)
|
||||
|
||||
var title = audioTrack.title
|
||||
if (media.chapters.length) {
|
||||
// If audio track start and chapter start are within 1 seconds of eachother then use the chapter title
|
||||
var matchingChapter = media.chapters.find(ch => Math.abs(ch.start - audioTrack.startOffset) < 1)
|
||||
if (matchingChapter && matchingChapter.title) title = matchingChapter.title
|
||||
}
|
||||
|
||||
feed.addItem({
|
||||
title,
|
||||
description: '',
|
||||
enclosure: {
|
||||
url: `${serverAddress}${contentUrl}`,
|
||||
type: audioTrack.mimeType,
|
||||
size: audioTrack.metadata.size
|
||||
},
|
||||
date: audiobookPubDate,
|
||||
url: `${serverAddress}${contentUrl}`,
|
||||
author: author || 'advplyr'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const feedData = {
|
||||
id: slug,
|
||||
@@ -97,7 +133,7 @@ class RssFeedManager {
|
||||
userId,
|
||||
libraryItemId: libraryItem.id,
|
||||
libraryItemPath: libraryItem.path,
|
||||
mediaCoverPath: podcast.coverPath,
|
||||
mediaCoverPath: media.coverPath,
|
||||
serverAddress: serverAddress,
|
||||
feedUrl,
|
||||
feed
|
||||
@@ -106,7 +142,7 @@ class RssFeedManager {
|
||||
return feedData
|
||||
}
|
||||
|
||||
openPodcastFeed(user, libraryItem, options) {
|
||||
openFeedForItem(user, libraryItem, options) {
|
||||
const serverAddress = options.serverAddress
|
||||
const slug = options.slug
|
||||
|
||||
@@ -118,12 +154,12 @@ class RssFeedManager {
|
||||
}
|
||||
|
||||
const feedData = this.openFeed(user.id, slug, libraryItem, serverAddress)
|
||||
Logger.debug(`[RssFeedManager] Opened podcast feed ${feedData.feedUrl}`)
|
||||
Logger.debug(`[RssFeedManager] Opened RSS feed ${feedData.feedUrl}`)
|
||||
this.emitter('rss_feed_open', { libraryItemId: libraryItem.id, feedUrl: feedData.feedUrl })
|
||||
return feedData
|
||||
}
|
||||
|
||||
closePodcastFeedForItem(libraryItemId) {
|
||||
closeFeedForItem(libraryItemId) {
|
||||
var feed = this.findFeedForItem(libraryItemId)
|
||||
if (!feed) return
|
||||
this.closeRssFeed(feed.id)
|
||||
|
||||
@@ -225,6 +225,7 @@ class Book {
|
||||
// Look for desc.txt, reader.txt, metadata.abs and opf file then update details if found
|
||||
async syncMetadataFiles(textMetadataFiles, opfMetadataOverrideDetails) {
|
||||
var metadataUpdatePayload = {}
|
||||
var tagsUpdated = false
|
||||
|
||||
var descTxt = textMetadataFiles.find(lf => lf.metadata.filename === 'desc.txt')
|
||||
if (descTxt) {
|
||||
@@ -264,8 +265,13 @@ class Book {
|
||||
var opfMetadata = await parseOpfMetadataXML(xmlText)
|
||||
if (opfMetadata) {
|
||||
for (const key in opfMetadata) {
|
||||
// Add genres only if genres are empty
|
||||
if (key === 'genres') {
|
||||
|
||||
if (key === 'tags') { // Add tags only if tags are empty
|
||||
if (opfMetadata.tags.length && (!this.tags.length || opfMetadataOverrideDetails)) {
|
||||
this.tags = opfMetadata.tags
|
||||
tagsUpdated = true
|
||||
}
|
||||
} else if (key === 'genres') { // Add genres only if genres are empty
|
||||
if (opfMetadata.genres.length && (!this.metadata.genres.length || opfMetadataOverrideDetails)) {
|
||||
metadataUpdatePayload[key] = opfMetadata.genres
|
||||
}
|
||||
@@ -290,9 +296,9 @@ class Book {
|
||||
}
|
||||
|
||||
if (Object.keys(metadataUpdatePayload).length) {
|
||||
return this.metadata.update(metadataUpdatePayload)
|
||||
return this.metadata.update(metadataUpdatePayload) || tagsUpdated
|
||||
}
|
||||
return false
|
||||
return tagsUpdated
|
||||
}
|
||||
|
||||
searchQuery(query) {
|
||||
|
||||
@@ -95,6 +95,8 @@ class ApiRouter {
|
||||
this.router.get('/items/:id/scan', LibraryItemController.middleware.bind(this), LibraryItemController.scan.bind(this))
|
||||
this.router.get('/items/:id/audio-metadata', LibraryItemController.middleware.bind(this), LibraryItemController.updateAudioFileMetadata.bind(this))
|
||||
this.router.post('/items/:id/chapters', LibraryItemController.middleware.bind(this), LibraryItemController.updateMediaChapters.bind(this))
|
||||
this.router.post('/items/:id/open-feed', LibraryItemController.middleware.bind(this), LibraryItemController.openRSSFeed.bind(this))
|
||||
this.router.post('/items/:id/close-feed', LibraryItemController.middleware.bind(this), LibraryItemController.closeRSSFeed.bind(this))
|
||||
|
||||
this.router.post('/items/batch/delete', LibraryItemController.batchDelete.bind(this))
|
||||
this.router.post('/items/batch/update', LibraryItemController.batchUpdate.bind(this))
|
||||
@@ -186,8 +188,6 @@ class ApiRouter {
|
||||
this.router.get('/podcasts/:id/downloads', PodcastController.middleware.bind(this), PodcastController.getEpisodeDownloads.bind(this))
|
||||
this.router.get('/podcasts/:id/clear-queue', PodcastController.middleware.bind(this), PodcastController.clearEpisodeDownloadQueue.bind(this))
|
||||
this.router.post('/podcasts/:id/download-episodes', PodcastController.middleware.bind(this), PodcastController.downloadEpisodes.bind(this))
|
||||
this.router.post('/podcasts/:id/open-feed', PodcastController.middleware.bind(this), PodcastController.openPodcastFeed.bind(this))
|
||||
this.router.post('/podcasts/:id/close-feed', PodcastController.middleware.bind(this), PodcastController.closePodcastFeed.bind(this))
|
||||
this.router.patch('/podcasts/:id/episode/:episodeId', PodcastController.middleware.bind(this), PodcastController.updateEpisode.bind(this))
|
||||
|
||||
//
|
||||
|
||||
@@ -418,7 +418,7 @@ module.exports = {
|
||||
books: [libraryItemJson],
|
||||
inProgress: bookInProgress,
|
||||
bookInProgressLastUpdate: bookInProgress ? mediaProgress.lastUpdate : null,
|
||||
sequenceInProgress: bookInProgress ? libraryItemJson.seriesSequence : null
|
||||
firstBookUnread: bookInProgress ? libraryItemJson : null
|
||||
}
|
||||
seriesMap[librarySeries.id] = series
|
||||
|
||||
@@ -445,10 +445,18 @@ module.exports = {
|
||||
|
||||
if (bookInProgress) { // Update if this series is in progress
|
||||
seriesMap[librarySeries.id].inProgress = true
|
||||
if (!seriesMap[librarySeries.id].sequenceInProgress || (librarySeries.sequence && String(librarySeries.sequence).localeCompare(String(seriesMap[librarySeries.id].sequenceInProgress), undefined, { sensitivity: 'base', numeric: true }) > 0)) {
|
||||
seriesMap[librarySeries.id].sequenceInProgress = librarySeries.sequence
|
||||
|
||||
if (seriesMap[librarySeries.id].bookInProgressLastUpdate > mediaProgress.lastUpdate) {
|
||||
seriesMap[librarySeries.id].bookInProgressLastUpdate = mediaProgress.lastUpdate
|
||||
}
|
||||
} else if (!seriesMap[librarySeries.id].firstBookUnread) {
|
||||
seriesMap[librarySeries.id].firstBookUnread = libraryItemJson
|
||||
} else if (libraryItemJson.seriesSequence) {
|
||||
// If current firstBookUnread has a series sequence greater than this series sequence, then update firstBookUnread
|
||||
const firstBookUnreadSequence = seriesMap[librarySeries.id].firstBookUnread.seriesSequence
|
||||
if (!firstBookUnreadSequence || String(firstBookUnreadSequence).localeCompare(String(librarySeries.sequence), undefined, { sensitivity: 'base', numeric: true }) > 0) {
|
||||
seriesMap[librarySeries.id].firstBookUnread = libraryItemJson
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -545,11 +553,8 @@ module.exports = {
|
||||
if (seriesMap[seriesId].inProgress) {
|
||||
seriesMap[seriesId].books = naturalSort(seriesMap[seriesId].books).asc(li => li.seriesSequence)
|
||||
|
||||
const nextBookInSeries = seriesMap[seriesId].books.find(li => {
|
||||
if (!seriesMap[seriesId].sequenceInProgress) return true
|
||||
// True if book series sequence is greater than the current book sequence in progress
|
||||
return String(li.seriesSequence).localeCompare(String(seriesMap[seriesId].sequenceInProgress), undefined, { sensitivity: 'base', numeric: true }) > 0
|
||||
})
|
||||
// NEW implementation takes the first book unread with the smallest series sequence
|
||||
const nextBookInSeries = seriesMap[seriesId].firstBookUnread
|
||||
|
||||
if (nextBookInSeries) {
|
||||
const bookForContinueSeries = {
|
||||
|
||||
@@ -70,14 +70,14 @@ function fetchLanguage(metadata) {
|
||||
return fetchTagString(metadata, 'dc:language')
|
||||
}
|
||||
|
||||
function fetchSeries(metadata) {
|
||||
if (typeof metadata.meta == "undefined") return null
|
||||
return fetchTagString(metadata.meta, "calibre:series")
|
||||
function fetchSeries(metadataMeta) {
|
||||
if (!metadataMeta) return null
|
||||
return fetchTagString(metadataMeta, "calibre:series")
|
||||
}
|
||||
|
||||
function fetchVolumeNumber(metadata) {
|
||||
if (typeof metadata.meta == "undefined") return null
|
||||
return fetchTagString(metadata.meta, "calibre:series_index")
|
||||
function fetchVolumeNumber(metadataMeta) {
|
||||
if (!metadataMeta) return null
|
||||
return fetchTagString(metadataMeta, "calibre:series_index")
|
||||
}
|
||||
|
||||
function fetchNarrators(creators, metadata) {
|
||||
@@ -91,21 +91,42 @@ function fetchNarrators(creators, metadata) {
|
||||
}
|
||||
}
|
||||
|
||||
function fetchTags(metadata) {
|
||||
if (!metadata['dc:tag'] || !metadata['dc:tag'].length) return []
|
||||
return metadata['dc:tag'].filter(tag => (typeof tag === 'string'))
|
||||
}
|
||||
|
||||
function stripPrefix(str) {
|
||||
if (!str) return ''
|
||||
return str.split(':').pop()
|
||||
}
|
||||
|
||||
module.exports.parseOpfMetadataXML = async (xml) => {
|
||||
var json = await xmlToJSON(xml)
|
||||
if (!json || !json.package || !json.package.metadata) return null
|
||||
var metadata = json.package.metadata
|
||||
|
||||
if (!json) return null
|
||||
|
||||
// Handle <package ...> or with prefix <ns0:package ...>
|
||||
const packageKey = Object.keys(json).find(key => stripPrefix(key) === 'package')
|
||||
if (!packageKey) return null
|
||||
const prefix = packageKey.split(':').shift()
|
||||
var metadata = prefix ? json[packageKey][`${prefix}:metadata`] || json[packageKey].metadata : json[packageKey].metadata
|
||||
if (!metadata) return null
|
||||
|
||||
if (Array.isArray(metadata)) {
|
||||
if (!metadata.length) return null
|
||||
metadata = metadata[0]
|
||||
}
|
||||
|
||||
if (typeof metadata.meta != "undefined") {
|
||||
metadata.meta = {}
|
||||
for (var match of xml.matchAll(/<meta name="(?<name>.+)" content="(?<content>.+)"\/>/g)) {
|
||||
metadata.meta[match.groups['name']] = [match.groups['content']]
|
||||
}
|
||||
const metadataMeta = prefix ? metadata[`${prefix}:meta`] || metadata.meta : metadata.meta
|
||||
|
||||
metadata.meta = {}
|
||||
if (metadataMeta && metadataMeta.length) {
|
||||
metadataMeta.forEach((meta) => {
|
||||
if (meta && meta['$'] && meta['$'].name) {
|
||||
metadata.meta[meta['$'].name] = [meta['$'].content || '']
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var creators = parseCreators(metadata)
|
||||
@@ -119,8 +140,9 @@ module.exports.parseOpfMetadataXML = async (xml) => {
|
||||
description: fetchDescription(metadata),
|
||||
genres: fetchGenres(metadata),
|
||||
language: fetchLanguage(metadata),
|
||||
series: fetchSeries(metadata),
|
||||
sequence: fetchVolumeNumber(metadata)
|
||||
series: fetchSeries(metadata.meta),
|
||||
sequence: fetchVolumeNumber(metadata.meta),
|
||||
tags: fetchTags(metadata)
|
||||
}
|
||||
return data
|
||||
}
|
||||
Reference in New Issue
Block a user