feat(api): turkcealtyazi gerçek modu desteği ekle

TurkceAltyazi sağlayıcısı için gerçek HTTP istekleri ve HTML
parsing özelliği eklendi. Özellik bayrak ile açılıp kapatılabilir
ve hata durumunda mock moduna dönüş yapabilir.

Yapılan değişiklikler:
- Yeni ortam değişkenleri eklendi (ENABLE_TURKCEALTYAZI_REAL, vb.)
- axios ve cheerio bağımlılıkları eklendi
- Gerçek indirme ve arama işlemleri için turkcealtyaziReal.ts modülü eklendi
- Dokümantasyon güncellendi
- Detaylı trace logging desteği eklendi
This commit is contained in:
2026-02-16 09:29:01 +03:00
parent a13db011fb
commit 9f07ff445e
13 changed files with 5196 additions and 7 deletions

View File

@@ -12,3 +12,8 @@ MEDIA_TV_PATH=/media/tv
MEDIA_MOVIE_PATH=/media/movie
ENABLE_API_KEY=false
API_KEY=
ENABLE_TURKCEALTYAZI_REAL=false
TURKCEALTYAZI_ALLOW_MOCK_FALLBACK=true
TURKCEALTYAZI_BASE_URL=https://turkcealtyazi.org
TURKCEALTYAZI_TIMEOUT_MS=12000
TURKCEALTYAZI_MIN_DELAY_MS=300

View File

@@ -20,13 +20,29 @@ Docker tabanli altyazi otomasyon sistemi.
## Mock Provider Notu
Gercek scraping/API cagrilari bu MVP'de yoktur.
Gercek scraping/API cagrilari varsayilan olarak kapali gelir.
- `TurkceAltyaziProvider`: mock + TODO
- `OpenSubtitlesProvider`: mock + TODO
Deterministik candidate uretimi vardir (aynı input = ayni aday davranisi).
## TurkceAltyazi Gercek Modu (v2)
Gercek entegrasyon feature flag ile acilabilir:
```env
ENABLE_TURKCEALTYAZI_REAL=true
TURKCEALTYAZI_ALLOW_MOCK_FALLBACK=true
TURKCEALTYAZI_BASE_URL=https://turkcealtyazi.org
TURKCEALTYAZI_TIMEOUT_MS=12000
TURKCEALTYAZI_MIN_DELAY_MS=300
```
- `ENABLE_TURKCEALTYAZI_REAL=true`: `TurkceAltyaziProvider` gercek HTTP+HTML parse dener.
- `TURKCEALTYAZI_ALLOW_MOCK_FALLBACK=true`: real akista hata olursa mock adaptere doner.
- `false` yaparsan real hata durumunda job tarafina hata/not found olarak yansir.
## Gelistirme (Dev)
1. Ortam dosyasi:

View File

View File

@@ -29,6 +29,7 @@ services:
- ENABLE_API_KEY=false
ports:
- "3002:3002"
command: sh -c "npm install && npm run dev"
volumes:
- ./services/api:/app
- api_node_modules:/app/node_modules
@@ -57,6 +58,7 @@ services:
- ENABLE_API_KEY=false
ports:
- "3001:3001"
command: sh -c "npm install && npm run dev"
volumes:
- ./services/core:/app
- core_node_modules:/app/node_modules
@@ -79,6 +81,7 @@ services:
- VITE_PUBLIC_CORE_URL=http://localhost:3001
ports:
- "5173:5173"
command: sh -c "npm install && npm run dev"
volumes:
- ./services/ui:/app
- ui_node_modules:/app/node_modules

