Files
MeBox/web/src/App.tsx
T
Kiro 7a18eb670a feat: site management + cross-site torrent search
Port the complete site management system from the original MediaStation:

Model:
  - model/model.go: Added Site table with fields for name, base_url,
    site_type (nexusphp/gazelle/unit3d/mteam/custom_rss), auth_type
    (cookie/api_key/authorization), cookie, api_key, auth_header,
    user_agent, rss_url, timeout, priority, use_proxy, enabled,
    login_status, downloader. Registered in AllModels().

Backend services:
  - service/site.go: SiteService with CRUD (Create/List/FindByID/Update/
    Delete), TestConnection (validates credentials via HTTP, updates
    login_status in DB), and Search (cross-site fan-out that queries
    every enabled site adapter and returns merged results sorted by
    seeders descending).
  - service/site_adapter.go: SiteAdapter interface + NewSiteAdapter
    factory + 5 implementations:
    * nexusPhpAdapter — regex HTML scraping of torrents.php
    * gazelleAdapter — JSON API /ajax.php?action=browse
    * unit3dAdapter — REST API /api/torrents/filter
    * mteamAdapter — M-Team v3 POST /api/torrent/search with x-api-key
    * rssAdapter — XML RSS/Atom feed parsing with keyword filter

Handler + routes:
  - handler/sites.go: listSites, getSite, createSite, updateSite,
    deleteSite, testSite, siteSearch handlers.
  - handler/handler.go: 7 new routes under authed group:
    GET/POST /sites, GET/PUT/DELETE /sites/:id,
    POST /sites/:id/test, GET /sites/search.

Frontend:
  - api/sites.ts: typed interfaces (Site, SiteSearchResult,
    CreateSiteInput) and sitesAPI helper.
  - pages/SitesPage.tsx: full CRUD UI with add-site form (site_type +
    auth_type selectors, cookie/api_key/rss inputs), connection test
    button with live status indicator, delete action.
  - pages/SiteSearchPage.tsx: cross-site search with keyword input,
    merged result table (site/title/size/seeders/leechers/free),
    one-click download-to-qBittorrent button.
  - App.tsx: lazy-loaded routes /sites and /site-search.
  - Layout.tsx: sidebar links 站点管理 + 站点搜索 under 自动化 group.

Verified: go build + go vet + go test pass; tsc -b + vite build passes
(32 lazy chunks, main bundle 255 KB / 85 KB gzipped).
2026-05-16 08:29:00 +00:00

189 lines
6.4 KiB
TypeScript

