fix: 订阅恢复补缺集/单媒体手动整理入口/媒体库排序/网盘媒体分类迁移

- 订阅(问题1): selectPreparedSubscriptionCandidates 在本地已有部分集时不再
  无条件丢弃整季/全集包,单集候选不足时用全集包兜底;Restore 重置 total_episodes
  让下次 run 从权威元数据重算,避免被 feed 低估值锁死误判'无缺集'
- 单媒体整理(问题2): 后端已具备,新增 OrganizeMediaDialog 组件并在媒体详情页
  接入'整理入库'按钮(可选类型/二级分类/目标路径/转移方式/预览)
- 媒体库排序(问题3): ListByLibrariesFiltered 改多级排序 year/updated_at/
  created_at/id,id 作稳定 tie-breaker 消除云盘批量扫描同批时间戳的随机观感
- 网盘媒体消失(问题4): 云盘媒体 upsert 时允许 library_id 迁移到重新挂载的
  更精确分类库(本地媒体物理位置固定不迁移),修复一键挂载子目录后媒体消失
This commit is contained in:
ShukeBta
2026-06-19 21:26:51 +08:00
parent 9b4884359b
commit b40e2c7280
8 changed files with 328 additions and 9 deletions
+13 -1
View File
@@ -1,7 +1,7 @@
import { motion } from 'framer-motion'
import { useEffect, useState } from 'react'
import { Link, useNavigate, useParams } from 'react-router-dom'
import { FileText, Heart, Play, RefreshCw, Sparkles, Trash2, Calendar, Database, Search, Pencil } from 'lucide-react'
import { FileText, Heart, Play, RefreshCw, Sparkles, Trash2, Calendar, Database, Search, Pencil, FolderInput } from 'lucide-react'
import toast from 'react-hot-toast'
import { mediaAPI } from '../api/library'
@@ -15,6 +15,7 @@ import { confirmAction } from '../components/ConfirmDialog'
import { ExternalPlayerButton } from '../components/ExternalPlayerButton'
import { ManualScrapeDialog } from '../components/ManualScrapeDialog'
import { MetadataEditDialog } from '../components/MetadataEditDialog'
import { OrganizeMediaDialog } from '../components/OrganizeMediaDialog'
function fmtDuration(sec: number): string {
if (!sec || sec <= 0) return '—'
@@ -49,6 +50,7 @@ export function MediaDetailPage() {
const [loading, setLoading] = useState(true)
const [manualScrapeOpen, setManualScrapeOpen] = useState(false)
const [metadataEditOpen, setMetadataEditOpen] = useState(false)
const [organizeOpen, setOrganizeOpen] = useState(false)
const refresh = async () => {
if (!id) return
@@ -327,6 +329,10 @@ export function MediaDetailPage() {
<Pencil size={13} className="text-gray-600" />
<span>编辑元数据</span>
</button>
<button onClick={() => setOrganizeOpen(true)} className="btn-outline py-2 px-3.5 text-xs gap-1.5 border-gray-200 hover:border-brand-500/50 hover:bg-brand-50">
<FolderInput size={13} className="text-[#c9954a]" />
<span>整理入库</span>
</button>
<button onClick={reprobe} className="btn-outline py-2 px-3.5 text-xs gap-1.5 border-gray-200 hover:border-brand-500/50 hover:bg-brand-50">
<Database size={13} className="text-gray-600" />
<span>探测媒体轨 (ffprobe)</span>
@@ -366,6 +372,12 @@ export function MediaDetailPage() {
await refresh()
}}
/>
<OrganizeMediaDialog
open={organizeOpen}
media={media}
onClose={() => setOrganizeOpen(false)}
onOrganized={refresh}
/>
</div>
)
}