1989
services/api/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,8 @@
"dependencies": {
"@fastify/cors": "^11.0.0",
"adm-zip": "^0.5.16",
"axios": "^1.8.2",
"cheerio": "^1.0.0",
"dotenv": "^16.4.7",
"fastify": "^5.2.1",
"fs-extra": "^11.3.0",

View File

@@ -7,5 +7,10 @@ export const env = {
port: Number(process.env.API_PORT ?? 3002),
tempRoot: process.env.TEMP_ROOT ?? '/temp',
enableApiKey: process.env.ENABLE_API_KEY === 'true',
apiKey: process.env.API_KEY ?? ''
apiKey: process.env.API_KEY ?? '',
enableTurkcealtyaziReal: process.env.ENABLE_TURKCEALTYAZI_REAL === 'true',
turkcealtyaziAllowMockFallback: process.env.TURKCEALTYAZI_ALLOW_MOCK_FALLBACK !== 'false',
turkcealtyaziBaseUrl: process.env.TURKCEALTYAZI_BASE_URL ?? 'https://turkcealtyazi.org',
turkcealtyaziTimeoutMs: Number(process.env.TURKCEALTYAZI_TIMEOUT_MS ?? 12000),
turkcealtyaziMinDelayMs: Number(process.env.TURKCEALTYAZI_MIN_DELAY_MS ?? 300)
};

View File

@@ -14,7 +14,10 @@ import { chooseBest, scoreCandidateFile } from './scoring.js';
const execFileAsync = promisify(execFile);
const providers: SubtitleProvider[] = [new TurkceAltyaziProvider(), new OpenSubtitlesProvider()];
const providerEntries: Array<{ name: Candidate['provider']; impl: SubtitleProvider }> = [
{ name: 'turkcealtyazi', impl: new TurkceAltyaziProvider() },
{ name: 'opensubtitles', impl: new OpenSubtitlesProvider() }
];
function defaultLimits() {
return { maxFiles: 300, maxTotalBytes: 250 * 1024 * 1024, maxSingleBytes: 10 * 1024 * 1024 };
@@ -44,18 +47,35 @@ export async function searchSubtitles(input: SearchParams) {
const dirs = await ensureJobDirs(jobToken);
const allCandidates: Candidate[] = [];
for (const p of providers) {
const c = await p.search(input);
for (const p of providerEntries) {
if (p.name === 'turkcealtyazi') {
trace.push({ level: 'info', step: 'TA_SEARCH_REQUEST', message: 'TurkceAltyazi provider search started' });
}
trace.push({ level: 'info', step: 'SUBTITLE_SEARCH_STARTED', message: `Provider search started: ${p.name}` });
const c = await p.impl.search(input);
trace.push({ level: 'info', step: 'SUBTITLE_SEARCH_DONE', message: `Provider search done: ${p.name}`, meta: { count: c.length } });
if (p.name === 'turkcealtyazi') {
const realCount = c.filter((item) => item.scoreHints.includes('real_provider')).length;
trace.push({
level: 'info',
step: 'TA_SEARCH_PARSED',
message: `TurkceAltyazi candidates parsed`,
meta: { total: c.length, real: realCount, mock: c.length - realCount }
});
}
allCandidates.push(...c);
}
const scored: any[] = [];
for (const candidate of allCandidates) {
const provider = providers.find((p: any) => p.constructor.name.toLowerCase().includes(candidate.provider === 'turkcealtyazi' ? 'turkce' : 'open'));
const provider = providerEntries.find((p) => p.name === candidate.provider)?.impl;
if (!provider) continue;
const dl = await provider.download(candidate, input, jobToken);
if (Array.isArray(dl.trace)) {
trace.push(...dl.trace);
}
trace.push({ level: 'info', step: 'ARCHIVE_DOWNLOADED', message: `${candidate.provider}:${candidate.id}`, meta: { path: dl.filePath, type: dl.type } });
let files: string[] = [];

View File

@@ -0,0 +1,165 @@
import axios from 'axios';
import * as cheerio from 'cheerio';
import { URL } from 'node:url';
import { env } from '../config/env.js';
import type { SearchParams } from '../types/index.js';
export interface RealTaCandidate {
id: string;
title: string;
detailUrl: string;
lang: string;
releaseHints: string[];
isHI: boolean;
isForced: boolean;
}
const client = axios.create({
timeout: env.turkcealtyaziTimeoutMs,
maxRedirects: 5,
headers: {
'user-agent':
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36',
accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'accept-language': 'tr-TR,tr;q=0.9,en;q=0.8'
}
});
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function getWithRetry(url: string, retries = 2): Promise<string> {
let lastError: unknown;
for (let i = 0; i <= retries; i++) {
try {
if (i > 0) await sleep(250 * i);
const res = await client.get(url);
return typeof res.data === 'string' ? res.data : String(res.data);
} catch (err) {
lastError = err;
}
}
throw lastError;
}
function normalizeReleaseHints(raw: string): string[] {
return raw
.toLowerCase()
.split(/[^a-z0-9]+/)
.filter((t) => t.length > 1)
.filter((t, i, arr) => arr.indexOf(t) === i)
.slice(0, 10);
}
function abs(base: string, maybeRelative: string): string {
return new URL(maybeRelative, base).toString();
}
function parseCandidateNodes(html: string, baseUrl: string): RealTaCandidate[] {
const $ = cheerio.load(html);
const results: RealTaCandidate[] = [];
$('a[href]').each((_, el) => {
const href = ($(el).attr('href') || '').trim();
const text = $(el).text().replace(/\s+/g, ' ').trim();
if (!href || text.length < 3) return;
const looksLikeSubtitle = /(altyazi|subtitle|sub|s\d{1,2}e\d{1,2}|\b\d{4}\b)/i.test(text + ' ' + href);
if (!looksLikeSubtitle) return;
const full = abs(baseUrl, href);
if (!/turkcealtyazi\.org/i.test(full)) return;
const id = `ta-real-${Buffer.from(full).toString('base64').slice(0, 18)}`;
const lowered = (text + ' ' + href).toLowerCase();
results.push({
id,
title: text,
detailUrl: full,
lang: /\btr\b|turkce|türkçe/i.test(lowered) ? 'tr' : 'tr',
releaseHints: normalizeReleaseHints(text),
isHI: /\bhi\b|isitme|hearing/i.test(lowered),
isForced: /forced|zorunlu/i.test(lowered)
});
});
const uniq = new Map<string, RealTaCandidate>();
for (const r of results) {
if (!uniq.has(r.detailUrl)) uniq.set(r.detailUrl, r);
}
return [...uniq.values()].slice(0, 12);
}
export async function searchTurkceAltyaziReal(params: SearchParams): Promise<RealTaCandidate[]> {
const q = [params.title, params.year, params.type === 'tv' ? `S${String(params.season ?? 1).padStart(2, '0')}E${String(params.episode ?? 1).padStart(2, '0')}` : '']
.filter(Boolean)
.join(' ');
const candidatesPages = [
`${env.turkcealtyaziBaseUrl}/arama?q=${encodeURIComponent(q)}`,
`${env.turkcealtyaziBaseUrl}/find.php?cat=sub&find=${encodeURIComponent(q)}`
];
const merged: RealTaCandidate[] = [];
for (const url of candidatesPages) {
try {
await sleep(env.turkcealtyaziMinDelayMs);
const html = await getWithRetry(url, 2);
merged.push(...parseCandidateNodes(html, env.turkcealtyaziBaseUrl));
if (merged.length >= 8) break;
} catch {
// bir sonraki endpoint denenecek
}
}
const uniq = new Map<string, RealTaCandidate>();
for (const item of merged) {
if (!uniq.has(item.detailUrl)) uniq.set(item.detailUrl, item);
}
return [...uniq.values()].slice(0, 10);
}
export async function resolveTurkceAltyaziDownloadUrl(detailUrl: string): Promise<string> {
await sleep(env.turkcealtyaziMinDelayMs);
const html = await getWithRetry(detailUrl, 2);
const $ = cheerio.load(html);
const linkCandidates: string[] = [];
$('a[href]').each((_, el) => {
const href = ($(el).attr('href') || '').trim();
const text = $(el).text().trim();
if (!href) return;
const looksDownload = /(indir|download|\.zip|\.rar|\.7z|\.srt|\.ass)/i.test(`${href} ${text}`);
if (!looksDownload) return;
linkCandidates.push(abs(detailUrl, href));
});
const preferred =
linkCandidates.find((l) => /\.(zip|rar|7z)(\?|$)/i.test(l)) ||
linkCandidates.find((l) => /\.(srt|ass)(\?|$)/i.test(l)) ||
linkCandidates[0];
if (!preferred) {
throw new Error('TA detail page download link parse failed');
}
return preferred;
}
export async function downloadTurkceAltyaziFile(url: string): Promise<{ buffer: Buffer; finalUrl: string; contentType?: string }> {
await sleep(env.turkcealtyaziMinDelayMs);
const res = await client.get<ArrayBuffer>(url, { responseType: 'arraybuffer' });
const buffer = Buffer.from(res.data);
return {
buffer,
finalUrl: (res.request as any)?.res?.responseUrl || url,
contentType: res.headers['content-type']
};
}

View File

@@ -1,11 +1,51 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import type { Candidate, SearchParams, SubtitleProvider } from '../types/index.js';
import { generateMockArtifact } from '../lib/mockArtifact.js';
import { hashString, seeded } from '../lib/deterministic.js';
import { env } from '../config/env.js';
import {
downloadTurkceAltyaziFile,
resolveTurkceAltyaziDownloadUrl,
searchTurkceAltyaziReal
} from '../lib/turkcealtyaziReal.js';
function extensionFromDownload(url: string, contentType?: string): 'zip' | 'rar' | '7z' | 'srt' | 'ass' {
const lowerUrl = url.toLowerCase();
if (lowerUrl.includes('.zip')) return 'zip';
if (lowerUrl.includes('.rar')) return 'rar';
if (lowerUrl.includes('.7z')) return '7z';
if (lowerUrl.includes('.ass')) return 'ass';
if (contentType?.includes('zip')) return 'zip';
return 'srt';
}
export class TurkceAltyaziProvider implements SubtitleProvider {
async search(params: SearchParams): Promise<Candidate[]> {
// TODO(v2): real TurkceAltyazi scraping implementation.
if (env.enableTurkcealtyaziReal) {
try {
const real = await searchTurkceAltyaziReal(params);
if (real.length > 0) {
return real.map((item, index) => ({
id: item.id || `ta-real-${index}`,
provider: 'turkcealtyazi',
displayName: item.title,
downloadType: 'archiveZip',
downloadUrl: item.detailUrl,
lang: item.lang || 'tr',
releaseHints: item.releaseHints,
scoreHints: ['real_provider'],
isHI: item.isHI,
isForced: item.isForced
}));
}
} catch (err) {
if (!env.turkcealtyaziAllowMockFallback) {
throw err;
}
}
}
const key = `${params.title}|${params.year}|${params.season}|${params.episode}|ta`;
const rnd = seeded(hashString(key));
const base = params.title.replace(/\s+/g, '.');
@@ -38,6 +78,27 @@ export class TurkceAltyaziProvider implements SubtitleProvider {
}
async download(candidate: Candidate, params: SearchParams, jobToken: string) {
if (env.enableTurkcealtyaziReal && /^https?:\/\//i.test(candidate.downloadUrl)) {
const downloadDir = `${env.tempRoot}/${jobToken}/download`;
await fs.mkdir(downloadDir, { recursive: true });
const trace: Array<{ level: 'info' | 'warn' | 'error'; step: string; message: string; meta?: any }> = [];
trace.push({ level: 'info', step: 'TA_DETAIL_FETCHED', message: candidate.downloadUrl });
const resolved = await resolveTurkceAltyaziDownloadUrl(candidate.downloadUrl);
trace.push({ level: 'info', step: 'TA_DOWNLOAD_URL_RESOLVED', message: resolved });
const downloaded = await downloadTurkceAltyaziFile(resolved);
const ext = extensionFromDownload(downloaded.finalUrl, downloaded.contentType);
const filePath = path.join(downloadDir, `${candidate.id}.${ext}`);
await fs.writeFile(filePath, downloaded.buffer);
return {
type: ext === 'srt' || ext === 'ass' ? 'direct' : 'archive',
filePath,
candidateId: candidate.id,
trace
};
}
const artifact = await generateMockArtifact(candidate, params, jobToken, `${env.tempRoot}/${jobToken}/download`);
return { type: artifact.type, filePath: artifact.filePath, candidateId: candidate.id };
}

View File

@@ -34,6 +34,7 @@ export interface DownloadedArtifact {
type: 'archive' | 'direct';
filePath: string;
candidateId: string;
trace?: TraceLog[];
}
export interface SubtitleProvider {

1995
services/core/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

927
services/ui/package-lock.json generated Normal file
View File

@@ -0,0 +1,927 @@
{
"name": "subwatcher-ui",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "subwatcher-ui",
"version": "1.0.0",
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@vitejs/plugin-react": "^4.3.4",
"typescript": "^5.7.3",
"vite": "^6.0.11"
}
},
"node_modules/@babel/code-frame": {
"version": "7.29.0",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.28.5",
"js-tokens": "^4.0.0",
"picocolors": "^1.1.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/compat-data": {
"version": "7.29.0",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/core": {
"version": "7.29.0",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
"@babel/helper-compilation-targets": "^7.28.6",
"@babel/helper-module-transforms": "^7.28.6",
"@babel/helpers": "^7.28.6",
"@babel/parser": "^7.29.0",
"@babel/template": "^7.28.6",
"@babel/traverse": "^7.29.0",
"@babel/types": "^7.29.0",
"@jridgewell/remapping": "^2.3.5",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
"json5": "^2.2.3",
"semver": "^6.3.1"
},
"engines": {
"node": ">=6.9.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/babel"
}
},
"node_modules/@babel/generator": {
"version": "7.29.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.29.0",
"@babel/types": "^7.29.0",
"@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-compilation-targets": {
"version": "7.28.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.28.6",
"@babel/helper-validator-option": "^7.27.1",
"browserslist": "^4.24.0",
"lru-cache": "^5.1.1",
"semver": "^6.3.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-globals": {
"version": "7.28.0",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-imports": {
"version": "7.28.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/traverse": "^7.28.6",
"@babel/types": "^7.28.6"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-transforms": {
"version": "7.28.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.28.6",
"@babel/helper-validator-identifier": "^7.28.5",
"@babel/traverse": "^7.28.6"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0"
}
},
"node_modules/@babel/helper-plugin-utils": {
"version": "7.28.6",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.27.1",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.28.5",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-option": {
"version": "7.27.1",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helpers": {
"version": "7.28.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.28.6",
"@babel/types": "^7.28.6"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.29.0",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.29.0"
},
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@babel/plugin-transform-react-jsx-self": {
"version": "7.27.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-react-jsx-source": {
"version": "7.27.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/template": {
"version": "7.28.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.28.6",
"@babel/parser": "^7.28.6",
"@babel/types": "^7.28.6"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
"version": "7.29.0",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
"@babel/helper-globals": "^7.28.0",
"@babel/parser": "^7.29.0",
"@babel/template": "^7.28.6",
"@babel/types": "^7.29.0",
"debug": "^4.3.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/types": {
"version": "7.29.0",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.28.5"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.12",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/remapping": {
"version": "2.3.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.27",
"dev": true,
"license": "MIT"
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.57.1",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.20.7",
"@babel/types": "^7.20.7",
"@types/babel__generator": "*",
"@types/babel__template": "*",
"@types/babel__traverse": "*"
}
},
"node_modules/@types/babel__generator": {
"version": "7.27.0",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.0.0"
}
},
"node_modules/@types/babel__template": {
"version": "7.4.4",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.1.0",
"@babel/types": "^7.0.0"
}
},
"node_modules/@types/babel__traverse": {
"version": "7.28.0",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.28.2"
}
},
"node_modules/@types/estree": {
"version": "1.0.8",
"dev": true,
"license": "MIT"
},
"node_modules/@types/prop-types": {
"version": "15.7.15",
"dev": true,
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.3.28",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.2.2"
}
},
"node_modules/@types/react-dom": {
"version": "18.3.7",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^18.0.0"
}
},
"node_modules/@vitejs/plugin-react": {
"version": "4.7.0",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.28.0",
"@babel/plugin-transform-react-jsx-self": "^7.27.1",
"@babel/plugin-transform-react-jsx-source": "^7.27.1",
"@rolldown/pluginutils": "1.0.0-beta.27",
"@types/babel__core": "^7.20.5",
"react-refresh": "^0.17.0"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"peerDependencies": {
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
}
},
"node_modules/baseline-browser-mapping": {
"version": "2.9.19",
"dev": true,
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.js"
}
},
"node_modules/browserslist": {
"version": "4.28.1",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/browserslist"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
"electron-to-chromium": "^1.5.263",
"node-releases": "^2.0.27",
"update-browserslist-db": "^1.2.0"
},
"bin": {
"browserslist": "cli.js"
},
"engines": {
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001770",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "CC-BY-4.0"
},
"node_modules/convert-source-map": {
"version": "2.0.0",
"dev": true,
"license": "MIT"
},
"node_modules/csstype": {
"version": "3.2.3",
"dev": true,
"license": "MIT"
},
"node_modules/debug": {
"version": "4.4.3",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.286",
"dev": true,
"license": "ISC"
},
"node_modules/esbuild": {
"version": "0.25.12",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.12",
"@esbuild/android-arm": "0.25.12",
"@esbuild/android-arm64": "0.25.12",
"@esbuild/android-x64": "0.25.12",
"@esbuild/darwin-arm64": "0.25.12",
"@esbuild/darwin-x64": "0.25.12",
"@esbuild/freebsd-arm64": "0.25.12",
"@esbuild/freebsd-x64": "0.25.12",
"@esbuild/linux-arm": "0.25.12",
"@esbuild/linux-arm64": "0.25.12",
"@esbuild/linux-ia32": "0.25.12",
"@esbuild/linux-loong64": "0.25.12",
"@esbuild/linux-mips64el": "0.25.12",
"@esbuild/linux-ppc64": "0.25.12",
"@esbuild/linux-riscv64": "0.25.12",
"@esbuild/linux-s390x": "0.25.12",
"@esbuild/linux-x64": "0.25.12",
"@esbuild/netbsd-arm64": "0.25.12",
"@esbuild/netbsd-x64": "0.25.12",
"@esbuild/openbsd-arm64": "0.25.12",
"@esbuild/openbsd-x64": "0.25.12",
"@esbuild/openharmony-arm64": "0.25.12",
"@esbuild/sunos-x64": "0.25.12",
"@esbuild/win32-arm64": "0.25.12",
"@esbuild/win32-ia32": "0.25.12",
"@esbuild/win32-x64": "0.25.12"
}
},
"node_modules/escalade": {
"version": "3.2.0",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/fdir": {
"version": "6.5.0",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"license": "MIT"
},
"node_modules/jsesc": {
"version": "3.1.0",
"dev": true,
"license": "MIT",
"bin": {
"jsesc": "bin/jsesc"
},
"engines": {
"node": ">=6"
}
},
"node_modules/json5": {
"version": "2.2.3",
"dev": true,
"license": "MIT",
"bin": {
"json5": "lib/cli.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/loose-envify": {
"version": "1.4.0",
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/lru-cache": {
"version": "5.1.1",
"dev": true,
"license": "ISC",
"dependencies": {
"yallist": "^3.0.2"
}
},
"node_modules/ms": {
"version": "2.1.3",
"dev": true,
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.11",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/node-releases": {
"version": "2.0.27",
"dev": true,
"license": "MIT"
},
"node_modules/picocolors": {
"version": "1.1.1",
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
"version": "4.0.3",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/postcss": {
"version": "8.5.6",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/react": {
"version": "18.3.1",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "18.3.1",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
},
"peerDependencies": {
"react": "^18.3.1"
}
},
"node_modules/react-refresh": {
"version": "0.17.0",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/rollup": {
"version": "4.57.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.8"
},
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.57.1",
"@rollup/rollup-android-arm64": "4.57.1",
"@rollup/rollup-darwin-arm64": "4.57.1",
"@rollup/rollup-darwin-x64": "4.57.1",
"@rollup/rollup-freebsd-arm64": "4.57.1",
"@rollup/rollup-freebsd-x64": "4.57.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.57.1",
"@rollup/rollup-linux-arm-musleabihf": "4.57.1",
"@rollup/rollup-linux-arm64-gnu": "4.57.1",
"@rollup/rollup-linux-arm64-musl": "4.57.1",
"@rollup/rollup-linux-loong64-gnu": "4.57.1",
"@rollup/rollup-linux-loong64-musl": "4.57.1",
"@rollup/rollup-linux-ppc64-gnu": "4.57.1",
"@rollup/rollup-linux-ppc64-musl": "4.57.1",
"@rollup/rollup-linux-riscv64-gnu": "4.57.1",
"@rollup/rollup-linux-riscv64-musl": "4.57.1",
"@rollup/rollup-linux-s390x-gnu": "4.57.1",
"@rollup/rollup-linux-x64-gnu": "4.57.1",
"@rollup/rollup-linux-x64-musl": "4.57.1",
"@rollup/rollup-openbsd-x64": "4.57.1",
"@rollup/rollup-openharmony-arm64": "4.57.1",
"@rollup/rollup-win32-arm64-msvc": "4.57.1",
"@rollup/rollup-win32-ia32-msvc": "4.57.1",
"@rollup/rollup-win32-x64-gnu": "4.57.1",
"@rollup/rollup-win32-x64-msvc": "4.57.1",
"fsevents": "~2.3.2"
}
},
"node_modules/scheduler": {
"version": "0.23.2",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
}
},
"node_modules/semver": {
"version": "6.3.1",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/tinyglobby": {
"version": "0.2.15",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.3"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/update-browserslist-db": {
"version": "1.2.3",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/browserslist"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"escalade": "^3.2.0",
"picocolors": "^1.1.1"
},
"bin": {
"update-browserslist-db": "cli.js"
},
"peerDependencies": {
"browserslist": ">= 4.21.0"
}
},
"node_modules/vite": {
"version": "6.4.1",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
"picomatch": "^4.0.2",
"postcss": "^8.5.3",
"rollup": "^4.34.9",
"tinyglobby": "^0.2.13"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
},
"peerDependencies": {
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
"jiti": ">=1.21.0",
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
"sass-embedded": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.16.0",
"tsx": "^4.8.1",
"yaml": "^2.4.2"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"jiti": {
"optional": true
},
"less": {
"optional": true
},
"lightningcss": {
"optional": true
},
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
},
"tsx": {
"optional": true
},
"yaml": {
"optional": true
}
}
},
"node_modules/yallist": {
"version": "3.1.1",
"dev": true,
"license": "ISC"
}
}
}