feat: allow non-admin users to blocklist & search under specific conditions
- Added addedOn timestamp to qBittorrent torrent mapping - Added canBlocklist helper function: true for admins, true for non-admins when (importIssues OR (torrent >1h old AND availability<100%)) - Added canBlocklist field to all download objects in /user-downloads and SSE /stream routes (8 blocks total) - Frontend button now shows when (isAdmin OR download.canBlocklist) && download.arrQueueId
This commit is contained in:
@@ -556,7 +556,7 @@ function createDownloadCard(download) {
|
|||||||
header.appendChild(issueBadge);
|
header.appendChild(issueBadge);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAdmin && download.arrQueueId) {
|
if ((isAdmin || download.canBlocklist) && download.arrQueueId) {
|
||||||
const blBtn = document.createElement('button');
|
const blBtn = document.createElement('button');
|
||||||
blBtn.className = 'blocklist-search-btn';
|
blBtn.className = 'blocklist-search-btn';
|
||||||
blBtn.textContent = '⛔ Blocklist & Search';
|
blBtn.textContent = '⛔ Blocklist & Search';
|
||||||
|
|||||||
@@ -94,6 +94,23 @@ function getRadarrLink(movie) {
|
|||||||
return `${movie._instanceUrl}/movie/${movie.titleSlug}`;
|
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.
|
// Extract episode info from a Sonarr queue/history record.
|
||||||
// Returns { season, episode, title } or null if data is missing.
|
// Returns { season, episode, title } or null if data is missing.
|
||||||
function extractEpisode(record) {
|
function extractEpisode(record) {
|
||||||
@@ -328,6 +345,7 @@ router.get('/user-downloads', requireAuth, async (req, res) => {
|
|||||||
dlObj.arrContentId = sonarrMatch.episodeId || null;
|
dlObj.arrContentId = sonarrMatch.episodeId || null;
|
||||||
dlObj.arrContentType = 'episode';
|
dlObj.arrContentType = 'episode';
|
||||||
}
|
}
|
||||||
|
dlObj.canBlocklist = canBlocklist(dlObj, isAdmin);
|
||||||
userDownloads.push(dlObj);
|
userDownloads.push(dlObj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -376,6 +394,7 @@ router.get('/user-downloads', requireAuth, async (req, res) => {
|
|||||||
dlObj.arrContentId = radarrMatch.movieId || null;
|
dlObj.arrContentId = radarrMatch.movieId || null;
|
||||||
dlObj.arrContentType = 'movie';
|
dlObj.arrContentType = 'movie';
|
||||||
}
|
}
|
||||||
|
dlObj.canBlocklist = canBlocklist(dlObj, isAdmin);
|
||||||
userDownloads.push(dlObj);
|
userDownloads.push(dlObj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -539,6 +558,7 @@ router.get('/user-downloads', requireAuth, async (req, res) => {
|
|||||||
download.arrContentId = sonarrMatch.episodeId || null;
|
download.arrContentId = sonarrMatch.episodeId || null;
|
||||||
download.arrContentType = 'episode';
|
download.arrContentType = 'episode';
|
||||||
}
|
}
|
||||||
|
download.canBlocklist = canBlocklist(download, isAdmin);
|
||||||
userDownloads.push(download);
|
userDownloads.push(download);
|
||||||
continue; // Skip to next torrent
|
continue; // Skip to next torrent
|
||||||
}
|
}
|
||||||
@@ -580,6 +600,7 @@ router.get('/user-downloads', requireAuth, async (req, res) => {
|
|||||||
download.arrContentId = radarrMatch.movieId || null;
|
download.arrContentId = radarrMatch.movieId || null;
|
||||||
download.arrContentType = 'movie';
|
download.arrContentType = 'movie';
|
||||||
}
|
}
|
||||||
|
download.canBlocklist = canBlocklist(download, isAdmin);
|
||||||
userDownloads.push(download);
|
userDownloads.push(download);
|
||||||
continue; // Skip to next torrent
|
continue; // Skip to next torrent
|
||||||
}
|
}
|
||||||
@@ -918,6 +939,7 @@ router.get('/stream', requireAuth, async (req, res) => {
|
|||||||
const issues = getImportIssues(sonarrMatch);
|
const issues = getImportIssues(sonarrMatch);
|
||||||
if (issues) dlObj.importIssues = issues;
|
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'; }
|
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);
|
userDownloads.push(dlObj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -937,6 +959,7 @@ router.get('/stream', requireAuth, async (req, res) => {
|
|||||||
const issues = getImportIssues(radarrMatch);
|
const issues = getImportIssues(radarrMatch);
|
||||||
if (issues) dlObj.importIssues = issues;
|
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'; }
|
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);
|
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 });
|
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;
|
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'; }
|
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;
|
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 });
|
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;
|
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'; }
|
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;
|
userDownloads.push(download); continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ function mapTorrentToDownload(torrent) {
|
|||||||
category: torrent.category,
|
category: torrent.category,
|
||||||
tags: torrent.tags,
|
tags: torrent.tags,
|
||||||
savePath: torrent.content_path || torrent.save_path || null,
|
savePath: torrent.content_path || torrent.save_path || null,
|
||||||
|
addedOn: torrent.added_on || null,
|
||||||
qbittorrent: true
|
qbittorrent: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user