mirror of
https://github.com/lifegpc/eh-downloader.git
synced 2026-06-06 05:38:44 +08:00
Setting Page responsive layout
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@ config.json
|
||||
test/
|
||||
downloads/
|
||||
utt.lock
|
||||
thumbnails/
|
||||
|
||||
@@ -20,6 +20,7 @@ export type ConfigType = {
|
||||
meili_update_api_key?: string;
|
||||
ffmpeg_path: string;
|
||||
thumbnail_method: ThumbnailMethod;
|
||||
thumbnail_dir: string;
|
||||
};
|
||||
|
||||
export enum ThumbnailMethod {
|
||||
@@ -124,6 +125,9 @@ export class Config {
|
||||
if (n < 0 || n > 1) return ThumbnailMethod.FFMPEG_BINARY;
|
||||
return n as ThumbnailMethod;
|
||||
}
|
||||
get thumbnail_dir() {
|
||||
return this._return_string("thumbnail_dir") || "./thumbnails";
|
||||
}
|
||||
to_json(): ConfigType {
|
||||
return {
|
||||
cookies: typeof this.cookies === "string",
|
||||
@@ -144,6 +148,7 @@ export class Config {
|
||||
meili_update_api_key: this.meili_update_api_key,
|
||||
ffmpeg_path: this.ffmpeg_path,
|
||||
thumbnail_method: this.thumbnail_method,
|
||||
thumbnail_dir: this.thumbnail_dir,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
8
db.ts
8
db.ts
@@ -1,5 +1,9 @@
|
||||
import { DB } from "sqlite/mod.ts";
|
||||
import { compare as compare_ver, parse as parse_ver } from "std/semver/mod.ts";
|
||||
import {
|
||||
compare as compare_ver,
|
||||
format as format_ver,
|
||||
parse as parse_ver,
|
||||
} from "std/semver/mod.ts";
|
||||
import { unescape } from "std/html/mod.ts";
|
||||
import { join, resolve } from "std/path/mod.ts";
|
||||
import { SqliteError } from "sqlite/mod.ts";
|
||||
@@ -401,7 +405,7 @@ export class EhDb {
|
||||
this.db.transaction(() => {
|
||||
this.db.query("INSERT OR REPLACE INTO version VALUES (?, ?);", [
|
||||
"eh",
|
||||
this.version.toString(),
|
||||
format_ver(this.version),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
12
fresh.gen.ts
12
fresh.gen.ts
@@ -12,8 +12,10 @@ import * as $6 from "./routes/api/file/random.ts";
|
||||
import * as $7 from "./routes/api/filemeta.ts";
|
||||
import * as $8 from "./routes/api/filemeta/[token].ts";
|
||||
import * as $9 from "./routes/api/gallery/[gid].ts";
|
||||
import * as $10 from "./routes/api/task.ts";
|
||||
import * as $11 from "./routes/index.tsx";
|
||||
import * as $10 from "./routes/api/status.ts";
|
||||
import * as $11 from "./routes/api/task.ts";
|
||||
import * as $12 from "./routes/api/thumbnail/[id].ts";
|
||||
import * as $13 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";
|
||||
@@ -30,8 +32,10 @@ const manifest = {
|
||||
"./routes/api/filemeta.ts": $7,
|
||||
"./routes/api/filemeta/[token].ts": $8,
|
||||
"./routes/api/gallery/[gid].ts": $9,
|
||||
"./routes/api/task.ts": $10,
|
||||
"./routes/index.tsx": $11,
|
||||
"./routes/api/status.ts": $10,
|
||||
"./routes/api/task.ts": $11,
|
||||
"./routes/api/thumbnail/[id].ts": $12,
|
||||
"./routes/index.tsx": $13,
|
||||
},
|
||||
islands: {
|
||||
"./islands/Container.tsx": $$0,
|
||||
|
||||
@@ -98,7 +98,7 @@ export default class Settings extends Component<SettingsProps> {
|
||||
set_changed={set_changed}
|
||||
set_settings={set_settings}
|
||||
>
|
||||
<div>
|
||||
<div class="check-box">
|
||||
<SettingsCheckbox
|
||||
name="download_original_img"
|
||||
checked={settings.download_original_img}
|
||||
@@ -120,7 +120,29 @@ export default class Settings extends Component<SettingsProps> {
|
||||
description={t("settings.export_zip_jpn_title")}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-box">
|
||||
<SettingsSelect
|
||||
name="thumbnail_method"
|
||||
list={[{
|
||||
value: ThumbnailMethod.FFMPEG_BINARY,
|
||||
text: t("settings.thumbnail_method0"),
|
||||
}, {
|
||||
value: ThumbnailMethod.FFMPEG_API,
|
||||
text: t("settings.thumbnail_method1"),
|
||||
}]}
|
||||
description={t("settings.thumbnail_method")}
|
||||
selectedIndex={settings.thumbnail_method}
|
||||
outlined={true}
|
||||
/>
|
||||
<SettingsText
|
||||
name="port"
|
||||
value={settings.port}
|
||||
description={t("settings.port")}
|
||||
type="number"
|
||||
min={0}
|
||||
max={65535}
|
||||
outlined={true}
|
||||
/>
|
||||
<SettingsText
|
||||
name="base"
|
||||
value={settings.base}
|
||||
@@ -128,40 +150,29 @@ export default class Settings extends Component<SettingsProps> {
|
||||
type="text"
|
||||
outlined={true}
|
||||
/>
|
||||
<SettingsText
|
||||
name="ua"
|
||||
value={settings.ua ? settings.ua : ""}
|
||||
description={t("settings.ua")}
|
||||
type="text"
|
||||
outlined={true}
|
||||
ref={ref}
|
||||
>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (ref.current) {
|
||||
const ua = navigator.userAgent;
|
||||
const t = ref.current;
|
||||
t.update(ua);
|
||||
t.set_value(ua);
|
||||
}
|
||||
}}
|
||||
<div class="ua">
|
||||
<SettingsText
|
||||
name="ua"
|
||||
value={settings.ua ? settings.ua : ""}
|
||||
description={t("settings.ua")}
|
||||
type="text"
|
||||
outlined={true}
|
||||
ref={ref}
|
||||
>
|
||||
{t("settings.ua_now")}
|
||||
</SettingsText>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (ref.current) {
|
||||
const ua = navigator.userAgent;
|
||||
const t = ref.current;
|
||||
t.update(ua);
|
||||
t.set_value(ua);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("settings.ua_now")}
|
||||
</Button>
|
||||
</SettingsText>
|
||||
<SettingsText
|
||||
name="cookies"
|
||||
value={new_cookies}
|
||||
description={t("settings.cookies")}
|
||||
type="text"
|
||||
set_value={set_new_cookies}
|
||||
label={t(
|
||||
`settings.enter${
|
||||
settings.cookies ? "_new" : ""
|
||||
}_cookies`,
|
||||
)}
|
||||
outlined={true}
|
||||
/>
|
||||
</div>
|
||||
<SettingsText
|
||||
name="max_task_count"
|
||||
value={settings.max_task_count}
|
||||
@@ -194,15 +205,6 @@ export default class Settings extends Component<SettingsProps> {
|
||||
helpertext={t("settings.db_path_help")}
|
||||
outlined={true}
|
||||
/>
|
||||
<SettingsText
|
||||
name="port"
|
||||
value={settings.port}
|
||||
description={t("settings.port")}
|
||||
type="number"
|
||||
min={0}
|
||||
max={65535}
|
||||
outlined={true}
|
||||
/>
|
||||
<SettingsText
|
||||
name="hostname"
|
||||
value={settings.hostname}
|
||||
@@ -238,17 +240,17 @@ export default class Settings extends Component<SettingsProps> {
|
||||
type="text"
|
||||
outlined={true}
|
||||
/>
|
||||
<SettingsSelect
|
||||
name="thumbnail_method"
|
||||
list={[{
|
||||
value: ThumbnailMethod.FFMPEG_BINARY,
|
||||
text: t("settings.thumbnail_method0"),
|
||||
}, {
|
||||
value: ThumbnailMethod.FFMPEG_API,
|
||||
text: t("settings.thumbnail_method1"),
|
||||
}]}
|
||||
description={t("settings.thumbnail_method")}
|
||||
selectedIndex={settings.thumbnail_method}
|
||||
<SettingsText
|
||||
name="cookies"
|
||||
value={new_cookies}
|
||||
description={t("settings.cookies")}
|
||||
type="text"
|
||||
set_value={set_new_cookies}
|
||||
label={t(
|
||||
`settings.enter${
|
||||
settings.cookies ? "_new" : ""
|
||||
}_cookies`,
|
||||
)}
|
||||
outlined={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -55,7 +55,9 @@ export class MeiliSearchServer {
|
||||
}
|
||||
#gallery_update(e: Event) {
|
||||
const ev = e as CustomEvent<number>;
|
||||
this.updateGallery(ev.detail);
|
||||
this.updateGallery(ev.detail).catch((e) => {
|
||||
console.log(e);
|
||||
});
|
||||
}
|
||||
async #updateGMetaSettings() {
|
||||
if (this.#gmeta) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { DOMParser, Element } from "deno_dom/deno-dom-wasm-noinit.ts";
|
||||
import { Client } from "../client.ts";
|
||||
import { initDOMParser, parse_bool } from "../utils.ts";
|
||||
import { initDOMParser, map, parse_bool } from "../utils.ts";
|
||||
import { parseUrl, UrlType } from "../url.ts";
|
||||
|
||||
class GalleryPage {
|
||||
dom;
|
||||
@@ -11,6 +12,7 @@ class GalleryPage {
|
||||
#meta_script: string | undefined = undefined;
|
||||
#gid: number | undefined = undefined;
|
||||
#token: string | undefined = undefined;
|
||||
#new_version: Array<{ gid: number; token: string }> | undefined = undefined;
|
||||
constructor(html: string, client: Client) {
|
||||
const dom = (new DOMParser()).parseFromString(html, "text/html");
|
||||
if (!dom) {
|
||||
@@ -86,6 +88,24 @@ class GalleryPage {
|
||||
if (!ele) throw Error("Failed to find gallery's name.");
|
||||
return ele.innerText;
|
||||
}
|
||||
get new_version() {
|
||||
if (this.#new_version === undefined) {
|
||||
const eles = this.doc.querySelectorAll("#gnd > a");
|
||||
const d = <{ gid: number; token: string }[]> map(eles, (e) => {
|
||||
const b = e as Element;
|
||||
const u = b.getAttribute("href");
|
||||
if (!u) return null;
|
||||
const d = parseUrl(u);
|
||||
if (d?.type === UrlType.Gallery) {
|
||||
return { gid: d.gid, token: d.token };
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}).filter((d) => d !== null);
|
||||
this.#new_version = d;
|
||||
return d;
|
||||
} else return this.#new_version;
|
||||
}
|
||||
get japanese_name() {
|
||||
return this.doc.getElementById("gj")?.innerText;
|
||||
}
|
||||
|
||||
@@ -36,4 +36,18 @@ Deno.test({
|
||||
assertEquals(re.language, "Chinese");
|
||||
assertEquals(re.gid, 2552611);
|
||||
assertEquals(re.token, "3132307627");
|
||||
assertEquals(re.new_version.length, 0);
|
||||
});
|
||||
|
||||
Deno.test({
|
||||
name: "GalleryPage_test2",
|
||||
permissions: API_PERMISSION,
|
||||
}, async () => {
|
||||
const cfg = await load_settings("./config.json");
|
||||
const client = new Client(cfg);
|
||||
const re = await client.fetchGalleryPage(2209409, "8c8b2b1fc3");
|
||||
assertEquals(re.name, "[Fanbox] houk1se1 (2022.03.08 - 2022.05.01)");
|
||||
assertEquals(re.japanese_name, "");
|
||||
assertEquals(re.length, 42);
|
||||
assertEquals(re.new_version[0], { gid: 2223198, token: "2a5788135e" });
|
||||
});
|
||||
|
||||
@@ -8,8 +8,10 @@ export const handler: Handlers = {
|
||||
const u = new URL(req.url);
|
||||
const is_nsfw = await parse_bool(u.searchParams.get("is_nsfw"), null);
|
||||
const is_ad = await parse_bool(u.searchParams.get("is_ad"), null);
|
||||
const thumb = await parse_bool(u.searchParams.get("thumb"), false);
|
||||
const f = m.db.get_random_file(is_nsfw, is_ad);
|
||||
if (!f) return new Response("File not found.", { status: 404 });
|
||||
return Response.redirect(`${u.origin}/api/file/${f.id}`);
|
||||
const t = thumb ? "thumbnail" : "file";
|
||||
return Response.redirect(`${u.origin}/api/${t}/${f.id}`);
|
||||
},
|
||||
};
|
||||
|
||||
28
routes/api/status.ts
Normal file
28
routes/api/status.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Handlers } from "$fresh/server.ts";
|
||||
import { get_task_manager } from "../../server.ts";
|
||||
import { StatusData } from "../../server/status.ts";
|
||||
import { return_data } from "../../server/utils.ts";
|
||||
import { check_ffmpeg_binary } from "../../thumbnail/ffmpeg_binary.ts";
|
||||
|
||||
export const handler: Handlers = {
|
||||
async GET(_req, _ctx) {
|
||||
const m = get_task_manager();
|
||||
const ffmpeg_binary_enabled = await check_ffmpeg_binary(
|
||||
m.cfg.ffmpeg_path,
|
||||
);
|
||||
const meilisearch_enabled = m.meilisearch !== undefined;
|
||||
const meilisearch = meilisearch_enabled && m.cfg.meili_host &&
|
||||
m.cfg.meili_update_api_key
|
||||
? {
|
||||
host: m.cfg.meili_host,
|
||||
key: m.cfg.meili_search_api_key ||
|
||||
m.cfg.meili_update_api_key,
|
||||
}
|
||||
: undefined;
|
||||
return return_data<StatusData>({
|
||||
ffmpeg_binary_enabled,
|
||||
meilisearch_enabled,
|
||||
meilisearch,
|
||||
});
|
||||
},
|
||||
};
|
||||
80
routes/api/thumbnail/[id].ts
Normal file
80
routes/api/thumbnail/[id].ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { Handlers } from "$fresh/server.ts";
|
||||
import { exists } from "std/fs/exists.ts";
|
||||
import { get_task_manager } from "../../../server.ts";
|
||||
import { parse_bool, parse_int } from "../../../server/parse_form.ts";
|
||||
import { generate_filename, ThumbnailConfig } from "../../../thumbnail/base.ts";
|
||||
import { sure_dir } from "../../../utils.ts";
|
||||
import { ThumbnailMethod } from "../../../config.ts";
|
||||
import { fb_generate_thumbnail } from "../../../thumbnail/ffmpeg_binary.ts";
|
||||
import {
|
||||
get_file_response,
|
||||
GetFileResponseOptions,
|
||||
} from "../../../server/get_file_response.ts";
|
||||
|
||||
export const handler: Handlers = {
|
||||
async GET(req, ctx) {
|
||||
const id = parseInt(ctx.params.id);
|
||||
if (isNaN(id)) {
|
||||
return new Response("Bad Request", { status: 400 });
|
||||
}
|
||||
const m = get_task_manager();
|
||||
const b = m.cfg.thumbnail_dir;
|
||||
const method = m.cfg.thumbnail_method;
|
||||
await sure_dir(b);
|
||||
const f = m.db.get_file(id);
|
||||
if (!f) {
|
||||
return new Response("File not found.", { status: 404 });
|
||||
}
|
||||
const u = new URL(req.url);
|
||||
const max = await parse_int(u.searchParams.get("max"), 1200);
|
||||
const width = await parse_int(u.searchParams.get("width"), null);
|
||||
const height = await parse_int(u.searchParams.get("height"), null);
|
||||
const quality = await parse_int(u.searchParams.get("quality"), 1);
|
||||
const force = await parse_bool(u.searchParams.get("force"), false);
|
||||
const cfg: ThumbnailConfig = { width: 0, height: 0, quality };
|
||||
if (width !== null && height !== null) {
|
||||
cfg.width = width;
|
||||
cfg.height = height;
|
||||
} else if (width !== null) {
|
||||
cfg.width = width;
|
||||
cfg.height = Math.round(f.height / f.width * width);
|
||||
} else if (height !== null) {
|
||||
cfg.height = height;
|
||||
cfg.width = Math.round(f.width / f.height * height);
|
||||
} else {
|
||||
if (f.width > f.height) {
|
||||
cfg.width = max;
|
||||
cfg.height = Math.round(f.height / f.width * max);
|
||||
} else {
|
||||
cfg.height = max;
|
||||
cfg.width = Math.round(f.width / f.height * max);
|
||||
}
|
||||
}
|
||||
if (!force) {
|
||||
if (cfg.width > f.width || cfg.height > f.height) {
|
||||
return Response.redirect(`${u.origin}/api/file/${f.id}`);
|
||||
}
|
||||
}
|
||||
const output = generate_filename(b, f, cfg);
|
||||
if (!(await exists(output))) {
|
||||
if (method === ThumbnailMethod.FFMPEG_BINARY) {
|
||||
const re = await fb_generate_thumbnail(
|
||||
m.cfg.ffmpeg_path,
|
||||
f.path,
|
||||
output,
|
||||
cfg,
|
||||
);
|
||||
if (!re) {
|
||||
return new Response("Failed to generate thumbnail.", {
|
||||
status: 500,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const opts: GetFileResponseOptions = {};
|
||||
opts.range = req.headers.get("range");
|
||||
opts.if_modified_since = req.headers.get("If-Modified-Since");
|
||||
opts.if_unmodified_since = req.headers.get("If-Unmodified-Since");
|
||||
return await get_file_response(output, opts);
|
||||
},
|
||||
};
|
||||
8
server/status.ts
Normal file
8
server/status.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export type StatusData = {
|
||||
ffmpeg_binary_enabled: boolean;
|
||||
meilisearch_enabled: boolean;
|
||||
meilisearch?: {
|
||||
host: string;
|
||||
key: string;
|
||||
};
|
||||
};
|
||||
@@ -19,6 +19,14 @@
|
||||
top: 64px;
|
||||
}
|
||||
|
||||
|
||||
.settings {
|
||||
margin: 0 18%;
|
||||
padding-top: 40px;
|
||||
transition: 0.6s;
|
||||
}
|
||||
|
||||
|
||||
.settings div.text {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -60,3 +68,43 @@
|
||||
.settings div.text.outlined.label {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.settings .text-box {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
|
||||
.settings .text-box .text {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
.settings .text-box .text .mdc-text-field::after{
|
||||
width: 0;
|
||||
}
|
||||
.settings .text-box .text .mdc-text-field::before{
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.settings .ua {
|
||||
position: relative;
|
||||
}
|
||||
.settings .ua > button {
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
left: -10px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@media (max-width:767px) {
|
||||
|
||||
}
|
||||
|
||||
@media (max-width:1280px) {
|
||||
.settings {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
22
thumbnail/base.ts
Normal file
22
thumbnail/base.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { join } from "std/path/mod.ts";
|
||||
import { filterFilename } from "../utils.ts";
|
||||
import { EhFile } from "../db.ts";
|
||||
|
||||
export type ThumbnailConfig = {
|
||||
width: number;
|
||||
height: number;
|
||||
quality: number;
|
||||
};
|
||||
|
||||
export function generate_filename(
|
||||
base: string,
|
||||
f: EhFile,
|
||||
cfg: ThumbnailConfig,
|
||||
) {
|
||||
return join(
|
||||
base,
|
||||
filterFilename(
|
||||
`${f.id}-${f.token}-${cfg.width}x${cfg.height}-q${cfg.quality}.jpg`,
|
||||
),
|
||||
);
|
||||
}
|
||||
43
thumbnail/ffmpeg_binary.ts
Normal file
43
thumbnail/ffmpeg_binary.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { ThumbnailConfig } from "./base.ts";
|
||||
|
||||
export async function check_ffmpeg_binary(p: string) {
|
||||
const cmd = new Deno.Command(p, {
|
||||
stdout: "null",
|
||||
stderr: "null",
|
||||
args: ["-h"],
|
||||
});
|
||||
const c = cmd.spawn();
|
||||
const o = await c.output();
|
||||
return o.code === 0;
|
||||
}
|
||||
|
||||
export async function fb_generate_thumbnail(
|
||||
p: string,
|
||||
i: string,
|
||||
o: string,
|
||||
cfg: ThumbnailConfig,
|
||||
) {
|
||||
const args = [
|
||||
"-i",
|
||||
i,
|
||||
"-vf",
|
||||
`scale=${cfg.width}:${cfg.height}`,
|
||||
"-qmin",
|
||||
`${cfg.quality}`,
|
||||
"-qmax",
|
||||
`${cfg.quality}`,
|
||||
o,
|
||||
];
|
||||
const cmd = new Deno.Command(p, { args, stdout: "null", stderr: "piped" });
|
||||
const c = cmd.spawn();
|
||||
const s = await c.output();
|
||||
if (s.code !== 0) {
|
||||
try {
|
||||
const d = (new TextDecoder()).decode(s.stderr);
|
||||
console.log(d);
|
||||
} catch (_) {
|
||||
console.log(s.stderr);
|
||||
}
|
||||
}
|
||||
return s.code === 0;
|
||||
}
|
||||
@@ -27,5 +27,6 @@
|
||||
"ffmpeg_path": "The path to the ffmpeg binary: ",
|
||||
"thumbnail_method": "The method used to generate thumbnail: ",
|
||||
"thumbnail_method0": "ffmpeg binary",
|
||||
"thumbnail_method1": "ffmpeg API"
|
||||
"thumbnail_method1": "ffmpeg API",
|
||||
"thumbnail_dir": "The folder used to store thumbnails: "
|
||||
}
|
||||
|
||||
@@ -27,5 +27,6 @@
|
||||
"ffmpeg_path": "FFMPEG二进制的位置:",
|
||||
"thumbnail_method": "生成缩略图的方式:",
|
||||
"thumbnail_method0": "FFMPEG二进制",
|
||||
"thumbnail_method1": "FFMPEG API"
|
||||
"thumbnail_method1": "FFMPEG API",
|
||||
"thumbnail_dir": "存放缩略图的文件夹:"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user