Merge branch 'master' into getBookDataFromDir-refactor

This commit is contained in:
Cassie Esposito 2022-05-24 12:56:10 -07:00
commit e2e5dd372a
36 changed files with 432 additions and 502 deletions

View File

@ -14,6 +14,6 @@ COPY index.js index.js
COPY package-lock.json package-lock.json COPY package-lock.json package-lock.json
COPY package.json package.json COPY package.json package.json
COPY server server COPY server server
RUN npm ci --production RUN npm ci --only=production
EXPOSE 80 EXPOSE 80
CMD ["npm", "start"] CMD ["npm", "start"]

View File

@ -153,9 +153,6 @@ export default {
}, },
currentChapterName() { currentChapterName() {
return this.currentChapter ? this.currentChapter.title : '' return this.currentChapter ? this.currentChapter.title : ''
},
showExperimentalFeatures() {
return this.$store.state.showExperimentalFeatures
} }
}, },
methods: { methods: {

View File

@ -9,9 +9,13 @@
<div v-show="routeName === route.iod" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> <div v-show="routeName === route.iod" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link> </nuxt-link>
<div class="w-full h-10 px-4 border-t border-black border-opacity-20 absolute left-0 flex flex-col justify-center" :style="{ bottom: streamLibraryItem && isMobileLandscape ? '300px' : '65px' }"> <div class="w-full h-12 px-4 border-t border-black border-opacity-20 absolute left-0 flex flex-col justify-center" :style="{ bottom: streamLibraryItem && isMobileLandscape ? '300px' : '65px' }">
<div class="flex justify-between">
<p class="font-mono text-sm">v{{ $config.version }}</p> <p class="font-mono text-sm">v{{ $config.version }}</p>
<a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-sm">Update available: {{ latestVersion }}</a>
<p class="font-mono text-xs text-gray-300 italic">{{ Source }}</p>
</div>
<a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xs">Latest: {{ latestVersion }}</a>
</div> </div>
</div> </div>
</template> </template>
@ -25,6 +29,9 @@ export default {
return {} return {}
}, },
computed: { computed: {
Source() {
return this.$store.state.Source
},
currentLibraryId() { currentLibraryId() {
return this.$store.state.libraries.currentLibraryId return this.$store.state.libraries.currentLibraryId
}, },

View File

@ -43,7 +43,6 @@ export default {
mixins: [bookshelfCardsHelpers], mixins: [bookshelfCardsHelpers],
data() { data() {
return { return {
routeName: null,
routeFullPath: null, routeFullPath: null,
initialized: false, initialized: false,
bookshelfHeight: 0, bookshelfHeight: 0,
@ -632,7 +631,6 @@ export default {
mounted() { mounted() {
this.initListeners() this.initListeners()
this.routeName = this.$route.name // beforeDestroy will have the new route name already, so need to store this
this.routeFullPath = window.location.pathname + (window.location.search || '') this.routeFullPath = window.location.pathname + (window.location.search || '')
}, },
updated() { updated() {

View File

@ -89,9 +89,6 @@ export default {
offsetTop() { offsetTop() {
return 64 return 64
}, },
showExperimentalFeatures() {
return this.$store.state.showExperimentalFeatures
},
userIsAdminOrUp() { userIsAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp'] return this.$store.getters['user/getIsAdminOrUp']
}, },

View File

@ -74,9 +74,6 @@ export default {
} }
}, },
computed: { computed: {
showExperimentalFeatures() {
return this.$store.state.showExperimentalFeatures
},
coverAspectRatio() { coverAspectRatio() {
return this.$store.getters['getServerSetting']('coverAspectRatio') return this.$store.getters['getServerSetting']('coverAspectRatio')
}, },

View File

@ -109,19 +109,14 @@ export default {
hasValidCovers() { hasValidCovers() {
var validCovers = this.bookItems.map((bookItem) => bookItem.media.coverPath) var validCovers = this.bookItems.map((bookItem) => bookItem.media.coverPath)
return !!validCovers.length return !!validCovers.length
},
showExperimentalFeatures() {
return this.$store.state.showExperimentalFeatures
} }
}, },
methods: { methods: {
mouseoverCard() { mouseoverCard() {
this.isHovering = true this.isHovering = true
// if (this.$refs.groupcover) this.$refs.groupcover.setHover(true)
}, },
mouseleaveCard() { mouseleaveCard() {
this.isHovering = false this.isHovering = false
// if (this.$refs.groupcover) this.$refs.groupcover.setHover(false)
}, },
clickCard() { clickCard() {
this.$emit('click', this.group) this.$emit('click', this.group)

View File

@ -147,6 +147,9 @@ export default {
showExperimentalFeatures() { showExperimentalFeatures() {
return this.store.state.showExperimentalFeatures return this.store.state.showExperimentalFeatures
}, },
enableEReader() {
return this.store.getters['getServerSetting']('enableEReader')
},
_libraryItem() { _libraryItem() {
return this.libraryItem || {} return this.libraryItem || {}
}, },
@ -287,13 +290,13 @@ export default {
return this.store.getters['getlibraryItemIdStreaming'] === this.libraryItemId return this.store.getters['getlibraryItemIdStreaming'] === this.libraryItemId
}, },
showReadButton() { showReadButton() {
return !this.isSelectionMode && this.showExperimentalFeatures && !this.showPlayButton && this.hasEbook return !this.isSelectionMode && !this.showPlayButton && this.hasEbook && (this.showExperimentalFeatures || this.enableEReader)
}, },
showPlayButton() { showPlayButton() {
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && !this.isStreaming && (this.numTracks || this.recentEpisode) return !this.isSelectionMode && !this.isMissing && !this.isInvalid && !this.isStreaming && (this.numTracks || this.recentEpisode)
}, },
showSmallEBookIcon() { showSmallEBookIcon() {
return !this.isSelectionMode && this.showExperimentalFeatures && this.hasEbook return !this.isSelectionMode && this.hasEbook && (this.showExperimentalFeatures || this.enableEReader)
}, },
isMissing() { isMissing() {
return this._libraryItem.isMissing return this._libraryItem.isMissing

View File

@ -33,8 +33,8 @@ export default {
showMenu: false, showMenu: false,
items: [ items: [
{ {
text: 'Current', text: 'Pub Date',
value: 'index' value: 'publishedAt'
}, },
{ {
text: 'Title', text: 'Title',
@ -47,10 +47,6 @@ export default {
{ {
text: 'Episode', text: 'Episode',
value: 'episode' value: 'episode'
},
{
text: 'Pub Date',
value: 'publishedAt'
} }
] ]
} }

View File

@ -59,9 +59,6 @@ export default {
if (this.bookCoverAspectRatio === 1) return this.width / (120 * 1.6 * 2) if (this.bookCoverAspectRatio === 1) return this.width / (120 * 1.6 * 2)
return this.width / 240 return this.width / 240
}, },
showExperimentalFeatures() {
return this.store.state.showExperimentalFeatures
},
store() { store() {
return this.$store || this.$nuxt.$store return this.$store || this.$nuxt.$store
}, },

View File

@ -6,12 +6,12 @@
</div> </div>
</template> </template>
<div class="p-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh"> <div class="p-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
<form @submit.prevent="submitForm"> <form v-if="author" @submit.prevent="submitForm">
<div class="flex"> <div class="flex">
<div class="w-40 p-2"> <div class="w-40 p-2">
<div class="w-full h-45 relative"> <div class="w-full h-45 relative">
<covers-author-image :author="author" /> <covers-author-image :author="author" />
<div v-show="!processing" class="absolute top-0 left-0 w-full h-full opacity-0 hover:opacity-100"> <div v-show="!processing && author.imagePath" class="absolute top-0 left-0 w-full h-full opacity-0 hover:opacity-100">
<span class="absolute top-2 right-2 material-icons text-error transform hover:scale-125 transition-transform cursor-pointer text-lg" @click="removeCover">delete</span> <span class="absolute top-2 right-2 material-icons text-error transform hover:scale-125 transition-transform cursor-pointer text-lg" @click="removeCover">delete</span>
</div> </div>
</div> </div>

View File

@ -64,8 +64,7 @@ export default {
{ {
id: 'manage', id: 'manage',
title: 'Manage', title: 'Manage',
component: 'modals-item-tabs-manage', component: 'modals-item-tabs-manage'
experimental: true
} }
] ]
} }

View File

@ -26,7 +26,7 @@
</div> </div>
<!-- Split to mp3 --> <!-- Split to mp3 -->
<div v-if="showMp3Split" class="w-full border border-black-200 p-4 my-8"> <div v-if="showMp3Split && showExperimentalFeatures" class="w-full border border-black-200 p-4 my-8">
<div class="flex items-center"> <div class="flex items-center">
<div> <div>
<p class="text-lg">Split M4B to MP3's</p> <p class="text-lg">Split M4B to MP3's</p>
@ -51,7 +51,7 @@
</div> </div>
<!-- Embed Metadata --> <!-- Embed Metadata -->
<div v-if="mediaTracks.length" class="w-full border border-black-200 p-4 my-8"> <div v-if="mediaTracks.length && showExperimentalFeatures" class="w-full border border-black-200 p-4 my-8">
<div class="flex items-center"> <div class="flex items-center">
<div> <div>
<p class="text-lg">Embed Metadata</p> <p class="text-lg">Embed Metadata</p>
@ -113,6 +113,9 @@ export default {
} }
}, },
computed: { computed: {
showExperimentalFeatures() {
return this.$store.state.showExperimentalFeatures
},
libraryItemId() { libraryItemId() {
return this.libraryItem ? this.libraryItem.id : null return this.libraryItem ? this.libraryItem.id : null
}, },

View File

@ -28,10 +28,9 @@
<ui-editable-text v-model="newFolderPath" placeholder="New folder path" type="text" class="w-full" @blur="newFolderInputBlurred" /> <ui-editable-text v-model="newFolderPath" placeholder="New folder path" type="text" class="w-full" @blur="newFolderInputBlurred" />
</div> </div>
<ui-btn class="w-full mt-2" color="primary" @click="showDirectoryPicker = true">Browse for Folder</ui-btn> <ui-btn class="w-full mt-2" color="primary" @click="browseForFolder">Browse for Folder</ui-btn>
</div> </div>
</div> </div>
<modals-libraries-folder-chooser v-else :paths="folderPaths" @back="showDirectoryPicker = false" @select="selectFolder" /> <modals-libraries-folder-chooser v-else :paths="folderPaths" @back="showDirectoryPicker = false" @select="selectFolder" />
</div> </div>
</template> </template>
@ -77,6 +76,9 @@ export default {
} }
}, },
methods: { methods: {
browseForFolder() {
this.showDirectoryPicker = true
},
getLibraryData() { getLibraryData() {
return { return {
name: this.name, name: this.name,

View File

@ -1,11 +1,6 @@
<template> <template>
<div class="w-full px-2 py-3 overflow-hidden relative border-b border-white border-opacity-10" @mouseover="mouseover" @mouseleave="mouseleave"> <div class="w-full px-2 py-3 overflow-hidden relative border-b border-white border-opacity-10" @mouseover="mouseover" @mouseleave="mouseleave">
<div v-if="episode" class="flex items-center h-24"> <div v-if="episode" class="flex items-center h-24">
<div v-show="userCanUpdate" class="w-12 min-w-12 max-w-16 h-full">
<div class="flex h-full items-center justify-center">
<span class="material-icons drag-handle text-lg text-white text-opacity-50 hover:text-opacity-100">menu</span>
</div>
</div>
<div class="flex-grow px-2"> <div class="flex-grow px-2">
<p class="text-sm font-semibold"> <p class="text-sm font-semibold">
{{ title }} {{ title }}
@ -49,8 +44,8 @@ export default {
episode: { episode: {
type: Object, type: Object,
default: () => {} default: () => {}
}, }
isDragging: Boolean // isDragging: Boolean
}, },
data() { data() {
return { return {
@ -59,15 +54,15 @@ export default {
isHovering: false isHovering: false
} }
}, },
watch: { // watch: {
isDragging: { // isDragging: {
handler(newVal) { // handler(newVal) {
if (newVal) { // if (newVal) {
this.isHovering = false // this.isHovering = false
} // }
} // }
} // }
}, // },
computed: { computed: {
userCanUpdate() { userCanUpdate() {
return this.$store.getters['user/getUserCanUpdate'] return this.$store.getters['user/getUserCanUpdate']
@ -117,7 +112,7 @@ export default {
}, },
methods: { methods: {
mouseover() { mouseover() {
if (this.isDragging) return // if (this.isDragging) return
this.isHovering = true this.isHovering = true
}, },
mouseleave() { mouseleave() {

View File

@ -9,23 +9,14 @@
</div> </div>
</div> </div>
<p v-if="!episodes.length" class="py-4 text-center text-lg">No Episodes</p> <p v-if="!episodes.length" class="py-4 text-center text-lg">No Episodes</p>
<draggable v-model="episodesCopy" v-bind="dragOptions" class="list-group" handle=".drag-handle" draggable=".item" tag="div" @start="drag = true" @end="drag = false" @update="draggableUpdate"> <template v-for="episode in episodes">
<transition-group type="transition" :name="!drag ? 'episode' : null"> <tables-podcast-episode-table-row :key="episode.id" :episode="episode" :library-item-id="libraryItem.id" class="item" @edit="editEpisode" />
<template v-for="episode in episodesCopy">
<tables-podcast-episode-table-row :key="episode.id" :is-dragging="drag" :episode="episode" :library-item-id="libraryItem.id" class="item" :class="drag ? '' : 'episode'" @edit="editEpisode" />
</template> </template>
</transition-group>
</draggable>
</div> </div>
</template> </template>
<script> <script>
import draggable from 'vuedraggable'
export default { export default {
components: {
draggable
},
props: { props: {
libraryItem: { libraryItem: {
type: Object, type: Object,
@ -34,30 +25,11 @@ export default {
}, },
data() { data() {
return { return {
sortKey: 'index', sortKey: 'publishedAt',
sortDesc: true, sortDesc: true
drag: false,
episodesCopy: [],
orderChanged: false,
savingOrder: false
}
},
watch: {
libraryItem: {
handler(newVal) {
this.init()
}
} }
}, },
computed: { computed: {
dragOptions() {
return {
animation: 200,
group: 'description',
ghostClass: 'ghost',
disabled: !this.userCanUpdate
}
},
userCanUpdate() { userCanUpdate() {
return this.$store.getters['user/getUserCanUpdate'] return this.$store.getters['user/getUserCanUpdate']
}, },
@ -72,66 +44,13 @@ export default {
} }
}, },
methods: { methods: {
changeSort() {
this.episodesCopy.sort((a, b) => {
if (this.sortDesc) {
return String(b[this.sortKey]).localeCompare(String(a[this.sortKey]), undefined, { numeric: true, sensitivity: 'base' })
}
return String(a[this.sortKey]).localeCompare(String(b[this.sortKey]), undefined, { numeric: true, sensitivity: 'base' })
})
this.orderChanged = this.checkHasOrderChanged()
},
checkHasOrderChanged() {
for (let i = 0; i < this.episodesCopy.length; i++) {
var epc = this.episodesCopy[i]
var ep = this.episodes[i]
if (epc.index != ep.index) {
return true
}
}
return false
},
editEpisode(episode) { editEpisode(episode) {
this.$store.commit('setSelectedLibraryItem', this.libraryItem) this.$store.commit('setSelectedLibraryItem', this.libraryItem)
this.$store.commit('globals/setSelectedEpisode', episode) this.$store.commit('globals/setSelectedEpisode', episode)
this.$store.commit('globals/setShowEditPodcastEpisodeModal', true) this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)
},
draggableUpdate() {
this.orderChanged = this.checkHasOrderChanged()
},
async saveOrder() {
if (!this.userCanUpdate) return
this.savingOrder = true
var episodesUpdate = {
episodes: this.episodesCopy.map((b) => b.id)
}
await this.$axios
.$patch(`/api/items/${this.libraryItem.id}/episodes`, episodesUpdate)
.then((podcast) => {
console.log('Podcast updated', podcast)
this.$toast.success('Saved episode order')
this.orderChanged = false
})
.catch((error) => {
console.error('Failed to update podcast', error)
this.$toast.error('Failed to save podcast episode order')
})
this.savingOrder = false
},
init() {
this.episodesCopy = this.episodes.map((ep) => {
return {
...ep
}
})
} }
}, },
mounted() { mounted() {}
this.init()
}
} }
</script> </script>

View File

@ -126,9 +126,6 @@ export default {
} }
}, },
computed: { computed: {
showExperimentalFeatures() {
return this.$store.state.showExperimentalFeatures
},
media() { media() {
return this.libraryItem.media || {} return this.libraryItem.media || {}
}, },

View File

@ -122,6 +122,20 @@
</p> </p>
</ui-tooltip> </ui-tooltip>
</div> </div>
<div class="flex items-center mb-2 mt-8">
<h1 class="text-xl">Experimental Feature Settings</h1>
</div>
<div class="flex items-center py-2">
<ui-toggle-switch v-model="newServerSettings.enableEReader" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('enableEReader', val)" />
<ui-tooltip :text="tooltips.enableEReader">
<p class="pl-4 text-lg">
Enable e-reader for all users
<span class="material-icons icon-text">info_outlined</span>
</p>
</ui-tooltip>
</div>
</div> </div>
<div class="h-0.5 bg-primary bg-opacity-30 w-full" /> <div class="h-0.5 bg-primary bg-opacity-30 w-full" />
@ -169,10 +183,12 @@
<div> <div>
<div class="flex items-center"> <div class="flex items-center">
<ui-toggle-switch v-model="showExperimentalFeatures" /> <ui-toggle-switch v-model="showExperimentalFeatures" />
<ui-tooltip :text="experimentalFeaturesTooltip"> <ui-tooltip :text="tooltips.experimentalFeatures">
<p class="pl-4 text-lg"> <p class="pl-4 text-lg">
Experimental Features Experimental Features
<a href="https://github.com/advplyr/audiobookshelf/discussions/75" target="_blank">
<span class="material-icons icon-text">info_outlined</span> <span class="material-icons icon-text">info_outlined</span>
</a>
</p> </p>
</ui-tooltip> </ui-tooltip>
</div> </div>
@ -207,6 +223,7 @@ export default {
isPurgingCache: false, isPurgingCache: false,
newServerSettings: {}, newServerSettings: {},
tooltips: { tooltips: {
experimentalFeatures: 'Features in development that could use your feedback and help testing. Click to open github discussion.',
scannerDisableWatcher: 'Disables the automatic adding/updating of items when file changes are detected. *Requires server restart', scannerDisableWatcher: 'Disables the automatic adding/updating of items when file changes are detected. *Requires server restart',
scannerPreferOpfMetadata: 'OPF file metadata will be used for book details over folder names', scannerPreferOpfMetadata: 'OPF file metadata will be used for book details over folder names',
scannerPreferAudioMetadata: 'Audio file ID3 meta tags will be used for book details over folder names', scannerPreferAudioMetadata: 'Audio file ID3 meta tags will be used for book details over folder names',
@ -216,7 +233,8 @@ export default {
bookshelfView: 'Alternative view without wooden bookshelf', bookshelfView: 'Alternative view without wooden bookshelf',
storeCoverWithItem: 'By default covers are stored in /metadata/items, enabling this setting will store covers in your library item folder. Only one file named "cover" will be kept', storeCoverWithItem: 'By default covers are stored in /metadata/items, enabling this setting will store covers in your library item folder. Only one file named "cover" will be kept',
storeMetadataWithItem: 'By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders. Uses .abs file extension', storeMetadataWithItem: 'By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders. Uses .abs file extension',
coverAspectRatio: 'Prefer to use square covers over standard 1.6:1 book covers' coverAspectRatio: 'Prefer to use square covers over standard 1.6:1 book covers',
enableEReader: 'E-reader is still a work in progress, but use this setting to open it up to all your users (or use the "Experimental Features" toggle below just for you)'
}, },
showConfirmPurgeCache: false showConfirmPurgeCache: false
} }
@ -229,9 +247,6 @@ export default {
} }
}, },
computed: { computed: {
experimentalFeaturesTooltip() {
return 'Features in development that could use your feedback and help testing.'
},
serverSettings() { serverSettings() {
return this.$store.state.serverSettings return this.$store.state.serverSettings
}, },

View File

@ -104,9 +104,6 @@ export default {
bookCoverAspectRatio() { bookCoverAspectRatio() {
return this.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE ? 1 : 1.6 return this.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE ? 1 : 1.6
}, },
showExperimentalFeatures() {
return this.$store.state.showExperimentalFeatures
},
username() { username() {
return this.user.username return this.user.username
}, },

View File

@ -92,7 +92,8 @@
<!-- Alerts --> <!-- Alerts -->
<div v-show="showExperimentalReadAlert" class="bg-error p-4 rounded-xl flex items-center"> <div v-show="showExperimentalReadAlert" class="bg-error p-4 rounded-xl flex items-center">
<span class="material-icons text-2xl">warning_amber</span> <span class="material-icons text-2xl">warning_amber</span>
<p class="ml-4">Book has no audio tracks but has valid ebook files. The e-reader is experimental and can be turned on in config.</p> <p v-if="userIsAdminOrUp" class="ml-4">Book has no audio tracks but has an ebook. The experimental e-reader can be enabled in config.</p>
<p v-else class="ml-4">Book has no audio tracks but has an ebook. The experimental e-reader must be enabled by a server admin.</p>
</div> </div>
<!-- Podcast episode downloads queue --> <!-- Podcast episode downloads queue -->
@ -135,7 +136,7 @@
{{ isMissing ? 'Missing' : 'Incomplete' }} {{ isMissing ? 'Missing' : 'Incomplete' }}
</ui-btn> </ui-btn>
<ui-btn v-if="showExperimentalFeatures && ebookFile" color="info" :padding-x="4" small class="flex items-center h-9 mr-2" @click="openEbook"> <ui-btn v-if="showReadButton" color="info" :padding-x="4" small class="flex items-center h-9 mr-2" @click="openEbook">
<span class="material-icons -ml-2 pr-2 text-white">auto_stories</span> <span class="material-icons -ml-2 pr-2 text-white">auto_stories</span>
Read Read
</ui-btn> </ui-btn>
@ -223,6 +224,12 @@ export default {
} }
}, },
computed: { computed: {
showExperimentalFeatures() {
return this.$store.state.showExperimentalFeatures
},
enableEReader() {
return this.$store.getters['getServerSetting']('enableEReader')
},
userIsAdminOrUp() { userIsAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp'] return this.$store.getters['user/getIsAdminOrUp']
}, },
@ -241,9 +248,6 @@ export default {
isDeveloperMode() { isDeveloperMode() {
return this.$store.state.developerMode return this.$store.state.developerMode
}, },
showExperimentalFeatures() {
return this.$store.state.showExperimentalFeatures
},
isPodcast() { isPodcast() {
return this.libraryItem.mediaType === 'podcast' return this.libraryItem.mediaType === 'podcast'
}, },
@ -262,6 +266,9 @@ export default {
if (this.isPodcast) return this.podcastEpisodes.length if (this.isPodcast) return this.podcastEpisodes.length
return this.tracks.length return this.tracks.length
}, },
showReadButton() {
return this.ebookFile && (this.showExperimentalFeatures || this.enableEReader)
},
libraryId() { libraryId() {
return this.libraryItem.libraryId return this.libraryItem.libraryId
}, },
@ -342,7 +349,7 @@ export default {
return this.media.ebookFile return this.media.ebookFile
}, },
showExperimentalReadAlert() { showExperimentalReadAlert() {
return !this.tracks.length && this.ebookFile && !this.showExperimentalFeatures return !this.tracks.length && this.ebookFile && !this.showExperimentalFeatures && !this.enableEReader
}, },
description() { description() {
return this.mediaMetadata.description || '' return this.mediaMetadata.description || ''

View File

@ -124,8 +124,9 @@ export default {
location.reload() location.reload()
}, },
setUser({ user, userDefaultLibraryId, serverSettings }) { setUser({ user, userDefaultLibraryId, serverSettings, Source }) {
this.$store.commit('setServerSettings', serverSettings) this.$store.commit('setServerSettings', serverSettings)
this.$store.commit('setSource', Source)
if (serverSettings.chromecastEnabled) { if (serverSettings.chromecastEnabled) {
console.log('Chromecast enabled import script') console.log('Chromecast enabled import script')

View File

@ -163,17 +163,26 @@ Vue.prototype.$sanitizeSlug = (str) => {
Vue.prototype.$copyToClipboard = (str, ctx) => { Vue.prototype.$copyToClipboard = (str, ctx) => {
return new Promise((resolve) => { return new Promise((resolve) => {
if (!navigator.clipboard) { if (!navigator.clipboard) {
console.warn('Clipboard not supported')
return resolve(false)
}
navigator.clipboard.writeText(str).then(() => { navigator.clipboard.writeText(str).then(() => {
console.log('Clipboard copy success', str) if (ctx) ctx.$toast.success('Copied to clipboard')
ctx.$toast.success('Copied to clipboard')
resolve(true) resolve(true)
}, (err) => { }, (err) => {
console.error('Clipboard copy failed', str, err) console.error('Clipboard copy failed', str, err)
resolve(false) resolve(false)
}) })
} else {
const el = document.createElement('textarea')
el.value = str
el.setAttribute('readonly', '')
el.style.position = 'absolute'
el.style.left = '-9999px'
document.body.appendChild(el)
el.select()
document.execCommand('copy')
document.body.removeChild(el)
if (ctx) ctx.$toast.success('Copied to clipboard')
}
}) })
} }

View File

@ -2,6 +2,7 @@ import { checkForUpdate } from '@/plugins/version'
import Vue from 'vue' import Vue from 'vue'
export const state = () => ({ export const state = () => ({
Source: null,
versionData: null, versionData: null,
serverSettings: null, serverSettings: null,
streamLibraryItem: null, streamLibraryItem: null,
@ -81,6 +82,9 @@ export const actions = {
} }
export const mutations = { export const mutations = {
setSource(state, source) {
state.Source = source
},
setLastBookshelfScrollData(state, { scrollTop, path, name }) { setLastBookshelfScrollData(state, { scrollTop, path, name }) {
state.lastBookshelfScrollData[name] = { scrollTop, path } state.lastBookshelfScrollData[name] = { scrollTop, path }
}, },

View File

@ -6,9 +6,9 @@
"scripts": { "scripts": {
"dev": "node index.js", "dev": "node index.js",
"start": "node index.js", "start": "node index.js",
"client": "cd client && npm install && npm run generate", "client": "cd client && npm ci && npm run generate",
"prod": "npm run client && npm install && node prod.js", "prod": "npm run client && npm ci && node prod.js",
"build-win": "pkg -t node16-win-x64 -o ./dist/win/audiobookshelf -C GZip .", "build-win": "npm run client && pkg -t node16-win-x64 -o ./dist/win/audiobookshelf -C GZip .",
"build-linux": "build/linuxpackager", "build-linux": "build/linuxpackager",
"docker": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 --push . -t advplyr/audiobookshelf", "docker": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 --push . -t advplyr/audiobookshelf",
"deploy": "node dist/autodeploy" "deploy": "node dist/autodeploy"
@ -52,6 +52,5 @@
"string-strip-html": "^8.3.0", "string-strip-html": "^8.3.0",
"watcher": "^1.2.0", "watcher": "^1.2.0",
"xml2js": "^0.4.23" "xml2js": "^0.4.23"
}, }
"devDependencies": {}
} }

View File

@ -96,7 +96,8 @@ class Auth {
return { return {
user: user.toJSONForBrowser(), user: user.toJSONForBrowser(),
userDefaultLibraryId: user.getDefaultLibraryId(this.db.libraries), userDefaultLibraryId: user.getDefaultLibraryId(this.db.libraries),
serverSettings: this.db.serverSettings.toJSON() serverSettings: this.db.serverSettings.toJSON(),
Source: global.Source
} }
} }

View File

@ -35,9 +35,9 @@ const RssFeedManager = require('./managers/RssFeedManager')
class Server { class Server {
constructor(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH) { constructor(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH) {
this.Source = SOURCE
this.Port = PORT this.Port = PORT
this.Host = HOST this.Host = HOST
global.Source = SOURCE
global.Uid = isNaN(UID) ? 0 : Number(UID) global.Uid = isNaN(UID) ? 0 : Number(UID)
global.Gid = isNaN(GID) ? 0 : Number(GID) global.Gid = isNaN(GID) ? 0 : Number(GID)
global.ConfigPath = Path.normalize(CONFIG_PATH) global.ConfigPath = Path.normalize(CONFIG_PATH)

View File

@ -162,13 +162,6 @@ class FolderWatcher extends EventEmitter {
} }
var folderFullPath = folder.fullPath.replace(/\\/g, '/') var folderFullPath = folder.fullPath.replace(/\\/g, '/')
// Check if file was added to root directory
var dir = Path.dirname(path)
if (dir === folderFullPath) {
Logger.warn(`[Watcher] New file "${Path.basename(path)}" added to folder root - ignoring it`)
return
}
var relPath = path.replace(folderFullPath, '') var relPath = path.replace(folderFullPath, '')
var hasDotPath = relPath.split('/').find(p => p.startsWith('.')) var hasDotPath = relPath.split('/').find(p => p.startsWith('.'))

View File

@ -224,20 +224,6 @@ class LibraryItemController {
res.json(libraryItem.toJSON()) res.json(libraryItem.toJSON())
} }
// PATCH: api/items/:id/episodes
async updateEpisodes(req, res) { // For updating podcast episode order
var libraryItem = req.libraryItem
var orderedFileData = req.body.episodes
if (!libraryItem.media.setEpisodeOrder) {
Logger.error(`[LibraryItemController] updateEpisodes invalid media type ${libraryItem.id}`)
return res.sendStatus(500)
}
libraryItem.media.setEpisodeOrder(orderedFileData)
await this.db.updateLibraryItem(libraryItem)
this.emitter('item_updated', libraryItem.toJSONExpanded())
res.json(libraryItem.toJSON())
}
// DELETE: api/items/:id/episode/:episodeId // DELETE: api/items/:id/episode/:episodeId
async removeEpisode(req, res) { async removeEpisode(req, res) {
var episodeId = req.params.episodeId var episodeId = req.params.episodeId

View File

@ -242,7 +242,8 @@ class MiscController {
const userResponse = { const userResponse = {
user: req.user, user: req.user,
userDefaultLibraryId: req.user.getDefaultLibraryId(this.db.libraries), userDefaultLibraryId: req.user.getDefaultLibraryId(this.db.libraries),
serverSettings: this.db.serverSettings.toJSON() serverSettings: this.db.serverSettings.toJSON(),
Source: global.Source
} }
res.json(userResponse) res.json(userResponse)
} }

View File

@ -224,18 +224,10 @@ class Podcast {
this.episodes.push(pe) this.episodes.push(pe)
} }
setEpisodeOrder(episodeIds) {
episodeIds.reverse() // episode Ids will already be in descending order
this.episodes = this.episodes.map(ep => {
var indexOf = episodeIds.findIndex(id => id === ep.id)
ep.index = indexOf + 1
return ep
})
this.episodes.sort((a, b) => b.index - a.index)
}
reorderEpisodes() { reorderEpisodes() {
var hasUpdates = false var hasUpdates = false
// TODO: Sort by published date
this.episodes = naturalSort(this.episodes).asc((ep) => ep.bestFilename) this.episodes = naturalSort(this.episodes).asc((ep) => ep.bestFilename)
for (let i = 0; i < this.episodes.length; i++) { for (let i = 0; i < this.episodes.length; i++) {
if (this.episodes[i].index !== (i + 1)) { if (this.episodes[i].index !== (i + 1)) {

View File

@ -5,10 +5,6 @@ class ServerSettings {
constructor(settings) { constructor(settings) {
this.id = 'server-settings' this.id = 'server-settings'
// Misc/Unused
this.autoTagNew = false
this.newTagExpireDays = 15
// Scanner // Scanner
this.scannerParseSubtitle = false this.scannerParseSubtitle = false
this.scannerFindCovers = false this.scannerFindCovers = false
@ -43,11 +39,16 @@ class ServerSettings {
// Podcasts // Podcasts
this.podcastEpisodeSchedule = '0 * * * *' // Every hour this.podcastEpisodeSchedule = '0 * * * *' // Every hour
// Sorting
this.sortingIgnorePrefix = false this.sortingIgnorePrefix = false
this.sortingPrefixes = ['the', 'a'] this.sortingPrefixes = ['the', 'a']
// Misc Flags
this.chromecastEnabled = false this.chromecastEnabled = false
this.enableEReader = false
this.logLevel = Logger.logLevel this.logLevel = Logger.logLevel
this.version = null this.version = null
if (settings) { if (settings) {
@ -56,8 +57,6 @@ class ServerSettings {
} }
construct(settings) { construct(settings) {
this.autoTagNew = settings.autoTagNew
this.newTagExpireDays = settings.newTagExpireDays
this.scannerFindCovers = !!settings.scannerFindCovers this.scannerFindCovers = !!settings.scannerFindCovers
this.scannerCoverProvider = settings.scannerCoverProvider || 'google' this.scannerCoverProvider = settings.scannerCoverProvider || 'google'
this.scannerParseSubtitle = settings.scannerParseSubtitle this.scannerParseSubtitle = settings.scannerParseSubtitle
@ -91,6 +90,7 @@ class ServerSettings {
this.sortingIgnorePrefix = !!settings.sortingIgnorePrefix this.sortingIgnorePrefix = !!settings.sortingIgnorePrefix
this.sortingPrefixes = settings.sortingPrefixes || ['the', 'a'] this.sortingPrefixes = settings.sortingPrefixes || ['the', 'a']
this.chromecastEnabled = !!settings.chromecastEnabled this.chromecastEnabled = !!settings.chromecastEnabled
this.enableEReader = !!settings.enableEReader
this.logLevel = settings.logLevel || Logger.logLevel this.logLevel = settings.logLevel || Logger.logLevel
this.version = settings.version || null this.version = settings.version || null
@ -102,8 +102,6 @@ class ServerSettings {
toJSON() { toJSON() {
return { return {
id: this.id, id: this.id,
autoTagNew: this.autoTagNew,
newTagExpireDays: this.newTagExpireDays,
scannerFindCovers: this.scannerFindCovers, scannerFindCovers: this.scannerFindCovers,
scannerCoverProvider: this.scannerCoverProvider, scannerCoverProvider: this.scannerCoverProvider,
scannerParseSubtitle: this.scannerParseSubtitle, scannerParseSubtitle: this.scannerParseSubtitle,
@ -125,6 +123,7 @@ class ServerSettings {
sortingIgnorePrefix: this.sortingIgnorePrefix, sortingIgnorePrefix: this.sortingIgnorePrefix,
sortingPrefixes: [...this.sortingPrefixes], sortingPrefixes: [...this.sortingPrefixes],
chromecastEnabled: this.chromecastEnabled, chromecastEnabled: this.chromecastEnabled,
enableEReader: this.enableEReader,
logLevel: this.logLevel, logLevel: this.logLevel,
version: this.version version: this.version
} }

View File

@ -90,7 +90,6 @@ class ApiRouter {
this.router.post('/items/:id/play', LibraryItemController.middleware.bind(this), LibraryItemController.startPlaybackSession.bind(this)) this.router.post('/items/:id/play', LibraryItemController.middleware.bind(this), LibraryItemController.startPlaybackSession.bind(this))
this.router.post('/items/:id/play/:episodeId', LibraryItemController.middleware.bind(this), LibraryItemController.startEpisodePlaybackSession.bind(this)) this.router.post('/items/:id/play/:episodeId', LibraryItemController.middleware.bind(this), LibraryItemController.startEpisodePlaybackSession.bind(this))
this.router.patch('/items/:id/tracks', LibraryItemController.middleware.bind(this), LibraryItemController.updateTracks.bind(this)) this.router.patch('/items/:id/tracks', LibraryItemController.middleware.bind(this), LibraryItemController.updateTracks.bind(this))
this.router.patch('/items/:id/episodes', LibraryItemController.middleware.bind(this), LibraryItemController.updateEpisodes.bind(this))
this.router.delete('/items/:id/episode/:episodeId', LibraryItemController.middleware.bind(this), LibraryItemController.removeEpisode.bind(this)) this.router.delete('/items/:id/episode/:episodeId', LibraryItemController.middleware.bind(this), LibraryItemController.removeEpisode.bind(this))
this.router.get('/items/:id/scan', LibraryItemController.middleware.bind(this), LibraryItemController.scan.bind(this)) this.router.get('/items/:id/scan', LibraryItemController.middleware.bind(this), LibraryItemController.scan.bind(this))
this.router.get('/items/:id/audio-metadata', LibraryItemController.middleware.bind(this), LibraryItemController.updateAudioFileMetadata.bind(this)) this.router.get('/items/:id/audio-metadata', LibraryItemController.middleware.bind(this), LibraryItemController.updateAudioFileMetadata.bind(this))

View File

@ -17,7 +17,9 @@ class StaticRouter {
if (!item) return res.status(404).send('Item not found with id ' + req.params.id) if (!item) return res.status(404).send('Item not found with id ' + req.params.id)
var remainingPath = req.params['0'] var remainingPath = req.params['0']
var fullPath = Path.join(item.path, remainingPath) var fullPath = null
if (item.isFile) fullPath = item.path
else fullPath = Path.join(item.path, remainingPath)
res.sendFile(fullPath) res.sendFile(fullPath)
}) })
} }

View File

@ -62,7 +62,8 @@ class Scanner {
} }
async scanLibraryItem(libraryMediaType, folder, libraryItem) { async scanLibraryItem(libraryMediaType, folder, libraryItem) {
var libraryItemData = await getLibraryItemFileData(libraryMediaType, folder, libraryItem.path, this.db.serverSettings) // TODO: Support for single media item
var libraryItemData = await getLibraryItemFileData(libraryMediaType, folder, libraryItem.path, false, this.db.serverSettings)
if (!libraryItemData) { if (!libraryItemData) {
return ScanResult.NOTHING return ScanResult.NOTHING
} }
@ -499,7 +500,11 @@ class Scanner {
continue; continue;
} }
var relFilePaths = folderGroups[folderId].fileUpdates.map(fileUpdate => fileUpdate.relPath) var relFilePaths = folderGroups[folderId].fileUpdates.map(fileUpdate => fileUpdate.relPath)
var fileUpdateGroup = groupFilesIntoLibraryItemPaths(relFilePaths, true) var fileUpdateGroup = groupFilesIntoLibraryItemPaths(library.mediaType, relFilePaths)
if (!Object.keys(fileUpdateGroup).length) {
Logger.info(`[Scanner] No important changes to scan for in folder "${folderId}"`)
continue;
}
var folderScanResults = await this.scanFolderUpdates(library, folder, fileUpdateGroup) var folderScanResults = await this.scanFolderUpdates(library, folder, fileUpdateGroup)
Logger.debug(`[Scanner] Folder scan results`, folderScanResults) Logger.debug(`[Scanner] Folder scan results`, folderScanResults)
} }
@ -513,6 +518,8 @@ class Scanner {
// Test Case: Moving audio files from library item folder to author folder should trigger a re-scan of the item // Test Case: Moving audio files from library item folder to author folder should trigger a re-scan of the item
var updateGroup = { ...fileUpdateGroup } var updateGroup = { ...fileUpdateGroup }
for (const itemDir in updateGroup) { for (const itemDir in updateGroup) {
if (itemDir == fileUpdateGroup[itemDir]) continue; // Media in root path
var itemDirNestedFiles = fileUpdateGroup[itemDir].filter(b => b.includes('/')) var itemDirNestedFiles = fileUpdateGroup[itemDir].filter(b => b.includes('/'))
if (!itemDirNestedFiles.length) continue; if (!itemDirNestedFiles.length) continue;
@ -582,7 +589,8 @@ class Scanner {
} }
Logger.debug(`[Scanner] Folder update group must be a new item "${itemDir}" in library "${library.name}"`) Logger.debug(`[Scanner] Folder update group must be a new item "${itemDir}" in library "${library.name}"`)
var newLibraryItem = await this.scanPotentialNewLibraryItem(library.mediaType, folder, fullPath) var isSingleMediaItem = itemDir === fileUpdateGroup[itemDir]
var newLibraryItem = await this.scanPotentialNewLibraryItem(library.mediaType, folder, fullPath, isSingleMediaItem)
if (newLibraryItem) { if (newLibraryItem) {
await this.createNewAuthorsAndSeries(newLibraryItem) await this.createNewAuthorsAndSeries(newLibraryItem)
await this.db.insertLibraryItem(newLibraryItem) await this.db.insertLibraryItem(newLibraryItem)
@ -594,8 +602,8 @@ class Scanner {
return itemGroupingResults return itemGroupingResults
} }
async scanPotentialNewLibraryItem(libraryMediaType, folder, fullPath) { async scanPotentialNewLibraryItem(libraryMediaType, folder, fullPath, isSingleMediaItem = false) {
var libraryItemData = await getLibraryItemFileData(libraryMediaType, folder, fullPath, this.db.serverSettings) var libraryItemData = await getLibraryItemFileData(libraryMediaType, folder, fullPath, isSingleMediaItem, this.db.serverSettings)
if (!libraryItemData) return null if (!libraryItemData) return null
var serverSettings = this.db.serverSettings var serverSettings = this.db.serverSettings
return this.scanNewLibraryItem(libraryItemData, libraryMediaType, serverSettings.scannerPreferAudioMetadata, serverSettings.scannerPreferOpfMetadata, serverSettings.scannerFindCovers) return this.scanNewLibraryItem(libraryItemData, libraryMediaType, serverSettings.scannerPreferAudioMetadata, serverSettings.scannerPreferOpfMetadata, serverSettings.scannerFindCovers)

View File

@ -418,7 +418,7 @@ module.exports = {
books: [libraryItemJson], books: [libraryItemJson],
inProgress: bookInProgress, inProgress: bookInProgress,
bookInProgressLastUpdate: bookInProgress ? mediaProgress.lastUpdate : null, bookInProgressLastUpdate: bookInProgress ? mediaProgress.lastUpdate : null,
firstBookUnread: bookInProgress ? libraryItemJson : null firstBookUnread: bookInProgress ? null : libraryItemJson
} }
seriesMap[librarySeries.id] = series seriesMap[librarySeries.id] = series

View File

@ -4,8 +4,6 @@ const Logger = require('../Logger')
const { recurseFiles, getFileTimestampsWithIno } = require('./fileUtils') const { recurseFiles, getFileTimestampsWithIno } = require('./fileUtils')
const globals = require('./globals') const globals = require('./globals')
const LibraryFile = require('../objects/files/LibraryFile') const LibraryFile = require('../objects/files/LibraryFile')
const { response } = require('express')
const e = require('express')
function isMediaFile(mediaType, ext) { function isMediaFile(mediaType, ext) {
// if (!path) return false // if (!path) return false
@ -19,11 +17,14 @@ function isMediaFile(mediaType, ext) {
// TODO: Function needs to be re-done // TODO: Function needs to be re-done
// Input: array of relative file paths // Input: array of relative file paths
// Output: map of files grouped into potential item dirs // Output: map of files grouped into potential item dirs
function groupFilesIntoLibraryItemPaths(paths) { function groupFilesIntoLibraryItemPaths(mediaType, paths) {
// Step 1: Clean path, Remove leading "/", Filter out files in root dir // Step 1: Clean path, Remove leading "/", Filter out non-media files in root dir
var pathsFiltered = paths.map(path => { var pathsFiltered = paths.map(path => {
return path.startsWith('/') ? path.slice(1) : path return path.startsWith('/') ? path.slice(1) : path
}).filter(path => Path.parse(path).dir) }).filter(path => {
let parsedPath = Path.parse(path)
return parsedPath.dir || (mediaType === 'book' && isMediaFile(mediaType, parsedPath.ext))
})
// Step 2: Sort by least number of directories // Step 2: Sort by least number of directories
pathsFiltered.sort((a, b) => { pathsFiltered.sort((a, b) => {
@ -35,10 +36,14 @@ function groupFilesIntoLibraryItemPaths(paths) {
// Step 3: Group files in dirs // Step 3: Group files in dirs
var itemGroup = {} var itemGroup = {}
pathsFiltered.forEach((path) => { pathsFiltered.forEach((path) => {
var dirparts = Path.dirname(path).split('/') var dirparts = Path.dirname(path).split('/').filter(p => !!p && p !== '.') // dirname returns . if no directory
var numparts = dirparts.length var numparts = dirparts.length
var _path = '' var _path = ''
if (!numparts) {
// Media file in root
itemGroup[path] = path
} else {
// Iterate over directories in path // Iterate over directories in path
for (let i = 0; i < numparts; i++) { for (let i = 0; i < numparts; i++) {
var dirpart = dirparts.shift() var dirpart = dirparts.shift()
@ -56,6 +61,7 @@ function groupFilesIntoLibraryItemPaths(paths) {
return return
} }
} }
}
}) })
return itemGroup return itemGroup
} }
@ -64,9 +70,9 @@ module.exports.groupFilesIntoLibraryItemPaths = groupFilesIntoLibraryItemPaths
// Input: array of relative file items (see recurseFiles) // Input: array of relative file items (see recurseFiles)
// Output: map of files grouped into potential libarary item dirs // Output: map of files grouped into potential libarary item dirs
function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems) { function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems) {
// Step 1: Filter out non-media files in root dir (with depth of 0) // Step 1: Filter out non-book-media files in root dir (with depth of 0)
var itemsFiltered = fileItems.filter(i => { var itemsFiltered = fileItems.filter(i => {
return i.deep > 0 || isMediaFile(mediaType, i.extension) return i.deep > 0 || (mediaType === 'book' && isMediaFile(mediaType, i.extension))
}) })
// Step 2: Seperate media files and other files // Step 2: Seperate media files and other files
@ -149,16 +155,6 @@ async function scanFolder(libraryMediaType, folder, serverSettings = {}) {
} }
var fileItems = await recurseFiles(folderPath) var fileItems = await recurseFiles(folderPath)
var basePath = folderPath
const isOpenAudibleFolder = fileItems.find(fi => fi.deep === 0 && fi.name === 'books.json')
if (isOpenAudibleFolder) {
Logger.info(`[scandir] Detected Open Audible Folder, looking in books folder`)
basePath = Path.posix.join(folderPath, 'books')
fileItems = await recurseFiles(basePath)
Logger.debug(`[scandir] ${fileItems.length} files found in books folder`)
}
var libraryItemGrouping = groupFileItemsIntoLibraryItemDirs(libraryMediaType, fileItems) var libraryItemGrouping = groupFileItemsIntoLibraryItemDirs(libraryMediaType, fileItems)
if (!Object.keys(libraryItemGrouping).length) { if (!Object.keys(libraryItemGrouping).length) {
@ -177,10 +173,10 @@ async function scanFolder(libraryMediaType, folder, serverSettings = {}) {
mediaMetadata: { mediaMetadata: {
title: Path.basename(libraryItemPath, Path.extname(libraryItemPath)) title: Path.basename(libraryItemPath, Path.extname(libraryItemPath))
}, },
path: Path.posix.join(basePath, libraryItemPath), path: Path.posix.join(folderPath, libraryItemPath),
relPath: libraryItemPath relPath: libraryItemPath
} }
fileObjs = await cleanFileObjects(basePath, [libraryItemPath]) fileObjs = await cleanFileObjects(folderPath, [libraryItemPath])
isFile = true isFile = true
} else { } else {
libraryItemData = getDataFromMediaDir(libraryMediaType, folderPath, libraryItemPath, serverSettings) libraryItemData = getDataFromMediaDir(libraryMediaType, folderPath, libraryItemPath, serverSettings)
@ -319,14 +315,32 @@ function getDataFromMediaDir(libraryMediaType, folderPath, relPath, serverSettin
} }
// Called from Scanner.js // Called from Scanner.js
async function getLibraryItemFileData(libraryMediaType, folder, libraryItemPath, serverSettings = {}) { async function getLibraryItemFileData(libraryMediaType, folder, libraryItemPath, isSingleMediaItem, serverSettings = {}) {
var fileItems = await recurseFiles(libraryItemPath)
libraryItemPath = libraryItemPath.replace(/\\/g, '/') libraryItemPath = libraryItemPath.replace(/\\/g, '/')
var folderFullPath = folder.fullPath.replace(/\\/g, '/') var folderFullPath = folder.fullPath.replace(/\\/g, '/')
var libraryItemDir = libraryItemPath.replace(folderFullPath, '').slice(1) var libraryItemDir = libraryItemPath.replace(folderFullPath, '').slice(1)
var libraryItemData = getDataFromMediaDir(libraryMediaType, folderFullPath, libraryItemDir, serverSettings) var libraryItemData = {}
var fileItems = []
if (isSingleMediaItem) { // Single media item in root of folder
fileItems = [{
fullpath: libraryItemPath,
path: libraryItemDir // actually the relPath (only filename here)
}]
libraryItemData = {
path: libraryItemPath, // full path
relPath: libraryItemDir, // only filename
mediaMetadata: {
title: Path.basename(libraryItemDir, Path.extname(libraryItemDir))
}
}
} else {
fileItems = await recurseFiles(libraryItemPath)
libraryItemData = getDataFromMediaDir(libraryMediaType, folderFullPath, libraryItemDir, serverSettings)
}
var libraryItemDirStats = await getFileTimestampsWithIno(libraryItemData.path) var libraryItemDirStats = await getFileTimestampsWithIno(libraryItemData.path)
var libraryItem = { var libraryItem = {
ino: libraryItemDirStats.ino, ino: libraryItemDirStats.ino,
@ -337,6 +351,7 @@ async function getLibraryItemFileData(libraryMediaType, folder, libraryItemPath,
libraryId: folder.libraryId, libraryId: folder.libraryId,
path: libraryItemData.path, path: libraryItemData.path,
relPath: libraryItemData.relPath, relPath: libraryItemData.relPath,
isFile: isSingleMediaItem,
media: { media: {
metadata: libraryItemData.mediaMetadata || null metadata: libraryItemData.mediaMetadata || null
}, },