Merge branch 'advplyr:master' into scan-for-narrator

This commit is contained in:
Cassie Esposito 2022-05-19 19:45:35 -07:00 committed by GitHub
commit efdb43e2d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 197 additions and 194 deletions

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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": {

View File

@ -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>

View File

@ -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: {

View File

@ -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": {

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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))
//

View File

@ -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 = {