Add quality-aware release picker modal
Magent CI/CD / verify (push) Canceled after 7m6s
Magent CI/CD / deploy-prod (push) Canceled after 0s
Magent CI/CD / deploy-beta (push) Canceled after 0s

This commit is contained in:
2026-09-01 22:02:22 +12:00
parent 2dbe11e6bc
commit 0ac53b7f59
4 changed files with 341 additions and 31 deletions
+141 -17
View File
@@ -95,6 +95,10 @@ type ReleaseOption = {
magnetUrl?: string
fullSeason?: boolean
seasonNumber?: number
quality?: string
customFormatScore?: number
approved?: boolean
bestPick?: boolean
}
type SnapshotHistory = {
@@ -298,6 +302,9 @@ export default function RequestTimelinePage() {
const [actionError, setActionError] = useState<string | null>(null)
const [busyAction, setBusyAction] = useState<string | null>(null)
const [releaseOptions, setReleaseOptions] = useState<ReleaseOption[]>([])
const [releasePickerOpen, setReleasePickerOpen] = useState(false)
const [releaseCollector, setReleaseCollector] = useState<string | null>(null)
const [releaseSearchMessage, setReleaseSearchMessage] = useState<string | null>(null)
const [historySnapshots, setHistorySnapshots] = useState<SnapshotHistory[]>([])
const [historyActions, setHistoryActions] = useState<ActionHistory[]>([])
const [operationProgress, setOperationProgress] = useState<OperationProgress | null>(null)
@@ -312,6 +319,27 @@ export default function RequestTimelinePage() {
!['complete', 'attention'].includes(snapshot.presentation.repairActivity.state ?? '')
)
const closeReleasePicker = () => {
if (busyAction?.startsWith('grab:')) return
setReleasePickerOpen(false)
setReleaseOptions([])
setReleaseSearchMessage(null)
}
useEffect(() => {
if (!releasePickerOpen) return
const previousOverflow = document.body.style.overflow
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') closeReleasePicker()
}
document.body.style.overflow = 'hidden'
window.addEventListener('keydown', handleKeyDown)
return () => {
document.body.style.overflow = previousOverflow
window.removeEventListener('keydown', handleKeyDown)
}
}, [releasePickerOpen, busyAction])
useEffect(() => {
if (!requestId) return
const load = async () => {
@@ -563,6 +591,8 @@ export default function RequestTimelinePage() {
setActionError(null)
setActionMessage(null)
setReleaseOptions([])
setReleasePickerOpen(false)
setReleaseSearchMessage(null)
try {
const response = await trackedPost(
'Recheck request status',
@@ -603,6 +633,12 @@ export default function RequestTimelinePage() {
setActionError('This action is not connected yet.')
return
}
if (action.id === 'search_releases') {
setReleaseOptions([])
setReleaseCollector(snapshot.request_type === 'tv' ? 'Sonarr' : 'Radarr')
setReleaseSearchMessage(null)
setReleasePickerOpen(true)
}
setBusyAction(action.id)
setActionError(null)
setActionMessage(null)
@@ -621,11 +657,16 @@ export default function RequestTimelinePage() {
if (action.id === 'search_releases') {
const releases = Array.isArray(data.releases) ? data.releases : []
setReleaseOptions(releases)
setActionMessage(
setReleaseCollector(data?.collector ?? (snapshot.request_type === 'tv' ? 'Sonarr' : 'Radarr'))
setReleaseSearchMessage(
data?.message ??
(
releases.length
? `Found ${releases.length} possible release${releases.length === 1 ? '' : 's'}. Choose one below.`
: 'No matching releases were found. Magent will keep the request in the search stage.'
? `Found ${releases.length} approved release${releases.length === 1 ? '' : 's'}.`
: 'No releases currently meet the assigned quality profile.'
)
)
setActionMessage(null)
} else {
setActionMessage(data?.message ?? `${action.label} was started successfully.`)
}
@@ -643,7 +684,6 @@ export default function RequestTimelinePage() {
return
}
const collector = snapshot.request_type === 'tv' ? 'Sonarr' : 'Radarr'
if (!window.confirm(`Send “${release.title ?? 'this release'}” through ${collector}?`)) return
setBusyAction(`grab:${release.guid}`)
setActionError(null)
try {
@@ -662,6 +702,8 @@ export default function RequestTimelinePage() {
const data = await response.json()
setActionMessage(data?.message ?? 'The selected release was queued for download.')
setReleaseOptions([])
setReleasePickerOpen(false)
setReleaseSearchMessage(null)
} catch (error) {
console.error(error)
setActionError(error instanceof Error ? error.message : 'The selected release could not be started.')
@@ -897,21 +939,103 @@ export default function RequestTimelinePage() {
})}
</div>
{releaseOptions.length > 0 && (
<div className="request-release-picker">
<div className="request-release-heading"><div><span className="section-kicker">Manual selection through {snapshot.request_type === 'tv' ? 'Sonarr' : 'Radarr'}</span><h3>Choose a release</h3></div><button type="button" className="ghost-button" onClick={() => setReleaseOptions([])}>Close</button></div>
<div className="request-release-list">
{releaseOptions.map((release) => (
<div className="request-release" key={`${release.guid ?? release.title}`}>
<div><strong>{release.title ?? 'Unknown release'}</strong><span>{release.indexer ?? 'Unknown indexer'} · {release.seeders ?? 0} seeders · {formatBytes(release.size)}{release.fullSeason ? ` · Season ${release.seasonNumber ?? ''} pack` : ''}</span></div>
<button type="button" disabled={Boolean(busyAction) || !release.guid || !release.indexerId} onClick={() => void downloadRelease(release)}>{busyAction === `grab:${release.guid}` ? 'Sending…' : `Send through ${snapshot.request_type === 'tv' ? 'Sonarr' : 'Radarr'}`}</button>
</div>
))}
</div>
</div>
)}
</section>
{releasePickerOpen && (
<div className="request-release-modal-layer">
<button
type="button"
className="request-release-modal-backdrop"
aria-label="Close available releases"
onClick={closeReleasePicker}
/>
<section
className="request-release-modal"
role="dialog"
aria-modal="true"
aria-labelledby="release-picker-title"
>
<header className="request-release-modal-header">
<div>
<span className="section-kicker">Approved by {releaseCollector ?? (snapshot.request_type === 'tv' ? 'Sonarr' : 'Radarr')}</span>
<h2 id="release-picker-title">Choose an available download</h2>
<p>Only releases accepted by the title&apos;s assigned quality profile are shown.</p>
</div>
<button type="button" className="ghost-button" onClick={closeReleasePicker} disabled={busyAction?.startsWith('grab:')}>Close</button>
</header>
<div className="request-release-modal-body">
{busyAction === 'search_releases' && (
<div className="request-release-searching" role="status">
<i aria-hidden="true" />
<div>
<strong>Checking available releases</strong>
<span>{releaseCollector ?? 'The collector'} is applying its quality profile and ranking the results.</span>
</div>
</div>
)}
{busyAction !== 'search_releases' && releaseSearchMessage && (
<div className="request-release-profile-note">
<strong>Quality limits applied</strong>
<span>{releaseSearchMessage}</span>
</div>
)}
{busyAction !== 'search_releases' && actionError && (
<div className="request-release-empty is-error" role="alert">
<strong>The release search could not be completed</strong>
<span>{actionError}</span>
</div>
)}
{busyAction !== 'search_releases' && !actionError && releaseOptions.length === 0 && (
<div className="request-release-empty">
<strong>No suitable downloads are available right now</strong>
<span>{releaseCollector ?? 'The collector'} did not approve anything within the assigned quality limits. Nothing outside those limits has been shown.</span>
</div>
)}
{releaseOptions.length > 0 && (
<div className="request-release-list">
{releaseOptions.map((release, index) => {
const isBestPick = release.bestPick || index === 0
return (
<article className={`request-release ${isBestPick ? 'is-best-pick' : ''}`} key={`${release.indexerId ?? ''}:${release.guid ?? release.title}`}>
<div className="request-release-copy">
<div className="request-release-badges">
{isBestPick && <span className="request-release-best-badge">Best pick</span>}
{release.quality && <span>{release.quality}</span>}
{release.fullSeason && <span>Season {release.seasonNumber ?? ''} pack</span>}
</div>
<strong>{release.title ?? 'Unknown release'}</strong>
<span>
{release.indexer ?? 'Unknown indexer'} · {release.seeders ?? 0} seeders · {formatBytes(release.size)}
{typeof release.customFormatScore === 'number' ? ` · Score ${release.customFormatScore}` : ''}
</span>
{isBestPick && <small>This is the highest-ranked release approved by {releaseCollector ?? 'the collector'}.</small>}
</div>
<button
type="button"
disabled={Boolean(busyAction) || !release.guid || !release.indexerId}
onClick={() => void downloadRelease(release)}
>
{busyAction === `grab:${release.guid}`
? 'Sending…'
: isBestPick
? 'Download best pick'
: 'Download this release'}
</button>
</article>
)
})}
</div>
)}
</div>
</section>
</div>
)}
{isAdmin && <section className="request-advanced">
<button type="button" className="request-advanced-toggle" aria-expanded={showDetails} onClick={() => setShowDetails((current) => !current)}>
<span><strong>Advanced details</strong><small>Service diagnostics, status history and recorded actions</small></span>