mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Merge branch 'advplyr:master' into tooltips_for_appbar
This commit is contained in:
		
						commit
						21785c8e72
					
				
							
								
								
									
										78
									
								
								.github/workflows/docker-build.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								.github/workflows/docker-build.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,78 @@ | ||||
| --- | ||||
| 
 | ||||
| name: Build and Push Docker Image | ||||
| 
 | ||||
| on: | ||||
|   push: | ||||
|     branches: [master] | ||||
|     tags: | ||||
|       - 'v*.*.*' | ||||
|     # Only build when files in these directories have been changed | ||||
|     paths: | ||||
|       - client/** | ||||
|       - server/** | ||||
|       - index.js | ||||
|       - package.json | ||||
|   release: | ||||
|     types: [published, edited] | ||||
|   # Allows you to run workflow manually from Actions tab | ||||
|   workflow_dispatch: | ||||
| 
 | ||||
| jobs: | ||||
|   build: | ||||
|     if: "!contains(github.event.head_commit.message, 'skip ci')" | ||||
|     runs-on: ubuntu-20.04 | ||||
| 
 | ||||
|     steps: | ||||
|       - name: Check out | ||||
|         uses: actions/checkout@v2 | ||||
| 
 | ||||
|       - name: Docker meta | ||||
|         id: meta | ||||
|         uses: docker/metadata-action@v3 | ||||
|         with: | ||||
|           images: advplyr/audiobookshelf,ghcr.io/${{ github.repository_owner }}/audiobookshelf | ||||
|           tags: | | ||||
|             type=edge,branch=master | ||||
|             type=semver,pattern={{version}} | ||||
|       - name: Setup QEMU | ||||
|         uses: docker/setup-qemu-action@v1 | ||||
| 
 | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v1 | ||||
|          | ||||
|       - name: Cache Docker layers | ||||
|         uses: actions/cache@v2 | ||||
|         with: | ||||
|           path: /tmp/.buildx-cache | ||||
|           key: ${{ runner.os }}-buildx-${{ github.sha }} | ||||
|           restore-keys: | | ||||
|             ${{ runner.os }}-buildx- | ||||
| 
 | ||||
|       - name: Login to Dockerhub | ||||
|         uses: docker/login-action@v1 | ||||
|         with: | ||||
|           username: ${{ secrets.DOCKERHUB_USERNAME }} | ||||
|           password: ${{ secrets.DOCKERHUB_PASSWORD }} | ||||
|        | ||||
|       - name: Login to ghcr | ||||
|         uses: docker/login-action@v1 | ||||
|         with: | ||||
|           registry: ghcr.io | ||||
|           username: ${{ github.repository_owner }} | ||||
|           password: ${{ secrets.GHCR_PASSWORD }} | ||||
| 
 | ||||
|       - name: Build image | ||||
|         uses: docker/build-push-action@v2 | ||||
|         with: | ||||
|           tags: ${{ steps.meta.outputs.tags }} | ||||
|           context: . | ||||
|           platforms: linux/amd64,linux/arm64,linux/arm/v7 | ||||
|           push: true | ||||
|           cache-from: type=local,src=/tmp/.buildx-cache | ||||
|           cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max | ||||
|            | ||||
|       - name: Move cache | ||||
|         run: | | ||||
|           rm -rf /tmp/.buildx-cache | ||||
|           mv /tmp/.buildx-cache-new /tmp/.buildx-cache | ||||
| @ -128,8 +128,7 @@ export default { | ||||
|           type: 'series', | ||||
|           entities: this.results.series.map((seriesObj) => { | ||||
|             return { | ||||
|               name: seriesObj.series.name, | ||||
|               series: seriesObj.series, | ||||
|               ...seriesObj.series, | ||||
|               books: seriesObj.books, | ||||
|               type: 'series' | ||||
|             } | ||||
|  | ||||
| @ -52,7 +52,7 @@ | ||||
|       <div v-show="isAuthorsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> | ||||
|     </nuxt-link> | ||||
| 
 | ||||
|     <nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/search`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> | ||||
|     <nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> | ||||
|       <icons-podcast-svg class="w-6 h-6" /> | ||||
| 
 | ||||
|       <p class="font-book pt-1.5" style="font-size: 0.9rem">Search</p> | ||||
| @ -82,6 +82,9 @@ export default { | ||||
|     showExperimentalFeatures() { | ||||
|       return this.$store.state.showExperimentalFeatures | ||||
|     }, | ||||
|     userIsAdminOrUp() { | ||||
|       return this.$store.getters['user/getIsAdminOrUp'] | ||||
|     }, | ||||
|     paramId() { | ||||
|       return this.$route.params ? this.$route.params.id || '' : '' | ||||
|     }, | ||||
|  | ||||
| @ -44,6 +44,14 @@ export default { | ||||
|           this.$nextTick(this.init) | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     width: { | ||||
|       handler(newVal) { | ||||
|         if (newVal) { | ||||
|           this.isInit = false | ||||
|           this.$nextTick(this.init) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| <template> | ||||
|   <modals-modal v-model="show" name="account" :width="800" :height="'unset'" :processing="processing"> | ||||
|   <modals-modal ref="modal" v-model="show" name="account" :width="800" :height="'unset'" :processing="processing"> | ||||
|     <template #outer> | ||||
|       <div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden"> | ||||
|         <p class="font-book text-3xl text-white truncate">{{ title }}</p> | ||||
| @ -8,20 +8,20 @@ | ||||
|     <form @submit.prevent="submitForm"> | ||||
|       <div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300"> | ||||
|         <div class="w-full p-8"> | ||||
|           <div class="flex py-2 -mx-2"> | ||||
|           <div class="flex py-2"> | ||||
|             <div class="w-1/2 px-2"> | ||||
|               <ui-text-input-with-label v-model="newUser.username" label="Username" class="mx-2" /> | ||||
|               <ui-text-input-with-label v-model="newUser.username" label="Username" /> | ||||
|             </div> | ||||
|             <div class="w-1/2 px-2"> | ||||
|               <ui-text-input-with-label v-if="!isEditingRoot" v-model="newUser.password" :label="isNew ? 'Password' : 'Change Password'" type="password" class="mx-2" /> | ||||
|               <ui-text-input-with-label v-if="!isEditingRoot" v-model="newUser.password" :label="isNew ? 'Password' : 'Change Password'" type="password" /> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div class="flex py-2"> | ||||
|             <div class="px-2"> | ||||
|               <ui-input-dropdown v-model="newUser.type" label="Account Type" :disabled="isEditingRoot" :editable="false" :items="accountTypes" @input="userTypeUpdated" /> | ||||
|           <div v-show="!isEditingRoot" class="flex py-2"> | ||||
|             <div class="px-2 w-52"> | ||||
|               <ui-dropdown v-model="newUser.type" label="Account Type" :disabled="isEditingRoot" :items="accountTypes" @input="userTypeUpdated" /> | ||||
|             </div> | ||||
|             <div class="flex-grow" /> | ||||
|             <div v-show="!isEditingRoot" class="flex items-center pt-4 px-2"> | ||||
|             <div class="flex items-center pt-4 px-2"> | ||||
|               <p class="px-3 font-semibold" :class="isEditingRoot ? 'text-gray-300' : ''">Is Active</p> | ||||
|               <ui-toggle-switch v-model="newUser.isActive" :disabled="isEditingRoot" /> | ||||
|             </div> | ||||
| @ -92,7 +92,8 @@ | ||||
|             </div> | ||||
|           </div> | ||||
| 
 | ||||
|           <div class="flex pt-4"> | ||||
|           <div class="flex pt-4 px-2"> | ||||
|             <ui-btn v-if="isEditingRoot" to="/account">Change Root Password</ui-btn> | ||||
|             <div class="flex-grow" /> | ||||
|             <ui-btn color="success" type="submit">Submit</ui-btn> | ||||
|           </div> | ||||
| @ -116,7 +117,20 @@ export default { | ||||
|       processing: false, | ||||
|       newUser: {}, | ||||
|       isNew: true, | ||||
|       accountTypes: ['guest', 'user', 'admin'], | ||||
|       accountTypes: [ | ||||
|         { | ||||
|           text: 'Guest', | ||||
|           value: 'guest' | ||||
|         }, | ||||
|         { | ||||
|           text: 'User', | ||||
|           value: 'user' | ||||
|         }, | ||||
|         { | ||||
|           text: 'Admin', | ||||
|           value: 'admin' | ||||
|         } | ||||
|       ], | ||||
|       tags: [], | ||||
|       loadingTags: false | ||||
|     } | ||||
| @ -124,6 +138,7 @@ export default { | ||||
|   watch: { | ||||
|     show: { | ||||
|       handler(newVal) { | ||||
|         console.log('accoutn modal show change', newVal) | ||||
|         if (newVal) { | ||||
|           this.init() | ||||
|         } | ||||
| @ -140,7 +155,7 @@ export default { | ||||
|       } | ||||
|     }, | ||||
|     title() { | ||||
|       return this.isNew ? 'Add New Account' : `Update Account: ${(this.account || {}).username}` | ||||
|       return this.isNew ? 'Add New Account' : `Update ${(this.account || {}).username}` | ||||
|     }, | ||||
|     isEditingRoot() { | ||||
|       return this.account && this.account.type === 'root' | ||||
| @ -161,6 +176,10 @@ export default { | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     close() { | ||||
|       // Force close when navigating - used in UsersTable | ||||
|       if (this.$refs.modal) this.$refs.modal.setHide() | ||||
|     }, | ||||
|     accessAllTagsToggled(val) { | ||||
|       if (!val && !this.newUser.itemTagsAccessible.length) { | ||||
|         this.newUser.itemTagsAccessible = this.libraries.map((l) => l.id) | ||||
|  | ||||
| @ -1,11 +1,11 @@ | ||||
| <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="flex items-center mb-4"> | ||||
|         <p v-if="autoDownloadEpisodes">Last new episode check {{ $formatDate(lastEpisodeCheck) }}</p> | ||||
|         <div class="flex-grow" /> | ||||
|         <ui-btn :loading="checkingNewEpisodes" @click="checkForNewEpisodes">Check for new episodes</ui-btn> | ||||
|       </div> --> | ||||
|       <div v-if="userIsAdminOrUp" class="flex items-end justify-end mb-4"> | ||||
|         <!-- <p v-if="autoDownloadEpisodes">Last new episode check {{ $formatDate(lastEpisodeCheck) }}</p> --> | ||||
|         <ui-text-input-with-label ref="lastCheckInput" v-model="lastEpisodeCheckInput" :disabled="checkingNewEpisodes" type="datetime-local" label="Look for new episodes after this date" class="max-w-xs mr-2" /> | ||||
|         <ui-btn :loading="checkingNewEpisodes" @click="checkForNewEpisodes">Check & Download New Episodes</ui-btn> | ||||
|       </div> | ||||
| 
 | ||||
|       <div v-if="episodes.length" class="w-full p-4 bg-primary"> | ||||
|         <p>Podcast Episodes</p> | ||||
| @ -51,10 +51,23 @@ export default { | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       checkingNewEpisodes: false | ||||
|       checkingNewEpisodes: false, | ||||
|       lastEpisodeCheckInput: null | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
|     lastEpisodeCheck: { | ||||
|       handler(newVal) { | ||||
|         if (newVal) { | ||||
|           this.setLastEpisodeCheckInput() | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     userIsAdminOrUp() { | ||||
|       return this.$store.getters['user/getIsAdminOrUp'] | ||||
|     }, | ||||
|     autoDownloadEpisodes() { | ||||
|       return !!this.media.autoDownloadEpisodes | ||||
|     }, | ||||
| @ -72,8 +85,22 @@ export default { | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     checkForNewEpisodes() { | ||||
|     async checkForNewEpisodes() { | ||||
|       if (this.$refs.lastCheckInput) { | ||||
|         this.$refs.lastCheckInput.blur() | ||||
|       } | ||||
|       this.checkingNewEpisodes = true | ||||
|       const lastEpisodeCheck = new Date(this.lastEpisodeCheckInput).valueOf() | ||||
| 
 | ||||
|       // If last episode check changed then update it first | ||||
|       if (lastEpisodeCheck && lastEpisodeCheck !== this.lastEpisodeCheck) { | ||||
|         var updateResult = await this.$axios.$patch(`/api/items/${this.libraryItemId}/media`, { lastEpisodeCheck }).catch((error) => { | ||||
|           console.error('Failed to update', error) | ||||
|           return false | ||||
|         }) | ||||
|         console.log('updateResult', updateResult) | ||||
|       } | ||||
| 
 | ||||
|       this.$axios | ||||
|         .$get(`/api/podcasts/${this.libraryItemId}/checknew`) | ||||
|         .then((response) => { | ||||
| @ -91,7 +118,13 @@ export default { | ||||
|           this.$toast.error(errorMsg) | ||||
|           this.checkingNewEpisodes = false | ||||
|         }) | ||||
|     }, | ||||
|     setLastEpisodeCheckInput() { | ||||
|       this.lastEpisodeCheckInput = this.lastEpisodeCheck ? this.$formatDate(this.lastEpisodeCheck, "yyyy-MM-dd'T'HH:mm") : null | ||||
|     } | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.setLastEpisodeCheckInput() | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| @ -58,7 +58,7 @@ | ||||
|       </table> | ||||
|     </div> | ||||
| 
 | ||||
|     <modals-account-modal v-model="showAccountModal" :account="selectedAccount" /> | ||||
|     <modals-account-modal ref="accountModal" v-model="showAccountModal" :account="selectedAccount" /> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| @ -156,6 +156,10 @@ export default { | ||||
|     this.init() | ||||
|   }, | ||||
|   beforeDestroy() { | ||||
|     if (this.$refs.accountModal) { | ||||
|       this.$refs.accountModal.close() | ||||
|     } | ||||
| 
 | ||||
|     if (this.$root.socket) { | ||||
|       this.$root.socket.off('user_added', this.newUserAdded) | ||||
|       this.$root.socket.off('user_updated', this.userUpdated) | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| <template> | ||||
|   <div class="relative w-full" v-click-outside="clickOutsideObj"> | ||||
|     <p class="text-sm font-semibold" :class="disabled ? 'text-gray-300' : ''">{{ label }}</p> | ||||
|     <p class="text-sm font-semibold px-1" :class="disabled ? 'text-gray-300' : ''">{{ label }}</p> | ||||
|     <button type="button" :disabled="disabled" class="relative w-full border rounded shadow-sm pl-3 pr-8 py-2 text-left focus:outline-none sm:text-sm" :class="buttonClass" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu"> | ||||
|       <span class="flex items-center"> | ||||
|         <span class="block truncate" :class="small ? 'text-sm' : ''">{{ selectedText }}</span> | ||||
|  | ||||
| @ -106,12 +106,6 @@ export default { | ||||
|         } | ||||
|       } | ||||
|       if (payload.serverSettings) { | ||||
|         this.$store.commit('setServerSettings', payload.serverSettings) | ||||
| 
 | ||||
|         if (payload.serverSettings.chromecastEnabled) { | ||||
|           console.log('Chromecast enabled import script') | ||||
|           require('@/plugins/chromecast.js').default(this) | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       // Start scans currently running | ||||
| @ -167,8 +161,28 @@ export default { | ||||
|     libraryUpdated(library) { | ||||
|       this.$store.commit('libraries/addUpdate', library) | ||||
|     }, | ||||
|     libraryRemoved(library) { | ||||
|     async libraryRemoved(library) { | ||||
|       this.$store.commit('libraries/remove', library) | ||||
| 
 | ||||
|       // When removed currently selected library then set next accessible library | ||||
|       const currLibraryId = this.$store.state.libraries.currentLibraryId | ||||
|       if (currLibraryId === library.id) { | ||||
|         var nextLibrary = this.$store.getters['libraries/getNextAccessibleLibrary'] | ||||
|         if (nextLibrary) { | ||||
|           await this.$store.dispatch('libraries/fetch', nextLibrary.id) | ||||
| 
 | ||||
|           if (this.$route.name.startsWith('config')) { | ||||
|             // No need to refresh | ||||
|           } else if (this.$route.name.startsWith('library')) { | ||||
|             var newRoute = this.$route.path.replace(currLibraryId, nextLibrary.id) | ||||
|             this.$router.push(newRoute) | ||||
|           } else { | ||||
|             this.$router.push(`/library/${nextLibrary.id}`) | ||||
|           } | ||||
|         } else { | ||||
|           console.error('User has no accessible libraries') | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     libraryItemAdded(libraryItem) { | ||||
|       // this.$store.commit('libraries/updateFilterDataWithAudiobook', libraryItem) | ||||
| @ -485,6 +499,25 @@ export default { | ||||
|     }, | ||||
|     resize() { | ||||
|       this.$store.commit('globals/updateWindowSize', { width: window.innerWidth, height: window.innerHeight }) | ||||
|     }, | ||||
|     checkVersionUpdate() { | ||||
|       // Version check is only run if time since last check was 5 minutes | ||||
|       const VERSION_CHECK_BUFF = 1000 * 60 * 5 // 5 minutes | ||||
|       var lastVerCheck = localStorage.getItem('lastVerCheck') || 0 | ||||
|       if (Date.now() - Number(lastVerCheck) > VERSION_CHECK_BUFF) { | ||||
|         this.$store | ||||
|           .dispatch('checkForUpdate') | ||||
|           .then((res) => { | ||||
|             localStorage.setItem('lastVerCheck', Date.now()) | ||||
|             if (res && res.hasUpdate) this.showUpdateToast(res) | ||||
|           }) | ||||
|           .catch((err) => console.error(err)) | ||||
| 
 | ||||
|         if (this.$route.query.error) { | ||||
|           this.$toast.error(this.$route.query.error) | ||||
|           this.$router.replace(this.$route.path) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   beforeMount() { | ||||
| @ -503,17 +536,7 @@ export default { | ||||
|       this.$store.commit('setExperimentalFeatures', true) | ||||
|     } | ||||
| 
 | ||||
|     this.$store | ||||
|       .dispatch('checkForUpdate') | ||||
|       .then((res) => { | ||||
|         if (res && res.hasUpdate) this.showUpdateToast(res) | ||||
|       }) | ||||
|       .catch((err) => console.error(err)) | ||||
| 
 | ||||
|     if (this.$route.query.error) { | ||||
|       this.$toast.error(this.$route.query.error) | ||||
|       this.$router.replace(this.$route.path) | ||||
|     } | ||||
|     this.checkVersionUpdate() | ||||
|   }, | ||||
|   beforeDestroy() { | ||||
|     window.removeEventListener('resize', this.resize) | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "audiobookshelf-client", | ||||
|   "version": "2.0.3", | ||||
|   "version": "2.0.7", | ||||
|   "description": "Audiobook manager and player", | ||||
|   "main": "index.js", | ||||
|   "scripts": { | ||||
|  | ||||
| @ -15,8 +15,8 @@ | ||||
| 
 | ||||
|         <div class="w-full h-px bg-primary my-4" /> | ||||
| 
 | ||||
|         <p class="mb-4 text-lg">Change Password</p> | ||||
|         <form @submit.prevent="submitChangePassword"> | ||||
|         <p v-if="!isGuest" class="mb-4 text-lg">Change Password</p> | ||||
|         <form v-if="!isGuest" @submit.prevent="submitChangePassword"> | ||||
|           <ui-text-input-with-label v-model="password" :disabled="changingPassword" type="password" label="Password" class="my-2" /> | ||||
|           <ui-text-input-with-label v-model="newPassword" :disabled="changingPassword" type="password" label="New Password" class="my-2" /> | ||||
|           <ui-text-input-with-label v-model="confirmPassword" :disabled="changingPassword" type="password" label="Confirm Password" class="my-2" /> | ||||
| @ -60,6 +60,9 @@ export default { | ||||
|     }, | ||||
|     isRoot() { | ||||
|       return this.usertype === 'root' | ||||
|     }, | ||||
|     isGuest() { | ||||
|       return this.usertype === 'guest' | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|  | ||||
| @ -95,14 +95,16 @@ | ||||
|             <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> | ||||
|           </div> | ||||
| 
 | ||||
|           <!-- Podcast episode downloads queue --> | ||||
|           <div v-if="episodeDownloadsQueued.length" class="px-4 py-2 mt-4 bg-info bg-opacity-40 text-sm font-semibold rounded-md text-gray-100 relative max-w-max mx-auto md:mx-0"> | ||||
|             <div class="flex items-center"> | ||||
|               <p class="text-sm py-1">{{ episodeDownloadsQueued.length }} Episode{{ episodeDownloadsQueued.length === 1 ? '' : 's' }} queued for download</p> | ||||
| 
 | ||||
|               <span class="material-icons hover:text-error text-xl ml-3 cursor-pointer" @click="clearDownloadQueue">close</span> | ||||
|               <span v-if="userIsAdminOrUp" class="material-icons hover:text-error text-xl ml-3 cursor-pointer" @click="clearDownloadQueue">close</span> | ||||
|             </div> | ||||
|           </div> | ||||
| 
 | ||||
|           <!-- Podcast episodes currently downloading --> | ||||
|           <div v-if="episodesDownloading.length" class="px-4 py-2 mt-4 bg-success bg-opacity-20 text-sm font-semibold rounded-md text-gray-100 relative max-w-max mx-auto md:mx-0"> | ||||
|             <div v-for="episode in episodesDownloading" :key="episode.id" class="flex items-center"> | ||||
|               <widgets-loading-spinner /> | ||||
| @ -150,7 +152,8 @@ | ||||
|               <ui-icon-btn icon="collections_bookmark" class="mx-0.5" outlined @click="collectionsClick" /> | ||||
|             </ui-tooltip> | ||||
| 
 | ||||
|             <ui-tooltip v-if="isPodcast" text="Find Episodes" direction="top"> | ||||
|             <!-- Only admin or root user can download new episodes --> | ||||
|             <ui-tooltip v-if="isPodcast && userIsAdminOrUp" text="Find Episodes" direction="top"> | ||||
|               <ui-icon-btn icon="search" class="mx-0.5" :loading="fetchingRSSFeed" outlined @click="findEpisodesClick" /> | ||||
|             </ui-tooltip> | ||||
|           </div> | ||||
| @ -210,6 +213,9 @@ export default { | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     userIsAdminOrUp() { | ||||
|       return this.$store.getters['user/getIsAdminOrUp'] | ||||
|     }, | ||||
|     isFile() { | ||||
|       return this.libraryItem.isFile | ||||
|     }, | ||||
|  | ||||
| @ -48,8 +48,15 @@ export default { | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     setUser(user, defaultLibraryId) { | ||||
|       this.$store.commit('libraries/setCurrentLibrary', defaultLibraryId) | ||||
|     setUser({ user, userDefaultLibraryId, serverSettings }) { | ||||
|       this.$store.commit('setServerSettings', serverSettings) | ||||
| 
 | ||||
|       if (serverSettings.chromecastEnabled) { | ||||
|         console.log('Chromecast enabled import script') | ||||
|         require('@/plugins/chromecast.js').default(this) | ||||
|       } | ||||
| 
 | ||||
|       this.$store.commit('libraries/setCurrentLibrary', userDefaultLibraryId) | ||||
|       this.$store.commit('user/setUser', user) | ||||
|     }, | ||||
|     async submitForm() { | ||||
| @ -69,7 +76,7 @@ export default { | ||||
|       if (authRes && authRes.error) { | ||||
|         this.error = authRes.error | ||||
|       } else if (authRes) { | ||||
|         this.setUser(authRes.user, authRes.userDefaultLibraryId) | ||||
|         this.setUser(authRes) | ||||
|       } | ||||
|       this.processing = false | ||||
|     }, | ||||
| @ -87,7 +94,7 @@ export default { | ||||
|               } | ||||
|             }) | ||||
|             .then((res) => { | ||||
|               this.setUser(res.user, res.userDefaultLibraryId) | ||||
|               this.setUser(res) | ||||
|               this.processing = false | ||||
|             }) | ||||
|             .catch((error) => { | ||||
|  | ||||
| @ -33,11 +33,12 @@ export async function checkForUpdate() { | ||||
|     return | ||||
|   } | ||||
|   var largestVer = null | ||||
|   await axios.get(`https://api.github.com/repos/advplyr/audiobookshelf/tags`).then((res) => { | ||||
|     var tags = res.data | ||||
|     if (tags && tags.length) { | ||||
|       tags.forEach((tag) => { | ||||
|         var verObj = parseSemver(tag.name) | ||||
|   await axios.get(`https://api.github.com/repos/advplyr/audiobookshelf/releases`).then((res) => { | ||||
|     var releases = res.data | ||||
|     if (releases && releases.length) { | ||||
|       releases.forEach((release) => { | ||||
|         var tagName = release.tag_name | ||||
|         var verObj = parseSemver(tagName) | ||||
|         if (verObj) { | ||||
|           if (!largestVer || largestVer.total < verObj.total) { | ||||
|             largestVer = verObj | ||||
| @ -50,6 +51,7 @@ export async function checkForUpdate() { | ||||
|     console.error('No valid version tags to compare with') | ||||
|     return | ||||
|   } | ||||
| 
 | ||||
|   return { | ||||
|     hasUpdate: largestVer.total > currVerObj.total, | ||||
|     latestVersion: largestVer.version, | ||||
|  | ||||
| @ -29,6 +29,19 @@ export const getters = { | ||||
|     var library = state.libraries.find(l => l.id === libraryId) | ||||
|     if (!library) return null | ||||
|     return library.provider | ||||
|   }, | ||||
|   getNextAccessibleLibrary: (state, getters, rootState, rootGetters) => { | ||||
|     var librariesSorted = getters['getSortedLibraries']() | ||||
|     if (!librariesSorted.length) return null | ||||
| 
 | ||||
|     var canAccessAllLibraries = rootGetters['user/getUserCanAccessAllLibraries'] | ||||
|     var userAccessibleLibraries = rootGetters['user/getLibrariesAccessible'] | ||||
|     if (canAccessAllLibraries) return librariesSorted[0] | ||||
|     librariesSorted = librariesSorted.filter((lib) => { | ||||
|       return userAccessibleLibraries.includes(lib.id) | ||||
|     }) | ||||
|     if (!librariesSorted.length) return null | ||||
|     return librariesSorted[0] | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -16,6 +16,7 @@ export const state = () => ({ | ||||
| 
 | ||||
| export const getters = { | ||||
|   getIsRoot: (state) => state.user && state.user.type === 'root', | ||||
|   getIsAdminOrUp: (state) => state.user && (state.user.type === 'admin' || state.user.type === 'root'), | ||||
|   getToken: (state) => { | ||||
|     return state.user ? state.user.token : null | ||||
|   }, | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "audiobookshelf", | ||||
|   "version": "2.0.3", | ||||
|   "version": "2.0.7", | ||||
|   "description": "Self-hosted audiobook server for managing and playing audiobooks", | ||||
|   "main": "index.js", | ||||
|   "scripts": { | ||||
|  | ||||
							
								
								
									
										10
									
								
								readme.md
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								readme.md
									
									
									
									
									
								
							| @ -74,7 +74,15 @@ docker run -d \ | ||||
|   -v </path/to/config>:/config \ | ||||
|   -v </path/to/metadata>:/metadata \ | ||||
|   --name audiobookshelf \ | ||||
|   --rm ghcr.io/advplyr/audiobookshelf | ||||
|   ghcr.io/advplyr/audiobookshelf | ||||
| ``` | ||||
| 
 | ||||
| ### Docker Update | ||||
| 
 | ||||
| ```bash | ||||
| docker stop audiobookshelf | ||||
| docker pull ghcr.io/advplyr/audiobookshelf | ||||
| docker start audiobookshelf | ||||
| ``` | ||||
| 
 | ||||
| ### Running with Docker Compose | ||||
|  | ||||
| @ -100,6 +100,14 @@ class Auth { | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   getUserLoginResponsePayload(user) { | ||||
|     return { | ||||
|       user: user.toJSONForBrowser(), | ||||
|       userDefaultLibraryId: user.getDefaultLibraryId(this.db.libraries), | ||||
|       serverSettings: this.db.serverSettings.toJSON() | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   async login(req, res) { | ||||
|     var username = (req.body.username || '').toLowerCase() | ||||
|     var password = req.body.password || '' | ||||
| @ -120,17 +128,14 @@ class Auth { | ||||
|       if (password) { | ||||
|         return res.status(401).send('Invalid root password (hint: there is none)') | ||||
|       } else { | ||||
|         return res.json({ user: user.toJSONForBrowser(), userDefaultLibraryId: user.getDefaultLibraryId(this.db.libraries) }) | ||||
|         return res.json(this.getUserLoginResponsePayload(user)) | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     // Check password match
 | ||||
|     var compare = await bcrypt.compare(password, user.pash) | ||||
|     if (compare) { | ||||
|       res.json({ | ||||
|         user: user.toJSONForBrowser(), | ||||
|         userDefaultLibraryId: user.getDefaultLibraryId(this.db.libraries) | ||||
|       }) | ||||
|       res.json(this.getUserLoginResponsePayload(user)) | ||||
|     } else { | ||||
|       Logger.debug(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit}`) | ||||
|       if (req.rateLimit.remaining <= 2) { | ||||
|  | ||||
| @ -409,6 +409,7 @@ class Server { | ||||
|     await this.db.updateEntity('user', user) | ||||
| 
 | ||||
|     const initialPayload = { | ||||
|       // TODO: this is sent with user auth now, update mobile app to use that then remove this
 | ||||
|       serverSettings: this.db.serverSettings.toJSON(), | ||||
|       audiobookPath: global.AudiobookPath, | ||||
|       metadataPath: global.MetadataPath, | ||||
|  | ||||
| @ -133,6 +133,10 @@ class MeController { | ||||
| 
 | ||||
|   // PATCH: api/me/password
 | ||||
|   updatePassword(req, res) { | ||||
|     if (req.user.isGuest) { | ||||
|       Logger.error(`[MeController] Guest user attempted to change password`, req.user.username) | ||||
|       return res.sendStatus(500) | ||||
|     } | ||||
|     this.auth.userChangePassword(req, res) | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -230,7 +230,12 @@ class MiscController { | ||||
|       Logger.error('Invalid user in authorize') | ||||
|       return res.sendStatus(401) | ||||
|     } | ||||
|     res.json({ user: req.user, userDefaultLibraryId: req.user.getDefaultLibraryId(this.db.libraries) }) | ||||
|     const userResponse = { | ||||
|       user: req.user, | ||||
|       userDefaultLibraryId: req.user.getDefaultLibraryId(this.db.libraries), | ||||
|       serverSettings: this.db.serverSettings.toJSON() | ||||
|     } | ||||
|     res.json(userResponse) | ||||
|   } | ||||
| 
 | ||||
|   getAllTags(req, res) { | ||||
|  | ||||
| @ -9,8 +9,8 @@ const filePerms = require('../utils/filePerms') | ||||
| class PodcastController { | ||||
| 
 | ||||
|   async create(req, res) { | ||||
|     if (!req.user.isRoot) { | ||||
|       Logger.error(`[PodcastController] Non-root user attempted to create podcast`, req.user) | ||||
|     if (!req.user.isAdminOrUp) { | ||||
|       Logger.error(`[PodcastController] Non-admin user attempted to create podcast`, req.user) | ||||
|       return res.sendStatus(500) | ||||
|     } | ||||
|     const payload = req.body | ||||
| @ -115,24 +115,33 @@ class PodcastController { | ||||
|   } | ||||
| 
 | ||||
|   async checkNewEpisodes(req, res) { | ||||
|     if (!req.user.isAdminOrUp) { | ||||
|       Logger.error(`[PodcastController] Non-admin user attempted to check/download episodes`, req.user) | ||||
|       return res.sendStatus(500) | ||||
|     } | ||||
| 
 | ||||
|     var libraryItem = this.db.getLibraryItem(req.params.id) | ||||
|     if (!libraryItem || libraryItem.mediaType !== 'podcast') { | ||||
|       return res.sendStatus(404) | ||||
|     } | ||||
|     if (!req.user.checkCanAccessLibrary(libraryItem.libraryId)) { | ||||
|       Logger.error(`[PodcastController] User attempted to check/download episodes for a library without permission`, req.user) | ||||
|       return res.sendStatus(500) | ||||
|     } | ||||
|     if (!libraryItem.media.metadata.feedUrl) { | ||||
|       Logger.error(`[PodcastController] checkNewEpisodes no feed url for item ${libraryItem.id}`) | ||||
|       return res.status(500).send('Podcast has no rss feed url') | ||||
|     } | ||||
| 
 | ||||
|     var newEpisodes = await this.podcastManager.checkPodcastForNewEpisodes(libraryItem) | ||||
|     var newEpisodes = await this.podcastManager.checkAndDownloadNewEpisodes(libraryItem) | ||||
|     res.json({ | ||||
|       episodes: newEpisodes || [] | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   clearEpisodeDownloadQueue(req, res) { | ||||
|     if (!req.user.canUpdate) { | ||||
|       Logger.error(`[PodcastController] User attempting to clear download queue without permission "${req.user.username}"`) | ||||
|     if (!req.user.isAdminOrUp) { | ||||
|       Logger.error(`[PodcastController] Non-admin user attempting to clear download queue "${req.user.username}"`) | ||||
|       return res.sendStatus(500) | ||||
|     } | ||||
|     this.podcastManager.clearDownloadQueue(req.params.id) | ||||
| @ -151,11 +160,17 @@ class PodcastController { | ||||
|   } | ||||
| 
 | ||||
|   async downloadEpisodes(req, res) { | ||||
|     if (!req.user.isAdminOrUp) { | ||||
|       Logger.error(`[PodcastController] Non-admin user attempted to download episodes`, req.user) | ||||
|       return res.sendStatus(500) | ||||
|     } | ||||
| 
 | ||||
|     var libraryItem = this.db.getLibraryItem(req.params.id) | ||||
|     if (!libraryItem || libraryItem.mediaType !== 'podcast') { | ||||
|       return res.sendStatus(404) | ||||
|     } | ||||
|     if (!req.user.canUpload || !req.user.checkCanAccessLibrary(libraryItem.libraryId)) { | ||||
|     if (!req.user.checkCanAccessLibrary(libraryItem.libraryId)) { | ||||
|       Logger.error(`[PodcastController] User attempted to download episodes for library without permission`, req.user) | ||||
|       return res.sendStatus(404) | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -208,8 +208,27 @@ class PodcastManager { | ||||
|     } | ||||
|     // Filter new and not already has
 | ||||
|     var newEpisodes = feed.episodes.filter(ep => ep.publishedAt > podcastLibraryItem.media.lastEpisodeCheck && !podcastLibraryItem.media.checkHasEpisodeByFeedUrl(ep.enclosure.url)) | ||||
|     // Max new episodes for safety = 2
 | ||||
|     newEpisodes = newEpisodes.slice(0, 2) | ||||
|     // Max new episodes for safety = 3
 | ||||
|     newEpisodes = newEpisodes.slice(0, 3) | ||||
|     return newEpisodes | ||||
|   } | ||||
| 
 | ||||
|   async checkAndDownloadNewEpisodes(libraryItem) { | ||||
|     const lastEpisodeCheckDate = new Date(libraryItem.media.lastEpisodeCheck || 0) | ||||
|     Logger.info(`[PodcastManager] checkAndDownloadNewEpisodes for "${libraryItem.media.metadata.title}" - Last episode check: ${lastEpisodeCheckDate}`) | ||||
|     var newEpisodes = await this.checkPodcastForNewEpisodes(libraryItem) | ||||
|     if (newEpisodes.length) { | ||||
|       Logger.info(`[PodcastManager] Found ${newEpisodes.length} new episodes for podcast "${libraryItem.media.metadata.title}" - starting download`) | ||||
|       this.downloadPodcastEpisodes(libraryItem, newEpisodes) | ||||
|     } else { | ||||
|       Logger.info(`[PodcastManager] No new episodes found for podcast "${libraryItem.media.metadata.title}"`) | ||||
|     } | ||||
| 
 | ||||
|     libraryItem.media.lastEpisodeCheck = Date.now() | ||||
|     libraryItem.updatedAt = Date.now() | ||||
|     await this.db.updateLibraryItem(libraryItem) | ||||
|     this.emitter('item_updated', libraryItem.toJSONExpanded()) | ||||
| 
 | ||||
|     return newEpisodes | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -79,7 +79,7 @@ class ServerSettings { | ||||
| 
 | ||||
|     this.backupSchedule = settings.backupSchedule || false | ||||
|     this.backupsToKeep = settings.backupsToKeep || 2 | ||||
|     this.maxBackupSize  = settings.maxBackupSize || 1 | ||||
|     this.maxBackupSize = settings.maxBackupSize || 1 | ||||
|     this.backupMetadataCovers = settings.backupMetadataCovers !== false | ||||
| 
 | ||||
|     this.loggerDailyLogsToKeep = settings.loggerDailyLogsToKeep || 7 | ||||
|  | ||||
| @ -30,6 +30,15 @@ class User { | ||||
|   get isRoot() { | ||||
|     return this.type === 'root' | ||||
|   } | ||||
|   get isAdmin() { | ||||
|     return this.type === 'admin' | ||||
|   } | ||||
|   get isGuest() { | ||||
|     return this.type === 'guest' | ||||
|   } | ||||
|   get isAdminOrUp() { | ||||
|     return this.isAdmin || this.isRoot | ||||
|   } | ||||
|   get canDelete() { | ||||
|     return !!this.permissions.delete && this.isActive | ||||
|   } | ||||
| @ -186,6 +195,7 @@ class User { | ||||
|         } | ||||
|       } | ||||
|     }) | ||||
| 
 | ||||
|     // And update permissions
 | ||||
|     if (payload.permissions) { | ||||
|       for (const key in payload.permissions) { | ||||
| @ -195,8 +205,15 @@ class User { | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     // Update accessible libraries
 | ||||
|     if (payload.librariesAccessible !== undefined) { | ||||
|     if (this.permissions.accessAllLibraries) { | ||||
|       // Access all libraries
 | ||||
|       if (this.librariesAccessible.length) { | ||||
|         this.librariesAccessible = [] | ||||
|         hasUpdates = true | ||||
|       } | ||||
|     } else if (payload.librariesAccessible !== undefined) { | ||||
|       if (payload.librariesAccessible.length) { | ||||
|         if (payload.librariesAccessible.join(',') !== this.librariesAccessible.join(',')) { | ||||
|           hasUpdates = true | ||||
| @ -208,8 +225,14 @@ class User { | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     // Update accessible libraries
 | ||||
|     if (payload.itemTagsAccessible !== undefined) { | ||||
|     // Update accessible tags
 | ||||
|     if (this.permissions.accessAllTags) { | ||||
|       // Access all tags
 | ||||
|       if (this.itemTagsAccessible.length) { | ||||
|         this.itemTagsAccessible = [] | ||||
|         hasUpdates = true | ||||
|       } | ||||
|     } else if (payload.itemTagsAccessible !== undefined) { | ||||
|       if (payload.itemTagsAccessible.length) { | ||||
|         if (payload.itemTagsAccessible.join(',') !== this.itemTagsAccessible.join(',')) { | ||||
|           hasUpdates = true | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user