first commit

This commit is contained in:
Pierre 2025-10-24 23:56:54 +02:00
commit 753f31f4dd
10 changed files with 788 additions and 0 deletions

BIN
MyVideos131.db Normal file

Binary file not shown.

4
README.txt Normal file
View File

@ -0,0 +1,4 @@
apt install python3-flask
# Databae
DB_PATH = r"/root/.kodi/userdata/Database/MyVideos131.db"

24
algo.txt Normal file
View File

@ -0,0 +1,24 @@
Recuperer la base :
scp -r kodi.nerk:/root/.kodi/userdata/Database/MyVideos131.db
#FILM
## Liste des films :
SELECT c00 from movie;
## Description :
SELECT c01 from movie WHERE c00 = 'Wonder';
##Acteurs
SELECT c06 from movie WHERE c00 = 'Yves Saint Laurent';
##Titre
SELECT c16 from movie WHERE c00 = 'Yves Saint Laurent';
##Pays
SELECT c21 from movie WHERE c00 = 'Yves Saint Laurent';
##Path
SELECT c22 from movie WHERE c00 = 'Yves Saint Laurent';
#Date
SELECT premiered FROM `movie` WHERE c00 = 'Yves Saint Laurent';
SELECT c00, c01, c06, c16, c21, c22, premiered FROM `movie` ORDER BY c00 ASC;
nom,desc,acteurs,titre,pays,path,date =
#SERIE

295
app.py Normal file
View File

@ -0,0 +1,295 @@
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"/root/.kodi/userdata/Database/MyVideos131.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)

45
templates/base.html Normal file
View 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>

9
templates/error.html Normal file
View 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 %}

View 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 %}

View File

@ -0,0 +1,33 @@
{% extends "base.html" %}
{% block title %}{{ m.nom }} - Détail{% endblock %}
{% block content %}
<a href="javascript:history.back()" class="text-sm text-indigo-600 hover:underline">← Retour</a>
<div class="bg-white rounded-2xl shadow p-6 mt-3">
<h1 class="text-2xl font-bold mb-2">{{ m.nom }}</h1>
<div class="text-gray-600 mb-4">
{% if m.year %}<span class="mr-2">{{ m.year }}</span>{% endif %}
{% if m.pays %}<span class="mr-2">• {{ m.pays }}</span>{% endif %}
</div>
{% if m.titre %}
<div class="mb-2"><span class="font-medium">Titre original :</span> {{ m.titre }}</div>
{% endif %}
{% if m.date %}
<div class="mb-2"><span class="font-medium">Date de sortie :</span> {{ m.date }}</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>
{% endblock %}

201
templates/movies.html Normal file
View 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&nbsp;:</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&nbsp;:</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 %}

135
templates/movies.html.ori Normal file
View File

@ -0,0 +1,135 @@
{% extends "base.html" %}
{% block title %}Kodi - Films{% endblock %}
{% block content %}
<h1 class="text-2xl md:text-3xl font-bold mb-4">📚 Films Kodi</h1>
<div class="bg-white rounded-2xl shadow p-4 md:p-6 mb-6">
<form method="get" class="grid grid-cols-1 md:grid-cols-6 gap-3 items-end">
<div>
<label class="block text-sm font-medium mb-1">Recherche</label>
<input type="text" name="q" value="{{ q }}" placeholder="Titre / synopsis"
class="w-full rounded-xl border border-gray-300 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">Acteur</label>
<input type="text" name="actor" value="{{ actor }}" placeholder="ex: Tom Hanks"
class="w-full rounded-xl border border-gray-300 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">Pays</label>
<input type="text" name="country" value="{{ country }}" placeholder="ex: France"
class="w-full rounded-xl border border-gray-300 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">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 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">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 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="/"
class="rounded-xl px-4 py-2 bg-gray-100 text-gray-800 font-medium hover:bg-gray-200 border">
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>
</form>
</div>
<div class="mb-3 text-sm text-gray-600">
Résultats: <span class="font-semibold">{{ total }}</span>
</div>
{% macro sort_link(label, key) -%}
{%- set is_active = (sort == key) -%}
{%- set next_dir = ('desc' if direction == 'asc' else 'asc') if is_active else 'asc' -%}
<a href="/?{{ build_querystring(sort=key, dir=next_dir, page=1) }}"
class="inline-flex items-center gap-1 hover:underline">
<span>{{ label }}</span>
{%- if is_active -%}
<span class="text-xs">{{ '▲' if direction=='asc' else '▼' }}</span>
{%- else -%}
<span class="text-xs text-gray-400">↕</span>
{%- endif -%}
</a>
{%- endmacro %}
<div class="bg-white rounded-2xl shadow overflow-x-auto">
<table class="min-w-full text-sm">
<thead class="bg-gray-100 text-gray-700">
<tr>
<th class="text-left px-4 py-2">{{ sort_link('Nom', 'name') }}</th>
<th class="text-left px-4 py-2">{{ sort_link('Titre originel', 'original') }}</th>
<th class="text-left px-4 py-2">{{ sort_link('Acteurs', 'actors') }}</th>
<th class="text-left px-4 py-2">{{ sort_link('Pays', 'country') }}</th>
<th class="text-left px-4 py-2">{{ sort_link('Année', 'year') }}</th>
<th class="text-left px-4 py-2">{{ sort_link('Date', 'date') }}</th>
<th class="text-left px-4 py-2">{{ sort_link('Chemin', 'path') }}</th>
</tr>
</thead>
<tbody>
{% for m in movies %}
<tr class="border-t hover:bg-gray-50">
<td class="px-4 py-2">
{% if m.id %}
<a href="/movie/{{ m.id }}" class="text-indigo-600 hover:underline">{{ m.nom|default('—') }}</a>
{% elif m.path %}
<a href="/movie/by-path?path={{ m.path | urlencode }}" class="text-indigo-600 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="7" class="px-4 py-6 text-center text-gray-500">Aucun résultat.</td></tr>
{% endif %}
</tbody>
</table>
</div>
{% 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 {{ 'bg-gray-200' if page==1 else 'bg-white hover:bg-gray-100' }}">«</a>
<a href="/?{{ build_querystring(page=page-1) }}"
class="px-3 py-1 rounded-lg border {{ 'bg-white hover:bg-gray-100' }}"></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 {{ 'bg-indigo-600 text-white' if p==page else 'bg-white hover:bg-gray-100' }}">{{ 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 {{ 'bg-white hover:bg-gray-100' }}"></a>
<a href="/?{{ build_querystring(page=last_page) }}"
class="px-3 py-1 rounded-lg border {{ 'bg-gray-200' if page==last_page else 'bg-white hover:bg-gray-100' }}">»</a>
</nav>
{% endif %}
{% endblock %}