diff --git a/public/app.js b/public/app.js index cf28451..0016d07 100644 --- a/public/app.js +++ b/public/app.js @@ -556,7 +556,7 @@ function createDownloadCard(download) { header.appendChild(issueBadge); } - if (isAdmin && download.arrQueueId) { + if ((isAdmin || download.canBlocklist) && download.arrQueueId) { const blBtn = document.createElement('button'); blBtn.className = 'blocklist-search-btn'; blBtn.textContent = '⛔ Blocklist & Search'; diff --git a/server/routes/dashboard.js b/server/routes/dashboard.js index 321ae45..290b42c 100644 --- a/server/routes/dashboard.js +++ b/server/routes/dashboard.js @@ -94,6 +94,23 @@ function getRadarrLink(movie) { return `${movie._instanceUrl}/movie/${movie.titleSlug}`; } +// Determine if a download can be blocklisted by the current user +// Admins: always true (they have arrQueueId) +// Non-admins: true if importIssues OR (torrent >1h old AND availability<100%) +function canBlocklist(download, isAdmin) { + if (isAdmin) return true; + if (download.importIssues && download.importIssues.length > 0) return true; + if (download.qbittorrent && download.addedOn && download.availability) { + const oneHourAgo = Date.now() - 3600000; // 1 hour in ms + const addedOn = new Date(download.addedOn).getTime(); + const isOldEnough = addedOn < oneHourAgo; + const availability = parseFloat(download.availability); + const isLowAvailability = availability < 100; + return isOldEnough && isLowAvailability; + } + return false; +} + // Extract episode info from a Sonarr queue/history record. // Returns { season, episode, title } or null if data is missing. function extractEpisode(record) { @@ -328,6 +345,7 @@ router.get('/user-downloads', requireAuth, async (req, res) => { dlObj.arrContentId = sonarrMatch.episodeId || null; dlObj.arrContentType = 'episode'; } + dlObj.canBlocklist = canBlocklist(dlObj, isAdmin); userDownloads.push(dlObj); } } @@ -376,6 +394,7 @@ router.get('/user-downloads', requireAuth, async (req, res) => { dlObj.arrContentId = radarrMatch.movieId || null; dlObj.arrContentType = 'movie'; } + dlObj.canBlocklist = canBlocklist(dlObj, isAdmin); userDownloads.push(dlObj); } } @@ -539,6 +558,7 @@ router.get('/user-downloads', requireAuth, async (req, res) => { download.arrContentId = sonarrMatch.episodeId || null; download.arrContentType = 'episode'; } + download.canBlocklist = canBlocklist(download, isAdmin); userDownloads.push(download); continue; // Skip to next torrent } @@ -580,6 +600,7 @@ router.get('/user-downloads', requireAuth, async (req, res) => { download.arrContentId = radarrMatch.movieId || null; download.arrContentType = 'movie'; } + download.canBlocklist = canBlocklist(download, isAdmin); userDownloads.push(download); continue; // Skip to next torrent } @@ -918,6 +939,7 @@ router.get('/stream', requireAuth, async (req, res) => { const issues = getImportIssues(sonarrMatch); if (issues) dlObj.importIssues = issues; if (isAdmin) { dlObj.downloadPath = slot.storage || null; dlObj.targetPath = series.path || null; dlObj.arrLink = getSonarrLink(series); dlObj.arrQueueId = sonarrMatch.id; dlObj.arrType = 'sonarr'; dlObj.arrInstanceUrl = sonarrMatch._instanceUrl || null; dlObj.arrInstanceKey = sonarrMatch._instanceKey || null; dlObj.arrContentId = sonarrMatch.episodeId || null; dlObj.arrContentType = 'episode'; } + dlObj.canBlocklist = canBlocklist(dlObj, isAdmin); userDownloads.push(dlObj); } } @@ -937,6 +959,7 @@ router.get('/stream', requireAuth, async (req, res) => { const issues = getImportIssues(radarrMatch); if (issues) dlObj.importIssues = issues; if (isAdmin) { dlObj.downloadPath = slot.storage || null; dlObj.targetPath = movie.path || null; dlObj.arrLink = getRadarrLink(movie); dlObj.arrQueueId = radarrMatch.id; dlObj.arrType = 'radarr'; dlObj.arrInstanceUrl = radarrMatch._instanceUrl || null; dlObj.arrInstanceKey = radarrMatch._instanceKey || null; dlObj.arrContentId = radarrMatch.movieId || null; dlObj.arrContentType = 'movie'; } + dlObj.canBlocklist = canBlocklist(dlObj, isAdmin); userDownloads.push(dlObj); } } @@ -1004,6 +1027,7 @@ router.get('/stream', requireAuth, async (req, res) => { Object.assign(download, { type: 'series', coverArt: getCoverArt(series), seriesName: series.title, episodes: gatherEpisodes(torrentNameLower, sonarrQueue.data.records), allTags, matchedUserTag: matchedUserTag || null, tagBadges: showAll ? buildTagBadges(allTags, embyUserMap) : undefined }); const issues = getImportIssues(sonarrMatch); if (issues) download.importIssues = issues; if (isAdmin) { download.downloadPath = download.savePath || null; download.targetPath = series.path || null; download.arrLink = getSonarrLink(series); download.arrQueueId = sonarrMatch.id; download.arrType = 'sonarr'; download.arrInstanceUrl = sonarrMatch._instanceUrl || null; download.arrInstanceKey = sonarrMatch._instanceKey || null; download.arrContentId = sonarrMatch.episodeId || null; download.arrContentType = 'episode'; } + download.canBlocklist = canBlocklist(download, isAdmin); userDownloads.push(download); continue; } } @@ -1020,6 +1044,7 @@ router.get('/stream', requireAuth, async (req, res) => { Object.assign(download, { type: 'movie', coverArt: getCoverArt(movie), movieName: movie.title, movieInfo: radarrMatch, allTags, matchedUserTag: matchedUserTag || null, tagBadges: showAll ? buildTagBadges(allTags, embyUserMap) : undefined }); const issues = getImportIssues(radarrMatch); if (issues) download.importIssues = issues; if (isAdmin) { download.downloadPath = download.savePath || null; download.targetPath = movie.path || null; download.arrLink = getRadarrLink(movie); download.arrQueueId = radarrMatch.id; download.arrType = 'radarr'; download.arrInstanceUrl = radarrMatch._instanceUrl || null; download.arrInstanceKey = radarrMatch._instanceKey || null; download.arrContentId = radarrMatch.movieId || null; download.arrContentType = 'movie'; } + download.canBlocklist = canBlocklist(download, isAdmin); userDownloads.push(download); continue; } } diff --git a/server/utils/qbittorrent.js b/server/utils/qbittorrent.js index 7073587..7c218ce 100644 --- a/server/utils/qbittorrent.js +++ b/server/utils/qbittorrent.js @@ -204,6 +204,7 @@ function mapTorrentToDownload(torrent) { category: torrent.category, tags: torrent.tags, savePath: torrent.content_path || torrent.save_path || null, + addedOn: torrent.added_on || null, qbittorrent: true }; }