This commit is contained in:
Admin9705
2026-02-06 22:58:26 -05:00
parent 3cce40eaeb
commit 536365e749
5 changed files with 611 additions and 73 deletions

View File

@@ -208,7 +208,7 @@
/* Instance Selector */
.movie-detail-instance-selector {
display: flex;
display: inline-flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
@@ -217,6 +217,7 @@
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10px);
max-width: 400px;
}
.movie-detail-instance-selector label {
@@ -227,6 +228,7 @@
align-items: center;
gap: 8px;
margin: 0;
white-space: nowrap;
}
.movie-detail-instance-selector label i {
@@ -243,6 +245,7 @@
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
min-width: 200px;
}
.movie-detail-select:hover {

View File

@@ -8,8 +8,8 @@
window.MovieHuntDetail = {
currentMovie: null,
tmdbApiKey: null,
radarrInstances: [],
selectedInstance: null,
movieHuntInstances: [],
selectedInstanceId: null,
init() {
console.log('[MovieHuntDetail] Module initialized');
@@ -18,9 +18,9 @@
window.addEventListener('popstate', (e) => {
if (e.state && e.state.movieDetail) {
// User navigated to a movie detail via back/forward
this.openDetail(e.state.movieDetail, e.state.options || {}, true);
} else {
// User navigated away from movie detail
this.openDetail(e.state.movieDetail, {}, true);
} else if (!e.state || !e.state.requestarrMovieDetail) {
// User navigated away from movie detail (and not to Requestarr detail)
this.closeDetail(true);
}
});
@@ -68,21 +68,20 @@
},
/**
* Open detail view for a movie
* Open detail view for a movie from Movie Hunt
* @param {Object} movie - Movie data with at least title, year, tmdb_id
* @param {Object} options - Optional config: { source: 'movie-hunt' | 'requestarr', suggestedInstance: string }
* @param {Object} options - Optional config (unused, kept for compatibility)
* @param {Boolean} fromHistory - True if opened from browser history (don't push state again)
*/
async openDetail(movie, options = {}, fromHistory = false) {
if (!movie) return;
this.currentMovie = movie;
this.options = options || {};
console.log('[MovieHuntDetail] Opening detail for:', movie.title, 'from', this.options.source || 'unknown');
console.log('[MovieHuntDetail] Opening detail for:', movie.title);
// Load Radarr instances if not already loaded
if (this.radarrInstances.length === 0) {
await this.loadRadarrInstances();
// Load Movie Hunt instances if not already loaded
if (this.movieHuntInstances.length === 0) {
await this.loadMovieHuntInstances();
}
// Get or create detail view container
@@ -104,7 +103,7 @@
const tmdbId = movie.tmdb_id || movie.id;
const url = `${window.location.pathname}${window.location.search}#movie/${tmdbId}`;
history.pushState(
{ movieDetail: movie, options: this.options },
{ movieDetail: movie },
movie.title,
url
);
@@ -124,6 +123,7 @@
const details = await this.fetchMovieDetails(tmdbId);
if (details) {
console.log('[MovieHuntDetail] Rendering detail with instances:', this.movieHuntInstances.length);
detailView.innerHTML = this.renderMovieDetail(details, movie);
this.setupDetailInteractions();
} else {
@@ -246,20 +246,30 @@
}
}
// Build instance selector HTML
// Build Movie Hunt instance selector (always show, even if loading)
let instanceSelectorHTML = '';
if (this.radarrInstances.length > 0) {
if (this.movieHuntInstances.length > 0) {
instanceSelectorHTML = `
<div class="movie-detail-instance-selector">
<label for="movie-detail-instance-select"><i class="fas fa-server"></i> Instance:</label>
<select id="movie-detail-instance-select" class="movie-detail-select">
${this.radarrInstances.map(instance => {
const selected = instance.name === this.selectedInstance ? 'selected' : '';
return `<option value="${this.escapeHtml(instance.name)}" ${selected}>Radarr - ${this.escapeHtml(instance.name)}</option>`;
${this.movieHuntInstances.map(instance => {
const selected = instance.id === this.selectedInstanceId ? 'selected' : '';
return `<option value="${instance.id}" ${selected}>Movie Hunt - ${this.escapeHtml(instance.name)}</option>`;
}).join('')}
</select>
</div>
`;
} else {
// Show loading state if instances haven't loaded yet
instanceSelectorHTML = `
<div class="movie-detail-instance-selector">
<label for="movie-detail-instance-select"><i class="fas fa-server"></i> Instance:</label>
<select id="movie-detail-instance-select" class="movie-detail-select" disabled>
<option>Loading instances...</option>
</select>
</div>
`;
}
return `
@@ -395,8 +405,8 @@
const instanceSelect = document.getElementById('movie-detail-instance-select');
if (instanceSelect) {
instanceSelect.addEventListener('change', async () => {
this.selectedInstance = instanceSelect.value;
console.log('[MovieHuntDetail] Instance changed to:', this.selectedInstance);
this.selectedInstanceId = parseInt(instanceSelect.value);
console.log('[MovieHuntDetail] Instance changed to:', this.selectedInstanceId);
// Update movie status for new instance
await this.updateMovieStatus();
@@ -410,18 +420,7 @@
const requestBtn = document.getElementById('movie-detail-request-btn');
if (requestBtn && this.currentMovie) {
requestBtn.addEventListener('click', () => {
// Don't close detail page - just open the modal on top of it
// If called from Requestarr, use Requestarr modal with selected instance
if (this.options.source === 'requestarr' && window.RequestarrDiscover && window.RequestarrDiscover.modal) {
window.RequestarrDiscover.modal.openModal(
this.currentMovie.tmdb_id,
'movie',
this.selectedInstance
);
}
// Otherwise use Movie Hunt request modal
else if (window.MovieHunt && window.MovieHunt.openMovieHuntRequestModal) {
if (window.MovieHunt && window.MovieHunt.openMovieHuntRequestModal) {
window.MovieHunt.openMovieHuntRequestModal(this.currentMovie);
}
});
@@ -446,11 +445,10 @@
backdrop_path: details.backdrop_path,
overview: details.overview,
vote_average: details.vote_average,
in_library: false,
in_cooldown: false
in_library: false
};
// Open detail and update URL (fromHistory = false)
this.openDetail(movieData, this.options || {}, false);
this.openDetail(movieData, {}, false);
}
} catch (error) {
console.error('[MovieHuntDetail] Error opening similar movie:', error);
@@ -469,55 +467,67 @@
document.addEventListener('keydown', escHandler);
},
async loadRadarrInstances() {
async loadMovieHuntInstances() {
console.log('[MovieHuntDetail] Starting to load Movie Hunt instances...');
try {
const response = await fetch('./api/requestarr/instances/radarr');
const response = await fetch('./api/movie-hunt/instances');
const data = await response.json();
console.log('[MovieHuntDetail] Instances API response:', data);
if (data.instances && data.instances.length > 0) {
this.radarrInstances = data.instances;
this.movieHuntInstances = data.instances;
// Set initial selected instance (prioritize suggested instance from options)
if (this.options.suggestedInstance) {
this.selectedInstance = this.options.suggestedInstance;
} else if (!this.selectedInstance) {
this.selectedInstance = this.radarrInstances[0].name;
// Set initial selected instance (Movie Hunt uses integer IDs)
if (!this.selectedInstanceId) {
// Get current instance from server
const currentResponse = await fetch('./api/movie-hunt/current-instance');
const currentData = await currentResponse.json();
this.selectedInstanceId = currentData.instance_id || this.movieHuntInstances[0].id;
}
console.log('[MovieHuntDetail] Loaded', this.radarrInstances.length, 'Radarr instances, selected:', this.selectedInstance);
console.log('[MovieHuntDetail] Loaded', this.movieHuntInstances.length, 'Movie Hunt instances, selected:', this.selectedInstanceId);
} else {
this.radarrInstances = [];
this.selectedInstance = null;
console.log('[MovieHuntDetail] No instances found in response');
this.movieHuntInstances = [];
this.selectedInstanceId = null;
}
} catch (error) {
console.error('[MovieHuntDetail] Error loading Radarr instances:', error);
this.radarrInstances = [];
this.selectedInstance = null;
console.error('[MovieHuntDetail] Error loading Movie Hunt instances:', error);
this.movieHuntInstances = [];
this.selectedInstanceId = null;
}
},
async checkMovieStatus(tmdbId, instanceName) {
if (!instanceName) return { in_library: false, in_cooldown: false };
async checkMovieStatus(tmdbId, instanceId) {
if (!instanceId) return { in_library: false };
try {
const response = await fetch(`./api/requestarr/movie-status?tmdb_id=${tmdbId}&instance=${encodeURIComponent(instanceName)}`);
// Check Movie Hunt collection for this instance
const response = await fetch(`./api/movie-hunt/collection?instance_id=${instanceId}`);
const data = await response.json();
return {
in_library: data.in_library || false,
in_cooldown: (data.cooldown_status && data.cooldown_status.in_cooldown) || false
};
// Find this movie in the collection
const items = data.items || [];
const movie = items.find(item => item.tmdb_id === tmdbId);
if (movie) {
return {
in_library: movie.status === 'available'
};
}
return { in_library: false };
} catch (error) {
console.error('[MovieHuntDetail] Error checking movie status:', error);
return { in_library: false, in_cooldown: false };
return { in_library: false };
}
},
async updateMovieStatus() {
if (!this.currentMovie || !this.selectedInstance) return;
if (!this.currentMovie || !this.selectedInstanceId) return;
const tmdbId = this.currentMovie.tmdb_id || this.currentMovie.id;
const status = await this.checkMovieStatus(tmdbId, this.selectedInstance);
const status = await this.checkMovieStatus(tmdbId, this.selectedInstanceId);
// Update the action button based on new status
const actionsContainer = document.querySelector('.movie-detail-actions');
@@ -526,8 +536,6 @@
if (status.in_library) {
actionButton = '<button class="movie-detail-btn movie-detail-btn-primary" disabled><i class="fas fa-check"></i> Already Available</button>';
} else if (status.in_cooldown) {
actionButton = '<button class="movie-detail-btn movie-detail-btn-primary" disabled><i class="fas fa-clock"></i> In Cooldown</button>';
} else {
actionButton = '<button class="movie-detail-btn movie-detail-btn-primary" id="movie-detail-request-btn"><i class="fas fa-download"></i> Request Movie</button>';
}
@@ -538,13 +546,7 @@
const requestBtn = document.getElementById('movie-detail-request-btn');
if (requestBtn) {
requestBtn.addEventListener('click', () => {
if (this.options.source === 'requestarr' && window.RequestarrDiscover && window.RequestarrDiscover.modal) {
window.RequestarrDiscover.modal.openModal(
this.currentMovie.tmdb_id,
'movie',
this.selectedInstance
);
} else if (window.MovieHunt && window.MovieHunt.openMovieHuntRequestModal) {
if (window.MovieHunt && window.MovieHunt.openMovieHuntRequestModal) {
window.MovieHunt.openMovieHuntRequestModal(this.currentMovie);
}
});

View File

@@ -982,8 +982,8 @@ export class RequestarrContent {
return;
}
// For movies, open detail page if available
if (item.media_type === 'movie' && window.MovieHuntDetail && window.MovieHuntDetail.openDetail) {
// For movies, open Requestarr detail page if available
if (item.media_type === 'movie' && window.RequestarrDetail && window.RequestarrDetail.openDetail) {
const movieData = {
tmdb_id: item.tmdb_id,
id: item.tmdb_id,
@@ -996,8 +996,7 @@ export class RequestarrContent {
in_library: inLibrary,
in_cooldown: inCooldown
};
window.MovieHuntDetail.openDetail(movieData, {
source: 'requestarr',
window.RequestarrDetail.openDetail(movieData, {
suggestedInstance: card.suggestedInstance
});
} else {

View File

@@ -0,0 +1,533 @@
/**
* Requestarr Movie Detail Page - Detail view for Requestarr movies
* Handles Radarr instances and movie status checking
*/
(function() {
'use strict';
window.RequestarrDetail = {
currentMovie: null,
tmdbApiKey: null,
radarrInstances: [],
selectedInstanceName: null,
init() {
console.log('[RequestarrDetail] Module initialized');
// Listen for browser back/forward buttons
window.addEventListener('popstate', (e) => {
if (e.state && e.state.requestarrMovieDetail) {
// User navigated to a movie detail via back/forward
this.openDetail(e.state.requestarrMovieDetail, e.state.options || {}, true);
} else if (e.state && !e.state.movieDetail) {
// User navigated away from movie detail (but not to Movie Hunt detail)
this.closeDetail(true);
}
});
},
/**
* Open detail view for a movie from Requestarr
* @param {Object} movie - Movie data with at least title, year, tmdb_id
* @param {Object} options - Optional config: { suggestedInstance: string }
* @param {Boolean} fromHistory - True if opened from browser history (don't push state again)
*/
async openDetail(movie, options = {}, fromHistory = false) {
if (!movie) return;
this.currentMovie = movie;
this.options = options || {};
console.log('[RequestarrDetail] Opening detail for:', movie.title);
// Load Radarr instances if not already loaded
if (this.radarrInstances.length === 0) {
await this.loadRadarrInstances();
}
// Get or create detail view container
let detailView = document.getElementById('requestarr-detail-view');
if (!detailView) {
detailView = document.createElement('div');
detailView.id = 'requestarr-detail-view';
detailView.className = 'movie-detail-view';
document.body.appendChild(detailView);
}
// Show loading state
detailView.innerHTML = this.getLoadingHTML();
detailView.classList.add('active');
// Add to browser history (so back button works)
if (!fromHistory) {
const tmdbId = movie.tmdb_id || movie.id;
const url = `${window.location.pathname}${window.location.search}#requestarr-movie/${tmdbId}`;
history.pushState(
{ requestarrMovieDetail: movie, options: this.options },
movie.title,
url
);
}
// Setup close button
setTimeout(() => {
const closeBtn = detailView.querySelector('.movie-detail-close');
if (closeBtn) {
closeBtn.addEventListener('click', () => this.closeDetail());
}
}, 0);
try {
// Fetch full movie details from TMDB
const tmdbId = movie.tmdb_id || movie.id;
const details = await this.fetchMovieDetails(tmdbId);
if (details) {
detailView.innerHTML = this.renderMovieDetail(details, movie);
this.setupDetailInteractions();
} else {
detailView.innerHTML = this.getErrorHTML('Failed to load movie details');
}
} catch (error) {
console.error('[RequestarrDetail] Error loading details:', error);
detailView.innerHTML = this.getErrorHTML('Failed to load movie details');
}
},
closeDetail(fromHistory = false) {
const detailView = document.getElementById('requestarr-detail-view');
if (detailView) {
detailView.classList.remove('active');
}
// Remove from browser history if not already navigating
if (!fromHistory && detailView && detailView.classList.contains('active')) {
history.back();
}
},
async fetchMovieDetails(tmdbId) {
if (!tmdbId) return null;
try {
// Get TMDB API key from Movie Hunt (shared endpoint)
if (!this.tmdbApiKey) {
const keyResponse = await fetch('./api/movie-hunt/tmdb-key');
if (!keyResponse.ok) throw new Error('TMDB key endpoint failed: ' + keyResponse.status);
const keyData = await keyResponse.json();
this.tmdbApiKey = keyData.api_key;
}
if (!this.tmdbApiKey) {
console.error('[RequestarrDetail] No TMDB API key available');
return null;
}
// Fetch movie details with credits and similar movies
const url = `https://api.themoviedb.org/3/movie/${tmdbId}?api_key=${this.tmdbApiKey}&append_to_response=credits,similar,videos,release_dates`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`TMDB API returned ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('[RequestarrDetail] Error fetching from TMDB:', error);
return null;
}
},
async loadRadarrInstances() {
try {
const response = await fetch('./api/requestarr/instances/radarr');
const data = await response.json();
if (data.instances && data.instances.length > 0) {
this.radarrInstances = data.instances;
// Set initial selected instance (prioritize suggested instance from options)
if (this.options.suggestedInstance) {
this.selectedInstanceName = this.options.suggestedInstance;
} else if (!this.selectedInstanceName) {
this.selectedInstanceName = this.radarrInstances[0].name;
}
console.log('[RequestarrDetail] Loaded', this.radarrInstances.length, 'Radarr instances, selected:', this.selectedInstanceName);
} else {
this.radarrInstances = [];
this.selectedInstanceName = null;
}
} catch (error) {
console.error('[RequestarrDetail] Error loading Radarr instances:', error);
this.radarrInstances = [];
this.selectedInstanceName = null;
}
},
async checkMovieStatus(tmdbId, instanceName) {
if (!instanceName) return { in_library: false, in_cooldown: false };
try {
const response = await fetch(`./api/requestarr/movie-status?tmdb_id=${tmdbId}&instance=${encodeURIComponent(instanceName)}`);
const data = await response.json();
return {
in_library: data.in_library || false,
in_cooldown: (data.cooldown_status && data.cooldown_status.in_cooldown) || false
};
} catch (error) {
console.error('[RequestarrDetail] Error checking movie status:', error);
return { in_library: false, in_cooldown: false };
}
},
async updateMovieStatus() {
if (!this.currentMovie || !this.selectedInstanceName) return;
const tmdbId = this.currentMovie.tmdb_id || this.currentMovie.id;
const status = await this.checkMovieStatus(tmdbId, this.selectedInstanceName);
// Update the action button based on new status
const actionsContainer = document.querySelector('.movie-detail-actions');
if (actionsContainer) {
let actionButton = '';
if (status.in_library) {
actionButton = '<button class="movie-detail-btn movie-detail-btn-primary" disabled><i class="fas fa-check"></i> Already Available</button>';
} else if (status.in_cooldown) {
actionButton = '<button class="movie-detail-btn movie-detail-btn-primary" disabled><i class="fas fa-clock"></i> In Cooldown</button>';
} else {
actionButton = '<button class="movie-detail-btn movie-detail-btn-primary" id="requestarr-detail-request-btn"><i class="fas fa-download"></i> Request Movie</button>';
}
actionsContainer.innerHTML = actionButton;
// Re-setup request button if it exists
const requestBtn = document.getElementById('requestarr-detail-request-btn');
if (requestBtn) {
requestBtn.addEventListener('click', () => {
if (window.RequestarrDiscover && window.RequestarrDiscover.modal) {
window.RequestarrDiscover.modal.openModal(
this.currentMovie.tmdb_id,
'movie',
this.selectedInstanceName
);
}
});
}
}
},
renderMovieDetail(details, originalMovie) {
const backdropUrl = details.backdrop_path
? `https://image.tmdb.org/t/p/original${details.backdrop_path}`
: (details.poster_path ? `https://image.tmdb.org/t/p/original${details.poster_path}` : '');
const posterUrl = details.poster_path
? `https://image.tmdb.org/t/p/w500${details.poster_path}`
: './static/images/blackout.jpg';
const rating = details.vote_average ? Number(details.vote_average).toFixed(1) : 'N/A';
const year = details.release_date ? new Date(details.release_date).getFullYear() : 'N/A';
const runtime = details.runtime ? `${Math.floor(details.runtime / 60)}h ${details.runtime % 60}m` : 'N/A';
const genres = details.genres && details.genres.length > 0
? details.genres.map(g => `<span class="movie-detail-genre">${this.escapeHtml(g.name)}</span>`).join('')
: '<span class="movie-detail-genre">Unknown</span>';
const overview = details.overview || 'No overview available.';
// Check status from original movie data
const inLibrary = originalMovie.in_library || false;
const inCooldown = originalMovie.in_cooldown || false;
let actionButton = '';
if (inLibrary) {
actionButton = '<button class="movie-detail-btn movie-detail-btn-primary" disabled><i class="fas fa-check"></i> Already Available</button>';
} else if (inCooldown) {
actionButton = '<button class="movie-detail-btn movie-detail-btn-primary" disabled><i class="fas fa-clock"></i> In Cooldown</button>';
} else {
actionButton = '<button class="movie-detail-btn movie-detail-btn-primary" id="requestarr-detail-request-btn"><i class="fas fa-download"></i> Request Movie</button>';
}
// Director and main cast
let director = 'N/A';
let mainCast = [];
if (details.credits) {
if (details.credits.crew) {
const directorObj = details.credits.crew.find(c => c.job === 'Director');
if (directorObj) director = directorObj.name;
}
if (details.credits.cast) {
mainCast = details.credits.cast.slice(0, 10);
}
}
// Similar movies
let similarMovies = [];
if (details.similar && details.similar.results) {
similarMovies = details.similar.results.slice(0, 6);
}
// Certification/Rating
let certification = 'Not Rated';
if (details.release_dates && details.release_dates.results) {
const usRelease = details.release_dates.results.find(r => r.iso_3166_1 === 'US');
if (usRelease && usRelease.release_dates && usRelease.release_dates.length > 0) {
const cert = usRelease.release_dates[0].certification;
if (cert) certification = cert;
}
}
// Build Radarr instance selector
let instanceSelectorHTML = '';
if (this.radarrInstances.length > 0) {
instanceSelectorHTML = `
<div class="movie-detail-instance-selector">
<label for="requestarr-detail-instance-select"><i class="fas fa-server"></i> Instance:</label>
<select id="requestarr-detail-instance-select" class="movie-detail-select">
${this.radarrInstances.map(instance => {
const selected = instance.name === this.selectedInstanceName ? 'selected' : '';
return `<option value="${this.escapeHtml(instance.name)}" ${selected}>Radarr - ${this.escapeHtml(instance.name)}</option>`;
}).join('')}
</select>
</div>
`;
}
return `
<button class="movie-detail-close"><i class="fas fa-times"></i></button>
<div class="movie-detail-hero" style="background-image: url('${backdropUrl}');">
<div class="movie-detail-hero-content">
<div class="movie-detail-poster">
<img src="${posterUrl}" alt="${this.escapeHtml(details.title)}" onerror="this.src='./static/images/blackout.jpg'">
</div>
<div class="movie-detail-info">
<h1 class="movie-detail-title">${this.escapeHtml(details.title)}</h1>
${instanceSelectorHTML}
<div class="movie-detail-meta">
<div class="movie-detail-year"><i class="fas fa-calendar"></i> ${year}</div>
<div class="movie-detail-runtime"><i class="fas fa-clock"></i> ${runtime}</div>
<div class="movie-detail-rating"><i class="fas fa-star"></i> ${rating}</div>
</div>
<div class="movie-detail-genres">${genres}</div>
<p class="movie-detail-overview">${this.escapeHtml(overview)}</p>
<div class="movie-detail-actions">
${actionButton}
</div>
</div>
</div>
</div>
<div class="movie-detail-content">
<!-- Movie Details -->
<div class="movie-detail-section">
<h2 class="movie-detail-section-title"><i class="fas fa-info-circle"></i> Movie Details</h2>
<div class="movie-detail-grid">
<div class="movie-detail-item">
<div class="movie-detail-item-label">Director</div>
<div class="movie-detail-item-value">${this.escapeHtml(director)}</div>
</div>
<div class="movie-detail-item">
<div class="movie-detail-item-label">Release Date</div>
<div class="movie-detail-item-value">${details.release_date || 'N/A'}</div>
</div>
<div class="movie-detail-item">
<div class="movie-detail-item-label">Rating</div>
<div class="movie-detail-item-value">${certification}</div>
</div>
<div class="movie-detail-item">
<div class="movie-detail-item-label">Budget</div>
<div class="movie-detail-item-value">${details.budget ? '$' + (details.budget / 1000000).toFixed(1) + 'M' : 'N/A'}</div>
</div>
<div class="movie-detail-item">
<div class="movie-detail-item-label">Revenue</div>
<div class="movie-detail-item-value">${details.revenue ? '$' + (details.revenue / 1000000).toFixed(1) + 'M' : 'N/A'}</div>
</div>
<div class="movie-detail-item">
<div class="movie-detail-item-label">Language</div>
<div class="movie-detail-item-value">${details.original_language ? details.original_language.toUpperCase() : 'N/A'}</div>
</div>
</div>
</div>
${mainCast.length > 0 ? `
<!-- Cast -->
<div class="movie-detail-section">
<h2 class="movie-detail-section-title"><i class="fas fa-users"></i> Cast</h2>
<div class="movie-detail-cast">
${mainCast.map(actor => this.renderCastCard(actor)).join('')}
</div>
</div>
` : ''}
${similarMovies.length > 0 ? `
<!-- Similar Movies -->
<div class="movie-detail-section">
<h2 class="movie-detail-section-title"><i class="fas fa-film"></i> Similar Movies</h2>
<div class="movie-detail-similar">
${similarMovies.map(movie => this.renderSimilarCard(movie)).join('')}
</div>
</div>
` : ''}
</div>
`;
},
renderCastCard(actor) {
const photoUrl = actor.profile_path
? `https://image.tmdb.org/t/p/w185${actor.profile_path}`
: './static/images/blackout.jpg';
return `
<div class="movie-detail-cast-card">
<div class="movie-detail-cast-photo">
<img src="${photoUrl}" alt="${this.escapeHtml(actor.name)}" onerror="this.src='./static/images/blackout.jpg'">
</div>
<div class="movie-detail-cast-info">
<div class="movie-detail-cast-name">${this.escapeHtml(actor.name)}</div>
<div class="movie-detail-cast-character">${this.escapeHtml(actor.character || 'Unknown')}</div>
</div>
</div>
`;
},
renderSimilarCard(movie) {
const posterUrl = movie.poster_path
? `https://image.tmdb.org/t/p/w185${movie.poster_path}`
: './static/images/blackout.jpg';
return `
<div class="media-card" data-tmdb-id="${movie.id}">
<div class="media-card-poster">
<img src="${posterUrl}" alt="${this.escapeHtml(movie.title)}" onerror="this.src='./static/images/blackout.jpg'">
<div class="media-card-overlay">
<div class="media-card-overlay-title">${this.escapeHtml(movie.title)}</div>
</div>
</div>
<div class="media-card-info">
<div class="media-card-title">${this.escapeHtml(movie.title)}</div>
<div class="media-card-meta">
<span class="media-card-year">${movie.release_date ? new Date(movie.release_date).getFullYear() : 'N/A'}</span>
<span class="media-card-rating"><i class="fas fa-star"></i> ${movie.vote_average ? Number(movie.vote_average).toFixed(1) : 'N/A'}</span>
</div>
</div>
</div>
`;
},
setupDetailInteractions() {
// Close button
const closeBtn = document.querySelector('.movie-detail-close');
if (closeBtn) {
closeBtn.addEventListener('click', () => this.closeDetail());
}
// Instance selector change handler
const instanceSelect = document.getElementById('requestarr-detail-instance-select');
if (instanceSelect) {
instanceSelect.addEventListener('change', async () => {
this.selectedInstanceName = instanceSelect.value;
console.log('[RequestarrDetail] Instance changed to:', this.selectedInstanceName);
// Update movie status for new instance
await this.updateMovieStatus();
});
// Initial status check for selected instance
this.updateMovieStatus();
}
// Request button
const requestBtn = document.getElementById('requestarr-detail-request-btn');
if (requestBtn && this.currentMovie) {
requestBtn.addEventListener('click', () => {
if (window.RequestarrDiscover && window.RequestarrDiscover.modal) {
window.RequestarrDiscover.modal.openModal(
this.currentMovie.tmdb_id,
'movie',
this.selectedInstanceName
);
}
});
}
// Similar movie cards - open their details
const similarCards = document.querySelectorAll('.movie-detail-similar .media-card');
similarCards.forEach(card => {
card.addEventListener('click', async () => {
const tmdbId = card.getAttribute('data-tmdb-id');
if (tmdbId) {
try {
const details = await this.fetchMovieDetails(tmdbId);
if (details) {
const movieData = {
tmdb_id: details.id,
id: details.id,
title: details.title,
year: details.release_date ? new Date(details.release_date).getFullYear() : null,
poster_path: details.poster_path,
backdrop_path: details.backdrop_path,
overview: details.overview,
vote_average: details.vote_average,
in_library: false,
in_cooldown: false
};
this.openDetail(movieData, this.options || {}, false);
}
} catch (error) {
console.error('[RequestarrDetail] Error opening similar movie:', error);
}
}
});
});
// ESC key to close
const escHandler = (e) => {
if (e.key === 'Escape') {
this.closeDetail();
document.removeEventListener('keydown', escHandler);
}
};
document.addEventListener('keydown', escHandler);
},
getLoadingHTML() {
return `
<button class="movie-detail-close"><i class="fas fa-times"></i></button>
<div class="movie-detail-loading">
<i class="fas fa-spinner fa-spin"></i>
<p>Loading movie details...</p>
</div>
`;
},
getErrorHTML(message) {
return `
<button class="movie-detail-close"><i class="fas fa-times"></i></button>
<div class="movie-detail-loading">
<i class="fas fa-exclamation-triangle" style="color: #ef4444;"></i>
<p style="color: #ef4444;">${this.escapeHtml(message)}</p>
</div>
`;
},
escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
};
// Initialize on load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => window.RequestarrDetail.init());
} else {
window.RequestarrDetail.init();
}
})();

View File

@@ -71,6 +71,7 @@
<script src="./static/js/modules/utils/tmdb-image-cache-standalone.js"></script>
<!-- Requestarr System -->
<script src="./static/js/modules/features/requestarr/requestarr-detail.js"></script>
<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>
<script src="./static/js/modules/features/requestarr/requestarr-content.js" type="module"></script>