Dockerized media library scanner for NAS drives. Identifies movies and series from filenames, fetches posters via the public IMDB suggestion API (no key required), and exposes a dark-themed web UI with multilingual search support. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
103 lines
3.7 KiB
Python
103 lines
3.7 KiB
Python
from fastapi import APIRouter, Query
|
|
|
|
from database import Movie, NasSource, Season, Series, get_session
|
|
from metadata import _imdb_suggest
|
|
|
|
router = APIRouter(prefix="/api/search", tags=["search"])
|
|
|
|
_IMDB_QID = {"movie": "movie", "series": "tvSeries"}
|
|
|
|
|
|
def _imdb_ids_for_query(q: str, kind: str) -> list[str]:
|
|
"""Search IMDB with the user query and return matching IMDB IDs."""
|
|
items = _imdb_suggest(q)
|
|
qid = _IMDB_QID.get(kind)
|
|
return [it["id"] for it in items if it.get("qid") == qid and it.get("id")]
|
|
|
|
|
|
def _movie_matches(session, pattern: str, q: str) -> list:
|
|
"""Return movies matching by title in DB, OR via IMDB cross-reference."""
|
|
# Direct title match in DB
|
|
by_title = session.query(Movie).filter(Movie.title.ilike(pattern)).all()
|
|
found_ids = {m.id for m in by_title}
|
|
|
|
# Cross-language: search IMDB and look up matching IMDB IDs in our DB
|
|
imdb_ids = _imdb_ids_for_query(q, "movie")
|
|
if imdb_ids:
|
|
cross = session.query(Movie).filter(
|
|
Movie.imdb_id.in_(imdb_ids),
|
|
Movie.id.notin_(found_ids) if found_ids else Movie.id.isnot(None),
|
|
).all()
|
|
by_title = by_title + cross
|
|
|
|
return sorted(by_title, key=lambda m: m.title)[:50]
|
|
|
|
|
|
def _series_matches(session, pattern: str, q: str) -> list:
|
|
"""Return series matching by title in DB, OR via IMDB cross-reference."""
|
|
by_title = session.query(Series).filter(Series.title.ilike(pattern)).all()
|
|
found_ids = {s.id for s in by_title}
|
|
|
|
imdb_ids = _imdb_ids_for_query(q, "series")
|
|
if imdb_ids:
|
|
cross = session.query(Series).filter(
|
|
Series.imdb_id.in_(imdb_ids),
|
|
Series.id.notin_(found_ids) if found_ids else Series.id.isnot(None),
|
|
).all()
|
|
by_title = by_title + cross
|
|
|
|
return sorted(by_title, key=lambda s: s.title)[:50]
|
|
|
|
|
|
@router.get("")
|
|
def search(
|
|
q: str = Query(..., min_length=1),
|
|
type: str = Query("all", pattern="^(all|movie|series)$"),
|
|
):
|
|
session = get_session()
|
|
try:
|
|
pattern = f"%{q}%"
|
|
results = {"movies": [], "series": []}
|
|
|
|
if type in ("all", "movie"):
|
|
for m in _movie_matches(session, pattern, q):
|
|
nas = session.query(NasSource).get(m.nas_id)
|
|
results["movies"].append({
|
|
"id": m.id,
|
|
"title": m.title,
|
|
"year": m.year,
|
|
"quality": m.quality,
|
|
"codec": m.codec,
|
|
"nas_name": nas.name if nas else None,
|
|
"file_path": m.file_path,
|
|
"file_size": m.file_size,
|
|
"poster_url": m.poster_url,
|
|
"imdb_id": m.imdb_id,
|
|
})
|
|
|
|
if type in ("all", "series"):
|
|
for s in _series_matches(session, pattern, q):
|
|
seasons_data = []
|
|
for season in sorted(s.seasons, key=lambda x: (x.season_number, x.nas_id)):
|
|
nas = session.query(NasSource).get(season.nas_id)
|
|
seasons_data.append({
|
|
"season_number": season.season_number,
|
|
"episodes_count": len(season.episodes),
|
|
"nas_name": nas.name if nas else None,
|
|
"nas_id": season.nas_id,
|
|
})
|
|
results["series"].append({
|
|
"id": s.id,
|
|
"title": s.title,
|
|
"year": s.year,
|
|
"seasons_count": len(s.seasons),
|
|
"seasons": seasons_data,
|
|
"poster_url": s.poster_url,
|
|
"imdb_id": s.imdb_id,
|
|
})
|
|
|
|
total = len(results["movies"]) + len(results["series"])
|
|
return {"query": q, "results": results, "total": total}
|
|
finally:
|
|
session.close()
|