Refactor TMDB data scraper and enhance Import UI with styling.

Enhanced TMDB API scraper with better episode and year handling, streamlined year match logic, and improved metadata updates. CSS updates provide a more polished UI with better hover effects, spacing, and theming adjustments for a consistent dark mode appearance.
This commit is contained in:
mhdzumair
2025-01-15 13:06:36 +05:30
parent 80b7208eae
commit a6492af5c5
8 changed files with 875 additions and 376 deletions
+7
View File
@@ -809,6 +809,8 @@ async def get_tv_meta(meta_id: str):
async def get_existing_metadata(
metadata: dict, model: Type[MediaFusionMovieMetaData | MediaFusionSeriesMetaData]
) -> Optional[MediaFusionMovieMetaData | MediaFusionSeriesMetaData]:
if metadata.get("id"):
return await model.get(metadata["id"])
title = metadata["title"]
year = metadata.get("year")
if isinstance(year, str):
@@ -1590,6 +1592,11 @@ async def update_metadata(imdb_ids: list[str], metadata_type: str):
)
# Get existing metadata to preserve values
existing_metadata = await meta_class.get(meta_id)
if not existing_metadata:
logging.warning(
f"Metadata not found for {metadata_type} {meta_id}. Skipping update."
)
continue
# Merge new data with existing data, preserving existing values when new ones are None/empty
update_data = {
+344 -8
View File
@@ -11,6 +11,7 @@ body, html {
background-position: center center;
background-repeat: no-repeat;
}
.mode-switch {
background: rgba(20, 20, 35, 0.7);
padding: 1.5rem;
@@ -51,16 +52,19 @@ body, html {
background: rgba(74, 71, 163, 0.4);
transform: translateY(-2px);
}
.mode-btn:focus-visible {
outline: 2px solid #fff;
outline-offset: 2px;
}
.mode-btn[aria-pressed="true"] {
background: #4a47a3;
color: white;
border-color: #4a47a3;
box-shadow: 0 4px 12px rgba(74, 71, 163, 0.3);
}
.mode-btn.active {
background: #4a47a3;
color: white;
@@ -100,18 +104,136 @@ body, html {
}
.section-container {
margin-bottom: 1rem;
padding: 1rem;
background: rgba(0, 0, 0, 0.37);
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 1.5rem;
background: rgba(20, 20, 35, 0.85);
border-radius: 12px;
transition: margin-bottom 0.3s ease, padding 0.3s ease;
}
.section-container .card {
background: rgba(30, 30, 50, 0.9);
border: 1px solid rgba(74, 71, 163, 0.3);
margin-bottom: 1.5rem;
}
.section-container .card-header {
background: rgba(74, 71, 163, 0.2);
border-bottom: 1px solid rgba(74, 71, 163, 0.3);
padding: 1rem 1.5rem;
}
.section-container .card-header h5 {
color: #ffffff; /* Bright white for headers */
font-weight: 500;
margin: 0;
}
.section-container .card-body {
padding: 1.5rem;
}
.section-header {
color: #E0E0E0;
margin-bottom: 0.5rem;
}
.list-group-item-action {
transition: all 0.2s ease;
border: 1px solid rgba(74, 71, 163, 0.2);
background: rgba(30, 30, 50, 0.95); /* Slightly more opaque */
color: #ffffff; /* Bright white for main text */
}
.list-group-item-action:hover {
transform: translateY(-2px);
border-color: rgba(74, 71, 163, 0.4);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
background: rgba(40, 40, 60, 0.98); /* Even more opaque on hover */
}
/* Title and year */
.list-group-item h5 {
color: #ffffff;
font-weight: 500;
}
.list-group-item h5 .text-muted {
color: #bdbdbd !important; /* Lighter gray for year */
}
/* Description and other text */
.list-group-item .text-muted {
color: #bdbdbd !important; /* Lighter gray for better readability */
}
/* Meta information */
.list-group-item .small,
.list-group-item small {
color: #e0e0e0; /* Light gray for small text */
}
/* Genre badges */
.badge.bg-primary {
background-color: rgba(74, 71, 163, 0.3) !important;
color: #ffffff !important;
border: 1px solid rgba(74, 71, 163, 0.5);
}
/* Icons */
.list-group-item i {
color: #4a47a3; /* Accent color for icons */
}
/* Rating */
.badge.bg-secondary {
background-color: rgba(158, 158, 158, 0.3) !important;
color: #e0e0e0 !important;
}
/* Meta sections */
.list-group-item .d-flex.align-items-center {
color: #e0e0e0; /* Light gray for meta information */
}
/* Content type labels (Comedy, Romance, etc.) */
.genre-tag {
background: rgba(74, 71, 163, 0.2);
color: #ffffff;
padding: 0.25rem 0.75rem;
border-radius: 4px;
font-size: 0.9rem;
margin-right: 0.5rem;
border: 1px solid rgba(74, 71, 163, 0.3);
}
/* IMDb rating */
.text-warning {
color: #ffd700 !important; /* Brighter yellow for rating stars */
}
.list-group-item img {
transition: transform 0.2s ease;
}
.list-group-item:hover img {
transform: scale(1.05);
}
.badge {
font-weight: 500;
padding: 0.5em 0.8em;
}
.gap-2 {
gap: 0.5rem !important;
}
.gap-3 {
gap: 1rem !important;
}
.section-divider {
border: 0;
height: 2px;
@@ -126,12 +248,14 @@ body, html {
transform: translateY(-10px);
transition: all 0.3s ease-in-out;
}
@media (prefers-reduced-motion: reduce) {
.pro-mode-section {
transform: none;
transition: opacity 0.3s ease-in-out;
}
}
.pro-mode-section[style*="display: block"] {
opacity: 1;
transform: translateY(0);
@@ -220,6 +344,7 @@ input[type="text"]:focus, input[type="password"]:focus, select.form-control:focu
display: block;
align-items: center;
border: 2px solid transparent;
margin-top: 1rem;
margin-bottom: 0.5rem;
padding: 0.2rem;
border-radius: 5px;
@@ -250,6 +375,7 @@ input[type="text"]:focus, input[type="password"]:focus, select.form-control:focu
display: flex;
align-items: center;
gap: 1rem;
color: #e0e0e0;
min-height: 36px; /* Minimum touch target size */
}
@@ -257,6 +383,11 @@ input[type="text"]:focus, input[type="password"]:focus, select.form-control:focu
content: none; /* Remove the round indicator */
}
.form-check-input:checked {
background-color: #4a47a3;
border-color: #4a47a3;
}
.form-check-input:checked + .form-check-label {
background: #4a47a3;
border-color: #4a47a3;
@@ -274,6 +405,10 @@ input[type="text"]:focus, input[type="password"]:focus, select.form-control:focu
color: white;
}
.form-check-input:checked ~ .form-check-label {
color: #ffffff;
}
/* Style for disabled checkboxes */
.form-check.disabled-checkbox {
opacity: 0.5;
@@ -288,6 +423,11 @@ input[type="text"]:focus, input[type="password"]:focus, select.form-control:focu
pointer-events: none;
}
.form-label {
color: #e0e0e0;
font-weight: 500;
margin-bottom: 0.5rem;
}
.form-control {
display: block;
@@ -307,6 +447,19 @@ input[type="text"]:focus, input[type="password"]:focus, select.form-control:focu
transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out;
}
.form-control, .form-select {
background: rgba(255, 255, 255, 0.9); /* More opaque white background */
border: 1px solid rgba(74, 71, 163, 0.3);
color: #000; /* Black text for better contrast */
}
.form-control:focus, .form-select:focus {
background: #ffffff;
border-color: #4a47a3;
box-shadow: 0 0 0 0.25rem rgba(74, 71, 163, 0.25);
}
/* Styling for the range slider */
.form-range {
-webkit-appearance: none; /* Override default look for Chrome, Safari */
@@ -433,6 +586,12 @@ input[type="text"]:focus, input[type="password"]:focus, select.form-control:focu
margin-bottom: 10px;
}
.alert-info {
background: rgba(74, 71, 163, 0.15);
border-color: rgba(74, 71, 163, 0.3);
color: #e0e0e0;
}
.loading-overlay {
position: fixed;
top: 0;
@@ -480,16 +639,16 @@ input[type="text"]:focus, input[type="password"]:focus, select.form-control:focu
}
.configured-field {
background-color: #f8f9fa;
background-color: #f8f9fa;
}
.reset-config-btn {
margin-left: 0.5rem;
margin-left: 0.5rem;
}
.input-group .reset-config-btn {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.sort-item {
@@ -514,6 +673,183 @@ input[type="text"]:focus, input[type="password"]:focus, select.form-control:focu
margin-bottom: 0.75rem;
}
.advanced-toggle {
background: rgba(74, 71, 163, 0.15);
padding: 0.75rem 1rem;
border-radius: 8px;
border: 1px solid rgba(74, 71, 163, 0.2);
color: #e0e0e0;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
margin: 1rem 0;
transition: all 0.3s ease;
}
.advanced-toggle:hover {
background: rgba(74, 71, 163, 0.25);
}
.advanced-toggle i {
transition: transform 0.3s ease;
margin-right: 0.5rem;
}
.advanced-toggle.active i {
transform: rotate(180deg);
}
.advanced-section {
display: none;
opacity: 0;
transform: translateY(-10px);
transition: all 0.3s ease;
}
.advanced-section.show {
display: block;
opacity: 1;
transform: translateY(0);
}
.parsed-value {
color: #4a47a3;
font-size: 0.9rem;
margin-top: 0.25rem;
}
.technical-specs-basic {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
}
.technical-specs-basic .spec-item {
background: rgba(74, 71, 163, 0.1);
padding: 0.75rem;
border-radius: 6px;
display: flex;
align-items: center;
gap: 0.5rem;
}
.spec-item {
background: rgba(74, 71, 163, 0.15);
padding: 1rem;
border-radius: 8px;
border: 1px solid rgba(74, 71, 163, 0.2);
}
.spec-item i {
color: #4a47a3;
}
.spec-label {
color: #e0e0e0;
font-size: 0.9rem;
margin-bottom: 0.25rem;
}
.spec-value {
color: #ffffff;
font-weight: 500;
}
.not-available {
color: #6c757d;
font-style: italic;
}
/* Catalog Section Styles */
.catalogs-section {
border-top: 1px solid rgba(74, 71, 163, 0.2);
padding-top: 1.5rem;
}
.section-title {
color: #4a47a3;
margin-bottom: 1rem;
font-weight: 500;
}
.catalog-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
.catalog-item {
background: rgba(74, 71, 163, 0.1);
border-radius: 6px;
padding: 0.5rem;
transition: all 0.3s ease;
}
.catalog-item:hover {
background: rgba(74, 71, 163, 0.2);
transform: translateY(-2px);
}
.catalog-item .form-check {
margin: 0;
padding: 0.25rem;
}
.catalog-group {
margin-bottom: 1.5rem;
}
.catalog-group .alert {
margin-bottom: 1rem;
border-left: 4px solid #4a47a3;
}
/* Text utility classes for dark theme */
.text-muted {
color: #9e9e9e !important; /* Lighter gray for better visibility */
}
/* Other text utility classes that might need adjustment */
.text-secondary {
color: #b0b0b0 !important;
}
.text-body-secondary {
color: #a0a0a0 !important;
}
/* Form text and help text */
.form-text {
color: #9e9e9e !important;
}
/* Subtitle styling */
.card-subtitle {
color: #b0b0b0 !important;
}
/* Description text */
.description-text {
color: #9e9e9e !important;
}
/* Optional help text */
small.text-muted,
.small.text-muted {
color: #9e9e9e !important;
}
/* Alert text */
.alert .text-muted {
color: #b0b0b0 !important;
}
@media (max-width: 576px) {
.sort-direction-toggle {
min-width: auto;
+219 -191
View File
@@ -109,49 +109,53 @@
</div>
</div>
<!-- Torrent Upload Parameters -->
<!-- Torrent Upload Parameters Section -->
<div id="torrentUploadParameters" style="display: none;">
<div class="section-container">
<!-- Content Type Selection -->
<div class="row mb-4">
<div class="col-md-4">
<label for="metaType" class="form-label">Content Type</label>
<select class="form-select" id="metaType" name="metaType" onchange="updateContentType()">
<option value="movie" {% if prefill_data.meta_type == 'movie' %}selected{% endif %}>Movie</option>
<option value="series" {% if prefill_data.meta_type == 'series' %}selected{% endif %}>Series</option>
<option value="sports">Sports</option>
</select>
</div>
<div class="col-md-4">
<label for="torrentType" class="form-label">Torrent Type</label>
<select class="form-select" id="torrentType" name="torrentType">
<option value="public">Public</option>
<option value="semi-private">Semi-Private</option>
<option value="private">Private</option>
<option value="web-seed">Web-Seed</option>
</select>
</div>
<div class="col-md-4">
<label for="createdAt" class="form-label">Release Date</label>
<input type="date" class="form-control" id="createdAt" name="createdAt" required>
</div>
</div>
<!-- Content Metadata Section -->
<!-- Basic Content Information -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">Content Metadata</h5>
<h5 class="mb-0">Basic Information</h5>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="col-md-6" id="metaIdContainer">
<label for="torrentImdbId" class="form-label">IMDb ID</label>
<input type="text" class="form-control" id="torrentImdbId" maxlength="10"
placeholder="e.g., tt1234567" value="{% if prefill_data.meta_id %}{{ prefill_data.meta_id }}{% endif %}">
<!-- Content Type and Release Date -->
<div class="row mb-4">
<div class="col-md-4">
<label for="metaType" class="form-label">Content Type</label>
<select class="form-select" id="metaType" name="metaType" onchange="updateContentType()">
<option value="movie" {% if prefill_data.meta_type == 'movie' %}selected{% endif %}>Movie</option>
<option value="series" {% if prefill_data.meta_type == 'series' %}selected{% endif %}>Series</option>
<option value="sports">Sports</option>
</select>
</div>
<div class="col-md-6">
<div class="col-md-4">
<label for="torrentType" class="form-label">Torrent Type</label>
<select class="form-select" id="torrentType" name="torrentType">
<option value="public">Public</option>
<option value="semi-private">Semi-Private</option>
<option value="private">Private</option>
<option value="web-seed">Web-Seed</option>
</select>
</div>
<div class="col-md-4">
<label for="createdAt" class="form-label">Release Date</label>
<input type="date" class="form-control" id="createdAt" name="createdAt" required>
</div>
</div>
<!-- Content Metadata -->
<div class="row mb-3">
<div class="col-md-4" id="metaIdContainer">
<label for="torrentImdbId" class="form-label">IMDb ID</label>
<input type="text" class="form-control" id="torrentImdbId" maxlength="10" placeholder="e.g., tt1234567"
value="{% if prefill_data.meta_id %}{{ prefill_data.meta_id }}{% endif %}">
</div>
<div class="col-md-8">
<label for="title" class="form-label">Title</label>
<alert class="alert alert-info py-1" role="alert">
<i class="bi bi-info circle"></i> Validate auto-filled data before submission
</alert>
<input type="text" class="form-control" id="title" placeholder="Content title">
</div>
</div>
@@ -181,51 +185,87 @@
</label>
</div>
</div>
</div>
</div>
<!-- Technical Specs -->
<div class="row mb-3">
<div class="col-md-3">
<label for="resolution" class="form-label">Resolution</label>
<select class="form-select" id="resolution">
<option value="">Select Resolution</option>
<option value="480p">480p</option>
<option value="576p">576p</option>
<option value="720p">720p</option>
<option value="1080p">1080p</option>
<option value="1440p">1440p</option>
<option value="2160p">2160p</option>
<option value="4K">4K</option>
</select>
</div>
<div class="col-md-3">
<label for="quality" class="form-label">Quality</label>
<select class="form-select" id="quality">
<option value="">Select Quality</option>
<option value="BluRay">BluRay</option>
<option value="BluRay REMUX">BluRay REMUX</option>
<option value="BRRip">BRRip</option>
<option value="BDRip">BDRip</option>
<option value="WEB-DL">WEB-DL</option>
<option value="HDRip">HDRip</option>
<option value="DVDRip">DVDRip</option>
<option value="HDTV">HDTV</option>
<option value="CAM">CAM</option>
<option value="TeleSync">TeleSync</option>
<option value="SCR">SCR</option>
</select>
<!-- Technical Specifications -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">Technical Specifications</h5>
</div>
<div class="card-body">
<!-- Basic specs grid -->
<div class="technical-specs-basic"></div>
<!-- Advanced Toggle Button will be inserted here -->
<div class="advanced-toggle" id="toggleAdvanced">
<i class="bi bi-chevron-down"></i>
<span>Advanced Technical Options</span>
</div>
<!-- Advanced Technical Specifications -->
<div class="advanced-section" id="advancedSection">
<div class="row mb-4">
<div class="col-md-4">
<label for="resolution" class="form-label">Resolution</label>
<select class="form-select" id="resolution" onchange="updateSpecField('resolution', this.value)">
<option value="">Select Resolution</option>
<option value="480p">480p</option>
<option value="576p">576p</option>
<option value="720p">720p</option>
<option value="1080p">1080p</option>
<option value="1440p">1440p</option>
<option value="2160p">2160p</option>
<option value="4K">4K</option>
</select>
</div>
<div class="col-md-4">
<label for="quality" class="form-label">Quality</label>
<select class="form-select" id="quality" onchange="updateSpecField('quality', this.value)">
<option value="">Select Quality</option>
<option value="BluRay">BluRay</option>
<option value="BluRay REMUX">BluRay REMUX</option>
<option value="BRRip">BRRip</option>
<option value="BDRip">BDRip</option>
<option value="WEB-DL">WEB-DL</option>
<option value="HDRip">HDRip</option>
<option value="DVDRip">DVDRip</option>
<option value="HDTV">HDTV</option>
<option value="CAM">CAM</option>
<option value="TeleSync">TeleSync</option>
<option value="SCR">SCR</option>
</select>
</div>
<!-- Video Codec -->
<div id="video-codec-section" class="col-md-4">
<label for="videoCodec" class="form-label">Video Codec</label>
<select class="form-select" id="videoCodec" onchange="updateSpecField('codec', this.value)">
<option value="">Select Video Codec</option>
<option value="x264">x264</option>
<option value="x265">x265</option>
<option value="h.264">H.264</option>
<option value="h.265">H.265</option>
<option value="hevc">HEVC</option>
<option value="avc">AVC</option>
<option value="mpeg-2">MPEG-2</option>
<option value="mpeg-4">MPEG-4</option>
<option value="vp9">VP9</option>
</select>
</div>
</div>
<!-- Audio Codecs -->
<div class="mb-3">
<div id="audio-codecs-section">
<label class="form-label">Audio Codecs</label>
<div class="row">
{% for codec in ['AAC', 'AC3', 'DTS', 'DTS-HD MA', 'TrueHD', 'Atmos', 'DD+'] %}
{% for codec in ['AAC', 'AC3', 'DTS', 'DTS-HD MA', 'TrueHD', 'Atmos', 'DD+', 'Dolby Digital Plus', 'DTS Lossless'] %}
<div class="col-md-3 col-sm-6">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox"
name="audioCodecs" value="{{ codec }}"
id="audio-{{ codec|lower|replace('-', '')|replace('+', 'plus') }}">
id="audio-{{ codec|replace(' ', '')|replace('+', '') }}">
<label class="form-check-label"
for="audio-{{ codec|lower|replace('-', '')|replace('+', 'plus') }}">
for="audio-{{ codec|replace(' ', '')|replace('+', '') }}">
{{ codec }}
</label>
</div>
@@ -234,124 +274,115 @@
</div>
</div>
<!-- Video Codecs -->
<div class="col-md-3">
<label for="videoCodec" class="form-label">Video Codec</label>
<select class="form-select" id="videoCodec">
<option value="">Select Video Codec</option>
<option value="x264">x264</option>
<option value="x265">x265</option>
<option value="h.264">H.264</option>
<option value="h.265">H.265</option>
<option value="hevc">HEVC</option>
<option value="avc">AVC</option>
<option value="mpeg-2">MPEG-2</option>
<option value="mpeg-4">MPEG-4</option>
<option value="vp9">VP9</option>
<!-- HDR Formats -->
<div id="hdr-formats-section">
<label class="form-label">HDR Formats</label>
<div class="row">
{% for format in ['DV', 'HDR10+', 'HDR', 'SDR'] %}
<div class="col-md-3 col-sm-6">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox"
name="hdrFormats" value="{{ format }}"
id="hdr-{{ format|replace('+', '') }}">
<label class="form-check-label"
for="hdr-{{ format|replace('+', '') }}">
{{ format }}
</label>
</div>
</div>
{% endfor %}
</div>
</div>
<!-- Languages -->
<div id="languages-section">
<label class="form-label">Languages</label>
<div class="row">
{% for language in supported_languages %}
<div class="col-md-3 col-sm-6">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox"
name="languages" value="{{ language }}"
id="lang-{{ language }}">
<label class="form-check-label"
for="lang-{{ language }}">
{{ language }}
</label>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
<!-- Catalogs Section -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">Content Categories</h5>
</div>
<div class="card-body">
<!-- Movie Catalogs -->
<div id="movieCatalogs" class="catalog-group" style="display: none;">
<div class="alert alert-info" role="alert">
<i class="bi bi-info-circle"></i> Select categories to help organize your content
</div>
<div class="catalog-grid">
{% for catalog in supported_movie_catalog_ids %}
<div class="catalog-item">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="catalogs"
value="{{ catalog }}" id="catalog-{{ catalog }}">
<label class="form-check-label" for="catalog-{{ catalog }}">
{{ catalog_data.get(catalog) }}
</label>
</div>
</div>
{% endfor %}
</div>
</div>
<!-- Series Catalogs -->
<div id="seriesCatalogs" class="catalog-group" style="display: none;">
<div class="alert alert-info" role="alert">
<i class="bi bi-info-circle"></i> Select categories to help organize your content
</div>
<div class="catalog-grid">
{% for catalog in supported_series_catalog_ids %}
<div class="catalog-item">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="catalogs"
value="{{ catalog }}" id="catalog-{{ catalog }}">
<label class="form-check-label" for="catalog-{{ catalog }}">
{{ catalog_data.get(catalog) }}
</label>
</div>
</div>
{% endfor %}
</div>
</div>
<!-- Sports Catalogs -->
<div id="sportsCatalogs" class="catalog-group" style="display: none;">
<div class="col-md-12">
<select class="form-select" id="sportsCatalog">
<option value="">Select Sports Category</option>
<option value="american_football">American Football / NFL</option>
<option value="baseball">Baseball / MLB</option>
<option value="basketball">Basketball / NBA</option>
<option value="football">Football / Soccer</option>
<option value="formula_racing">Formula Racing</option>
<option value="hockey">Hockey / NHL</option>
<option value="motogp_racing">MotoGP Racing</option>
<option value="other_sports">Other Sports</option>
<option value="rugby">Rugby / AFL</option>
<option value="wwe">WWE</option>
<option value="ufc">UFC</option>
<option value="fighting">Fighting</option>
</select>
</div>
</div>
<!-- HDR Formats -->
<div class="mb-3">
<label class="form-label">HDR Formats</label>
<div class="row">
{% for format in ['DV', 'HDR10+', 'HDR', 'SDR'] %}
<div class="col-md-3 col-sm-6">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox"
name="hdrFormats" value="{{ format }}"
id="hdr-{{ format|lower|replace('+', 'plus') }}">
<label class="form-check-label"
for="hdr-{{ format|lower|replace('+', 'plus') }}">
{{ format }}
</label>
</div>
</div>
{% endfor %}
</div>
</div>
<!-- Languages Selection -->
<div class="mb-3">
<label class="form-label">Languages</label>
<div class="row">
{% for language in supported_languages %}
<div class="col-md-3 col-sm-6">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="languages"
value="{{ language }}" id="lang-{{ language }}">
<label class="form-check-label" for="lang-{{ language }}">
{{ language }}
</label>
</div>
</div>
{% endfor %}
</div>
</div>
<!-- Catalogs Section -->
<div class="mb-4">
<label class="form-label">Catalogs</label>
<!-- Movie Catalogs -->
<div id="movieCatalogs" class="row" style="display: none;">
<div class="alert alert-info" role="alert">
<i class="bi bi-info-circle"></i> Selecting catalogs helps organize content but is optional
</div>
<div class="row">
{% for catalog in supported_movie_catalog_ids %}
<div class="col-md-4 col-sm-6">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="catalogs" value="{{ catalog }}" id="catalog-{{ catalog }}">
<label class="form-check-label" for="catalog-{{ catalog }}">
{{ catalog_data.get(catalog) }}
</label>
</div>
</div>
{% endfor %}
</div>
</div>
<!-- Series Catalogs -->
<div id="seriesCatalogs" class="row" style="display: none;">
<div class="alert alert-info" role="alert">
<i class="bi bi-info-circle"></i> Selecting catalogs helps organize content but is optional
</div>
<div class="row">
{% for catalog in supported_series_catalog_ids %}
<div class="col-md-4 col-sm-6">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="catalogs" value="{{ catalog }}" id="catalog-{{ catalog }}">
<label class="form-check-label" for="catalog-{{ catalog }}">
{{ catalog_data.get(catalog) }}
</label>
</div>
</div>
{% endfor %}
</div>
</div>
<!-- Sports Catalogs -->
<div id="sportsCatalogs" class="row" style="display: none;">
<div class="col-md-12">
<select class="form-select" id="sportsCatalog">
<option value="">Select Sports Category</option>
<option value="american_football">American Football / NFL</option>
<option value="baseball">Baseball / MLB</option>
<option value="basketball">Basketball / NBA</option>
<option value="football">Football / Soccer</option>
<option value="formula_racing">Formula Racing</option>
<option value="hockey">Hockey / NHL</option>
<option value="motogp_racing">MotoGP Racing</option>
<option value="other_sports">Other Sports</option>
<option value="rugby">Rugby / AFL</option>
<option value="wwe">WWE</option>
<option value="ufc">UFC</option>
<option value="fighting">Fighting</option>
</select>
</div>
</div>
</div>
</div>
</div>
@@ -363,14 +394,11 @@
<div class="card-body">
<div class="mb-3">
<label for="torrentFile" class="form-label">Torrent File</label>
<input type="file" class="form-control" id="torrentFile" accept=".torrent"
onchange="toggleInput('magnetLink', this)">
<input type="file" class="form-control" id="torrentFile" accept=".torrent" onchange="toggleInput('magnetLink', this)">
</div>
<div class="mb-3">
<label for="magnetLink" class="form-label">Magnet Link</label>
<input type="text" class="form-control" id="magnetLink"
placeholder="magnet:?xt=urn:btih:..."
onchange="toggleInput('torrentFile', this)">
<input type="text" class="form-control" id="magnetLink" placeholder="magnet:?xt=urn:btih:..." onchange="toggleInput('torrentFile', this)">
</div>
</div>
</div>
@@ -378,11 +406,11 @@
<!-- Uploader Information -->
<div class="mb-3">
<label for="uploaderName" class="form-label">Uploader Name (Optional)</label>
<input type="text" class="form-control" id="uploaderName"
placeholder="Enter your name or leave blank for Anonymous">
<input type="text" class="form-control" id="uploaderName" placeholder="Enter your name or leave blank for Anonymous">
</div>
</div>
</div>
<!-- Scrapy Parameters -->
<div id="scrapyParameters" style="display: none;">
<div class="mb-3">
+207 -18
View File
@@ -673,24 +673,111 @@ function displayMatchResults(matches, torrentData) {
if (matches && matches.length > 0) {
matches.forEach(match => {
const safeMatchData = encodeURIComponent(JSON.stringify(match));
const safeTorrentData = encodeURIComponent(JSON.stringify(torrentData));
// Format runtime nicely
const runtime = match.runtime ? match.runtime.replace('min', '').trim() + ' minutes' : 'N/A';
// Format rating with stars
const rating = match.imdb_rating ?
`<i class="bi bi-star-fill text-warning"></i> ${match.imdb_rating}/10` :
'<span class="badge bg-secondary">No rating</span>';
// Handle multiple AKA titles
const akaTitles = match.aka_titles && match.aka_titles.length > 0 ?
`<div class="small mb-1">Also known as: ${match.aka_titles.join(' • ')}</div>` : '';
const matchHtml = `
<div class="list-group-item list-group-item-action">
<div class="d-flex w-100">
<div class="flex-shrink-0">
<div class="list-group-item list-group-item-action p-3">
<div class="d-flex">
<!-- Poster Section -->
<div class="flex-shrink-0 me-3">
<img src="${match.poster || '/static/img/placeholder.jpg'}"
alt="${match.title}"
class="img-thumbnail" style="width: 100px;">
class="rounded shadow-sm"
style="width: 120px; height: 180px; object-fit: cover;">
</div>
<div class="ms-3">
<h5 class="mb-1">${match.title} (${match.year})</h5>
<p class="mb-1">${match.type}${match.runtime || 'N/A'}</p>
<p class="mb-1 text-muted">${match.plot || ''}</p>
<button class="btn btn-primary btn-sm mt-2"
data-match='${btoa(JSON.stringify(match))}'
data-torrent='${btoa(JSON.stringify(torrentData))}'
onclick="selectMatchFromData(this)">
Select This Match
</button>
<!-- Content Section -->
<div class="flex-grow-1">
<!-- Header -->
<div class="d-flex justify-content-between align-items-start mb-2">
<div>
<h5 class="mb-1">${match.title}
<span class="text-muted">(${match.year})</span>
</h5>
${akaTitles}
</div>
<div>
${rating}
</div>
</div>
<!-- Meta Information -->
<div class="d-flex flex-wrap gap-3 mb-2">
<div class="d-flex align-items-center">
<i class="bi bi-film me-1"></i>
<span>${match.type.charAt(0).toUpperCase() + match.type.slice(1)}</span>
</div>
<div class="d-flex align-items-center">
<i class="bi bi-clock me-1"></i>
<span>${runtime}</span>
</div>
<div class="d-flex align-items-center">
<i class="bi bi-fingerprint me-1"></i>
<span>${match.imdb_id}</span>
</div>
</div>
<!-- Description -->
<p class="mb-2 text-muted">${match.description || 'No description available.'}</p>
<!-- Tags Section -->
<div class="mb-3">
<div class="d-flex flex-wrap gap-2">
${match.genres.map(genre =>
`<span class="badge bg-primary bg-opacity-25 text-primary">${genre}</span>`
).join('')}
</div>
</div>
<!-- Additional Information -->
<div class="row g-3 mb-3">
<div class="col-md-6">
<div class="d-flex align-items-center">
<i class="bi bi-globe2 me-2"></i>
<small>${match.countries.join(', ') || 'N/A'}</small>
</div>
</div>
<div class="col-md-6">
<div class="d-flex align-items-center">
<i class="bi bi-translate me-2"></i>
<small>${match.languages.join(', ') || 'N/A'}</small>
</div>
</div>
</div>
<!-- Stars/Cast -->
${match.stars && match.stars.length > 0 ? `
<div class="mb-3">
<div class="d-flex align-items-center gap-2">
<i class="bi bi-person-circle"></i>
<small class="text-muted">${match.stars.join(', ')}</small>
</div>
</div>
` : ''}
<!-- Action Buttons -->
<div class="d-flex justify-content-end gap-2">
<button class="btn btn-primary btn-sm"
data-match='${safeMatchData}'
data-torrent='${safeTorrentData}'
onclick="selectMatchFromData(this)">
<i class="bi bi-check2 me-1"></i>
Select This Match
</button>
</div>
</div>
</div>
</div>
@@ -705,11 +792,108 @@ function displayMatchResults(matches, torrentData) {
}
function selectMatchFromData(button) {
const match = JSON.parse(atob(button.getAttribute('data-match')));
const torrentData = JSON.parse(atob(button.getAttribute('data-torrent')));
const match = JSON.parse(decodeURIComponent(button.getAttribute('data-match')));
const torrentData = JSON.parse(decodeURIComponent(button.getAttribute('data-torrent')));
selectMatch(match, torrentData);
}
function formatTechnicalSpec(value, type) {
if (!value) return '<span class="not-available">Not Set</span>';
switch (type) {
case 'resolution':
return `<span class="spec-value">${value}</span>`;
case 'audio' || 'languages' || 'hdr':
return Array.isArray(value)
? value.join(', ')
: value.split(',').join(', ');
default:
return value;
}
}
function updateBasicTechnicalSpecs(torrentData = {}) {
const specsContainer = document.querySelector('.technical-specs-basic');
if (!specsContainer) return;
const specs = [
{icon: 'bi-display', label: 'Resolution', value: torrentData.resolution, type: 'resolution'},
{icon: 'bi-camera-reels', label: 'Quality', value: torrentData.quality, type: 'quality'},
{icon: 'bi-film', label: 'Video Codec', value: torrentData.codec, type: 'codec'},
{icon: 'bi-music-note-beamed', label: 'Audio', value: torrentData.audio, type: 'audio'},
{icon: 'bi-translate', label: 'Languages', value: torrentData.languages, type: 'languages'},
{icon: 'bi-tv', label: 'HDR', value: torrentData.hdr, type: 'hdr'},
];
specsContainer.innerHTML = specs.map(spec => `
<div class="spec-item">
<i class="bi ${spec.icon}"></i>
<div>
<div class="spec-label">${spec.label}</div>
<div class="spec-value" id="${spec.type}Spec" >${formatTechnicalSpec(spec.value, spec.type)}</div>
</div>
</div>
`).join('');
}
// Function to set up the advanced toggle system
function setupAdvancedToggle() {
document.getElementById('toggleAdvanced').addEventListener('click', () => {
const toggleButton = document.getElementById('toggleAdvanced');
toggleButton.classList.toggle('active');
const advancedSection = document.getElementById('advancedSection');
advancedSection.classList.toggle('show');
});
// Initial update of basic specs
updateBasicTechnicalSpecs();
}
function updateSpecField(fieldId, value) {
const targetInput = document.getElementById(`${fieldId}Spec`);
if (targetInput) {
targetInput.innerHTML = formatTechnicalSpec(value, fieldId);
}
}
function setupFieldChangeHandlers() {
// Audio codecs handler
const audioCodecsInputs = document.querySelectorAll('input[name="audioCodecs"]');
audioCodecsInputs.forEach(input => {
input.addEventListener('change', () => {
const selectedAudioCodecs = Array.from(audioCodecsInputs)
.filter(input => input.checked)
.map(input => input.value)
.join(', ');
updateSpecField('audio', selectedAudioCodecs);
});
});
// HDR formats handler
const hdrFormatsInputs = document.querySelectorAll('input[name="hdrFormats"]');
hdrFormatsInputs.forEach(input => {
input.addEventListener('change', () => {
const selectedHdrFormats = Array.from(hdrFormatsInputs)
.filter(input => input.checked)
.map(input => input.value)
.join(', ');
updateSpecField('hdr', selectedHdrFormats);
});
});
// Languages handler
const languageInputs = document.querySelectorAll('input[name="languages"]');
languageInputs.forEach(input => {
input.addEventListener('change', () => {
const selectedLanguages = Array.from(languageInputs)
.filter(input => input.checked)
.map(input => input.value)
.join(', ');
updateSpecField('languages', selectedLanguages);
});
});
}
// Utility functions for form handling
const formUtils = {
resetForm(preserveFileAndMagnet = false) {
@@ -724,7 +908,7 @@ const formUtils = {
// Reset basic fields
const basicFields = ['torrentImdbId', 'metaType', 'title', 'poster', 'background', 'logo',
'resolution', 'quality', 'videoCodec', 'createdAt'];
'resolution', 'quality', 'videoCodec', 'createdAt'];
basicFields.forEach(fieldId => {
const element = document.getElementById(fieldId);
if (element) element.value = '';
@@ -770,7 +954,7 @@ const formUtils = {
const values = Array.isArray(items) ? items : items.split(',');
values.forEach(value => {
const checkboxId = `${prefix}-${value.toLowerCase().replace(/[^a-z0-9]/g, '')}`;
const checkboxId = `${prefix}-${value.replace(/[^a-zA-Z0-9]/g, '')}`;
const checkbox = document.getElementById(checkboxId);
if (checkbox) checkbox.checked = true;
});
@@ -864,6 +1048,9 @@ function switchToManualImport(torrentData) {
function applyTorrentData(torrentData) {
if (!torrentData) return;
// Update basic specs display
updateBasicTechnicalSpecs(torrentData);
// check title and if its empty then add title from torrentData
if (!document.getElementById('title').value) {
document.getElementById('title').value = torrentData.title;
@@ -1330,5 +1517,7 @@ async function submitScraperForm() {
// Initial update for form fields on page load
document.addEventListener('DOMContentLoaded', function () {
handleInitialSetup();
setupAdvancedToggle();
setupFieldChangeHandlers();
});
document.getElementById('spiderName').addEventListener('change', toggleSpiderSpecificFields);
+2
View File
@@ -170,6 +170,8 @@ def parse_imdb_title(imdb_title: model.Title, episodes: list[dict]) -> dict:
"year": imdb_title.year,
"end_year": end_year,
"description": imdb_title.plot.get("en-US"),
"countries": imdb_title.countries,
"languages": imdb_title.languages,
"genres": imdb_title.genres,
"imdb_rating": float(imdb_title.rating) if imdb_title.rating else None,
"aka_titles": list(set(aka.title for aka in imdb_title.akas)),
+6 -5
View File
@@ -325,10 +325,11 @@ async def add_torrent(
raise_error("Failed to create metadata for sports content.")
meta_id = metadata_result["id"]
elif not meta_id:
elif not meta_id or meta_id.startswith("mf"):
# search for metadata from imdb/tmdb
metadata_result = await get_or_create_metadata(
{
"id": meta_id,
"title": title or torrent_data.get("title"),
"year": torrent_data.get("year") or created_at.year,
"poster": poster,
@@ -339,13 +340,13 @@ async def add_torrent(
"created_at": created_at,
},
meta_type,
is_search_imdb_title=True,
is_search_imdb_title=meta_id is None,
)
meta_id = metadata_result["id"]
else:
# Regular IMDb content validation
if not meta_id or not meta_id.startswith("tt") or not meta_id[2:].isdigit():
raise_error("Invalid IMDb ID. Must start with 'tt'.")
if not meta_id or not meta_id.startswith(("tt", "mf")):
raise_error("Invalid IMDb ID. Must start with 'tt' or 'mf'.")
# For series, check if we need file annotation
if meta_type == "series":
@@ -732,6 +733,7 @@ async def analyze_torrent(
status_code=400, detail="Could not extract title from torrent"
)
torrent_data["type"] = meta_type
if meta_type == "sports":
# parse title for sports content
title = torrent_data["torrent_name"]
@@ -748,7 +750,6 @@ async def analyze_torrent(
)
title = re.sub(r"\s+", " ", title).strip()
torrent_data["title"] = title
torrent_data["type"] = "sports"
return {
"torrent_data": torrent_data,
"matches": [],
+2 -5
View File
@@ -382,11 +382,8 @@ class MetadataFetcher:
logging.error(f"Error searching IMDB: {e}")
return []
# Run searches in parallel
tmdb_candidates, imdb_candidates = await asyncio.gather(
get_tmdb_candidates(), get_imdb_candidates()
)
imdb_candidates = await get_imdb_candidates()
tmdb_candidates = await get_tmdb_candidates()
return tmdb_candidates + imdb_candidates
+88 -149
View File
@@ -18,7 +18,7 @@ TMDB_IMAGE_BASE_URL = "https://image.tmdb.org/t/p/"
async def get_tmdb_data(
tmdb_id: str, media_type: str, max_retries: int = 3
tmdb_id: str, media_type: str, max_retries: int = 3, load_episodes: bool = True
) -> Optional[Dict[str, Any]]:
"""
Enhanced TMDB data fetcher with retry logic and better error handling
@@ -45,10 +45,13 @@ async def get_tmdb_data(
data = response.json()
episodes = []
for season_number in range(1, data["number_of_seasons"] + 1):
episodes.extend(
await get_tmdb_season_episodes(str(data["id"]), season_number)
)
if media_type == "series" and load_episodes:
for season_number in range(1, data.get("number_of_seasons", 0) + 1):
episodes.extend(
await get_tmdb_season_episodes(
str(data["id"]), season_number
)
)
return format_tmdb_response(data, media_type, episodes)
except httpx.TimeoutException:
@@ -157,7 +160,8 @@ def format_tmdb_response(
formatted = {
"tmdb_id": str(data["id"]),
"imdb_id": data["external_ids"].get("imdb_id") or f"mf{uuid4().fields[-1]}",
"imdb_id": data.get("external_ids", {}).get("imdb_id")
or f"mf{uuid4().fields[-1]}",
"title": data.get("title" if is_movie else "name"),
"original_title": data.get("original_title" if is_movie else "original_name"),
"year": int(release_date[:4]) if release_date else None,
@@ -172,6 +176,10 @@ def format_tmdb_response(
else None
),
"description": data.get("overview"),
"countries": [
country["name"] for country in data.get("production_countries", [])
],
"languages": [lang["name"] for lang in data.get("spoken_languages", [])],
"genres": [genre["name"] for genre in data.get("genres", [])],
"tmdb_rating": data.get("vote_average"),
"runtime": f"{data.get('runtime')} min" if data.get("runtime") else None,
@@ -255,6 +263,65 @@ async def get_tmdb_data_by_imdb(
return None
def calculate_year_score(
result: Dict[str, Any], year: int, created_year: int
) -> Tuple[bool, float]:
"""Calculate year match score and validity"""
try:
result_date = result.get("release_date") or result.get("first_air_date")
if not result_date:
return False, float("inf")
try:
result_year = int(result_date[:4])
except (ValueError, TypeError, IndexError):
return False, float("inf")
# Handle exact year matching
if year is not None:
if result.get("first_air_date"):
last_air_date = result.get("last_air_date", "")
try:
end_year = int(last_air_date[:4]) if last_air_date else None
except (ValueError, TypeError, IndexError):
end_year = None
if end_year is None:
end_year = math.inf if result_year <= year else None
if end_year is not None:
return (result_year <= year <= end_year), (
0 if (result_year <= year <= end_year) else float("inf")
)
return False, float("inf")
else:
return result_year == year, (0 if result_year == year else float("inf"))
# Use created_year for sorting when no exact year match is required
if created_year:
if result.get("first_air_date"):
last_air_date = result.get("last_air_date", "")
try:
end_year = int(last_air_date[:4]) if last_air_date else None
except (ValueError, TypeError, IndexError):
end_year = None
if end_year is None:
return True, abs(result_year - created_year)
return True, min(
abs(result_year - created_year), abs(end_year - created_year)
)
else:
return True, abs(result_year - created_year)
return True, 0 # No year criteria
except Exception as err:
logging.error(f"TMDB search: Error calculating year score: {err}")
return False, float("inf")
async def search_tmdb(
title: str,
year: int | None,
@@ -271,71 +338,6 @@ async def search_tmdb(
logging.error("TMDB API key is not set")
return {}
def calculate_year_difference(result: Dict[str, Any]) -> float | None:
"""Calculate year difference score for sorting"""
try:
result_type = result.get("media_type")
if media_type and result_type != media_type:
return None
# Extract year based on media type
date_field = "release_date" if result_type == "movie" else "first_air_date"
result_date = result.get(date_field, "")
if not result_date:
return None
try:
result_year = int(result_date[:4])
except (ValueError, TypeError, IndexError):
return None
# If target year is provided, only allow exact matches
if year is not None:
if result_type == "movie":
return 0 if result_year == year else None
else:
last_air_date = result.get("last_air_date", "")
try:
end_year = int(last_air_date[:4]) if last_air_date else None
except (ValueError, TypeError, IndexError):
end_year = None
# If it's an ongoing series, use math.inf for end_year
if end_year is None:
end_year = math.inf if result_year <= year else None
# Only proceed if we have valid end_year
if end_year is not None:
return 0 if (result_year <= year <= end_year) else None
return None
# Only use created_year for sorting when target year is None
if created_year:
if result_type == "movie":
return abs(result_year - created_year)
else:
# For series, use the minimum difference between created_year
# and either start or end year
last_air_date = result.get("last_air_date", "")
try:
end_year = int(last_air_date[:4]) if last_air_date else None
except (ValueError, TypeError, IndexError):
end_year = None
if end_year is None:
# If series is ongoing/unknown end, just use start year difference
return abs(result_year - created_year)
return min(
abs(result_year - created_year), abs(end_year - created_year)
)
return float("inf") # No year information available
except (AttributeError, ValueError) as err:
logging.error(f"TMDB search: Error calculating year difference: {err}")
return float("inf")
if media_type:
media_type = "movie" if media_type == "movie" else "tv"
endpoint = f"search/{media_type}"
@@ -371,20 +373,19 @@ async def search_tmdb(
# Filter by title similarity and calculate year differences
candidates = []
for result in results:
result_title = (
result.get("title")
if result.get("media_type") == "movie"
else result.get("name")
)
result_title = result.get("name") or result.get("title")
if (
not result_title
or fuzz.ratio(result_title.lower(), title.lower()) < 85
):
continue
year_diff = calculate_year_difference(result)
if year_diff is not None: # Only include valid matches
candidates.append((result, year_diff))
valid_year, year_score = calculate_year_score(
result, year, created_year
)
if not valid_year:
continue
candidates.append((result, year_score))
# Sort candidates by year difference
candidates.sort(key=lambda x: x[1])
@@ -430,69 +431,6 @@ async def search_multiple_tmdb(
logging.error("TMDB API key is not set")
return []
def calculate_year_score(result: Dict[str, Any]) -> Tuple[bool, float]:
"""Calculate year match score and validity"""
try:
result_type = result.get("media_type")
if media_type and result_type != media_type:
return False, float("inf")
date_field = "release_date" if result_type == "movie" else "first_air_date"
result_date = result.get(date_field, "")
if not result_date:
return False, float("inf")
try:
result_year = int(result_date[:4])
except (ValueError, TypeError, IndexError):
return False, float("inf")
# Handle exact year matching
if year is not None:
if result_type == "movie":
return result_year == year, (
0 if result_year == year else float("inf")
)
else:
last_air_date = result.get("last_air_date", "")
try:
end_year = int(last_air_date[:4]) if last_air_date else None
except (ValueError, TypeError, IndexError):
end_year = None
if end_year is None:
end_year = math.inf if result_year <= year else None
if end_year is not None:
return (result_year <= year <= end_year), (
0 if (result_year <= year <= end_year) else float("inf")
)
return False, float("inf")
# Use created_year for sorting when no exact year match is required
if created_year:
if result_type == "movie":
return True, abs(result_year - created_year)
else:
last_air_date = result.get("last_air_date", "")
try:
end_year = int(last_air_date[:4]) if last_air_date else None
except (ValueError, TypeError, IndexError):
end_year = None
if end_year is None:
return True, abs(result_year - created_year)
return True, min(
abs(result_year - created_year), abs(end_year - created_year)
)
return True, 0 # No year criteria
except Exception as err:
logging.error(f"TMDB search: Error calculating year score: {err}")
return False, float("inf")
if media_type:
media_type = "movie" if media_type == "movie" else "tv"
endpoint = f"search/{media_type}"
@@ -522,11 +460,7 @@ async def search_multiple_tmdb(
candidates = []
for result in results:
result_title = (
result.get("title")
if result.get("media_type") == "movie"
else result.get("name")
)
result_title = result.get("name") or result.get("title")
if not result_title:
continue
@@ -535,12 +469,15 @@ async def search_multiple_tmdb(
if similarity < min_similarity:
continue
valid_year, year_score = calculate_year_score(result)
valid_year, year_score = calculate_year_score(
result, year, created_year
)
if not valid_year:
continue
# Combine similarity and year score for ranking
combined_score = (similarity / 100.0) * (1.0 / (1.0 + year_score))
result["match_score"] = combined_score
candidates.append((result, combined_score))
# Sort by combined score and take top results
@@ -551,7 +488,9 @@ async def search_multiple_tmdb(
full_results = []
for candidate, _ in top_candidates:
try:
result = format_tmdb_response(candidate, media_type, [])
result = await get_tmdb_data(
str(candidate["id"]), media_type, load_episodes=False
)
full_results.append(result)
except Exception as err:
logging.error(f"Error fetching TMDB data for candidate: {err}")