mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-30 03:36:37 +08:00
Add advanced discovery subscriptions
This commit is contained in:
@@ -12,10 +12,22 @@ export const subscriptionsAPI = {
|
||||
media_type?: string
|
||||
media_category?: string
|
||||
save_path?: string
|
||||
search_mode?: string
|
||||
imdb_id?: string
|
||||
resolution?: string
|
||||
quality?: string
|
||||
effects?: string
|
||||
release_groups?: string
|
||||
exclude_words?: string
|
||||
wash_priority?: string
|
||||
priority?: number
|
||||
enabled?: boolean
|
||||
}) =>
|
||||
api.post<Subscription>('/subscriptions', input).then((r) => r.data),
|
||||
|
||||
update: (id: string, input: Partial<Subscription>) =>
|
||||
api.put(`/subscriptions/${id}`, input).then((r) => r.data),
|
||||
|
||||
remove: (id: string) => api.delete(`/subscriptions/${id}`).then((r) => r.data),
|
||||
|
||||
runNow: (id: string) =>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { AlertTriangle, Sparkles } from 'lucide-react'
|
||||
import toast from 'react-hot-toast'
|
||||
import { AlertTriangle, Download, Info, Rss, Sparkles, X } from 'lucide-react'
|
||||
|
||||
import { discoverAPI, type DiscoverItem, type DiscoverSection } from '../api/discover'
|
||||
import { imageURL } from '../api/client'
|
||||
import { subscriptionsAPI } from '../api/subscriptions'
|
||||
|
||||
const defaultSections = [
|
||||
'tmdb_trending_day',
|
||||
@@ -19,6 +21,7 @@ export function DiscoverPage() {
|
||||
const [rows, setRows] = useState<Record<string, DiscoverItem[]>>({})
|
||||
const [error, setError] = useState('')
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [activeItem, setActiveItem] = useState<DiscoverItem | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
discoverAPI
|
||||
@@ -138,6 +141,7 @@ export function DiscoverPage() {
|
||||
key={key}
|
||||
title={sectionMap.get(key)?.label ?? key}
|
||||
items={items}
|
||||
onSelect={setActiveItem}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
@@ -151,27 +155,46 @@ export function DiscoverPage() {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeItem && (
|
||||
<DiscoverDetailModal
|
||||
item={activeItem}
|
||||
onClose={() => setActiveItem(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ContentRow({ title, items }: { title: string; items: DiscoverItem[] }) {
|
||||
function ContentRow({
|
||||
title,
|
||||
items,
|
||||
onSelect,
|
||||
}: {
|
||||
title: string
|
||||
items: DiscoverItem[]
|
||||
onSelect: (item: DiscoverItem) => void
|
||||
}) {
|
||||
return (
|
||||
<section className="space-y-4">
|
||||
<h2 className="pl-1 font-display text-2xl font-semibold text-ink-600">{title}</h2>
|
||||
<div className="grid grid-cols-3 gap-4 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-7 xl:grid-cols-8">
|
||||
{items.map((item, index) => (
|
||||
<DiscoverCard key={discoverKey(item, index)} item={item} />
|
||||
<DiscoverCard key={discoverKey(item, index)} item={item} onSelect={onSelect} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function DiscoverCard({ item }: { item: DiscoverItem }) {
|
||||
function DiscoverCard({ item, onSelect }: { item: DiscoverItem; onSelect: (item: DiscoverItem) => void }) {
|
||||
const source = item.source || (item.bangumi_id ? 'bangumi' : item.douban_id ? 'douban' : 'tmdb')
|
||||
return (
|
||||
<div className="group relative overflow-hidden rounded-xl border border-gray-200 bg-gray-50 transition-all duration-300 hover:border-primary-500/30">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSelect(item)}
|
||||
className="group relative overflow-hidden rounded-xl border border-gray-200 bg-gray-50 text-left transition-all duration-300 hover:-translate-y-1 hover:border-primary-500/30 hover:shadow-xl focus:outline-none focus:ring-2 focus:ring-primary-400/40"
|
||||
>
|
||||
<div className="relative aspect-[2/3] w-full overflow-hidden bg-surface-900">
|
||||
{item.poster_url ? (
|
||||
<img
|
||||
@@ -202,11 +225,217 @@ function DiscoverCard({ item }: { item: DiscoverItem }) {
|
||||
<p className="text-[11px] text-sand-500">
|
||||
{[item.media_type, item.year && item.year > 0 ? item.year : ''].filter(Boolean).join(' · ') || '推荐'}
|
||||
</p>
|
||||
<p className="flex items-center gap-1 pt-1 text-[10px] font-semibold text-brand-500">
|
||||
<Info size={10} />
|
||||
详情 / 订阅
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function DiscoverDetailModal({ item, onClose }: { item: DiscoverItem; onClose: () => void }) {
|
||||
const source = item.source || (item.bangumi_id ? 'bangumi' : item.douban_id ? 'douban' : 'tmdb')
|
||||
const keyword = item.subscribe_keyword || buildSubscribeKeyword(item)
|
||||
const [form, setForm] = useState({
|
||||
keyword,
|
||||
search_mode: 'keyword',
|
||||
imdb_id: '',
|
||||
media_type: item.media_type || '',
|
||||
resolution: 'best',
|
||||
quality: '',
|
||||
effects: '',
|
||||
release_groups: '',
|
||||
exclude_words: 'cam,ts,tc,枪版',
|
||||
wash_priority: 'balanced',
|
||||
save_path: '',
|
||||
media_category: '',
|
||||
priority: 50,
|
||||
run_now: true,
|
||||
})
|
||||
const [busy, setBusy] = useState(false)
|
||||
|
||||
const submit = async () => {
|
||||
const finalKeyword = form.keyword.trim() || keyword
|
||||
const feed = `site-search://search?keyword=${encodeURIComponent(finalKeyword)}&source=${encodeURIComponent(source)}`
|
||||
setBusy(true)
|
||||
try {
|
||||
const sub = await subscriptionsAPI.create({
|
||||
name: `${item.title} 自动订阅`,
|
||||
feed_url: feed,
|
||||
filter: finalKeyword,
|
||||
media_type: form.media_type || undefined,
|
||||
media_category: form.media_category || undefined,
|
||||
save_path: form.save_path || undefined,
|
||||
search_mode: form.search_mode,
|
||||
imdb_id: form.imdb_id || undefined,
|
||||
resolution: form.resolution === 'best' ? 'best' : form.resolution,
|
||||
quality: form.quality || undefined,
|
||||
effects: form.effects || undefined,
|
||||
release_groups: form.release_groups || undefined,
|
||||
exclude_words: form.exclude_words || undefined,
|
||||
wash_priority: form.wash_priority,
|
||||
priority: form.priority,
|
||||
enabled: true,
|
||||
})
|
||||
if (form.run_now) {
|
||||
const run = await subscriptionsAPI.runNow(sub.id)
|
||||
toast.success(run.queued > 0 ? `已订阅并加入 ${run.queued} 个下载` : '已订阅,暂未命中可下载资源')
|
||||
} else {
|
||||
toast.success('已创建订阅')
|
||||
}
|
||||
onClose()
|
||||
} catch (err) {
|
||||
const msg =
|
||||
(err as { response?: { data?: { error?: string } } })?.response?.data?.error ??
|
||||
'订阅失败'
|
||||
toast.error(msg)
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/45 p-4 backdrop-blur-sm">
|
||||
<div className="max-h-[92vh] w-full max-w-5xl overflow-y-auto rounded-3xl border border-white/60 bg-white p-5 shadow-2xl">
|
||||
<div className="mb-4 flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-widest text-brand-500">{source}</p>
|
||||
<h2 className="font-display text-2xl font-bold text-ink-600">{item.title}</h2>
|
||||
<p className="mt-1 text-sm text-sand-500">
|
||||
{[item.media_type, item.year && item.year > 0 ? item.year : '', item.rating ? `★ ${item.rating.toFixed(1)}` : '']
|
||||
.filter(Boolean)
|
||||
.join(' · ')}
|
||||
</p>
|
||||
</div>
|
||||
<button className="rounded-full border border-gray-200 p-2 text-ink-50 hover:bg-gray-50" onClick={onClose}>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-5 lg:grid-cols-[260px_1fr]">
|
||||
<div className="space-y-3">
|
||||
<div className="overflow-hidden rounded-2xl bg-gray-100">
|
||||
{item.poster_url ? (
|
||||
<img src={imageURL(item.poster_url)} alt={item.title} className="aspect-[2/3] w-full object-cover" />
|
||||
) : (
|
||||
<div className="flex aspect-[2/3] items-center justify-center text-sand-500">无海报</div>
|
||||
)}
|
||||
</div>
|
||||
{item.backdrop_url && (
|
||||
<img src={imageURL(item.backdrop_url)} alt="" className="h-24 w-full rounded-2xl object-cover" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-5">
|
||||
<section className="rounded-2xl border border-gray-200 bg-gray-50 p-4">
|
||||
<h3 className="mb-2 font-semibold text-ink-600">简介</h3>
|
||||
<p className="text-sm leading-6 text-ink-100">{item.overview || '当前数据源没有返回简介。'}</p>
|
||||
</section>
|
||||
|
||||
<section className="rounded-2xl border border-primary-400/20 bg-primary-400/5 p-4">
|
||||
<h3 className="mb-3 flex items-center gap-2 font-semibold text-ink-600">
|
||||
<Rss size={16} />
|
||||
订阅下载规则
|
||||
</h3>
|
||||
<div className="grid gap-3 md:grid-cols-3">
|
||||
<label className="text-xs text-sand-500 md:col-span-2">
|
||||
搜索关键词
|
||||
<input className="input-base mt-1" value={form.keyword} onChange={(e) => setForm((f) => ({ ...f, keyword: e.target.value }))} />
|
||||
</label>
|
||||
<label className="text-xs text-sand-500">
|
||||
搜索方式
|
||||
<select className="input-base mt-1" value={form.search_mode} onChange={(e) => setForm((f) => ({ ...f, search_mode: e.target.value }))}>
|
||||
<option value="keyword">标题关键词</option>
|
||||
<option value="imdb">IMDB ID</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="text-xs text-sand-500">
|
||||
IMDB ID
|
||||
<input className="input-base mt-1" placeholder="tt1160419" value={form.imdb_id} onChange={(e) => setForm((f) => ({ ...f, imdb_id: e.target.value }))} />
|
||||
</label>
|
||||
<label className="text-xs text-sand-500">
|
||||
类型
|
||||
<select className="input-base mt-1" value={form.media_type} onChange={(e) => setForm((f) => ({ ...f, media_type: e.target.value }))}>
|
||||
<option value="">自动识别</option>
|
||||
<option value="movie">电影</option>
|
||||
<option value="tv">电视剧</option>
|
||||
<option value="anime">动漫</option>
|
||||
<option value="variety">综艺</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="text-xs text-sand-500">
|
||||
分辨率
|
||||
<select className="input-base mt-1" value={form.resolution} onChange={(e) => setForm((f) => ({ ...f, resolution: e.target.value }))}>
|
||||
<option value="best">自动择优</option>
|
||||
<option value="2160p">2160p / 4K</option>
|
||||
<option value="1080p">1080p</option>
|
||||
<option value="720p">720p</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="text-xs text-sand-500">
|
||||
质量
|
||||
<select className="input-base mt-1" value={form.quality} onChange={(e) => setForm((f) => ({ ...f, quality: e.target.value }))}>
|
||||
<option value="">不限</option>
|
||||
<option value="remux">REMUX</option>
|
||||
<option value="bluray">BluRay</option>
|
||||
<option value="web-dl">WEB-DL</option>
|
||||
<option value="hdtv">HDTV</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="text-xs text-sand-500">
|
||||
特效 / 音轨
|
||||
<input className="input-base mt-1" placeholder="hdr,dolby-vision,atmos" value={form.effects} onChange={(e) => setForm((f) => ({ ...f, effects: e.target.value }))} />
|
||||
</label>
|
||||
<label className="text-xs text-sand-500">
|
||||
洗版优先级
|
||||
<select className="input-base mt-1" value={form.wash_priority} onChange={(e) => setForm((f) => ({ ...f, wash_priority: e.target.value }))}>
|
||||
<option value="balanced">均衡</option>
|
||||
<option value="resolution">分辨率优先</option>
|
||||
<option value="quality">片源质量优先</option>
|
||||
<option value="effects">HDR/DV/Atmos 优先</option>
|
||||
<option value="seeders">做种数优先</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="text-xs text-sand-500">
|
||||
发布组
|
||||
<input className="input-base mt-1" placeholder="如 FRDS,OurTV" value={form.release_groups} onChange={(e) => setForm((f) => ({ ...f, release_groups: e.target.value }))} />
|
||||
</label>
|
||||
<label className="text-xs text-sand-500">
|
||||
排除词
|
||||
<input className="input-base mt-1" value={form.exclude_words} onChange={(e) => setForm((f) => ({ ...f, exclude_words: e.target.value }))} />
|
||||
</label>
|
||||
<label className="text-xs text-sand-500">
|
||||
分类覆盖
|
||||
<input className="input-base mt-1" placeholder="综艺 / 日番 / 欧美剧" value={form.media_category} onChange={(e) => setForm((f) => ({ ...f, media_category: e.target.value }))} />
|
||||
</label>
|
||||
<label className="text-xs text-sand-500">
|
||||
保存路径覆盖
|
||||
<input className="input-base mt-1" value={form.save_path} onChange={(e) => setForm((f) => ({ ...f, save_path: e.target.value }))} />
|
||||
</label>
|
||||
</div>
|
||||
<div className="mt-4 flex flex-wrap items-center justify-between gap-3">
|
||||
<label className="flex items-center gap-2 text-sm text-ink-100">
|
||||
<input type="checkbox" checked={form.run_now} onChange={(e) => setForm((f) => ({ ...f, run_now: e.target.checked }))} />
|
||||
创建后立即搜索并下载
|
||||
</label>
|
||||
<button disabled={busy} onClick={submit} className="neon-button disabled:opacity-60">
|
||||
<Download size={16} />
|
||||
{busy ? '处理中…' : '创建订阅'}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function buildSubscribeKeyword(item: DiscoverItem): string {
|
||||
return [item.title, item.year && item.year > 0 ? item.year : ''].filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
function DiscoverSkeleton() {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FormEvent, useEffect, useState } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
import { Play, Plus, Trash2 } from 'lucide-react'
|
||||
import { Pencil, Play, Plus, Save, Trash2 } from 'lucide-react'
|
||||
|
||||
import { subscriptionsAPI } from '../api/subscriptions'
|
||||
import type { Subscription } from '../types'
|
||||
@@ -13,6 +13,15 @@ export function SubscriptionsPage() {
|
||||
const [mediaType, setMediaType] = useState('')
|
||||
const [mediaCategory, setMediaCategory] = useState('')
|
||||
const [savePath, setSavePath] = useState('')
|
||||
const [searchMode, setSearchMode] = useState('keyword')
|
||||
const [imdbID, setImdbID] = useState('')
|
||||
const [resolution, setResolution] = useState('best')
|
||||
const [quality, setQuality] = useState('')
|
||||
const [effects, setEffects] = useState('')
|
||||
const [releaseGroups, setReleaseGroups] = useState('')
|
||||
const [excludeWords, setExcludeWords] = useState('cam,ts,tc,枪版')
|
||||
const [washPriority, setWashPriority] = useState('balanced')
|
||||
const [editingId, setEditingId] = useState('')
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
const refresh = () =>
|
||||
@@ -28,21 +37,31 @@ export function SubscriptionsPage() {
|
||||
const onCreate = async (e: FormEvent) => {
|
||||
e.preventDefault()
|
||||
try {
|
||||
await subscriptionsAPI.create({
|
||||
const payload = {
|
||||
name,
|
||||
feed_url: feed,
|
||||
filter,
|
||||
media_type: mediaType || undefined,
|
||||
media_category: mediaCategory || undefined,
|
||||
save_path: savePath || undefined,
|
||||
})
|
||||
toast.success('已创建订阅')
|
||||
setName('')
|
||||
setFeed('')
|
||||
setFilter('')
|
||||
setMediaType('')
|
||||
setMediaCategory('')
|
||||
setSavePath('')
|
||||
search_mode: searchMode,
|
||||
imdb_id: imdbID || undefined,
|
||||
resolution,
|
||||
quality: quality || undefined,
|
||||
effects: effects || undefined,
|
||||
release_groups: releaseGroups || undefined,
|
||||
exclude_words: excludeWords || undefined,
|
||||
wash_priority: washPriority,
|
||||
priority: 50,
|
||||
}
|
||||
if (editingId) {
|
||||
await subscriptionsAPI.update(editingId, payload)
|
||||
toast.success('已更新订阅')
|
||||
} else {
|
||||
await subscriptionsAPI.create(payload)
|
||||
toast.success('已创建订阅')
|
||||
}
|
||||
resetForm()
|
||||
await refresh()
|
||||
} catch (err: unknown) {
|
||||
const msg =
|
||||
@@ -51,6 +70,43 @@ export function SubscriptionsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
setEditingId('')
|
||||
setName('')
|
||||
setFeed('')
|
||||
setFilter('')
|
||||
setMediaType('')
|
||||
setMediaCategory('')
|
||||
setSavePath('')
|
||||
setSearchMode('keyword')
|
||||
setImdbID('')
|
||||
setResolution('best')
|
||||
setQuality('')
|
||||
setEffects('')
|
||||
setReleaseGroups('')
|
||||
setExcludeWords('cam,ts,tc,枪版')
|
||||
setWashPriority('balanced')
|
||||
}
|
||||
|
||||
const startEdit = (s: Subscription) => {
|
||||
setEditingId(s.id)
|
||||
setName(s.name)
|
||||
setFeed(s.feed_url)
|
||||
setFilter(s.filter || '')
|
||||
setMediaType(s.media_type || '')
|
||||
setMediaCategory(s.media_category || '')
|
||||
setSavePath(s.save_path || '')
|
||||
setSearchMode(s.search_mode || 'keyword')
|
||||
setImdbID(s.imdb_id || '')
|
||||
setResolution(s.resolution || 'best')
|
||||
setQuality(s.quality || '')
|
||||
setEffects(s.effects || '')
|
||||
setReleaseGroups(s.release_groups || '')
|
||||
setExcludeWords(s.exclude_words || '')
|
||||
setWashPriority(s.wash_priority || 'balanced')
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h1 className="font-display text-3xl font-bold text-ink-600">RSS 订阅</h1>
|
||||
@@ -98,9 +154,43 @@ export function SubscriptionsPage() {
|
||||
value={savePath}
|
||||
onChange={(e) => setSavePath(e.target.value)}
|
||||
/>
|
||||
<select className="input-base" value={searchMode} onChange={(e) => setSearchMode(e.target.value)}>
|
||||
<option value="keyword">标题关键词搜索</option>
|
||||
<option value="imdb">IMDB ID 搜索</option>
|
||||
</select>
|
||||
<input className="input-base" placeholder="IMDB ID,如 tt1160419" value={imdbID} onChange={(e) => setImdbID(e.target.value)} />
|
||||
<select className="input-base" value={resolution} onChange={(e) => setResolution(e.target.value)}>
|
||||
<option value="best">分辨率自动择优</option>
|
||||
<option value="2160p">2160p / 4K</option>
|
||||
<option value="1080p">1080p</option>
|
||||
<option value="720p">720p</option>
|
||||
</select>
|
||||
<select className="input-base" value={quality} onChange={(e) => setQuality(e.target.value)}>
|
||||
<option value="">质量不限</option>
|
||||
<option value="remux">REMUX</option>
|
||||
<option value="bluray">BluRay</option>
|
||||
<option value="web-dl">WEB-DL</option>
|
||||
<option value="hdtv">HDTV</option>
|
||||
</select>
|
||||
<input className="input-base" placeholder="特效/音轨 hdr,dolby-vision,atmos" value={effects} onChange={(e) => setEffects(e.target.value)} />
|
||||
<select className="input-base" value={washPriority} onChange={(e) => setWashPriority(e.target.value)}>
|
||||
<option value="balanced">洗版:均衡</option>
|
||||
<option value="resolution">洗版:分辨率优先</option>
|
||||
<option value="quality">洗版:片源质量优先</option>
|
||||
<option value="effects">洗版:HDR/DV/Atmos 优先</option>
|
||||
<option value="seeders">洗版:做种数优先</option>
|
||||
</select>
|
||||
<input className="input-base" placeholder="发布组白名单,如 FRDS,OurTV" value={releaseGroups} onChange={(e) => setReleaseGroups(e.target.value)} />
|
||||
<input className="input-base" placeholder="排除词,如 cam,ts,tc" value={excludeWords} onChange={(e) => setExcludeWords(e.target.value)} />
|
||||
<button type="submit" className="neon-button md:col-span-1">
|
||||
<Plus size={16} /> 添加
|
||||
{editingId ? <Save size={16} /> : <Plus size={16} />}
|
||||
{editingId ? '保存' : '添加'}
|
||||
</button>
|
||||
{editingId && (
|
||||
<button type="button" onClick={resetForm} className="rounded-xl border border-gray-200 px-3 py-2 text-sm text-ink-100 hover:bg-gray-50">
|
||||
取消编辑
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
|
||||
{loading && <p className="text-sand-500">加载中…</p>}
|
||||
@@ -115,6 +205,7 @@ export function SubscriptionsPage() {
|
||||
<th>RSS</th>
|
||||
<th>过滤器</th>
|
||||
<th>分类</th>
|
||||
<th>规则</th>
|
||||
<th>最近运行</th>
|
||||
<th className="text-right">操作</th>
|
||||
</tr>
|
||||
@@ -130,10 +221,21 @@ export function SubscriptionsPage() {
|
||||
<td className="text-ink-100">
|
||||
{[s.media_type, s.media_category].filter(Boolean).join(' / ') || '自动'}
|
||||
</td>
|
||||
<td className="max-w-xs text-xs text-ink-100">
|
||||
{[s.search_mode === 'imdb' ? `IMDB:${s.imdb_id || '未填'}` : '', s.resolution, s.quality, s.effects, s.wash_priority]
|
||||
.filter(Boolean)
|
||||
.join(' · ') || '默认'}
|
||||
</td>
|
||||
<td className="text-sand-500">
|
||||
{s.last_run_at ? new Date(s.last_run_at).toLocaleString() : '—'}
|
||||
</td>
|
||||
<td className="space-x-2 py-2 text-right">
|
||||
<button
|
||||
className="rounded-lg border border-gray-300 px-2 py-1 text-xs text-ink-100 hover:bg-gray-50"
|
||||
onClick={() => startEdit(s)}
|
||||
>
|
||||
<Pencil size={12} />
|
||||
</button>
|
||||
<button
|
||||
className="rounded-lg border border-primary-400/40 px-2 py-1 text-xs text-brand-500 hover:bg-primary-400/10"
|
||||
onClick={async () => {
|
||||
|
||||
@@ -176,6 +176,15 @@ export interface Subscription {
|
||||
media_type?: string
|
||||
media_category?: string
|
||||
save_path?: string
|
||||
search_mode?: string
|
||||
imdb_id?: string
|
||||
resolution?: string
|
||||
quality?: string
|
||||
effects?: string
|
||||
release_groups?: string
|
||||
exclude_words?: string
|
||||
wash_priority?: string
|
||||
priority?: number
|
||||
enabled: boolean
|
||||
last_run_at?: string
|
||||
created_at: string
|
||||
|
||||
Reference in New Issue
Block a user