feat: Emby 兼容层完整实现 + 多模块功能增强

## Emby/Jellyfin 兼容层 (emby_compat.go / emby.go)
- 新增 SystemInfoPublic、FindUser、Items、Item、LatestItems、ResumeItems
- 新增 SetFavorite、MarkPlayed、RecordProgress 用户播放状态同步
- 新增 itemPayload、mediaSource、mediaStreams 媒体信息组装
- 双前缀路由 /emby/* 和 / 根路径,兼容 Infuse/Yamby/Senplayer/Kodi
- 新增 Ping、SystemEndpoint、AuthByName 端点
- emby.go 扩展对应 handler 函数

## 站点适配器 (site_adapter.go / site.go)
- SiteConfig 扩展 UserAgent/Timeout/Extra/FlareSolverrURL 字段
- doRequest() GET 请求支持 FlareSolverr 代理绕过 Cloudflare/WAF
- MTeam api_key 认证改为 Authorization: Bearer 格式
- Search() 重构为 sync.WaitGroup 并发执行,提升多站搜索性能
- siteModelToConfig() 改为 SiteService 方法,按 BrowserEmulation 填充 FlareSolverrURL

## 图片代理 (image_proxy.go)
- 重构图片代理服务,支持更多来源和缓存策略

## 下载管理 (downloads.go / download_clients.go / qbittorrent.go)
- 下载任务增强:状态管理、进度追踪优化
- qBittorrent 客户端连接稳定性改进

## 刮削与数据库 (scraper.go / tmdb.go / repository.go)
- 刮削器增强 TMDB 集成,补全元数据字段
- repository 扩展查询方法

## 前端 (web/src/)
- HomePage: 首页布局重构,按媒体库分组展示,系列聚合优化
- DiscoverPage: 发现页增强,错误处理改进(API key 缺失/网络错误分离)
- PosterWallPage: 海报墙优化,系列聚合展示
- MediaCard: 媒体卡片优化
- PlayerPage: 播放器改进
- 新增 utils/groupSeries.ts: 系列聚合工具函数
- .gitignore: 添加 .tmp_* 临时文件排除规则
This commit is contained in:
ShukeBta
2026-05-26 16:09:13 +08:00
parent 5d1ae91419
commit 3a2db6bdd9
32 changed files with 2263 additions and 465 deletions
+23 -11
View File
@@ -1,24 +1,28 @@
import { useEffect, useState } from 'react'
import { GalleryHorizontalEnd } from 'lucide-react'
import { useEffect, useMemo, useState } from 'react'
import { GalleryHorizontalEnd, Layers } from 'lucide-react'
import { mediaAPI } from '../api/library'
import { MediaCard } from '../components/MediaCard'
import { groupSeries } from '../utils/groupSeries'
import type { Media } from '../types'
// PosterWallPage shows a dense poster grid of all media across every
// library — a visual "poster wall" that matches the original MediaStation's
// PosterWallView.vue. Loads the first 200 items sorted by rating.
// PosterWallPage 把所有媒体的代表海报聚合到同一面墙,便于一目了然
// 浏览整个站点的内容。所有 episode 行会按剧集折叠,避免同一海报刷屏。
export function PosterWallPage() {
const [items, setItems] = useState<Media[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
// 拉得多一点(500),折叠后才能凑够展示量;后端 page_size 上限可能
// 需要后续观察,暂以 500 试探。
mediaAPI
.search('', 200)
.search('', 500)
.then((d) => setItems(d.items))
.finally(() => setLoading(false))
}, [])
const cards = useMemo(() => groupSeries(items).slice(0, 240), [items])
return (
<div className="space-y-6">
<header className="flex items-center gap-3">
@@ -26,18 +30,26 @@ export function PosterWallPage() {
<div>
<h1 className="font-display text-3xl font-bold text-white">海报墙</h1>
<p className="text-sm text-slate-400">
全部媒体的海报展示(最多 200 项)。
按剧集聚合 · 共 {cards.length} 个条目
</p>
</div>
</header>
{loading && <p className="text-slate-500">加载中…</p>}
{!loading && items.length === 0 && (
{!loading && cards.length === 0 && (
<p className="text-slate-400">暂无媒体。请先添加媒体库并扫描。</p>
)}
<div className="grid grid-cols-3 gap-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-8">
{items.map((m) => (
<MediaCard key={m.id} media={m} />
<div className="grid grid-cols-3 gap-3 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8 xl:grid-cols-10">
{cards.map((s) => (
<div key={s.rep.id} className="relative">
<MediaCard media={s.rep} />
{s.count > 1 && (
<span className="pointer-events-none absolute right-1.5 top-1.5 inline-flex items-center gap-0.5 rounded-md bg-black/60 px-1.5 py-0.5 text-[10px] font-medium text-white backdrop-blur-sm">
<Layers size={10} />
{s.count}
</span>
)}
</div>
))}
</div>
</div>