mirror of
https://github.com/plexguide/Huntarr.io.git
synced 2026-04-20 11:06:53 -04:00
330 lines
14 KiB
JavaScript
330 lines
14 KiB
JavaScript
/**
|
|
* MediaUtils — Shared utility functions for media cards across Movie Hunt and Requestarr.
|
|
* Consolidates duplicated logic into a single source of truth.
|
|
*
|
|
* Provides:
|
|
* - hideMedia() — Hide media from discovery with confirmation + animation
|
|
* - getStatusBadge() — Consistent status badge HTML
|
|
* - animateCardRemoval() — Shared card removal animation
|
|
* - resolveMovieInstance() — Resolve instance info from various sources
|
|
*/
|
|
(function() {
|
|
'use strict';
|
|
|
|
/* ── Card removal animation (used by hide, delete, etc.) ── */
|
|
function animateCardRemoval(cardElement, callback) {
|
|
if (!cardElement) {
|
|
if (typeof callback === 'function') callback();
|
|
return;
|
|
}
|
|
cardElement.style.transition = 'opacity 0.3s, transform 0.3s';
|
|
cardElement.style.opacity = '0';
|
|
cardElement.style.transform = 'scale(0.8)';
|
|
setTimeout(function() {
|
|
cardElement.remove();
|
|
if (typeof callback === 'function') callback();
|
|
}, 300);
|
|
}
|
|
|
|
/* ── Status badge HTML ── */
|
|
/**
|
|
* Returns status badge HTML for a media card.
|
|
* @param {boolean} inLibrary - Item is fully available (downloaded)
|
|
* @param {boolean} partial - Item is requested/monitored but not downloaded
|
|
* @param {boolean} hasInstance - An instance is configured
|
|
* @returns {string} HTML string
|
|
*/
|
|
function getStatusBadge(inLibrary, partial, hasInstance, importable, pending) {
|
|
if (!hasInstance) return '';
|
|
if (inLibrary) {
|
|
return '<div class="media-card-status-badge complete"><i class="fas fa-check"></i></div>';
|
|
}
|
|
if (partial) {
|
|
return '<div class="media-card-status-badge partial"><i class="fas fa-bookmark"></i></div>';
|
|
}
|
|
if (pending) {
|
|
return '<div class="media-card-status-badge pending"><i class="fas fa-clock"></i></div>';
|
|
}
|
|
// Non-owner users: show download icon instead of import icon (import is owner-only)
|
|
if (importable && window._huntarrUserRole === 'owner') {
|
|
return '<div class="media-card-status-badge importable" title="Found on disk — importable"><i class="fas fa-file-import"></i></div>';
|
|
}
|
|
return '<div class="media-card-status-badge available"><i class="fas fa-download"></i></div>';
|
|
}
|
|
|
|
/* ── Action button HTML (trash or eye-slash) ── */
|
|
/**
|
|
* Returns the action button HTML for a media card.
|
|
* @param {boolean} inLibrary - Item is fully available
|
|
* @param {boolean} partial - Item is requested
|
|
* @param {boolean} hasInstance - An instance is configured
|
|
* @returns {string} HTML string
|
|
*/
|
|
function getActionButton(inLibrary, partial, hasInstance) {
|
|
if (!hasInstance) return '';
|
|
if (inLibrary || partial) {
|
|
return '<button class="media-card-delete-btn" title="Remove / Delete"><i class="fas fa-trash-alt"></i></button>';
|
|
}
|
|
return '<button class="media-card-hide-btn" title="Hide this media permanently"><i class="fas fa-eye-slash"></i></button>';
|
|
}
|
|
|
|
/* ── Decode compound instance value ── */
|
|
/**
|
|
* Decode a compound instance value like "movie_hunt:MyInstance" or "radarr:MyRadarr".
|
|
* @param {string} value - Compound value
|
|
* @returns {{appType: string, name: string}}
|
|
*/
|
|
function decodeInstanceValue(value) {
|
|
if (!value) return { appType: 'radarr', name: '' };
|
|
var idx = value.indexOf(':');
|
|
if (idx === -1) return { appType: 'radarr', name: value };
|
|
return { appType: value.substring(0, idx), name: value.substring(idx + 1) };
|
|
}
|
|
|
|
/* ── Hide media (single source of truth) ── */
|
|
/**
|
|
* Hide media from discovery pages.
|
|
* @param {Object} options
|
|
* - tmdbId {number|string} - TMDB ID of the media
|
|
* - mediaType {string} - 'movie' or 'tv'
|
|
* - title {string} - Display title
|
|
* - posterPath {string|null} - Poster URL
|
|
* - appType {string} - 'movie_hunt', 'radarr', or 'sonarr'
|
|
* - instanceName {string} - Instance display name
|
|
* - cardElement {HTMLElement} - Card DOM element (for animation)
|
|
* - hiddenMediaSet {Set|null} - Optional Set to add the hidden key to
|
|
* - onHidden {function|null} - Optional callback after successful hide
|
|
*/
|
|
function hideMedia(options) {
|
|
var tmdbId = options.tmdbId;
|
|
var mediaType = options.mediaType || 'movie';
|
|
var title = options.title || 'this media';
|
|
var posterPath = options.posterPath || null;
|
|
var cardElement = options.cardElement || null;
|
|
var hiddenMediaSet = options.hiddenMediaSet || null;
|
|
var onHidden = options.onHidden || null;
|
|
|
|
var doPersonalBlacklist = function() {
|
|
fetch('./api/requestarr/hidden-media', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
tmdb_id: tmdbId,
|
|
media_type: mediaType,
|
|
title: title,
|
|
poster_path: posterPath
|
|
})
|
|
})
|
|
.then(function(r) {
|
|
if (!r.ok) throw new Error('Failed to blacklist media');
|
|
return r.json();
|
|
})
|
|
.then(function() {
|
|
if (hiddenMediaSet) {
|
|
var key = tmdbId + ':' + mediaType;
|
|
hiddenMediaSet.add(key);
|
|
}
|
|
animateCardRemoval(cardElement);
|
|
if (window.huntarrUI && window.huntarrUI.showNotification) {
|
|
window.huntarrUI.showNotification('"' + title + '" added to Personal Blacklist.', 'success');
|
|
}
|
|
if (typeof onHidden === 'function') onHidden();
|
|
})
|
|
.catch(function(err) {
|
|
console.error('[MediaUtils] Error blacklisting media:', err);
|
|
if (window.huntarrUI && window.huntarrUI.showNotification) {
|
|
window.huntarrUI.showNotification('Failed to blacklist media.', 'error');
|
|
}
|
|
});
|
|
};
|
|
|
|
var doGlobalBlacklist = function() {
|
|
fetch('./api/requestarr/requests/global-blacklist', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
tmdb_id: tmdbId,
|
|
media_type: mediaType,
|
|
title: title,
|
|
poster_path: posterPath,
|
|
year: options.year || ''
|
|
})
|
|
})
|
|
.then(function(r) {
|
|
if (!r.ok) throw new Error('Failed to globally blacklist media');
|
|
return r.json();
|
|
})
|
|
.then(function() {
|
|
animateCardRemoval(cardElement);
|
|
if (window.huntarrUI && window.huntarrUI.showNotification) {
|
|
window.huntarrUI.showNotification('"' + title + '" added to Global Blacklist.', 'success');
|
|
}
|
|
if (typeof onHidden === 'function') onHidden();
|
|
})
|
|
.catch(function(err) {
|
|
console.error('[MediaUtils] Error globally blacklisting media:', err);
|
|
if (window.huntarrUI && window.huntarrUI.showNotification) {
|
|
window.huntarrUI.showNotification('Failed to globally blacklist media.', 'error');
|
|
}
|
|
});
|
|
};
|
|
|
|
var isOwner = window._huntarrUserRole === 'owner';
|
|
|
|
if (window.HuntarrConfirm && window.HuntarrConfirm.show) {
|
|
window.HuntarrConfirm.show({
|
|
title: 'Blacklist Media',
|
|
message: 'Blacklist "' + title + '"?\n\nPersonal blacklist hides it from your discovery pages.\n' + (isOwner ? 'Global blacklist hides it for all users and blocks requests.' : ''),
|
|
confirmLabel: 'Personal Blacklist',
|
|
onConfirm: doPersonalBlacklist,
|
|
extraButton: isOwner ? { label: 'Global Blacklist', className: 'danger', onClick: doGlobalBlacklist } : null
|
|
});
|
|
} else {
|
|
doPersonalBlacklist();
|
|
}
|
|
}
|
|
|
|
/* ── Resolve movie instance info from various DOM/data sources ── */
|
|
/**
|
|
* Resolve instance details for a movie, trying multiple sources.
|
|
* @param {Object} options
|
|
* - selectElementId {string} - DOM select element ID to read from
|
|
* - compoundValue {string|null} - Compound value like "movie_hunt:Name"
|
|
* - instancePool {Array|null} - Array of {name, id} objects to search
|
|
* - movieHuntInstances {Array|null} - Fallback Movie Hunt instances
|
|
* - radarrInstances {Array|null} - Fallback Radarr instances
|
|
* @returns {{appType: string, instanceName: string, instanceId: string|number}}
|
|
*/
|
|
function resolveMovieInstance(options) {
|
|
options = options || {};
|
|
|
|
// Try compound value first (Requestarr pattern)
|
|
if (options.compoundValue) {
|
|
var decoded = decodeInstanceValue(options.compoundValue);
|
|
var result = { appType: decoded.appType, instanceName: decoded.name, instanceId: '' };
|
|
|
|
// Try to find numeric ID from pool
|
|
var pool = options.instancePool || [];
|
|
var match = pool.find(function(i) { return i.name === decoded.name; });
|
|
if (match) result.instanceId = match.id || '';
|
|
return result;
|
|
}
|
|
|
|
// Try DOM select element (Movie Hunt pattern)
|
|
if (options.selectElementId) {
|
|
var select = document.getElementById(options.selectElementId);
|
|
if (select && select.value) {
|
|
var name = select.options && select.options[select.selectedIndex]
|
|
? select.options[select.selectedIndex].textContent
|
|
: select.value;
|
|
return { appType: 'movie_hunt', instanceName: name, instanceId: select.value };
|
|
}
|
|
}
|
|
|
|
// Fallback: first available instance
|
|
var mh = options.movieHuntInstances || [];
|
|
var rr = options.radarrInstances || [];
|
|
if (mh.length > 0) return { appType: 'movie_hunt', instanceName: mh[0].name || '', instanceId: mh[0].id || '' };
|
|
if (rr.length > 0) return { appType: 'radarr', instanceName: rr[0].name || '', instanceId: rr[0].id || '' };
|
|
|
|
return { appType: 'movie_hunt', instanceName: '', instanceId: '' };
|
|
}
|
|
|
|
/* ── Detail page refresh-after-action event system ── */
|
|
/*
|
|
* Single source of truth for:
|
|
* - Listening for request-success events (from Requestarr modal)
|
|
* - Listening for status-changed events (from edit save, delete, etc.)
|
|
* - Dispatching status-changed events
|
|
* - Delayed re-fetch pattern (immediate + 3s + 8s for fast downloads)
|
|
*
|
|
* Both Movie Hunt detail and Requestarr detail call setupDetailRefreshListeners()
|
|
* with their own refreshCallback. The logic lives here once.
|
|
*/
|
|
|
|
/**
|
|
* Set up event listeners that auto-refresh a detail page after user actions.
|
|
* Call this from setupDetailInteractions() in any detail page module.
|
|
*
|
|
* @param {Object} options
|
|
* - getTmdbId {function} — returns the current movie's TMDB ID (called on each event)
|
|
* - refreshCallback {function} — called to refresh the detail page status/toolbar
|
|
* - label {string} — log label, e.g. 'RequestarrDetail' or 'RequestarrTVDetail'
|
|
* @returns {Object} handle — pass to teardownDetailRefreshListeners() on cleanup
|
|
*/
|
|
function setupDetailRefreshListeners(options) {
|
|
var getTmdbId = options.getTmdbId;
|
|
var refreshCb = options.refreshCallback;
|
|
var label = options.label || 'DetailPage';
|
|
|
|
function onRequestSuccess(e) {
|
|
var detail = e.detail || {};
|
|
var myId = getTmdbId();
|
|
if (!myId || String(detail.tmdbId) !== String(myId)) return;
|
|
|
|
console.log('[' + label + '] Request succeeded, refreshing status...');
|
|
refreshCb();
|
|
setTimeout(function() { refreshCb(); }, 3000);
|
|
setTimeout(function() { refreshCb(); }, 8000);
|
|
}
|
|
|
|
function onStatusChanged(e) {
|
|
var detail = e.detail || {};
|
|
var myId = getTmdbId();
|
|
if (!myId || String(detail.tmdbId) !== String(myId)) return;
|
|
|
|
console.log('[' + label + '] Status changed (' + (detail.action || '?') + '), refreshing...');
|
|
refreshCb();
|
|
}
|
|
|
|
window.addEventListener('requestarr-request-success', onRequestSuccess);
|
|
window.addEventListener('media-status-changed', onStatusChanged);
|
|
|
|
return { _reqHandler: onRequestSuccess, _statusHandler: onStatusChanged };
|
|
}
|
|
|
|
/**
|
|
* Remove listeners created by setupDetailRefreshListeners().
|
|
* @param {Object} handle — the return value from setupDetailRefreshListeners()
|
|
*/
|
|
function teardownDetailRefreshListeners(handle) {
|
|
if (!handle) return;
|
|
if (handle._reqHandler) window.removeEventListener('requestarr-request-success', handle._reqHandler);
|
|
if (handle._statusHandler) window.removeEventListener('media-status-changed', handle._statusHandler);
|
|
}
|
|
|
|
/**
|
|
* Dispatch a status-changed event so all listening detail pages refresh.
|
|
* Call this after edit-save, force-search, force-upgrade, delete, etc.
|
|
*
|
|
* @param {number|string} tmdbId
|
|
* @param {string} action — e.g. 'edit', 'force-search', 'force-upgrade', 'delete'
|
|
*/
|
|
function dispatchStatusChanged(tmdbId, action) {
|
|
window.dispatchEvent(new CustomEvent('media-status-changed', {
|
|
detail: { tmdbId: tmdbId, action: action || 'unknown' }
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Encode a compound instance value: "appType:instanceName"
|
|
*/
|
|
function encodeInstanceValue(appType, name) {
|
|
return appType + ':' + name;
|
|
}
|
|
|
|
// Export to window
|
|
window.MediaUtils = {
|
|
hideMedia: hideMedia,
|
|
getStatusBadge: getStatusBadge,
|
|
getActionButton: getActionButton,
|
|
animateCardRemoval: animateCardRemoval,
|
|
encodeInstanceValue: encodeInstanceValue,
|
|
decodeInstanceValue: decodeInstanceValue,
|
|
resolveMovieInstance: resolveMovieInstance,
|
|
setupDetailRefreshListeners: setupDetailRefreshListeners,
|
|
teardownDetailRefreshListeners: teardownDetailRefreshListeners,
|
|
dispatchStatusChanged: dispatchStatusChanged
|
|
};
|
|
})();
|