mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 11:36:36 +08:00
7a18eb670a
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).
189 lines
6.4 KiB
TypeScript
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>
|
|
)
|
|
}
|