mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-12-31 11:38:47 -05:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdece944f4 | ||
|
|
d7952dab04 | ||
|
|
bec599f325 | ||
|
|
affcc03c61 | ||
|
|
db18c71857 | ||
|
|
dcc223949a | ||
|
|
6a6d384d88 | ||
|
|
cd57667444 | ||
|
|
3900db14d3 | ||
|
|
1fa94cbfad | ||
|
|
793233e782 | ||
|
|
94012e5dff | ||
|
|
d440a9fd6a | ||
|
|
928c6cf5b3 | ||
|
|
23a25d420c | ||
|
|
dc779a3fc5 | ||
|
|
876badbeea | ||
|
|
8563bdde74 |
@@ -6,7 +6,7 @@ RUN npm ci && npm cache clean --force
|
||||
RUN npm run generate
|
||||
|
||||
### STAGE 1: Build server ###
|
||||
FROM sandreas/tone:v0.0.9 AS tone
|
||||
FROM sandreas/tone:v0.1.1 AS tone
|
||||
FROM node:16-alpine
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
@@ -50,7 +50,7 @@ install_ffmpeg() {
|
||||
echo "Starting FFMPEG Install"
|
||||
|
||||
WGET="wget https://johnvansickle.com/ffmpeg/builds/ffmpeg-git-amd64-static.tar.xz"
|
||||
WGET_TONE="wget https://github.com/sandreas/tone/releases/download/v0.0.9/tone-0.0.9-linux-x64.tar.gz"
|
||||
WGET_TONE="wget https://github.com/sandreas/tone/releases/download/v0.1.1/tone-0.1.1-linux-x64.tar.gz"
|
||||
|
||||
if ! cd "$FFMPEG_INSTALL_DIR"; then
|
||||
echo "Creating ffmpeg install dir at $FFMPEG_INSTALL_DIR"
|
||||
@@ -66,8 +66,8 @@ install_ffmpeg() {
|
||||
# Temp downloading tone library to the ffmpeg dir
|
||||
echo "Getting tone.."
|
||||
$WGET_TONE
|
||||
tar xvf tone-0.0.9-linux-x64.tar.gz --strip-components=1
|
||||
rm tone-0.0.9-linux-x64.tar.gz
|
||||
tar xvf tone-0.1.1-linux-x64.tar.gz --strip-components=1
|
||||
rm tone-0.1.1-linux-x64.tar.gz
|
||||
|
||||
echo "Good to go on Ffmpeg (& tone)... hopefully"
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ ExecStart=/usr/share/audiobookshelf/audiobookshelf
|
||||
ExecReload=/bin/kill -HUP $MAINPID
|
||||
Restart=always
|
||||
User=audiobookshelf
|
||||
Group=audiobookshelf
|
||||
PermissionsStartOnly=true
|
||||
|
||||
[Install]
|
||||
|
||||
@@ -231,8 +231,10 @@ export default {
|
||||
}
|
||||
},
|
||||
selectTab(tab) {
|
||||
if (this.selectedTab === tab) return
|
||||
if (this.availableTabs.find((t) => t.id === tab)) {
|
||||
this.selectedTab = tab
|
||||
this.processing = false
|
||||
}
|
||||
},
|
||||
libraryItemUpdated(expandedLibraryItem) {
|
||||
|
||||
@@ -125,7 +125,7 @@ export default {
|
||||
return this.$store.state.scanners.providers
|
||||
},
|
||||
searchTitleLabel() {
|
||||
if (this.provider == 'audible') return 'Search Title or ASIN'
|
||||
if (this.provider.startsWith('audible')) return 'Search Title or ASIN'
|
||||
else if (this.provider == 'itunes') return 'Search Term'
|
||||
return 'Search Title'
|
||||
},
|
||||
|
||||
@@ -258,7 +258,7 @@ export default {
|
||||
return this.$store.state.scanners.providers
|
||||
},
|
||||
searchTitleLabel() {
|
||||
if (this.provider == 'audible') return 'Search Title or ASIN'
|
||||
if (this.provider.startsWith('audible')) return 'Search Title or ASIN'
|
||||
else if (this.provider == 'itunes') return 'Search Term'
|
||||
return 'Search Title'
|
||||
},
|
||||
@@ -312,7 +312,7 @@ export default {
|
||||
this.isProcessing = true
|
||||
this.lastSearch = searchQuery
|
||||
var searchEntity = this.isPodcast ? 'podcast' : 'books'
|
||||
var results = await this.$axios.$get(`/api/search/${searchEntity}?${searchQuery}`, { timeout: 10000 }).catch((error) => {
|
||||
var results = await this.$axios.$get(`/api/search/${searchEntity}?${searchQuery}`, { timeout: 20000 }).catch((error) => {
|
||||
console.error('Failed', error)
|
||||
return []
|
||||
})
|
||||
|
||||
@@ -96,7 +96,9 @@ export default {
|
||||
if (itemRelPath.startsWith('/')) itemRelPath = itemRelPath.slice(1)
|
||||
var relPath = this.ebookFile.metadata.relPath
|
||||
if (relPath.startsWith('/')) relPath = relPath.slice(1)
|
||||
return `/ebook/${this.libraryId}/${this.folderId}/${itemRelPath}/${relPath}`
|
||||
|
||||
const relRelPath = this.$encodeUriPath(`${itemRelPath}/${relPath}`)
|
||||
return `/ebook/${this.libraryId}/${this.folderId}/${relRelPath}`
|
||||
},
|
||||
userToken() {
|
||||
return this.$store.getters['user/getToken']
|
||||
|
||||
4
client/package-lock.json
generated
4
client/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.2.0",
|
||||
"version": "2.2.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.1.5",
|
||||
"version": "2.2.1",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@nuxtjs/axios": "^5.13.6",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.2.0",
|
||||
"version": "2.2.1",
|
||||
"description": "Self-hosted audiobook and podcast client",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -18,11 +18,35 @@
|
||||
<p class="text-lg mb-4 font-semibold">Audiobook Chapters</p>
|
||||
<div class="flex-grow" />
|
||||
<ui-checkbox v-model="showSecondInputs" checkbox-bg="primary" small label-class="text-sm text-gray-200 pl-1" label="Show seconds" class="mx-2" />
|
||||
<div class="w-40" />
|
||||
</div>
|
||||
<div class="flex items-center mb-3 py-1">
|
||||
<div class="flex-grow" />
|
||||
<ui-btn v-if="newChapters.length > 1" :color="showShiftTimes ? 'bg' : 'primary'" small @click="showShiftTimes = !showShiftTimes">Shift Times</ui-btn>
|
||||
<ui-btn color="primary" small class="mx-2" @click="showFindChaptersModal = true">Lookup</ui-btn>
|
||||
<ui-btn color="success" small @click="saveChapters">Save</ui-btn>
|
||||
<div class="w-40" />
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden">
|
||||
<transition name="slide">
|
||||
<div v-if="showShiftTimes" class="flex mb-4">
|
||||
<div class="w-12"></div>
|
||||
<div class="flex-grow">
|
||||
<div class="flex items-center">
|
||||
<p class="text-sm mb-1 font-semibold pr-2">Time to shift in seconds</p>
|
||||
<ui-text-input v-model="shiftAmount" type="number" class="max-w-20" style="height: 30px" />
|
||||
<ui-btn color="primary" class="mx-1" small @click="shiftChapterTimes">Add</ui-btn>
|
||||
<div class="flex-grow" />
|
||||
<span class="material-icons text-gray-200 hover:text-white cursor-pointer" @click="showShiftTimes = false">close</span>
|
||||
</div>
|
||||
<p class="text-xs py-1.5 text-gray-300 max-w-md">Note: First chapter start time must remain at 0:00 and the last chapter start time cannot exceed this audiobooks duration.</p>
|
||||
</div>
|
||||
<div class="w-40"></div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<div class="flex text-xs uppercase text-gray-300 font-semibold mb-2">
|
||||
<div class="w-12"></div>
|
||||
<div class="w-32 px-2">Start</div>
|
||||
@@ -186,6 +210,8 @@ export default {
|
||||
return {
|
||||
newChapters: [],
|
||||
selectedChapter: null,
|
||||
showShiftTimes: false,
|
||||
shiftAmount: 0,
|
||||
audioEl: null,
|
||||
isPlayingChapter: false,
|
||||
isLoadingChapter: false,
|
||||
@@ -237,6 +263,32 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
shiftChapterTimes() {
|
||||
if (!this.shiftAmount || isNaN(this.shiftAmount) || this.newChapters.length <= 1) {
|
||||
return
|
||||
}
|
||||
|
||||
const amount = Number(this.shiftAmount)
|
||||
|
||||
const lastChapter = this.newChapters[this.newChapters.length - 1]
|
||||
if (lastChapter.start + amount > this.mediaDurationRounded) {
|
||||
this.$toast.error('Invalid shift amount. Last chapter start time would extend beyond the duration of this audiobook.')
|
||||
return
|
||||
}
|
||||
|
||||
if (this.newChapters[0].end + amount <= 0) {
|
||||
this.$toast.error('Invalid shift amount. First chapter would have zero or negative length.')
|
||||
return
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.newChapters.length; i++) {
|
||||
const chap = this.newChapters[i]
|
||||
chap.end = Math.min(chap.end + amount, this.mediaDuration)
|
||||
if (i > 0) {
|
||||
chap.start = Math.max(0, chap.start + amount)
|
||||
}
|
||||
}
|
||||
},
|
||||
editItem() {
|
||||
this.$store.commit('showEditModal', this.libraryItem)
|
||||
},
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
<script>
|
||||
export default {
|
||||
async asyncData({ store, app, params, redirect }) {
|
||||
const author = await app.$axios.$get(`/api/authors/${params.id}?include=items,series`).catch((error) => {
|
||||
const author = await app.$axios.$get(`/api/authors/${params.id}?library=${store.state.libraries.currentLibraryId}&include=items,series`).catch((error) => {
|
||||
console.error('Failed to get author', error)
|
||||
return null
|
||||
})
|
||||
|
||||
@@ -25,7 +25,9 @@
|
||||
<div class="flex items-center py-2">
|
||||
<ui-text-input type="number" v-model="backupsToKeep" no-spinner :disabled="updatingServerSettings" :padding-x="1" text-center class="w-10" @change="updateBackupsSettings" />
|
||||
|
||||
<p class="pl-4 text-lg">Number of backups to keep</p>
|
||||
<ui-tooltip :text="numBackupsToKeepTooltip">
|
||||
<p class="pl-4 text-lg">Number of backups to keep <span class="material-icons icon-text">info_outlined</span></p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2">
|
||||
@@ -53,7 +55,10 @@ export default {
|
||||
maxBackupSize: 1,
|
||||
cronExpression: '',
|
||||
newServerSettings: {},
|
||||
showCronBuilder: false
|
||||
showCronBuilder: false,
|
||||
backupsTooltip: 'Backups saved in /metadata/backups',
|
||||
numBackupsToKeepTooltip: 'Only 1 backup will be removed at a time so if you already have more backups than this you should manually remove them.',
|
||||
maxBackupSizeTooltip: 'As a safeguard against misconfiguration, backups will fail if they exceed the configured size.'
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -65,12 +70,6 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
backupsTooltip() {
|
||||
return 'Backups saved in /metadata/backups'
|
||||
},
|
||||
maxBackupSizeTooltip() {
|
||||
return 'As a safeguard against misconfiguration, backups will fail if they exceed the configured size.'
|
||||
},
|
||||
serverSettings() {
|
||||
return this.$store.state.serverSettings
|
||||
},
|
||||
|
||||
@@ -10,12 +10,48 @@ export const state = () => ({
|
||||
value: 'openlibrary'
|
||||
},
|
||||
{
|
||||
text: 'Audible',
|
||||
text: 'iTunes',
|
||||
value: 'itunes'
|
||||
},
|
||||
{
|
||||
text: 'Audible.com',
|
||||
value: 'audible'
|
||||
},
|
||||
{
|
||||
text: 'iTunes',
|
||||
value: 'itunes'
|
||||
text: 'Audible.ca',
|
||||
value: 'audible.ca'
|
||||
},
|
||||
{
|
||||
text: 'Audible.co.uk',
|
||||
value: 'audible.uk'
|
||||
},
|
||||
{
|
||||
text: 'Audible.co.au',
|
||||
value: 'audible.au'
|
||||
},
|
||||
{
|
||||
text: 'Audible.fr',
|
||||
value: 'audible.fr'
|
||||
},
|
||||
{
|
||||
text: 'Audible.de',
|
||||
value: 'audible.de'
|
||||
},
|
||||
{
|
||||
text: 'Audible.co.jp',
|
||||
value: 'audible.jp'
|
||||
},
|
||||
{
|
||||
text: 'Audible.it',
|
||||
value: 'audible.it'
|
||||
},
|
||||
{
|
||||
text: 'Audible.co.in',
|
||||
value: 'audible.in'
|
||||
},
|
||||
{
|
||||
text: 'Audible.es',
|
||||
value: 'audible.es'
|
||||
}
|
||||
],
|
||||
podcastProviders: [
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.2.0",
|
||||
"version": "2.2.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.1.5",
|
||||
"version": "2.2.1",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"axios": "^0.26.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.2.0",
|
||||
"version": "2.2.1",
|
||||
"description": "Self-hosted audiobook and podcast server",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -9,6 +9,7 @@ class AuthorController {
|
||||
constructor() { }
|
||||
|
||||
async findOne(req, res) {
|
||||
const libraryId = req.query.library
|
||||
const include = (req.query.include || '').split(',')
|
||||
|
||||
const authorJson = req.author.toJSON()
|
||||
@@ -16,6 +17,7 @@ class AuthorController {
|
||||
// Used on author landing page to include library items and items grouped in series
|
||||
if (include.includes('items')) {
|
||||
authorJson.libraryItems = this.db.libraryItems.filter(li => {
|
||||
if (libraryId && li.libraryId !== libraryId) return false
|
||||
if (!req.user.checkCanAccessLibraryItem(li)) return false // filter out library items user cannot access
|
||||
return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(req.author.id)
|
||||
})
|
||||
|
||||
@@ -150,8 +150,9 @@ class BookFinder {
|
||||
return this.iTunesApi.searchAudiobooks(title)
|
||||
}
|
||||
|
||||
async getAudibleResults(title, author, asin) {
|
||||
var books = await this.audible.search(title, author, asin);
|
||||
async getAudibleResults(title, author, asin, provider) {
|
||||
const region = provider.includes('.') ? provider.split('.').pop() : ''
|
||||
const books = await this.audible.search(title, author, asin, region)
|
||||
if (this.verbose) Logger.debug(`Audible Book Search Results: ${books.length || 0}`)
|
||||
if (!books) return []
|
||||
return books
|
||||
@@ -165,8 +166,8 @@ class BookFinder {
|
||||
|
||||
if (provider === 'google') {
|
||||
books = await this.getGoogleBooksResults(title, author)
|
||||
} else if (provider === 'audible') {
|
||||
books = await this.getAudibleResults(title, author, asin)
|
||||
} else if (provider.startsWith('audible')) {
|
||||
books = await this.getAudibleResults(title, author, asin, provider)
|
||||
} else if (provider === 'itunes') {
|
||||
books = await this.getiTunesAudiobooksResults(title, author)
|
||||
} else if (provider === 'openlibrary') {
|
||||
|
||||
@@ -22,7 +22,7 @@ class PodcastEpisodeDownload {
|
||||
toJSONForClient() {
|
||||
return {
|
||||
id: this.id,
|
||||
episodeDisplayTitle: this.podcastEpisode ? this.podcastEpisode.bestFilename : null,
|
||||
episodeDisplayTitle: this.podcastEpisode ? this.podcastEpisode.title : null,
|
||||
url: this.url,
|
||||
libraryItemId: this.libraryItem ? this.libraryItem.id : null,
|
||||
isDownloading: this.isDownloading,
|
||||
@@ -35,7 +35,7 @@ class PodcastEpisodeDownload {
|
||||
}
|
||||
|
||||
get targetFilename() {
|
||||
return sanitizeFilename(`${this.podcastEpisode.bestFilename}.mp3`)
|
||||
return sanitizeFilename(`${this.podcastEpisode.title}.mp3`)
|
||||
}
|
||||
get targetPath() {
|
||||
return Path.join(this.libraryItem.path, this.targetFilename)
|
||||
|
||||
@@ -103,10 +103,6 @@ class PodcastEpisode {
|
||||
return this.audioFile.duration
|
||||
}
|
||||
get size() { return this.audioFile.metadata.size }
|
||||
get bestFilename() {
|
||||
if (this.episode) return `${this.episode} - ${this.title}`
|
||||
return this.title
|
||||
}
|
||||
get enclosureUrl() {
|
||||
return this.enclosure ? this.enclosure.url : null
|
||||
}
|
||||
|
||||
@@ -408,54 +408,29 @@ class Book {
|
||||
setChapters(preferOverdriveMediaMarker = false) {
|
||||
// If 1 audio file without chapters, then no chapters will be set
|
||||
var includedAudioFiles = this.audioFiles.filter(af => !af.exclude)
|
||||
if (!includedAudioFiles.length) return
|
||||
|
||||
// If overdrive media markers are present and preferred, use those instead
|
||||
if (preferOverdriveMediaMarker) {
|
||||
var overdriveChapters = parseOverdriveMediaMarkersAsChapters(includedAudioFiles)
|
||||
if (overdriveChapters) {
|
||||
Logger.info('[Book] Overdrive Media Markers and preference found! Using these for chapter definitions')
|
||||
return this.chapters = overdriveChapters
|
||||
this.chapters = overdriveChapters
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (includedAudioFiles.length === 1) {
|
||||
// 1 audio file with chapters
|
||||
if (includedAudioFiles[0].chapters) {
|
||||
this.chapters = includedAudioFiles[0].chapters.map(c => ({ ...c }))
|
||||
}
|
||||
} else {
|
||||
// IF first audio file has embedded chapters then use embedded chapters
|
||||
if (includedAudioFiles[0].chapters && includedAudioFiles[0].chapters.length) {
|
||||
Logger.debug(`[Book] setChapters: Using embedded chapters in audio file ${includedAudioFiles[0].metadata.path}`)
|
||||
this.chapters = includedAudioFiles[0].chapters.map(c => ({ ...c }))
|
||||
} else if (includedAudioFiles.length > 1) {
|
||||
// Build chapters from audio files
|
||||
this.chapters = []
|
||||
var currChapterId = 0
|
||||
var currStartTime = 0
|
||||
includedAudioFiles.forEach((file) => {
|
||||
// If audio file has chapters use chapters
|
||||
if (file.chapters && file.chapters.length) {
|
||||
file.chapters.forEach((chapter) => {
|
||||
if (currStartTime > this.duration) {
|
||||
Logger.warn(`[Book] Invalid chapter start time > duration`)
|
||||
} else {
|
||||
var chapterAlreadyExists = this.chapters.find(ch => ch.start === currStartTime)
|
||||
if (!chapterAlreadyExists) {
|
||||
var chapterDuration = chapter.end - chapter.start
|
||||
if (chapterDuration > 0) {
|
||||
var title = `Chapter ${currChapterId}`
|
||||
if (chapter.title) {
|
||||
title += ` (${chapter.title})`
|
||||
}
|
||||
var endTime = Math.min(this.duration, currStartTime + chapterDuration)
|
||||
this.chapters.push({
|
||||
id: currChapterId++,
|
||||
start: currStartTime,
|
||||
end: endTime,
|
||||
title
|
||||
})
|
||||
currStartTime += chapterDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
} else if (file.duration) {
|
||||
// Otherwise just use track has chapter
|
||||
if (file.duration) {
|
||||
this.chapters.push({
|
||||
id: currChapterId++,
|
||||
start: currStartTime,
|
||||
|
||||
@@ -3,7 +3,20 @@ const htmlSanitizer = require('../utils/htmlSanitizer')
|
||||
const Logger = require('../Logger')
|
||||
|
||||
class Audible {
|
||||
constructor() { }
|
||||
constructor() {
|
||||
this.regionMap = {
|
||||
'us': '.com',
|
||||
'ca': '.ca',
|
||||
'uk': '.co.uk',
|
||||
'au': '.co.au',
|
||||
'fr': '.fr',
|
||||
'de': '.de',
|
||||
'jp': '.co.jp',
|
||||
'it': '.it',
|
||||
'in': '.co.in',
|
||||
'es': '.es'
|
||||
}
|
||||
}
|
||||
|
||||
cleanResult(item) {
|
||||
var { title, subtitle, asin, authors, narrators, publisherName, summary, releaseDate, image, genres, seriesPrimary, seriesSecondary, language, runtimeLengthMin } = item
|
||||
@@ -29,7 +42,9 @@ class Audible {
|
||||
tags: tagsFiltered.length ? tagsFiltered.join(', ') : null,
|
||||
series: series != [] ? series.map(({ name, position }) => ({ series: name, sequence: position })) : null,
|
||||
language: language ? language.charAt(0).toUpperCase() + language.slice(1) : null,
|
||||
duration: runtimeLengthMin && !isNaN(runtimeLengthMin) ? Number(runtimeLengthMin) : 0
|
||||
duration: runtimeLengthMin && !isNaN(runtimeLengthMin) ? Number(runtimeLengthMin) : 0,
|
||||
region: item.region || null,
|
||||
rating: item.rating || null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,9 +52,10 @@ class Audible {
|
||||
return /^[0-9A-Z]{10}$/.test(title)
|
||||
}
|
||||
|
||||
asinSearch(asin) {
|
||||
asinSearch(asin, region) {
|
||||
asin = encodeURIComponent(asin);
|
||||
var url = `https://api.audnex.us/books/${asin}`
|
||||
var regionQuery = region ? `?region=${region}` : ''
|
||||
var url = `https://api.audnex.us/books/${asin}${regionQuery}`
|
||||
Logger.debug(`[Audible] ASIN url: ${url}`)
|
||||
return axios.get(url).then((res) => {
|
||||
if (!res || !res.data || !res.data.asin) return null
|
||||
@@ -50,14 +66,19 @@ class Audible {
|
||||
})
|
||||
}
|
||||
|
||||
async search(title, author, asin) {
|
||||
async search(title, author, asin, region) {
|
||||
if (region && !this.regionMap[region]) {
|
||||
Logger.error(`[Audible] search: Invalid region ${region}`)
|
||||
region = ''
|
||||
}
|
||||
|
||||
var items
|
||||
if (asin) {
|
||||
items = [await this.asinSearch(asin)]
|
||||
items = [await this.asinSearch(asin, region)]
|
||||
}
|
||||
|
||||
if (!items && this.isProbablyAsin(title)) {
|
||||
items = [await this.asinSearch(title)]
|
||||
items = [await this.asinSearch(title, region)]
|
||||
}
|
||||
|
||||
if (!items) {
|
||||
@@ -65,14 +86,15 @@ class Audible {
|
||||
num_results: '10',
|
||||
products_sort_by: 'Relevance',
|
||||
title: title
|
||||
};
|
||||
}
|
||||
if (author) queryObj.author = author
|
||||
var queryString = (new URLSearchParams(queryObj)).toString();
|
||||
var url = `https://api.audible.com/1.0/catalog/products?${queryString}`
|
||||
const queryString = (new URLSearchParams(queryObj)).toString()
|
||||
const tld = region ? this.regionMap[region] : '.com'
|
||||
const url = `https://api.audible${tld}/1.0/catalog/products?${queryString}`
|
||||
Logger.debug(`[Audible] Search url: ${url}`)
|
||||
items = await axios.get(url).then((res) => {
|
||||
if (!res || !res.data || !res.data.products) return null
|
||||
return Promise.all(res.data.products.map(result => this.asinSearch(result.asin)))
|
||||
return Promise.all(res.data.products.map(result => this.asinSearch(result.asin, region)))
|
||||
}).catch(error => {
|
||||
Logger.error('[Audible] query search error', error)
|
||||
return []
|
||||
|
||||
@@ -35,7 +35,7 @@ class Audnexus {
|
||||
return {
|
||||
asin: author.asin,
|
||||
description: author.description,
|
||||
image: author.image,
|
||||
image: author.image || null,
|
||||
name: author.name
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,7 @@ class Audnexus {
|
||||
return {
|
||||
asin: author.asin,
|
||||
description: author.description,
|
||||
image: author.image,
|
||||
image: author.image || null,
|
||||
name: author.name
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,11 +60,15 @@ class iTunes {
|
||||
}
|
||||
|
||||
cleanAudiobook(data) {
|
||||
// artistName can be "Name1, Name2 & Name3" so we refactor this to "Name1, Name2, Name3"
|
||||
// see: https://github.com/advplyr/audiobookshelf/issues/1022
|
||||
const author = (data.artistName || '').split(' & ').join(', ')
|
||||
|
||||
return {
|
||||
id: data.collectionId,
|
||||
artistId: data.artistId,
|
||||
title: data.collectionName,
|
||||
author: data.artistName,
|
||||
author,
|
||||
description: htmlSanitizer.stripAllTags(data.description || ''),
|
||||
publishedYear: data.releaseDate ? data.releaseDate.split('-')[0] : null,
|
||||
genres: data.primaryGenreName ? [data.primaryGenreName] : null,
|
||||
|
||||
@@ -819,7 +819,7 @@ class Scanner {
|
||||
if ((!libraryItem.media.tags.length || options.overrideDetails)) {
|
||||
var tagsArray = []
|
||||
if (Array.isArray(matchData[key])) tagsArray = [...matchData[key]]
|
||||
else tagsArray = tagsArray[key].split(',').map(v => v.trim()).filter(v => !!v)
|
||||
else tagsArray = matchData[key].split(',').map(v => v.trim()).filter(v => !!v)
|
||||
updatePayload[key] = tagsArray
|
||||
}
|
||||
} else if ((!libraryItem.media.metadata[key] || options.overrideDetails)) {
|
||||
|
||||
@@ -168,7 +168,8 @@ module.exports.downloadFile = async (url, filepath) => {
|
||||
const response = await axios({
|
||||
url,
|
||||
method: 'GET',
|
||||
responseType: 'stream'
|
||||
responseType: 'stream',
|
||||
timeout: 30000
|
||||
})
|
||||
response.data.pipe(writer)
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
@@ -254,6 +254,7 @@ function getBookDataFromDir(folderPath, relPath, parseSubtitle = false) {
|
||||
path: Path.posix.join(folderPath, relPath) // i.e. /audiobook/Author Name/Book Name/..
|
||||
}
|
||||
}
|
||||
module.exports.getBookDataFromDir = getBookDataFromDir
|
||||
|
||||
function getNarrator(folder) {
|
||||
let pattern = /^(?<title>.*) \{(?<narrators>.*)\}$/
|
||||
@@ -277,16 +278,15 @@ function getSequence(folder) {
|
||||
// ]
|
||||
|
||||
// Matches a valid volume string. Also matches a book whose title starts with a 1 to 3 digit number. Will handle that later.
|
||||
let pattern = /^(?<volumeLabel>vol\.? |volume |book )?(?<sequence>\d{1,3}(?:\.\d{1,2})?)(?<trailingDot>\.?)(?: (?<suffix>.*))?$/i
|
||||
let pattern = /^(?<volumeLabel>vol\.? |volume |book )?(?<sequence>\d{0,3}(?:\.\d{1,2})?)(?<trailingDot>\.?)(?: (?<suffix>.*))?$/i
|
||||
|
||||
let volumeNumber = null
|
||||
let parts = folder.split(' - ')
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
let match = parts[i].match(pattern)
|
||||
|
||||
// This excludes '101 Dalmations' but includes '101. Dalmations'
|
||||
if (match && !(match.groups.suffix && !(match.groups.volumeLabel || match.groups.trailingDot))) {
|
||||
volumeNumber = match.groups.sequence
|
||||
volumeNumber = isNaN(match.groups.sequence) ? match.groups.sequence : Number(match.groups.sequence).toString()
|
||||
parts[i] = match.groups.suffix
|
||||
if (!parts[i]) { parts.splice(i, 1) }
|
||||
break
|
||||
@@ -338,7 +338,7 @@ function getDataFromMediaDir(libraryMediaType, folderPath, relPath, serverSettin
|
||||
var parseSubtitle = !!serverSettings.scannerParseSubtitle
|
||||
return getBookDataFromDir(folderPath, relPath, parseSubtitle)
|
||||
} else {
|
||||
return this.getPodcastDataFromDir(folderPath, relPath)
|
||||
return getPodcastDataFromDir(folderPath, relPath)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user