This commit is contained in:
Admin9705
2026-02-01 16:13:22 -05:00
parent 10c64c66f8
commit f35a37cea1
10 changed files with 652 additions and 6 deletions

View File

@@ -2404,6 +2404,13 @@ let huntarrUI = {
// Initialize when document is ready
document.addEventListener('DOMContentLoaded', function() {
// Initialize TMDB image cache first
if (typeof tmdbImageCache !== 'undefined') {
tmdbImageCache.init().catch(error => {
console.error('[app.js] Failed to initialize TMDB image cache:', error);
});
}
huntarrUI.init();
// Initialize our enhanced UI features

View File

@@ -862,6 +862,20 @@ export class RequestarrContent {
</div>
`;
// Load and cache image asynchronously after card is created
if (posterUrl && !posterUrl.includes('./static/images/') && window.getCachedTMDBImage && window.tmdbImageCache) {
const imgElement = card.querySelector('.media-card-poster img');
if (imgElement) {
window.getCachedTMDBImage(posterUrl, window.tmdbImageCache).then(cachedUrl => {
if (cachedUrl && cachedUrl !== posterUrl) {
imgElement.src = cachedUrl;
}
}).catch(err => {
console.error('[RequestarrContent] Failed to cache image:', err);
});
}
}
const posterDiv = card.querySelector('.media-card-poster');
const requestBtn = card.querySelector('.media-card-request-btn');
const hideBtn = card.querySelector('.media-card-hide-btn');

View File

@@ -85,8 +85,10 @@ export class RequestarrModal {
finalDefault: defaultInstance
});
const backdropUrl = data.backdrop_path || '';
let modalHTML = `
<div class="request-modal-header" style="background-image: url(${data.backdrop_path || ''});">
<div class="request-modal-header" style="background-image: url(${backdropUrl});">
<button class="modal-close-btn" onclick="window.RequestarrDiscover.modal.closeModal()">
<i class="fas fa-times"></i>
</button>
@@ -646,6 +648,21 @@ export class RequestarrModal {
}
document.body.appendChild(modal);
// Cache backdrop image asynchronously after modal is in DOM
const backdropUrl = this.core.currentModalData?.backdrop_path;
if (backdropUrl && !backdropUrl.includes('./static/images/') && window.getCachedTMDBImage && window.tmdbImageCache) {
const header = modal.querySelector('.request-modal-header');
if (header) {
window.getCachedTMDBImage(backdropUrl, window.tmdbImageCache).then(cachedUrl => {
if (cachedUrl && cachedUrl !== backdropUrl) {
header.style.backgroundImage = `url(${cachedUrl})`;
}
}).catch(err => {
console.error('[RequestarrModal] Failed to cache backdrop:', err);
});
}
}
}
closeModal() {

View File

@@ -34,9 +34,11 @@ export class RequestarrSettings {
if (data.requests && data.requests.length > 0) {
container.innerHTML = '';
data.requests.forEach(request => {
container.appendChild(this.createHistoryItem(request));
});
// Use Promise.all to wait for all async createHistoryItem calls
const items = await Promise.all(
data.requests.map(request => this.createHistoryItem(request))
);
items.forEach(item => container.appendChild(item));
} else {
container.innerHTML = '<p style="color: #888; text-align: center; padding: 60px;">No request history</p>';
}
@@ -46,7 +48,7 @@ export class RequestarrSettings {
}
}
createHistoryItem(request) {
async createHistoryItem(request) {
const item = document.createElement('div');
item.className = 'history-item';
@@ -66,6 +68,19 @@ export class RequestarrSettings {
</div>
`;
// Load and cache image asynchronously
if (posterUrl && !posterUrl.includes('./static/images/') && window.getCachedTMDBImage && window.tmdbImageCache) {
try {
const cachedUrl = await window.getCachedTMDBImage(posterUrl, window.tmdbImageCache);
if (cachedUrl && cachedUrl !== posterUrl) {
const imgElement = item.querySelector('.history-poster img');
if (imgElement) imgElement.src = cachedUrl;
}
} catch (err) {
console.error('[RequestarrSettings] Failed to cache history image:', err);
}
}
return item;
}
@@ -362,7 +377,7 @@ export class RequestarrSettings {
};
}
createHiddenMediaCard(item) {
async createHiddenMediaCard(item) {
const card = document.createElement('div');
card.className = 'media-card';
card.setAttribute('data-tmdb-id', item.tmdb_id);
@@ -379,6 +394,19 @@ export class RequestarrSettings {
</div>
`;
// Load and cache image asynchronously
if (posterUrl && !posterUrl.includes('./static/images/') && window.getCachedTMDBImage && window.tmdbImageCache) {
try {
const cachedUrl = await window.getCachedTMDBImage(posterUrl, window.tmdbImageCache);
if (cachedUrl && cachedUrl !== posterUrl) {
const imgElement = card.querySelector('.media-card-poster img');
if (imgElement) imgElement.src = cachedUrl;
}
} catch (err) {
console.error('[RequestarrSettings] Failed to cache hidden media image:', err);
}
}
const unhideBtn = card.querySelector('.media-card-unhide-btn');
if (unhideBtn) {
unhideBtn.addEventListener('click', async (e) => {

View File

@@ -757,6 +757,7 @@ window.SettingsForms = {
enable_requestarr: getVal('enable_requestarr', true),
low_usage_mode: getVal('low_usage_mode', true),
show_trending: getVal('show_trending', true),
tmdb_image_cache_days: parseInt(container.querySelector('#tmdb_image_cache_days')?.value || '7'),
auth_mode: (container.querySelector('#auth_mode') && container.querySelector('#auth_mode').value) || 'login',
ssl_verify: getVal('ssl_verify', true),
base_url: getVal('base_url', ''),

View File

@@ -246,6 +246,16 @@
box-shadow: 0 4px 12px rgba(90, 109, 137, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1);
">
<h3>Display Settings</h3>
<div class="setting-item">
<label for="tmdb_image_cache_days"><a href="https://plexguide.github.io/Huntarr.io/settings/settings.html#tmdb-image-cache" class="info-icon" title="Learn more about TMDB image caching" target="_blank" rel="noopener"><i class="fas fa-info-circle"></i></a>TMDB Image Cache:</label>
<select id="tmdb_image_cache_days" class="control-select" style="width: 200px;">
<option value="0" ${settings.tmdb_image_cache_days === 0 ? "selected" : ""}>Disabled (Always Load)</option>
<option value="1" ${settings.tmdb_image_cache_days === 1 ? "selected" : ""}>1 Day</option>
<option value="7" ${(settings.tmdb_image_cache_days === 7 || settings.tmdb_image_cache_days === undefined) ? "selected" : ""}>7 Days</option>
<option value="30" ${settings.tmdb_image_cache_days === 30 ? "selected" : ""}>30 Days</option>
</select>
<p class="setting-help" style="margin-left: -3ch !important;">Cache TMDB images to reduce load times and API usage. Missing images will still attempt to load. Set to "Disabled" to always fetch fresh images.</p>
</div>
<div class="setting-item">
<label for="enable_requestarr">Enable Requestarr:</label>
<label class="toggle-switch" style="width:40px; height:20px; display:inline-block; position:relative;">

View File

@@ -0,0 +1,281 @@
/**
* TMDB Image Cache Utility (Non-Module Version)
* Caches TMDB images in localStorage to reduce API calls and improve load times
*/
(function() {
const CACHE_PREFIX = 'tmdb_img_';
const CACHE_METADATA_KEY = 'tmdb_cache_metadata';
class TMDBImageCache {
constructor() {
this.cacheDays = 7; // Default to 7 days
this.enabled = true;
this.metadata = this.loadMetadata();
}
/**
* Initialize cache with settings from API
*/
async init() {
try {
const response = await fetch('./api/settings');
const data = await response.json();
if (data.success && data.settings && data.settings.general) {
const cacheDays = data.settings.general.tmdb_image_cache_days;
this.cacheDays = cacheDays !== undefined ? cacheDays : 7;
this.enabled = this.cacheDays > 0;
console.log(`[TMDBImageCache] Initialized with ${this.cacheDays} day cache ${this.enabled ? 'enabled' : 'disabled'}`);
}
} catch (error) {
console.error('[TMDBImageCache] Failed to load settings, using defaults:', error);
}
// Clean up expired entries
this.cleanup();
}
/**
* Load cache metadata from localStorage
*/
loadMetadata() {
try {
const stored = localStorage.getItem(CACHE_METADATA_KEY);
return stored ? JSON.parse(stored) : {};
} catch (error) {
console.error('[TMDBImageCache] Failed to load metadata:', error);
return {};
}
}
/**
* Save cache metadata to localStorage
*/
saveMetadata() {
try {
localStorage.setItem(CACHE_METADATA_KEY, JSON.stringify(this.metadata));
} catch (error) {
console.error('[TMDBImageCache] Failed to save metadata:', error);
}
}
/**
* Get cache key for an image URL
*/
getCacheKey(url) {
if (!url) return null;
// Extract just the image filename/path from TMDB URL
const match = url.match(/\/(w\d+)\/(.+)$/);
if (match) {
return `${CACHE_PREFIX}${match[1]}_${match[2]}`;
}
return null;
}
/**
* Check if cached image is still valid
*/
isCacheValid(key) {
if (!this.enabled || this.cacheDays === 0) return false;
const meta = this.metadata[key];
if (!meta || !meta.timestamp) return false;
const now = Date.now();
const age = now - meta.timestamp;
const maxAge = this.cacheDays * 24 * 60 * 60 * 1000; // Convert days to ms
return age < maxAge;
}
/**
* Get cached image URL
*/
get(url) {
if (!this.enabled || this.cacheDays === 0) return null;
const key = this.getCacheKey(url);
if (!key) return null;
if (!this.isCacheValid(key)) {
this.remove(key);
return null;
}
try {
const cached = localStorage.getItem(key);
if (cached) {
console.log(`[TMDBImageCache] Cache HIT: ${url}`);
return cached;
}
} catch (error) {
console.error('[TMDBImageCache] Failed to get cached image:', error);
}
return null;
}
/**
* Cache an image URL
*/
async set(url, imageData) {
if (!this.enabled || this.cacheDays === 0) return;
const key = this.getCacheKey(url);
if (!key) return;
try {
// Store the image data
localStorage.setItem(key, imageData);
// Update metadata
this.metadata[key] = {
timestamp: Date.now(),
url: url
};
this.saveMetadata();
console.log(`[TMDBImageCache] Cached: ${url}`);
} catch (error) {
// If we hit storage quota, try to cleanup old entries
if (error.name === 'QuotaExceededError') {
console.warn('[TMDBImageCache] Storage quota exceeded, cleaning up...');
this.cleanup(true);
// Try again after cleanup
try {
localStorage.setItem(key, imageData);
this.metadata[key] = {
timestamp: Date.now(),
url: url
};
this.saveMetadata();
} catch (retryError) {
console.error('[TMDBImageCache] Failed to cache even after cleanup:', retryError);
}
} else {
console.error('[TMDBImageCache] Failed to cache image:', error);
}
}
}
/**
* Remove a cached image
*/
remove(key) {
try {
localStorage.removeItem(key);
delete this.metadata[key];
this.saveMetadata();
} catch (error) {
console.error('[TMDBImageCache] Failed to remove cached image:', error);
}
}
/**
* Clean up expired cache entries
*/
cleanup(force = false) {
try {
const keys = Object.keys(this.metadata);
let removed = 0;
for (const key of keys) {
if (force || !this.isCacheValid(key)) {
this.remove(key);
removed++;
}
}
if (removed > 0) {
console.log(`[TMDBImageCache] Cleaned up ${removed} expired entries`);
}
} catch (error) {
console.error('[TMDBImageCache] Failed to cleanup cache:', error);
}
}
/**
* Clear all cached images
*/
clearAll() {
try {
const keys = Object.keys(this.metadata);
for (const key of keys) {
localStorage.removeItem(key);
}
this.metadata = {};
this.saveMetadata();
console.log('[TMDBImageCache] Cleared all cached images');
} catch (error) {
console.error('[TMDBImageCache] Failed to clear cache:', error);
}
}
/**
* Get cache statistics
*/
getStats() {
const entries = Object.keys(this.metadata).length;
let totalSize = 0;
try {
for (const key of Object.keys(this.metadata)) {
const data = localStorage.getItem(key);
if (data) {
totalSize += data.length;
}
}
} catch (error) {
console.error('[TMDBImageCache] Failed to calculate cache size:', error);
}
return {
entries,
totalSizeKB: Math.round(totalSize / 1024),
cacheDays: this.cacheDays,
enabled: this.enabled
};
}
}
/**
* Get cached TMDB image or fetch and cache it
*/
async function getCachedTMDBImage(url, cache) {
if (!url || !cache) return url;
// Check cache first
const cached = cache.get(url);
if (cached) return cached;
// If not cached or cache disabled, fetch and cache
try {
const response = await fetch(url);
if (response.ok) {
const blob = await response.blob();
const reader = new FileReader();
return new Promise((resolve, reject) => {
reader.onloadend = () => {
const base64 = reader.result;
// Cache the base64 data
cache.set(url, base64);
resolve(base64);
};
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
} catch (error) {
console.error('[TMDBImageCache] Failed to fetch image:', error);
}
// Return original URL if fetch fails
return url;
}
// Create singleton instance and make it globally available
window.tmdbImageCache = new TMDBImageCache();
window.getCachedTMDBImage = getCachedTMDBImage;
})();

View File

@@ -0,0 +1,284 @@
/**
* TMDB Image Cache Utility
* Caches TMDB images in localStorage to reduce API calls and improve load times
*/
const CACHE_PREFIX = 'tmdb_img_';
const CACHE_METADATA_KEY = 'tmdb_cache_metadata';
export class TMDBImageCache {
constructor() {
this.cacheDays = 7; // Default to 7 days
this.enabled = true;
this.metadata = this.loadMetadata();
}
/**
* Initialize cache with settings from API
*/
async init() {
try {
const response = await fetch('./api/settings');
const data = await response.json();
if (data.success && data.settings && data.settings.general) {
const cacheDays = data.settings.general.tmdb_image_cache_days;
this.cacheDays = cacheDays !== undefined ? cacheDays : 7;
this.enabled = this.cacheDays > 0;
console.log(`[TMDBImageCache] Initialized with ${this.cacheDays} day cache ${this.enabled ? 'enabled' : 'disabled'}`);
}
} catch (error) {
console.error('[TMDBImageCache] Failed to load settings, using defaults:', error);
}
// Clean up expired entries
this.cleanup();
}
/**
* Load cache metadata from localStorage
*/
loadMetadata() {
try {
const stored = localStorage.getItem(CACHE_METADATA_KEY);
return stored ? JSON.parse(stored) : {};
} catch (error) {
console.error('[TMDBImageCache] Failed to load metadata:', error);
return {};
}
}
/**
* Save cache metadata to localStorage
*/
saveMetadata() {
try {
localStorage.setItem(CACHE_METADATA_KEY, JSON.stringify(this.metadata));
} catch (error) {
console.error('[TMDBImageCache] Failed to save metadata:', error);
}
}
/**
* Get cache key for an image URL
*/
getCacheKey(url) {
if (!url) return null;
// Extract just the image filename/path from TMDB URL
const match = url.match(/\/(w\d+)\/(.+)$/);
if (match) {
return `${CACHE_PREFIX}${match[1]}_${match[2]}`;
}
return null;
}
/**
* Check if cached image is still valid
*/
isCacheValid(key) {
if (!this.enabled || this.cacheDays === 0) return false;
const meta = this.metadata[key];
if (!meta || !meta.timestamp) return false;
const now = Date.now();
const age = now - meta.timestamp;
const maxAge = this.cacheDays * 24 * 60 * 60 * 1000; // Convert days to ms
return age < maxAge;
}
/**
* Get cached image URL
*/
get(url) {
if (!this.enabled || this.cacheDays === 0) return null;
const key = this.getCacheKey(url);
if (!key) return null;
if (!this.isCacheValid(key)) {
this.remove(key);
return null;
}
try {
const cached = localStorage.getItem(key);
if (cached) {
console.log(`[TMDBImageCache] Cache HIT: ${url}`);
return cached;
}
} catch (error) {
console.error('[TMDBImageCache] Failed to get cached image:', error);
}
return null;
}
/**
* Cache an image URL
*/
async set(url, imageData) {
if (!this.enabled || this.cacheDays === 0) return;
const key = this.getCacheKey(url);
if (!key) return;
try {
// Store the image data
localStorage.setItem(key, imageData);
// Update metadata
this.metadata[key] = {
timestamp: Date.now(),
url: url
};
this.saveMetadata();
console.log(`[TMDBImageCache] Cached: ${url}`);
} catch (error) {
// If we hit storage quota, try to cleanup old entries
if (error.name === 'QuotaExceededError') {
console.warn('[TMDBImageCache] Storage quota exceeded, cleaning up...');
this.cleanup(true);
// Try again after cleanup
try {
localStorage.setItem(key, imageData);
this.metadata[key] = {
timestamp: Date.now(),
url: url
};
this.saveMetadata();
} catch (retryError) {
console.error('[TMDBImageCache] Failed to cache even after cleanup:', retryError);
}
} else {
console.error('[TMDBImageCache] Failed to cache image:', error);
}
}
}
/**
* Remove a cached image
*/
remove(key) {
try {
localStorage.removeItem(key);
delete this.metadata[key];
this.saveMetadata();
} catch (error) {
console.error('[TMDBImageCache] Failed to remove cached image:', error);
}
}
/**
* Clean up expired cache entries
*/
cleanup(force = false) {
try {
const keys = Object.keys(this.metadata);
let removed = 0;
for (const key of keys) {
if (force || !this.isCacheValid(key)) {
this.remove(key);
removed++;
}
}
if (removed > 0) {
console.log(`[TMDBImageCache] Cleaned up ${removed} expired entries`);
}
} catch (error) {
console.error('[TMDBImageCache] Failed to cleanup cache:', error);
}
}
/**
* Clear all cached images
*/
clearAll() {
try {
const keys = Object.keys(this.metadata);
for (const key of keys) {
localStorage.removeItem(key);
}
this.metadata = {};
this.saveMetadata();
console.log('[TMDBImageCache] Cleared all cached images');
} catch (error) {
console.error('[TMDBImageCache] Failed to clear cache:', error);
}
}
/**
* Get cache statistics
*/
getStats() {
const entries = Object.keys(this.metadata).length;
let totalSize = 0;
try {
for (const key of Object.keys(this.metadata)) {
const data = localStorage.getItem(key);
if (data) {
totalSize += data.length;
}
}
} catch (error) {
console.error('[TMDBImageCache] Failed to calculate cache size:', error);
}
return {
entries,
totalSizeKB: Math.round(totalSize / 1024),
cacheDays: this.cacheDays,
enabled: this.enabled
};
}
}
/**
* Get cached TMDB image or fetch and cache it
*/
export async function getCachedTMDBImage(url, cache) {
if (!url || !cache) return url;
// Check cache first
const cached = cache.get(url);
if (cached) return cached;
// If not cached or cache disabled, fetch and cache
try {
const response = await fetch(url);
if (response.ok) {
const blob = await response.blob();
const reader = new FileReader();
return new Promise((resolve, reject) => {
reader.onloadend = () => {
const base64 = reader.result;
// Cache the base64 data
cache.set(url, base64);
resolve(base64);
};
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
} catch (error) {
console.error('[TMDBImageCache] Failed to fetch image:', error);
}
// Return original URL if fetch fails
return url;
}
// Create singleton instance
export const tmdbImageCache = new TMDBImageCache();
// Make it globally available for non-module scripts
if (typeof window !== 'undefined') {
window.tmdbImageCache = tmdbImageCache;
window.getCachedTMDBImage = getCachedTMDBImage;
}

View File

@@ -55,6 +55,9 @@
<!-- Movie Hunt (movies only, standalone from Requestarr) -->
<script src="./static/js/modules/features/movie-hunt.js"></script>
<!-- TMDB Image Cache Utility (Standalone - must load before Requestarr modules) -->
<script src="./static/js/modules/utils/tmdb-image-cache-standalone.js"></script>
<!-- Requestarr System -->
<script src="./static/js/modules/features/requestarr/requestarr-controller.js" type="module"></script>
<script src="./static/js/modules/features/requestarr/requestarr-home.js" type="module"></script>

View File

@@ -173,6 +173,7 @@ SWAPARR_DEFAULTS = {
# General settings default configuration
GENERAL_DEFAULTS = {
"tmdb_image_cache_days": 7,
"display_community_resources": True,
"display_huntarr_support": True,
"log_refresh_interval_seconds": 30,