Ajout Serie
This commit is contained in:
parent
753f31f4dd
commit
7b97e74b39
BIN
MyVideos131.db
BIN
MyVideos131.db
Binary file not shown.
422
app.py
422
app.py
@ -6,15 +6,16 @@ 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"/root/.kodi/userdata/Database/MyVideos131.db"
|
||||
DEFAULT_PER_PAGE = 24 # gallery default
|
||||
DEFAULT_VIEW = "gallery" # gallery by default
|
||||
#DB_PATH = os.environ.get("KODI_DB_PATH", r"/home/pi/.kodi/userdata/Database/MyVideos121.db")
|
||||
DB_PATH = r"/home/kodi/.kodi/userdata/Database/MyVideos131.db"
|
||||
DEFAULT_PER_PAGE_GALLERY = 24
|
||||
DEFAULT_PER_PAGE_LIST = 25
|
||||
DEFAULT_VIEW = "gallery"
|
||||
|
||||
app = Flask(__name__)
|
||||
app.jinja_env.globals.update(range=range)
|
||||
|
||||
# ---------- DB Helpers ----------
|
||||
# ---------- DB ----------
|
||||
def get_db():
|
||||
if not os.path.exists(DB_PATH):
|
||||
raise FileNotFoundError(f"Base Kodi introuvable: {DB_PATH}")
|
||||
@ -22,58 +23,25 @@ def get_db():
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
def table_exists(conn, name):
|
||||
cur = conn.execute("SELECT 1 FROM sqlite_master WHERE (type='table' OR type='view') AND name=?", (name,))
|
||||
return cur.fetchone() is not None
|
||||
|
||||
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.")
|
||||
for n in ("movie_view", "movie"):
|
||||
if table_exists(conn, n):
|
||||
return n
|
||||
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
|
||||
def resolve_tvshow_table(conn):
|
||||
for n in ("tvshow_view", "tvshow"):
|
||||
if table_exists(conn, n):
|
||||
return n
|
||||
raise RuntimeError("Ni 'tvshow_view' ni 'tvshow' n'existent dans cette base.")
|
||||
|
||||
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):
|
||||
# ---------- Parsers ----------
|
||||
def extract_movie_poster_from_c08(c08: str):
|
||||
"""Movies: c08 holds <thumb ... aspect='poster'> entries. Prefer TMDB, else first poster."""
|
||||
if not c08 or not isinstance(c08, str):
|
||||
return None
|
||||
xml_text = f"<root>{c08}</root>"
|
||||
@ -92,17 +60,79 @@ def extract_poster_from_c08(c08: str):
|
||||
return u
|
||||
return posters[0] if posters else None
|
||||
|
||||
# ---------- Queries ----------
|
||||
def extract_show_poster_from_c06(c06: str):
|
||||
"""TV shows: c06 contains <thumb aspect="poster">…</thumb>. Return first poster (prefer TMDB)."""
|
||||
if not c06 or not isinstance(c06, str):
|
||||
return None
|
||||
xml_text = f"<root>{c06}</root>"
|
||||
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 url and (aspect == "poster"):
|
||||
posters.append(url)
|
||||
if posters:
|
||||
for u in posters:
|
||||
if "image.tmdb.org" in u:
|
||||
return u
|
||||
return posters[0]
|
||||
# fallback: first thumb (any aspect)
|
||||
th2 = root.find("thumb")
|
||||
return (th2.text or "").strip() if th2 is not None else None
|
||||
|
||||
# ---------- Movies ----------
|
||||
SORT_COLUMNS_MOVIES = {
|
||||
"name": "c00",
|
||||
"original": "c16",
|
||||
"actors": "c06",
|
||||
"country": "c21",
|
||||
"year": "CAST(substr(COALESCE(premiered, ''), 1, 4) AS INTEGER)",
|
||||
"date": "premiered",
|
||||
"path": "c22"
|
||||
}
|
||||
DEFAULT_SORT_MOVIES = "name"
|
||||
DEFAULT_DIR = "asc"
|
||||
|
||||
def sanitize_sort(key, mapping, default_key, direction):
|
||||
key = key if key in mapping else default_key
|
||||
direction = "desc" if (direction or "").lower() == "desc" else "asc"
|
||||
return key, direction
|
||||
|
||||
def base_where_movies(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"]))
|
||||
return ("WHERE " + " AND ".join(where)) if where else "", params
|
||||
|
||||
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}"
|
||||
where, params = base_where_movies(filters)
|
||||
sql = f"SELECT COUNT(*) as n FROM {table} {where}"
|
||||
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])
|
||||
where_sql, params = base_where_movies(filters)
|
||||
sort_sql = SORT_COLUMNS_MOVIES.get(sort, SORT_COLUMNS_MOVIES[DEFAULT_SORT_MOVIES])
|
||||
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,
|
||||
@ -117,42 +147,88 @@ def fetch_movies(conn, filters, page, per_page, sort, direction):
|
||||
result = []
|
||||
for r in rows:
|
||||
d = dict(r)
|
||||
d["poster"] = extract_poster_from_c08(d.get("thumbs"))
|
||||
d["poster"] = extract_movie_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)
|
||||
# ---------- TV Shows ----------
|
||||
SORT_COLUMNS_SHOWS = {
|
||||
"title": "c00",
|
||||
"original": "c09",
|
||||
"network": "c14",
|
||||
"genre": "c08",
|
||||
"year": "CAST(substr(COALESCE(c05, ''), 1, 4) AS INTEGER)",
|
||||
"date": "c05"
|
||||
}
|
||||
DEFAULT_SORT_SHOWS = "title"
|
||||
|
||||
def base_where_shows(filters):
|
||||
where = []
|
||||
params = []
|
||||
if filters.get("q"):
|
||||
where.append("(LOWER(c00) LIKE ? OR LOWER(c09) LIKE ? OR LOWER(c01) LIKE ?)")
|
||||
like = f"%{filters['q'].lower()}%"
|
||||
params += [like, like, like]
|
||||
if filters.get("network"):
|
||||
where.append("LOWER(c14) LIKE ?")
|
||||
params.append(f"%{filters['network'].lower()}%")
|
||||
if filters.get("genre"):
|
||||
where.append("LOWER(c08) LIKE ?")
|
||||
params.append(f"%{filters['genre'].lower()}%")
|
||||
if filters.get("year_from"):
|
||||
where.append("CAST(substr(COALESCE(c05, ''), 1, 4) AS INTEGER) >= ?")
|
||||
params.append(int(filters["year_from"]))
|
||||
if filters.get("year_to"):
|
||||
where.append("CAST(substr(COALESCE(c05, ''), 1, 4) AS INTEGER) <= ?")
|
||||
params.append(int(filters["year_to"]))
|
||||
return ("WHERE " + " AND ".join(where)) if where else "", params
|
||||
|
||||
def count_shows(conn, filters):
|
||||
table = resolve_tvshow_table(conn)
|
||||
where, params = base_where_shows(filters)
|
||||
sql = f"SELECT COUNT(*) as n FROM {table} {where}"
|
||||
return conn.execute(sql, params).fetchone()["n"]
|
||||
|
||||
def fetch_shows(conn, filters, page, per_page, sort, direction):
|
||||
table = resolve_tvshow_table(conn)
|
||||
where_sql, params = base_where_shows(filters)
|
||||
sort_sql = SORT_COLUMNS_SHOWS.get(sort, SORT_COLUMNS_SHOWS[DEFAULT_SORT_SHOWS])
|
||||
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
|
||||
SELECT idShow AS id, c00 AS titre, c01 AS desc, c05 AS date,
|
||||
c06 AS thumbs, c08 AS genre, c09 AS original, c14 AS diffuseur
|
||||
FROM {table}
|
||||
WHERE idMovie = ?
|
||||
{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["year"] = int((d.get("date") or "")[:4]) if d.get("date") and str(d["date"])[:4].isdigit() else None
|
||||
d["poster"] = extract_show_poster_from_c06(d.get("thumbs"))
|
||||
result.append(d)
|
||||
return result
|
||||
|
||||
def fetch_show_by_id(conn, show_id):
|
||||
table = resolve_tvshow_table(conn)
|
||||
sql = f"""
|
||||
SELECT idShow AS id, c00 AS titre, c01 AS desc, c05 AS date,
|
||||
c06 AS thumbs, c08 AS genre, c09 AS original, c14 AS diffuseur
|
||||
FROM {table}
|
||||
WHERE idShow = ?
|
||||
LIMIT 1
|
||||
"""
|
||||
r = conn.execute(sql, (movie_id,)).fetchone()
|
||||
if not r: return None
|
||||
r = conn.execute(sql, (show_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"))
|
||||
d["year"] = int((d.get("date") or "")[:4]) if d.get("date") and str(d["date"])[:4].isdigit() else None
|
||||
d["poster"] = extract_show_poster_from_c06(d.get("thumbs"))
|
||||
return d
|
||||
|
||||
# ---------- Utils ----------
|
||||
def build_querystring(extras=None, **kwargs):
|
||||
args = dict(request.args)
|
||||
args.update(kwargs)
|
||||
@ -163,32 +239,28 @@ def build_querystring(extras=None, **kwargs):
|
||||
|
||||
# ---------- Routes ----------
|
||||
@app.route("/")
|
||||
def index():
|
||||
# Filters
|
||||
def root():
|
||||
return movies_page()
|
||||
|
||||
@app.route("/movies")
|
||||
def movies_page():
|
||||
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
|
||||
sort = request.args.get("sort", DEFAULT_SORT_MOVIES)
|
||||
direction = request.args.get("dir", DEFAULT_DIR)
|
||||
sort, direction = sanitize_sort(sort, SORT_COLUMNS_MOVIES, DEFAULT_SORT_MOVIES, direction)
|
||||
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)
|
||||
default_pp = DEFAULT_PER_PAGE_GALLERY if view == 'gallery' else DEFAULT_PER_PAGE_LIST
|
||||
per_page = int(request.args.get("per_page", default_pp) or default_pp)
|
||||
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,
|
||||
}
|
||||
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()
|
||||
@ -202,90 +274,142 @@ def index():
|
||||
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,
|
||||
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
|
||||
build_querystring=build_querystring, start_page=start_page, end_page=end_page
|
||||
)
|
||||
|
||||
@app.route("/movie/<int:movie_id>")
|
||||
def movie_detail(movie_id):
|
||||
try:
|
||||
conn = get_db()
|
||||
movie = fetch_movie_by_id(conn, movie_id)
|
||||
if not movie:
|
||||
abort(404)
|
||||
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: abort(404)
|
||||
m = dict(r)
|
||||
m["poster"] = extract_movie_poster_from_c08(m.get("thumbs"))
|
||||
except Exception as e:
|
||||
return render_template("error.html", error=str(e))
|
||||
return render_template("movie_detail.html", m=movie)
|
||||
return render_template("movie_detail.html", m=m)
|
||||
|
||||
@app.route("/shows")
|
||||
def shows_page():
|
||||
q = request.args.get("q", "").strip()
|
||||
network = request.args.get("network", "").strip()
|
||||
genre = request.args.get("genre", "").strip()
|
||||
year_from = request.args.get("year_from", "").strip()
|
||||
year_to = request.args.get("year_to", "").strip()
|
||||
sort = request.args.get("sort", DEFAULT_SORT_SHOWS)
|
||||
direction = request.args.get("dir", DEFAULT_DIR)
|
||||
sort, direction = sanitize_sort(sort, SORT_COLUMNS_SHOWS, DEFAULT_SORT_SHOWS, direction)
|
||||
view = request.args.get("view", DEFAULT_VIEW)
|
||||
view = view if view in ("gallery","list") else DEFAULT_VIEW
|
||||
page = int(request.args.get("page", 1) or 1)
|
||||
default_pp = DEFAULT_PER_PAGE_GALLERY if view == 'gallery' else DEFAULT_PER_PAGE_LIST
|
||||
per_page = int(request.args.get("per_page", default_pp) or default_pp)
|
||||
per_page = max(6, min(per_page, 120))
|
||||
|
||||
filters = {"q": q or None, "network": network or None, "genre": genre or None,
|
||||
"year_from": year_from or None, "year_to": year_to or None}
|
||||
|
||||
@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)
|
||||
total = count_shows(conn, filters)
|
||||
last_page = max(1, (total + per_page - 1) // per_page)
|
||||
page = max(1, min(page, last_page))
|
||||
shows = fetch_shows(conn, filters, page, per_page, sort, direction)
|
||||
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():
|
||||
start_page = 1 if page <= 3 else page - 2
|
||||
end_page = last_page if page + 2 >= last_page else page + 2
|
||||
|
||||
return render_template("shows.html",
|
||||
shows=shows, total=total, page=page, last_page=last_page, per_page=per_page,
|
||||
sort=sort, direction=direction, view=view,
|
||||
q=q, network=network, genre=genre, year_from=year_from, year_to=year_to,
|
||||
build_querystring=build_querystring, start_page=start_page, end_page=end_page
|
||||
)
|
||||
|
||||
@app.route("/show/<int:show_id>")
|
||||
def show_detail(show_id):
|
||||
try:
|
||||
conn = get_db()
|
||||
s = fetch_show_by_id(conn, show_id)
|
||||
if not s: abort(404)
|
||||
except Exception as e:
|
||||
return render_template("error.html", error=str(e))
|
||||
return render_template("show_detail.html", s=s)
|
||||
|
||||
@app.route("/export_movies.csv")
|
||||
def export_movies_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,
|
||||
}
|
||||
sort = request.args.get("sort", DEFAULT_SORT_MOVIES)
|
||||
direction = request.args.get("dir", DEFAULT_DIR)
|
||||
sort, direction = sanitize_sort(sort, SORT_COLUMNS_MOVIES, DEFAULT_SORT_MOVIES, 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"}
|
||||
)
|
||||
return Response(si.getvalue(), mimetype="text/csv; charset=utf-8",
|
||||
headers={"Content-Disposition": "attachment; filename=movies_export.csv"})
|
||||
|
||||
@app.route("/export_shows.csv")
|
||||
def export_shows_csv():
|
||||
q = request.args.get("q", "").strip()
|
||||
network = request.args.get("network", "").strip()
|
||||
genre = request.args.get("genre", "").strip()
|
||||
year_from = request.args.get("year_from", "").strip()
|
||||
year_to = request.args.get("year_to", "").strip()
|
||||
sort = request.args.get("sort", DEFAULT_SORT_SHOWS)
|
||||
direction = request.args.get("dir", DEFAULT_DIR)
|
||||
sort, direction = sanitize_sort(sort, SORT_COLUMNS_SHOWS, DEFAULT_SORT_SHOWS, direction)
|
||||
|
||||
filters = {"q": q or None, "network": network or None, "genre": genre or None,
|
||||
"year_from": year_from or None, "year_to": year_to or None}
|
||||
try:
|
||||
conn = get_db()
|
||||
total = count_shows(conn, filters)
|
||||
rows = fetch_shows(conn, filters, 1, max(total, 1), sort, direction)
|
||||
except Exception as e:
|
||||
return render_template("error.html", error=str(e))
|
||||
|
||||
si = StringIO()
|
||||
writer = csv.writer(si)
|
||||
writer.writerow(["titre", "original", "desc", "genre", "diffuseur", "date", "year", "poster"])
|
||||
for r in rows:
|
||||
writer.writerow([r.get("titre",""), r.get("original",""), r.get("desc",""),
|
||||
r.get("genre",""), r.get("diffuseur",""), r.get("date",""),
|
||||
r.get("year",""), r.get("poster","")])
|
||||
return Response(si.getvalue(), mimetype="text/csv; charset=utf-8",
|
||||
headers={"Content-Disposition": "attachment; filename=shows_export.csv"})
|
||||
|
||||
# ---------- Jinja filters ----------
|
||||
@app.template_filter("none_to_dash")
|
||||
def none_to_dash(value):
|
||||
return value if value not in (None, "", "None") else "—"
|
||||
|
||||
296
app.py.v1
Normal file
296
app.py.v1
Normal file
@ -0,0 +1,296 @@
|
||||
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"<root>{c08}</root>"
|
||||
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/<int:movie_id>")
|
||||
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)
|
||||
@ -1,45 +1,22 @@
|
||||
<!doctype html>
|
||||
<html lang="fr" class="h-full">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}Kodi - Films{% endblock %}</title>
|
||||
<script>
|
||||
(function() {
|
||||
const stored = localStorage.getItem('theme');
|
||||
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
if (stored === 'dark' || (!stored && prefersDark)) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = { darkMode: 'class' };
|
||||
</script>
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}Kodi - Médiathèque{% endblock %}</title>
|
||||
<script>(function(){const s=localStorage.getItem('theme');const d=window.matchMedia&&window.matchMedia('(prefers-color-scheme: dark)').matches;if(s==='dark'||(!s&&d)){document.documentElement.classList.add('dark');}else{document.documentElement.classList.remove('dark');}})();</script>
|
||||
<script src="https://cdn.tailwindcss.com"></script><script>tailwind.config={darkMode:'class'}</script>
|
||||
</head>
|
||||
<body class="h-full bg-gray-50 text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<div class="max-w-7xl mx-auto p-4 md:p-6">
|
||||
<header class="flex items-center gap-3 mb-4">
|
||||
<h1 class="text-xl md:text-2xl font-semibold">Kodi - Films</h1>
|
||||
<div class="ml-auto flex items-center gap-2">
|
||||
<button id="themeToggle" class="px-3 py-1 rounded-lg border dark:border-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||
<span class="inline dark:hidden">🌙 Mode sombre</span>
|
||||
<span class="hidden dark:inline">☀️ Mode clair</span>
|
||||
</button>
|
||||
</div>
|
||||
<nav class="flex gap-2">
|
||||
<a href="/movies" class="px-3 py-1 rounded-lg border dark:border-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700">🎬 Films</a>
|
||||
<a href="/shows" class="px-3 py-1 rounded-lg border dark:border-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700">📺 Séries</a>
|
||||
</nav>
|
||||
<div class="ml-auto"><button id="themeToggle" class="px-3 py-1 rounded-lg border dark:border-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700"><span class="inline dark:hidden">🌙 Mode sombre</span><span class="hidden dark:inline">☀️ Mode clair</span></button></div>
|
||||
</header>
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('themeToggle')?.addEventListener('click', () => {
|
||||
const el = document.documentElement;
|
||||
const newTheme = el.classList.contains('dark') ? 'light' : 'dark';
|
||||
el.classList.toggle('dark');
|
||||
localStorage.setItem('theme', newTheme);
|
||||
});
|
||||
</script>
|
||||
<script>document.getElementById('themeToggle')?.addEventListener('click',()=>{const el=document.documentElement;const t=el.classList.contains('dark')?'light':'dark';el.classList.toggle('dark');localStorage.setItem('theme',t);});</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
45
templates/base.html.v1
Normal file
45
templates/base.html.v1
Normal file
@ -0,0 +1,45 @@
|
||||
<!doctype html>
|
||||
<html lang="fr" class="h-full">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}Kodi - Films{% endblock %}</title>
|
||||
<script>
|
||||
(function() {
|
||||
const stored = localStorage.getItem('theme');
|
||||
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
if (stored === 'dark' || (!stored && prefersDark)) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = { darkMode: 'class' };
|
||||
</script>
|
||||
</head>
|
||||
<body class="h-full bg-gray-50 text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<div class="max-w-7xl mx-auto p-4 md:p-6">
|
||||
<header class="flex items-center gap-3 mb-4">
|
||||
<h1 class="text-xl md:text-2xl font-semibold">Kodi - Films</h1>
|
||||
<div class="ml-auto flex items-center gap-2">
|
||||
<button id="themeToggle" class="px-3 py-1 rounded-lg border dark:border-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||
<span class="inline dark:hidden">🌙 Mode sombre</span>
|
||||
<span class="hidden dark:inline">☀️ Mode clair</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('themeToggle')?.addEventListener('click', () => {
|
||||
const el = document.documentElement;
|
||||
const newTheme = el.classList.contains('dark') ? 'light' : 'dark';
|
||||
el.classList.toggle('dark');
|
||||
localStorage.setItem('theme', newTheme);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,9 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Erreur{% endblock %}
|
||||
{% block content %}
|
||||
<div class="bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 text-red-800 dark:text-red-200 p-4 rounded-2xl shadow">
|
||||
{% extends "base.html" %}{% block title %}Erreur{% endblock %}{% block content %}
|
||||
<div class="bg-red-50 dark:bg-red-950 border text-red-800 dark:text-red-200 p-4 rounded-2xl shadow">
|
||||
<h1 class="text-xl font-semibold mb-2">Erreur</h1>
|
||||
<pre class="whitespace-pre-wrap text-sm">{{ error }}</pre>
|
||||
<p class="mt-4 text-sm text-gray-600 dark:text-gray-300">Vérifie le chemin de la base (<code>KODI_DB_PATH</code>) et tes permissions.</p>
|
||||
<p class="mt-4 text-sm">Vérifie le chemin de la base (<code>KODI_DB_PATH</code>) et tes permissions.</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
9
templates/error.html.v1
Normal file
9
templates/error.html.v1
Normal file
@ -0,0 +1,9 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Erreur{% endblock %}
|
||||
{% block content %}
|
||||
<div class="bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 text-red-800 dark:text-red-200 p-4 rounded-2xl shadow">
|
||||
<h1 class="text-xl font-semibold mb-2">Erreur</h1>
|
||||
<pre class="whitespace-pre-wrap text-sm">{{ error }}</pre>
|
||||
<p class="mt-4 text-sm text-gray-600 dark:text-gray-300">Vérifie le chemin de la base (<code>KODI_DB_PATH</code>) et tes permissions.</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -1,42 +1,16 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ m.nom }} - Détail{% endblock %}
|
||||
{% block content %}
|
||||
{% extends "base.html" %}{% block title %}Détail Film{% endblock %}{% block content %}
|
||||
<a href="javascript:history.back()" class="text-sm text-indigo-600 dark:text-indigo-400 hover:underline">← Retour</a>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow p-6 mt-3 border border-gray-200 dark:border-gray-700">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow p-6 mt-3 border">
|
||||
<div class="flex gap-6">
|
||||
<div>
|
||||
{% if m.poster %}
|
||||
<img src="{{ m.poster }}" alt="poster" class="w-40 object-cover rounded-xl shadow">
|
||||
{% else %}
|
||||
<div class="w-40 h-60 bg-gray-200 dark:bg-gray-700 rounded-xl"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>{% if m.poster %}<img src="{{ m.poster }}" alt="poster" class="w-40 object-cover rounded-xl shadow">{% else %}<div class="w-40 h-60 bg-gray-200 dark:bg-gray-700 rounded-xl"></div>{% endif %}</div>
|
||||
<div class="flex-1">
|
||||
<h1 class="text-2xl font-bold mb-2">{{ m.nom }}</h1>
|
||||
<div class="text-gray-600 dark:text-gray-300 mb-4">
|
||||
{% if m.year %}<span class="mr-2">{{ m.year }}</span>{% endif %}
|
||||
{% if m.pays %}<span class="mr-2">• {{ m.pays }}</span>{% endif %}
|
||||
{% if m.date %}<span class="mr-2">• {{ m.date }}</span>{% endif %}
|
||||
</div>
|
||||
{% if m.titre %}
|
||||
<div class="mb-2"><span class="font-medium">Titre original :</span> {{ m.titre }}</div>
|
||||
{% endif %}
|
||||
{% if m.acteurs %}
|
||||
<div class="mb-2"><span class="font-medium">Acteurs :</span> {{ m.acteurs }}</div>
|
||||
{% endif %}
|
||||
{% if m.desc %}
|
||||
<div class="mt-4">
|
||||
<h2 class="font-semibold mb-1">Résumé</h2>
|
||||
<p class="whitespace-pre-wrap">{{ m.desc }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if m.path %}
|
||||
<div class="mt-4">
|
||||
<h2 class="font-semibold mb-1">Chemin</h2>
|
||||
<code class="text-sm">{{ m.path }}</code>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="text-gray-600 dark:text-gray-300 mb-4">{% if m.year %}<span class="mr-2">{{ m.year }}</span>{% endif %}{% if m.pays %}<span class="mr-2">• {{ m.pays }}</span>{% endif %}{% if m.date %}<span class="mr-2">• {{ m.date }}</span>{% endif %}</div>
|
||||
{% if m.titre %}<div class="mb-2"><span class="font-medium">Titre original :</span> {{ m.titre }}</div>{% endif %}
|
||||
{% if m.acteurs %}<div class="mb-2"><span class="font-medium">Acteurs :</span> {{ m.acteurs }}</div>{% endif %}
|
||||
{% if m.desc %}<div class="mt-4"><h2 class="font-semibold mb-1">Résumé</h2><p class="whitespace-pre-wrap">{{ m.desc }}</p></div>{% endif %}
|
||||
{% if m.path %}<div class="mt-4"><h2 class="font-semibold mb-1">Chemin</h2><code class="text-sm">{{ m.path }}</code></div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
42
templates/movie_detail.html.v1
Normal file
42
templates/movie_detail.html.v1
Normal file
@ -0,0 +1,42 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ m.nom }} - Détail{% endblock %}
|
||||
{% block content %}
|
||||
<a href="javascript:history.back()" class="text-sm text-indigo-600 dark:text-indigo-400 hover:underline">← Retour</a>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow p-6 mt-3 border border-gray-200 dark:border-gray-700">
|
||||
<div class="flex gap-6">
|
||||
<div>
|
||||
{% if m.poster %}
|
||||
<img src="{{ m.poster }}" alt="poster" class="w-40 object-cover rounded-xl shadow">
|
||||
{% else %}
|
||||
<div class="w-40 h-60 bg-gray-200 dark:bg-gray-700 rounded-xl"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h1 class="text-2xl font-bold mb-2">{{ m.nom }}</h1>
|
||||
<div class="text-gray-600 dark:text-gray-300 mb-4">
|
||||
{% if m.year %}<span class="mr-2">{{ m.year }}</span>{% endif %}
|
||||
{% if m.pays %}<span class="mr-2">• {{ m.pays }}</span>{% endif %}
|
||||
{% if m.date %}<span class="mr-2">• {{ m.date }}</span>{% endif %}
|
||||
</div>
|
||||
{% if m.titre %}
|
||||
<div class="mb-2"><span class="font-medium">Titre original :</span> {{ m.titre }}</div>
|
||||
{% endif %}
|
||||
{% if m.acteurs %}
|
||||
<div class="mb-2"><span class="font-medium">Acteurs :</span> {{ m.acteurs }}</div>
|
||||
{% endif %}
|
||||
{% if m.desc %}
|
||||
<div class="mt-4">
|
||||
<h2 class="font-semibold mb-1">Résumé</h2>
|
||||
<p class="whitespace-pre-wrap">{{ m.desc }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if m.path %}
|
||||
<div class="mt-4">
|
||||
<h2 class="font-semibold mb-1">Chemin</h2>
|
||||
<code class="text-sm">{{ m.path }}</code>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -1,160 +1,76 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Kodi - Films{% endblock %}
|
||||
{% block title %}Films - Kodi{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow p-4 md:p-6 mb-6 border border-gray-200 dark:border-gray-700">
|
||||
<form method="get" class="grid grid-cols-1 md:grid-cols-6 gap-3 items-end">
|
||||
<!-- Hidden to preserve state -->
|
||||
<form method="get" action="/movies" class="grid grid-cols-1 md:grid-cols-6 gap-3 items-end">
|
||||
<input type="hidden" name="view" value="{{ view }}">
|
||||
<input type="hidden" name="dir" value="{{ direction }}">
|
||||
<input type="hidden" name="page" value="{{ page }}">
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Recherche</label>
|
||||
<input type="text" name="q" value="{{ q }}" placeholder="Titre / synopsis"
|
||||
class="w-full rounded-xl border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500">
|
||||
<div><label class="block text-sm font-medium mb-1">Recherche</label>
|
||||
<input type="text" name="q" value="{{ q }}" class="w-full rounded-xl border px-3 py-2 dark:bg-gray-900 dark:border-gray-700">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Acteur</label>
|
||||
<input type="text" name="actor" value="{{ actor }}" placeholder="ex: Tom Hanks"
|
||||
class="w-full rounded-xl border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500">
|
||||
<div><label class="block text-sm font-medium mb-1">Acteur</label>
|
||||
<input type="text" name="actor" value="{{ actor }}" class="w-full rounded-xl border px-3 py-2 dark:bg-gray-900 dark:border-gray-700">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Pays</label>
|
||||
<input type="text" name="country" value="{{ country }}" placeholder="ex: France"
|
||||
class="w-full rounded-xl border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500">
|
||||
<div><label class="block text-sm font-medium mb-1">Pays</label>
|
||||
<input type="text" name="country" value="{{ country }}" class="w-full rounded-xl border px-3 py-2 dark:bg-gray-900 dark:border-gray-700">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Année min</label>
|
||||
<input type="number" name="year_from" value="{{ year_from }}" min="1900" max="2100"
|
||||
class="w-full rounded-xl border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500">
|
||||
<div><label class="block text-sm font-medium mb-1">Année min</label>
|
||||
<input type="number" name="year_from" value="{{ year_from }}" class="w-full rounded-xl border px-3 py-2 dark:bg-gray-900 dark:border-gray-700">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Année max</label>
|
||||
<input type="number" name="year_to" value="{{ year_to }}" min="1900" max="2100"
|
||||
class="w-full rounded-xl border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500">
|
||||
<div><label class="block text-sm font-medium mb-1">Année max</label>
|
||||
<input type="number" name="year_to" value="{{ year_to }}" class="w-full rounded-xl border px-3 py-2 dark:bg-gray-900 dark:border-gray-700">
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button type="submit"
|
||||
class="rounded-xl px-4 py-2 bg-indigo-600 text-white font-medium hover:bg-indigo-700 shadow">
|
||||
Filtrer
|
||||
</button>
|
||||
<a href="/?view={{ view }}"
|
||||
class="rounded-xl px-4 py-2 bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 font-medium hover:bg-gray-200 dark:hover:bg-gray-600 border border-gray-300 dark:border-gray-600">
|
||||
Réinitialiser
|
||||
</a>
|
||||
<a href="/export.csv?{{ build_querystring() }}"
|
||||
class="rounded-xl px-4 py-2 bg-emerald-600 text-white font-medium hover:bg-emerald-700 shadow">
|
||||
Export CSV
|
||||
</a>
|
||||
<button class="rounded-xl px-4 py-2 bg-indigo-600 text-white">Filtrer</button>
|
||||
<a href="/movies?view={{ view }}" class="rounded-xl px-4 py-2 border dark:border-gray-600">Réinitialiser</a>
|
||||
<a href="/export_movies.csv?{{ build_querystring() }}" class="rounded-xl px-4 py-2 bg-emerald-600 text-white">Export CSV</a>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-6 flex items-center gap-2 mt-1">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-300">Vue :</span>
|
||||
<a href="/?{{ build_querystring(view='gallery', page=1, per_page=24) }}"
|
||||
class="text-sm px-3 py-1 rounded-lg border {{ 'bg-indigo-600 text-white' if view=='gallery' else 'bg-white dark:bg-gray-900 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 border-gray-300 dark:border-gray-600' }}">
|
||||
Galerie
|
||||
</a>
|
||||
<a href="/?{{ build_querystring(view='list', page=1, per_page=25) }}"
|
||||
class="text-sm px-3 py-1 rounded-lg border {{ 'bg-indigo-600 text-white' if view=='list' else 'bg-white dark:bg-gray-900 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 border-gray-300 dark:border-gray-600' }}">
|
||||
Liste
|
||||
</a>
|
||||
|
||||
<span class="text-sm">Vue :</span>
|
||||
<a href="/movies?{{ build_querystring(view='gallery', page=1, per_page=24) }}" class="text-sm px-3 py-1 rounded-lg border {{ 'bg-indigo-600 text-white' if view=='gallery' else '' }}">Galerie</a>
|
||||
<a href="/movies?{{ build_querystring(view='list', page=1, per_page=25) }}" class="text-sm px-3 py-1 rounded-lg border {{ 'bg-indigo-600 text-white' if view=='list' else '' }}">Liste</a>
|
||||
<div class="ml-auto flex items-center gap-2">
|
||||
<label class="text-sm text-gray-600 dark:text-gray-300">Trier par :</label>
|
||||
<select name="sort"
|
||||
onchange="this.form.page.value=1; this.form.submit()"
|
||||
class="rounded-xl border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-2 py-1">
|
||||
<label class="text-sm">Trier par :</label>
|
||||
<select name="sort" onchange="this.form.page.value=1; this.form.submit()" class="rounded-xl border px-2 py-1 dark:bg-gray-900 dark:border-gray-700">
|
||||
{% for key,label in {'name':'Nom','original':'Titre','actors':'Acteurs','country':'Pays','year':'Année','date':'Date','path':'Chemin'}.items() %}
|
||||
<option value="{{key}}" {% if sort==key %}selected{% endif %}>{{label}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% set dir_toggle = ('desc' if direction=='asc' else 'asc') %}
|
||||
<a href="/?{{ build_querystring(dir=dir_toggle, page=1) }}"
|
||||
class="text-sm px-3 py-1 rounded-lg border bg-white dark:bg-gray-900 border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-800">
|
||||
{{ '▲ Asc' if direction=='asc' else '▼ Desc' }}
|
||||
</a>
|
||||
|
||||
<label class="text-sm text-gray-600 dark:text-gray-300 ml-3">Par page :</label>
|
||||
<select name="per_page"
|
||||
onchange="this.form.page.value=1; this.form.submit()"
|
||||
class="rounded-xl border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-2 py-1">
|
||||
{% if view=='gallery' %}
|
||||
{% for n in (12,24,48,96) %}
|
||||
<option value="{{n}}" {% if per_page==n %}selected{% endif %}>{{n}}</option>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% for n in (10,25,50,100) %}
|
||||
<option value="{{n}}" {% if per_page==n %}selected{% endif %}>{{n}}</option>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<a href="/movies?{{ build_querystring(dir=dir_toggle, page=1) }}" class="text-sm px-3 py-1 rounded-lg border">{{ '▲ Asc' if direction=='asc' else '▼ Desc' }}</a>
|
||||
<label class="text-sm ml-3">Par page :</label>
|
||||
<select name="per_page" onchange="this.form.page.value=1; this.form.submit()" class="rounded-xl border px-2 py-1 dark:bg-gray-900 dark:border-gray-700">
|
||||
{% if view=='gallery' %}{% for n in (12,24,48,96) %}<option value="{{n}}" {% if per_page==n %}selected{% endif %}>{{n}}</option>{% endfor %}
|
||||
{% else %}{% for n in (10,25,50,100) %}<option value="{{n}}" {% if per_page==n %}selected{% endif %}>{{n}}</option>{% endfor %}{% endif %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 text-sm text-gray-600 dark:text-gray-300">
|
||||
Résultats: <span class="font-semibold">{{ total }}</span>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 text-sm">Résultats: <span class="font-semibold">{{ total }}</span></div>
|
||||
{% if view == 'gallery' %}
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4">
|
||||
{% for m in movies %}
|
||||
<a href="{% if m.id %}/movie/{{ m.id }}{% elif m.path %}/movie/by-path?path={{ m.path | urlencode }}{% else %}#{% endif %}"
|
||||
class="group block bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-2xl overflow-hidden shadow hover:shadow-md transition">
|
||||
{% if m.poster %}
|
||||
<img src="{{ m.poster }}" alt="poster" class="w-full aspect-[2/3] object-cover">
|
||||
{% else %}
|
||||
<div class="w-full aspect-[2/3] bg-gray-200 dark:bg-gray-700"></div>
|
||||
{% endif %}
|
||||
<a href="/movie/{{ m.id }}" class="group block border rounded-2xl overflow-hidden shadow dark:border-gray-700">
|
||||
{% if m.poster %}<img src="{{ m.poster }}" class="w-full aspect-[2/3] object-cover">{% else %}<div class="w-full aspect-[2/3] bg-gray-200 dark:bg-gray-700"></div>{% endif %}
|
||||
<div class="p-2">
|
||||
<div class="text-sm font-medium line-clamp-2 group-hover:underline">{{ m.nom|default('—') }}</div>
|
||||
{% if m.year %}
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">{{ m.year }}</div>
|
||||
{% endif %}
|
||||
{% if m.year %}<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">{{ m.year }}</div>{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% if movies|length == 0 %}
|
||||
<div class="col-span-full text-center text-gray-500 dark:text-gray-400 py-8">Aucun résultat.</div>
|
||||
{% endif %}
|
||||
{% if movies|length == 0 %}<div class="col-span-full text-center py-8">Aucun résultat.</div>{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow overflow-x-auto border border-gray-200 dark:border-gray-700">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow overflow-x-auto border">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-200">
|
||||
<tr>
|
||||
<th class="text-left px-4 py-2">Poster</th>
|
||||
<th class="text-left px-4 py-2">Nom</th>
|
||||
<th class="text-left px-4 py-2">Titre originel</th>
|
||||
<th class="text-left px-4 py-2">Acteurs</th>
|
||||
<th class="text-left px-4 py-2">Pays</th>
|
||||
<th class="text-left px-4 py-2">Année</th>
|
||||
<th class="text-left px-4 py-2">Date</th>
|
||||
<th class="text-left px-4 py-2">Chemin</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<thead><tr><th class="text-left px-4 py-2">Poster</th><th class="text-left px-4 py-2">Nom</th><th class="text-left px-4 py-2">Titre originel</th><th class="text-left px-4 py-2">Acteurs</th><th class="text-left px-4 py-2">Pays</th><th class="text-left px-4 py-2">Année</th><th class="text-left px-4 py-2">Date</th><th class="text-left px-4 py-2">Chemin</th></tr></thead>
|
||||
<tbody>
|
||||
{% for m in movies %}
|
||||
<tr class="border-t border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/40">
|
||||
<td class="px-4 py-2">
|
||||
{% if m.poster %}
|
||||
<img src="{{ m.poster }}" alt="poster" class="w-10 h-16 object-cover rounded-lg shadow">
|
||||
{% else %}
|
||||
<div class="w-10 h-16 bg-gray-200 dark:bg-gray-700 rounded-lg"></div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-4 py-2">
|
||||
{% if m.id %}
|
||||
<a href="/movie/{{ m.id }}" class="text-indigo-600 dark:text-indigo-400 hover:underline">{{ m.nom|default('—') }}</a>
|
||||
{% elif m.path %}
|
||||
<a href="/movie/by-path?path={{ m.path | urlencode }}" class="text-indigo-600 dark:text-indigo-400 hover:underline">{{ m.nom|default('—') }}</a>
|
||||
{% else %}
|
||||
<span>{{ m.nom|default('—') }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<tr class="border-t dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/40">
|
||||
<td class="px-4 py-2">{% if m.poster %}<img src="{{ m.poster }}" class="w-10 h-16 object-cover rounded-lg shadow">{% else %}<div class="w-10 h-16 bg-gray-200 dark:bg-gray-700 rounded-lg"></div>{% endif %}</td>
|
||||
<td class="px-4 py-2"><a href="/movie/{{ m.id }}" class="text-indigo-600 dark:text-indigo-400 hover:underline">{{ m.nom|default('—') }}</a></td>
|
||||
<td class="px-4 py-2">{{ m.titre|none_to_dash }}</td>
|
||||
<td class="px-4 py-2"><div class="line-clamp-2">{{ m.acteurs|none_to_dash }}</div></td>
|
||||
<td class="px-4 py-2">{{ m.pays|none_to_dash }}</td>
|
||||
@ -163,39 +79,20 @@
|
||||
<td class="px-4 py-2"><code class="text-xs">{{ m.path|none_to_dash }}</code></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if movies|length == 0 %}
|
||||
<tr><td colspan="8" class="px-4 py-6 text-center text-gray-500 dark:text-gray-400">Aucun résultat.</td></tr>
|
||||
{% endif %}
|
||||
{% if movies|length == 0 %}<tr><td colspan="8" class="px-4 py-6 text-center">Aucun résultat.</td></tr>{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if last_page > 1 %}
|
||||
<nav class="mt-6 flex items-center justify-center gap-2 flex-wrap">
|
||||
<a href="/?{{ build_querystring(page=1) }}"
|
||||
class="px-3 py-1 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 {{ 'opacity-60 pointer-events-none' if page==1 else 'hover:bg-gray-100 dark:hover:bg-gray-800' }}">«</a>
|
||||
<a href="/?{{ build_querystring(page=page-1) }}"
|
||||
class="px-3 py-1 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 hover:bg-gray-100 dark:hover:bg-gray-800">‹</a>
|
||||
|
||||
{% if start_page > 1 %}
|
||||
<span class="px-2">…</span>
|
||||
{% endif %}
|
||||
|
||||
<a href="/movies?{{ build_querystring(page=1) }}" class="px-3 py-1 rounded-lg border {{ 'opacity-60 pointer-events-none' if page==1 else '' }}">«</a>
|
||||
<a href="/movies?{{ build_querystring(page=page-1) }}" class="px-3 py-1 rounded-lg border">‹</a>
|
||||
{% for p in range(start_page, end_page + 1) %}
|
||||
<a href="/?{{ build_querystring(page=p) }}"
|
||||
class="px-3 py-1 rounded-lg border border-gray-300 dark:border-gray-700 {{ 'bg-indigo-600 text-white' if p==page else 'bg-white dark:bg-gray-900 hover:bg-gray-100 dark:hover:bg-gray-800' }}">{{ p }}</a>
|
||||
<a href="/movies?{{ build_querystring(page=p) }}" class="px-3 py-1 rounded-lg border {{ 'bg-indigo-600 text-white' if p==page else '' }}">{{ p }}</a>
|
||||
{% endfor %}
|
||||
|
||||
{% if end_page < last_page %}
|
||||
<span class="px-2">…</span>
|
||||
{% endif %}
|
||||
|
||||
<a href="/?{{ build_querystring(page=page+1) }}"
|
||||
class="px-3 py-1 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 hover:bg-gray-100 dark:hover:bg-gray-800">›</a>
|
||||
<a href="/?{{ build_querystring(page=last_page) }}"
|
||||
class="px-3 py-1 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 {{ 'opacity-60 pointer-events-none' if page==last_page else 'hover:bg-gray-100 dark:hover:bg-gray-800' }}">»</a>
|
||||
<a href="/movies?{{ build_querystring(page=page+1) }}" class="px-3 py-1 rounded-lg border">›</a>
|
||||
<a href="/movies?{{ build_querystring(page=last_page) }}" class="px-3 py-1 rounded-lg border {{ 'opacity-60 pointer-events-none' if page==last_page else '' }}">»</a>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
201
templates/movies.html.v1
Normal file
201
templates/movies.html.v1
Normal file
@ -0,0 +1,201 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Kodi - Films{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow p-4 md:p-6 mb-6 border border-gray-200 dark:border-gray-700">
|
||||
<form method="get" class="grid grid-cols-1 md:grid-cols-6 gap-3 items-end">
|
||||
<!-- Hidden to preserve state -->
|
||||
<input type="hidden" name="view" value="{{ view }}">
|
||||
<input type="hidden" name="dir" value="{{ direction }}">
|
||||
<input type="hidden" name="page" value="{{ page }}">
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Recherche</label>
|
||||
<input type="text" name="q" value="{{ q }}" placeholder="Titre / synopsis"
|
||||
class="w-full rounded-xl border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Acteur</label>
|
||||
<input type="text" name="actor" value="{{ actor }}" placeholder="ex: Tom Hanks"
|
||||
class="w-full rounded-xl border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Pays</label>
|
||||
<input type="text" name="country" value="{{ country }}" placeholder="ex: France"
|
||||
class="w-full rounded-xl border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Année min</label>
|
||||
<input type="number" name="year_from" value="{{ year_from }}" min="1900" max="2100"
|
||||
class="w-full rounded-xl border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Année max</label>
|
||||
<input type="number" name="year_to" value="{{ year_to }}" min="1900" max="2100"
|
||||
class="w-full rounded-xl border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500">
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button type="submit"
|
||||
class="rounded-xl px-4 py-2 bg-indigo-600 text-white font-medium hover:bg-indigo-700 shadow">
|
||||
Filtrer
|
||||
</button>
|
||||
<a href="/?view={{ view }}"
|
||||
class="rounded-xl px-4 py-2 bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 font-medium hover:bg-gray-200 dark:hover:bg-gray-600 border border-gray-300 dark:border-gray-600">
|
||||
Réinitialiser
|
||||
</a>
|
||||
<a href="/export.csv?{{ build_querystring() }}"
|
||||
class="rounded-xl px-4 py-2 bg-emerald-600 text-white font-medium hover:bg-emerald-700 shadow">
|
||||
Export CSV
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-6 flex items-center gap-2 mt-1">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-300">Vue :</span>
|
||||
<a href="/?{{ build_querystring(view='gallery', page=1, per_page=24) }}"
|
||||
class="text-sm px-3 py-1 rounded-lg border {{ 'bg-indigo-600 text-white' if view=='gallery' else 'bg-white dark:bg-gray-900 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 border-gray-300 dark:border-gray-600' }}">
|
||||
Galerie
|
||||
</a>
|
||||
<a href="/?{{ build_querystring(view='list', page=1, per_page=25) }}"
|
||||
class="text-sm px-3 py-1 rounded-lg border {{ 'bg-indigo-600 text-white' if view=='list' else 'bg-white dark:bg-gray-900 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 border-gray-300 dark:border-gray-600' }}">
|
||||
Liste
|
||||
</a>
|
||||
|
||||
<div class="ml-auto flex items-center gap-2">
|
||||
<label class="text-sm text-gray-600 dark:text-gray-300">Trier par :</label>
|
||||
<select name="sort"
|
||||
onchange="this.form.page.value=1; this.form.submit()"
|
||||
class="rounded-xl border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-2 py-1">
|
||||
{% for key,label in {'name':'Nom','original':'Titre','actors':'Acteurs','country':'Pays','year':'Année','date':'Date','path':'Chemin'}.items() %}
|
||||
<option value="{{key}}" {% if sort==key %}selected{% endif %}>{{label}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% set dir_toggle = ('desc' if direction=='asc' else 'asc') %}
|
||||
<a href="/?{{ build_querystring(dir=dir_toggle, page=1) }}"
|
||||
class="text-sm px-3 py-1 rounded-lg border bg-white dark:bg-gray-900 border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-800">
|
||||
{{ '▲ Asc' if direction=='asc' else '▼ Desc' }}
|
||||
</a>
|
||||
|
||||
<label class="text-sm text-gray-600 dark:text-gray-300 ml-3">Par page :</label>
|
||||
<select name="per_page"
|
||||
onchange="this.form.page.value=1; this.form.submit()"
|
||||
class="rounded-xl border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-2 py-1">
|
||||
{% if view=='gallery' %}
|
||||
{% for n in (12,24,48,96) %}
|
||||
<option value="{{n}}" {% if per_page==n %}selected{% endif %}>{{n}}</option>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% for n in (10,25,50,100) %}
|
||||
<option value="{{n}}" {% if per_page==n %}selected{% endif %}>{{n}}</option>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 text-sm text-gray-600 dark:text-gray-300">
|
||||
Résultats: <span class="font-semibold">{{ total }}</span>
|
||||
</div>
|
||||
|
||||
{% if view == 'gallery' %}
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4">
|
||||
{% for m in movies %}
|
||||
<a href="{% if m.id %}/movie/{{ m.id }}{% elif m.path %}/movie/by-path?path={{ m.path | urlencode }}{% else %}#{% endif %}"
|
||||
class="group block bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-2xl overflow-hidden shadow hover:shadow-md transition">
|
||||
{% if m.poster %}
|
||||
<img src="{{ m.poster }}" alt="poster" class="w-full aspect-[2/3] object-cover">
|
||||
{% else %}
|
||||
<div class="w-full aspect-[2/3] bg-gray-200 dark:bg-gray-700"></div>
|
||||
{% endif %}
|
||||
<div class="p-2">
|
||||
<div class="text-sm font-medium line-clamp-2 group-hover:underline">{{ m.nom|default('—') }}</div>
|
||||
{% if m.year %}
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">{{ m.year }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% if movies|length == 0 %}
|
||||
<div class="col-span-full text-center text-gray-500 dark:text-gray-400 py-8">Aucun résultat.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow overflow-x-auto border border-gray-200 dark:border-gray-700">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-200">
|
||||
<tr>
|
||||
<th class="text-left px-4 py-2">Poster</th>
|
||||
<th class="text-left px-4 py-2">Nom</th>
|
||||
<th class="text-left px-4 py-2">Titre originel</th>
|
||||
<th class="text-left px-4 py-2">Acteurs</th>
|
||||
<th class="text-left px-4 py-2">Pays</th>
|
||||
<th class="text-left px-4 py-2">Année</th>
|
||||
<th class="text-left px-4 py-2">Date</th>
|
||||
<th class="text-left px-4 py-2">Chemin</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for m in movies %}
|
||||
<tr class="border-t border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/40">
|
||||
<td class="px-4 py-2">
|
||||
{% if m.poster %}
|
||||
<img src="{{ m.poster }}" alt="poster" class="w-10 h-16 object-cover rounded-lg shadow">
|
||||
{% else %}
|
||||
<div class="w-10 h-16 bg-gray-200 dark:bg-gray-700 rounded-lg"></div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-4 py-2">
|
||||
{% if m.id %}
|
||||
<a href="/movie/{{ m.id }}" class="text-indigo-600 dark:text-indigo-400 hover:underline">{{ m.nom|default('—') }}</a>
|
||||
{% elif m.path %}
|
||||
<a href="/movie/by-path?path={{ m.path | urlencode }}" class="text-indigo-600 dark:text-indigo-400 hover:underline">{{ m.nom|default('—') }}</a>
|
||||
{% else %}
|
||||
<span>{{ m.nom|default('—') }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-4 py-2">{{ m.titre|none_to_dash }}</td>
|
||||
<td class="px-4 py-2"><div class="line-clamp-2">{{ m.acteurs|none_to_dash }}</div></td>
|
||||
<td class="px-4 py-2">{{ m.pays|none_to_dash }}</td>
|
||||
<td class="px-4 py-2">{{ m.year if m.year else '—' }}</td>
|
||||
<td class="px-4 py-2">{{ m.date if m.date else '—' }}</td>
|
||||
<td class="px-4 py-2"><code class="text-xs">{{ m.path|none_to_dash }}</code></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if movies|length == 0 %}
|
||||
<tr><td colspan="8" class="px-4 py-6 text-center text-gray-500 dark:text-gray-400">Aucun résultat.</td></tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if last_page > 1 %}
|
||||
<nav class="mt-6 flex items-center justify-center gap-2 flex-wrap">
|
||||
<a href="/?{{ build_querystring(page=1) }}"
|
||||
class="px-3 py-1 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 {{ 'opacity-60 pointer-events-none' if page==1 else 'hover:bg-gray-100 dark:hover:bg-gray-800' }}">«</a>
|
||||
<a href="/?{{ build_querystring(page=page-1) }}"
|
||||
class="px-3 py-1 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 hover:bg-gray-100 dark:hover:bg-gray-800">‹</a>
|
||||
|
||||
{% if start_page > 1 %}
|
||||
<span class="px-2">…</span>
|
||||
{% endif %}
|
||||
|
||||
{% for p in range(start_page, end_page + 1) %}
|
||||
<a href="/?{{ build_querystring(page=p) }}"
|
||||
class="px-3 py-1 rounded-lg border border-gray-300 dark:border-gray-700 {{ 'bg-indigo-600 text-white' if p==page else 'bg-white dark:bg-gray-900 hover:bg-gray-100 dark:hover:bg-gray-800' }}">{{ p }}</a>
|
||||
{% endfor %}
|
||||
|
||||
{% if end_page < last_page %}
|
||||
<span class="px-2">…</span>
|
||||
{% endif %}
|
||||
|
||||
<a href="/?{{ build_querystring(page=page+1) }}"
|
||||
class="px-3 py-1 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 hover:bg-gray-100 dark:hover:bg-gray-800">›</a>
|
||||
<a href="/?{{ build_querystring(page=last_page) }}"
|
||||
class="px-3 py-1 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 {{ 'opacity-60 pointer-events-none' if page==last_page else 'hover:bg-gray-100 dark:hover:bg-gray-800' }}">»</a>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
15
templates/show_detail.html
Normal file
15
templates/show_detail.html
Normal file
@ -0,0 +1,15 @@
|
||||
{% extends "base.html" %}{% block title %}Détail Série{% endblock %}{% block content %}
|
||||
<a href="javascript:history.back()" class="text-sm text-indigo-600 dark:text-indigo-400 hover:underline">← Retour</a>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow p-6 mt-3 border">
|
||||
<div class="flex gap-6">
|
||||
<div>{% if s.poster %}<img src="{{ s.poster }}" alt="poster" class="w-40 object-cover rounded-xl shadow">{% else %}<div class="w-40 h-60 bg-gray-200 dark:bg-gray-700 rounded-xl"></div>{% endif %}</div>
|
||||
<div class="flex-1">
|
||||
<h1 class="text-2xl font-bold mb-2">{{ s.titre }}</h1>
|
||||
<div class="text-gray-600 dark:text-gray-300 mb-4">{% if s.year %}<span class="mr-2">{{ s.year }}</span>{% endif %}{% if s.diffuseur %}<span class="mr-2">• {{ s.diffuseur }}</span>{% endif %}{% if s.date %}<span class="mr-2">• {{ s.date }}</span>{% endif %}</div>
|
||||
{% if s.original %}<div class="mb-2"><span class="font-medium">Titre original :</span> {{ s.original }}</div>{% endif %}
|
||||
{% if s.genre %}<div class="mb-2"><span class="font-medium">Genre :</span> {{ s.genre }}</div>{% endif %}
|
||||
{% if s.desc %}<div class="mt-4"><h2 class="font-semibold mb-1">Résumé</h2><p class="whitespace-pre-wrap">{{ s.desc }}</p></div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
85
templates/shows.html
Normal file
85
templates/shows.html
Normal file
@ -0,0 +1,85 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Séries - Kodi{% endblock %}
|
||||
{% block content %}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow p-4 md:p-6 mb-6 border">
|
||||
<form method="get" action="/shows" class="grid grid-cols-1 md:grid-cols-6 gap-3 items-end">
|
||||
<input type="hidden" name="view" value="{{ view }}"><input type="hidden" name="dir" value="{{ direction }}"><input type="hidden" name="page" value="{{ page }}">
|
||||
<div><label class="block text-sm font-medium mb-1">Recherche</label><input type="text" name="q" value="{{ q }}" class="w-full rounded-xl border px-3 py-2 dark:bg-gray-900 dark:border-gray-700"></div>
|
||||
<div><label class="block text-sm font-medium mb-1">Diffuseur</label><input type="text" name="network" value="{{ network }}" class="w-full rounded-xl border px-3 py-2 dark:bg-gray-900 dark:border-gray-700"></div>
|
||||
<div><label class="block text-sm font-medium mb-1">Genre</label><input type="text" name="genre" value="{{ genre }}" class="w-full rounded-xl border px-3 py-2 dark:bg-gray-900 dark:border-gray-700"></div>
|
||||
<div><label class="block text-sm font-medium mb-1">Année min</label><input type="number" name="year_from" value="{{ year_from }}" class="w-full rounded-xl border px-3 py-2 dark:bg-gray-900 dark:border-gray-700"></div>
|
||||
<div><label class="block text-sm font-medium mb-1">Année max</label><input type="number" name="year_to" value="{{ year_to }}" class="w-full rounded-xl border px-3 py-2 dark:bg-gray-900 dark:border-gray-700"></div>
|
||||
<div class="flex gap-2">
|
||||
<button class="rounded-xl px-4 py-2 bg-indigo-600 text-white">Filtrer</button>
|
||||
<a href="/shows?view={{ view }}" class="rounded-xl px-4 py-2 border dark:border-gray-600">Réinitialiser</a>
|
||||
<a href="/export_shows.csv?{{ build_querystring() }}" class="rounded-xl px-4 py-2 bg-emerald-600 text-white">Export CSV</a>
|
||||
</div>
|
||||
<div class="md:col-span-6 flex items-center gap-2 mt-1">
|
||||
<span class="text-sm">Vue :</span>
|
||||
<a href="/shows?{{ build_querystring(view='gallery', page=1, per_page=24) }}" class="text-sm px-3 py-1 rounded-lg border {{ 'bg-indigo-600 text-white' if view=='gallery' else '' }}">Galerie</a>
|
||||
<a href="/shows?{{ build_querystring(view='list', page=1, per_page=25) }}" class="text-sm px-3 py-1 rounded-lg border {{ 'bg-indigo-600 text-white' if view=='list' else '' }}">Liste</a>
|
||||
<div class="ml-auto flex items-center gap-2">
|
||||
<label class="text-sm">Trier par :</label>
|
||||
<select name="sort" onchange="this.form.page.value=1; this.form.submit()" class="rounded-xl border px-2 py-1 dark:bg-gray-900 dark:border-gray-700">
|
||||
{% for key,label in {'title':'Titre','original':'Original','network':'Diffuseur','genre':'Genre','year':'Année','date':'Date'}.items() %}
|
||||
<option value="{{key}}" {% if sort==key %}selected{% endif %}>{{label}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% set dir_toggle = ('desc' if direction=='asc' else 'asc') %}
|
||||
<a href="/shows?{{ build_querystring(dir=dir_toggle, page=1) }}" class="text-sm px-3 py-1 rounded-lg border">{{ '▲ Asc' if direction=='asc' else '▼ Desc' }}</a>
|
||||
<label class="text-sm ml-3">Par page :</label>
|
||||
<select name="per_page" onchange="this.form.page.value=1; this.form.submit()" class="rounded-xl border px-2 py-1 dark:bg-gray-900 dark:border-gray-700">
|
||||
{% if view=='gallery' %}{% for n in (12,24,48,96) %}<option value="{{n}}" {% if per_page==n %}selected{% endif %}>{{n}}</option>{% endfor %}
|
||||
{% else %}{% for n in (10,25,50,100) %}<option value="{{n}}" {% if per_page==n %}selected{% endif %}>{{n}}</option>{% endfor %}{% endif %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="mb-3 text-sm">Résultats: <span class="font-semibold">{{ total }}</span></div>
|
||||
{% if view == 'gallery' %}
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4">
|
||||
{% for s in shows %}
|
||||
<a href="/show/{{ s.id }}" class="group block border rounded-2xl overflow-hidden shadow dark:border-gray-700">
|
||||
{% if s.poster %}<img src="{{ s.poster }}" class="w-full aspect-[2/3] object-cover">{% else %}<div class="w-full aspect-[2/3] bg-gray-200 dark:bg-gray-700"></div>{% endif %}
|
||||
<div class="p-2">
|
||||
<div class="text-sm font-medium line-clamp-2 group-hover:underline">{{ s.titre|default('—') }}</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">{% if s.year %}{{ s.year }}{% endif %}{% if s.genre %} • {{ s.genre }}{% endif %}</div>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% if shows|length == 0 %}<div class="col-span-full text-center py-8">Aucun résultat.</div>{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow overflow-x-auto border">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead><tr><th class="text-left px-4 py-2">Poster</th><th class="text-left px-4 py-2">Titre</th><th class="text-left px-4 py-2">Original</th><th class="text-left px-4 py-2">Genre</th><th class="text-left px-4 py-2">Diffuseur</th><th class="text-left px-4 py-2">Année</th><th class="text-left px-4 py-2">Date</th></tr></thead>
|
||||
<tbody>
|
||||
{% for s in shows %}
|
||||
<tr class="border-t dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/40">
|
||||
<td class="px-4 py-2">{% if s.poster %}<img src="{{ s.poster }}" class="w-10 h-16 object-cover rounded-lg shadow">{% else %}<div class="w-10 h-16 bg-gray-200 dark:bg-gray-700 rounded-lg"></div>{% endif %}</td>
|
||||
<td class="px-4 py-2"><a href="/show/{{ s.id }}" class="text-indigo-600 dark:text-indigo-400 hover:underline">{{ s.titre|default('—') }}</a></td>
|
||||
<td class="px-4 py-2">{{ s.original|none_to_dash }}</td>
|
||||
<td class="px-4 py-2">{{ s.genre|none_to_dash }}</td>
|
||||
<td class="px-4 py-2">{{ s.diffuseur|none_to_dash }}</td>
|
||||
<td class="px-4 py-2">{{ s.year if s.year else '—' }}</td>
|
||||
<td class="px-4 py-2">{{ s.date if s.date else '—' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if shows|length == 0 %}<tr><td colspan="7" class="px-4 py-6 text-center">Aucun résultat.</td></tr>{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if last_page > 1 %}
|
||||
<nav class="mt-6 flex items-center justify-center gap-2 flex-wrap">
|
||||
<a href="/shows?{{ build_querystring(page=1) }}" class="px-3 py-1 rounded-lg border {{ 'opacity-60 pointer-events-none' if page==1 else '' }}">«</a>
|
||||
<a href="/shows?{{ build_querystring(page=page-1) }}" class="px-3 py-1 rounded-lg border">‹</a>
|
||||
{% for p in range(start_page, end_page + 1) %}
|
||||
<a href="/shows?{{ build_querystring(page=p) }}" class="px-3 py-1 rounded-lg border {{ 'bg-indigo-600 text-white' if p==page else '' }}">{{ p }}</a>
|
||||
{% endfor %}
|
||||
<a href="/shows?{{ build_querystring(page=page+1) }}" class="px-3 py-1 rounded-lg border">›</a>
|
||||
<a href="/shows?{{ build_querystring(page=last_page) }}" class="px-3 py-1 rounded-lg border {{ 'opacity-60 pointer-events-none' if page==last_page else '' }}">»</a>
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
Loading…
x
Reference in New Issue
Block a user