Compare commits

..

No commits in common. "6fc747c3d9d906c03f72b9c93a9bfbe71a586d97" and "a5c4a74a691701ae5fb21aa76494f9ad92689d1e" have entirely different histories.

6 changed files with 35 additions and 150 deletions

View file

@ -1,148 +1,44 @@
import { MediaUpload } from "@/models/media"; import { MediaUpload } from "@/models/media";
type PixivTag = { function fetchPixivData(url: string): Promise<any> {
tag?: unknown; // https://www.pixiv.net/artworks/143552616
translation?: { en?: unknown } | null; const match = url.match(/\/artworks\/(\d+)/);
}; if (!match) {
throw new Error("Invalid Pixiv URL");
}
const artworkId = match[1];
type PixivIllustBody = {
illustId?: unknown;
id?: unknown;
illustTitle?: unknown;
title?: unknown;
illustComment?: unknown;
description?: unknown;
urls?: Record<string, unknown>;
tags?: { tags?: PixivTag[] } | PixivTag[] | string[];
userId?: unknown;
userName?: unknown;
createDate?: unknown;
uploadDate?: unknown;
pageCount?: unknown;
xRestrict?: unknown;
url?: unknown;
};
type PixivAjaxResponse<T> = { return fetch(`https://www.phixiv.net/api/info?id=${artworkId}&language=ko`)
error?: boolean; .then((response) => {
message?: string; if (!response.ok) {
body?: T; throw new Error(`Failed to fetch Pixiv data: ${response.status} ${response.statusText}`);
}; }
return response.json();
})
.then((data) => {
// if #R-18 in tags, throw error
if (data.tags && data.tags.some((tag: string) => tag.includes("R-18"))) {
throw new Error("Pixiv artwork is marked as R-18");
}
type PixivPage = { return data;
urls?: Record<string, unknown>; })
}; .then((data) => {
if (data.error) {
throw new Error(`Pixiv API error: ${data.message}`);
}
return data;
});
}
function extractArtworkId(url: string): string { async function checkPixivData(url: string, selected: Array<boolean>) {
const match = url.match(/(?:\/artworks\/|\/ajax\/illust\/)(\d+)/); const match = url.match(/\/artworks\/(\d+)/);
if (!match) { if (!match) {
throw new Error("Invalid Pixiv URL"); throw new Error("Invalid Pixiv URL");
} }
return match[1]; const artworkId = match[1];
}
function pixivAjaxUrl(path: string): string {
return `https://www.pixiv.net/ajax/illust/${path}`;
}
function getPreferredImageUrl(urls: Record<string, unknown> | undefined): string | null {
if (!urls) return null;
for (const key of ["original", "regular", "small", "thumb"]) {
const value = urls[key];
if (typeof value === "string" && value.length > 0) return value;
}
return null;
}
function normalizeTags(tags: PixivIllustBody["tags"]): string[] {
const tagList = Array.isArray(tags)
? tags
: tags && typeof tags === "object" && "tags" in tags
? tags.tags
: [];
if (!Array.isArray(tagList)) return [];
return tagList
.map((entry) => {
if (typeof entry === "string") return entry;
if (typeof entry.tag === "string") return entry.tag;
return null;
})
.filter((tag): tag is string => tag !== null && tag.length > 0);
}
function makePixivRequest(url: string) {
return fetch(url, {
headers: {
"Accept": "application/json",
"Referer": "https://www.pixiv.net/",
"User-Agent": "Mozilla/5.0 (compatible; akiyama.mizuki.guru/1.0)",
},
});
}
async function readPixivJson<T>(url: string): Promise<T> {
const response = await makePixivRequest(url);
if (!response.ok) {
throw new Error(`Failed to fetch Pixiv data: ${response.status} ${response.statusText}`);
}
const data = (await response.json()) as PixivAjaxResponse<T>;
if (data.error) {
throw new Error(`Pixiv API error: ${data.message || "unknown error"}`);
}
if (data.body === undefined) {
throw new Error("Pixiv API response did not include a body");
}
return data.body;
}
async function fetchPixivData(url: string): Promise<any> {
const artworkId = extractArtworkId(url);
const body = await readPixivJson<PixivIllustBody>(pixivAjaxUrl(`${artworkId}?lang=ko`));
const pageCount = typeof body.pageCount === "number" ? body.pageCount : 1;
const tags = normalizeTags(body.tags);
if (tags.some((tag) => tag.includes("R-18"))) {
throw new Error("Pixiv artwork is marked as R-18");
}
if (body.xRestrict === 1 || body.xRestrict === 2) {
throw new Error("Pixiv artwork is restricted");
}
let imageUrls: string[] = [];
if (pageCount > 1) {
const pages = await readPixivJson<PixivPage[]>(pixivAjaxUrl(`${artworkId}/pages?lang=ko`));
imageUrls = pages
.map((page) => getPreferredImageUrl(page.urls))
.filter((imageUrl): imageUrl is string => imageUrl !== null);
} else {
const imageUrl = getPreferredImageUrl(body.urls);
if (imageUrl) imageUrls = [imageUrl];
}
return {
url: `https://www.pixiv.net/artworks/${artworkId}`,
illust_id: String(body.illustId || body.id || artworkId),
title: String(body.illustTitle || body.title || "Pixiv Artwork"),
description: String(body.illustComment || body.description || ""),
author_id: String(body.userId || "unknown"),
author_name: String(body.userName || "unknown"),
create_date: typeof body.createDate === "string" ? body.createDate : body.uploadDate,
tags,
image_proxy_urls: imageUrls,
image_urls: imageUrls,
page_count: pageCount,
raw: body,
};
}
async function checkPixivData(url: string, selected: Array<boolean>) {
const artworkId = extractArtworkId(url);
const selectedIndices = selected const selectedIndices = selected
.map((isSelected, index) => (isSelected ? index : -1)) .map((isSelected, index) => (isSelected ? index : -1))
.filter((index) => index >= 0); .filter((index) => index >= 0);

View file

@ -31,16 +31,7 @@ async function uploadToS3(fileName: string, mediaUrl: string, maxRetry = 3) {
for (let attempt = 1; attempt <= maxRetry; attempt++) { for (let attempt = 1; attempt <= maxRetry; attempt++) {
try { try {
const parsedUrl = new URL(mediaUrl); const response = await fetch(mediaUrl);
const isPixivImage = parsedUrl.hostname === "i.pximg.net";
const response = await fetch(mediaUrl, {
headers: isPixivImage
? {
"Referer": "https://www.pixiv.net/",
"User-Agent": "Mozilla/5.0 (compatible; akiyama.mizuki.guru/1.0)",
}
: undefined,
});
if (!response.ok) { if (!response.ok) {
throw new Error(`Fetch failed: ${response.status}`); throw new Error(`Fetch failed: ${response.status}`);
} }

View file

@ -5,7 +5,6 @@ const ALLOWED_HOSTS = [
"video.twimg.com", "video.twimg.com",
"ton.twimg.com", "ton.twimg.com",
"abs.twimg.com", "abs.twimg.com",
"i.pximg.net",
]; ];
export default new Elysia({ prefix: "/proxy" }) export default new Elysia({ prefix: "/proxy" })
@ -26,7 +25,7 @@ export default new Elysia({ prefix: "/proxy" })
const response = await fetch(targetUrl, { const response = await fetch(targetUrl, {
headers: { headers: {
"User-Agent": "Mozilla/5.0 (compatible; akiyama.mizuki.guru/1.0)", "User-Agent": "Mozilla/5.0 (compatible; akiyama.mizuki.guru/1.0)",
"Referer": parsed.hostname === "i.pximg.net" ? "https://www.pixiv.net/" : "https://twitter.com/", "Referer": "https://twitter.com/",
}, },
}); });

View file

@ -250,7 +250,7 @@ export default function AddPage() {
const data = (await response.json()) as PixivApiResponse; const data = (await response.json()) as PixivApiResponse;
const items = (data.image_proxy_urls ?? []) const items = (data.image_proxy_urls ?? [])
.filter((imageUrl): imageUrl is string => typeof imageUrl === "string" && imageUrl.length > 0) .filter((imageUrl): imageUrl is string => typeof imageUrl === "string" && imageUrl.length > 0)
.map((imageUrl) => ({ url: proxyMediaUrl(imageUrl) })); .map((imageUrl) => ({ url: imageUrl }));
if (items.length === 0) throw new Error("이미지를 찾지 못했습니다."); if (items.length === 0) throw new Error("이미지를 찾지 못했습니다.");

View file

@ -3,7 +3,6 @@ const PROXIED_HOSTS = [
"video.twimg.com", "video.twimg.com",
"ton.twimg.com", "ton.twimg.com",
"abs.twimg.com", "abs.twimg.com",
"i.pximg.net",
]; ];
export function proxyMediaUrl(url: string | undefined | null): string { export function proxyMediaUrl(url: string | undefined | null): string {

View file

@ -9,11 +9,11 @@
"name": "elysia", "name": "elysia",
"version": "1.0.50", "version": "1.0.50",
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.1075.0", "@aws-sdk/client-s3": "^3.1053.0",
"@elysiajs/jwt": "^1.4.2", "@elysiajs/jwt": "^1.4.2",
"@elysiajs/openapi": "^1.4.15", "@elysiajs/openapi": "^1.4.15",
"elysia": "latest", "elysia": "latest",
"mongoose": "^9.7.2", "mongoose": "^9.6.2",
"ulid": "^3.0.2", "ulid": "^3.0.2",
}, },
"devDependencies": { "devDependencies": {