import os import csv import sqlite3 from io import StringIO from urllib.parse import urlencode from xml.etree import ElementTree as ET from flask import Flask, request, render_template, Response, abort # ---------- Configuration ---------- DB_PATH = r"./MyVideos131.db" #DB_PATH = os.environ.get("KODI_DB_PATH", r"/home/pi/.kodi/userdata/Database/MyVideos121.db") DEFAULT_PER_PAGE = 24 # gallery default DEFAULT_VIEW = "gallery" # gallery by default app = Flask(__name__) app.jinja_env.globals.update(range=range) # ---------- DB Helpers ---------- def get_db(): if not os.path.exists(DB_PATH): raise FileNotFoundError(f"Base Kodi introuvable: {DB_PATH}") conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row return conn def resolve_movie_table(conn): cur = conn.execute("SELECT name FROM sqlite_master WHERE type IN ('table','view') AND name IN ('movie_view','movie')") names = [r["name"] for r in cur.fetchall()] if "movie_view" in names: return "movie_view" elif "movie" in names: return "movie" else: raise RuntimeError("Ni 'movie_view' ni 'movie' n'existent dans cette base.") def base_where(filters): where = [] params = [] if filters.get("q"): where.append("(LOWER(c00) LIKE ? OR LOWER(c16) LIKE ? OR LOWER(c01) LIKE ?)") like = f"%{filters['q'].lower()}%" params += [like, like, like] if filters.get("actor"): where.append("LOWER(c06) LIKE ?") params.append(f"%{filters['actor'].lower()}%") if filters.get("country"): where.append("LOWER(c21) LIKE ?") params.append(f"%{filters['country'].lower()}%") if filters.get("year_from"): where.append("CAST(substr(COALESCE(premiered, ''), 1, 4) AS INTEGER) >= ?") params.append(int(filters["year_from"])) if filters.get("year_to"): where.append("CAST(substr(COALESCE(premiered, ''), 1, 4) AS INTEGER) <= ?") params.append(int(filters["year_to"])) where_sql = ("WHERE " + " AND ".join(where)) if where else "" return where_sql, params SORT_COLUMNS = { "name": "c00", "original": "c16", "actors": "c06", "country": "c21", "year": "CAST(substr(COALESCE(premiered, ''), 1, 4) AS INTEGER)", "date": "premiered", "path": "c22" } DEFAULT_SORT = "name" DEFAULT_DIR = "asc" def sanitize_sort(sort, direction): sort = sort if sort in SORT_COLUMNS else DEFAULT_SORT direction = direction.lower() direction = "desc" if direction == "desc" else "asc" return sort, direction # ---------- c08 parser ---------- def extract_poster_from_c08(c08: str): if not c08 or not isinstance(c08, str): return None xml_text = f"{c08}" try: root = ET.fromstring(xml_text) except ET.ParseError: return None posters = [] for th in root.findall("thumb"): aspect = (th.attrib.get("aspect") or "").lower() url = (th.text or "").strip() if aspect == "poster" and url: posters.append(url) for u in posters: if "image.tmdb.org" in u: return u return posters[0] if posters else None # ---------- Queries ---------- def count_movies(conn, filters): table = resolve_movie_table(conn) where_sql, params = base_where(filters) sql = f"SELECT COUNT(*) as n FROM {table} {where_sql}" return conn.execute(sql, params).fetchone()["n"] def fetch_movies(conn, filters, page, per_page, sort, direction): table = resolve_movie_table(conn) where_sql, params = base_where(filters) sort_sql = SORT_COLUMNS.get(sort, SORT_COLUMNS[DEFAULT_SORT]) order_clause = f"{sort_sql} {direction.upper()}, c00 ASC" sql = f""" SELECT idMovie AS id, c00 AS nom, c01 AS desc, c06 AS acteurs, c16 AS titre, c21 AS pays, c22 AS path, premiered AS date, c08 AS thumbs, CAST(substr(COALESCE(premiered, ''), 1, 4) AS INTEGER) AS year FROM {table} {where_sql} ORDER BY {order_clause} LIMIT ? OFFSET ? """ rows = conn.execute(sql, params + [per_page, (page - 1) * per_page]).fetchall() result = [] for r in rows: d = dict(r) d["poster"] = extract_poster_from_c08(d.get("thumbs")) result.append(d) return result def fetch_movie_by_id(conn, movie_id: int): table = resolve_movie_table(conn) sql = f""" SELECT idMovie AS id, c00 AS nom, c01 AS desc, c06 AS acteurs, c16 AS titre, c21 AS pays, c22 AS path, premiered AS date, c08 AS thumbs, CAST(substr(COALESCE(premiered, ''), 1, 4) AS INTEGER) AS year FROM {table} WHERE idMovie = ? LIMIT 1 """ r = conn.execute(sql, (movie_id,)).fetchone() if not r: return None d = dict(r) d["poster"] = extract_poster_from_c08(d.get("thumbs")) return d def fetch_movie_by_path(conn, movie_path: str): table = resolve_movie_table(conn) sql = f""" SELECT idMovie AS id, c00 AS nom, c01 AS desc, c06 AS acteurs, c16 AS titre, c21 AS pays, c22 AS path, premiered AS date, c08 AS thumbs, CAST(substr(COALESCE(premiered, ''), 1, 4) AS INTEGER) AS year FROM {table} WHERE c22 = ? LIMIT 1 """ r = conn.execute(sql, (movie_path,)).fetchone() if not r: return None d = dict(r) d["poster"] = extract_poster_from_c08(d.get("thumbs")) return d def build_querystring(extras=None, **kwargs): args = dict(request.args) args.update(kwargs) if extras: args.update(extras) args = {k: v for k, v in args.items() if v not in (None, "", [])} return urlencode(args) # ---------- Routes ---------- @app.route("/") def index(): # Filters q = request.args.get("q", "").strip() actor = request.args.get("actor", "").strip() country = request.args.get("country", "").strip() year_from = request.args.get("year_from", "").strip() year_to = request.args.get("year_to", "").strip() # Sorting sort = request.args.get("sort", "name") direction = request.args.get("dir", "asc") sort, direction = sanitize_sort(sort, direction) # View view = request.args.get("view", DEFAULT_VIEW) view = view if view in ("gallery","list") else DEFAULT_VIEW # Paging page = int(request.args.get("page", 1) or 1) per_page = int(request.args.get("per_page", DEFAULT_PER_PAGE) or DEFAULT_PER_PAGE) per_page = max(6, min(per_page, 120)) filters = { "q": q or None, "actor": actor or None, "country": country or None, "year_from": year_from or None, "year_to": year_to or None, } try: conn = get_db() total = count_movies(conn, filters) last_page = max(1, (total + per_page - 1) // per_page) page = max(1, min(page, last_page)) movies = fetch_movies(conn, filters, page, per_page, sort, direction) except Exception as e: return render_template("error.html", error=str(e)) start_page = 1 if page <= 3 else page - 2 end_page = last_page if page + 2 >= last_page else page + 2 return render_template( "movies.html", movies=movies, total=total, page=page, last_page=last_page, per_page=per_page, sort=sort, direction=direction, view=view, q=q, actor=actor, country=country, year_from=year_from, year_to=year_to, build_querystring=build_querystring, start_page=start_page, end_page=end_page ) @app.route("/movie/") def movie_detail(movie_id): try: conn = get_db() movie = fetch_movie_by_id(conn, movie_id) if not movie: abort(404) except Exception as e: return render_template("error.html", error=str(e)) return render_template("movie_detail.html", m=movie) @app.route("/movie/by-path") def movie_by_path(): path = request.args.get("path", "").strip() if not path: abort(400) try: conn = get_db() movie = fetch_movie_by_path(conn, path) if not movie: abort(404) except Exception as e: return render_template("error.html", error=str(e)) return render_template("movie_detail.html", m=movie) # CSV export (respects filters/sort/view irrelevant) @app.route("/export.csv") def export_csv(): q = request.args.get("q", "").strip() actor = request.args.get("actor", "").strip() country = request.args.get("country", "").strip() year_from = request.args.get("year_from", "").strip() year_to = request.args.get("year_to", "").strip() sort = request.args.get("sort", "name") direction = request.args.get("dir", "asc") sort, direction = sanitize_sort(sort, direction) filters = { "q": q or None, "actor": actor or None, "country": country or None, "year_from": year_from or None, "year_to": year_to or None, } try: conn = get_db() # reuse fetch_movies with large per_page to get all (or reimplement full fetch if needed) total = count_movies(conn, filters) rows = fetch_movies(conn, filters, 1, max(total, 1), sort, direction) except Exception as e: return render_template("error.html", error=str(e)) si = StringIO() import csv writer = csv.writer(si) writer.writerow(["nom", "desc", "acteurs", "titre", "pays", "path", "date", "year", "poster"]) for r in rows: writer.writerow([r.get("nom",""), r.get("desc",""), r.get("acteurs",""), r.get("titre",""), r.get("pays",""), r.get("path",""), r.get("date",""), r.get("year",""), r.get("poster","")]) output = si.getvalue() return Response( output, mimetype="text/csv; charset=utf-8", headers={"Content-Disposition": "attachment; filename=movies_export.csv"} ) @app.template_filter("none_to_dash") def none_to_dash(value): return value if value not in (None, "", "None") else "—" if __name__ == "__main__": port = int(os.environ.get("PORT", "5000")) app.run(debug=True, host="0.0.0.0", port=port)