mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-28 17:49:06 +01:00
Podcast library item card, edit details, batch edit
This commit is contained in:
parent
5446aea910
commit
e32d05ea27
@ -47,10 +47,10 @@
|
||||
<div v-show="numLibraryItemsSelected" class="absolute top-0 left-0 w-full h-full px-4 bg-primary flex items-center">
|
||||
<h1 class="text-2xl px-4">{{ numLibraryItemsSelected }} Selected</h1>
|
||||
<div class="flex-grow" />
|
||||
<ui-tooltip :text="`Mark as ${selectedIsFinished ? 'Not Finished' : 'Finished'}`" direction="bottom">
|
||||
<ui-tooltip v-if="!isPodcastLibrary" :text="`Mark as ${selectedIsFinished ? 'Not Finished' : 'Finished'}`" direction="bottom">
|
||||
<ui-read-icon-btn :disabled="processingBatch" :is-read="selectedIsFinished" @click="toggleBatchRead" class="mx-1.5" />
|
||||
</ui-tooltip>
|
||||
<ui-tooltip v-if="userCanUpdate" text="Add to Collection" direction="bottom">
|
||||
<ui-tooltip v-if="userCanUpdate && !isPodcastLibrary" text="Add to Collection" direction="bottom">
|
||||
<ui-icon-btn :disabled="processingBatch" icon="collections_bookmark" @click="batchAddToCollectionClick" class="mx-1.5" />
|
||||
</ui-tooltip>
|
||||
<template v-if="userCanUpdate && numLibraryItemsSelected < 50">
|
||||
@ -79,6 +79,12 @@ export default {
|
||||
libraryName() {
|
||||
return this.currentLibrary ? this.currentLibrary.name : 'unknown'
|
||||
},
|
||||
libraryMediaType() {
|
||||
return this.currentLibrary ? this.currentLibrary.mediaType : null
|
||||
},
|
||||
isPodcastLibrary() {
|
||||
return this.libraryMediaType === 'podcast'
|
||||
},
|
||||
isHome() {
|
||||
return this.$route.name === 'library-library'
|
||||
},
|
||||
|
@ -10,7 +10,7 @@
|
||||
<p class="truncate" :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }">
|
||||
{{ displayTitle }}
|
||||
</p>
|
||||
<p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayAuthor }}</p>
|
||||
<p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayAuthor || ' ' }}</p>
|
||||
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
|
||||
</div>
|
||||
|
||||
@ -146,6 +146,12 @@ export default {
|
||||
mediaMetadata() {
|
||||
return this.media.metadata || {}
|
||||
},
|
||||
mediaType() {
|
||||
return this._libraryItem.mediaType
|
||||
},
|
||||
isPodcast() {
|
||||
return this.mediaType === 'podcast'
|
||||
},
|
||||
placeholderUrl() {
|
||||
return '/book_placeholder.jpg'
|
||||
},
|
||||
@ -195,6 +201,7 @@ export default {
|
||||
return this.mediaMetadata.authors || []
|
||||
},
|
||||
author() {
|
||||
if (this.isPodcast) return this.mediaMetadata.author
|
||||
return this.authors.map((au) => au.name).join(', ')
|
||||
},
|
||||
authorLF() {
|
||||
@ -216,6 +223,7 @@ export default {
|
||||
return this.title
|
||||
},
|
||||
displayAuthor() {
|
||||
if (this.isPodcast) return this.author
|
||||
if (this.orderBy === 'media.metadata.authorNameLF') return this.authorLF
|
||||
return this.author
|
||||
},
|
||||
@ -301,29 +309,30 @@ export default {
|
||||
return this.store.getters['user/getIsRoot']
|
||||
},
|
||||
moreMenuItems() {
|
||||
var items = [
|
||||
{
|
||||
func: 'toggleFinished',
|
||||
text: `Mark as ${this.itemIsFinished ? 'Not Finished' : 'Finished'}`
|
||||
},
|
||||
{
|
||||
func: 'openCollections',
|
||||
text: 'Add to Collection'
|
||||
}
|
||||
]
|
||||
var items = []
|
||||
if (!this.isPodcast) {
|
||||
items = [
|
||||
{
|
||||
func: 'toggleFinished',
|
||||
text: `Mark as ${this.itemIsFinished ? 'Not Finished' : 'Finished'}`
|
||||
},
|
||||
{
|
||||
func: 'openCollections',
|
||||
text: 'Add to Collection'
|
||||
}
|
||||
]
|
||||
}
|
||||
if (this.userCanUpdate) {
|
||||
if (this.numTracks) {
|
||||
items.push({
|
||||
func: 'showEditModalTracks',
|
||||
text: 'Tracks'
|
||||
})
|
||||
}
|
||||
items.push({
|
||||
func: 'showEditModalTracks',
|
||||
text: 'Files'
|
||||
})
|
||||
items.push({
|
||||
func: 'showEditModalMatch',
|
||||
text: 'Match'
|
||||
})
|
||||
}
|
||||
if (this.userCanDownload) {
|
||||
if (this.userCanDownload && !this.isPodcast) {
|
||||
items.push({
|
||||
func: 'showEditModalDownload',
|
||||
text: 'Download'
|
||||
|
@ -47,6 +47,11 @@ export default {
|
||||
title: 'Chapters',
|
||||
component: 'modals-item-tabs-chapters'
|
||||
},
|
||||
{
|
||||
id: 'episodes',
|
||||
title: 'Episodes',
|
||||
component: 'modals-item-tabs-episodes'
|
||||
},
|
||||
{
|
||||
id: 'files',
|
||||
title: 'Files',
|
||||
@ -118,8 +123,10 @@ export default {
|
||||
if (!this.userCanUpdate && !this.userCanDownload) return []
|
||||
return this.tabs.filter((tab) => {
|
||||
if (tab.id === 'download' && this.isMissing) return false
|
||||
if ((tab.id === 'download' || tab.id === 'files' || tab.id === 'authors') && this.userCanDownload) return true
|
||||
if (tab.id !== 'download' && tab.id !== 'files' && tab.id !== 'authors' && this.userCanUpdate) return true
|
||||
if (tab.id === 'chapters' && this.mediaType !== 'book') return false
|
||||
if (tab.id === 'episodes' && this.mediaType !== 'podcast') return false
|
||||
if ((tab.id === 'download' || tab.id === 'files') && this.userCanDownload) return true
|
||||
if (tab.id !== 'download' && tab.id !== 'files' && this.userCanUpdate) return true
|
||||
if (tab.id === 'match' && this.userCanUpdate && this.showExperimentalFeatures) return true
|
||||
return false
|
||||
})
|
||||
@ -147,6 +154,9 @@ export default {
|
||||
mediaMetadata() {
|
||||
return this.media.metadata || {}
|
||||
},
|
||||
mediaType() {
|
||||
return this.libraryItem ? this.libraryItem.mediaType : null
|
||||
},
|
||||
title() {
|
||||
return this.mediaMetadata.title || 'No Title'
|
||||
},
|
||||
|
@ -1,201 +0,0 @@
|
||||
<template>
|
||||
<div class="w-full h-full overflow-hidden px-4 py-6 relative">
|
||||
<template v-for="(authorName, index) in searchAuthors">
|
||||
<cards-search-author-card :key="index" :author-name="authorName" @match="setSelectedMatch" />
|
||||
</template>
|
||||
|
||||
<div v-show="processing" class="flex h-full items-center justify-center">
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
<div v-if="selectedMatch" class="absolute top-0 left-0 w-full bg-bg h-full p-8 max-h-full overflow-y-auto overflow-x-hidden">
|
||||
<div class="flex mb-2">
|
||||
<div class="w-8 h-8 rounded-full hover:bg-white hover:bg-opacity-10 flex items-center justify-center cursor-pointer" @click="selectedMatch = null">
|
||||
<span class="material-icons text-3xl">arrow_back</span>
|
||||
</div>
|
||||
<p class="text-xl pl-3">Update Author Details</p>
|
||||
</div>
|
||||
<form @submit.prevent="submitMatchUpdate">
|
||||
<div v-if="selectedMatch.image" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.image" />
|
||||
<img :src="selectedMatch.image" class="w-24 object-contain ml-4" />
|
||||
<ui-text-input-with-label v-model="selectedMatch.image" :disabled="!selectedMatchUsage.image" label="Image" class="flex-grow ml-4" />
|
||||
</div>
|
||||
<div v-if="selectedMatch.name" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.name" />
|
||||
<ui-text-input-with-label v-model="selectedMatch.name" :disabled="!selectedMatchUsage.name" label="Name" class="flex-grow ml-4" />
|
||||
</div>
|
||||
<div v-if="selectedMatch.description" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.description" />
|
||||
<ui-textarea-with-label v-model="selectedMatch.description" :rows="3" :disabled="!selectedMatchUsage.description" label="Description" class="flex-grow ml-4" />
|
||||
</div>
|
||||
<div class="flex items-center justify-end py-2">
|
||||
<ui-btn color="success" type="submit">Update</ui-btn>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
processing: Boolean,
|
||||
audiobook: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchAuthors: [],
|
||||
audiobookId: null,
|
||||
searchAuthor: null,
|
||||
lastSearch: null,
|
||||
hasSearched: false,
|
||||
selectedMatch: null,
|
||||
|
||||
selectedMatchUsage: {
|
||||
image: true,
|
||||
name: true,
|
||||
description: true
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
audiobook: {
|
||||
immediate: true,
|
||||
handler(newVal) {
|
||||
if (newVal) this.init()
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isProcessing: {
|
||||
get() {
|
||||
return this.processing
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('update:processing', val)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// getSearchQuery() {
|
||||
// return `q=${this.searchAuthor}`
|
||||
// },
|
||||
// submitSearch() {
|
||||
// if (!this.searchTitle) {
|
||||
// this.$toast.warning('Search title is required')
|
||||
// return
|
||||
// }
|
||||
// this.runSearch()
|
||||
// },
|
||||
// async runSearch() {
|
||||
// var searchQuery = this.getSearchQuery()
|
||||
// if (this.lastSearch === searchQuery) return
|
||||
// this.selectedMatch = null
|
||||
// this.isProcessing = true
|
||||
// this.lastSearch = searchQuery
|
||||
// var result = await this.$axios.$get(`/api/authors/search?${searchQuery}`).catch((error) => {
|
||||
// console.error('Failed', error)
|
||||
// return []
|
||||
// })
|
||||
// if (result) {
|
||||
// this.selectedMatch = result
|
||||
// }
|
||||
// this.isProcessing = false
|
||||
// this.hasSearched = true
|
||||
// },
|
||||
init() {
|
||||
this.selectedMatch = null
|
||||
// this.selectedMatchUsage = {
|
||||
// title: true,
|
||||
// subtitle: true,
|
||||
// cover: true,
|
||||
// author: true,
|
||||
// description: true,
|
||||
// isbn: true,
|
||||
// publisher: true,
|
||||
// publishYear: true
|
||||
// }
|
||||
|
||||
if (this.audiobook.id !== this.audiobookId) {
|
||||
this.selectedMatch = null
|
||||
this.hasSearched = false
|
||||
this.audiobookId = this.audiobook.id
|
||||
}
|
||||
|
||||
if (!this.audiobook.book || !this.audiobook.book.authorFL) {
|
||||
this.searchAuthors = []
|
||||
return
|
||||
}
|
||||
this.searchAuthors = (this.audiobook.book.authorFL || '').split(', ')
|
||||
},
|
||||
selectMatch(match) {
|
||||
this.selectedMatch = match
|
||||
},
|
||||
buildMatchUpdatePayload() {
|
||||
var updatePayload = {}
|
||||
for (const key in this.selectedMatchUsage) {
|
||||
if (this.selectedMatchUsage[key] && this.selectedMatch[key]) {
|
||||
updatePayload[key] = this.selectedMatch[key]
|
||||
}
|
||||
}
|
||||
return updatePayload
|
||||
},
|
||||
async submitMatchUpdate() {
|
||||
var updatePayload = this.buildMatchUpdatePayload()
|
||||
if (!Object.keys(updatePayload).length) {
|
||||
return
|
||||
}
|
||||
this.isProcessing = true
|
||||
|
||||
if (updatePayload.cover) {
|
||||
var coverPayload = {
|
||||
url: updatePayload.cover
|
||||
}
|
||||
var success = await this.$axios.$post(`/api/items/${this.audiobook.id}/cover`, coverPayload).catch((error) => {
|
||||
console.error('Failed to update', error)
|
||||
return false
|
||||
})
|
||||
if (success) {
|
||||
this.$toast.success('Book Cover Updated')
|
||||
} else {
|
||||
this.$toast.error('Book Cover Failed to Update')
|
||||
}
|
||||
console.log('Updated cover')
|
||||
delete updatePayload.cover
|
||||
}
|
||||
|
||||
if (Object.keys(updatePayload).length) {
|
||||
var bookUpdatePayload = {
|
||||
book: updatePayload
|
||||
}
|
||||
var success = await this.$axios.$patch(`/api/items/${this.audiobook.id}`, bookUpdatePayload).catch((error) => {
|
||||
console.error('Failed to update', error)
|
||||
return false
|
||||
})
|
||||
if (success) {
|
||||
this.$toast.success('Book Details Updated')
|
||||
this.selectedMatch = null
|
||||
this.$emit('selectTab', 'details')
|
||||
} else {
|
||||
this.$toast.error('Book Details Failed to Update')
|
||||
}
|
||||
} else {
|
||||
this.selectedMatch = null
|
||||
}
|
||||
this.isProcessing = false
|
||||
},
|
||||
setSelectedMatch(authorMatchObj) {
|
||||
this.selectedMatch = authorMatchObj
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.matchListWrapper {
|
||||
height: calc(100% - 80px);
|
||||
}
|
||||
</style>
|
@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div class="w-full h-full relative">
|
||||
<widgets-item-details-edit ref="itemDetailsEdit" :library-item="libraryItem" @submit="submitForm" />
|
||||
<widgets-book-details-edit v-if="mediaType == 'book'" ref="itemDetailsEdit" :library-item="libraryItem" @submit="submitForm" />
|
||||
<widgets-podcast-details-edit v-else ref="itemDetailsEdit" :library-item="libraryItem" @submit="submitForm" />
|
||||
|
||||
<div class="absolute bottom-0 left-0 w-full py-4 bg-bg" :class="isScrollable ? 'box-shadow-md-up' : 'box-shadow-sm-up border-t border-primary border-opacity-50'">
|
||||
<div class="flex items-center px-4">
|
||||
@ -8,11 +9,11 @@
|
||||
|
||||
<div class="flex-grow" />
|
||||
|
||||
<ui-tooltip v-if="!isMissing" text="(Root User Only) Save a NFO metadata file in your audiobooks directory" direction="bottom" class="mr-4 hidden sm:block">
|
||||
<ui-tooltip v-if="!isMissing && mediaType == 'book'" text="(Root User Only) Save a NFO metadata file in your audiobooks directory" direction="bottom" class="mr-4 hidden sm:block">
|
||||
<ui-btn v-if="isRootUser" :loading="savingMetadata" color="bg" type="button" class="h-full" small @click.stop.prevent="saveMetadata">Save Metadata</ui-btn>
|
||||
</ui-tooltip>
|
||||
|
||||
<ui-tooltip :disabled="!!quickMatching" :text="`(Root User Only) Populate empty book details & cover with first book result from '${libraryProvider}'. Does not overwrite details.`" direction="bottom" class="mr-4">
|
||||
<ui-tooltip v-if="mediaType == 'book'" :disabled="!!quickMatching" :text="`(Root User Only) Populate empty book details & cover with first book result from '${libraryProvider}'. Does not overwrite details.`" direction="bottom" class="mr-4">
|
||||
<ui-btn v-if="isRootUser" :loading="quickMatching" color="bg" type="button" class="h-full" small @click.stop.prevent="quickMatch">Quick Match</ui-btn>
|
||||
</ui-tooltip>
|
||||
|
||||
@ -65,6 +66,9 @@ export default {
|
||||
media() {
|
||||
return this.libraryItem ? this.libraryItem.media || {} : {}
|
||||
},
|
||||
mediaType() {
|
||||
return this.libraryItem ? this.libraryItem.mediaType : null
|
||||
},
|
||||
mediaMetadata() {
|
||||
return this.media.metadata || {}
|
||||
},
|
||||
|
55
client/components/modals/item/tabs/Episodes.vue
Normal file
55
client/components/modals/item/tabs/Episodes.vue
Normal file
@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div class="w-full h-full overflow-y-auto overflow-x-hidden px-4 py-6">
|
||||
<div class="w-full mb-4">
|
||||
<div class="w-full p-4 bg-primary">
|
||||
<p>Podcast Episodes</p>
|
||||
</div>
|
||||
<div v-if="!episodes.length" class="flex my-4 text-center justify-center text-xl">No Episodes</div>
|
||||
<table v-else class="text-sm tracksTable">
|
||||
<tr class="font-book">
|
||||
<th class="text-left w-16"><span class="px-4">#</span></th>
|
||||
<th class="text-left">Title</th>
|
||||
<th class="text-center w-28">Duration</th>
|
||||
<th class="text-center w-28">Size</th>
|
||||
</tr>
|
||||
<tr v-for="episode in episodes" :key="episode.id">
|
||||
<td class="text-left">
|
||||
<p class="px-4">{{ episode.index }}</p>
|
||||
</td>
|
||||
<td class="font-book">
|
||||
{{ episode.title }}
|
||||
</td>
|
||||
<td class="font-mono text-center">
|
||||
{{ $secondsToTimestamp(episode.duration) }}
|
||||
</td>
|
||||
<td class="font-mono text-center">
|
||||
{{ $bytesPretty(episode.size) }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
libraryItem: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
media() {
|
||||
return this.libraryItem ? this.libraryItem.media || {} : {}
|
||||
},
|
||||
episodes() {
|
||||
return this.media.episodes || []
|
||||
}
|
||||
},
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
224
client/components/widgets/PodcastDetailsEdit.vue
Normal file
224
client/components/widgets/PodcastDetailsEdit.vue
Normal file
@ -0,0 +1,224 @@
|
||||
<template>
|
||||
<div class="w-full h-full relative">
|
||||
<form class="w-full h-full" @submit.prevent="submitForm">
|
||||
<div id="formWrapper" class="px-4 py-6 details-form-wrapper w-full overflow-hidden overflow-y-auto">
|
||||
<div class="flex -mx-1">
|
||||
<div class="w-1/2 px-1">
|
||||
<ui-text-input-with-label ref="titleInput" v-model="details.title" label="Title" />
|
||||
</div>
|
||||
<div class="flex-grow px-1">
|
||||
<ui-text-input-with-label ref="authorInput" v-model="details.author" label="Author" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ui-text-input-with-label ref="feedUrlInput" v-model="details.feedUrl" label="RSS Feed URL" class="mt-2" />
|
||||
|
||||
<ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" label="Description" class="mt-2" />
|
||||
|
||||
<div class="flex mt-2 -mx-1">
|
||||
<div class="w-1/2 px-1">
|
||||
<ui-multi-select ref="genresSelect" v-model="details.genres" label="Genres" :items="genres" />
|
||||
</div>
|
||||
<div class="flex-grow px-1">
|
||||
<ui-multi-select ref="tagsSelect" v-model="newTags" label="Tags" :items="tags" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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="Release Date" />
|
||||
</div>
|
||||
<div class="w-1/4 px-1">
|
||||
<ui-text-input-with-label ref="itunesIdInput" v-model="details.itunesId" label="iTunes ID" />
|
||||
</div>
|
||||
<div class="w-1/4 px-1">
|
||||
<ui-text-input-with-label ref="languageInput" v-model="details.language" label="Language" />
|
||||
</div>
|
||||
<div class="flex-grow px-1 pt-6">
|
||||
<div class="flex justify-center">
|
||||
<ui-checkbox v-model="details.explicit" label="Explicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
libraryItem: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
details: {
|
||||
title: null,
|
||||
author: null,
|
||||
description: null,
|
||||
releaseDate: null,
|
||||
genres: [],
|
||||
feedUrl: null,
|
||||
imageUrl: null,
|
||||
itunesPageUrl: null,
|
||||
itunesId: null,
|
||||
itunesArtistId: null,
|
||||
explicit: false,
|
||||
language: null
|
||||
},
|
||||
autoDownloadEpisodes: false,
|
||||
newTags: []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
libraryItem: {
|
||||
immediate: true,
|
||||
handler(newVal) {
|
||||
if (newVal) this.init()
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
media() {
|
||||
return this.libraryItem ? this.libraryItem.media || {} : {}
|
||||
},
|
||||
mediaMetadata() {
|
||||
return this.media.metadata || {}
|
||||
},
|
||||
genres() {
|
||||
return this.filterData.genres || []
|
||||
},
|
||||
tags() {
|
||||
return this.filterData.tags || []
|
||||
},
|
||||
filterData() {
|
||||
return this.$store.state.libraries.filterData || {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getDetails() {
|
||||
this.forceBlur()
|
||||
return this.checkForChanges()
|
||||
},
|
||||
getTitleAndAuthorName() {
|
||||
this.forceBlur()
|
||||
return {
|
||||
title: this.details.title,
|
||||
author: this.details.author
|
||||
}
|
||||
},
|
||||
mapBatchDetails(batchDetails) {
|
||||
for (const key in batchDetails) {
|
||||
if (key === 'tags') {
|
||||
this.newTags = [...batchDetails.tags]
|
||||
} else if (key === 'genres') {
|
||||
this.details[key] = [...batchDetails[key]]
|
||||
} else {
|
||||
this.details[key] = batchDetails[key]
|
||||
}
|
||||
}
|
||||
},
|
||||
forceBlur() {
|
||||
if (this.$refs.titleInput) this.$refs.titleInput.blur()
|
||||
if (this.$refs.authorInput) this.$refs.authorInput.blur()
|
||||
if (this.$refs.releaseDateInput) this.$refs.releaseDateInput.blur()
|
||||
if (this.$refs.descriptionInput) this.$refs.descriptionInput.blur()
|
||||
if (this.$refs.feedUrlInput) this.$refs.feedUrlInput.blur()
|
||||
if (this.$refs.itunesIdInput) this.$refs.itunesIdInput.blur()
|
||||
if (this.$refs.languageInput) this.$refs.languageInput.blur()
|
||||
|
||||
if (this.$refs.genresSelect && this.$refs.genresSelect.isFocused) {
|
||||
this.$refs.genresSelect.forceBlur()
|
||||
}
|
||||
if (this.$refs.tagsSelect && this.$refs.tagsSelect.isFocused) {
|
||||
this.$refs.tagsSelect.forceBlur()
|
||||
}
|
||||
},
|
||||
stringArrayEqual(array1, array2) {
|
||||
// return false if different
|
||||
if (array1.length !== array2.length) return false
|
||||
for (var item of array1) {
|
||||
if (!array2.includes(item)) return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
objectArrayEqual(array1, array2) {
|
||||
const isIterable = (value) => {
|
||||
return Symbol.iterator in Object(value)
|
||||
}
|
||||
if (!isIterable(array1) || !isIterable(array2)) {
|
||||
console.error(array1, array2)
|
||||
throw new Error('Invalid arrays passed in')
|
||||
}
|
||||
|
||||
// array of objects with id key
|
||||
if (array1.length !== array2.length) return false
|
||||
|
||||
for (var item of array1) {
|
||||
var matchingItem = array2.find((a) => a.id === item.id)
|
||||
if (!matchingItem) return false
|
||||
for (var key in item) {
|
||||
if (item[key] !== matchingItem[key]) {
|
||||
// console.log('Object array item keys changed', key, item[key], matchingItem[key])
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
checkForChanges() {
|
||||
var metadata = {}
|
||||
for (const key in this.details) {
|
||||
var newValue = this.details[key]
|
||||
var oldValue = this.mediaMetadata[key]
|
||||
// Key cleared out or key first populated
|
||||
if ((!newValue && oldValue) || (newValue && !oldValue)) {
|
||||
metadata[key] = newValue
|
||||
} else if (key === 'genres') {
|
||||
// Check array of strings
|
||||
if (!this.stringArrayEqual(newValue, oldValue)) {
|
||||
metadata[key] = [...newValue]
|
||||
}
|
||||
} else if (newValue && newValue != oldValue) {
|
||||
// Intentional !=
|
||||
metadata[key] = newValue
|
||||
}
|
||||
}
|
||||
var updatePayload = {}
|
||||
if (!!Object.keys(metadata).length) updatePayload.metadata = metadata
|
||||
|
||||
if (!this.stringArrayEqual(this.newTags, this.media.tags || [])) {
|
||||
updatePayload.tags = [...this.newTags]
|
||||
}
|
||||
return {
|
||||
updatePayload,
|
||||
hasChanges: !!Object.keys(updatePayload).length
|
||||
}
|
||||
},
|
||||
init() {
|
||||
this.details.title = this.mediaMetadata.title
|
||||
this.details.author = this.mediaMetadata.author || ''
|
||||
this.details.description = this.mediaMetadata.description || ''
|
||||
this.details.releaseDate = this.mediaMetadata.releaseDate || ''
|
||||
this.details.genres = [...(this.mediaMetadata.genres || [])]
|
||||
this.details.feedUrl = this.mediaMetadata.feedUrl || ''
|
||||
this.details.imageUrl = this.mediaMetadata.imageUrl || ''
|
||||
this.details.itunesPageUrl = this.mediaMetadata.itunesPageUrl || ''
|
||||
this.details.itunesId = this.mediaMetadata.itunesId || ''
|
||||
this.details.itunesArtistId = this.mediaMetadata.itunesArtistId || ''
|
||||
this.details.language = this.mediaMetadata.language || ''
|
||||
this.details.explicit = !!this.mediaMetadata.explicit
|
||||
|
||||
this.autoDownloadEpisodes = !!this.media.autoDownloadEpisodes
|
||||
this.newTags = [...(this.media.tags || [])]
|
||||
},
|
||||
submitForm() {
|
||||
this.$emit('submit')
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
@ -9,20 +9,20 @@
|
||||
<div class="overflow-hidden">
|
||||
<transition name="slide">
|
||||
<div v-if="openMapOptions" class="flex flex-wrap">
|
||||
<div class="flex items-center px-4 w-1/2">
|
||||
<div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
|
||||
<ui-checkbox v-model="selectedBatchUsage.subtitle" />
|
||||
<ui-text-input-with-label ref="subtitleInput" v-model="batchDetails.subtitle" :disabled="!selectedBatchUsage.subtitle" label="Subtitle" class="mb-4 ml-4" />
|
||||
</div>
|
||||
<div class="flex items-center px-4 w-1/2">
|
||||
<div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
|
||||
<ui-checkbox v-model="selectedBatchUsage.authors" />
|
||||
<!-- Authors filter only contains authors in this library, use query input to query all authors -->
|
||||
<ui-multi-select-query-input ref="authorsSelect" v-model="batchDetails.authors" :disabled="!selectedBatchUsage.authors" label="Authors" endpoint="authors/search" class="mb-4 ml-4" />
|
||||
</div>
|
||||
<div class="flex items-center px-4 w-1/2">
|
||||
<div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
|
||||
<ui-checkbox v-model="selectedBatchUsage.publishedYear" />
|
||||
<ui-text-input-with-label ref="publishedYearInput" v-model="batchDetails.publishedYear" :disabled="!selectedBatchUsage.publishedYear" label="Publish Year" class="mb-4 ml-4" />
|
||||
</div>
|
||||
<div class="flex items-center px-4 w-1/2">
|
||||
<div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
|
||||
<ui-checkbox v-model="selectedBatchUsage.series" />
|
||||
<ui-multi-select ref="seriesSelect" v-model="batchDetails.series" :disabled="!selectedBatchUsage.series" label="Series" :items="seriesItems" @newItem="newSeriesItem" @removedItem="removedSeriesItem" class="mb-4 ml-4" />
|
||||
</div>
|
||||
@ -34,11 +34,11 @@
|
||||
<ui-checkbox v-model="selectedBatchUsage.tags" />
|
||||
<ui-multi-select ref="tagsSelect" v-model="batchDetails.tags" label="Tags" :disabled="!selectedBatchUsage.tags" :items="tagItems" @newItem="newTagItem" @removedItem="removedTagItem" class="mb-4 ml-4" />
|
||||
</div>
|
||||
<div class="flex items-center px-4 w-1/2">
|
||||
<div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
|
||||
<ui-checkbox v-model="selectedBatchUsage.narrators" />
|
||||
<ui-multi-select ref="narratorsSelect" v-model="batchDetails.narrators" :disabled="!selectedBatchUsage.narrators" label="Narrators" :items="narratorItems" @newItem="newNarratorItem" @removedItem="removedNarratorItem" class="mb-4 ml-4" />
|
||||
</div>
|
||||
<div class="flex items-center px-4 w-1/2">
|
||||
<div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
|
||||
<ui-checkbox v-model="selectedBatchUsage.publisher" />
|
||||
<ui-text-input-with-label ref="publisherInput" v-model="batchDetails.publisher" :disabled="!selectedBatchUsage.publisher" label="Publisher" class="mb-4 ml-4" />
|
||||
</div>
|
||||
@ -72,7 +72,8 @@
|
||||
<div class="flex justify-center flex-wrap">
|
||||
<template v-for="libraryItem in libraryItemCopies">
|
||||
<div :key="libraryItem.id" class="w-full max-w-3xl border border-black-300 p-6 -ml-px -mt-px">
|
||||
<widgets-item-details-edit :ref="`itemForm-${libraryItem.id}`" :library-item="libraryItem" />
|
||||
<widgets-book-details-edit v-if="libraryItem.mediaType === 'book'" :ref="`itemForm-${libraryItem.id}`" :library-item="libraryItem" />
|
||||
<widgets-podcast-details-edit v-else :ref="`itemForm-${libraryItem.id}`" :library-item="libraryItem" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@ -99,6 +100,7 @@ export default {
|
||||
return []
|
||||
})
|
||||
return {
|
||||
mediaType: libraryItems[0].mediaType,
|
||||
libraryItems
|
||||
}
|
||||
},
|
||||
@ -139,6 +141,9 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isPodcastLibrary() {
|
||||
return this.mediaType === 'podcast'
|
||||
},
|
||||
coverAspectRatio() {
|
||||
return this.$store.getters['getServerSetting']('coverAspectRatio')
|
||||
},
|
||||
|
@ -31,7 +31,7 @@
|
||||
<p v-if="bookSubtitle" class="sm:ml-4 text-gray-400 text-xl md:text-2xl">{{ bookSubtitle }}</p>
|
||||
</div>
|
||||
|
||||
<p v-if="isPodcast" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl">by {{ podcastAuthor }}</p>
|
||||
<p v-if="isPodcast" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl">by {{ podcastAuthor || 'Unknown' }}</p>
|
||||
<p v-else-if="authorsList.length" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl">
|
||||
by <nuxt-link v-for="(author, index) in authorsList" :key="index" :to="`/library/${libraryId}/bookshelf?filter=authors.${$encode(author)}`" class="hover:underline">{{ author }}<span v-if="index < authorsList.length - 1">, </span></nuxt-link>
|
||||
</p>
|
||||
@ -126,15 +126,15 @@
|
||||
<ui-icon-btn icon="edit" class="mx-0.5" @click="editClick" />
|
||||
</ui-tooltip>
|
||||
|
||||
<ui-tooltip v-if="userCanDownload" :disabled="isMissing" text="Download" direction="top">
|
||||
<ui-tooltip v-if="userCanDownload && !isPodcast" :disabled="isMissing" text="Download" direction="top">
|
||||
<ui-icon-btn icon="download" :disabled="isMissing" class="mx-0.5" @click="downloadClick" />
|
||||
</ui-tooltip>
|
||||
|
||||
<ui-tooltip :text="userIsFinished ? 'Mark as Not Finished' : 'Mark as Finished'" direction="top">
|
||||
<ui-tooltip v-if="!isPodcast" :text="userIsFinished ? 'Mark as Not Finished' : 'Mark as Finished'" direction="top">
|
||||
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" class="mx-0.5" @click="toggleFinished" />
|
||||
</ui-tooltip>
|
||||
|
||||
<ui-tooltip text="Collections" direction="top">
|
||||
<ui-tooltip v-if="!isPodcast" text="Collections" direction="top">
|
||||
<ui-icon-btn icon="collections_bookmark" class="mx-0.5" outlined @click="collectionsClick" />
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
|
@ -59,6 +59,26 @@ class PodcastEpisode {
|
||||
}
|
||||
}
|
||||
|
||||
toJSONExpanded() {
|
||||
return {
|
||||
id: this.id,
|
||||
index: this.index,
|
||||
episode: this.episode,
|
||||
episodeType: this.episodeType,
|
||||
title: this.title,
|
||||
subtitle: this.subtitle,
|
||||
description: this.description,
|
||||
enclosure: this.enclosure ? { ...this.enclosure } : null,
|
||||
pubDate: this.pubDate,
|
||||
audioFile: this.audioFile.toJSON(),
|
||||
publishedAt: this.publishedAt,
|
||||
addedAt: this.addedAt,
|
||||
updatedAt: this.updatedAt,
|
||||
duration: this.duration,
|
||||
size: this.size
|
||||
}
|
||||
}
|
||||
|
||||
get tracks() {
|
||||
return [this.audioFile]
|
||||
}
|
||||
|
@ -47,7 +47,8 @@ class Podcast {
|
||||
coverPath: this.coverPath,
|
||||
tags: [...this.tags],
|
||||
episodes: this.episodes.map(e => e.toJSON()),
|
||||
autoDownloadEpisodes: this.autoDownloadEpisodes
|
||||
autoDownloadEpisodes: this.autoDownloadEpisodes,
|
||||
size: this.size
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,8 +57,9 @@ class Podcast {
|
||||
metadata: this.metadata.toJSONExpanded(),
|
||||
coverPath: this.coverPath,
|
||||
tags: [...this.tags],
|
||||
episodes: this.episodes.map(e => e.toJSON()),
|
||||
autoDownloadEpisodes: this.autoDownloadEpisodes
|
||||
episodes: this.episodes.map(e => e.toJSONExpanded()),
|
||||
autoDownloadEpisodes: this.autoDownloadEpisodes,
|
||||
size: this.size
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,6 @@
|
||||
const Logger = require('../../Logger')
|
||||
const { areEquivalent, copyValue } = require('../../utils/index')
|
||||
|
||||
class PodcastMetadata {
|
||||
constructor(metadata) {
|
||||
this.title = null
|
||||
@ -87,5 +90,20 @@ class PodcastMetadata {
|
||||
this.genres = [...mediaMetadata.genres]
|
||||
}
|
||||
}
|
||||
|
||||
update(payload) {
|
||||
var json = this.toJSON()
|
||||
var hasUpdates = false
|
||||
for (const key in json) {
|
||||
if (payload[key] !== undefined) {
|
||||
if (!areEquivalent(payload[key], json[key])) {
|
||||
this[key] = copyValue(payload[key])
|
||||
Logger.debug('[PodcastMetadata] Key updated', key, this[key])
|
||||
hasUpdates = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return hasUpdates
|
||||
}
|
||||
}
|
||||
module.exports = PodcastMetadata
|
@ -120,12 +120,11 @@ function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems) {
|
||||
return libraryItemGroup
|
||||
}
|
||||
|
||||
function cleanFileObjects(libraryItemPath, folderPath, files) {
|
||||
function cleanFileObjects(libraryItemPath, files) {
|
||||
return Promise.all(files.map(async (file) => {
|
||||
var filePath = Path.posix.join(libraryItemPath, file)
|
||||
var relFilePath = filePath.replace(folderPath, '')
|
||||
var newLibraryFile = new LibraryFile()
|
||||
await newLibraryFile.setDataFromPath(filePath, relFilePath)
|
||||
await newLibraryFile.setDataFromPath(filePath, file)
|
||||
return newLibraryFile
|
||||
}))
|
||||
}
|
||||
@ -153,7 +152,7 @@ async function scanFolder(libraryMediaType, folder, serverSettings = {}) {
|
||||
for (const libraryItemPath in libraryItemGrouping) {
|
||||
var libraryItemData = getDataFromMediaDir(libraryMediaType, folderPath, libraryItemPath, serverSettings)
|
||||
|
||||
var fileObjs = await cleanFileObjects(libraryItemData.path, folderPath, libraryItemGrouping[libraryItemPath])
|
||||
var fileObjs = await cleanFileObjects(libraryItemData.path, libraryItemGrouping[libraryItemPath])
|
||||
var libraryItemFolderStats = await getFileTimestampsWithIno(libraryItemData.path)
|
||||
items.push({
|
||||
folderId: folder.id,
|
||||
|
Loading…
Reference in New Issue
Block a user