mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-30 11:46:36 +08:00
feat: 网页端完整浏览/播放远程 Emby 挂载库
- 后端新增远程库→本地结构映射(emby_remote_web.go):Library/Media/SeriesCard 与本地完全同构,前端无感知 - /api/libraries(含 with_preview)、/api/libraries/:id、/api/libraries/:id/media、 /api/libraries/:id/series、/api/libraries/:id/series/episodes、/api/media/:id、 /api/media/:id/episodes 全部支持远程伪装 ID 透传映射 - /api/stream/:id 对远程条目 302 直连远程 Emby 原地址(播放不经过本机) - 库内容按 CollectionType 过滤(电影库取 Movie、剧集库取 Series), RecursiveItemCount 作为系列集数;修复伪装 ID 双重编码导致的图片/详情 500 - SPA fallback 放行 /library/embyremote~ 前缀(不再被当作 Emby API 路由 404) - 前端:远程库/条目标记 is_remote_emby,隐藏扫描/刮削/编辑/NFO/回收站/ HLS 转码等不适用操作;后台管理面板过滤远程库
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { Database, FileText, Search, Sparkles, Trash2 } from 'lucide-react'
|
||||
|
||||
import type { Media } from '../types'
|
||||
import { isRemoteEmbyID } from '../utils/remoteEmby'
|
||||
|
||||
type LibraryMovieActionsProps = {
|
||||
media: Media
|
||||
@@ -23,6 +24,11 @@ export function LibraryMovieActions({
|
||||
}: LibraryMovieActionsProps) {
|
||||
const buttonClass = 'flex h-8 w-8 items-center justify-center rounded-lg border border-white/70 bg-white/90 text-gray-700 shadow-sm backdrop-blur transition hover:bg-brand-50 hover:text-brand-600 disabled:opacity-50'
|
||||
|
||||
// 远程 Emby 条目为只读视图:刮削/探测/NFO/回收站不适用。
|
||||
if (isRemoteEmbyID(media.id)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<button title="智能刮削" disabled={busy} onClick={() => onSmartScrape(media)} className={buttonClass}>
|
||||
|
||||
@@ -63,7 +63,7 @@ export function LibraryPageHeader({
|
||||
onChange={onSortChange}
|
||||
onReshuffle={onReshuffle}
|
||||
/>
|
||||
{isAdmin && (
|
||||
{isAdmin && !library?.is_remote_emby && (
|
||||
<>
|
||||
<EpisodeArtworkToggle
|
||||
checked={scrapeEpisodeArtwork}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isRemoteEmbyID } from '../utils/remoteEmby'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { ArrowLeft, Database, FileText, Film, FolderInput, Pencil, Play, Search, Sparkles, Trash2 } from 'lucide-react'
|
||||
|
||||
@@ -84,7 +85,7 @@ export function LibrarySeriesDetailHeader({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isAdmin && allEpisodes.length > 0 && (
|
||||
{isAdmin && allEpisodes.length > 0 && !isRemoteEmbyID(series.rep.id) && (
|
||||
<div className="rounded-2xl border border-sand-200 bg-white/80 p-4 shadow-sm">
|
||||
<p className="mb-3 text-[10px] font-bold uppercase tracking-[0.2em] text-[#c9954a]">系统后台高级控制面板</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Database, FileText, FolderInput, Pencil, Search, Sparkles, Trash2 } fro
|
||||
|
||||
import { EpisodeArtworkToggle } from '../components/EpisodeArtworkToggle'
|
||||
import type { Media } from '../types'
|
||||
import { isRemoteEmbyID } from '../utils/remoteEmby'
|
||||
|
||||
type MediaDetailAdminPanelProps = {
|
||||
media: Media
|
||||
@@ -28,6 +29,10 @@ export function MediaDetailAdminPanel({
|
||||
onExportNFO,
|
||||
onSoftDelete,
|
||||
}: MediaDetailAdminPanelProps) {
|
||||
if (isRemoteEmbyID(media.id)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-2xl border border-gray-200 bg-gray-50/50 p-5 space-y-3">
|
||||
<p className="text-[10px] font-bold uppercase tracking-[0.2em] text-[#c9954a]">系统后台高级控制面板</p>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isRemoteEmbyID } from '../utils/remoteEmby'
|
||||
import { ArrowLeft, Heart, Play, RefreshCw } from 'lucide-react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
@@ -95,14 +96,16 @@ export function MediaDetailPlaybackActions({
|
||||
<span>立即播放</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
{!isRemoteEmbyID(media.id) && (
|
||||
<Link
|
||||
to={`/play/${media.id}?mode=hls`}
|
||||
state={{ from: `/media/${media.id}` }}
|
||||
className="btn-outline border-brand-500/30 hover:border-brand-500 text-[#c9954a] hover:bg-brand-50 px-5"
|
||||
>
|
||||
<RefreshCw size={14} className="animate-spin-slow" />
|
||||
<span>HLS 兼容转码播放</span>
|
||||
</Link>
|
||||
<RefreshCw size={14} className="animate-spin-slow" />
|
||||
<span>HLS 兼容转码播放</span>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<ExternalPlayerButton mediaId={media.id} />
|
||||
|
||||
|
||||
@@ -18,7 +18,10 @@ export function useAdminLibraryPanel() {
|
||||
|
||||
function useAdminLibraryList() {
|
||||
const [libs, setLibs] = useState<Library[]>([])
|
||||
const refresh = () => libraryAPI.list({ includeHidden: true }).then(setLibs)
|
||||
const refresh = () =>
|
||||
libraryAPI
|
||||
.list({ includeHidden: true })
|
||||
.then((libs) => setLibs(libs.filter((l) => !l.is_remote_emby))) // 远程挂载库只读,不在后台管理列表内
|
||||
|
||||
useEffect(() => {
|
||||
refresh().catch(() => undefined)
|
||||
|
||||
@@ -21,6 +21,9 @@ export interface Library {
|
||||
roots?: LibraryRoot[]
|
||||
created_at: string
|
||||
updated_at: string
|
||||
/** 远程 Emby 挂载库(只读,不支持扫描/刮削/编辑) */
|
||||
is_remote_emby?: boolean
|
||||
remote_source?: string
|
||||
}
|
||||
|
||||
export interface ScanResult {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// 远程 Emby 挂载条目 / 媒体库的识别与只读标记。
|
||||
// 与后端 internal/service/emby_remote_ids.go 的 ID 伪装协议保持一致:
|
||||
// embyremote~{accountID}~{remoteID}。
|
||||
export function isRemoteEmbyID(id?: string | null): boolean {
|
||||
return Boolean(id && id.startsWith('embyremote~'))
|
||||
}
|
||||
Reference in New Issue
Block a user