mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-01-01 20:17:51 -05:00
Compare commits
15 Commits
v2.19.0
...
fix_next_p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36ef675556 | ||
|
|
0dd57a8912 | ||
|
|
25ae6dd59a | ||
|
|
a37fe3c3d2 | ||
|
|
59bcbe0dfa | ||
|
|
b5e69630de | ||
|
|
0bba709124 | ||
|
|
e93bb5cb07 | ||
|
|
3f7af8acfb | ||
|
|
5e5a604d03 | ||
|
|
201e12ecc3 | ||
|
|
24d6e390f0 | ||
|
|
0cf7a6abec | ||
|
|
76ac0d001b | ||
|
|
00343a953b |
@@ -99,6 +99,7 @@ export default {
|
||||
this.$store.commit('showEditModal', libraryItem)
|
||||
},
|
||||
editEpisode({ libraryItem, episode }) {
|
||||
this.$store.commit('setEpisodeTableEpisodeIds', [episode.id])
|
||||
this.$store.commit('setSelectedLibraryItem', libraryItem)
|
||||
this.$store.commit('globals/setSelectedEpisode', episode)
|
||||
this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div ref="container" class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
||||
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
||||
<div v-if="show" class="w-full h-full py-4">
|
||||
<div class="w-full overflow-y-auto overflow-x-hidden max-h-96">
|
||||
<div class="flex px-8 items-center py-2">
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div ref="container" class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
||||
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
||||
<div v-if="show" class="w-full h-full">
|
||||
<div class="py-4 px-4">
|
||||
<h1 v-if="!showBatchCollectionModal" class="text-2xl">{{ $strings.LabelAddToCollection }}</h1>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="flex items-center px-4 py-2 justify-start relative hover:bg-bg" @mouseover="mouseover" @mouseleave="mouseleave">
|
||||
<div class="flex items-center px-4 py-2 justify-start relative hover:bg-black-400" @mouseover="mouseover" @mouseleave="mouseleave">
|
||||
<div v-if="isBookIncluded" class="absolute top-0 left-0 h-full w-1 bg-success z-10" />
|
||||
<div class="w-20 max-w-20 text-center">
|
||||
<covers-collection-cover :book-items="books" :width="80" :height="40 * bookCoverAspectRatio" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
|
||||
@@ -196,6 +196,9 @@ export default {
|
||||
methods: {
|
||||
async goPrevBook() {
|
||||
if (this.currentBookshelfIndex - 1 < 0) return
|
||||
// Remove focus from active input
|
||||
document.activeElement?.blur?.()
|
||||
|
||||
var prevBookId = this.bookshelfBookIds[this.currentBookshelfIndex - 1]
|
||||
this.processing = true
|
||||
var prevBook = await this.$axios.$get(`/api/items/${prevBookId}?expanded=1`).catch((error) => {
|
||||
@@ -215,6 +218,9 @@ export default {
|
||||
},
|
||||
async goNextBook() {
|
||||
if (this.currentBookshelfIndex >= this.bookshelfBookIds.length - 1) return
|
||||
// Remove focus from active input
|
||||
document.activeElement?.blur?.()
|
||||
|
||||
this.processing = true
|
||||
var nextBookId = this.bookshelfBookIds[this.currentBookshelfIndex + 1]
|
||||
var nextBook = await this.$axios.$get(`/api/items/${nextBookId}?expanded=1`).catch((error) => {
|
||||
@@ -300,4 +306,4 @@ export default {
|
||||
.tab.tab-selected {
|
||||
height: 41px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div ref="container" class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
||||
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
||||
<div v-if="show" class="w-full h-full">
|
||||
<div class="py-4 px-4">
|
||||
<h1 v-if="!isBatch" class="text-2xl">{{ $strings.LabelAddToPlaylist }}</h1>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="flex items-center px-4 py-2 justify-start relative hover:bg-bg" @mouseover="mouseover" @mouseleave="mouseleave">
|
||||
<div class="flex items-center px-4 py-2 justify-start relative hover:bg-black-400" @mouseover="mouseover" @mouseleave="mouseleave">
|
||||
<div v-if="isItemIncluded" class="absolute top-0 left-0 h-full w-1 bg-success z-10" />
|
||||
<div class="w-16 max-w-16 text-center">
|
||||
<covers-playlist-cover :items="items" :width="64" :height="64" />
|
||||
|
||||
@@ -117,8 +117,12 @@ export default {
|
||||
methods: {
|
||||
async goPrevEpisode() {
|
||||
if (this.currentEpisodeIndex - 1 < 0) return
|
||||
// Remove focus from active input
|
||||
document.activeElement?.blur?.()
|
||||
|
||||
const prevEpisodeId = this.episodeTableEpisodeIds[this.currentEpisodeIndex - 1]
|
||||
this.processing = true
|
||||
|
||||
const prevEpisode = await this.$axios.$get(`/api/podcasts/${this.libraryItem.id}/episode/${prevEpisodeId}`).catch((error) => {
|
||||
const errorMsg = error.response && error.response.data ? error.response.data : 'Failed to fetch episode'
|
||||
this.$toast.error(errorMsg)
|
||||
@@ -134,8 +138,12 @@ export default {
|
||||
},
|
||||
async goNextEpisode() {
|
||||
if (this.currentEpisodeIndex >= this.episodeTableEpisodeIds.length - 1) return
|
||||
// Remove focus from active input
|
||||
document.activeElement?.blur?.()
|
||||
|
||||
this.processing = true
|
||||
const nextEpisodeId = this.episodeTableEpisodeIds[this.currentEpisodeIndex + 1]
|
||||
|
||||
const nextEpisode = await this.$axios.$get(`/api/podcasts/${this.libraryItem.id}/episode/${nextEpisodeId}`).catch((error) => {
|
||||
const errorMsg = error.response && error.response.data ? error.response.data : 'Failed to fetch book'
|
||||
this.$toast.error(errorMsg)
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
<div>
|
||||
<div class="flex flex-wrap">
|
||||
<div class="w-1/5 p-1">
|
||||
<ui-text-input-with-label v-model="newEpisode.season" :label="$strings.LabelSeason" />
|
||||
<ui-text-input-with-label v-model="newEpisode.season" trim-whitespace :label="$strings.LabelSeason" />
|
||||
</div>
|
||||
<div class="w-1/5 p-1">
|
||||
<ui-text-input-with-label v-model="newEpisode.episode" :label="$strings.LabelEpisode" />
|
||||
<ui-text-input-with-label v-model="newEpisode.episode" trim-whitespace :label="$strings.LabelEpisode" />
|
||||
</div>
|
||||
<div class="w-1/5 p-1">
|
||||
<ui-dropdown v-model="newEpisode.episodeType" :label="$strings.LabelEpisodeType" :items="episodeTypes" small />
|
||||
@@ -14,10 +14,10 @@
|
||||
<ui-text-input-with-label v-model="pubDateInput" ref="pubdate" type="datetime-local" :label="$strings.LabelPubDate" @input="updatePubDate" />
|
||||
</div>
|
||||
<div class="w-full p-1">
|
||||
<ui-text-input-with-label v-model="newEpisode.title" :label="$strings.LabelTitle" />
|
||||
<ui-text-input-with-label v-model="newEpisode.title" :label="$strings.LabelTitle" trim-whitespace />
|
||||
</div>
|
||||
<div class="w-full p-1">
|
||||
<ui-textarea-with-label v-model="newEpisode.subtitle" :label="$strings.LabelSubtitle" :rows="3" />
|
||||
<ui-textarea-with-label v-model="newEpisode.subtitle" :label="$strings.LabelSubtitle" :rows="3" trim-whitespace />
|
||||
</div>
|
||||
<div class="w-full p-1">
|
||||
<ui-rich-text-editor :label="$strings.LabelDescription" v-model="newEpisode.description" />
|
||||
|
||||
@@ -215,6 +215,10 @@ export default {
|
||||
inputBlur() {
|
||||
if (!this.isFocused) return
|
||||
|
||||
if (typeof this.textInput === 'string') {
|
||||
this.textInput = this.textInput.trim()
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (document.activeElement === this.$refs.input) {
|
||||
return
|
||||
@@ -231,6 +235,11 @@ export default {
|
||||
},
|
||||
forceBlur() {
|
||||
this.isFocused = false
|
||||
|
||||
if (typeof this.textInput === 'string') {
|
||||
this.textInput = this.textInput.trim()
|
||||
}
|
||||
|
||||
if (this.textInput) this.submitForm()
|
||||
if (this.$refs.input) this.$refs.input.blur()
|
||||
},
|
||||
@@ -289,11 +298,12 @@ export default {
|
||||
this.selectedMenuItemIndex = null
|
||||
},
|
||||
submitForm() {
|
||||
if (!this.textInput) return
|
||||
if (!this.textInput || !this.textInput.trim?.()) return
|
||||
|
||||
this.textInput = this.textInput.trim()
|
||||
|
||||
const cleaned = this.textInput.trim()
|
||||
const matchesItem = this.items.find((i) => {
|
||||
return i.name === cleaned
|
||||
return i.name === this.textInput
|
||||
})
|
||||
|
||||
if (matchesItem) {
|
||||
|
||||
@@ -40,7 +40,8 @@ export default {
|
||||
showCopy: Boolean,
|
||||
step: [String, Number],
|
||||
min: [String, Number],
|
||||
customInputClass: String
|
||||
customInputClass: String,
|
||||
trimWhitespace: Boolean
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -101,9 +102,13 @@ export default {
|
||||
this.$emit('focus')
|
||||
},
|
||||
blurred() {
|
||||
if (this.trimWhitespace && typeof this.inputValue === 'string') {
|
||||
this.inputValue = this.inputValue.trim()
|
||||
}
|
||||
this.isFocused = false
|
||||
this.$emit('blur')
|
||||
},
|
||||
|
||||
change(e) {
|
||||
this.$emit('change', e.target.value)
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em>
|
||||
</label>
|
||||
</slot>
|
||||
<ui-text-input :placeholder="placeholder || label" :inputId="identifier" ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" :show-copy="showCopy" class="w-full" :class="inputClass" @blur="inputBlurred" />
|
||||
<ui-text-input :placeholder="placeholder || label" :inputId="identifier" ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" :show-copy="showCopy" class="w-full" :class="inputClass" :trim-whitespace="trimWhitespace" @blur="inputBlurred" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -24,7 +24,8 @@ export default {
|
||||
readonly: Boolean,
|
||||
disabled: Boolean,
|
||||
inputClass: String,
|
||||
showCopy: Boolean
|
||||
showCopy: Boolean,
|
||||
trimWhitespace: Boolean
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
|
||||
@@ -351,8 +351,10 @@ export default {
|
||||
background-color: white;
|
||||
}
|
||||
trix-editor {
|
||||
max-height: calc(4 * 1lh);
|
||||
height: calc(4 * 1lh);
|
||||
min-height: calc(4 * 1lh);
|
||||
overflow-y: auto;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
trix-editor * {
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
<form class="w-full h-full px-2 md:px-4 py-6" @submit.prevent="submitForm">
|
||||
<div class="flex flex-wrap -mx-1">
|
||||
<div class="w-full md:w-1/2 px-1">
|
||||
<ui-text-input-with-label ref="titleInput" v-model="details.title" :label="$strings.LabelTitle" @input="handleInputChange" />
|
||||
<ui-text-input-with-label ref="titleInput" v-model="details.title" :label="$strings.LabelTitle" trim-whitespace @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="flex-grow px-1 mt-2 md:mt-0">
|
||||
<ui-text-input-with-label ref="subtitleInput" v-model="details.subtitle" :label="$strings.LabelSubtitle" @input="handleInputChange" />
|
||||
<ui-text-input-with-label ref="subtitleInput" v-model="details.subtitle" :label="$strings.LabelSubtitle" trim-whitespace @input="handleInputChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -42,19 +42,19 @@
|
||||
<ui-multi-select ref="narratorsSelect" v-model="details.narrators" :label="$strings.LabelNarrators" :items="narrators" @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
|
||||
<ui-text-input-with-label ref="isbnInput" v-model="details.isbn" label="ISBN" @input="handleInputChange" />
|
||||
<ui-text-input-with-label ref="isbnInput" v-model="details.isbn" label="ISBN" trim-whitespace @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
|
||||
<ui-text-input-with-label ref="asinInput" v-model="details.asin" label="ASIN" @input="handleInputChange" />
|
||||
<ui-text-input-with-label ref="asinInput" v-model="details.asin" label="ASIN" trim-whitespace @input="handleInputChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap mt-2 -mx-1">
|
||||
<div class="w-full md:w-1/4 px-1">
|
||||
<ui-text-input-with-label ref="publisherInput" v-model="details.publisher" :label="$strings.LabelPublisher" @input="handleInputChange" />
|
||||
<ui-text-input-with-label ref="publisherInput" v-model="details.publisher" :label="$strings.LabelPublisher" trim-whitespace @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
|
||||
<ui-text-input-with-label ref="languageInput" v-model="details.language" :label="$strings.LabelLanguage" @input="handleInputChange" />
|
||||
<ui-text-input-with-label ref="languageInput" v-model="details.language" :label="$strings.LabelLanguage" trim-whitespace @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="flex-grow px-1 pt-6 mt-2 md:mt-0">
|
||||
<div class="flex justify-center">
|
||||
|
||||
@@ -124,6 +124,7 @@ export default {
|
||||
this.updateSelectionMode(false)
|
||||
},
|
||||
editEpisode({ libraryItem, episode }) {
|
||||
this.$store.commit('setEpisodeTableEpisodeIds', [episode.id])
|
||||
this.$store.commit('setSelectedLibraryItem', libraryItem)
|
||||
this.$store.commit('globals/setSelectedEpisode', episode)
|
||||
this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
<form class="w-full h-full px-4 py-6" @submit.prevent="submitForm">
|
||||
<div class="flex -mx-1">
|
||||
<div class="w-1/2 px-1">
|
||||
<ui-text-input-with-label ref="titleInput" v-model="details.title" :label="$strings.LabelTitle" @input="handleInputChange" />
|
||||
<ui-text-input-with-label ref="titleInput" v-model="details.title" :label="$strings.LabelTitle" trim-whitespace @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="flex-grow px-1">
|
||||
<ui-text-input-with-label ref="authorInput" v-model="details.author" :label="$strings.LabelAuthor" @input="handleInputChange" />
|
||||
<ui-text-input-with-label ref="authorInput" v-model="details.author" :label="$strings.LabelAuthor" trim-whitespace @input="handleInputChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ui-text-input-with-label ref="feedUrlInput" v-model="details.feedUrl" :label="$strings.LabelRSSFeedURL" class="mt-2" @input="handleInputChange" />
|
||||
<ui-text-input-with-label ref="feedUrlInput" v-model="details.feedUrl" :label="$strings.LabelRSSFeedURL" trim-whitespace class="mt-2" @input="handleInputChange" />
|
||||
|
||||
<ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" :label="$strings.LabelDescription" class="mt-2" @input="handleInputChange" />
|
||||
|
||||
@@ -25,13 +25,13 @@
|
||||
|
||||
<div class="flex mt-2 -mx-1">
|
||||
<div class="w-1/4 px-1">
|
||||
<ui-text-input-with-label ref="releaseDateInput" v-model="details.releaseDate" :label="$strings.LabelReleaseDate" @input="handleInputChange" />
|
||||
<ui-text-input-with-label ref="releaseDateInput" v-model="details.releaseDate" :label="$strings.LabelReleaseDate" trim-whitespace @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="w-1/4 px-1">
|
||||
<ui-text-input-with-label ref="itunesIdInput" v-model="details.itunesId" label="iTunes ID" @input="handleInputChange" />
|
||||
<ui-text-input-with-label ref="itunesIdInput" v-model="details.itunesId" label="iTunes ID" trim-whitespace @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="w-1/4 px-1">
|
||||
<ui-text-input-with-label ref="languageInput" v-model="details.language" :label="$strings.LabelLanguage" @input="handleInputChange" />
|
||||
<ui-text-input-with-label ref="languageInput" v-model="details.language" :label="$strings.LabelLanguage" trim-whitespace @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="flex-grow px-1 pt-6">
|
||||
<div class="flex justify-center">
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<div v-if="openMapOptions" class="flex flex-wrap">
|
||||
<div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 h-18 w-1/2">
|
||||
<ui-checkbox v-model="selectedBatchUsage.subtitle" />
|
||||
<ui-text-input-with-label ref="subtitleInput" v-model="batchDetails.subtitle" :disabled="!selectedBatchUsage.subtitle" :label="$strings.LabelSubtitle" class="mb-5 ml-4" />
|
||||
<ui-text-input-with-label ref="subtitleInput" v-model="batchDetails.subtitle" :disabled="!selectedBatchUsage.subtitle" :label="$strings.LabelSubtitle" trim-whitespace class="mb-5 ml-4" />
|
||||
</div>
|
||||
<div v-if="!isPodcastLibrary" class="flex items-center px-4 h-18 w-1/2">
|
||||
<ui-checkbox v-model="selectedBatchUsage.authors" />
|
||||
@@ -31,7 +31,7 @@
|
||||
</div>
|
||||
<div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 h-18 w-1/2">
|
||||
<ui-checkbox v-model="selectedBatchUsage.publishedYear" />
|
||||
<ui-text-input-with-label ref="publishedYearInput" v-model="batchDetails.publishedYear" :disabled="!selectedBatchUsage.publishedYear" :label="$strings.LabelPublishYear" class="mb-5 ml-4" />
|
||||
<ui-text-input-with-label ref="publishedYearInput" v-model="batchDetails.publishedYear" :disabled="!selectedBatchUsage.publishedYear" :label="$strings.LabelPublishYear" trim-whitespace class="mb-5 ml-4" />
|
||||
</div>
|
||||
<div v-if="!isPodcastLibrary" class="flex items-center px-4 h-18 w-1/2">
|
||||
<ui-checkbox v-model="selectedBatchUsage.series" />
|
||||
@@ -51,11 +51,11 @@
|
||||
</div>
|
||||
<div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 h-18 w-1/2">
|
||||
<ui-checkbox v-model="selectedBatchUsage.publisher" />
|
||||
<ui-text-input-with-label ref="publisherInput" v-model="batchDetails.publisher" :disabled="!selectedBatchUsage.publisher" :label="$strings.LabelPublisher" class="mb-5 ml-4" />
|
||||
<ui-text-input-with-label ref="publisherInput" v-model="batchDetails.publisher" :disabled="!selectedBatchUsage.publisher" :label="$strings.LabelPublisher" trim-whitespace class="mb-5 ml-4" />
|
||||
</div>
|
||||
<div v-if="!isMapAppend" class="flex items-center px-4 h-18 w-1/2">
|
||||
<ui-checkbox v-model="selectedBatchUsage.language" />
|
||||
<ui-text-input-with-label ref="languageInput" v-model="batchDetails.language" :disabled="!selectedBatchUsage.language" :label="$strings.LabelLanguage" class="mb-5 ml-4" />
|
||||
<ui-text-input-with-label ref="languageInput" v-model="batchDetails.language" :disabled="!selectedBatchUsage.language" :label="$strings.LabelLanguage" trim-whitespace class="mb-5 ml-4" />
|
||||
</div>
|
||||
<div v-if="!isMapAppend" class="flex items-center px-4 h-18 w-1/2">
|
||||
<ui-checkbox v-model="selectedBatchUsage.explicit" />
|
||||
|
||||
@@ -251,6 +251,7 @@ class CollectionController {
|
||||
/**
|
||||
* DELETE: /api/collections/:id/book/:bookId
|
||||
* Remove a single book from a collection. Re-order books
|
||||
* Users with update permission can remove books from collections
|
||||
* TODO: bookId is actually libraryItemId. Clients need updating to use bookId
|
||||
*
|
||||
* @param {CollectionControllerRequest} req
|
||||
@@ -427,7 +428,8 @@ class CollectionController {
|
||||
req.collection = collection
|
||||
}
|
||||
|
||||
if (req.method == 'DELETE' && !req.user.canDelete) {
|
||||
// Users with update permission can remove books from collections
|
||||
if (req.method == 'DELETE' && !req.params.bookId && !req.user.canDelete) {
|
||||
Logger.warn(`[CollectionController] User "${req.user.username}" attempted to delete without permission`)
|
||||
return res.sendStatus(403)
|
||||
} else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) {
|
||||
|
||||
@@ -365,7 +365,7 @@ class Book extends Model {
|
||||
if (payload.metadata) {
|
||||
const metadataStringKeys = ['title', 'subtitle', 'publishedYear', 'publishedDate', 'publisher', 'description', 'isbn', 'asin', 'language']
|
||||
metadataStringKeys.forEach((key) => {
|
||||
if (typeof payload.metadata[key] === 'string' && this[key] !== payload.metadata[key]) {
|
||||
if ((typeof payload.metadata[key] === 'string' || payload.metadata[key] === null) && this[key] !== payload.metadata[key]) {
|
||||
this[key] = payload.metadata[key] || null
|
||||
|
||||
if (key === 'title') {
|
||||
|
||||
@@ -202,8 +202,9 @@ class Podcast extends Model {
|
||||
} else if (key === 'itunesPageUrl') {
|
||||
newKey = 'itunesPageURL'
|
||||
}
|
||||
if (typeof payload.metadata[key] === 'string' && payload.metadata[key] !== this[newKey]) {
|
||||
this[newKey] = payload.metadata[key]
|
||||
if ((typeof payload.metadata[key] === 'string' || payload.metadata[key] === null) && payload.metadata[key] !== this[newKey]) {
|
||||
this[newKey] = payload.metadata[key] || null
|
||||
|
||||
if (key === 'title') {
|
||||
this.titleIgnorePrefix = getTitleIgnorePrefix(this.title)
|
||||
}
|
||||
|
||||
@@ -286,10 +286,23 @@ module.exports.downloadFile = (url, filepath, contentTypeFilter = null) => {
|
||||
return reject(new Error(`Invalid content type "${response.headers?.['content-type'] || ''}"`))
|
||||
}
|
||||
|
||||
const totalSize = parseInt(response.headers['content-length'], 10)
|
||||
let downloadedSize = 0
|
||||
|
||||
// Write to filepath
|
||||
const writer = fs.createWriteStream(filepath)
|
||||
response.data.pipe(writer)
|
||||
|
||||
let lastProgress = 0
|
||||
response.data.on('data', (chunk) => {
|
||||
downloadedSize += chunk.length
|
||||
const progress = totalSize ? Math.round((downloadedSize / totalSize) * 100) : 0
|
||||
if (progress >= lastProgress + 5) {
|
||||
Logger.debug(`[fileUtils] File "${Path.basename(filepath)}" download progress: ${progress}% (${downloadedSize}/${totalSize} bytes)`)
|
||||
lastProgress = progress
|
||||
}
|
||||
})
|
||||
|
||||
writer.on('finish', resolve)
|
||||
writer.on('error', reject)
|
||||
})
|
||||
|
||||
@@ -35,11 +35,18 @@ module.exports.nameToLastFirst = (firstLast) => {
|
||||
return `${nameObj.last_name}, ${nameObj.first_name}`
|
||||
}
|
||||
|
||||
// Handle any name string
|
||||
/**
|
||||
* Parses a name string into an array of names
|
||||
*
|
||||
* @param {string} nameString - The name string to parse
|
||||
* @returns {{ names: string[] }} Array of names
|
||||
*/
|
||||
module.exports.parse = (nameString) => {
|
||||
if (!nameString) return null
|
||||
|
||||
var splitNames = []
|
||||
let splitNames = []
|
||||
const isCommaSeparated = nameString.includes(',')
|
||||
|
||||
// Example &LF: Friedman, Milton & Friedman, Rose
|
||||
if (nameString.includes('&')) {
|
||||
nameString.split('&').forEach((asa) => (splitNames = splitNames.concat(asa.split(','))))
|
||||
@@ -59,17 +66,18 @@ module.exports.parse = (nameString) => {
|
||||
}
|
||||
}
|
||||
|
||||
var names = []
|
||||
let names = []
|
||||
|
||||
// 1 name FIRST LAST
|
||||
if (splitNames.length === 1) {
|
||||
names.push(parseName(nameString))
|
||||
} else {
|
||||
var firstChunkIsALastName = checkIsALastName(splitNames[0])
|
||||
var isEvenNum = splitNames.length % 2 === 0
|
||||
// Determines whether this is formatted as last, first or first last (only if using comma separator)
|
||||
// Example: "Smith; James Jones" -> ["Smith", "James Jones"]
|
||||
let firstChunkIsALastName = !isCommaSeparated ? false : checkIsALastName(splitNames[0])
|
||||
let isEvenNum = splitNames.length % 2 === 0
|
||||
|
||||
if (!isEvenNum && firstChunkIsALastName) {
|
||||
// console.error('Multi-name LAST,FIRST entry has a straggler (could be roman numerals or a suffix), ignore it')
|
||||
splitNames = splitNames.slice(0, splitNames.length - 1)
|
||||
}
|
||||
|
||||
|
||||
@@ -311,6 +311,7 @@ module.exports.getPodcastFeed = (feedUrl, excludeEpisodeMetadata = false) => {
|
||||
responseType: 'arraybuffer',
|
||||
headers: {
|
||||
Accept: 'application/rss+xml, application/xhtml+xml, application/xml, */*;q=0.8',
|
||||
'Accept-Encoding': 'gzip, compress, deflate',
|
||||
'User-Agent': userAgent
|
||||
},
|
||||
httpAgent: global.DisableSsrfRequestFilter?.(feedUrl) ? null : ssrfFilter(feedUrl),
|
||||
|
||||
99
test/server/utils/parsers/parseNameString.test.js
Normal file
99
test/server/utils/parsers/parseNameString.test.js
Normal file
@@ -0,0 +1,99 @@
|
||||
const chai = require('chai')
|
||||
const expect = chai.expect
|
||||
const { parse, nameToLastFirst } = require('../../../../server/utils/parsers/parseNameString')
|
||||
|
||||
describe('parseNameString', () => {
|
||||
describe('parse', () => {
|
||||
it('returns null if nameString is empty', () => {
|
||||
const result = parse('')
|
||||
expect(result).to.be.null
|
||||
})
|
||||
|
||||
it('parses single name in First Last format', () => {
|
||||
const result = parse('John Smith')
|
||||
expect(result.names).to.deep.equal(['John Smith'])
|
||||
})
|
||||
|
||||
it('parses single name in Last, First format', () => {
|
||||
const result = parse('Smith, John')
|
||||
expect(result.names).to.deep.equal(['John Smith'])
|
||||
})
|
||||
|
||||
it('parses multiple names separated by &', () => {
|
||||
const result = parse('John Smith & Jane Doe')
|
||||
expect(result.names).to.deep.equal(['John Smith', 'Jane Doe'])
|
||||
})
|
||||
|
||||
it('parses multiple names separated by "and"', () => {
|
||||
const result = parse('John Smith and Jane Doe')
|
||||
expect(result.names).to.deep.equal(['John Smith', 'Jane Doe'])
|
||||
})
|
||||
|
||||
it('parses multiple names separated by comma and "and"', () => {
|
||||
const result = parse('John Smith, Jane Doe and John Doe')
|
||||
expect(result.names).to.deep.equal(['John Smith', 'Jane Doe', 'John Doe'])
|
||||
})
|
||||
|
||||
it('parses multiple names separated by semicolon', () => {
|
||||
const result = parse('John Smith; Jane Doe')
|
||||
expect(result.names).to.deep.equal(['John Smith', 'Jane Doe'])
|
||||
})
|
||||
|
||||
it('parses multiple names in Last, First format', () => {
|
||||
const result = parse('Smith, John, Doe, Jane')
|
||||
expect(result.names).to.deep.equal(['John Smith', 'Jane Doe'])
|
||||
})
|
||||
|
||||
it('parses multiple names with single word name', () => {
|
||||
const result = parse('John Smith, Jones, James Doe, Ludwig von Mises')
|
||||
expect(result.names).to.deep.equal(['John Smith', 'Jones', 'James Doe', 'Ludwig von Mises'])
|
||||
})
|
||||
|
||||
it('parses multiple names with single word name listed first (semicolon separator)', () => {
|
||||
const result = parse('Jones; John Smith; James Doe; Ludwig von Mises')
|
||||
expect(result.names).to.deep.equal(['Jones', 'John Smith', 'James Doe', 'Ludwig von Mises'])
|
||||
})
|
||||
|
||||
it('handles names with suffixes', () => {
|
||||
const result = parse('Smith, John Jr.')
|
||||
expect(result.names).to.deep.equal(['John Jr. Smith'])
|
||||
})
|
||||
|
||||
it('handles compound last names', () => {
|
||||
const result = parse('von Mises, Ludwig')
|
||||
expect(result.names).to.deep.equal(['Ludwig von Mises'])
|
||||
})
|
||||
|
||||
it('handles Chinese/Japanese/Korean names', () => {
|
||||
const result = parse('张三, 李四')
|
||||
expect(result.names).to.deep.equal(['张三', '李四'])
|
||||
})
|
||||
|
||||
it('removes duplicate names', () => {
|
||||
const result = parse('John Smith & John Smith')
|
||||
expect(result.names).to.deep.equal(['John Smith'])
|
||||
})
|
||||
|
||||
it('filters out empty names', () => {
|
||||
const result = parse('John Smith,')
|
||||
expect(result.names).to.deep.equal(['John Smith'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('nameToLastFirst', () => {
|
||||
it('converts First Last to Last, First format', () => {
|
||||
const result = nameToLastFirst('John Smith')
|
||||
expect(result).to.equal('Smith, John')
|
||||
})
|
||||
|
||||
it('returns last name only when no first name', () => {
|
||||
const result = nameToLastFirst('Smith')
|
||||
expect(result).to.equal('Smith')
|
||||
})
|
||||
|
||||
it('handles names with middle names', () => {
|
||||
const result = nameToLastFirst('John Middle Smith')
|
||||
expect(result).to.equal('Smith, John Middle')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user