Initial release — MediaTracker v1.0

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>
This commit is contained in:
snoopy
2026-05-19 11:40:44 +00:00
commit 6f551ba439
21 changed files with 2770 additions and 0 deletions

102
backend/routers/search.py Normal file
View File

@@ -0,0 +1,102 @@
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()