Add quality-aware release picker modal
This commit is contained in:
@@ -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'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>
|
||||
|
||||
Reference in New Issue
Block a user