feat(webui): import ComicRack lists as read lists

This commit is contained in:
Gauthier Roebroeck 2021-03-19 10:11:04 +08:00
parent c1e435762c
commit 8b0dac3125
17 changed files with 247 additions and 16 deletions

View file

@ -7,6 +7,10 @@
"itemsPerPageText": "الصفوف لكل صفحة:",
"sortBy": "مفروزة حسب"
},
"fileInput": {
"counter": "{0} ملفات",
"counterSize": "{0} ملفات ({1} في المجموع)"
},
"noDataText": "لا توجد بيانات متاحة"
},
"account_settings": {

View file

@ -7,6 +7,10 @@
"itemsPerPageText": "Zeilen pro Seite:",
"sortBy": "Sortiere nach"
},
"fileInput": {
"counter": "{0} Dateien",
"counterSize": "{0} Dateien ({1} gesamt)"
},
"noDataText": "Keine Daten vorhanden"
},
"account_settings": {

View file

@ -7,6 +7,10 @@
"itemsPerPageText": "Rows per page:",
"sortBy": "Sort by"
},
"fileInput": {
"counter": "{0} files",
"counterSize": "{0} files ({1} in total)"
},
"noDataText": "No data available"
},
"account_settings": {
@ -153,6 +157,18 @@
"recently_added_series": "Recently Added Series",
"recently_updated_series": "Recently Updated Series"
},
"data_import": {
"book_number": "Book number: {name}",
"book_series": "Series: {name}",
"button_import": "Import",
"comicrack_preambule_html": "You can import existing ComicRack Reading Lists in <code>.cbl</code> format.<br/>Komga will try to match the provided series and book number with series and books in your libraries.",
"field_files_label": "ComicRack Reading Lists (.cbl)",
"import_read_lists": "Import Read Lists",
"imported_as": "Imported as {name}",
"results_preambule": "Result of the import is shown below. You can also check the unmatched books for each provided file.",
"size_limit": "Size should be less than {size} MB",
"tab_title": "Data Import"
},
"dialog": {
"add_to_collection": {
"button_create": "Create",
@ -363,7 +379,14 @@
"ERR_1005": "Unknown error while analyzing book",
"ERR_1006": "Book does not contain any page",
"ERR_1007": "Some entries could not be analyzed",
"ERR_1008": "Unknown error while getting book's entries"
"ERR_1008": "Unknown error while getting book's entries",
"ERR_1009": "A read list with that name already exists",
"ERR_1010": "No books were matched within the read list request",
"ERR_1011": "No unique match for series",
"ERR_1012": "No match for series",
"ERR_1013": "No unique match for book number within series",
"ERR_1014": "No match for book number within series",
"ERR_1015": "Error while deserializing ComicRack ReadingList"
},
"filter": {
"age_rating": "age rating",

View file

@ -7,6 +7,10 @@
"itemsPerPageText": "Filas por página:",
"sortBy": "Ordenado por"
},
"fileInput": {
"counter": "{0} archivos",
"counterSize": "{0} archivos ({1} en total)"
},
"noDataText": "No hay datos disponibles"
},
"account_settings": {

View file

@ -7,6 +7,10 @@
"itemsPerPageText": "Élements par page:",
"sortBy": "Trier par"
},
"fileInput": {
"counter": "{0} fichiers",
"counterSize": "{0} fichiers ({1} au total)"
},
"noDataText": "Aucune donnée disponible"
},
"account_settings": {

View file

@ -6,7 +6,12 @@
"dataTable": {
"itemsPerPageText": "Righe per pagina:",
"sortBy": "Ordina per"
}
},
"fileInput": {
"counter": "{0} files",
"counterSize": "{0} files ({1} in totale)"
},
"noDataText": "Nessun elemento disponibile"
},
"account_settings": {
"account_settings": "Impostazioni Account",

View file

@ -7,6 +7,10 @@
"itemsPerPageText": "ページ毎の行数:",
"sortBy": "ソート方式"
},
"fileInput": {
"counter": "{0} ファイル",
"counterSize": "{0} ファイル (合計 {1})"
},
"noDataText": "データがありません"
},
"account_settings": {

View file

@ -7,6 +7,10 @@
"itemsPerPageText": "Rader per side:",
"sortBy": "Sorter etter"
},
"fileInput": {
"counter": "{0} filer",
"counterSize": "{0} filer ({1} totalt)"
},
"noDataText": "Ingen data er tilgjengelig"
},
"account_settings": {

View file

@ -7,6 +7,10 @@
"itemsPerPageText": "Linhas por página:",
"sortBy": "Ordenar por"
},
"fileInput": {
"counter": "{0} arquivo(s)",
"counterSize": "{0} arquivo(s) ({1} no total)"
},
"noDataText": "Não há dados disponíveis"
},
"account_settings": {

View file

@ -7,6 +7,10 @@
"itemsPerPageText": "Строк на странице:",
"sortBy": "Сортировать по"
},
"fileInput": {
"counter": "Файлов: {0}",
"counterSize": "Файлов: {0} (всего {1})"
},
"noDataText": "Отсутствуют данные"
},
"account_settings": {

View file

@ -7,6 +7,10 @@
"itemsPerPageText": "Rader per sida:",
"sortBy": "Sortera efter"
},
"fileInput": {
"counter": "{0} filer",
"counterSize": "{0} filer (av {1} totalt)"
},
"noDataText": "Ingen data tillgänglig"
},
"account_settings": {

View file

@ -7,6 +7,10 @@
"itemsPerPageText": "每页行数:",
"sortBy": "排序方式"
},
"fileInput": {
"counter": "{0} 个文件",
"counterSize": "{0} 个文件(共 {1}"
},
"noDataText": "无数据可用"
},
"account_settings": {

View file

@ -70,6 +70,12 @@ const router = new Router({
beforeEnter: adminGuard,
component: () => import(/* webpackChunkName: "settings-server" */ './views/SettingsServer.vue'),
},
{
path: '/settings/data-import',
name: 'settings-data-import',
beforeEnter: adminGuard,
component: () => import(/* webpackChunkName: "settings-data-import" */ './views/SettingsDataImport.vue'),
},
],
},
{

View file

@ -1,5 +1,5 @@
import { AxiosInstance } from 'axios'
import { BookDto } from '@/types/komga-books'
import {AxiosInstance} from 'axios'
import {BookDto} from '@/types/komga-books'
const qs = require('qs')
@ -8,19 +8,19 @@ const API_READLISTS = '/api/v1/readlists'
export default class KomgaReadListsService {
private http: AxiosInstance
constructor (http: AxiosInstance) {
constructor(http: AxiosInstance) {
this.http = http
}
async getReadLists (libraryIds?: string[], pageRequest?: PageRequest, search?: string): Promise<Page<ReadListDto>> {
async getReadLists(libraryIds?: string[], pageRequest?: PageRequest, search?: string): Promise<Page<ReadListDto>> {
try {
const params = { ...pageRequest } as any
const params = {...pageRequest} as any
if (libraryIds) params.library_id = libraryIds
if (search) params.search = search
return (await this.http.get(API_READLISTS, {
params: params,
paramsSerializer: params => qs.stringify(params, { indices: false }),
paramsSerializer: params => qs.stringify(params, {indices: false}),
})).data
} catch (e) {
let msg = 'An error occurred while trying to retrieve readLists'
@ -31,7 +31,7 @@ export default class KomgaReadListsService {
}
}
async getOneReadList (readListId: string): Promise<ReadListDto> {
async getOneReadList(readListId: string): Promise<ReadListDto> {
try {
return (await this.http.get(`${API_READLISTS}/${readListId}`)).data
} catch (e) {
@ -43,7 +43,7 @@ export default class KomgaReadListsService {
}
}
async postReadList (readList: ReadListCreationDto): Promise<ReadListDto> {
async postReadList(readList: ReadListCreationDto): Promise<ReadListDto> {
try {
return (await this.http.post(API_READLISTS, readList)).data
} catch (e) {
@ -55,7 +55,25 @@ export default class KomgaReadListsService {
}
}
async patchReadList (readListId: string, readList: ReadListUpdateDto) {
async postReadListImport(files: any): Promise<ReadListRequestResultDto[]> {
try {
const formData = new FormData();
files.forEach((f: any) => formData.append("files", f))
return (await this.http.post(`${API_READLISTS}/import`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})).data
} catch (e) {
let msg = `An error occurred while trying to import readlists'`
if (e.response.data.message) {
msg += `: ${e.response.data.message}`
}
throw new Error(msg)
}
}
async patchReadList(readListId: string, readList: ReadListUpdateDto) {
try {
await this.http.patch(`${API_READLISTS}/${readListId}`, readList)
} catch (e) {
@ -67,7 +85,7 @@ export default class KomgaReadListsService {
}
}
async deleteReadList (readListId: string) {
async deleteReadList(readListId: string) {
try {
await this.http.delete(`${API_READLISTS}/${readListId}`)
} catch (e) {
@ -79,9 +97,9 @@ export default class KomgaReadListsService {
}
}
async getBooks (readListId: string, pageRequest?: PageRequest): Promise<Page<BookDto>> {
async getBooks(readListId: string, pageRequest?: PageRequest): Promise<Page<BookDto>> {
try {
const params = { ...pageRequest }
const params = {...pageRequest}
return (await this.http.get(`${API_READLISTS}/${readListId}/books`, {
params: params,
})).data
@ -94,7 +112,7 @@ export default class KomgaReadListsService {
}
}
async getBookSiblingNext (readListId: string, bookId: string): Promise<BookDto> {
async getBookSiblingNext(readListId: string, bookId: string): Promise<BookDto> {
try {
return (await this.http.get(`${API_READLISTS}/${readListId}/books/${bookId}/next`)).data
} catch (e) {
@ -106,7 +124,7 @@ export default class KomgaReadListsService {
}
}
async getBookSiblingPrevious (readListId: string, bookId: string): Promise<BookDto> {
async getBookSiblingPrevious(readListId: string, bookId: string): Promise<BookDto> {
try {
return (await this.http.get(`${API_READLISTS}/${readListId}/books/${bookId}/previous`)).data
} catch (e) {

View file

@ -16,3 +16,20 @@ interface ReadListUpdateDto {
name?: string,
bookIds?: string[]
}
interface ReadListRequestResultDto {
readList?: ReadListDto,
unmatchedBooks: ReadListRequestResultBookDto[],
errorCode: string,
requestName: string,
}
interface ReadListRequestResultBookDto {
book: ReadListRequestBookDto,
errorCode: string,
}
interface ReadListRequestBookDto {
series: string,
number: number,
}

View file

@ -4,6 +4,7 @@
<v-tab :to="{name: 'settings-analysis'}">{{ $t('media_analysis.media_analysis') }}</v-tab>
<v-tab :to="{name: 'settings-users'}">{{ $t('users.users') }}</v-tab>
<v-tab :to="{name: 'settings-server'}">{{ $t('server.tab_title') }}</v-tab>
<v-tab :to="{name: 'settings-data-import'}">{{ $t('data_import.tab_title') }}</v-tab>
</v-tabs>
<router-view/>
</div>

View file

@ -0,0 +1,121 @@
<template>
<v-container fluid class="pa-6">
<v-row>
<v-col class="text-h5">{{ $t('data_import.import_read_lists') }}</v-col>
</v-row>
<v-form v-model="valid" ref="form">
<v-row>
<v-col class="body-2" v-html="$t('data_import.comicrack_preambule_html')"></v-col>
</v-row>
<v-row align="center">
<v-col cols>
<v-file-input
v-model="files"
:label="$t('data_import.field_files_label')"
multiple
prepend-icon="mdi-file-document-multiple"
accept=".cbl"
show-size
:rules="rules"
/>
</v-col>
<v-col cols="auto">
<v-btn
:disabled="!valid"
@click="importFiles"
>{{ $t('data_import.button_import') }}
</v-btn>
</v-col>
</v-row>
</v-form>
<template v-if="results.length > 0">
<div class="mb-4 body-2">{{ $t('data_import.results_preambule') }}</div>
<v-expansion-panels>
<v-expansion-panel
v-for="result in results"
:key="result.requestName"
>
<v-expansion-panel-header>
<v-row no-gutters align="center">
<v-col cols="1">
<v-icon v-if="result.readList === null" color="error">mdi-alert</v-icon>
<v-icon v-if="result.readList && result.unmatchedBooks.length === 0" color="success">mdi-check</v-icon>
<v-icon v-if="result.readList && result.unmatchedBooks.length > 0" color="warning">mdi-alert-circle
</v-icon>
</v-col>
<v-col cols="3">
{{ result.requestName }}
</v-col>
<v-col
cols="8"
class="text--secondary"
>
<template v-if="result.readList">{{
$t('data_import.imported_as', {name: result.readList.name})
}}
</template>
<template v-else>{{ convertErrorCodes(result.errorCode) }}</template>
</v-col>
</v-row>
</v-expansion-panel-header>
<v-expansion-panel-content v-if="result.unmatchedBooks.length > 0">
<v-list elevation="1" dense>
<v-list-item
v-for="(book, i) in result.unmatchedBooks"
:key="i"
three-line
>
<v-list-item-content>
<v-list-item-title>{{ convertErrorCodes(book.errorCode) }}</v-list-item-title>
<v-list-item-subtitle>{{
$t('data_import.book_series', {name: book.book.series})
}}
</v-list-item-subtitle>
<v-list-item-subtitle>{{
$t('data_import.book_number', {name: book.book.number})
}}
</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
</v-list>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
</template>
</v-container>
</template>
<script lang="ts">
import Vue from 'vue'
import {convertErrorCodes} from "@/functions/error-codes";
export default Vue.extend({
name: 'SettingsDataImport',
data: () => ({
convertErrorCodes,
files: [],
results: [] as ReadListRequestResultDto[],
valid: true,
}),
computed: {
rules(): any {
return [
(value: any) => !value || value.reduce((a: any, b: any) => a + (b['size'] || 0), 0) < 10_000_000 || this.$t('data_import.size_limit', {size: '10'}).toString(),
]
},
},
methods: {
async importFiles() {
this.results.splice(0, this.results.length, ...(await this.$komgaReadLists.postReadListImport(this.files)));
(this.$refs.form as any).reset()
},
},
})
</script>
<style scoped>
</style>