feat: rescan library

also moved the actions into a menu

closes #38
This commit is contained in:
Gauthier Roebroeck 2020-01-06 13:39:44 +08:00
parent b599b72c48
commit 30208a2340
9 changed files with 133 additions and 47 deletions

View file

@ -5,8 +5,12 @@
class="sticky-bar"
:style="barStyle"
>
<!-- Action menu -->
<library-actions-menu v-if="library"
:library="library"/>
<v-toolbar-title>
<span>{{ libraryName }}</span>
<span>{{ library ? library.name : 'All libraries' }}</span>
<span class="ml-4 badge-count"
v-if="totalElements"
>
@ -16,6 +20,7 @@
<v-spacer/>
<!-- Sort menu -->
<v-menu offset-y>
<template v-slot:activator="{on}">
<v-btn icon v-on="on">
@ -41,19 +46,6 @@
</v-list-item>
</v-list>
</v-menu>
<v-menu offset-y v-if="libraryId !== 0">
<template v-slot:activator="{ on }">
<v-btn icon v-on="on">
<v-icon>mdi-dots-vertical</v-icon>
</v-btn>
</template>
<v-list>
<v-list-item @click="analyze()">
<v-list-item-title>Analyze</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-toolbar>
<v-container fluid class="px-6">
@ -80,15 +72,16 @@
<script lang="ts">
import CardSeries from '@/components/CardSeries.vue'
import LibraryActionsMenu from '@/components/LibraryActionsMenu.vue'
import { LoadState } from '@/types/common'
import Vue from 'vue'
export default Vue.extend({
name: 'BrowseLibraries',
components: { CardSeries },
components: { LibraryActionsMenu, CardSeries },
data: () => {
return {
libraryName: '',
library: undefined as LibraryDto | undefined,
series: [] as SeriesDto[],
pagesState: [] as LoadState[],
pageSize: 20,
@ -122,7 +115,7 @@ export default Vue.extend({
}
},
async created () {
this.libraryName = await this.getLibraryNameLazy(this.libraryId)
this.library = await this.getLibraryLazy(this.libraryId)
},
mounted () {
// fill series skeletons if an index is provided, so scroll position can be restored
@ -137,7 +130,7 @@ export default Vue.extend({
},
beforeRouteUpdate (to, from, next) {
if (to.params.libraryId !== from.params.libraryId) {
this.libraryName = this.getLibraryNameLazy(Number(to.params.libraryId))
this.library = this.getLibraryLazy(Number(to.params.libraryId))
this.sortActive = this.parseQuerySortOrDefault(to.query.sort)
this.reloadData(Number(to.params.libraryId))
}
@ -249,20 +242,16 @@ export default Vue.extend({
this.series.splice(page.number * page.size, page.size, ...page.content)
this.pagesState[page.number] = LoadState.Loaded
},
getLibraryNameLazy (libraryId: any): string {
getLibraryLazy (libraryId: any): LibraryDto | undefined {
if (libraryId !== 0) {
return (this.$store.getters.getLibraryById(libraryId)).name
return this.$store.getters.getLibraryById(libraryId)
} else {
return 'All libraries'
return undefined
}
},
analyze () {
this.$komgaLibraries.analyzeLibrary(this.libraryId)
}
}
})
</script>
<style scoped>
@import "../assets/css/badge.css";
@import "../assets/css/sticky-bar.css";

View file

@ -0,0 +1,76 @@
<template>
<div>
<v-menu offset-y v-if="isAdmin">
<template v-slot:activator="{ on }">
<v-btn icon v-on="on" @click.prevent="">
<v-icon>mdi-dots-vertical</v-icon>
</v-btn>
</template>
<v-list>
<v-list-item @click="scan">
<v-list-item-title>Scan library files</v-list-item-title>
</v-list-item>
<v-list-item @click="analyze">
<v-list-item-title>Analyze</v-list-item-title>
</v-list-item>
<v-list-item @click="promptDeleteLibrary"
class="list-warning">
<v-list-item-title>Delete</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<library-delete-dialog v-model="modalDeleteLibrary"
:library="library"
@deleted="navigateHome"
/>
</div>
</template>
<script lang="ts">
import LibraryDeleteDialog from '@/components/LibraryDeleteDialog.vue'
import Vue from 'vue'
export default Vue.extend({
name: 'library-actions-menu',
components: { LibraryDeleteDialog },
data: function () {
return {
modalDeleteLibrary: false
}
},
props: {
library: {
type: Object,
required: true
}
},
computed: {
isAdmin (): boolean {
return this.$store.getters.meAdmin
}
},
methods: {
scan () {
this.$komgaLibraries.scanLibrary(this.library)
},
analyze () {
this.$komgaLibraries.analyzeLibrary(this.library)
},
promptDeleteLibrary () {
this.modalDeleteLibrary = true
},
navigateHome () {
this.$router.push({ name: 'home' })
}
}
})
</script>
<style scoped>
.list-warning:hover {
background: #F44336;
}
.list-warning:hover .v-list-item__title {
color: white;
}
</style>

View file

@ -99,6 +99,7 @@ export default Vue.extend({
async deleteLibrary () {
try {
await this.$store.dispatch('deleteLibrary', this.library)
this.$emit('deleted', true)
} catch (e) {
this.showSnack(e.message)
}

View file

@ -57,11 +57,23 @@ export default class KomgaLibrariesService {
}
}
async analyzeLibrary (libraryId: number) {
async scanLibrary (library: LibraryDto) {
try {
await this.http.post(`${API_LIBRARIES}/${libraryId}/analyze`)
await this.http.post(`${API_LIBRARIES}/${library.id}/scan`)
} catch (e) {
let msg = `An error occurred while trying to analyze library`
let msg = `An error occurred while trying to scan library '${library.name}'`
if (e.response.data.message) {
msg += `: ${e.response.data.message}`
}
throw new Error(msg)
}
}
async analyzeLibrary (library: LibraryDto) {
try {
await this.http.post(`${API_LIBRARIES}/${library.id}/analyze`)
} catch (e) {
let msg = `An error occurred while trying to analyze library '${library.name}'`
if (e.response.data.message) {
msg += `: ${e.response.data.message}`
}

View file

@ -65,9 +65,7 @@
</v-tooltip>
</v-list-item-content>
<v-list-item-action v-if="isAdmin">
<v-btn icon @click.prevent="promptDeleteLibrary(l)">
<v-icon>mdi-delete</v-icon>
</v-btn>
<library-actions-menu :library="l"/>
</v-list-item-action>
</v-list-item>
@ -103,27 +101,21 @@
<v-content>
<router-view/>
</v-content>
<library-delete-dialog v-model="modalDeleteLibrary"
:library="libraryToDelete">
</library-delete-dialog>
</div>
</template>
<script lang="ts">
import LibraryDeleteDialog from '@/components/LibraryDeleteDialog.vue'
import LibraryActionsMenu from '@/components/LibraryActionsMenu.vue'
import SearchBox from '@/components/SearchBox.vue'
import Vue from 'vue'
export default Vue.extend({
name: 'home',
components: { LibraryDeleteDialog, SearchBox },
components: { LibraryActionsMenu, SearchBox },
data: function () {
return {
drawerVisible: this.$vuetify.breakpoint.lgAndUp,
modalAddLibrary: false,
modalDeleteLibrary: false,
libraryToDelete: {} as LibraryDto
modalAddLibrary: false
}
},
computed: {
@ -138,10 +130,6 @@ export default Vue.extend({
toggleDrawer () {
this.drawerVisible = !this.drawerVisible
},
promptDeleteLibrary (library: LibraryDto) {
this.libraryToDelete = library
this.modalDeleteLibrary = true
},
logout () {
this.$store.dispatch('logout')
this.$router.push({ name: 'login' })

View file

@ -3,6 +3,7 @@ package org.gotson.komga.domain.service
import mu.KotlinLogging
import org.apache.commons.lang3.time.DurationFormatUtils
import org.gotson.komga.domain.model.Book
import org.gotson.komga.domain.model.Library
import org.gotson.komga.domain.persistence.BookRepository
import org.gotson.komga.domain.persistence.LibraryRepository
import org.springframework.scheduling.annotation.Async
@ -21,7 +22,7 @@ class AsyncOrchestrator(
) {
@Async("periodicScanTaskExecutor")
fun scanAndAnalyze() {
fun scanAndAnalyzeAllLibraries() {
logger.info { "Starting periodic libraries scan" }
val libraries = libraryRepository.findAll()
@ -37,6 +38,12 @@ class AsyncOrchestrator(
}
}
@Async("periodicScanTaskExecutor")
fun scanAndAnalyzeOneLibrary(library: Library) {
libraryScanner.scanRootFolder(library)
libraryScanner.analyzeUnknownBooks()
}
@Async("regenerateThumbnailsTaskExecutor")
fun regenerateAllThumbnails() {
logger.info { "Regenerate thumbnail for all books" }

View file

@ -54,7 +54,7 @@ class LibraryLifecycle(
logger.info { "Trying to launch a scan for the newly added library: ${library.name}" }
try {
asyncOrchestrator.scanAndAnalyze()
asyncOrchestrator.scanAndAnalyzeAllLibraries()
} catch (e: RejectedExecutionException) {
logger.warn { "Another scan is already running, skipping" }
}

View file

@ -21,7 +21,7 @@ class PeriodicScannerController(
@Scheduled(cron = "#{@komgaProperties.librariesScanCron ?: '-'}")
fun scanRootFolder() {
try {
asyncOrchestrator.scanAndAnalyze()
asyncOrchestrator.scanAndAnalyzeAllLibraries()
} catch (e: RejectedExecutionException) {
logger.warn { "Another scan is already running, skipping" }
}

View file

@ -89,6 +89,19 @@ class LibraryController(
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
@PostMapping("{libraryId}/scan")
@PreAuthorize("hasRole('ROLE_ADMIN')")
@ResponseStatus(HttpStatus.ACCEPTED)
fun scan(@PathVariable libraryId: Long) {
libraryRepository.findByIdOrNull(libraryId)?.let { library ->
try {
asyncOrchestrator.scanAndAnalyzeOneLibrary(library)
} catch (e: RejectedExecutionException) {
throw ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "Another scan task is already running")
}
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
@PostMapping("{libraryId}/analyze")
@PreAuthorize("hasRole('ROLE_ADMIN')")
@ResponseStatus(HttpStatus.ACCEPTED)