fix: search issues in webinterfacey
Local Docker Build (Dev) / build-dev (pull_request) Successful in 4s
Local Docker Build (Dev) / build-dev (pull_request) Successful in 4s
This commit is contained in:
+15
-3
@@ -16,7 +16,8 @@ from api.models import (
|
|||||||
)
|
)
|
||||||
from api.web_routes import (
|
from api.web_routes import (
|
||||||
get_movies_list, get_tv_series_list, get_series_episodes, get_missing_dates_report,
|
get_movies_list, get_tv_series_list, get_series_episodes, get_missing_dates_report,
|
||||||
get_dashboard_stats, update_movie_date, update_episode_date, bulk_update_source
|
get_dashboard_stats, update_movie_date, update_episode_date, bulk_update_source,
|
||||||
|
get_movie_date_options, get_episode_date_options
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -1022,9 +1023,10 @@ def register_routes(app, dependencies: dict):
|
|||||||
|
|
||||||
@app.get("/api/movies")
|
@app.get("/api/movies")
|
||||||
async def _movies_list(skip: int = 0, limit: int = 100, has_date: Optional[bool] = None,
|
async def _movies_list(skip: int = 0, limit: int = 100, has_date: Optional[bool] = None,
|
||||||
source_filter: Optional[str] = None, search: Optional[str] = None):
|
source_filter: Optional[str] = None, search: Optional[str] = None,
|
||||||
|
imdb_search: Optional[str] = None):
|
||||||
"""Get paginated movies list with filtering"""
|
"""Get paginated movies list with filtering"""
|
||||||
return await get_movies_list(dependencies, skip, limit, has_date, source_filter, search)
|
return await get_movies_list(dependencies, skip, limit, has_date, source_filter, search, imdb_search)
|
||||||
|
|
||||||
@app.get("/api/series")
|
@app.get("/api/series")
|
||||||
async def _series_list(skip: int = 0, limit: int = 50, search: Optional[str] = None):
|
async def _series_list(skip: int = 0, limit: int = 50, search: Optional[str] = None):
|
||||||
@@ -1056,6 +1058,16 @@ def register_routes(app, dependencies: dict):
|
|||||||
"""Bulk update source for movies or episodes"""
|
"""Bulk update source for movies or episodes"""
|
||||||
return await bulk_update_source(dependencies, request.media_type, request.old_source, request.new_source)
|
return await bulk_update_source(dependencies, request.media_type, request.old_source, request.new_source)
|
||||||
|
|
||||||
|
@app.get("/api/movies/{imdb_id}/date-options")
|
||||||
|
async def _movie_date_options(imdb_id: str):
|
||||||
|
"""Get available date options for a movie"""
|
||||||
|
return await get_movie_date_options(dependencies, imdb_id)
|
||||||
|
|
||||||
|
@app.get("/api/episodes/{imdb_id}/{season}/{episode}/date-options")
|
||||||
|
async def _episode_date_options(imdb_id: str, season: int, episode: int):
|
||||||
|
"""Get available date options for an episode"""
|
||||||
|
return await get_episode_date_options(dependencies, imdb_id, season, episode)
|
||||||
|
|
||||||
# ---------------------------
|
# ---------------------------
|
||||||
# Static Web Interface
|
# Static Web Interface
|
||||||
# ---------------------------
|
# ---------------------------
|
||||||
|
|||||||
+132
-1
@@ -20,7 +20,8 @@ async def get_movies_list(dependencies: dict,
|
|||||||
limit: int = Query(100, le=1000),
|
limit: int = Query(100, le=1000),
|
||||||
has_date: Optional[bool] = Query(None),
|
has_date: Optional[bool] = Query(None),
|
||||||
source_filter: Optional[str] = Query(None),
|
source_filter: Optional[str] = Query(None),
|
||||||
search: Optional[str] = Query(None)):
|
search: Optional[str] = Query(None),
|
||||||
|
imdb_search: Optional[str] = Query(None)):
|
||||||
"""Get paginated list of movies with filtering options"""
|
"""Get paginated list of movies with filtering options"""
|
||||||
db = dependencies["db"]
|
db = dependencies["db"]
|
||||||
|
|
||||||
@@ -45,6 +46,10 @@ async def get_movies_list(dependencies: dict,
|
|||||||
where_conditions.append("(imdb_id LIKE ? OR path LIKE ?)")
|
where_conditions.append("(imdb_id LIKE ? OR path LIKE ?)")
|
||||||
params.extend([f"%{search}%", f"%{search}%"])
|
params.extend([f"%{search}%", f"%{search}%"])
|
||||||
|
|
||||||
|
if imdb_search:
|
||||||
|
where_conditions.append("imdb_id LIKE ?")
|
||||||
|
params.append(f"%{imdb_search}%")
|
||||||
|
|
||||||
where_clause = " AND ".join(where_conditions) if where_conditions else "1=1"
|
where_clause = " AND ".join(where_conditions) if where_conditions else "1=1"
|
||||||
|
|
||||||
# Get total count
|
# Get total count
|
||||||
@@ -432,3 +437,129 @@ async def bulk_update_source(dependencies: dict, media_type: str, old_source: st
|
|||||||
"status": "success",
|
"status": "success",
|
||||||
"message": f"Updated {updated_count} {media_type} from source '{old_source}' to '{new_source}'"
|
"message": f"Updated {updated_count} {media_type} from source '{old_source}' to '{new_source}'"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def get_movie_date_options(dependencies: dict, imdb_id: str):
|
||||||
|
"""Get available date options for a movie (Radarr import, digital release, etc.)"""
|
||||||
|
db = dependencies["db"]
|
||||||
|
nfo_manager = dependencies["nfo_manager"]
|
||||||
|
|
||||||
|
# Get current movie data
|
||||||
|
movie = db.get_movie_dates(imdb_id)
|
||||||
|
if not movie:
|
||||||
|
raise HTTPException(status_code=404, detail="Movie not found")
|
||||||
|
|
||||||
|
options = []
|
||||||
|
|
||||||
|
# Option 1: Current dateadded (if exists and is different from released)
|
||||||
|
if movie.get('dateadded'):
|
||||||
|
current_source = movie.get('source', 'Unknown')
|
||||||
|
current_date = movie['dateadded']
|
||||||
|
|
||||||
|
# Determine what type of current date this is
|
||||||
|
if 'radarr' in current_source.lower() and 'import' in current_source.lower():
|
||||||
|
label = "Keep Current (Radarr Import Date)"
|
||||||
|
description = f"Keep using Radarr download/import date: {current_date}"
|
||||||
|
elif current_source == 'digital_release':
|
||||||
|
label = "Keep Current (Digital Release)"
|
||||||
|
description = f"Keep using digital release date: {current_date}"
|
||||||
|
elif current_source == 'nfo_file_existing':
|
||||||
|
label = "Keep Current (From Existing NFO)"
|
||||||
|
description = f"Keep using date from existing NFO file: {current_date}"
|
||||||
|
else:
|
||||||
|
label = f"Keep Current ({current_source})"
|
||||||
|
description = f"Keep using current date from {current_source}: {current_date}"
|
||||||
|
|
||||||
|
options.append({
|
||||||
|
"type": "current",
|
||||||
|
"label": label,
|
||||||
|
"date": current_date,
|
||||||
|
"source": current_source,
|
||||||
|
"description": description
|
||||||
|
})
|
||||||
|
|
||||||
|
# Option 2: Released date as digital release (if different from current)
|
||||||
|
if movie.get('released'):
|
||||||
|
release_date = f"{movie['released']}T00:00:00"
|
||||||
|
# Only add if it's different from current dateadded
|
||||||
|
current_dateadded = movie.get('dateadded', '')
|
||||||
|
if not current_dateadded or not current_dateadded.startswith(movie['released']):
|
||||||
|
options.append({
|
||||||
|
"type": "digital_release",
|
||||||
|
"label": "Use Actual Release Date",
|
||||||
|
"date": release_date,
|
||||||
|
"source": "digital_release",
|
||||||
|
"description": f"Use the movie's actual release date: {movie['released']} (instead of download date)"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Option 3: Manual entry
|
||||||
|
options.append({
|
||||||
|
"type": "manual",
|
||||||
|
"label": "Manual Entry",
|
||||||
|
"date": None,
|
||||||
|
"source": "manual",
|
||||||
|
"description": "Enter custom date and time"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Try to get additional metadata from external sources
|
||||||
|
try:
|
||||||
|
# This would integrate with your existing TMDB/OMDB logic
|
||||||
|
# For now, we'll use the existing released date as an example
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return {
|
||||||
|
"imdb_id": imdb_id,
|
||||||
|
"current_data": movie,
|
||||||
|
"options": options
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def get_episode_date_options(dependencies: dict, imdb_id: str, season: int, episode: int):
|
||||||
|
"""Get available date options for an episode"""
|
||||||
|
db = dependencies["db"]
|
||||||
|
|
||||||
|
# Get current episode data
|
||||||
|
episode_data = db.get_episode_date(imdb_id, season, episode)
|
||||||
|
if not episode_data:
|
||||||
|
raise HTTPException(status_code=404, detail="Episode not found")
|
||||||
|
|
||||||
|
options = []
|
||||||
|
|
||||||
|
# Option 1: Current dateadded (if exists)
|
||||||
|
if episode_data.get('dateadded'):
|
||||||
|
options.append({
|
||||||
|
"type": "current",
|
||||||
|
"label": f"Keep Current ({episode_data.get('source', 'Unknown')})",
|
||||||
|
"date": episode_data['dateadded'],
|
||||||
|
"source": episode_data.get('source', 'manual'),
|
||||||
|
"description": f"Currently using: {episode_data.get('source', 'Unknown')}"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Option 2: Aired date
|
||||||
|
if episode_data.get('aired'):
|
||||||
|
options.append({
|
||||||
|
"type": "airdate",
|
||||||
|
"label": "Use Air Date",
|
||||||
|
"date": f"{episode_data['aired']}T20:00:00", # Default to 8 PM
|
||||||
|
"source": "airdate",
|
||||||
|
"description": f"Use original air date: {episode_data['aired']}"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Option 3: Manual entry
|
||||||
|
options.append({
|
||||||
|
"type": "manual",
|
||||||
|
"label": "Manual Entry",
|
||||||
|
"date": None,
|
||||||
|
"source": "manual",
|
||||||
|
"description": "Enter custom date and time"
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"imdb_id": imdb_id,
|
||||||
|
"season": season,
|
||||||
|
"episode": episode,
|
||||||
|
"current_data": episode_data,
|
||||||
|
"options": options
|
||||||
|
}
|
||||||
@@ -214,6 +214,19 @@ body {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
.search-box {
|
.search-box {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -605,6 +618,135 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Smart Fix Modal */
|
||||||
|
.smart-fix-options {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-card {
|
||||||
|
border: 2px solid var(--border-color);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-card:hover {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-label {
|
||||||
|
display: block;
|
||||||
|
padding: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-label input[type="radio"] {
|
||||||
|
margin-right: 0.75rem;
|
||||||
|
margin-top: 0.1rem;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-content h4 {
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
color: var(--dark-color);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-content p {
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-content small {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manual-date-input {
|
||||||
|
width: 100% !important;
|
||||||
|
margin-top: 0.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-card input[type="radio"]:checked + .option-content {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-card:has(input[type="radio"]:checked) {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
background-color: rgba(0, 123, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Additional badge styles */
|
||||||
|
.badge-info {
|
||||||
|
background-color: #d1ecf1;
|
||||||
|
color: #0c5460;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced Edit Modal Date Options */
|
||||||
|
.date-options {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-option-card {
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-option-card:hover {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-option-label {
|
||||||
|
display: block;
|
||||||
|
padding: 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-option-label input[type="radio"] {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
margin-top: 0.1rem;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-option-content h4 {
|
||||||
|
margin: 0 0 0.25rem 0;
|
||||||
|
color: var(--dark-color);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-option-content p {
|
||||||
|
margin: 0 0 0.25rem 0;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-option-content small {
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-option-card input[type="radio"]:checked + .date-option-content h4 {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-option-card:has(input[type="radio"]:checked) {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
background-color: rgba(0, 123, 255, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive */
|
/* Responsive */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.content-header {
|
.content-header {
|
||||||
|
|||||||
+31
-4
@@ -101,10 +101,17 @@
|
|||||||
<div class="content-header">
|
<div class="content-header">
|
||||||
<h2><i class="fas fa-film"></i> Movies Database</h2>
|
<h2><i class="fas fa-film"></i> Movies Database</h2>
|
||||||
<div class="content-controls">
|
<div class="content-controls">
|
||||||
|
<div class="search-controls">
|
||||||
<div class="search-box">
|
<div class="search-box">
|
||||||
<i class="fas fa-search"></i>
|
<i class="fas fa-search"></i>
|
||||||
<input type="text" id="movies-search" placeholder="Search movies...">
|
<input type="text" id="movies-search" placeholder="Search title/path...">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="search-box">
|
||||||
|
<i class="fas fa-hashtag"></i>
|
||||||
|
<input type="text" id="movies-imdb-search" placeholder="Search IMDb ID...">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="filter-controls">
|
||||||
<select id="movies-filter-date">
|
<select id="movies-filter-date">
|
||||||
<option value="">All Movies</option>
|
<option value="">All Movies</option>
|
||||||
<option value="true">With Dates</option>
|
<option value="true">With Dates</option>
|
||||||
@@ -118,6 +125,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<table class="data-table" id="movies-table">
|
<table class="data-table" id="movies-table">
|
||||||
@@ -128,13 +136,14 @@
|
|||||||
<th>Released</th>
|
<th>Released</th>
|
||||||
<th>Date Added</th>
|
<th>Date Added</th>
|
||||||
<th>Source</th>
|
<th>Source</th>
|
||||||
|
<th>Date Type</th>
|
||||||
<th>Video File</th>
|
<th>Video File</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="movies-tbody">
|
<tbody id="movies-tbody">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="7" class="loading">Loading movies...</td>
|
<td colspan="8" class="loading">Loading movies...</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -216,7 +225,7 @@
|
|||||||
<th>IMDb ID</th>
|
<th>IMDb ID</th>
|
||||||
<th>Released</th>
|
<th>Released</th>
|
||||||
<th>Source</th>
|
<th>Source</th>
|
||||||
<th>Quick Fix</th>
|
<th>Smart Fix</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="report-movies-tbody">
|
<tbody id="report-movies-tbody">
|
||||||
@@ -239,7 +248,7 @@
|
|||||||
<th>IMDb ID</th>
|
<th>IMDb ID</th>
|
||||||
<th>Aired</th>
|
<th>Aired</th>
|
||||||
<th>Source</th>
|
<th>Source</th>
|
||||||
<th>Quick Fix</th>
|
<th>Smart Fix</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="report-episodes-tbody">
|
<tbody id="report-episodes-tbody">
|
||||||
@@ -308,6 +317,24 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Smart Fix Modal -->
|
||||||
|
<div class="modal" id="smart-fix-modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 id="smart-fix-title">Choose Date Source</h3>
|
||||||
|
<button class="modal-close" onclick="closeSmartFixModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div id="smart-fix-content">
|
||||||
|
<p>Loading available options...</p>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="closeSmartFixModal()">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Edit Modal -->
|
<!-- Edit Modal -->
|
||||||
<div class="modal" id="edit-modal">
|
<div class="modal" id="edit-modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
|||||||
+286
-50
@@ -61,6 +61,7 @@ function switchTab(tabName) {
|
|||||||
function initializeEventListeners() {
|
function initializeEventListeners() {
|
||||||
// Search inputs
|
// Search inputs
|
||||||
document.getElementById('movies-search').addEventListener('input', debounce(loadMovies, 500));
|
document.getElementById('movies-search').addEventListener('input', debounce(loadMovies, 500));
|
||||||
|
document.getElementById('movies-imdb-search').addEventListener('input', debounce(loadMovies, 500));
|
||||||
document.getElementById('series-search').addEventListener('input', debounce(loadSeries, 500));
|
document.getElementById('series-search').addEventListener('input', debounce(loadSeries, 500));
|
||||||
|
|
||||||
// Filter dropdowns
|
// Filter dropdowns
|
||||||
@@ -171,6 +172,7 @@ function getChartColor(index) {
|
|||||||
// Movies
|
// Movies
|
||||||
async function loadMovies(page = 1) {
|
async function loadMovies(page = 1) {
|
||||||
const search = document.getElementById('movies-search').value;
|
const search = document.getElementById('movies-search').value;
|
||||||
|
const imdbSearch = document.getElementById('movies-imdb-search').value;
|
||||||
const hasDate = document.getElementById('movies-filter-date').value;
|
const hasDate = document.getElementById('movies-filter-date').value;
|
||||||
const sourceFilter = document.getElementById('movies-filter-source').value;
|
const sourceFilter = document.getElementById('movies-filter-source').value;
|
||||||
|
|
||||||
@@ -180,6 +182,7 @@ async function loadMovies(page = 1) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (search) params.append('search', search);
|
if (search) params.append('search', search);
|
||||||
|
if (imdbSearch) params.append('imdb_search', imdbSearch);
|
||||||
if (hasDate) params.append('has_date', hasDate);
|
if (hasDate) params.append('has_date', hasDate);
|
||||||
if (sourceFilter) params.append('source_filter', sourceFilter);
|
if (sourceFilter) params.append('source_filter', sourceFilter);
|
||||||
|
|
||||||
@@ -198,19 +201,37 @@ function updateMoviesTable(data) {
|
|||||||
const tbody = document.getElementById('movies-tbody');
|
const tbody = document.getElementById('movies-tbody');
|
||||||
|
|
||||||
if (!data.movies || data.movies.length === 0) {
|
if (!data.movies || data.movies.length === 0) {
|
||||||
tbody.innerHTML = '<tr><td colspan="7" class="text-center">No movies found</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="8" class="text-center">No movies found</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody.innerHTML = data.movies.map(movie => {
|
tbody.innerHTML = data.movies.map(movie => {
|
||||||
const dateadded = movie.dateadded ? formatDateTime(movie.dateadded) : '';
|
const dateadded = movie.dateadded ? formatDateTime(movie.dateadded) : '';
|
||||||
const hasDateBadge = movie.dateadded ?
|
|
||||||
'<span class="badge badge-success">Yes</span>' :
|
|
||||||
'<span class="badge badge-warning">No</span>';
|
|
||||||
const hasVideoBadge = movie.has_video_file ?
|
const hasVideoBadge = movie.has_video_file ?
|
||||||
'<span class="badge badge-success">Yes</span>' :
|
'<span class="badge badge-success">Yes</span>' :
|
||||||
'<span class="badge badge-secondary">No</span>';
|
'<span class="badge badge-secondary">No</span>';
|
||||||
|
|
||||||
|
// Determine date type based on source and dates
|
||||||
|
let dateType = 'Unknown';
|
||||||
|
let dateTypeBadge = 'badge-secondary';
|
||||||
|
|
||||||
|
if (movie.source === 'digital_release') {
|
||||||
|
dateType = 'Digital Release';
|
||||||
|
dateTypeBadge = 'badge-success';
|
||||||
|
} else if (movie.source && movie.source.includes('radarr') && movie.source.includes('import')) {
|
||||||
|
dateType = 'Radarr Import';
|
||||||
|
dateTypeBadge = 'badge-warning';
|
||||||
|
} else if (movie.source === 'manual') {
|
||||||
|
dateType = 'Manual';
|
||||||
|
dateTypeBadge = 'badge-info';
|
||||||
|
} else if (movie.source === 'nfo_file_existing') {
|
||||||
|
dateType = 'Existing NFO';
|
||||||
|
dateTypeBadge = 'badge-secondary';
|
||||||
|
} else if (movie.source === 'no_valid_date_source') {
|
||||||
|
dateType = 'No Valid Source';
|
||||||
|
dateTypeBadge = 'badge-danger';
|
||||||
|
}
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<tr>
|
<tr>
|
||||||
<td>${escapeHtml(movie.title)}</td>
|
<td>${escapeHtml(movie.title)}</td>
|
||||||
@@ -218,6 +239,7 @@ function updateMoviesTable(data) {
|
|||||||
<td>${movie.released || '-'}</td>
|
<td>${movie.released || '-'}</td>
|
||||||
<td>${dateadded || '-'}</td>
|
<td>${dateadded || '-'}</td>
|
||||||
<td><span class="badge badge-secondary">${movie.source || 'Unknown'}</span></td>
|
<td><span class="badge badge-secondary">${movie.source || 'Unknown'}</span></td>
|
||||||
|
<td><span class="badge ${dateTypeBadge}">${dateType}</span></td>
|
||||||
<td>${hasVideoBadge}</td>
|
<td>${hasVideoBadge}</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn btn-sm btn-primary" onclick="editMovie('${movie.imdb_id}', '${dateadded}', '${movie.source || ''}')">
|
<button class="btn btn-sm btn-primary" onclick="editMovie('${movie.imdb_id}', '${dateadded}', '${movie.source || ''}')">
|
||||||
@@ -460,8 +482,8 @@ function updateReportTables(data) {
|
|||||||
<td>${movie.released || '-'}</td>
|
<td>${movie.released || '-'}</td>
|
||||||
<td><span class="badge badge-warning">${movie.source || 'Unknown'}</span></td>
|
<td><span class="badge badge-warning">${movie.source || 'Unknown'}</span></td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn btn-sm btn-success" onclick="quickFixMovie('${movie.imdb_id}', '${movie.released || ''}')">
|
<button class="btn btn-sm btn-success" onclick="smartFixMovie('${movie.imdb_id}')">
|
||||||
<i class="fas fa-magic"></i> Fix
|
<i class="fas fa-magic"></i> Smart Fix
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -481,8 +503,8 @@ function updateReportTables(data) {
|
|||||||
<td>${episode.aired || '-'}</td>
|
<td>${episode.aired || '-'}</td>
|
||||||
<td><span class="badge badge-warning">${episode.source || 'Unknown'}</span></td>
|
<td><span class="badge badge-warning">${episode.source || 'Unknown'}</span></td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn btn-sm btn-success" onclick="quickFixEpisode('${episode.imdb_id}', ${episode.season}, ${episode.episode}, '${episode.aired || ''}')">
|
<button class="btn btn-sm btn-success" onclick="smartFixEpisode('${episode.imdb_id}', ${episode.season}, ${episode.episode})">
|
||||||
<i class="fas fa-magic"></i> Fix
|
<i class="fas fa-magic"></i> Smart Fix
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -494,33 +516,108 @@ function refreshReport() {
|
|||||||
loadReport();
|
loadReport();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Quick fix functions
|
// Smart fix functions
|
||||||
function quickFixMovie(imdbId, released) {
|
async function smartFixMovie(imdbId) {
|
||||||
let newSource = 'manual';
|
try {
|
||||||
let newDate = null;
|
const options = await apiCall(`/api/movies/${imdbId}/date-options`);
|
||||||
|
showSmartFixModal('movie', options);
|
||||||
if (released) {
|
} catch (error) {
|
||||||
newSource = 'digital_release';
|
console.error('Failed to load movie options:', error);
|
||||||
newDate = released + 'T00:00:00';
|
showToast('Failed to load movie options', 'error');
|
||||||
} else {
|
|
||||||
newDate = new Date().toISOString().slice(0, 16);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateMovieDate(imdbId, newDate, newSource);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function quickFixEpisode(imdbId, season, episode, aired) {
|
async function smartFixEpisode(imdbId, season, episode) {
|
||||||
let newSource = 'manual';
|
try {
|
||||||
let newDate = null;
|
const options = await apiCall(`/api/episodes/${imdbId}/${season}/${episode}/date-options`);
|
||||||
|
showSmartFixModal('episode', options);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load episode options:', error);
|
||||||
|
showToast('Failed to load episode options', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (aired) {
|
function showSmartFixModal(mediaType, options) {
|
||||||
newSource = 'airdate';
|
const modal = document.getElementById('smart-fix-modal');
|
||||||
newDate = aired + 'T00:00:00';
|
const title = document.getElementById('smart-fix-title');
|
||||||
|
const content = document.getElementById('smart-fix-content');
|
||||||
|
|
||||||
|
if (mediaType === 'movie') {
|
||||||
|
title.textContent = `Fix Date for Movie: ${options.imdb_id}`;
|
||||||
} else {
|
} else {
|
||||||
newDate = new Date().toISOString().slice(0, 16);
|
title.textContent = `Fix Date for Episode: ${options.imdb_id} S${options.season.toString().padStart(2, '0')}E${options.episode.toString().padStart(2, '0')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateEpisodeDate(imdbId, season, episode, newDate, newSource);
|
// Build options HTML
|
||||||
|
let optionsHtml = '<div class="smart-fix-options">';
|
||||||
|
|
||||||
|
options.options.forEach((option, index) => {
|
||||||
|
const isChecked = index === 0 ? 'checked' : '';
|
||||||
|
const dateInput = option.type === 'manual' ?
|
||||||
|
`<input type="datetime-local" id="manual-date-${index}" class="manual-date-input" style="margin-top: 0.5rem;">` : '';
|
||||||
|
|
||||||
|
optionsHtml += `
|
||||||
|
<div class="option-card">
|
||||||
|
<label class="option-label">
|
||||||
|
<input type="radio" name="date-option" value="${index}" ${isChecked}>
|
||||||
|
<div class="option-content">
|
||||||
|
<h4>${option.label}</h4>
|
||||||
|
<p>${option.description}</p>
|
||||||
|
${option.date ? `<small><strong>Date:</strong> ${formatDateTime(option.date)}</small>` : ''}
|
||||||
|
${dateInput}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
optionsHtml += '</div>';
|
||||||
|
|
||||||
|
optionsHtml += `
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="closeSmartFixModal()">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-success" onclick="applySmartFix('${mediaType}', ${JSON.stringify(options).replace(/'/g, "'")})">
|
||||||
|
<i class="fas fa-magic"></i> Apply Fix
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
content.innerHTML = optionsHtml;
|
||||||
|
modal.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSmartFixModal() {
|
||||||
|
document.getElementById('smart-fix-modal').classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applySmartFix(mediaType, options) {
|
||||||
|
const selectedIndex = document.querySelector('input[name="date-option"]:checked').value;
|
||||||
|
const selectedOption = options.options[selectedIndex];
|
||||||
|
|
||||||
|
let dateadded = selectedOption.date;
|
||||||
|
let source = selectedOption.source;
|
||||||
|
|
||||||
|
// Handle manual date entry
|
||||||
|
if (selectedOption.type === 'manual') {
|
||||||
|
const manualDateInput = document.getElementById(`manual-date-${selectedIndex}`);
|
||||||
|
if (manualDateInput && manualDateInput.value) {
|
||||||
|
dateadded = new Date(manualDateInput.value).toISOString();
|
||||||
|
} else {
|
||||||
|
showToast('Please enter a date for manual option', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (mediaType === 'movie') {
|
||||||
|
await updateMovieDate(options.imdb_id, dateadded, source);
|
||||||
|
} else {
|
||||||
|
await updateEpisodeDate(options.imdb_id, options.season, options.episode, dateadded, source);
|
||||||
|
}
|
||||||
|
closeSmartFixModal();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Smart fix failed:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tools
|
// Tools
|
||||||
@@ -601,14 +698,120 @@ async function handleBulkUpdate(event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Edit modal functions
|
// Edit modal functions
|
||||||
function editMovie(imdbId, dateadded, source) {
|
async function editMovie(imdbId, dateadded, source) {
|
||||||
document.getElementById('modal-title').textContent = `Edit Movie: ${imdbId}`;
|
try {
|
||||||
document.getElementById('edit-imdb-id').value = imdbId;
|
// Load movie options to populate available dates
|
||||||
document.getElementById('edit-media-type').value = 'movie';
|
const options = await apiCall(`/api/movies/${imdbId}/date-options`);
|
||||||
document.getElementById('edit-season').value = '';
|
showEnhancedEditModal('movie', options, dateadded, source);
|
||||||
document.getElementById('edit-episode').value = '';
|
} catch (error) {
|
||||||
|
console.error('Failed to load movie options for edit:', error);
|
||||||
|
// Fallback to basic edit modal
|
||||||
|
showBasicEditModal('movie', imdbId, dateadded, source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showEnhancedEditModal(mediaType, options, currentDateadded, currentSource) {
|
||||||
|
const modal = document.getElementById('edit-modal');
|
||||||
|
const title = document.getElementById('modal-title');
|
||||||
|
const modalBody = document.querySelector('#edit-modal .modal-body');
|
||||||
|
|
||||||
|
if (mediaType === 'movie') {
|
||||||
|
title.textContent = `Edit Movie: ${options.imdb_id}`;
|
||||||
|
} else {
|
||||||
|
title.textContent = `Edit Episode: ${options.imdb_id} S${options.season.toString().padStart(2, '0')}E${options.episode.toString().padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build enhanced edit form with date options
|
||||||
|
let formHtml = `
|
||||||
|
<input type="hidden" id="edit-imdb-id" value="${options.imdb_id}">
|
||||||
|
<input type="hidden" id="edit-media-type" value="${mediaType}">
|
||||||
|
${mediaType === 'episode' ? `
|
||||||
|
<input type="hidden" id="edit-season" value="${options.season}">
|
||||||
|
<input type="hidden" id="edit-episode" value="${options.episode}">
|
||||||
|
` : `
|
||||||
|
<input type="hidden" id="edit-season" value="">
|
||||||
|
<input type="hidden" id="edit-episode" value="">
|
||||||
|
`}
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Choose Date Source:</label>
|
||||||
|
<div class="date-options">
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add available date options
|
||||||
|
options.options.forEach((option, index) => {
|
||||||
|
const isSelected = option.source === currentSource ? 'checked' : '';
|
||||||
|
const optionId = `date-option-${index}`;
|
||||||
|
|
||||||
|
formHtml += `
|
||||||
|
<div class="date-option-card">
|
||||||
|
<label class="date-option-label">
|
||||||
|
<input type="radio" name="edit-date-option" value="${index}" ${isSelected}
|
||||||
|
onchange="updateEditDateFromOption(${index}, ${JSON.stringify(option).replace(/"/g, '"')})">
|
||||||
|
<div class="date-option-content">
|
||||||
|
<h4>${option.label}</h4>
|
||||||
|
<p>${option.description}</p>
|
||||||
|
${option.date ? `<small><strong>Date:</strong> ${formatDateTime(option.date)}</small>` : ''}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
formHtml += `
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-dateadded">Date Added:</label>
|
||||||
|
<input type="datetime-local" id="edit-dateadded" required>
|
||||||
|
<small>Adjust the date/time as needed</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-source">Source:</label>
|
||||||
|
<select id="edit-source" required>
|
||||||
|
<option value="manual">Manual</option>
|
||||||
|
<option value="airdate">Air Date</option>
|
||||||
|
<option value="digital_release">Digital Release</option>
|
||||||
|
<option value="radarr:db.history.import">Radarr Import</option>
|
||||||
|
<option value="sonarr:history.import">Sonarr Import</option>
|
||||||
|
<option value="no_valid_date_source">No Valid Source</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="closeModal()">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-primary" onclick="handleEnhancedEditSubmit(event)">Save Changes</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
modalBody.innerHTML = formHtml;
|
||||||
|
|
||||||
|
// Set current values
|
||||||
|
if (currentDateadded && currentDateadded !== '-') {
|
||||||
|
try {
|
||||||
|
const date = new Date(currentDateadded);
|
||||||
|
document.getElementById('edit-dateadded').value = date.toISOString().slice(0, 16);
|
||||||
|
} catch (e) {
|
||||||
|
document.getElementById('edit-dateadded').value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('edit-source').value = currentSource || 'manual';
|
||||||
|
|
||||||
|
// Store options for later use
|
||||||
|
modal.dataset.options = JSON.stringify(options);
|
||||||
|
|
||||||
|
modal.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showBasicEditModal(mediaType, imdbId, dateadded, source) {
|
||||||
|
// Fallback to original basic edit modal
|
||||||
|
document.getElementById('modal-title').textContent = `Edit ${mediaType}: ${imdbId}`;
|
||||||
|
document.getElementById('edit-imdb-id').value = imdbId;
|
||||||
|
document.getElementById('edit-media-type').value = mediaType;
|
||||||
|
|
||||||
// Convert dateadded to datetime-local format if present
|
|
||||||
if (dateadded && dateadded !== '-') {
|
if (dateadded && dateadded !== '-') {
|
||||||
try {
|
try {
|
||||||
const date = new Date(dateadded);
|
const date = new Date(dateadded);
|
||||||
@@ -621,32 +824,65 @@ function editMovie(imdbId, dateadded, source) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('edit-source').value = source || 'manual';
|
document.getElementById('edit-source').value = source || 'manual';
|
||||||
|
|
||||||
document.getElementById('edit-modal').classList.add('active');
|
document.getElementById('edit-modal').classList.add('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
function editEpisode(imdbId, season, episode, dateadded, source) {
|
function updateEditDateFromOption(optionIndex, option) {
|
||||||
document.getElementById('modal-title').textContent = `Edit Episode: ${imdbId} S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`;
|
const dateInput = document.getElementById('edit-dateadded');
|
||||||
document.getElementById('edit-imdb-id').value = imdbId;
|
const sourceSelect = document.getElementById('edit-source');
|
||||||
document.getElementById('edit-media-type').value = 'episode';
|
|
||||||
document.getElementById('edit-season').value = season;
|
|
||||||
document.getElementById('edit-episode').value = episode;
|
|
||||||
|
|
||||||
// Convert dateadded to datetime-local format if present
|
if (option.date) {
|
||||||
if (dateadded && dateadded !== '-') {
|
// Convert to datetime-local format
|
||||||
try {
|
try {
|
||||||
const date = new Date(dateadded);
|
const date = new Date(option.date);
|
||||||
document.getElementById('edit-dateadded').value = date.toISOString().slice(0, 16);
|
dateInput.value = date.toISOString().slice(0, 16);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
document.getElementById('edit-dateadded').value = '';
|
dateInput.value = '';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
document.getElementById('edit-dateadded').value = '';
|
// Manual option - clear the date for user input
|
||||||
|
dateInput.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('edit-source').value = source || 'manual';
|
sourceSelect.value = option.source;
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById('edit-modal').classList.add('active');
|
async function handleEnhancedEditSubmit(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const modal = document.getElementById('edit-modal');
|
||||||
|
const options = JSON.parse(modal.dataset.options);
|
||||||
|
const imdbId = options.imdb_id;
|
||||||
|
const mediaType = document.getElementById('edit-media-type').value;
|
||||||
|
const dateadded = document.getElementById('edit-dateadded').value;
|
||||||
|
const source = document.getElementById('edit-source').value;
|
||||||
|
|
||||||
|
// Convert datetime-local to ISO string
|
||||||
|
const isoDateadded = dateadded ? new Date(dateadded).toISOString() : null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (mediaType === 'movie') {
|
||||||
|
await updateMovieDate(imdbId, isoDateadded, source);
|
||||||
|
} else {
|
||||||
|
await updateEpisodeDate(imdbId, options.season, options.episode, isoDateadded, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
closeModal();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Enhanced edit failed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function editEpisode(imdbId, season, episode, dateadded, source) {
|
||||||
|
try {
|
||||||
|
// Load episode options to populate available dates
|
||||||
|
const options = await apiCall(`/api/episodes/${imdbId}/${season}/${episode}/date-options`);
|
||||||
|
showEnhancedEditModal('episode', options, dateadded, source);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load episode options for edit:', error);
|
||||||
|
// Fallback to basic edit modal
|
||||||
|
showBasicEditModal('episode', imdbId, dateadded, source, season, episode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeModal() {
|
function closeModal() {
|
||||||
|
|||||||
Reference in New Issue
Block a user