Skip to content

Commit

Permalink
[update] support SauceNAO & gif converter.
Browse files Browse the repository at this point in the history
  • Loading branch information
MarkPoloChina committed Feb 8, 2024
1 parent 85a3af5 commit 46f654f
Show file tree
Hide file tree
Showing 29 changed files with 833 additions and 86 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "icxor",
"version": "0.7.1",
"version": "0.8.0",
"private": true,
"packageManager": "yarn@1.22.19",
"description": "ICXOR (Illust Complexor) is a tool built by electron to manage illusts at both local and remote.",
Expand Down Expand Up @@ -41,6 +41,7 @@
"image-size": "^1.0.2",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.0",
"sagiri": "^3.4.0",
"sqlite3": "^5.1.6",
"typeorm": "^0.3.17",
"vue": "^3.2.47",
Expand Down
2 changes: 2 additions & 0 deletions src/main/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import { AppService } from '@main/app.service'
import { IllustModule } from '@main/illust/illust.module'
import { PixivApiModule } from '@main/pixiv-api/pixiv-api.module'
import { createWindow } from '@main/index'
import { KnownDuplicateModule } from './known-duplicate/known-duplicate.module'

@Module({
imports: [
IllustModule,
PixivApiModule,
KnownDuplicateModule,
TypeOrmModule.forRoot({
type: 'sqlite',
database: path.join(app.getPath('userData'), 'main.db'),
Expand Down
2 changes: 2 additions & 0 deletions src/main/illust/dto/illust_today.dto.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
export interface IllustTodayDto {
id?: number
type: string
base: string
target: string
tags?: string[]
char?: string
source?: string
}
16 changes: 16 additions & 0 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import { FS } from '@main/node-processor/FSService'
import { DS } from './node-processor/DownloadService'
import { PS } from './node-processor/PathService'
import { CS } from './node-processor/CloudService'
import { SS } from './node-processor/SagiriService'
import { GifCoverter } from './node-processor/MediaService'

process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true'

Expand Down Expand Up @@ -315,6 +317,9 @@ function afterReady() {
ipcMain.handle('fs:getStringFromFile', async (event, filename) => {
return await FS.loadStringFromFile(filename)
})
ipcMain.handle('fs:saveStringToFile', async (event, filename, content) => {
await FS.saveStringToFile(filename, content)
})

ipcMain.handle('ds:download', async (event, url, filename, dir, isPixiv) => {
await DS.downloadAndSave(url, filename, dir, isPixiv)
Expand Down Expand Up @@ -343,6 +348,17 @@ function afterReady() {
return await CS.downloadFile()
})

ipcMain.handle('ss:run', async (event, filePath) => {
return await SS.runAndProcess(filePath)
})
ipcMain.handle('ss:runJson', async (event, filePath) => {
return SS.parseJsonResult(filePath)
})

ipcMain.handle('ms:convertGif', async (event, input, output, delay) => {
await GifCoverter.zipToGif(input, output, delay)
})

autoUpdater.on('error', (err) => {
const currentWin = BrowserWindow.getAllWindows()[0]
if (!currentWin)
Expand Down
14 changes: 14 additions & 0 deletions src/main/known-duplicate/entities/pixiv-twitter.entities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'

@Entity()
@Index(['pixiv_id', 'twitter_status_id'], { unique: true })
export class PixivTwitter {
@PrimaryGeneratedColumn({ type: 'int' })
id: number

@Column({ type: 'varchar', nullable: false })
pixiv_id: string

@Column({ type: 'varchar', nullable: false })
twitter_status_id: string
}
24 changes: 24 additions & 0 deletions src/main/known-duplicate/known-duplicate.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Controller } from '@nestjs/common'
import { IpcHandle } from '@doubleshot/nest-electron'
import { Payload } from '@nestjs/microservices'
import { KnownDuplicateService } from './known-duplicate.service'

@Controller()
export class KnownDuplicateController {
constructor(private readonly knownDuplicateService: KnownDuplicateService) {}

@IpcHandle('api:GET/known-duplicate/pixiv')
getDuplicateByPixivId(@Payload() [{ pixiv_id }]: [{ pixiv_id: string }]) {
return this.knownDuplicateService.getDuplicateByPixivId(pixiv_id)
}

@IpcHandle('api:GET/known-duplicate/twitter')
getDuplicateByTwitterStatusId(@Payload() [{ twitter_status_id }]: [{ twitter_status_id: string }]) {
return this.knownDuplicateService.getDuplicateByTwitterStatusId(twitter_status_id)
}

@IpcHandle('api:POST/known-duplicate/twitter')
addDuplicate(@Payload() [{ pixiv_id, twitter_status_id }]: [{ pixiv_id: string; twitter_status_id: string }]) {
return this.knownDuplicateService.addDuplicate(pixiv_id, twitter_status_id)
}
}
16 changes: 16 additions & 0 deletions src/main/known-duplicate/known-duplicate.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Module } from '@nestjs/common'
import { TypeOrmModule } from '@nestjs/typeorm'
import { PixivTwitter } from './entities/pixiv-twitter.entities'
import { KnownDuplicateService } from './known-duplicate.service'
import { KnownDuplicateController } from './known-duplicate.controller'

@Module({
imports: [
TypeOrmModule.forFeature([
PixivTwitter,
]),
],
providers: [KnownDuplicateService, PixivTwitter],
controllers: [KnownDuplicateController],
})
export class KnownDuplicateModule {}
31 changes: 31 additions & 0 deletions src/main/known-duplicate/known-duplicate.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from 'typeorm'
import { PixivTwitter } from './entities/pixiv-twitter.entities'

@Injectable()
export class KnownDuplicateService {
constructor(
@InjectRepository(PixivTwitter)
private readonly pixivTwitterRepository: Repository<PixivTwitter>,
) {}

async getDuplicateByPixivId(pixiv_id: string) {
const result = await this.pixivTwitterRepository.findOneBy({ pixiv_id })
if (result)
return { target: result.twitter_status_id }
}

async getDuplicateByTwitterStatusId(twitter_status_id: string) {
const result = await this.pixivTwitterRepository.findOneBy({ twitter_status_id })
if (result)
return { target: result.pixiv_id }
}

async addDuplicate(pixiv_id: string, twitter_status_id: string) {
if (await this.pixivTwitterRepository.findOneBy({ twitter_status_id }))
return
const newDuplicate = this.pixivTwitterRepository.create({ pixiv_id, twitter_status_id })
await this.pixivTwitterRepository.save(newDuplicate)
}
}
4 changes: 4 additions & 0 deletions src/main/node-processor/FSService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,8 @@ export class FS {
static async loadStringFromFile(path: string) {
return await fs.readFile(path, 'utf8')
}

static async saveStringToFile(path: string, content: string) {
return await fs.outputFile(path, content)
}
}
141 changes: 141 additions & 0 deletions src/main/node-processor/SagiriService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import fs from 'node:fs'
import path from 'node:path'
import type { Readable } from 'node:stream'
import type { Buffer } from 'node:buffer'
import sagiri from 'sagiri'
import type { Options, SagiriResult } from 'sagiri'
import { app } from 'electron'
import { ConfigDB } from './DBService'

type File = string | Buffer | Readable
let client: (file: File, optionOverrides?: Options) => Promise<SagiriResult[]> = null
const sleep_func = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
const RESULT_ROOT = path.join(app.getPath('userData'), 'sagiri_result')
const GLOBAL_SIMILARITY_THRESHOLD = 75

class ProcessCore {
static pixivFilter(json_obj: SagiriResult[]) {
for (const result of json_obj) {
if (result.similarity > GLOBAL_SIMILARITY_THRESHOLD && result.site === 'Pixiv')
return new URL(result.url).searchParams.get('illust_id') ?? result.url.split('/')[result.url.split('/').length - 1]

if (result.similarity > GLOBAL_SIMILARITY_THRESHOLD && result.raw?.data?.source?.startsWith('https://i.pximg.net'))
return result.raw?.data?.source?.split('/')[result.raw?.data?.source?.split('/').length - 1]

if (result.similarity > GLOBAL_SIMILARITY_THRESHOLD && result.raw?.data?.source?.startsWith('https://www.pixiv.net/artworks'))
return result.raw?.data?.source?.split('/')[result.raw?.data?.source?.split('/').length - 1]
}
}

static twiiterAllFilter(json_obj: SagiriResult[]) {
for (const result of json_obj) {
if (result.similarity > GLOBAL_SIMILARITY_THRESHOLD && result.site === 'Twitter')
return result.url

if (result.similarity > GLOBAL_SIMILARITY_THRESHOLD && result.raw?.data?.source?.startsWith('https://twitter.com'))
return result.raw?.data?.source.split(' ')[0]
}
}

static twiiterWithoutPixivFilter(json_obj: SagiriResult[]) {
for (const result of json_obj) {
if (result.similarity > GLOBAL_SIMILARITY_THRESHOLD && result.site === 'Pixiv')
return

if (result.similarity > GLOBAL_SIMILARITY_THRESHOLD && result.raw?.data?.source?.startsWith('https://i.pximg.net'))
return

if (result.similarity > GLOBAL_SIMILARITY_THRESHOLD && result.raw?.data?.source?.startsWith('https://www.pixiv.net/artworks'))
return
}
return this.twiiterAllFilter(json_obj)
}
}

export class SS {
static async init() {
if (!fs.existsSync(RESULT_ROOT))
fs.mkdirSync(RESULT_ROOT)
const token = ConfigDB.getByKey('sauceNAOToken')
if (token)
client = sagiri(token)
}

static async runBatchAndDump(dir: string) {
if (!client)
throw new Error('SauceNAO client not initialized')
const files = fs.readdirSync(dir).filter(file => ['.png', '.jpg', '.jpeg'].includes(path.extname(file).toLowerCase()))

for (const file of files) {
const prefix = path.basename(file, path.extname(file))
if (fs.existsSync(path.join(RESULT_ROOT, `${prefix}.json`)))
continue
const fileSize = fs.statSync(path.join(dir, file)).size
if (fileSize > 20 * 1024 * 1024)
continue
const process = async () => {
try {
const result = await client(path.join(dir, file))
fs.writeFileSync(path.join(RESULT_ROOT, `${prefix}.json`), JSON.stringify(result))
await sleep_func(3000)
}
catch (err) {
if (err.statusCode !== 413) {
await sleep_func(10000)
await process()
}
}
}
await process()
}
}

static async runAndProcess(filePath: string) {
if (!client)
throw new Error('SauceNAO client not initialized')

if (!['.png', '.jpg', '.jpeg'].includes(path.extname(filePath).toLowerCase()))
return { error: 'Invalid file type' }

const prefix = path.basename(filePath, path.extname(filePath))
if (fs.existsSync(path.join(RESULT_ROOT, `${prefix}.json`)))
return { error: 'Result already exists' }

const fileSize = fs.statSync(filePath).size
if (fileSize > 20 * 1024 * 1024)
return { error: 'File size too large' }

let retry = 0

const process = async () => {
try {
const result = await client(filePath)
return {
pixiv: ProcessCore.pixivFilter(result),
twitter: ProcessCore.twiiterAllFilter(result),
}
}
catch (err) {
if (retry++ < 3) {
await sleep_func(10000)
return await process()
}
else {
return { error: `Failed to process image ${err}` }
}
}
}
return await process()
}

static parseJsonResult(filePath: string) {
const json = fs.readFileSync(filePath, 'utf-8')
const result: SagiriResult[] = JSON.parse(json)
return {
pixiv: ProcessCore.pixivFilter(result),
twitter: ProcessCore.twiiterAllFilter(result),
}
}
}

SS.init()
12 changes: 12 additions & 0 deletions src/render/components/IcxorSettings/IcxorAbout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,18 @@ function linkClick(url) {
</el-link>
</div>
</div>
<div class="cpr">
<el-link
type="info"
href=""
style="
vertical-align: baseline;
"
@click="linkClick('https://saucenao.com/')"
>
SauceNAO
</el-link>&nbsp;is a reverse image search engine.
</div>
<div class="cpr">
"pixiv"和"p"图标是日本ピクシブ株式会社持有的商标,
不代表本产品由其提供或支持
Expand Down
12 changes: 12 additions & 0 deletions src/render/components/IcxorSettings/IcxorConfig.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const configForm = reactive({
cosSecretKey: '',
cosBucket: '',
cosRegion: '',
sauceNAOToken: '',
})
const store = useStore()
onMounted(() => {
Expand Down Expand Up @@ -324,6 +325,17 @@ async function download() {
</el-form-item>
</el-form>
</div>
<div class="title-block">
SauceNAO API
</div>
<el-form :model="configForm" label-width="100px" style="width: 100%">
<el-form-item label="Token">
<el-input
v-model="configForm.sauceNAOToken"
placeholder="请输入Token"
/>
</el-form-item>
</el-form>
</el-scrollbar>
</div>
<div class="btn-block">
Expand Down
Loading

0 comments on commit 46f654f

Please sign in to comment.