Add export zip task

This commit is contained in:
2023-06-29 19:04:09 +08:00
parent 218412b110
commit 42cb28e38e
12 changed files with 226 additions and 21 deletions

View File

@@ -1,6 +1,5 @@
import { Component, ContextType } from "preact";
import Checkbox from "preact-material-components/Checkbox";
import { StateUpdater } from "preact/hooks";
import { BCtx } from "./BContext.tsx";
type Props = {
@@ -8,7 +7,7 @@ type Props = {
checked: boolean;
name?: string;
description?: string;
set_value?: StateUpdater<boolean>;
set_value?: (v: boolean) => void;
};
export default class BCheckbox extends Component<Props, unknown> {

View File

@@ -1,6 +1,6 @@
import { Component, ComponentChildren, ContextType } from "preact";
import TextField from "preact-material-components/TextField";
import { Ref, StateUpdater, useRef } from "preact/hooks";
import { Ref, useRef } from "preact/hooks";
import { BCtx } from "./BContext.tsx";
interface TextType {
@@ -33,6 +33,7 @@ type Props<T extends keyof TextType> = {
max?: DataType[T];
outlined?: boolean;
id?: string;
list?: string;
};
export default class BTextField<T extends keyof TextType>
@@ -134,6 +135,7 @@ export default class BTextField<T extends keyof TextType>
min={this.props.min}
max={this.props.max}
outlined={this.props.outlined}
list={this.props.list}
/>
{this.props.children}
</div>

View File

@@ -0,0 +1,44 @@
import { Component } from "preact";
import { GMeta } from "../db.ts";
import { useEffect } from "preact/hooks";
import { GalleryListResult } from "../server/gallery.ts";
type Props = {
/**@default {false}*/
jpn_title?: boolean;
id: string;
};
type State = {
data?: GMeta[];
};
export default class GidDataList extends Component<Props, State> {
render() {
const fetchData = async () => {
const re = await fetch(
"/api/gallery/list?all=1&fields=gid,title,title_jpn",
);
const d: GalleryListResult = await re.json();
if (d.ok) {
this.setState({ data: d.data });
}
};
useEffect(() => {
fetchData().catch((e) => console.error(e));
}, []);
return (
<datalist id={this.props.id}>
{this.state.data
? this.state.data.map((d) => {
let title = d.title;
if (this.props.jpn_title && d.title_jpn) {
title = d.title_jpn;
}
return <option value={d.gid}>{title}</option>;
})
: null}
</datalist>
);
}
}

View File

@@ -9,7 +9,11 @@ import { StateUpdater, useRef, useState } from "preact/hooks";
import { TaskType } from "../task.ts";
import BTextField from "./BTextField.tsx";
import { parseUrl, UrlType } from "../url.ts";
import { generate_download_cfg } from "../server/cfg.ts";
import {
cfg,
generate_download_cfg,
generate_export_zip_cfg,
} from "../server/cfg.ts";
import BCheckbox from "./BCheckbox.tsx";
import BContext from "./BContext.tsx";
import Button from "preact-material-components/Button";
@@ -17,6 +21,8 @@ import { sendTaskMessage } from "../islands/TaskManager.tsx";
import Snackbar from "preact-material-components/Snackbar";
import { GalleryResult } from "../server/gallery.ts";
import { tw } from "twind";
import { ExportZipConfig } from "../tasks/export_zip.ts";
import GidDataList from "./GidDataList.tsx";
export type NewTaskProps = {
show: boolean;
@@ -46,6 +52,8 @@ export default class NewTask extends Component<NewTaskProps> {
const [ezgid, set_ezgid1] = useState<number>();
const [ginfo, set_ginfo] = useState<GalleryResult>();
const [abort, set_abort] = useState<AbortController>();
const [ezcfg, set_ezcfg1] = useState(generate_export_zip_cfg());
const [overwrite_ezcfg, set_overwrite_ezcfg] = useState(false);
if (task_type === TaskType.Download) {
const set_url: StateUpdater<string> = (u) => {
const n = typeof u === "string" ? u : u(url || "");
@@ -70,10 +78,9 @@ export default class NewTask extends Component<NewTaskProps> {
set_url1(`https://e-hentai.org/g/${dgid}/${n}/`);
}
};
const set_overwrite_cfg: StateUpdater<boolean> = (u) => {
const n = typeof u === "boolean" ? u : u(overwrite_cfg);
const set_overwrite_cfg = (n: boolean) => {
set_overwrite_cfg1(n);
if (n === true) {
if (n) {
set_dcfg(generate_download_cfg());
}
};
@@ -176,6 +183,14 @@ export default class NewTask extends Component<NewTaskProps> {
};
}
} else if (task_type === TaskType.ExportZip) {
const export_ad = overwrite_ezcfg
? ezcfg.export_ad || false
: false;
const jpn_title = overwrite_ezcfg
? ezcfg.jpn_title || false
: cfg.value
? cfg.value.export_zip_jpn_title
: false;
const fetch_ginfo = (gid: number) => {
set_abort(new AbortController());
fetch(`/api/gallery/${gid}`).then(async (res) => {
@@ -201,15 +216,22 @@ export default class NewTask extends Component<NewTaskProps> {
};
let ginfo_div = null;
if (ginfo?.ok) {
let title = ginfo.data.meta.title;
if (jpn_title && ginfo.data.meta.title_jpn) {
title = ginfo.data.meta.title_jpn;
}
const count = export_ad
? ginfo.data.pages.length
: ginfo.data.pages.reduce((p, c) => c.is_ad ? p : p + 1, 0);
ginfo_div = (
<div>
<div>
{t("task.gallery_title")}
{ginfo.data.meta.title}
{title}
</div>
<div>
{t("task.gallery_page")}
{ginfo.data.pages.length}
{count}
</div>
</div>
);
@@ -220,17 +242,71 @@ export default class NewTask extends Component<NewTaskProps> {
</div>
);
}
const set_ezcfg: StateUpdater<ExportZipConfig> = (v) => {
set_ezcfg1(v);
this.forceUpdate();
};
const set_overwrite_cfg = (v: boolean) => {
set_overwrite_ezcfg(v);
if (v) {
set_ezcfg(Object.assign(ezcfg, generate_export_zip_cfg()));
}
};
let cfg_div = null;
if (overwrite_ezcfg) {
cfg_div = (
<div>
<BContext set_value={set_ezcfg}>
<BCheckbox
id="ez-jpn_title"
name="jpn_title"
checked={ezcfg.jpn_title || false}
description={t("task.ezcfg_jpn_title")}
/>
<BCheckbox
id="ez-export_ad"
name="export_ad"
checked={ezcfg.export_ad || false}
description={t("task.ezcfg_export_ad")}
/>
<BTextField
name="output"
value={ezcfg.output}
description={t("task.ezcfg_output")}
type="text"
outlined={true}
/>
<BTextField
name="max_length"
value={ezcfg.max_length}
description={t("task.ezcfg_max_length")}
type="number"
outlined={true}
min={0}
/>
</BContext>
</div>
);
}
config_div = (
<div class="export_zip">
<GidDataList id="gid-list" jpn_title={jpn_title} />
<BTextField
id="ezgid"
value={ezgid}
description={t("task.gallery_id")}
type="number"
outlined={true}
set_value={set_ezgid}
list="gid-list"
/>
{ginfo_div}
<BCheckbox
id="ez-cfg"
checked={overwrite_ezcfg}
description={t("task.overwrite_cfg")}
set_value={set_overwrite_cfg}
/>
{cfg_div}
</div>
);
}

14
db.ts
View File

@@ -659,6 +659,7 @@ export class EhDb {
}
convert_gmeta(m: GMetaRaw[]): GMeta[] {
return m.map((m) => {
if (m.expunged === undefined) return <GMeta> <unknown> m;
const b = m.expunged !== 0;
const t = <GMeta> <unknown> m;
t.expunged = b;
@@ -752,6 +753,19 @@ export class EhDb {
[limit, offset],
).map((n) => n[0]);
}
get_gmetas(offset = 0, limit = 20, fields = "*") {
return this.convert_gmeta(
this.db.queryEntries<GMetaRaw>(
`SELECT ${fields} FROM gmeta LIMIT ? OFFSET ?;`,
[limit, offset],
),
);
}
get_gmetas_all(fields = "*") {
return this.convert_gmeta(
this.db.queryEntries<GMetaRaw>(`SELECT ${fields} FROM gmeta;`),
);
}
get_gmeta_by_gid(gid: number) {
const s = this.convert_gmeta(
this.db.queryEntries<GMetaRaw>(

View File

@@ -14,10 +14,11 @@ import * as $8 from "./routes/api/filemeta.ts";
import * as $9 from "./routes/api/filemeta/[token].ts";
import * as $10 from "./routes/api/files/[token].ts";
import * as $11 from "./routes/api/gallery/[gid].ts";
import * as $12 from "./routes/api/status.ts";
import * as $13 from "./routes/api/task.ts";
import * as $14 from "./routes/api/thumbnail/[id].ts";
import * as $15 from "./routes/index.tsx";
import * as $12 from "./routes/api/gallery/list.ts";
import * as $13 from "./routes/api/status.ts";
import * as $14 from "./routes/api/task.ts";
import * as $15 from "./routes/api/thumbnail/[id].ts";
import * as $16 from "./routes/index.tsx";
import * as $$0 from "./islands/Container.tsx";
import * as $$1 from "./islands/Settings.tsx";
import * as $$2 from "./islands/TaskManager.tsx";
@@ -36,10 +37,11 @@ const manifest = {
"./routes/api/filemeta/[token].ts": $9,
"./routes/api/files/[token].ts": $10,
"./routes/api/gallery/[gid].ts": $11,
"./routes/api/status.ts": $12,
"./routes/api/task.ts": $13,
"./routes/api/thumbnail/[id].ts": $14,
"./routes/index.tsx": $15,
"./routes/api/gallery/list.ts": $12,
"./routes/api/status.ts": $13,
"./routes/api/task.ts": $14,
"./routes/api/thumbnail/[id].ts": $15,
"./routes/index.tsx": $16,
},
islands: {
"./islands/Container.tsx": $$0,

View File

@@ -0,0 +1,46 @@
import { Handlers } from "$fresh/server.ts";
import { get_task_manager } from "../../../server.ts";
import { parse_bool, parse_int } from "../../../server/parse_form.ts";
import { return_data, return_error } from "../../../server/utils.ts";
const ALLOW_FIELDS = [
"gid",
"token",
"title",
"title_jpn",
"category",
"uploader",
"posted",
"filecount",
"filesize",
"expunged",
"rating",
"parent_gid",
"parent_key",
"first_gid",
"first_key",
];
export const handler: Handlers = {
async GET(req, _ctx) {
const u = new URL(req.url);
const t = get_task_manager();
const all = await parse_bool(u.searchParams.get("all"), false);
const offset = await parse_int(u.searchParams.get("offset"), 0);
const limit = await parse_int(u.searchParams.get("limit"), 20);
const fields = u.searchParams.get("fields") || "*";
if (fields !== "*") {
const fs = fields.split(",");
const ok = fs.every((d) => {
const c = d.trim();
return ALLOW_FIELDS.includes(c);
});
if (!ok) return return_error(1, "Some fields not allowed.");
}
if (all) {
return return_data(t.db.get_gmetas_all(fields));
} else {
return return_data(t.db.get_gmetas(offset, limit, fields));
}
},
};

View File

@@ -3,6 +3,10 @@ import { ConfigType } from "../config.ts";
import { get_ws_host } from "./utils.ts";
import { ConfigClientSocketData, ConfigSeverSocketData } from "./config.ts";
import { DEFAULT_DOWNLOAD_CONFIG, DownloadConfig } from "../tasks/download.ts";
import {
DEFAULT_EXPORT_ZIP_CONFIG,
ExportZipConfig,
} from "../tasks/export_zip.ts";
export const cfg = signal<ConfigType | undefined>(undefined);
@@ -36,3 +40,11 @@ export function generate_download_cfg(): DownloadConfig {
remove_previous_gallery: c.remove_previous_gallery,
};
}
export function generate_export_zip_cfg(): ExportZipConfig {
if (!cfg.value) return DEFAULT_EXPORT_ZIP_CONFIG;
const c = cfg.value;
return {
jpn_title: c.export_zip_jpn_title,
};
}

View File

@@ -8,3 +8,5 @@ export type GalleryData = {
};
export type GalleryResult = JSONResult<GalleryData>;
export type GalleryListResult = JSONResult<GMeta[]>;

View File

@@ -251,7 +251,7 @@ div.new_task>div.container {
height: calc(100% - 5% - 64px);
min-width: 400px;
width: calc(100% - 20%);
margin-top: 8rem;
margin-top: 2.5%;
}
div.new_task .top {

View File

@@ -15,5 +15,9 @@
"overwrite_cfg": "Overwrite default config.",
"submit_failed": "Failed to submit task.",
"gallery_title": "Gallery title: ",
"gallery_page": "Number of pages: "
"gallery_page": "Number of pages: ",
"ezcfg_output": "The path to output file: ",
"ezcfg_jpn_title": "Use japanese title first.",
"ezcfg_export_ad": "Export pages which marked as ads.",
"ezcfg_max_length": "Maximum length of filenames in Zip files: "
}

View File

@@ -15,5 +15,9 @@
"overwrite_cfg": "覆盖默认设置。",
"submit_failed": "提交任务失败",
"gallery_title": "画廊标题:",
"gallery_page": "页数:"
"gallery_page": "页数:",
"ezcfg_output": "输出文件的位置:",
"ezcfg_jpn_title": "优先使用日语标题。",
"ezcfg_export_ad": "导出标记为广告的页面。",
"ezcfg_max_length": "Zip文件中文件名的最大长度:"
}