mirror of
https://github.com/gotson/komga.git
synced 2026-05-17 11:33:19 +02:00
feat: page hashing enhancement
only hash pages for cbz delete non-cbz page hashes store page hashes
This commit is contained in:
parent
368d0d5147
commit
a96335dbee
34 changed files with 1189 additions and 165 deletions
30
komga-webui/package-lock.json
generated
30
komga-webui/package-lock.json
generated
|
|
@ -12,6 +12,7 @@
|
||||||
"axios": "^0.25.0",
|
"axios": "^0.25.0",
|
||||||
"core-js": "^3.20.3",
|
"core-js": "^3.20.3",
|
||||||
"date-fns": "^2.28.0",
|
"date-fns": "^2.28.0",
|
||||||
|
"filesize": "^8.0.7",
|
||||||
"js-file-downloader": "^1.1.24",
|
"js-file-downloader": "^1.1.24",
|
||||||
"language-tags": "^1.0.5",
|
"language-tags": "^1.0.5",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
|
@ -9261,10 +9262,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/filesize": {
|
"node_modules/filesize": {
|
||||||
"version": "3.6.1",
|
"version": "8.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz",
|
||||||
"integrity": "sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==",
|
"integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4.0"
|
"node": ">= 0.4.0"
|
||||||
}
|
}
|
||||||
|
|
@ -20275,6 +20275,15 @@
|
||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/webpack-bundle-analyzer/node_modules/filesize": {
|
||||||
|
"version": "3.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz",
|
||||||
|
"integrity": "sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/webpack-bundle-analyzer/node_modules/ws": {
|
"node_modules/webpack-bundle-analyzer/node_modules/ws": {
|
||||||
"version": "6.2.2",
|
"version": "6.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz",
|
||||||
|
|
@ -28286,10 +28295,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"filesize": {
|
"filesize": {
|
||||||
"version": "3.6.1",
|
"version": "8.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz",
|
||||||
"integrity": "sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==",
|
"integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"fill-range": {
|
"fill-range": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
|
|
@ -37203,6 +37211,12 @@
|
||||||
"integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==",
|
"integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"filesize": {
|
||||||
|
"version": "3.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz",
|
||||||
|
"integrity": "sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"ws": {
|
"ws": {
|
||||||
"version": "6.2.2",
|
"version": "6.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz",
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
"axios": "^0.25.0",
|
"axios": "^0.25.0",
|
||||||
"core-js": "^3.20.3",
|
"core-js": "^3.20.3",
|
||||||
"date-fns": "^2.28.0",
|
"date-fns": "^2.28.0",
|
||||||
|
"filesize": "^8.0.7",
|
||||||
"js-file-downloader": "^1.1.24",
|
"js-file-downloader": "^1.1.24",
|
||||||
"language-tags": "^1.0.5",
|
"language-tags": "^1.0.5",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
|
|
||||||
163
komga-webui/src/components/PageHashKnownCard.vue
Normal file
163
komga-webui/src/components/PageHashKnownCard.vue
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
<template>
|
||||||
|
<v-card>
|
||||||
|
<v-container fluid>
|
||||||
|
<v-row>
|
||||||
|
<v-col>
|
||||||
|
<v-img
|
||||||
|
width="200"
|
||||||
|
height="300"
|
||||||
|
contain
|
||||||
|
@click="$emit('image-clicked')"
|
||||||
|
:src="pageHashKnownThumbnailUrl(hash)"
|
||||||
|
style="cursor: zoom-in"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</v-col>
|
||||||
|
<v-col>
|
||||||
|
<v-card-text style="min-width: 200px">
|
||||||
|
<v-container>
|
||||||
|
<v-row>
|
||||||
|
<v-col>
|
||||||
|
<v-chip label small :color="actionColor">
|
||||||
|
{{ $t(`enums.page_hash_action.${hash.action}`) }}
|
||||||
|
</v-chip>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col>
|
||||||
|
<div>{{ hash.mediaType }}</div>
|
||||||
|
<div>{{ getFileSize(hash.size) || $t('duplicate_pages.unknown_size') }}</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col>
|
||||||
|
<v-btn
|
||||||
|
v-if="matchCount"
|
||||||
|
@click="$emit('matches-clicked')"
|
||||||
|
outlined
|
||||||
|
rounded
|
||||||
|
>
|
||||||
|
{{ $tc('duplicate_pages.matches_n', matchCount) }}
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col>
|
||||||
|
<div
|
||||||
|
v-if="hash.deleteCount"
|
||||||
|
>{{ $t('duplicate_pages.deleted_count', {count: hash.deleteCount}) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="hash.deleteCount"
|
||||||
|
>{{ $t('duplicate_pages.saved_size', {size: getFileSize(hash.size * hash.deleteCount)}) }}
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</v-card-text>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<v-btn v-if="hash.action === PageHashAction.DELETE_MANUAL" color="primary" @click="deleteMatches">
|
||||||
|
{{ $t('duplicate_pages.action_delete_matches') }}
|
||||||
|
</v-btn>
|
||||||
|
<v-btn v-if="hash.action !== PageHashAction.IGNORE" text @click="ignore">{{
|
||||||
|
$t('duplicate_pages.action_ignore')
|
||||||
|
}}
|
||||||
|
</v-btn>
|
||||||
|
<v-btn v-if="hash.action !== PageHashAction.DELETE_MANUAL" text @click="deleteManual">
|
||||||
|
{{ $t('duplicate_pages.action_delete_manual') }}
|
||||||
|
</v-btn>
|
||||||
|
<v-btn v-if="hash.action !== PageHashAction.DELETE_AUTO" text @click="deleteAuto" :disabled="!hash.size">
|
||||||
|
{{ $t('duplicate_pages.action_delete_auto') }}
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue, {PropType} from 'vue'
|
||||||
|
import {pageHashKnownThumbnailUrl} from '@/functions/urls'
|
||||||
|
import {PageHashKnownDto} from '@/types/komga-pagehashes'
|
||||||
|
import {PageHashAction} from '@/types/enum-pagehashes'
|
||||||
|
import {getFileSize} from '@/functions/file'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
name: 'PageHashKnownCard',
|
||||||
|
props: {
|
||||||
|
hash: {
|
||||||
|
type: Object as PropType<PageHashKnownDto>,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
pageHashKnownThumbnailUrl,
|
||||||
|
getFileSize,
|
||||||
|
PageHashAction,
|
||||||
|
matchCount: undefined as number | undefined,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
actionColor(): string {
|
||||||
|
switch (this.hash.action) {
|
||||||
|
case PageHashAction.DELETE_AUTO:
|
||||||
|
return 'success'
|
||||||
|
case PageHashAction.DELETE_MANUAL:
|
||||||
|
return 'warning'
|
||||||
|
default:
|
||||||
|
return 'grey'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.getMatchCount()
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
hash: {
|
||||||
|
handler() {
|
||||||
|
this.getMatchCount()
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async getMatchCount() {
|
||||||
|
if (this.hash?.action === PageHashAction.DELETE_MANUAL)
|
||||||
|
this.matchCount = (await this.$komgaPageHashes.getUnknownPageHashMatches(this.hash, {size: 0})).totalElements
|
||||||
|
else
|
||||||
|
this.matchCount = undefined
|
||||||
|
},
|
||||||
|
deleteMatches() {
|
||||||
|
},
|
||||||
|
ignore() {
|
||||||
|
this.updatePageHash(PageHashAction.IGNORE)
|
||||||
|
},
|
||||||
|
deleteManual() {
|
||||||
|
this.updatePageHash(PageHashAction.DELETE_MANUAL)
|
||||||
|
},
|
||||||
|
deleteAuto() {
|
||||||
|
this.updatePageHash(PageHashAction.DELETE_AUTO)
|
||||||
|
},
|
||||||
|
async updatePageHash(action: PageHashAction) {
|
||||||
|
try {
|
||||||
|
const p = {
|
||||||
|
hash: this.hash.hash,
|
||||||
|
mediaType: this.hash.mediaType,
|
||||||
|
size: this.hash.size,
|
||||||
|
action: action,
|
||||||
|
}
|
||||||
|
await this.$komgaPageHashes.createOrUpdatePageHash(p)
|
||||||
|
this.$emit('updated', p)
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
@ -33,13 +33,13 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue, {PropType} from 'vue'
|
import Vue, {PropType} from 'vue'
|
||||||
import {bookPageThumbnailUrl} from '@/functions/urls'
|
import {bookPageThumbnailUrl} from '@/functions/urls'
|
||||||
import {PageHashMatchDto, PageHashUnknownDto} from '@/types/komga-pagehashes'
|
import {PageHashDto, PageHashMatchDto, PageHashUnknownDto} from '@/types/komga-pagehashes'
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: 'PageHashMatchesTable',
|
name: 'PageHashMatchesTable',
|
||||||
props: {
|
props: {
|
||||||
hash: {
|
hash: {
|
||||||
type: Object as PropType<PageHashUnknownDto>,
|
type: Object as PropType<PageHashDto>,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
|
@ -74,7 +74,7 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async loadData(hash: PageHashUnknownDto) {
|
async loadData(hash: PageHashDto) {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
|
|
||||||
const {sortBy, sortDesc, page, itemsPerPage} = this.options
|
const {sortBy, sortDesc, page, itemsPerPage} = this.options
|
||||||
|
|
|
||||||
105
komga-webui/src/components/PageHashUnknownCard.vue
Normal file
105
komga-webui/src/components/PageHashUnknownCard.vue
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
<template>
|
||||||
|
<v-card>
|
||||||
|
<v-container fluid>
|
||||||
|
<v-row>
|
||||||
|
<v-col>
|
||||||
|
<v-img
|
||||||
|
width="200"
|
||||||
|
height="300"
|
||||||
|
contain
|
||||||
|
@click="$emit('image-clicked')"
|
||||||
|
:src="pageHashUnknownThumbnailUrl(hash, 500)"
|
||||||
|
style="cursor: zoom-in"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</v-col>
|
||||||
|
<v-col>
|
||||||
|
<v-card-text style="min-width: 200px">
|
||||||
|
<v-container>
|
||||||
|
<v-row>
|
||||||
|
<v-col>
|
||||||
|
<div>{{ hash.mediaType }}</div>
|
||||||
|
<div>{{ getFileSize(hash.size) || $t('duplicate_pages.unknown_size') }}</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col>
|
||||||
|
<v-btn
|
||||||
|
@click="$emit('matches-clicked')"
|
||||||
|
outlined
|
||||||
|
rounded
|
||||||
|
>
|
||||||
|
{{ $tc('duplicate_pages.matches_n', hash.matchCount) }}
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col>
|
||||||
|
<div
|
||||||
|
v-if="hash.size"
|
||||||
|
>{{ $t('duplicate_pages.delete_to_save', {size: getFileSize(hash.size * hash.matchCount)}) }}
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
</v-container>
|
||||||
|
</v-card-text>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<v-btn text @click="ignore">{{ $t('duplicate_pages.action_ignore') }}</v-btn>
|
||||||
|
<v-btn text @click="deleteManual">{{ $t('duplicate_pages.action_delete_manual') }}</v-btn>
|
||||||
|
<v-btn text @click="deleteAuto" :disabled="!hash.size">{{ $t('duplicate_pages.action_delete_auto') }}</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue, {PropType} from 'vue'
|
||||||
|
import {pageHashUnknownThumbnailUrl} from '@/functions/urls'
|
||||||
|
import {PageHashUnknownDto} from '@/types/komga-pagehashes'
|
||||||
|
import {PageHashAction} from '@/types/enum-pagehashes'
|
||||||
|
import {getFileSize} from '@/functions/file'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
name: 'PageHashUnknownCard',
|
||||||
|
props: {
|
||||||
|
hash: {
|
||||||
|
type: Object as PropType<PageHashUnknownDto>,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
pageHashUnknownThumbnailUrl,
|
||||||
|
getFileSize,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
ignore() {
|
||||||
|
this.createPageHash(PageHashAction.IGNORE)
|
||||||
|
},
|
||||||
|
deleteManual() {
|
||||||
|
this.createPageHash(PageHashAction.DELETE_MANUAL)
|
||||||
|
},
|
||||||
|
deleteAuto() {
|
||||||
|
this.createPageHash(PageHashAction.DELETE_AUTO)
|
||||||
|
},
|
||||||
|
async createPageHash(action: PageHashAction) {
|
||||||
|
try {
|
||||||
|
await this.$komgaPageHashes.createOrUpdatePageHash({
|
||||||
|
hash: this.hash.hash,
|
||||||
|
mediaType: this.hash.mediaType,
|
||||||
|
size: this.hash.size,
|
||||||
|
action: action,
|
||||||
|
})
|
||||||
|
this.$emit('created')
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import filesize from 'filesize'
|
||||||
|
|
||||||
export async function getFileFromUrl(url: string, name: string = url, defaultType = 'image/jpeg') {
|
export async function getFileFromUrl(url: string, name: string = url, defaultType = 'image/jpeg') {
|
||||||
const response = await fetch(url)
|
const response = await fetch(url)
|
||||||
const data = await response.blob()
|
const data = await response.blob()
|
||||||
|
|
@ -5,3 +7,11 @@ export async function getFileFromUrl(url: string, name: string = url, defaultTyp
|
||||||
type: data.type || defaultType,
|
type: data.type || defaultType,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const filesizePartial = filesize.partial({round: 1})
|
||||||
|
|
||||||
|
export function getFileSize(n?: number): string | undefined {
|
||||||
|
if(!n) return undefined
|
||||||
|
return filesizePartial(n)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import {PageHashUnknownDto} from '@/types/komga-pagehashes'
|
import {PageHashKnownDto, PageHashUnknownDto} from '@/types/komga-pagehashes'
|
||||||
|
|
||||||
const fullUrl = process.env.VUE_APP_KOMGA_API_URL
|
const fullUrl = process.env.VUE_APP_KOMGA_API_URL
|
||||||
? process.env.VUE_APP_KOMGA_API_URL
|
? process.env.VUE_APP_KOMGA_API_URL
|
||||||
|
|
@ -75,9 +75,13 @@ export function transientBookPageUrl(transientBookId: string, page: number): str
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pageHashUnknownThumbnailUrl(pageHash: PageHashUnknownDto, resize?: number): string {
|
export function pageHashUnknownThumbnailUrl(pageHash: PageHashUnknownDto, resize?: number): string {
|
||||||
let url = `${urls.originNoSlash}/api/v1/page-hashes/unknown/${pageHash.hash}/thumbnail?media_type=${pageHash.mediaType}&file_size=${pageHash.sizeBytes || -1}`
|
let url = `${urls.originNoSlash}/api/v1/page-hashes/unknown/${pageHash.hash}/thumbnail?media_type=${pageHash.mediaType}&file_size=${pageHash.size || -1}`
|
||||||
if(resize) {
|
if(resize) {
|
||||||
url += `&resize=${resize}`
|
url += `&resize=${resize}`
|
||||||
}
|
}
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function pageHashKnownThumbnailUrl(pageHash: PageHashKnownDto): string {
|
||||||
|
return `${urls.originNoSlash}/api/v1/page-hashes/${pageHash.hash}/thumbnail?media_type=${pageHash.mediaType}&file_size=${pageHash.size || -1}`
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -531,16 +531,21 @@
|
||||||
"duplicate_pages": {
|
"duplicate_pages": {
|
||||||
"action_delete_auto": "Auto delete",
|
"action_delete_auto": "Auto delete",
|
||||||
"action_delete_manual": "Manual delete",
|
"action_delete_manual": "Manual delete",
|
||||||
|
"action_delete_matches": "Delete matches",
|
||||||
"action_ignore": "Ignore",
|
"action_ignore": "Ignore",
|
||||||
"matches_n": "No matches | 1 match | {count} matches",
|
|
||||||
"title": "Duplicate pages",
|
|
||||||
"unknown_size": "Unknown size",
|
|
||||||
"delete_to_save": "Delete to save {size}",
|
"delete_to_save": "Delete to save {size}",
|
||||||
|
"deleted_count": "Deleted {count} times",
|
||||||
"filter": {
|
"filter": {
|
||||||
"total_size": "Total size",
|
"count": "Count",
|
||||||
|
"delete_count": "Deletion count",
|
||||||
|
"delete_size": "Space saved",
|
||||||
"size": "Size",
|
"size": "Size",
|
||||||
"count": "Count"
|
"total_size": "Total size"
|
||||||
}
|
},
|
||||||
|
"matches_n": "No matches | 1 match | {count} matches",
|
||||||
|
"saved_size": "Saved {size}",
|
||||||
|
"title": "Duplicate pages",
|
||||||
|
"unknown_size": "Unknown size"
|
||||||
},
|
},
|
||||||
"duplicates": {
|
"duplicates": {
|
||||||
"file_hash": "File hash",
|
"file_hash": "File hash",
|
||||||
|
|
@ -560,6 +565,11 @@
|
||||||
"UNKNOWN": "Unknown",
|
"UNKNOWN": "Unknown",
|
||||||
"UNSUPPORTED": "Unsupported"
|
"UNSUPPORTED": "Unsupported"
|
||||||
},
|
},
|
||||||
|
"page_hash_action": {
|
||||||
|
"DELETE_AUTO": "Auto delete",
|
||||||
|
"DELETE_MANUAL": "Manual delete",
|
||||||
|
"IGNORE": "Ignore"
|
||||||
|
},
|
||||||
"reading_direction": {
|
"reading_direction": {
|
||||||
"LEFT_TO_RIGHT": "Left to right",
|
"LEFT_TO_RIGHT": "Left to right",
|
||||||
"RIGHT_TO_LEFT": "Right to left",
|
"RIGHT_TO_LEFT": "Right to left",
|
||||||
|
|
|
||||||
|
|
@ -97,8 +97,22 @@ const router = new Router({
|
||||||
{
|
{
|
||||||
path: '/settings/duplicate-pages',
|
path: '/settings/duplicate-pages',
|
||||||
name: 'settings-duplicate-pages',
|
name: 'settings-duplicate-pages',
|
||||||
beforeEnter: adminGuard,
|
redirect: {name: 'settings-duplicate-pages-known'},
|
||||||
component: () => import(/* webpackChunkName: "settings-duplicate-pages" */ './views/SettingsDuplicatePages.vue'),
|
component: () => import(/* webpackChunkName: "settings-duplicate-pages" */ './views/SettingsDuplicatePagesHolder.vue'),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/settings/duplicate-pages/known',
|
||||||
|
name: 'settings-duplicate-pages-known',
|
||||||
|
beforeEnter: adminGuard,
|
||||||
|
component: () => import(/* webpackChunkName: "settings-duplicate-pages" */ './views/SettingsDuplicatePagesKnown.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/settings/duplicate-pages/unknown',
|
||||||
|
name: 'settings-duplicate-pages-unknown',
|
||||||
|
beforeEnter: adminGuard,
|
||||||
|
component: () => import(/* webpackChunkName: "settings-duplicate-pages" */ './views/SettingsDuplicatePagesUnknown.vue'),
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/settings/server',
|
path: '/settings/server',
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
import {AxiosInstance} from 'axios'
|
import {AxiosInstance} from 'axios'
|
||||||
import {PageHashMatchDto, PageHashUnknownDto} from '@/types/komga-pagehashes'
|
import {
|
||||||
|
PageHashCreationDto,
|
||||||
|
PageHashDto,
|
||||||
|
PageHashKnownDto,
|
||||||
|
PageHashMatchDto,
|
||||||
|
PageHashUnknownDto,
|
||||||
|
} from '@/types/komga-pagehashes'
|
||||||
|
|
||||||
const qs = require('qs')
|
const qs = require('qs')
|
||||||
|
|
||||||
|
|
@ -12,6 +18,23 @@ export default class KomgaPageHashesService {
|
||||||
this.http = http
|
this.http = http
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getKnownHashes(actions: string[], pageRequest?: PageRequest): Promise<Page<PageHashKnownDto>> {
|
||||||
|
try {
|
||||||
|
const params = {...pageRequest} as any
|
||||||
|
if (actions) params.action = actions
|
||||||
|
return (await this.http.get(API_PAGE_HASH, {
|
||||||
|
params: params,
|
||||||
|
paramsSerializer: params => qs.stringify(params, {indices: false}),
|
||||||
|
})).data
|
||||||
|
} catch (e) {
|
||||||
|
let msg = 'An error occurred while trying to retrieve known page hashes'
|
||||||
|
if (e.response.data.message) {
|
||||||
|
msg += `: ${e.response.data.message}`
|
||||||
|
}
|
||||||
|
throw new Error(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getUnknownHashes(pageRequest?: PageRequest): Promise<Page<PageHashUnknownDto>> {
|
async getUnknownHashes(pageRequest?: PageRequest): Promise<Page<PageHashUnknownDto>> {
|
||||||
try {
|
try {
|
||||||
return (await this.http.get(`${API_PAGE_HASH}/unknown`, {
|
return (await this.http.get(`${API_PAGE_HASH}/unknown`, {
|
||||||
|
|
@ -27,19 +50,31 @@ export default class KomgaPageHashesService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUnknownPageHashMatches(hash: PageHashUnknownDto, pageRequest?: PageRequest): Promise<Page<PageHashMatchDto>> {
|
async getUnknownPageHashMatches(pageHash: PageHashDto, pageRequest?: PageRequest): Promise<Page<PageHashMatchDto>> {
|
||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
...pageRequest,
|
...pageRequest,
|
||||||
media_type: hash.mediaType,
|
media_type: pageHash.mediaType,
|
||||||
file_size: hash.sizeBytes || -1,
|
file_size: pageHash.size || -1,
|
||||||
}
|
}
|
||||||
return (await this.http.get(`${API_PAGE_HASH}/unknown/${hash.hash}`, {
|
return (await this.http.get(`${API_PAGE_HASH}/unknown/${pageHash.hash}`, {
|
||||||
params: params,
|
params: params,
|
||||||
paramsSerializer: params => qs.stringify(params, {indices: false}),
|
paramsSerializer: params => qs.stringify(params, {indices: false}),
|
||||||
})).data
|
})).data
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let msg = `An error occurred while trying to retrieve matches for page hash: ${hash}`
|
let msg = `An error occurred while trying to retrieve matches for page hash: ${pageHash}`
|
||||||
|
if (e.response.data.message) {
|
||||||
|
msg += `: ${e.response.data.message}`
|
||||||
|
}
|
||||||
|
throw new Error(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createOrUpdatePageHash(pageHash: PageHashCreationDto) {
|
||||||
|
try {
|
||||||
|
await this.http.put(API_PAGE_HASH, pageHash)
|
||||||
|
} catch (e) {
|
||||||
|
let msg = `An error occurred while trying to add page hash ${pageHash}`
|
||||||
if (e.response.data.message) {
|
if (e.response.data.message) {
|
||||||
msg += `: ${e.response.data.message}`
|
msg += `: ${e.response.data.message}`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
5
komga-webui/src/types/enum-pagehashes.ts
Normal file
5
komga-webui/src/types/enum-pagehashes.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
export enum PageHashAction {
|
||||||
|
DELETE_AUTO = 'DELETE_AUTO',
|
||||||
|
DELETE_MANUAL = 'DELETE_MANUAL',
|
||||||
|
IGNORE = 'IGNORE',
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
export interface PageHashUnknownDto {
|
import {PageHashAction} from '@/types/enum-pagehashes'
|
||||||
|
|
||||||
|
export interface PageHashDto {
|
||||||
hash: string,
|
hash: string,
|
||||||
mediaType: string,
|
mediaType: string,
|
||||||
sizeBytes?: number,
|
size?: number,
|
||||||
size?: string,
|
}
|
||||||
totalSize?: string,
|
|
||||||
|
export interface PageHashUnknownDto extends PageHashDto {
|
||||||
matchCount: number,
|
matchCount: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -13,3 +16,17 @@ export interface PageHashMatchDto {
|
||||||
pageNumber: number,
|
pageNumber: number,
|
||||||
fileName: string,
|
fileName: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PageHashCreationDto {
|
||||||
|
hash: string,
|
||||||
|
mediaType: string,
|
||||||
|
size?: number,
|
||||||
|
action: PageHashAction,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PageHashKnownDto extends PageHashDto {
|
||||||
|
action: PageHashAction
|
||||||
|
deleteCount: number,
|
||||||
|
createdDate: string,
|
||||||
|
lastModifiedDate: string,
|
||||||
|
}
|
||||||
|
|
|
||||||
21
komga-webui/src/views/SettingsDuplicatePagesHolder.vue
Normal file
21
komga-webui/src/views/SettingsDuplicatePagesHolder.vue
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-tabs grow>
|
||||||
|
<v-tab :to="{name: 'settings-duplicate-pages-known'}">Known</v-tab>
|
||||||
|
<v-tab :to="{name: 'settings-duplicate-pages-unknown'}">New</v-tab>
|
||||||
|
</v-tabs>
|
||||||
|
<router-view/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
name: 'SettingsDuplicatePagesHolder',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
224
komga-webui/src/views/SettingsDuplicatePagesKnown.vue
Normal file
224
komga-webui/src/views/SettingsDuplicatePagesKnown.vue
Normal file
|
|
@ -0,0 +1,224 @@
|
||||||
|
<template>
|
||||||
|
<v-container fluid class="pa-6">
|
||||||
|
<v-row align="center">
|
||||||
|
<v-col cols="auto">
|
||||||
|
<v-pagination
|
||||||
|
v-if="totalPages > 1"
|
||||||
|
v-model="page"
|
||||||
|
:total-visible="paginationVisible"
|
||||||
|
:length="totalPages"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-spacer/>
|
||||||
|
|
||||||
|
<v-col>
|
||||||
|
<v-select
|
||||||
|
:items="selectOptions"
|
||||||
|
v-model="filterActive"
|
||||||
|
small-chips
|
||||||
|
deletable-chips
|
||||||
|
multiple
|
||||||
|
>
|
||||||
|
</v-select>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col
|
||||||
|
v-for="sortOption in sortOptions"
|
||||||
|
:key="sortOption.key"
|
||||||
|
cols="auto"
|
||||||
|
>
|
||||||
|
<v-btn
|
||||||
|
rounded
|
||||||
|
small
|
||||||
|
:color="sortActive.key === sortOption.key ? 'primary' : ''"
|
||||||
|
@click="setSort(sortOption.key)"
|
||||||
|
>
|
||||||
|
{{ sortOption.name }}
|
||||||
|
<v-icon
|
||||||
|
v-if="sortActive.key === sortOption.key"
|
||||||
|
class="ms-2"
|
||||||
|
>
|
||||||
|
{{ sortActive.order === 'desc' ? 'mdi-sort-variant' : 'mdi-sort-reverse-variant' }}
|
||||||
|
</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-slide-x-transition
|
||||||
|
v-for="(element, i) in elements"
|
||||||
|
:key="i"
|
||||||
|
>
|
||||||
|
<page-hash-known-card
|
||||||
|
class="ma-2"
|
||||||
|
:hash="element"
|
||||||
|
@image-clicked="showDialogImage(element)"
|
||||||
|
@matches-clicked="showDialogMatches(element)"
|
||||||
|
@updated="pageHashUpdated"
|
||||||
|
/>
|
||||||
|
</v-slide-x-transition>
|
||||||
|
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-pagination
|
||||||
|
v-if="totalPages > 1"
|
||||||
|
v-model="page"
|
||||||
|
:total-visible="paginationVisible"
|
||||||
|
:length="totalPages"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-dialog v-model="dialogImage">
|
||||||
|
<v-card>
|
||||||
|
<v-card-text>
|
||||||
|
<v-img
|
||||||
|
@click="dialogImage = false"
|
||||||
|
contain
|
||||||
|
:src="pageHashKnownThumbnailUrl(dialogImagePageHash)"
|
||||||
|
style="cursor: zoom-out;"
|
||||||
|
/>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<v-dialog
|
||||||
|
v-model="dialogMatches"
|
||||||
|
scrollable
|
||||||
|
>
|
||||||
|
<v-card>
|
||||||
|
<v-card-text>
|
||||||
|
<page-hash-matches-table
|
||||||
|
:hash="dialogMatchesPageHash"
|
||||||
|
class="my-2"
|
||||||
|
/>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-btn @click="dialogMatches = false" text>{{ $t('common.close') }}</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue'
|
||||||
|
import {PageHashDto, PageHashKnownDto} from '@/types/komga-pagehashes'
|
||||||
|
import {pageHashKnownThumbnailUrl} from '@/functions/urls'
|
||||||
|
import PageHashKnownCard from '@/components/PageHashKnownCard.vue'
|
||||||
|
import {PageHashAction} from '@/types/enum-pagehashes'
|
||||||
|
import PageHashMatchesTable from '@/components/PageHashMatchesTable.vue'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
name: 'SettingsDuplicatePagesKnown',
|
||||||
|
components: {PageHashKnownCard, PageHashMatchesTable},
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
pageHashKnownThumbnailUrl,
|
||||||
|
elements: [] as PageHashKnownDto[],
|
||||||
|
totalElements: 0,
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
sortActive: {key: 'deleteSize', order: 'desc'} as SortActive,
|
||||||
|
filterActive: [PageHashAction.DELETE_AUTO, PageHashAction.DELETE_MANUAL],
|
||||||
|
dialogImage: false,
|
||||||
|
dialogMatches: false,
|
||||||
|
dialogImagePageHash: {} as PageHashKnownDto,
|
||||||
|
dialogMatchesPageHash: {} as PageHashDto,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
await this.loadData(this.page, this.sortActive, this.filterActive)
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
page(val) {
|
||||||
|
this.loadData(val, this.sortActive, this.filterActive)
|
||||||
|
},
|
||||||
|
sortActive(val) {
|
||||||
|
this.loadData(this.page, val, this.filterActive)
|
||||||
|
},
|
||||||
|
filterActive(val) {
|
||||||
|
this.loadData(this.page, this.sortActive, val)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
selectOptions(): object[] {
|
||||||
|
return Object.keys(PageHashAction).map(x => ({
|
||||||
|
text: this.$t(`enums.page_hash_action.${x.valueOf()}`),
|
||||||
|
value: x.valueOf(),
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
sortOptions(): SortOption[] {
|
||||||
|
return [
|
||||||
|
{name: this.$t('duplicate_pages.filter.delete_size').toString(), key: 'deleteSize'},
|
||||||
|
{name: this.$t('duplicate_pages.filter.size').toString(), key: 'fileSize'},
|
||||||
|
{name: this.$t('duplicate_pages.filter.delete_count').toString(), key: 'deleteCount'},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
paginationVisible(): number {
|
||||||
|
switch (this.$vuetify.breakpoint.name) {
|
||||||
|
case 'xs':
|
||||||
|
return 5
|
||||||
|
case 'sm':
|
||||||
|
case 'md':
|
||||||
|
return 10
|
||||||
|
case 'lg':
|
||||||
|
case 'xl':
|
||||||
|
default:
|
||||||
|
return 15
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async loadData(page: number, sort: SortActive, actions: string[]) {
|
||||||
|
const pageRequest = {
|
||||||
|
page: page - 1,
|
||||||
|
sort: [`${sort.key},${sort.order}`],
|
||||||
|
} as PageRequest
|
||||||
|
|
||||||
|
const elementsPage = await this.$komgaPageHashes.getKnownHashes(actions, pageRequest)
|
||||||
|
this.totalElements = elementsPage.totalElements
|
||||||
|
this.totalPages = elementsPage.totalPages
|
||||||
|
this.elements = elementsPage.content
|
||||||
|
},
|
||||||
|
setSort(key: string) {
|
||||||
|
if (this.sortActive.key === key) {
|
||||||
|
if (this.sortActive.order === 'desc') {
|
||||||
|
this.sortActive = {key: key, order: 'asc'}
|
||||||
|
} else {
|
||||||
|
this.sortActive = {key: key, order: 'desc'}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.sortActive = {key: key, order: 'desc'}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showDialogImage(pageHash: PageHashKnownDto) {
|
||||||
|
this.dialogImagePageHash = pageHash
|
||||||
|
this.dialogImage = true
|
||||||
|
},
|
||||||
|
showDialogMatches(pageHash: PageHashDto) {
|
||||||
|
this.dialogMatchesPageHash = pageHash
|
||||||
|
this.dialogMatches = true
|
||||||
|
},
|
||||||
|
pageHashUpdated(updated: PageHashKnownDto) {
|
||||||
|
this.elements.find(x => {
|
||||||
|
if (x.hash === updated.hash && x.mediaType === updated.mediaType && x.size === updated.size) {
|
||||||
|
x.action = updated.action
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -36,54 +36,19 @@
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-card
|
<v-slide-x-transition
|
||||||
v-for="(element, i) in elements"
|
v-for="(element, i) in elements"
|
||||||
:key="i"
|
:key="i"
|
||||||
class="ma-2"
|
|
||||||
>
|
>
|
||||||
<v-container fluid>
|
<page-hash-unknown-card
|
||||||
<v-row>
|
v-show="!hiddenElements.includes(element)"
|
||||||
<v-col>
|
class="ma-2"
|
||||||
<v-img
|
:hash="element"
|
||||||
width="200"
|
@image-clicked="showDialogImage(element)"
|
||||||
height="300"
|
@matches-clicked="showDialogMatches(element)"
|
||||||
contain
|
@created="pageHashCreated(element)"
|
||||||
@click="showDialogImage(element)"
|
/>
|
||||||
:src="pageHashUnknownThumbnailUrl(element, 500)"
|
</v-slide-x-transition>
|
||||||
style="cursor: zoom-in"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</v-col>
|
|
||||||
<v-col>
|
|
||||||
<v-card-text style="min-width: 200px">
|
|
||||||
<div>{{ element.mediaType }}</div>
|
|
||||||
<div>{{ element.size || $t('duplicate_pages.unknown_size') }}</div>
|
|
||||||
|
|
||||||
<v-btn
|
|
||||||
@click="showDialogMatches(element)"
|
|
||||||
outlined
|
|
||||||
rounded
|
|
||||||
class="my-4"
|
|
||||||
>
|
|
||||||
{{ $tc('duplicate_pages.matches_n', element.matchCount) }}
|
|
||||||
</v-btn>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="element.totalSize"
|
|
||||||
style="max-width: 100px"
|
|
||||||
>{{ $t('duplicate_pages.delete_to_save', {size: element.totalSize}) }}
|
|
||||||
</div>
|
|
||||||
</v-card-text>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</v-container>
|
|
||||||
|
|
||||||
<v-card-actions>
|
|
||||||
<v-btn text disabled>{{ $t('duplicate_pages.action_ignore') }}</v-btn>
|
|
||||||
<v-btn text disabled>{{ $t('duplicate_pages.action_delete_manual') }}</v-btn>
|
|
||||||
<v-btn text disabled>{{ $t('duplicate_pages.action_delete_auto') }}</v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
|
||||||
|
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
|
|
@ -132,16 +97,18 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import {PageHashUnknownDto} from '@/types/komga-pagehashes'
|
import {PageHashDto, PageHashUnknownDto} from '@/types/komga-pagehashes'
|
||||||
import {pageHashUnknownThumbnailUrl} from '@/functions/urls'
|
import {pageHashUnknownThumbnailUrl} from '@/functions/urls'
|
||||||
import PageHashMatchesTable from '@/components/PageHashMatchesTable.vue'
|
import PageHashMatchesTable from '@/components/PageHashMatchesTable.vue'
|
||||||
|
import PageHashUnknownCard from '@/components/PageHashUnknownCard.vue'
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: 'SettingsDuplicatePages',
|
name: 'SettingsDuplicatePagesUnknown',
|
||||||
components: {PageHashMatchesTable},
|
components: {PageHashUnknownCard, PageHashMatchesTable},
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
elements: [] as PageHashUnknownDto[],
|
elements: [] as PageHashUnknownDto[],
|
||||||
|
hiddenElements: [] as PageHashUnknownDto[],
|
||||||
totalElements: 0,
|
totalElements: 0,
|
||||||
page: 1,
|
page: 1,
|
||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
|
|
@ -149,7 +116,7 @@ export default Vue.extend({
|
||||||
dialogImage: false,
|
dialogImage: false,
|
||||||
dialogMatches: false,
|
dialogMatches: false,
|
||||||
dialogImagePageHash: {} as PageHashUnknownDto,
|
dialogImagePageHash: {} as PageHashUnknownDto,
|
||||||
dialogMatchesPageHash: {} as PageHashUnknownDto,
|
dialogMatchesPageHash: {} as PageHashDto,
|
||||||
pageHashUnknownThumbnailUrl,
|
pageHashUnknownThumbnailUrl,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -197,6 +164,7 @@ export default Vue.extend({
|
||||||
this.totalElements = itemsPage.totalElements
|
this.totalElements = itemsPage.totalElements
|
||||||
this.totalPages = itemsPage.totalPages
|
this.totalPages = itemsPage.totalPages
|
||||||
this.elements = itemsPage.content
|
this.elements = itemsPage.content
|
||||||
|
if (this.page > this.totalPages) this.page = this.totalPages
|
||||||
},
|
},
|
||||||
setSort(key: string) {
|
setSort(key: string) {
|
||||||
if (this.sortActive.key === key) {
|
if (this.sortActive.key === key) {
|
||||||
|
|
@ -213,10 +181,16 @@ export default Vue.extend({
|
||||||
this.dialogImagePageHash = pageHash
|
this.dialogImagePageHash = pageHash
|
||||||
this.dialogImage = true
|
this.dialogImage = true
|
||||||
},
|
},
|
||||||
showDialogMatches(pageHash: PageHashUnknownDto) {
|
showDialogMatches(pageHash: PageHashDto) {
|
||||||
this.dialogMatchesPageHash = pageHash
|
this.dialogMatchesPageHash = pageHash
|
||||||
this.dialogMatches = true
|
this.dialogMatches = true
|
||||||
},
|
},
|
||||||
|
pageHashCreated(pageHash: PageHashUnknownDto) {
|
||||||
|
this.hiddenElements.push(pageHash)
|
||||||
|
if (this.elements.every(x => this.hiddenElements.includes(x))) {
|
||||||
|
this.loadData(this.page, this.sortActive)
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<v-tabs>
|
<v-tabs grow>
|
||||||
<v-tab :to="{name: 'settings-analysis'}">
|
<v-tab :to="{name: 'settings-analysis'}">
|
||||||
<v-badge
|
<v-badge
|
||||||
dot
|
dot
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
CREATE TABLE PAGE_HASH
|
||||||
|
(
|
||||||
|
HASH varchar NOT NULL,
|
||||||
|
MEDIA_TYPE varchar NOT NULL,
|
||||||
|
SIZE int8 NULL,
|
||||||
|
ACTION varchar NOT NULL,
|
||||||
|
DELETE_COUNT int NOT NULL default 0,
|
||||||
|
CREATED_DATE datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
LAST_MODIFIED_DATE datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (HASH, MEDIA_TYPE, SIZE)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE PAGE_HASH_THUMBNAIL
|
||||||
|
(
|
||||||
|
HASH varchar NOT NULL,
|
||||||
|
MEDIA_TYPE varchar NOT NULL,
|
||||||
|
SIZE int8 NULL,
|
||||||
|
THUMBNAIL blob NOT NULL,
|
||||||
|
PRIMARY KEY (HASH, MEDIA_TYPE, SIZE)
|
||||||
|
);
|
||||||
|
|
||||||
|
DELETE
|
||||||
|
FROM MEDIA_PAGE
|
||||||
|
WHERE BOOK_ID IN (
|
||||||
|
SELECT DISTINCT m.BOOK_ID
|
||||||
|
FROM MEDIA m
|
||||||
|
LEFT JOIN MEDIA_PAGE MP on m.BOOK_ID = MP.BOOK_ID
|
||||||
|
WHERE mp.FILE_HASH <> ''
|
||||||
|
AND m.MEDIA_TYPE <> 'application/zip'
|
||||||
|
);
|
||||||
|
|
@ -9,9 +9,8 @@ import org.gotson.komga.domain.model.Library
|
||||||
import org.gotson.komga.domain.model.Media
|
import org.gotson.komga.domain.model.Media
|
||||||
import org.gotson.komga.domain.persistence.BookRepository
|
import org.gotson.komga.domain.persistence.BookRepository
|
||||||
import org.gotson.komga.domain.persistence.LibraryRepository
|
import org.gotson.komga.domain.persistence.LibraryRepository
|
||||||
import org.gotson.komga.domain.persistence.MediaRepository
|
|
||||||
import org.gotson.komga.domain.service.BookConverter
|
import org.gotson.komga.domain.service.BookConverter
|
||||||
import org.gotson.komga.infrastructure.configuration.KomgaProperties
|
import org.gotson.komga.domain.service.PageHashLifecycle
|
||||||
import org.gotson.komga.infrastructure.jms.QUEUE_SUB_TYPE
|
import org.gotson.komga.infrastructure.jms.QUEUE_SUB_TYPE
|
||||||
import org.gotson.komga.infrastructure.jms.QUEUE_TASKS
|
import org.gotson.komga.infrastructure.jms.QUEUE_TASKS
|
||||||
import org.gotson.komga.infrastructure.jms.QUEUE_TASKS_TYPE
|
import org.gotson.komga.infrastructure.jms.QUEUE_TASKS_TYPE
|
||||||
|
|
@ -31,9 +30,8 @@ class TaskReceiver(
|
||||||
connectionFactory: ConnectionFactory,
|
connectionFactory: ConnectionFactory,
|
||||||
private val libraryRepository: LibraryRepository,
|
private val libraryRepository: LibraryRepository,
|
||||||
private val bookRepository: BookRepository,
|
private val bookRepository: BookRepository,
|
||||||
private val mediaRepository: MediaRepository,
|
|
||||||
private val bookConverter: BookConverter,
|
private val bookConverter: BookConverter,
|
||||||
private val komgaProperties: KomgaProperties,
|
private val pageHashLifecycle: PageHashLifecycle,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val jmsTemplates = (0..9).associateWith {
|
private val jmsTemplates = (0..9).associateWith {
|
||||||
|
|
@ -76,8 +74,8 @@ class TaskReceiver(
|
||||||
|
|
||||||
fun hashBookPagesWithMissingHash(library: Library) {
|
fun hashBookPagesWithMissingHash(library: Library) {
|
||||||
if (library.hashPages)
|
if (library.hashPages)
|
||||||
mediaRepository.findAllBookIdsByLibraryIdAndWithMissingPageHash(library.id, komgaProperties.pageHashing).forEach {
|
pageHashLifecycle.getBookAndSeriesIdsWithMissingPageHash(library).forEach {
|
||||||
submitTask(Task.HashBookPages(it, LOWEST_PRIORITY, bookRepository.getSeriesIdOrNull(it) ?: ""))
|
submitTask(Task.HashBookPages(it.first, LOWEST_PRIORITY, it.second))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,9 @@
|
||||||
package org.gotson.komga.domain.model
|
package org.gotson.komga.domain.model
|
||||||
|
|
||||||
import java.time.LocalDateTime
|
open class PageHash(
|
||||||
|
|
||||||
data class PageHash(
|
|
||||||
val hash: String,
|
val hash: String,
|
||||||
val mediaType: String,
|
val mediaType: String,
|
||||||
val size: Long? = null,
|
size: Long? = null,
|
||||||
val action: Action,
|
) {
|
||||||
val deleteCount: Int = 0,
|
val size: Long? = if (size != null && size < 0) null else size
|
||||||
|
|
||||||
override val createdDate: LocalDateTime = LocalDateTime.now(),
|
|
||||||
override val lastModifiedDate: LocalDateTime = createdDate,
|
|
||||||
) : Auditable {
|
|
||||||
enum class Action {
|
|
||||||
DELETE_AUTO,
|
|
||||||
DELETE_MANUAL,
|
|
||||||
IGNORE,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package org.gotson.komga.domain.model
|
||||||
|
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
class PageHashKnown(
|
||||||
|
hash: String,
|
||||||
|
mediaType: String,
|
||||||
|
size: Long? = null,
|
||||||
|
val action: Action,
|
||||||
|
val deleteCount: Int = 0,
|
||||||
|
|
||||||
|
override val createdDate: LocalDateTime = LocalDateTime.now(),
|
||||||
|
override val lastModifiedDate: LocalDateTime = createdDate,
|
||||||
|
) : Auditable, PageHash(hash, mediaType, size) {
|
||||||
|
enum class Action {
|
||||||
|
DELETE_AUTO,
|
||||||
|
DELETE_MANUAL,
|
||||||
|
IGNORE,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
package org.gotson.komga.domain.model
|
package org.gotson.komga.domain.model
|
||||||
|
|
||||||
data class PageHashUnknown(
|
class PageHashUnknown(
|
||||||
val hash: String,
|
hash: String,
|
||||||
val mediaType: String,
|
mediaType: String,
|
||||||
val size: Long? = null,
|
size: Long? = null,
|
||||||
val matchCount: Int = 0,
|
val matchCount: Int = 0,
|
||||||
)
|
) : PageHash(hash, mediaType, size)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import org.gotson.komga.domain.model.Media
|
||||||
interface MediaRepository {
|
interface MediaRepository {
|
||||||
fun findById(bookId: String): Media
|
fun findById(bookId: String): Media
|
||||||
|
|
||||||
fun findAllBookIdsByLibraryIdAndWithMissingPageHash(libraryId: String, pageHashing: Int): Collection<String>
|
fun findAllBookAndSeriesIdsByLibraryIdAndMediaTypeAndWithMissingPageHash(libraryId: String, mediaTypes: Collection<String>, pageHashing: Int): Collection<Pair<String, String>>
|
||||||
|
|
||||||
fun getPagesSize(bookId: String): Int
|
fun getPagesSize(bookId: String): Int
|
||||||
fun getPagesSizes(bookIds: Collection<String>): Collection<Pair<String, Int>>
|
fun getPagesSizes(bookIds: Collection<String>): Collection<Pair<String, Int>>
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,21 @@
|
||||||
package org.gotson.komga.domain.persistence
|
package org.gotson.komga.domain.persistence
|
||||||
|
|
||||||
import org.gotson.komga.domain.model.PageHash
|
import org.gotson.komga.domain.model.PageHash
|
||||||
|
import org.gotson.komga.domain.model.PageHashKnown
|
||||||
import org.gotson.komga.domain.model.PageHashMatch
|
import org.gotson.komga.domain.model.PageHashMatch
|
||||||
import org.gotson.komga.domain.model.PageHashUnknown
|
import org.gotson.komga.domain.model.PageHashUnknown
|
||||||
import org.springframework.data.domain.Page
|
import org.springframework.data.domain.Page
|
||||||
import org.springframework.data.domain.Pageable
|
import org.springframework.data.domain.Pageable
|
||||||
|
|
||||||
interface PageHashRepository {
|
interface PageHashRepository {
|
||||||
fun findAllKnown(actions: List<PageHash.Action>?, pageable: Pageable): Page<PageHash>
|
fun findKnown(pageHash: PageHash): PageHashKnown?
|
||||||
|
fun findAllKnown(actions: List<PageHashKnown.Action>?, pageable: Pageable): Page<PageHashKnown>
|
||||||
fun findAllUnknown(pageable: Pageable): Page<PageHashUnknown>
|
fun findAllUnknown(pageable: Pageable): Page<PageHashUnknown>
|
||||||
|
|
||||||
fun findMatchesByHash(pageHash: PageHashUnknown, pageable: Pageable): Page<PageHashMatch>
|
fun findMatchesByHash(pageHash: PageHash, pageable: Pageable): Page<PageHashMatch>
|
||||||
|
|
||||||
fun getKnownThumbnail(hash: String): ByteArray?
|
fun getKnownThumbnail(pageHash: PageHash): ByteArray?
|
||||||
|
|
||||||
|
fun insert(pageHash: PageHashKnown, thumbnail: ByteArray?)
|
||||||
|
fun update(pageHash: PageHashKnown)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,53 @@
|
||||||
package org.gotson.komga.domain.service
|
package org.gotson.komga.domain.service
|
||||||
|
|
||||||
import org.gotson.komga.domain.model.BookPageContent
|
import org.gotson.komga.domain.model.BookPageContent
|
||||||
import org.gotson.komga.domain.model.PageHashUnknown
|
import org.gotson.komga.domain.model.Library
|
||||||
|
import org.gotson.komga.domain.model.MediaType
|
||||||
|
import org.gotson.komga.domain.model.PageHash
|
||||||
|
import org.gotson.komga.domain.model.PageHashKnown
|
||||||
import org.gotson.komga.domain.persistence.BookRepository
|
import org.gotson.komga.domain.persistence.BookRepository
|
||||||
|
import org.gotson.komga.domain.persistence.MediaRepository
|
||||||
import org.gotson.komga.domain.persistence.PageHashRepository
|
import org.gotson.komga.domain.persistence.PageHashRepository
|
||||||
|
import org.gotson.komga.infrastructure.configuration.KomgaProperties
|
||||||
import org.springframework.data.domain.Pageable
|
import org.springframework.data.domain.Pageable
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class PageHashLifecycle(
|
class PageHashLifecycle(
|
||||||
private val pageHashRepository: PageHashRepository,
|
private val pageHashRepository: PageHashRepository,
|
||||||
|
private val mediaRepository: MediaRepository,
|
||||||
private val bookLifecycle: BookLifecycle,
|
private val bookLifecycle: BookLifecycle,
|
||||||
private val bookRepository: BookRepository,
|
private val bookRepository: BookRepository,
|
||||||
|
private val komgaProperties: KomgaProperties,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun getPage(hash: PageHashUnknown, resizeTo: Int? = null): BookPageContent? {
|
private val hashableMediaTypes = listOf(MediaType.ZIP.value)
|
||||||
val match = pageHashRepository.findMatchesByHash(hash, Pageable.ofSize(1)).firstOrNull() ?: return null
|
|
||||||
|
/**
|
||||||
|
* @return a Collection of Pair of BookId/SeriesId
|
||||||
|
*/
|
||||||
|
fun getBookAndSeriesIdsWithMissingPageHash(library: Library): Collection<Pair<String, String>> =
|
||||||
|
mediaRepository.findAllBookAndSeriesIdsByLibraryIdAndMediaTypeAndWithMissingPageHash(library.id, hashableMediaTypes, komgaProperties.pageHashing)
|
||||||
|
|
||||||
|
fun getPage(pageHash: PageHash, resizeTo: Int? = null): BookPageContent? {
|
||||||
|
val match = pageHashRepository.findMatchesByHash(pageHash, Pageable.ofSize(1)).firstOrNull() ?: return null
|
||||||
val book = bookRepository.findByIdOrNull(match.bookId) ?: return null
|
val book = bookRepository.findByIdOrNull(match.bookId) ?: return null
|
||||||
|
|
||||||
return bookLifecycle.getBookPage(book, match.pageNumber, resizeTo = resizeTo)
|
return bookLifecycle.getBookPage(book, match.pageNumber, resizeTo = resizeTo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun createOrUpdate(pageHash: PageHashKnown) {
|
||||||
|
if (pageHash.action == PageHashKnown.Action.DELETE_AUTO && pageHash.size == null) throw IllegalArgumentException("cannot create PageHash without size and Action.DELETE_AUTO")
|
||||||
|
|
||||||
|
val existing = pageHashRepository.findKnown(pageHash)
|
||||||
|
if (existing == null) {
|
||||||
|
pageHashRepository.insert(pageHash, getPage(pageHash, 500)?.content)
|
||||||
|
} else {
|
||||||
|
pageHashRepository.update(pageHash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun update(pageHash: PageHashKnown) {
|
||||||
|
if (pageHash.action == PageHashKnown.Action.DELETE_AUTO && pageHash.size == null) throw IllegalArgumentException("cannot create PageHash without size and Action.DELETE_AUTO")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,23 +31,23 @@ class MediaDao(
|
||||||
override fun findById(bookId: String): Media =
|
override fun findById(bookId: String): Media =
|
||||||
find(dsl, bookId)
|
find(dsl, bookId)
|
||||||
|
|
||||||
override fun findAllBookIdsByLibraryIdAndWithMissingPageHash(libraryId: String, pageHashing: Int): Collection<String> {
|
override fun findAllBookAndSeriesIdsByLibraryIdAndMediaTypeAndWithMissingPageHash(libraryId: String, mediaTypes: Collection<String>, pageHashing: Int): Collection<Pair<String, String>> {
|
||||||
val pagesCount = DSL.count(p.BOOK_ID)
|
val pagesCount = DSL.count(p.BOOK_ID)
|
||||||
val hashedCount = DSL.sum(DSL.`when`(p.FILE_HASH.eq(""), 0).otherwise(1)).cast(Int::class.java)
|
val hashedCount = DSL.sum(DSL.`when`(p.FILE_HASH.eq(""), 0).otherwise(1)).cast(Int::class.java)
|
||||||
val neededHash = pageHashing * 2
|
val neededHash = pageHashing * 2
|
||||||
val neededHashForBook = DSL.`when`(pagesCount.lt(neededHash), pagesCount).otherwise(neededHash)
|
val neededHashForBook = DSL.`when`(pagesCount.lt(neededHash), pagesCount).otherwise(neededHash)
|
||||||
|
|
||||||
val r = dsl.select(b.ID)
|
return dsl.select(b.ID, b.SERIES_ID)
|
||||||
.from(b)
|
.from(b)
|
||||||
.leftJoin(p).on(b.ID.eq(p.BOOK_ID))
|
.leftJoin(p).on(b.ID.eq(p.BOOK_ID))
|
||||||
.leftJoin(m).on(b.ID.eq(m.BOOK_ID))
|
.leftJoin(m).on(b.ID.eq(m.BOOK_ID))
|
||||||
.where(b.LIBRARY_ID.eq(libraryId))
|
.where(b.LIBRARY_ID.eq(libraryId))
|
||||||
.and(m.STATUS.eq(Media.Status.READY.name))
|
.and(m.STATUS.eq(Media.Status.READY.name))
|
||||||
.groupBy(b.ID)
|
.and(m.MEDIA_TYPE.`in`(mediaTypes))
|
||||||
|
.groupBy(b.ID, b.SERIES_ID)
|
||||||
.having(hashedCount.lt(neededHashForBook))
|
.having(hashedCount.lt(neededHashForBook))
|
||||||
.fetch()
|
.fetch()
|
||||||
|
.map { Pair(it.value1(), it.value2()) }
|
||||||
return r.getValues(b.ID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPagesSize(bookId: String): Int =
|
override fun getPagesSize(bookId: String): Int =
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
package org.gotson.komga.infrastructure.jooq
|
package org.gotson.komga.infrastructure.jooq
|
||||||
|
|
||||||
import org.gotson.komga.domain.model.PageHash
|
import org.gotson.komga.domain.model.PageHash
|
||||||
|
import org.gotson.komga.domain.model.PageHashKnown
|
||||||
import org.gotson.komga.domain.model.PageHashMatch
|
import org.gotson.komga.domain.model.PageHashMatch
|
||||||
import org.gotson.komga.domain.model.PageHashUnknown
|
import org.gotson.komga.domain.model.PageHashUnknown
|
||||||
import org.gotson.komga.domain.persistence.PageHashRepository
|
import org.gotson.komga.domain.persistence.PageHashRepository
|
||||||
import org.gotson.komga.jooq.Tables
|
import org.gotson.komga.jooq.Tables
|
||||||
|
import org.gotson.komga.jooq.tables.records.PageHashRecord
|
||||||
import org.jooq.DSLContext
|
import org.jooq.DSLContext
|
||||||
import org.jooq.impl.DSL
|
import org.jooq.impl.DSL
|
||||||
import org.springframework.data.domain.Page
|
import org.springframework.data.domain.Page
|
||||||
|
|
@ -13,7 +15,10 @@ import org.springframework.data.domain.PageRequest
|
||||||
import org.springframework.data.domain.Pageable
|
import org.springframework.data.domain.Pageable
|
||||||
import org.springframework.data.domain.Sort
|
import org.springframework.data.domain.Sort
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
|
import org.springframework.transaction.annotation.Transactional
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.ZoneId
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
class PageHashDao(
|
class PageHashDao(
|
||||||
|
|
@ -22,8 +27,18 @@ class PageHashDao(
|
||||||
|
|
||||||
private val p = Tables.MEDIA_PAGE
|
private val p = Tables.MEDIA_PAGE
|
||||||
private val b = Tables.BOOK
|
private val b = Tables.BOOK
|
||||||
|
private val ph = Tables.PAGE_HASH
|
||||||
|
private val pht = Tables.PAGE_HASH_THUMBNAIL
|
||||||
|
|
||||||
private val sorts = mapOf(
|
private val sortsKnown = mapOf(
|
||||||
|
"hash" to ph.HASH,
|
||||||
|
"mediatype" to ph.MEDIA_TYPE,
|
||||||
|
"fileSize" to ph.SIZE,
|
||||||
|
"deleteCount" to ph.DELETE_COUNT,
|
||||||
|
"deleteSize" to ph.SIZE * ph.DELETE_COUNT,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val sortsUnknown = mapOf(
|
||||||
"hash" to p.FILE_HASH,
|
"hash" to p.FILE_HASH,
|
||||||
"mediatype" to p.MEDIA_TYPE,
|
"mediatype" to p.MEDIA_TYPE,
|
||||||
"fileSize" to p.FILE_SIZE,
|
"fileSize" to p.FILE_SIZE,
|
||||||
|
|
@ -34,8 +49,37 @@ class PageHashDao(
|
||||||
"pageNumber" to p.NUMBER,
|
"pageNumber" to p.NUMBER,
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun findAllKnown(actions: List<PageHash.Action>?, pageable: Pageable): Page<PageHash> {
|
override fun findKnown(pageHash: PageHash): PageHashKnown? =
|
||||||
TODO("Not yet implemented")
|
dsl.selectFrom(ph)
|
||||||
|
.where(ph.HASH.eq(pageHash.hash))
|
||||||
|
.and(ph.MEDIA_TYPE.eq(pageHash.mediaType))
|
||||||
|
.apply {
|
||||||
|
if (pageHash.size == null) and(ph.SIZE.isNull)
|
||||||
|
else and(ph.SIZE.eq(pageHash.size))
|
||||||
|
}
|
||||||
|
.fetchOneInto(ph)
|
||||||
|
?.toDomain()
|
||||||
|
|
||||||
|
override fun findAllKnown(actions: List<PageHashKnown.Action>?, pageable: Pageable): Page<PageHashKnown> {
|
||||||
|
val query = dsl.selectFrom(ph)
|
||||||
|
.apply { actions?.let { where(ph.ACTION.`in`(actions)) } }
|
||||||
|
|
||||||
|
val count = dsl.fetchCount(query)
|
||||||
|
|
||||||
|
val orderBy = pageable.sort.toOrderBy(sortsKnown)
|
||||||
|
val items = query
|
||||||
|
.orderBy(orderBy)
|
||||||
|
.apply { if (pageable.isPaged) limit(pageable.pageSize).offset(pageable.offset) }
|
||||||
|
.fetch()
|
||||||
|
.map { it.toDomain() }
|
||||||
|
|
||||||
|
val pageSort = if (orderBy.isNotEmpty()) pageable.sort else Sort.unsorted()
|
||||||
|
return PageImpl(
|
||||||
|
items,
|
||||||
|
if (pageable.isPaged) PageRequest.of(pageable.pageNumber, pageable.pageSize, pageSort)
|
||||||
|
else PageRequest.of(0, maxOf(count, 20), pageSort),
|
||||||
|
count.toLong(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun findAllUnknown(pageable: Pageable): Page<PageHashUnknown> {
|
override fun findAllUnknown(pageable: Pageable): Page<PageHashUnknown> {
|
||||||
|
|
@ -49,12 +93,25 @@ class PageHashDao(
|
||||||
)
|
)
|
||||||
.from(p)
|
.from(p)
|
||||||
.where(p.FILE_HASH.ne(""))
|
.where(p.FILE_HASH.ne(""))
|
||||||
|
.and(
|
||||||
|
DSL.notExists(
|
||||||
|
dsl.selectOne()
|
||||||
|
.from(ph)
|
||||||
|
.where(ph.HASH.eq(p.FILE_HASH))
|
||||||
|
.and(ph.MEDIA_TYPE.eq(p.MEDIA_TYPE))
|
||||||
|
.and(
|
||||||
|
ph.SIZE.eq(p.FILE_SIZE).or(
|
||||||
|
ph.SIZE.isNull.and(p.FILE_SIZE.isNull),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
.groupBy(p.FILE_HASH, p.MEDIA_TYPE, p.FILE_SIZE)
|
.groupBy(p.FILE_HASH, p.MEDIA_TYPE, p.FILE_SIZE)
|
||||||
.having(DSL.count(p.BOOK_ID).gt(1))
|
.having(DSL.count(p.BOOK_ID).gt(1))
|
||||||
|
|
||||||
val count = dsl.fetchCount(query)
|
val count = dsl.fetchCount(query)
|
||||||
|
|
||||||
val orderBy = pageable.sort.toOrderBy(sorts)
|
val orderBy = pageable.sort.toOrderBy(sortsUnknown)
|
||||||
val items = query
|
val items = query
|
||||||
.orderBy(orderBy)
|
.orderBy(orderBy)
|
||||||
.apply { if (pageable.isPaged) limit(pageable.pageSize).offset(pageable.offset) }
|
.apply { if (pageable.isPaged) limit(pageable.pageSize).offset(pageable.offset) }
|
||||||
|
|
@ -71,7 +128,7 @@ class PageHashDao(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun findMatchesByHash(pageHash: PageHashUnknown, pageable: Pageable): Page<PageHashMatch> {
|
override fun findMatchesByHash(pageHash: PageHash, pageable: Pageable): Page<PageHashMatch> {
|
||||||
val query = dsl.select(p.BOOK_ID, b.URL, p.NUMBER, p.FILE_NAME)
|
val query = dsl.select(p.BOOK_ID, b.URL, p.NUMBER, p.FILE_NAME)
|
||||||
.from(p)
|
.from(p)
|
||||||
.leftJoin(b).on(p.BOOK_ID.eq(b.ID))
|
.leftJoin(b).on(p.BOOK_ID.eq(b.ID))
|
||||||
|
|
@ -84,7 +141,7 @@ class PageHashDao(
|
||||||
|
|
||||||
val count = dsl.fetchCount(query)
|
val count = dsl.fetchCount(query)
|
||||||
|
|
||||||
val orderBy = pageable.sort.toOrderBy(sorts)
|
val orderBy = pageable.sort.toOrderBy(sortsUnknown)
|
||||||
val items = query
|
val items = query
|
||||||
.orderBy(orderBy)
|
.orderBy(orderBy)
|
||||||
.apply { if (pageable.isPaged) limit(pageable.pageSize).offset(pageable.offset) }
|
.apply { if (pageable.isPaged) limit(pageable.pageSize).offset(pageable.offset) }
|
||||||
|
|
@ -106,7 +163,57 @@ class PageHashDao(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getKnownThumbnail(hash: String): ByteArray? {
|
override fun getKnownThumbnail(pageHash: PageHash): ByteArray? =
|
||||||
TODO("Not yet implemented")
|
dsl.select(pht.THUMBNAIL)
|
||||||
|
.from(pht)
|
||||||
|
.where(pht.HASH.eq(pageHash.hash))
|
||||||
|
.and(pht.MEDIA_TYPE.eq(pageHash.mediaType))
|
||||||
|
.apply {
|
||||||
|
if (pageHash.size == null) and(pht.SIZE.isNull)
|
||||||
|
else and(pht.SIZE.eq(pageHash.size))
|
||||||
|
}
|
||||||
|
.fetchOne()?.value1()
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
override fun insert(pageHash: PageHashKnown, thumbnail: ByteArray?) {
|
||||||
|
dsl.insertInto(ph)
|
||||||
|
.set(ph.HASH, pageHash.hash)
|
||||||
|
.set(ph.MEDIA_TYPE, pageHash.mediaType)
|
||||||
|
.set(ph.SIZE, pageHash.size)
|
||||||
|
.set(ph.ACTION, pageHash.action.name)
|
||||||
|
.execute()
|
||||||
|
|
||||||
|
if (thumbnail != null) {
|
||||||
|
dsl.insertInto(pht)
|
||||||
|
.set(pht.HASH, pageHash.hash)
|
||||||
|
.set(pht.MEDIA_TYPE, pageHash.mediaType)
|
||||||
|
.set(pht.SIZE, pageHash.size)
|
||||||
|
.set(pht.THUMBNAIL, thumbnail)
|
||||||
|
.execute()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun update(pageHash: PageHashKnown) {
|
||||||
|
dsl.update(ph)
|
||||||
|
.set(ph.ACTION, pageHash.action.name)
|
||||||
|
.set(ph.LAST_MODIFIED_DATE, LocalDateTime.now(ZoneId.of("Z")))
|
||||||
|
.where(ph.HASH.eq(pageHash.hash))
|
||||||
|
.and(ph.MEDIA_TYPE.eq(pageHash.mediaType))
|
||||||
|
.apply {
|
||||||
|
if (pageHash.size == null) and(ph.SIZE.isNull)
|
||||||
|
else and(ph.SIZE.eq(pageHash.size))
|
||||||
|
}
|
||||||
|
.execute()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun PageHashRecord.toDomain() =
|
||||||
|
PageHashKnown(
|
||||||
|
hash = hash,
|
||||||
|
mediaType = mediaType,
|
||||||
|
size = size,
|
||||||
|
deleteCount = deleteCount,
|
||||||
|
action = PageHashKnown.Action.valueOf(action),
|
||||||
|
createdDate = createdDate.toCurrentTimeZone(),
|
||||||
|
lastModifiedDate = lastModifiedDate.toCurrentTimeZone(),
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,14 @@ import io.swagger.v3.oas.annotations.media.Content
|
||||||
import io.swagger.v3.oas.annotations.media.Schema
|
import io.swagger.v3.oas.annotations.media.Schema
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
||||||
import org.gotson.komga.domain.model.PageHash
|
import org.gotson.komga.domain.model.PageHash
|
||||||
import org.gotson.komga.domain.model.PageHashUnknown
|
import org.gotson.komga.domain.model.PageHashKnown
|
||||||
import org.gotson.komga.domain.model.ROLE_ADMIN
|
import org.gotson.komga.domain.model.ROLE_ADMIN
|
||||||
import org.gotson.komga.domain.persistence.PageHashRepository
|
import org.gotson.komga.domain.persistence.PageHashRepository
|
||||||
import org.gotson.komga.domain.service.PageHashLifecycle
|
import org.gotson.komga.domain.service.PageHashLifecycle
|
||||||
import org.gotson.komga.infrastructure.swagger.PageableAsQueryParam
|
import org.gotson.komga.infrastructure.swagger.PageableAsQueryParam
|
||||||
import org.gotson.komga.infrastructure.web.getMediaTypeOrDefault
|
import org.gotson.komga.infrastructure.web.getMediaTypeOrDefault
|
||||||
import org.gotson.komga.interfaces.api.rest.dto.PageHashDto
|
import org.gotson.komga.interfaces.api.rest.dto.PageHashCreationDto
|
||||||
|
import org.gotson.komga.interfaces.api.rest.dto.PageHashKnownDto
|
||||||
import org.gotson.komga.interfaces.api.rest.dto.PageHashMatchDto
|
import org.gotson.komga.interfaces.api.rest.dto.PageHashMatchDto
|
||||||
import org.gotson.komga.interfaces.api.rest.dto.PageHashUnknownDto
|
import org.gotson.komga.interfaces.api.rest.dto.PageHashUnknownDto
|
||||||
import org.gotson.komga.interfaces.api.rest.dto.toDto
|
import org.gotson.komga.interfaces.api.rest.dto.toDto
|
||||||
|
|
@ -24,11 +25,13 @@ import org.springframework.security.access.prepost.PreAuthorize
|
||||||
import org.springframework.web.bind.annotation.GetMapping
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
import org.springframework.web.bind.annotation.PathVariable
|
import org.springframework.web.bind.annotation.PathVariable
|
||||||
import org.springframework.web.bind.annotation.PutMapping
|
import org.springframework.web.bind.annotation.PutMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody
|
||||||
import org.springframework.web.bind.annotation.RequestMapping
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
import org.springframework.web.bind.annotation.RequestParam
|
import org.springframework.web.bind.annotation.RequestParam
|
||||||
import org.springframework.web.bind.annotation.ResponseStatus
|
import org.springframework.web.bind.annotation.ResponseStatus
|
||||||
import org.springframework.web.bind.annotation.RestController
|
import org.springframework.web.bind.annotation.RestController
|
||||||
import org.springframework.web.server.ResponseStatusException
|
import org.springframework.web.server.ResponseStatusException
|
||||||
|
import javax.validation.Valid
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("api/v1/page-hashes", produces = [MediaType.APPLICATION_JSON_VALUE])
|
@RequestMapping("api/v1/page-hashes", produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||||
|
|
@ -40,16 +43,21 @@ class PageHashController(
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@PageableAsQueryParam
|
@PageableAsQueryParam
|
||||||
fun getPageHashes(
|
fun getKnownPageHashes(
|
||||||
@RequestParam(name = "action", required = false) actions: List<PageHash.Action>?,
|
@RequestParam(name = "action", required = false) actions: List<PageHashKnown.Action>?,
|
||||||
@Parameter(hidden = true) page: Pageable,
|
@Parameter(hidden = true) page: Pageable,
|
||||||
): Page<PageHashDto> =
|
): Page<PageHashKnownDto> =
|
||||||
pageHashRepository.findAllKnown(actions, page).map { it.toDto() }
|
pageHashRepository.findAllKnown(actions, page).map { it.toDto() }
|
||||||
|
|
||||||
@GetMapping("/{hash}/thumbnail", produces = [MediaType.IMAGE_JPEG_VALUE])
|
@GetMapping("/{pageHash}/thumbnail", produces = [MediaType.IMAGE_JPEG_VALUE])
|
||||||
@ApiResponse(content = [Content(schema = Schema(type = "string", format = "binary"))])
|
@ApiResponse(content = [Content(schema = Schema(type = "string", format = "binary"))])
|
||||||
fun getPageHashThumbnail(@PathVariable hash: String): ByteArray =
|
fun getKnownPageHashThumbnail(
|
||||||
pageHashRepository.getKnownThumbnail(hash) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
@PathVariable pageHash: String,
|
||||||
|
@RequestParam("media_type") mediaType: String,
|
||||||
|
@RequestParam("file_size") size: Long,
|
||||||
|
): ByteArray =
|
||||||
|
pageHashRepository.getKnownThumbnail(PageHash(pageHash, mediaType, size))
|
||||||
|
?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||||
|
|
||||||
@GetMapping("/unknown")
|
@GetMapping("/unknown")
|
||||||
@PageableAsQueryParam
|
@PageableAsQueryParam
|
||||||
|
|
@ -67,11 +75,7 @@ class PageHashController(
|
||||||
@Parameter(hidden = true) page: Pageable,
|
@Parameter(hidden = true) page: Pageable,
|
||||||
): Page<PageHashMatchDto> =
|
): Page<PageHashMatchDto> =
|
||||||
pageHashRepository.findMatchesByHash(
|
pageHashRepository.findMatchesByHash(
|
||||||
PageHashUnknown(
|
PageHash(pageHash, mediaType, size),
|
||||||
hash = pageHash,
|
|
||||||
mediaType = mediaType,
|
|
||||||
size = if (size < 0) null else size,
|
|
||||||
),
|
|
||||||
page,
|
page,
|
||||||
).map { it.toDto() }
|
).map { it.toDto() }
|
||||||
|
|
||||||
|
|
@ -84,11 +88,7 @@ class PageHashController(
|
||||||
@RequestParam("resize") resize: Int? = null,
|
@RequestParam("resize") resize: Int? = null,
|
||||||
): ResponseEntity<ByteArray> =
|
): ResponseEntity<ByteArray> =
|
||||||
pageHashLifecycle.getPage(
|
pageHashLifecycle.getPage(
|
||||||
PageHashUnknown(
|
PageHash(pageHash, mediaType, size),
|
||||||
hash = pageHash,
|
|
||||||
mediaType = mediaType,
|
|
||||||
size = if (size < 0) null else size,
|
|
||||||
),
|
|
||||||
resize,
|
resize,
|
||||||
)?.let {
|
)?.let {
|
||||||
ResponseEntity.ok()
|
ResponseEntity.ok()
|
||||||
|
|
@ -98,7 +98,20 @@ class PageHashController(
|
||||||
|
|
||||||
@PutMapping
|
@PutMapping
|
||||||
@ResponseStatus(HttpStatus.ACCEPTED)
|
@ResponseStatus(HttpStatus.ACCEPTED)
|
||||||
fun updatePageHash() {
|
fun createKnownPageHash(
|
||||||
TODO()
|
@Valid @RequestBody pageHash: PageHashCreationDto,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
pageHashLifecycle.createOrUpdate(
|
||||||
|
PageHashKnown(
|
||||||
|
hash = pageHash.hash,
|
||||||
|
mediaType = pageHash.mediaType,
|
||||||
|
size = pageHash.size,
|
||||||
|
action = pageHash.action,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, e.message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package org.gotson.komga.interfaces.api.rest.dto
|
||||||
|
|
||||||
|
import org.gotson.komga.domain.model.PageHashKnown
|
||||||
|
import javax.validation.constraints.NotBlank
|
||||||
|
|
||||||
|
data class PageHashCreationDto(
|
||||||
|
@get:NotBlank val hash: String,
|
||||||
|
@get:NotBlank val mediaType: String,
|
||||||
|
val size: Long? = null,
|
||||||
|
val action: PageHashKnown.Action,
|
||||||
|
)
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
package org.gotson.komga.interfaces.api.rest.dto
|
package org.gotson.komga.interfaces.api.rest.dto
|
||||||
|
|
||||||
import org.gotson.komga.domain.model.PageHash
|
import org.gotson.komga.domain.model.PageHashKnown
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
data class PageHashDto(
|
data class PageHashKnownDto(
|
||||||
val hash: String,
|
val hash: String,
|
||||||
val mediaType: String,
|
val mediaType: String,
|
||||||
val size: Long?,
|
val size: Long?,
|
||||||
val action: PageHash.Action,
|
val action: PageHashKnown.Action,
|
||||||
val deleteCount: Int,
|
val deleteCount: Int,
|
||||||
|
|
||||||
val created: LocalDateTime,
|
val created: LocalDateTime,
|
||||||
val lastModified: LocalDateTime,
|
val lastModified: LocalDateTime,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun PageHash.toDto() = PageHashDto(
|
fun PageHashKnown.toDto() = PageHashKnownDto(
|
||||||
hash = hash,
|
hash = hash,
|
||||||
mediaType = mediaType,
|
mediaType = mediaType,
|
||||||
size = size,
|
size = size,
|
||||||
|
|
@ -1,20 +1,17 @@
|
||||||
package org.gotson.komga.interfaces.api.rest.dto
|
package org.gotson.komga.interfaces.api.rest.dto
|
||||||
|
|
||||||
import com.jakewharton.byteunits.BinaryByteUnit
|
|
||||||
import org.gotson.komga.domain.model.PageHashUnknown
|
import org.gotson.komga.domain.model.PageHashUnknown
|
||||||
|
|
||||||
data class PageHashUnknownDto(
|
data class PageHashUnknownDto(
|
||||||
val hash: String,
|
val hash: String,
|
||||||
val mediaType: String,
|
val mediaType: String,
|
||||||
val sizeBytes: Long?,
|
val size: Long?,
|
||||||
val size: String? = sizeBytes?.let { BinaryByteUnit.format(it) },
|
|
||||||
val totalSize: String ? = sizeBytes?.let { BinaryByteUnit.format(it * matchCount) },
|
|
||||||
val matchCount: Int,
|
val matchCount: Int,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun PageHashUnknown.toDto() = PageHashUnknownDto(
|
fun PageHashUnknown.toDto() = PageHashUnknownDto(
|
||||||
hash = hash,
|
hash = hash,
|
||||||
mediaType = mediaType,
|
mediaType = mediaType,
|
||||||
sizeBytes = size,
|
size = size,
|
||||||
matchCount = matchCount,
|
matchCount = matchCount,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
package org.gotson.komga.domain.model
|
||||||
|
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class PageHashTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given negative size when creating a PageHash then its size is null`() {
|
||||||
|
val pageHash = PageHash("abc", "image/jpeg", -5)
|
||||||
|
|
||||||
|
assertThat(pageHash.size).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given null size when creating a PageHash then its size is null`() {
|
||||||
|
val pageHash = PageHash("abc", "image/jpeg", null)
|
||||||
|
|
||||||
|
assertThat(pageHash.size).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given size when creating a PageHash then its size is not null`() {
|
||||||
|
val pageHash = PageHash("abc", "image/jpeg", 5)
|
||||||
|
|
||||||
|
assertThat(pageHash.size).isNotNull
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
package org.gotson.komga.domain.service
|
||||||
|
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.assertj.core.api.Assertions.catchThrowable
|
||||||
|
import org.gotson.komga.domain.model.PageHashKnown
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest
|
||||||
|
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||||
|
|
||||||
|
@ExtendWith(SpringExtension::class)
|
||||||
|
@SpringBootTest
|
||||||
|
class PageHashLifecycleTest(
|
||||||
|
@Autowired private val pageHashLifecycle: PageHashLifecycle,
|
||||||
|
) {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a page hash without size and action DELETE_AUTO when creating it then IllegalArgumentException is thrown`() {
|
||||||
|
// given
|
||||||
|
val pageHash = PageHashKnown(
|
||||||
|
hash = "abcdef",
|
||||||
|
mediaType = "image/jpeg",
|
||||||
|
size = null,
|
||||||
|
action = PageHashKnown.Action.DELETE_AUTO,
|
||||||
|
)
|
||||||
|
|
||||||
|
// when
|
||||||
|
val thrown = catchThrowable { pageHashLifecycle.createOrUpdate(pageHash) }
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(thrown).isExactlyInstanceOf(IllegalArgumentException::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import org.assertj.core.api.Assertions.catchThrowable
|
||||||
import org.gotson.komga.domain.model.BookPage
|
import org.gotson.komga.domain.model.BookPage
|
||||||
import org.gotson.komga.domain.model.Dimension
|
import org.gotson.komga.domain.model.Dimension
|
||||||
import org.gotson.komga.domain.model.Media
|
import org.gotson.komga.domain.model.Media
|
||||||
|
import org.gotson.komga.domain.model.MediaType
|
||||||
import org.gotson.komga.domain.model.makeBook
|
import org.gotson.komga.domain.model.makeBook
|
||||||
import org.gotson.komga.domain.model.makeLibrary
|
import org.gotson.komga.domain.model.makeLibrary
|
||||||
import org.gotson.komga.domain.model.makeSeries
|
import org.gotson.komga.domain.model.makeSeries
|
||||||
|
|
@ -213,19 +214,40 @@ class MediaDaoTest(
|
||||||
mediaType = "image/jpeg",
|
mediaType = "image/jpeg",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
mediaType = MediaType.ZIP.value,
|
||||||
bookId = book.id,
|
bookId = book.id,
|
||||||
)
|
)
|
||||||
mediaDao.insert(media)
|
mediaDao.insert(media)
|
||||||
|
|
||||||
val found = mediaDao.findAllBookIdsByLibraryIdAndWithMissingPageHash(book.libraryId, komgaProperties.pageHashing)
|
val found = mediaDao.findAllBookAndSeriesIdsByLibraryIdAndMediaTypeAndWithMissingPageHash(book.libraryId, listOf(MediaType.ZIP.value), komgaProperties.pageHashing)
|
||||||
|
|
||||||
assertThat(found)
|
assertThat(found)
|
||||||
.hasSize(1)
|
.hasSize(1)
|
||||||
.containsOnly(book.id)
|
.containsOnly(Pair(book.id, book.seriesId))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given media with no pages hashed when finding for missing page hash then it is returned`() {
|
fun `given non-convertible media not hashed when finding for missing page hash then it is returned`() {
|
||||||
|
val media = Media(
|
||||||
|
status = Media.Status.READY,
|
||||||
|
pages = listOf(
|
||||||
|
BookPage(
|
||||||
|
fileName = "1.jpg",
|
||||||
|
mediaType = "image/jpeg",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
mediaType = MediaType.RAR_4.value,
|
||||||
|
bookId = book.id,
|
||||||
|
)
|
||||||
|
mediaDao.insert(media)
|
||||||
|
|
||||||
|
val found = mediaDao.findAllBookAndSeriesIdsByLibraryIdAndMediaTypeAndWithMissingPageHash(book.libraryId, listOf(MediaType.ZIP.value), komgaProperties.pageHashing)
|
||||||
|
|
||||||
|
assertThat(found).isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given media with no pages hashed when finding for missing page hash then it is not returned`() {
|
||||||
val media = Media(
|
val media = Media(
|
||||||
status = Media.Status.READY,
|
status = Media.Status.READY,
|
||||||
pages = (1..12).map {
|
pages = (1..12).map {
|
||||||
|
|
@ -234,15 +256,16 @@ class MediaDaoTest(
|
||||||
mediaType = "image/jpeg",
|
mediaType = "image/jpeg",
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
mediaType = MediaType.ZIP.value,
|
||||||
bookId = book.id,
|
bookId = book.id,
|
||||||
)
|
)
|
||||||
mediaDao.insert(media)
|
mediaDao.insert(media)
|
||||||
|
|
||||||
val found = mediaDao.findAllBookIdsByLibraryIdAndWithMissingPageHash(book.libraryId, komgaProperties.pageHashing)
|
val found = mediaDao.findAllBookAndSeriesIdsByLibraryIdAndMediaTypeAndWithMissingPageHash(book.libraryId, listOf(MediaType.ZIP.value), komgaProperties.pageHashing)
|
||||||
|
|
||||||
assertThat(found)
|
assertThat(found)
|
||||||
.hasSize(1)
|
.hasSize(1)
|
||||||
.containsOnly(book.id)
|
.containsOnly(Pair(book.id, book.seriesId))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -256,11 +279,12 @@ class MediaDaoTest(
|
||||||
fileHash = "hashed",
|
fileHash = "hashed",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
mediaType = MediaType.ZIP.value,
|
||||||
bookId = book.id,
|
bookId = book.id,
|
||||||
)
|
)
|
||||||
mediaDao.insert(media)
|
mediaDao.insert(media)
|
||||||
|
|
||||||
val found = mediaDao.findAllBookIdsByLibraryIdAndWithMissingPageHash(book.libraryId, komgaProperties.pageHashing)
|
val found = mediaDao.findAllBookAndSeriesIdsByLibraryIdAndMediaTypeAndWithMissingPageHash(book.libraryId, listOf(MediaType.ZIP.value), komgaProperties.pageHashing)
|
||||||
|
|
||||||
assertThat(found).isEmpty()
|
assertThat(found).isEmpty()
|
||||||
}
|
}
|
||||||
|
|
@ -276,11 +300,12 @@ class MediaDaoTest(
|
||||||
fileHash = if (it <= 3 || it >= 9) "hashed" else "",
|
fileHash = if (it <= 3 || it >= 9) "hashed" else "",
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
mediaType = MediaType.ZIP.value,
|
||||||
bookId = book.id,
|
bookId = book.id,
|
||||||
)
|
)
|
||||||
mediaDao.insert(media)
|
mediaDao.insert(media)
|
||||||
|
|
||||||
val found = mediaDao.findAllBookIdsByLibraryIdAndWithMissingPageHash(book.libraryId, komgaProperties.pageHashing)
|
val found = mediaDao.findAllBookAndSeriesIdsByLibraryIdAndMediaTypeAndWithMissingPageHash(book.libraryId, listOf(MediaType.ZIP.value), komgaProperties.pageHashing)
|
||||||
|
|
||||||
assertThat(found).isEmpty()
|
assertThat(found).isEmpty()
|
||||||
}
|
}
|
||||||
|
|
@ -296,11 +321,12 @@ class MediaDaoTest(
|
||||||
fileHash = "hashed",
|
fileHash = "hashed",
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
mediaType = MediaType.ZIP.value,
|
||||||
bookId = book.id,
|
bookId = book.id,
|
||||||
)
|
)
|
||||||
mediaDao.insert(media)
|
mediaDao.insert(media)
|
||||||
|
|
||||||
val found = mediaDao.findAllBookIdsByLibraryIdAndWithMissingPageHash(book.libraryId, komgaProperties.pageHashing)
|
val found = mediaDao.findAllBookAndSeriesIdsByLibraryIdAndMediaTypeAndWithMissingPageHash(book.libraryId, listOf(MediaType.ZIP.value), komgaProperties.pageHashing)
|
||||||
|
|
||||||
assertThat(found).isEmpty()
|
assertThat(found).isEmpty()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,139 @@
|
||||||
|
package org.gotson.komga.infrastructure.jooq
|
||||||
|
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.gotson.komga.domain.model.BookPage
|
||||||
|
import org.gotson.komga.domain.model.Media
|
||||||
|
import org.gotson.komga.domain.model.PageHashKnown
|
||||||
|
import org.gotson.komga.domain.model.makeBook
|
||||||
|
import org.gotson.komga.domain.model.makeLibrary
|
||||||
|
import org.gotson.komga.domain.model.makeSeries
|
||||||
|
import org.gotson.komga.domain.persistence.BookRepository
|
||||||
|
import org.gotson.komga.domain.persistence.LibraryRepository
|
||||||
|
import org.gotson.komga.domain.persistence.SeriesRepository
|
||||||
|
import org.junit.jupiter.api.AfterAll
|
||||||
|
import org.junit.jupiter.api.AfterEach
|
||||||
|
import org.junit.jupiter.api.BeforeAll
|
||||||
|
import org.junit.jupiter.api.Nested
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest
|
||||||
|
import org.springframework.data.domain.Pageable
|
||||||
|
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
@ExtendWith(SpringExtension::class)
|
||||||
|
@SpringBootTest
|
||||||
|
class PageHashDaoTest(
|
||||||
|
@Autowired private val pageHashDao: PageHashDao,
|
||||||
|
@Autowired private val mediaDao: MediaDao,
|
||||||
|
@Autowired private val bookRepository: BookRepository,
|
||||||
|
@Autowired private val seriesRepository: SeriesRepository,
|
||||||
|
@Autowired private val libraryRepository: LibraryRepository,
|
||||||
|
) {
|
||||||
|
private val library = makeLibrary()
|
||||||
|
private val series = makeSeries("Series", libraryId = library.id)
|
||||||
|
private val books = listOf(
|
||||||
|
makeBook("Book", libraryId = library.id, seriesId = series.id),
|
||||||
|
makeBook("Book2", libraryId = library.id, seriesId = series.id),
|
||||||
|
)
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
fun setup() {
|
||||||
|
libraryRepository.insert(library)
|
||||||
|
seriesRepository.insert(series)
|
||||||
|
bookRepository.insert(books)
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
fun deleteMedia() {
|
||||||
|
bookRepository.findAll().forEach {
|
||||||
|
mediaDao.delete(it.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterAll
|
||||||
|
fun tearDown() {
|
||||||
|
bookRepository.deleteAll()
|
||||||
|
seriesRepository.deleteAll()
|
||||||
|
libraryRepository.deleteAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
inner class Known {
|
||||||
|
@Test
|
||||||
|
fun `given a known page hash when inserting then it is persisted`() {
|
||||||
|
val now = LocalDateTime.now()
|
||||||
|
val pageHash = PageHashKnown(
|
||||||
|
hash = "hashed",
|
||||||
|
mediaType = "image/jpeg",
|
||||||
|
size = 10,
|
||||||
|
action = PageHashKnown.Action.IGNORE,
|
||||||
|
)
|
||||||
|
|
||||||
|
pageHashDao.insert(pageHash, null)
|
||||||
|
val known = pageHashDao.findKnown(pageHash)!!
|
||||||
|
|
||||||
|
assertThat(known.hash).isEqualTo(pageHash.hash)
|
||||||
|
assertThat(known.mediaType).isEqualTo(pageHash.mediaType)
|
||||||
|
assertThat(known.size).isEqualTo(pageHash.size)
|
||||||
|
assertThat(known.action).isEqualTo(pageHash.action)
|
||||||
|
assertThat(known.createdDate).isCloseTo(now, offset)
|
||||||
|
assertThat(known.lastModifiedDate).isCloseTo(now, offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
inner class Unknown {
|
||||||
|
@Test
|
||||||
|
fun `given known hashes when finding unknown then known are not included`() {
|
||||||
|
// given
|
||||||
|
pageHashDao.insert(
|
||||||
|
PageHashKnown(
|
||||||
|
hash = "hash-1",
|
||||||
|
mediaType = "image/jpeg",
|
||||||
|
size = 1,
|
||||||
|
action = PageHashKnown.Action.IGNORE,
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
|
||||||
|
pageHashDao.insert(
|
||||||
|
PageHashKnown(
|
||||||
|
hash = "hash-2",
|
||||||
|
mediaType = "image/jpeg",
|
||||||
|
size = null,
|
||||||
|
action = PageHashKnown.Action.IGNORE,
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
|
||||||
|
val media = Media(
|
||||||
|
status = Media.Status.READY,
|
||||||
|
mediaType = "application/zip",
|
||||||
|
pages = (1..10).map {
|
||||||
|
BookPage(
|
||||||
|
fileName = "$it.jpg",
|
||||||
|
mediaType = "image/jpeg",
|
||||||
|
fileHash = "hash-$it",
|
||||||
|
fileSize = it.toLong(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
files = listOf("ComicInfo.xml"),
|
||||||
|
comment = "comment",
|
||||||
|
bookId = books.first().id,
|
||||||
|
)
|
||||||
|
mediaDao.insert(media)
|
||||||
|
mediaDao.insert(media.copy(bookId = books.last().id))
|
||||||
|
|
||||||
|
// when
|
||||||
|
val unknown = pageHashDao.findAllUnknown(Pageable.unpaged())
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(unknown).hasSize(9)
|
||||||
|
assertThat(unknown.map { it.hash })
|
||||||
|
.doesNotContain("hash-1")
|
||||||
|
.containsExactlyInAnyOrderElementsOf((2..10).map { "hash-$it" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue