mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-04 01:40:40 +02: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 |
+1
-1
@@ -6,7 +6,7 @@ RUN npm ci && npm cache clean --force
|
|||||||
RUN npm run generate
|
RUN npm run generate
|
||||||
|
|
||||||
### STAGE 1: Build server ###
|
### STAGE 1: Build server ###
|
||||||
FROM sandreas/tone:v0.0.9 AS tone
|
FROM sandreas/tone:v0.1.1 AS tone
|
||||||
FROM node:16-alpine
|
FROM node:16-alpine
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ install_ffmpeg() {
|
|||||||
echo "Starting FFMPEG Install"
|
echo "Starting FFMPEG Install"
|
||||||
|
|
||||||
WGET="wget https://johnvansickle.com/ffmpeg/builds/ffmpeg-git-amd64-static.tar.xz"
|
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
|
if ! cd "$FFMPEG_INSTALL_DIR"; then
|
||||||
echo "Creating ffmpeg install dir at $FFMPEG_INSTALL_DIR"
|
echo "Creating ffmpeg install dir at $FFMPEG_INSTALL_DIR"
|
||||||
@@ -66,8 +66,8 @@ install_ffmpeg() {
|
|||||||
# Temp downloading tone library to the ffmpeg dir
|
# Temp downloading tone library to the ffmpeg dir
|
||||||
echo "Getting tone.."
|
echo "Getting tone.."
|
||||||
$WGET_TONE
|
$WGET_TONE
|
||||||
tar xvf tone-0.0.9-linux-x64.tar.gz --strip-components=1
|
tar xvf tone-0.1.1-linux-x64.tar.gz --strip-components=1
|
||||||
rm tone-0.0.9-linux-x64.tar.gz
|
rm tone-0.1.1-linux-x64.tar.gz
|
||||||
|
|
||||||
echo "Good to go on Ffmpeg (& tone)... hopefully"
|
echo "Good to go on Ffmpeg (& tone)... hopefully"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ ExecStart=/usr/share/audiobookshelf/audiobookshelf
|
|||||||
ExecReload=/bin/kill -HUP $MAINPID
|
ExecReload=/bin/kill -HUP $MAINPID
|
||||||
Restart=always
|
Restart=always
|
||||||
User=audiobookshelf
|
User=audiobookshelf
|
||||||
|
Group=audiobookshelf
|
||||||
PermissionsStartOnly=true
|
PermissionsStartOnly=true
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
|
|||||||
@@ -231,8 +231,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
selectTab(tab) {
|
selectTab(tab) {
|
||||||
|
if (this.selectedTab === tab) return
|
||||||
if (this.availableTabs.find((t) => t.id === tab)) {
|
if (this.availableTabs.find((t) => t.id === tab)) {
|
||||||
this.selectedTab = tab
|
this.selectedTab = tab
|
||||||
|
this.processing = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
libraryItemUpdated(expandedLibraryItem) {
|
libraryItemUpdated(expandedLibraryItem) {
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ export default {
|
|||||||
return this.$store.state.scanners.providers
|
return this.$store.state.scanners.providers
|
||||||
},
|
},
|
||||||
searchTitleLabel() {
|
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'
|
else if (this.provider == 'itunes') return 'Search Term'
|
||||||
return 'Search Title'
|
return 'Search Title'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ export default {
|
|||||||
return this.$store.state.scanners.providers
|
return this.$store.state.scanners.providers
|
||||||
},
|
},
|
||||||
searchTitleLabel() {
|
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'
|
else if (this.provider == 'itunes') return 'Search Term'
|
||||||
return 'Search Title'
|
return 'Search Title'
|
||||||
},
|
},
|
||||||
@@ -312,7 +312,7 @@ export default {
|
|||||||
this.isProcessing = true
|
this.isProcessing = true
|
||||||
this.lastSearch = searchQuery
|
this.lastSearch = searchQuery
|
||||||
var searchEntity = this.isPodcast ? 'podcast' : 'books'
|
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)
|
console.error('Failed', error)
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -96,7 +96,9 @@ export default {
|
|||||||
if (itemRelPath.startsWith('/')) itemRelPath = itemRelPath.slice(1)
|
if (itemRelPath.startsWith('/')) itemRelPath = itemRelPath.slice(1)
|
||||||
var relPath = this.ebookFile.metadata.relPath
|
var relPath = this.ebookFile.metadata.relPath
|
||||||
if (relPath.startsWith('/')) relPath = relPath.slice(1)
|
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() {
|
userToken() {
|
||||||
return this.$store.getters['user/getToken']
|
return this.$store.getters['user/getToken']
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.2.0",
|
"version": "2.2.1",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.1.5",
|
"version": "2.2.1",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxtjs/axios": "^5.13.6",
|
"@nuxtjs/axios": "^5.13.6",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.2.0",
|
"version": "2.2.1",
|
||||||
"description": "Self-hosted audiobook and podcast client",
|
"description": "Self-hosted audiobook and podcast client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -18,11 +18,35 @@
|
|||||||
<p class="text-lg mb-4 font-semibold">Audiobook Chapters</p>
|
<p class="text-lg mb-4 font-semibold">Audiobook Chapters</p>
|
||||||
<div class="flex-grow" />
|
<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" />
|
<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="primary" small class="mx-2" @click="showFindChaptersModal = true">Lookup</ui-btn>
|
||||||
<ui-btn color="success" small @click="saveChapters">Save</ui-btn>
|
<ui-btn color="success" small @click="saveChapters">Save</ui-btn>
|
||||||
<div class="w-40" />
|
<div class="w-40" />
|
||||||
</div>
|
</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="flex text-xs uppercase text-gray-300 font-semibold mb-2">
|
||||||
<div class="w-12"></div>
|
<div class="w-12"></div>
|
||||||
<div class="w-32 px-2">Start</div>
|
<div class="w-32 px-2">Start</div>
|
||||||
@@ -186,6 +210,8 @@ export default {
|
|||||||
return {
|
return {
|
||||||
newChapters: [],
|
newChapters: [],
|
||||||
selectedChapter: null,
|
selectedChapter: null,
|
||||||
|
showShiftTimes: false,
|
||||||
|
shiftAmount: 0,
|
||||||
audioEl: null,
|
audioEl: null,
|
||||||
isPlayingChapter: false,
|
isPlayingChapter: false,
|
||||||
isLoadingChapter: false,
|
isLoadingChapter: false,
|
||||||
@@ -237,6 +263,32 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
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() {
|
editItem() {
|
||||||
this.$store.commit('showEditModal', this.libraryItem)
|
this.$store.commit('showEditModal', this.libraryItem)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
async asyncData({ store, app, params, redirect }) {
|
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)
|
console.error('Failed to get author', error)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -25,7 +25,9 @@
|
|||||||
<div class="flex items-center py-2">
|
<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" />
|
<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>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
@@ -53,7 +55,10 @@ export default {
|
|||||||
maxBackupSize: 1,
|
maxBackupSize: 1,
|
||||||
cronExpression: '',
|
cronExpression: '',
|
||||||
newServerSettings: {},
|
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: {
|
watch: {
|
||||||
@@ -65,12 +70,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
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() {
|
serverSettings() {
|
||||||
return this.$store.state.serverSettings
|
return this.$store.state.serverSettings
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,12 +10,48 @@ export const state = () => ({
|
|||||||
value: 'openlibrary'
|
value: 'openlibrary'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Audible',
|
text: 'iTunes',
|
||||||
|
value: 'itunes'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Audible.com',
|
||||||
value: 'audible'
|
value: 'audible'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'iTunes',
|
text: 'Audible.ca',
|
||||||
value: 'itunes'
|
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: [
|
podcastProviders: [
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.2.0",
|
"version": "2.2.1",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.1.5",
|
"version": "2.2.1",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.26.1",
|
"axios": "^0.26.1",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.2.0",
|
"version": "2.2.1",
|
||||||
"description": "Self-hosted audiobook and podcast server",
|
"description": "Self-hosted audiobook and podcast server",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class AuthorController {
|
|||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
async findOne(req, res) {
|
async findOne(req, res) {
|
||||||
|
const libraryId = req.query.library
|
||||||
const include = (req.query.include || '').split(',')
|
const include = (req.query.include || '').split(',')
|
||||||
|
|
||||||
const authorJson = req.author.toJSON()
|
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
|
// Used on author landing page to include library items and items grouped in series
|
||||||
if (include.includes('items')) {
|
if (include.includes('items')) {
|
||||||
authorJson.libraryItems = this.db.libraryItems.filter(li => {
|
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
|
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)
|
return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(req.author.id)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -150,8 +150,9 @@ class BookFinder {
|
|||||||
return this.iTunesApi.searchAudiobooks(title)
|
return this.iTunesApi.searchAudiobooks(title)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAudibleResults(title, author, asin) {
|
async getAudibleResults(title, author, asin, provider) {
|
||||||
var books = await this.audible.search(title, author, asin);
|
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 (this.verbose) Logger.debug(`Audible Book Search Results: ${books.length || 0}`)
|
||||||
if (!books) return []
|
if (!books) return []
|
||||||
return books
|
return books
|
||||||
@@ -165,8 +166,8 @@ class BookFinder {
|
|||||||
|
|
||||||
if (provider === 'google') {
|
if (provider === 'google') {
|
||||||
books = await this.getGoogleBooksResults(title, author)
|
books = await this.getGoogleBooksResults(title, author)
|
||||||
} else if (provider === 'audible') {
|
} else if (provider.startsWith('audible')) {
|
||||||
books = await this.getAudibleResults(title, author, asin)
|
books = await this.getAudibleResults(title, author, asin, provider)
|
||||||
} else if (provider === 'itunes') {
|
} else if (provider === 'itunes') {
|
||||||
books = await this.getiTunesAudiobooksResults(title, author)
|
books = await this.getiTunesAudiobooksResults(title, author)
|
||||||
} else if (provider === 'openlibrary') {
|
} else if (provider === 'openlibrary') {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class PodcastEpisodeDownload {
|
|||||||
toJSONForClient() {
|
toJSONForClient() {
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
episodeDisplayTitle: this.podcastEpisode ? this.podcastEpisode.bestFilename : null,
|
episodeDisplayTitle: this.podcastEpisode ? this.podcastEpisode.title : null,
|
||||||
url: this.url,
|
url: this.url,
|
||||||
libraryItemId: this.libraryItem ? this.libraryItem.id : null,
|
libraryItemId: this.libraryItem ? this.libraryItem.id : null,
|
||||||
isDownloading: this.isDownloading,
|
isDownloading: this.isDownloading,
|
||||||
@@ -35,7 +35,7 @@ class PodcastEpisodeDownload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get targetFilename() {
|
get targetFilename() {
|
||||||
return sanitizeFilename(`${this.podcastEpisode.bestFilename}.mp3`)
|
return sanitizeFilename(`${this.podcastEpisode.title}.mp3`)
|
||||||
}
|
}
|
||||||
get targetPath() {
|
get targetPath() {
|
||||||
return Path.join(this.libraryItem.path, this.targetFilename)
|
return Path.join(this.libraryItem.path, this.targetFilename)
|
||||||
|
|||||||
@@ -103,10 +103,6 @@ class PodcastEpisode {
|
|||||||
return this.audioFile.duration
|
return this.audioFile.duration
|
||||||
}
|
}
|
||||||
get size() { return this.audioFile.metadata.size }
|
get size() { return this.audioFile.metadata.size }
|
||||||
get bestFilename() {
|
|
||||||
if (this.episode) return `${this.episode} - ${this.title}`
|
|
||||||
return this.title
|
|
||||||
}
|
|
||||||
get enclosureUrl() {
|
get enclosureUrl() {
|
||||||
return this.enclosure ? this.enclosure.url : null
|
return this.enclosure ? this.enclosure.url : null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -408,54 +408,29 @@ class Book {
|
|||||||
setChapters(preferOverdriveMediaMarker = false) {
|
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 (!includedAudioFiles.length) return
|
||||||
|
|
||||||
// If overdrive media markers are present and preferred, use those instead
|
// If overdrive media markers are present and preferred, use those instead
|
||||||
if (preferOverdriveMediaMarker) {
|
if (preferOverdriveMediaMarker) {
|
||||||
var overdriveChapters = parseOverdriveMediaMarkersAsChapters(includedAudioFiles)
|
var overdriveChapters = parseOverdriveMediaMarkersAsChapters(includedAudioFiles)
|
||||||
if (overdriveChapters) {
|
if (overdriveChapters) {
|
||||||
Logger.info('[Book] Overdrive Media Markers and preference found! Using these for chapter definitions')
|
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) {
|
// IF first audio file has embedded chapters then use embedded chapters
|
||||||
// 1 audio file with chapters
|
if (includedAudioFiles[0].chapters && includedAudioFiles[0].chapters.length) {
|
||||||
if (includedAudioFiles[0].chapters) {
|
Logger.debug(`[Book] setChapters: Using embedded chapters in audio file ${includedAudioFiles[0].metadata.path}`)
|
||||||
this.chapters = includedAudioFiles[0].chapters.map(c => ({ ...c }))
|
this.chapters = includedAudioFiles[0].chapters.map(c => ({ ...c }))
|
||||||
}
|
} else if (includedAudioFiles.length > 1) {
|
||||||
} else {
|
// Build chapters from audio files
|
||||||
this.chapters = []
|
this.chapters = []
|
||||||
var currChapterId = 0
|
var currChapterId = 0
|
||||||
var currStartTime = 0
|
var currStartTime = 0
|
||||||
includedAudioFiles.forEach((file) => {
|
includedAudioFiles.forEach((file) => {
|
||||||
// If audio file has chapters use chapters
|
if (file.duration) {
|
||||||
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
|
|
||||||
this.chapters.push({
|
this.chapters.push({
|
||||||
id: currChapterId++,
|
id: currChapterId++,
|
||||||
start: currStartTime,
|
start: currStartTime,
|
||||||
|
|||||||
+33
-11
@@ -3,7 +3,20 @@ const htmlSanitizer = require('../utils/htmlSanitizer')
|
|||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
class Audible {
|
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) {
|
cleanResult(item) {
|
||||||
var { title, subtitle, asin, authors, narrators, publisherName, summary, releaseDate, image, genres, seriesPrimary, seriesSecondary, language, runtimeLengthMin } = 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,
|
tags: tagsFiltered.length ? tagsFiltered.join(', ') : null,
|
||||||
series: series != [] ? series.map(({ name, position }) => ({ series: name, sequence: position })) : null,
|
series: series != [] ? series.map(({ name, position }) => ({ series: name, sequence: position })) : null,
|
||||||
language: language ? language.charAt(0).toUpperCase() + language.slice(1) : 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)
|
return /^[0-9A-Z]{10}$/.test(title)
|
||||||
}
|
}
|
||||||
|
|
||||||
asinSearch(asin) {
|
asinSearch(asin, region) {
|
||||||
asin = encodeURIComponent(asin);
|
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}`)
|
Logger.debug(`[Audible] ASIN url: ${url}`)
|
||||||
return axios.get(url).then((res) => {
|
return axios.get(url).then((res) => {
|
||||||
if (!res || !res.data || !res.data.asin) return null
|
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
|
var items
|
||||||
if (asin) {
|
if (asin) {
|
||||||
items = [await this.asinSearch(asin)]
|
items = [await this.asinSearch(asin, region)]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!items && this.isProbablyAsin(title)) {
|
if (!items && this.isProbablyAsin(title)) {
|
||||||
items = [await this.asinSearch(title)]
|
items = [await this.asinSearch(title, region)]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!items) {
|
if (!items) {
|
||||||
@@ -65,14 +86,15 @@ class Audible {
|
|||||||
num_results: '10',
|
num_results: '10',
|
||||||
products_sort_by: 'Relevance',
|
products_sort_by: 'Relevance',
|
||||||
title: title
|
title: title
|
||||||
};
|
}
|
||||||
if (author) queryObj.author = author
|
if (author) queryObj.author = author
|
||||||
var queryString = (new URLSearchParams(queryObj)).toString();
|
const queryString = (new URLSearchParams(queryObj)).toString()
|
||||||
var url = `https://api.audible.com/1.0/catalog/products?${queryString}`
|
const tld = region ? this.regionMap[region] : '.com'
|
||||||
|
const url = `https://api.audible${tld}/1.0/catalog/products?${queryString}`
|
||||||
Logger.debug(`[Audible] Search url: ${url}`)
|
Logger.debug(`[Audible] Search url: ${url}`)
|
||||||
items = await axios.get(url).then((res) => {
|
items = await axios.get(url).then((res) => {
|
||||||
if (!res || !res.data || !res.data.products) return null
|
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 => {
|
}).catch(error => {
|
||||||
Logger.error('[Audible] query search error', error)
|
Logger.error('[Audible] query search error', error)
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class Audnexus {
|
|||||||
return {
|
return {
|
||||||
asin: author.asin,
|
asin: author.asin,
|
||||||
description: author.description,
|
description: author.description,
|
||||||
image: author.image,
|
image: author.image || null,
|
||||||
name: author.name
|
name: author.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,7 +54,7 @@ class Audnexus {
|
|||||||
return {
|
return {
|
||||||
asin: author.asin,
|
asin: author.asin,
|
||||||
description: author.description,
|
description: author.description,
|
||||||
image: author.image,
|
image: author.image || null,
|
||||||
name: author.name
|
name: author.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,11 +60,15 @@ class iTunes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cleanAudiobook(data) {
|
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 {
|
return {
|
||||||
id: data.collectionId,
|
id: data.collectionId,
|
||||||
artistId: data.artistId,
|
artistId: data.artistId,
|
||||||
title: data.collectionName,
|
title: data.collectionName,
|
||||||
author: data.artistName,
|
author,
|
||||||
description: htmlSanitizer.stripAllTags(data.description || ''),
|
description: htmlSanitizer.stripAllTags(data.description || ''),
|
||||||
publishedYear: data.releaseDate ? data.releaseDate.split('-')[0] : null,
|
publishedYear: data.releaseDate ? data.releaseDate.split('-')[0] : null,
|
||||||
genres: data.primaryGenreName ? [data.primaryGenreName] : null,
|
genres: data.primaryGenreName ? [data.primaryGenreName] : null,
|
||||||
|
|||||||
@@ -819,7 +819,7 @@ class Scanner {
|
|||||||
if ((!libraryItem.media.tags.length || options.overrideDetails)) {
|
if ((!libraryItem.media.tags.length || options.overrideDetails)) {
|
||||||
var tagsArray = []
|
var tagsArray = []
|
||||||
if (Array.isArray(matchData[key])) tagsArray = [...matchData[key]]
|
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
|
updatePayload[key] = tagsArray
|
||||||
}
|
}
|
||||||
} else if ((!libraryItem.media.metadata[key] || options.overrideDetails)) {
|
} else if ((!libraryItem.media.metadata[key] || options.overrideDetails)) {
|
||||||
|
|||||||
@@ -168,7 +168,8 @@ module.exports.downloadFile = async (url, filepath) => {
|
|||||||
const response = await axios({
|
const response = await axios({
|
||||||
url,
|
url,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
responseType: 'stream'
|
responseType: 'stream',
|
||||||
|
timeout: 30000
|
||||||
})
|
})
|
||||||
response.data.pipe(writer)
|
response.data.pipe(writer)
|
||||||
return new Promise((resolve, reject) => {
|
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/..
|
path: Path.posix.join(folderPath, relPath) // i.e. /audiobook/Author Name/Book Name/..
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
module.exports.getBookDataFromDir = getBookDataFromDir
|
||||||
|
|
||||||
function getNarrator(folder) {
|
function getNarrator(folder) {
|
||||||
let pattern = /^(?<title>.*) \{(?<narrators>.*)\}$/
|
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.
|
// 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 volumeNumber = null
|
||||||
let parts = folder.split(' - ')
|
let parts = folder.split(' - ')
|
||||||
for (let i = 0; i < parts.length; i++) {
|
for (let i = 0; i < parts.length; i++) {
|
||||||
let match = parts[i].match(pattern)
|
let match = parts[i].match(pattern)
|
||||||
|
|
||||||
// This excludes '101 Dalmations' but includes '101. Dalmations'
|
// This excludes '101 Dalmations' but includes '101. Dalmations'
|
||||||
if (match && !(match.groups.suffix && !(match.groups.volumeLabel || match.groups.trailingDot))) {
|
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
|
parts[i] = match.groups.suffix
|
||||||
if (!parts[i]) { parts.splice(i, 1) }
|
if (!parts[i]) { parts.splice(i, 1) }
|
||||||
break
|
break
|
||||||
@@ -338,7 +338,7 @@ function getDataFromMediaDir(libraryMediaType, folderPath, relPath, serverSettin
|
|||||||
var parseSubtitle = !!serverSettings.scannerParseSubtitle
|
var parseSubtitle = !!serverSettings.scannerParseSubtitle
|
||||||
return getBookDataFromDir(folderPath, relPath, parseSubtitle)
|
return getBookDataFromDir(folderPath, relPath, parseSubtitle)
|
||||||
} else {
|
} else {
|
||||||
return this.getPodcastDataFromDir(folderPath, relPath)
|
return getPodcastDataFromDir(folderPath, relPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user