first commit
This commit is contained in:
commit
753f31f4dd
BIN
MyVideos131.db
Normal file
BIN
MyVideos131.db
Normal file
Binary file not shown.
4
README.txt
Normal file
4
README.txt
Normal file
@ -0,0 +1,4 @@
|
||||
apt install python3-flask
|
||||
# Databae
|
||||
DB_PATH = r"/root/.kodi/userdata/Database/MyVideos131.db"
|
||||
|
||||
24
algo.txt
Normal file
24
algo.txt
Normal 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
295
app.py
Normal 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
45
templates/base.html
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>
|
||||
9
templates/error.html
Normal file
9
templates/error.html
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 %}
|
||||
42
templates/movie_detail.html
Normal file
42
templates/movie_detail.html
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 %}
|
||||
33
templates/movie_detail.html.ori
Normal file
33
templates/movie_detail.html.ori
Normal 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
201
templates/movies.html
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 %}
|
||||
135
templates/movies.html.ori
Normal file
135
templates/movies.html.ori
Normal 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 %}
|
||||
Loading…
x
Reference in New Issue
Block a user