import { Suspense, lazy } from 'react'
import { Navigate, Route, Routes } from 'react-router-dom'
import { Layout } from './components/Layout'
import { RequireAdmin, RequireAuth } from './components/RequireAuth'
import { LoginPage } from './pages/LoginPage'
// Lazy-loaded routes — the login screen and the layout shell ship in the
// initial bundle; everything else is fetched on first navigation.
const HomePage = lazy(() => import('./pages/HomePage').then((m) => ({ default: m.HomePage })))
const LibraryPage = lazy(() =>
import('./pages/LibraryPage').then((m) => ({ default: m.LibraryPage })),
)
const SearchPage = lazy(() =>
import('./pages/SearchPage').then((m) => ({ default: m.SearchPage })),
)
const FavouritesPage = lazy(() =>
import('./pages/FavouritesPage').then((m) => ({ default: m.FavouritesPage })),
)
const PlaylistsPage = lazy(() =>
import('./pages/PlaylistsPage').then((m) => ({ default: m.PlaylistsPage })),
)
const PlaylistDetailPage = lazy(() =>
import('./pages/PlaylistDetailPage').then((m) => ({ default: m.PlaylistDetailPage })),
)
const MediaDetailPage = lazy(() =>
import('./pages/MediaDetailPage').then((m) => ({ default: m.MediaDetailPage })),
)
const PlayerPage = lazy(() =>
import('./pages/PlayerPage').then((m) => ({ default: m.PlayerPage })),
)
const AdminPage = lazy(() => import('./pages/AdminPage').then((m) => ({ default: m.AdminPage })))
const DownloadsPage = lazy(() =>
import('./pages/DownloadsPage').then((m) => ({ default: m.DownloadsPage })),
)
const SubscriptionsPage = lazy(() =>
import('./pages/SubscriptionsPage').then((m) => ({ default: m.SubscriptionsPage })),
)
const ProfilePage = lazy(() =>
import('./pages/ProfilePage').then((m) => ({ default: m.ProfilePage })),
)
const StatsPage = lazy(() => import('./pages/StatsPage').then((m) => ({ default: m.StatsPage })))
const DiscoverPage = lazy(() =>
import('./pages/DiscoverPage').then((m) => ({ default: m.DiscoverPage })),
)
const TasksPage = lazy(() => import('./pages/TasksPage').then((m) => ({ default: m.TasksPage })))
const RecycleBinPage = lazy(() =>
import('./pages/RecycleBinPage').then((m) => ({ default: m.RecycleBinPage })),
)
const DlnaPage = lazy(() => import('./pages/DlnaPage').then((m) => ({ default: m.DlnaPage })))
const FileManagerPage = lazy(() =>
import('./pages/FileManagerPage').then((m) => ({ default: m.FileManagerPage })),
)
const APIConfigsPage = lazy(() =>
import('./pages/APIConfigsPage').then((m) => ({ default: m.APIConfigsPage })),
)
const StoragePage = lazy(() =>
import('./pages/StoragePage').then((m) => ({ default: m.StoragePage })),
)
const DuplicatesPage = lazy(() =>
import('./pages/DuplicatesPage').then((m) => ({ default: m.DuplicatesPage })),
)
const SchedulerPage = lazy(() =>
import('./pages/SchedulerPage').then((m) => ({ default: m.SchedulerPage })),
)
const WatchHistoryPage = lazy(() =>
import('./pages/WatchHistoryPage').then((m) => ({ default: m.WatchHistoryPage })),
)
const PosterWallPage = lazy(() =>
import('./pages/PosterWallPage').then((m) => ({ default: m.PosterWallPage })),
)
const SitesPage = lazy(() =>
import('./pages/SitesPage').then((m) => ({ default: m.SitesPage })),
)
const SiteSearchPage = lazy(() =>
import('./pages/SiteSearchPage').then((m) => ({ default: m.SiteSearchPage })),
)
const Loading = () => <p className="px-6 py-8 text-slate-500">加载中…</p>
export default function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route
path="/"
element={
<RequireAuth>
<Layout />
</RequireAuth>
}
>
<Route index element={<HomePage />} />
<Route path="library/:id" element={<LibraryPage />} />
<Route path="discover" element={<DiscoverPage />} />
<Route path="search" element={<SearchPage />} />
<Route path="favourites" element={<FavouritesPage />} />
<Route path="playlists" element={<PlaylistsPage />} />
<Route path="playlist/:id" element={<PlaylistDetailPage />} />
<Route path="media/:id" element={<MediaDetailPage />} />
<Route path="play/:id" element={<PlayerPage />} />
<Route path="downloads" element={<DownloadsPage />} />
<Route path="subscriptions" element={<SubscriptionsPage />} />
<Route path="profile" element={<ProfilePage />} />
<Route path="dlna" element={<DlnaPage />} />
<Route path="history" element={<WatchHistoryPage />} />
<Route path="poster-wall" element={<PosterWallPage />} />
<Route path="sites" element={<SitesPage />} />
<Route path="site-search" element={<SiteSearchPage />} />
<Route
path="files"
element={
<RequireAdmin>
<FileManagerPage />
</RequireAdmin>
}
/>
<Route
path="storage"
element={
<RequireAdmin>
<StoragePage />
</RequireAdmin>
}
/>
<Route
path="duplicates"
element={
<RequireAdmin>
<DuplicatesPage />
</RequireAdmin>
}
/>
<Route
path="scheduler"
element={
<RequireAdmin>
<SchedulerPage />
</RequireAdmin>
}
/>
<Route
path="api-configs"
element={
<RequireAdmin>
<APIConfigsPage />
</RequireAdmin>
}
/>
<Route
path="tasks"
element={
<RequireAdmin>
<TasksPage />
</RequireAdmin>
}
/>
<Route
path="recycle"
element={
<RequireAdmin>
<RecycleBinPage />
</RequireAdmin>
}
/>
<Route
path="stats"
element={
<RequireAdmin>
<StatsPage />
</RequireAdmin>
}
/>
<Route
path="admin"
element={
<RequireAdmin>
<AdminPage />
</RequireAdmin>
}
/>
</Route>
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</Suspense>
)
}