Version stable : Synthèse 3 cinémas avec affiches et horaires
This commit is contained in:
+13
@@ -0,0 +1,13 @@
|
|||||||
|
# Cache des données (ne pas versionner)
|
||||||
|
data/
|
||||||
|
*.json
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
venv/
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
.DS_Store
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
# On part d'une version légère de Python
|
||||||
|
FROM python:3.9-slim
|
||||||
|
|
||||||
|
# On définit le dossier de travail dans le conteneur
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# On installe les dépendances
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# On copie le code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# On crée le dossier pour le cache
|
||||||
|
RUN mkdir -p /data
|
||||||
|
|
||||||
|
# On expose le port 5001
|
||||||
|
EXPOSE 5001
|
||||||
|
|
||||||
|
# Commande de démarrage
|
||||||
|
CMD ["python", "app.py"]
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
# On part d'une version légère de Python
|
||||||
|
FROM python:3.9-slim
|
||||||
|
|
||||||
|
# On définit le dossier de travail dans le conteneur
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# On installe les dépendances
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# On copie le code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# On crée le dossier pour le cache
|
||||||
|
RUN mkdir -p /data
|
||||||
|
|
||||||
|
# On expose le port 5000
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
# Commande de démarrage
|
||||||
|
CMD ["python", "app.py"]
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import os, json, logging, requests, re, time
|
||||||
|
from datetime import datetime
|
||||||
|
from flask import Flask, render_template, redirect, url_for
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from apscheduler.schedulers.background import BackgroundScheduler
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(message)s')
|
||||||
|
logger = logging.getLogger("CineSynthese")
|
||||||
|
|
||||||
|
CACHE_FILE = "/data/movies_cache.json"
|
||||||
|
HEADERS = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36'}
|
||||||
|
|
||||||
|
def get_today_formats():
|
||||||
|
now = datetime.now()
|
||||||
|
jours_courts = ["Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dim"]
|
||||||
|
day_short = jours_courts[now.weekday()]
|
||||||
|
return {
|
||||||
|
"cosmo_id": f"seance-{now.strftime('%Y-%m-%d')}",
|
||||||
|
"cratere_date": f"{day_short}. {now.day:02d}/{now.month:02d}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1. SCRAPER ABC (Ton code précis) ---
|
||||||
|
def scrape_abc():
|
||||||
|
url = "https://abc-toulouse.fr/horaires.html"
|
||||||
|
movies = []
|
||||||
|
try:
|
||||||
|
r = requests.get(url, headers=HEADERS, timeout=15)
|
||||||
|
soup = BeautifulSoup(r.content, 'html.parser')
|
||||||
|
movie_blocks = soup.find_all('div', class_='table_infos')
|
||||||
|
for block in movie_blocks:
|
||||||
|
title_tag = block.find('h2')
|
||||||
|
if not title_tag: continue
|
||||||
|
title = title_tag.get_text().strip()
|
||||||
|
img_tag = block.find('img')
|
||||||
|
img_url = "https://abc-toulouse.fr" + img_tag.get('src') if img_tag else ""
|
||||||
|
|
||||||
|
next_div = block.find_next_sibling()
|
||||||
|
showtimes_text = next_div.get_text(" ") if next_div else block.get_text(" ")
|
||||||
|
times = re.findall(r'(\d{2}:\d{2})', showtimes_text)
|
||||||
|
if times:
|
||||||
|
movies.append({
|
||||||
|
"title": title, "img": img_url, "times": sorted(list(set(times))),
|
||||||
|
"cinema": "ABC", "color": "#00bcd4"
|
||||||
|
})
|
||||||
|
except: logger.error("Fail ABC")
|
||||||
|
return movies
|
||||||
|
|
||||||
|
# --- 2. SCRAPER LE CRATÈRE (Ton code précis) ---
|
||||||
|
def scrape_cratere():
|
||||||
|
url_main = "https://www.cinemalecratere.fr/films"
|
||||||
|
target_date = get_today_formats()["cratere_date"]
|
||||||
|
movies_dict = {}
|
||||||
|
session = requests.Session()
|
||||||
|
try:
|
||||||
|
r = session.get(url_main, headers=HEADERS, timeout=15)
|
||||||
|
soup = BeautifulSoup(r.content, 'html.parser')
|
||||||
|
items = soup.find_all('div', attrs={'data-search': True})
|
||||||
|
for item in items:
|
||||||
|
if target_date in item['data-search']:
|
||||||
|
title_tag = item.find('span', class_='uk-text-bold')
|
||||||
|
title = title_tag.get_text(strip=True) if title_tag else "Inconnu"
|
||||||
|
time_match = re.search(r'(\d{2}h\d{2})', item['data-search'])
|
||||||
|
show_time = time_match.group(0).replace('h', ':') if time_match else "Séance"
|
||||||
|
if title not in movies_dict:
|
||||||
|
img_url = ""
|
||||||
|
link_tag = item.find('a', href=True)
|
||||||
|
if link_tag:
|
||||||
|
r_det = session.get("https://www.cinemalecratere.fr" + link_tag['href'], timeout=10)
|
||||||
|
img_tag = BeautifulSoup(r_det.content, 'html.parser').find('img', class_='el-image')
|
||||||
|
if img_tag:
|
||||||
|
img_url = img_tag.get('src')
|
||||||
|
if not img_url.startswith('http'): img_url = "https://www.cinemalecratere.fr" + img_url
|
||||||
|
movies_dict[title] = {"title": title, "img": img_url, "times": [show_time], "cinema": "Le Cratère", "color": "#e62117"}
|
||||||
|
else:
|
||||||
|
if show_time not in movies_dict[title]["times"]: movies_dict[title]["times"].append(show_time)
|
||||||
|
except: logger.error("Fail Cratère")
|
||||||
|
return list(movies_dict.values())
|
||||||
|
|
||||||
|
# --- 3. SCRAPER COSMO (REPRISE STRICTE DE TON CODE app_cosmo.py) ---
|
||||||
|
def scrape_cosmo():
|
||||||
|
BASE_URL = "https://www.american-cosmograph.fr"
|
||||||
|
url_horaires = f"{BASE_URL}/les-horaires.html"
|
||||||
|
target_id = get_today_formats()["cosmo_id"]
|
||||||
|
movies_today = []
|
||||||
|
session = requests.Session()
|
||||||
|
try:
|
||||||
|
r = session.get(url_horaires, headers=HEADERS, timeout=15)
|
||||||
|
soup = BeautifulSoup(r.content, 'html.parser')
|
||||||
|
day_container = soup.find('div', id=target_id)
|
||||||
|
if not day_container: return []
|
||||||
|
items = day_container.find_all('li', class_='heureReady')
|
||||||
|
for item in items:
|
||||||
|
title_tag = item.select_one('.filmTitle a')
|
||||||
|
if not title_tag: continue
|
||||||
|
title = title_tag.get_text(strip=True)
|
||||||
|
detail_path = title_tag.get('href')
|
||||||
|
time_tag = item.select_one('.horaire_debut')
|
||||||
|
time_raw = time_tag.get_text(strip=True) if time_tag else ""
|
||||||
|
st_match = re.search(r'\d{2}h\d{2}', time_raw)
|
||||||
|
show_time = st_match.group(0).replace('h', ':') if st_match else "Séance"
|
||||||
|
img_url = ""
|
||||||
|
if detail_path:
|
||||||
|
detail_url = f"{BASE_URL}{detail_path}"
|
||||||
|
try:
|
||||||
|
r_detail = session.get(detail_url, headers=HEADERS, timeout=10)
|
||||||
|
soup_detail = BeautifulSoup(r_detail.content, 'html.parser')
|
||||||
|
img_tag = soup_detail.find('img', class_='fc_field_image')
|
||||||
|
if img_tag:
|
||||||
|
img_url = img_tag.get('src')
|
||||||
|
if img_url and not img_url.startswith('http'):
|
||||||
|
img_url = f"{BASE_URL}{img_url}"
|
||||||
|
except: pass
|
||||||
|
movies_today.append({"title": title, "img": img_url, "times": [show_time], "cinema": "American Cosmograph", "color": "#ffc107"})
|
||||||
|
time.sleep(0.1)
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
unique_movies = {}
|
||||||
|
for m in movies_today:
|
||||||
|
if m['title'] not in unique_movies:
|
||||||
|
unique_movies[m['title']] = m
|
||||||
|
else:
|
||||||
|
if m['times'][0] not in unique_movies[m['title']]['times']:
|
||||||
|
unique_movies[m['title']]['times'].append(m['times'][0])
|
||||||
|
unique_movies[m['title']]['times'].sort()
|
||||||
|
return list(unique_movies.values())
|
||||||
|
|
||||||
|
# --- LOGIQUE MISE À JOUR ---
|
||||||
|
def update_all_data():
|
||||||
|
logger.info("🕒 Mise à jour programmée...")
|
||||||
|
try:
|
||||||
|
data = scrape_abc() + scrape_cratere() + scrape_cosmo()
|
||||||
|
data = sorted(data, key=lambda x: x['times'][0] if x['times'] else "99:99")
|
||||||
|
payload = {"last_update": datetime.now().strftime("%d/%m/%Y à %H:%M"), "movies": data}
|
||||||
|
os.makedirs(os.path.dirname(CACHE_FILE), exist_ok=True)
|
||||||
|
with open(CACHE_FILE, 'w') as f: json.dump(payload, f)
|
||||||
|
return payload
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur MAJ: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
scheduler = BackgroundScheduler()
|
||||||
|
scheduler.add_job(func=update_all_data, trigger="cron", hour=0, minute=5)
|
||||||
|
scheduler.start()
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
if not os.path.exists(CACHE_FILE):
|
||||||
|
cache = update_all_data()
|
||||||
|
else:
|
||||||
|
with open(CACHE_FILE, 'r') as f: cache = json.load(f)
|
||||||
|
if isinstance(cache, list): cache = update_all_data()
|
||||||
|
return render_template('index.html', films=cache["movies"], updated=cache["last_update"])
|
||||||
|
|
||||||
|
@app.route('/refresh')
|
||||||
|
def refresh():
|
||||||
|
update_all_data()
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(host='0.0.0.0', port=5000)
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
import os, json, logging, requests, re, time
|
||||||
|
from datetime import datetime
|
||||||
|
from flask import Flask, render_template, redirect, url_for
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from apscheduler.schedulers.background import BackgroundScheduler
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
logger = logging.getLogger("CineSynthese")
|
||||||
|
|
||||||
|
CACHE_FILE = "/data/movies_cache.json"
|
||||||
|
HEADERS = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36'}
|
||||||
|
|
||||||
|
def get_today_formats():
|
||||||
|
now = datetime.now()
|
||||||
|
jours_courts = ["Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dim"]
|
||||||
|
day_short = jours_courts[now.weekday()]
|
||||||
|
return {
|
||||||
|
"cosmo_id": f"seance-{now.strftime('%Y-%m-%d')}",
|
||||||
|
"cratere_date": f"{day_short}. {now.day:02d}/{now.month:02d}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1. SCRAPER ABC ---
|
||||||
|
def scrape_abc():
|
||||||
|
url = "https://abc-toulouse.fr/horaires.html"
|
||||||
|
movies = []
|
||||||
|
try:
|
||||||
|
r = requests.get(url, headers=HEADERS, timeout=15)
|
||||||
|
soup = BeautifulSoup(r.content, 'html.parser')
|
||||||
|
movie_blocks = soup.find_all('div', class_='table_infos')
|
||||||
|
for block in movie_blocks:
|
||||||
|
title_tag = block.find('h2')
|
||||||
|
if not title_tag: continue
|
||||||
|
title = title_tag.get_text().strip()
|
||||||
|
img_tag = block.find('img')
|
||||||
|
img_url = "https://abc-toulouse.fr" + img_tag.get('src') if img_tag else ""
|
||||||
|
next_div = block.find_next_sibling()
|
||||||
|
showtimes_text = next_div.get_text(" ") if next_div else block.get_text(" ")
|
||||||
|
times = re.findall(r'(\d{2}:\d{2})', showtimes_text)
|
||||||
|
if times:
|
||||||
|
movies.append({"title": title, "img": img_url, "times": sorted(list(set(times))), "cinema": "ABC", "color": "#00bcd4"})
|
||||||
|
except: logger.error("Fail ABC")
|
||||||
|
return movies
|
||||||
|
|
||||||
|
# --- 2. SCRAPER LE CRATÈRE ---
|
||||||
|
def scrape_cratere():
|
||||||
|
url_main = "https://www.cinemalecratere.fr/films"
|
||||||
|
target_date = get_today_formats()["cratere_date"]
|
||||||
|
movies_dict = {}
|
||||||
|
session = requests.Session()
|
||||||
|
try:
|
||||||
|
r = session.get(url_main, headers=HEADERS, timeout=15)
|
||||||
|
soup = BeautifulSoup(r.content, 'html.parser')
|
||||||
|
items = soup.find_all('div', attrs={'data-search': True})
|
||||||
|
for item in items:
|
||||||
|
if target_date in item['data-search']:
|
||||||
|
title_tag = item.find('span', class_='uk-text-bold')
|
||||||
|
title = title_tag.get_text(strip=True) if title_tag else "Inconnu"
|
||||||
|
time_match = re.search(r'(\d{2}h\d{2})', item['data-search'])
|
||||||
|
show_time = time_match.group(0).replace('h', ':') if time_match else "Séance"
|
||||||
|
if title not in movies_dict:
|
||||||
|
img_url = ""
|
||||||
|
link_tag = item.find('a', href=True)
|
||||||
|
if link_tag:
|
||||||
|
r_det = session.get("https://www.cinemalecratere.fr" + link_tag['href'], timeout=10)
|
||||||
|
img_tag = BeautifulSoup(r_det.content, 'html.parser').find('img', class_='el-image')
|
||||||
|
if img_tag:
|
||||||
|
img_url = img_tag.get('src')
|
||||||
|
if not img_url.startswith('http'): img_url = "https://www.cinemalecratere.fr" + img_url
|
||||||
|
movies_dict[title] = {"title": title, "img": img_url, "times": [show_time], "cinema": "Le Cratère", "color": "#e62117"}
|
||||||
|
else:
|
||||||
|
if show_time not in movies_dict[title]["times"]: movies_dict[title]["times"].append(show_time)
|
||||||
|
except: logger.error("Fail Cratère")
|
||||||
|
return list(movies_dict.values())
|
||||||
|
|
||||||
|
# --- 3. SCRAPER COSMOGRAPH (REPRISE EXACTE DE TON CODE FONCTIONNEL) ---
|
||||||
|
def scrape_cosmo():
|
||||||
|
BASE_URL = "https://www.american-cosmograph.fr"
|
||||||
|
url_horaires = f"{BASE_URL}/les-horaires.html"
|
||||||
|
target_id = get_today_formats()["cosmo_id"]
|
||||||
|
movies_today = []
|
||||||
|
session = requests.Session()
|
||||||
|
try:
|
||||||
|
r = session.get(url_horaires, headers=HEADERS, timeout=15)
|
||||||
|
soup = BeautifulSoup(r.content, 'html.parser')
|
||||||
|
day_container = soup.find('div', id=target_id)
|
||||||
|
if not day_container: return []
|
||||||
|
items = day_container.find_all('li', class_='heureReady')
|
||||||
|
for item in items:
|
||||||
|
title_tag = item.select_one('.filmTitle a')
|
||||||
|
if not title_tag: continue
|
||||||
|
title = title_tag.get_text(strip=True)
|
||||||
|
detail_path = title_tag.get('href')
|
||||||
|
time_tag = item.select_one('.horaire_debut')
|
||||||
|
time_raw = time_tag.get_text(strip=True) if time_tag else ""
|
||||||
|
show_time_match = re.search(r'\d{2}h\d{2}', time_raw)
|
||||||
|
show_time = show_time_match.group(0).replace('h', ':') if show_time_match else "Séance"
|
||||||
|
img_url = ""
|
||||||
|
if detail_path:
|
||||||
|
detail_url = f"{BASE_URL}{detail_path}"
|
||||||
|
try:
|
||||||
|
r_detail = session.get(detail_url, headers=HEADERS, timeout=10)
|
||||||
|
soup_detail = BeautifulSoup(r_detail.content, 'html.parser')
|
||||||
|
img_tag = soup_detail.find('img', class_='fc_field_image')
|
||||||
|
if img_tag:
|
||||||
|
img_url = img_tag.get('src')
|
||||||
|
if img_url and not img_url.startswith('http'):
|
||||||
|
img_url = f"{BASE_URL}{img_url}"
|
||||||
|
except: pass
|
||||||
|
movies_today.append({"title": title, "img": img_url, "times": [show_time], "cinema": "American Cosmograph", "color": "#ffc107"})
|
||||||
|
time.sleep(0.1)
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
# Fusion des horaires pour le Cosmo
|
||||||
|
unique_movies = {}
|
||||||
|
for m in movies_today:
|
||||||
|
if m['title'] not in unique_movies:
|
||||||
|
unique_movies[m['title']] = m
|
||||||
|
else:
|
||||||
|
if m['times'][0] not in unique_movies[m['title']]['times']:
|
||||||
|
unique_movies[m['title']]['times'].append(m['times'][0])
|
||||||
|
unique_movies[m['title']]['times'].sort()
|
||||||
|
return list(unique_movies.values())
|
||||||
|
|
||||||
|
# --- LOGIQUE MISE À JOUR ---
|
||||||
|
def update_all_data():
|
||||||
|
logger.info("🕒 Mise à jour programmée...")
|
||||||
|
try:
|
||||||
|
data = scrape_abc() + scrape_cratere() + scrape_cosmo()
|
||||||
|
data = sorted(data, key=lambda x: x['times'][0] if x['times'] else "99:99")
|
||||||
|
payload = {"last_update": datetime.now().strftime("%d/%m/%Y à %H:%M"), "movies": data}
|
||||||
|
os.makedirs(os.path.dirname(CACHE_FILE), exist_ok=True)
|
||||||
|
with open(CACHE_FILE, 'w') as f: json.dump(payload, f)
|
||||||
|
return payload
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur MAJ: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
scheduler = BackgroundScheduler()
|
||||||
|
scheduler.add_job(func=update_all_data, trigger="cron", hour=0, minute=5)
|
||||||
|
scheduler.start()
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
if not os.path.exists(CACHE_FILE):
|
||||||
|
cache = update_all_data()
|
||||||
|
else:
|
||||||
|
with open(CACHE_FILE, 'r') as f: cache = json.load(f)
|
||||||
|
if isinstance(cache, list): cache = update_all_data()
|
||||||
|
return render_template('index.html', films=cache["movies"], updated=cache["last_update"])
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(host='0.0.0.0', port=5000)
|
||||||
+73
@@ -0,0 +1,73 @@
|
|||||||
|
import os, logging, requests, re
|
||||||
|
from flask import Flask, render_template
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(message)s')
|
||||||
|
logger = logging.getLogger("ABC_Scraper")
|
||||||
|
|
||||||
|
HEADERS = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36'}
|
||||||
|
|
||||||
|
def scrape_abc():
|
||||||
|
url = "https://abc-toulouse.fr/horaires.html"
|
||||||
|
logger.info(f"\n[DEBUG] Scraping ABC (Sélecteurs précis) sur : {url}")
|
||||||
|
|
||||||
|
movies_list = []
|
||||||
|
try:
|
||||||
|
r = requests.get(url, headers=HEADERS, timeout=15)
|
||||||
|
soup = BeautifulSoup(r.content, 'html.parser')
|
||||||
|
|
||||||
|
# 1. On cherche tous les blocs de films
|
||||||
|
# D'après ton code source, chaque film commence par cette div
|
||||||
|
movie_blocks = soup.find_all('div', class_='table_infos')
|
||||||
|
logger.info(f"[DEBUG] {len(movie_blocks)} blocs 'table_infos' trouvés.")
|
||||||
|
|
||||||
|
for block in movie_blocks:
|
||||||
|
# 2. Extraction du Titre (dans le h2)
|
||||||
|
title_tag = block.find('h2')
|
||||||
|
if not title_tag: continue
|
||||||
|
title = title_tag.get_text().strip()
|
||||||
|
|
||||||
|
# 3. Extraction de l'Affiche (dans col_poster)
|
||||||
|
img_tag = block.find('img')
|
||||||
|
img_url = ""
|
||||||
|
if img_tag:
|
||||||
|
img_url = img_tag.get('src')
|
||||||
|
if img_url and not img_url.startswith('http'):
|
||||||
|
img_url = "https://abc-toulouse.fr" + img_url
|
||||||
|
|
||||||
|
# 4. Extraction des Horaires
|
||||||
|
# Les séances (bulles bleues) sont souvent dans la div suivante
|
||||||
|
# ou un frère direct du bloc table_infos.
|
||||||
|
# On cherche les horaires (00:00) dans le texte qui suit immédiatement le bloc.
|
||||||
|
next_div = block.find_next_sibling()
|
||||||
|
showtimes_text = next_div.get_text(" ") if next_div else ""
|
||||||
|
|
||||||
|
# Si on ne trouve rien dans le frère, on cherche dans le bloc lui-même
|
||||||
|
# (au cas où la structure varierait)
|
||||||
|
if not re.search(r'\d{2}:\d{2}', showtimes_text):
|
||||||
|
showtimes_text = block.get_text(" ")
|
||||||
|
|
||||||
|
times = re.findall(r'(\d{2}:\d{2})', showtimes_text)
|
||||||
|
unique_times = sorted(list(set(times)))
|
||||||
|
|
||||||
|
if unique_times:
|
||||||
|
logger.info(f"✅ TROUVÉ : {title} | {len(unique_times)} séances.")
|
||||||
|
movies_list.append({
|
||||||
|
"title": title,
|
||||||
|
"img": img_url,
|
||||||
|
"times": unique_times
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Erreur : {e}")
|
||||||
|
|
||||||
|
return movies_list
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
films = scrape_abc()
|
||||||
|
return render_template('index.html', films=films)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(host='0.0.0.0', port=5000)
|
||||||
+106
@@ -0,0 +1,106 @@
|
|||||||
|
import os, json, logging, requests, re, time
|
||||||
|
from datetime import datetime
|
||||||
|
from flask import Flask, render_template, redirect, url_for
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(message)s')
|
||||||
|
logger = logging.getLogger("Cosmo_Scraper")
|
||||||
|
|
||||||
|
CACHE_FILE = "/data/movies_cache.json"
|
||||||
|
BASE_URL = "https://www.american-cosmograph.fr"
|
||||||
|
HEADERS = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36'}
|
||||||
|
|
||||||
|
def get_today_id():
|
||||||
|
"""Génère l'ID du bloc jour, ex: 'seance-2026-02-19'"""
|
||||||
|
return f"seance-{datetime.now().strftime('%Y-%m-%d')}"
|
||||||
|
|
||||||
|
def scrape_cosmo_only():
|
||||||
|
url_horaires = f"{BASE_URL}/les-horaires.html"
|
||||||
|
target_id = get_today_id()
|
||||||
|
|
||||||
|
logger.info(f"\n[DEBUG] --- DÉBUT SCRAPING AMERICAN COSMOGRAPH ---")
|
||||||
|
logger.info(f"[DEBUG] Recherche du bloc ID : {target_id}")
|
||||||
|
|
||||||
|
movies_today = []
|
||||||
|
session = requests.Session()
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = session.get(url_horaires, headers=HEADERS, timeout=15)
|
||||||
|
soup = BeautifulSoup(r.content, 'html.parser')
|
||||||
|
|
||||||
|
# 1. On cible le container du jour (ex: id="seance-2026-02-19")
|
||||||
|
day_container = soup.find('div', id=target_id)
|
||||||
|
|
||||||
|
if not day_container:
|
||||||
|
logger.warning(f"[DEBUG] Bloc {target_id} non trouvé sur la page.")
|
||||||
|
return []
|
||||||
|
|
||||||
|
# 2. On cherche tous les films du jour (balises li.heureReady)
|
||||||
|
items = day_container.find_all('li', class_='heureReady')
|
||||||
|
logger.info(f"[DEBUG] {len(items)} séances trouvées pour aujourd'hui.")
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
# Extraction du Titre et du Lien
|
||||||
|
title_tag = item.select_one('.filmTitle a')
|
||||||
|
if not title_tag: continue
|
||||||
|
|
||||||
|
title = title_tag.get_text(strip=True)
|
||||||
|
detail_path = title_tag.get('href')
|
||||||
|
|
||||||
|
# Extraction de l'Heure (on nettoie "Début du film : 13h40")
|
||||||
|
time_tag = item.select_one('.horaire_debut')
|
||||||
|
time_raw = time_tag.get_text(strip=True) if time_tag else ""
|
||||||
|
show_time = re.search(r'\d{2}h\d{2}', time_raw)
|
||||||
|
show_time = show_time.group(0).replace('h', ':') if show_time else "Séance"
|
||||||
|
|
||||||
|
# 3. Visite de la page de détails pour l'affiche
|
||||||
|
img_url = ""
|
||||||
|
if detail_path:
|
||||||
|
detail_url = f"{BASE_URL}{detail_path}"
|
||||||
|
logger.info(f"[DEBUG] -> Page détail : {title}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
r_detail = session.get(detail_url, headers=HEADERS, timeout=10)
|
||||||
|
soup_detail = BeautifulSoup(r_detail.content, 'html.parser')
|
||||||
|
|
||||||
|
# Sélecteur d'après ta capture : img class="fc_field_image"
|
||||||
|
img_tag = soup_detail.find('img', class_='fc_field_image')
|
||||||
|
if img_tag:
|
||||||
|
img_url = img_tag.get('src')
|
||||||
|
if img_url and not img_url.startswith('http'):
|
||||||
|
img_url = f"{BASE_URL}{img_url}"
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[DEBUG] Erreur page détail {title}: {e}")
|
||||||
|
|
||||||
|
movies_today.append({
|
||||||
|
"title": title,
|
||||||
|
"img": img_url,
|
||||||
|
"times": [show_time],
|
||||||
|
"cinema": "American Cosmograph"
|
||||||
|
})
|
||||||
|
time.sleep(0.3) # Politesse
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[DEBUG] Erreur critique : {e}")
|
||||||
|
|
||||||
|
# Fusion des horaires pour un même film (le Cosmo sépare les séances par bloc)
|
||||||
|
unique_movies = {}
|
||||||
|
for m in movies_today:
|
||||||
|
if m['title'] not in unique_movies:
|
||||||
|
unique_movies[m['title']] = m
|
||||||
|
else:
|
||||||
|
if m['times'][0] not in unique_movies[m['title']]['times']:
|
||||||
|
unique_movies[m['title']]['times'].append(m['times'][0])
|
||||||
|
unique_movies[m['title']]['times'].sort()
|
||||||
|
|
||||||
|
logger.info(f"[DEBUG] FIN : {len(unique_movies)} films uniques trouvés.")
|
||||||
|
return list(unique_movies.values())
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
films = scrape_cosmo_only()
|
||||||
|
return render_template('index.html', films=films)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(host='0.0.0.0', port=5000)
|
||||||
+106
@@ -0,0 +1,106 @@
|
|||||||
|
import os, json, logging, requests, re, time
|
||||||
|
from datetime import datetime
|
||||||
|
from flask import Flask, render_template, redirect, url_for
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
# Logs en mode "Info" pour voir le suivi dans le terminal
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(message)s')
|
||||||
|
logger = logging.getLogger("Cratere_Scraper")
|
||||||
|
|
||||||
|
CACHE_FILE = "/data/movies_cache.json"
|
||||||
|
HEADERS = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36'}
|
||||||
|
|
||||||
|
def get_cratere_date():
|
||||||
|
"""Génère le format 'Jeu. 19/02' correspondant au site"""
|
||||||
|
jours_courts = ["Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dim"]
|
||||||
|
now = datetime.now()
|
||||||
|
day_short = jours_courts[now.weekday()]
|
||||||
|
return f"{day_short}. {now.day:02d}/{now.month:02d}"
|
||||||
|
|
||||||
|
def scrape_cratere_only():
|
||||||
|
url_main = "https://www.cinemalecratere.fr/films"
|
||||||
|
target_date = get_cratere_date()
|
||||||
|
|
||||||
|
logger.info(f"\n[DEBUG] --- DÉBUT SCRAPING LE CRATÈRE ---")
|
||||||
|
logger.info(f"[DEBUG] Date cible : {target_date}")
|
||||||
|
|
||||||
|
movies_today = []
|
||||||
|
session = requests.Session()
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = session.get(url_main, headers=HEADERS, timeout=15)
|
||||||
|
soup = BeautifulSoup(r.content, 'html.parser')
|
||||||
|
|
||||||
|
# On cherche les div avec l'attribut data-search (vu sur ta capture)
|
||||||
|
items = soup.find_all('div', attrs={'data-search': True})
|
||||||
|
logger.info(f"[DEBUG] {len(items)} blocs trouvés sur la page principale.")
|
||||||
|
|
||||||
|
# Dictionnaire pour éviter de scraper deux fois le même film s'il y a 2 séances
|
||||||
|
seen_movies = {}
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
data_search = item['data-search']
|
||||||
|
|
||||||
|
if target_date in data_search:
|
||||||
|
# 1. Extraction Titre
|
||||||
|
title_tag = item.find('span', class_='uk-text-bold')
|
||||||
|
title = title_tag.get_text(strip=True) if title_tag else "Titre inconnu"
|
||||||
|
|
||||||
|
# 2. Extraction Heure (ex: 16h00)
|
||||||
|
time_match = re.search(r'(\d{2}h\d{2})', data_search)
|
||||||
|
show_time = time_match.group(0).replace('h', ':') if time_match else "Séance"
|
||||||
|
|
||||||
|
if title not in seen_movies:
|
||||||
|
# 3. On suit le lien pour l'affiche
|
||||||
|
img_url = ""
|
||||||
|
link_tag = item.find('a', href=True)
|
||||||
|
|
||||||
|
if link_tag:
|
||||||
|
detail_url = "https://www.cinemalecratere.fr" + link_tag['href']
|
||||||
|
logger.info(f"[DEBUG] -> Visite page détail : {title}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
r_detail = session.get(detail_url, headers=HEADERS, timeout=10)
|
||||||
|
soup_detail = BeautifulSoup(r_detail.content, 'html.parser')
|
||||||
|
# Sélecteur précis : img avec la classe el-image (vu sur ta capture)
|
||||||
|
img_tag = soup_detail.find('img', class_='el-image')
|
||||||
|
if img_tag:
|
||||||
|
img_url = img_tag.get('src')
|
||||||
|
# Gérer les chemins relatifs
|
||||||
|
if img_url and not img_url.startswith('http'):
|
||||||
|
img_url = "https://www.cinemalecratere.fr" + img_url
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[DEBUG] Erreur page détail {title}: {e}")
|
||||||
|
|
||||||
|
seen_movies[title] = {
|
||||||
|
"title": title,
|
||||||
|
"img": img_url,
|
||||||
|
"times": [show_time],
|
||||||
|
"cinema": "Le Cratère"
|
||||||
|
}
|
||||||
|
# Petit délai pour ne pas brusquer le serveur
|
||||||
|
time.sleep(0.3)
|
||||||
|
else:
|
||||||
|
# Si on a déjà vu le film (autre séance), on ajoute juste l'heure
|
||||||
|
if show_time not in seen_movies[title]["times"]:
|
||||||
|
seen_movies[title]["times"].append(show_time)
|
||||||
|
|
||||||
|
movies_today = list(seen_movies.values())
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[DEBUG] Erreur critique : {e}")
|
||||||
|
|
||||||
|
logger.info(f"[DEBUG] FIN : {len(movies_today)} films trouvés pour aujourd'hui.")
|
||||||
|
return movies_today
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
# Force le rafraîchissement au chargement pour tester
|
||||||
|
films = scrape_cratere_only()
|
||||||
|
# Tri par heure de la première séance
|
||||||
|
films = sorted(films, key=lambda x: x['times'][0])
|
||||||
|
return render_template('index.html', films=films)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(host='0.0.0.0', port=5000)
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import requests
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
HEADERS = {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||||
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||||
|
'Accept-Language': 'fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7',
|
||||||
|
}
|
||||||
|
|
||||||
|
def check_site(name, url):
|
||||||
|
print(f"\n--- TEST {name.upper()} ---")
|
||||||
|
try:
|
||||||
|
r = requests.get(url, headers=HEADERS, timeout=10)
|
||||||
|
print(f"Status Code: {r.status_code}")
|
||||||
|
|
||||||
|
if r.status_code != 200:
|
||||||
|
print("ERREUR: Le site bloque ou ne répond pas.")
|
||||||
|
return
|
||||||
|
|
||||||
|
soup = BeautifulSoup(r.content, 'html.parser')
|
||||||
|
print(f"Taille du contenu téléchargé: {len(r.text)} caractères")
|
||||||
|
|
||||||
|
# Vérif si on est bloqué
|
||||||
|
if "captcha" in r.text.lower() or "forbidden" in r.text.lower():
|
||||||
|
print("ALERTE: Détection de blocage (Captcha/Forbidden) !")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Afficher les titres potentiels (H2, H3) pour voir la structure
|
||||||
|
print("Recherche de balises titres (H2, H3, H4) :")
|
||||||
|
titles = soup.find_all(['h2', 'h3', 'h4'])
|
||||||
|
|
||||||
|
found_count = 0
|
||||||
|
for i, t in enumerate(titles[:10]): # On affiche les 10 premiers
|
||||||
|
txt = t.get_text(" ", strip=True)
|
||||||
|
if len(txt) > 3:
|
||||||
|
print(f" [{t.name}] {txt}")
|
||||||
|
found_count += 1
|
||||||
|
|
||||||
|
if found_count == 0:
|
||||||
|
print(" AUCUN TITRE TROUVÉ. Le sélecteur CSS doit être revu.")
|
||||||
|
# Affiche un bout du HTML pour comprendre
|
||||||
|
print("\nExtrait du HTML (500 premiers caractères du body):")
|
||||||
|
body = soup.find('body')
|
||||||
|
print(body.prettify()[:500] if body else "Pas de body")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERREUR CRITIQUE: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
check_site("Cosmograph", "https://www.american-cosmograph.fr/les-horaires.html")
|
||||||
|
check_site("Le Cratère", "https://www.cinemalecratere.fr/films")
|
||||||
|
check_site("ABC", "https://abc-toulouse.fr/horaires.html")
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
services:
|
||||||
|
cine-app:
|
||||||
|
build: .
|
||||||
|
container_name: cine_toulouse
|
||||||
|
ports:
|
||||||
|
- "5001:5000"
|
||||||
|
volumes:
|
||||||
|
# On lie le dossier ./data du NUC au dossier /data du conteneur
|
||||||
|
- ./data:/data
|
||||||
|
- ./:/app
|
||||||
|
restart: unless-stopped
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
services:
|
||||||
|
cine-app:
|
||||||
|
build: .
|
||||||
|
container_name: cine_toulouse
|
||||||
|
ports:
|
||||||
|
- "5001:5000"
|
||||||
|
volumes:
|
||||||
|
# On lie le dossier ./data du NUC au dossier /data du conteneur
|
||||||
|
- ./data:/data
|
||||||
|
restart: unless-stopped
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
flask
|
||||||
|
requests
|
||||||
|
beautifulsoup4
|
||||||
|
thefuzz
|
||||||
|
gunicorn
|
||||||
|
apscheduler
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
flask
|
||||||
|
requests
|
||||||
|
beautifulsoup4
|
||||||
|
thefuzz
|
||||||
|
gunicorn
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="referrer" content="no-referrer">
|
||||||
|
<title>Ciné Toulouse</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||||
|
<style>
|
||||||
|
body { background-color: #0a0a0a; color: #fff; font-family: 'Segoe UI', system-ui, sans-serif; }
|
||||||
|
|
||||||
|
.main-container { max-width: 1200px; margin: 0 auto; padding: 20px; }
|
||||||
|
|
||||||
|
.movie-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 15px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.movie-grid { grid-template-columns: repeat(3, 1fr); gap: 20px; }
|
||||||
|
}
|
||||||
|
@media (min-width: 1100px) {
|
||||||
|
.movie-grid { grid-template-columns: repeat(4, 1fr); gap: 30px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.movie-card {
|
||||||
|
background: #161616;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #252525;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
.movie-card:hover { transform: translateY(-5px); border-color: #ffc107; }
|
||||||
|
|
||||||
|
/* Ratio portrait 2:3 */
|
||||||
|
.poster-box {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
padding-top: 150%;
|
||||||
|
background: #222;
|
||||||
|
}
|
||||||
|
.poster-img {
|
||||||
|
position: absolute;
|
||||||
|
top: 0; left: 0; width: 100%; height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cinema-label {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px; left: 10px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 800;
|
||||||
|
text-transform: uppercase;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 5px rgba(0,0,0,0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.movie-info { padding: 15px; text-align: center; flex-grow: 1; display: flex; flex-direction: column; }
|
||||||
|
.movie-title {
|
||||||
|
font-size: 0.95rem; font-weight: 700; color: #fff;
|
||||||
|
margin-bottom: 12px; min-height: 2.4rem;
|
||||||
|
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-wrap { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; margin-top: auto; }
|
||||||
|
.time-pill {
|
||||||
|
background: #ffc107; color: #000;
|
||||||
|
font-weight: 800; font-size: 0.85rem;
|
||||||
|
padding: 3px 10px; border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-note { text-align: center; color: #444; font-size: 0.7rem; margin-top: 50px; padding-bottom: 30px; }
|
||||||
|
h1 { font-weight: 900; letter-spacing: -1px; text-align: center; color: #ffc107; margin-bottom: 30px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="main-container">
|
||||||
|
<h1><i class="fas fa-film me-3"></i>CINÉ TOULOUSE</h1>
|
||||||
|
|
||||||
|
<div class="movie-grid">
|
||||||
|
{% for film in films %}
|
||||||
|
<div class="movie-card">
|
||||||
|
<div class="poster-box">
|
||||||
|
{% if film.img %}
|
||||||
|
<img src="{{ film.img }}" class="poster-img" loading="lazy">
|
||||||
|
{% else %}
|
||||||
|
<div class="poster-img d-flex align-items-center justify-content-center bg-dark text-muted small">IMAGE NON DISPONIBLE</div>
|
||||||
|
{% endif %}
|
||||||
|
<span class="cinema-label" style="background-color: {{ film.color }}; color: {{ '#000' if film.cinema != 'Le Cratère' else '#fff' }};">
|
||||||
|
{{ film.cinema }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="movie-info">
|
||||||
|
<div class="movie-title">{{ film.title }}</div>
|
||||||
|
<div class="time-wrap">
|
||||||
|
{% for t in film.times %}
|
||||||
|
<span class="time-pill">{{ t }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer-note">
|
||||||
|
Auto-sync à 00:05 | Mise à jour : {{ updated }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="referrer" content="no-referrer">
|
||||||
|
<title>Ciné Toulouse</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||||
|
<style>
|
||||||
|
body { background-color: #0a0a0a; color: #ffffff; font-family: 'Segoe UI', system-ui, sans-serif; }
|
||||||
|
|
||||||
|
/* Conteneur principal plus étroit sur Desktop pour centrer */
|
||||||
|
.main-container { max-width: 1400px; margin: 0 auto; padding: 20px; }
|
||||||
|
|
||||||
|
/* Grille : 2 par ligne sur mobile, 5 sur Desktop */
|
||||||
|
.movie-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.movie-grid { grid-template-columns: repeat(4, 1fr); gap: 25px; }
|
||||||
|
}
|
||||||
|
@media (min-width: 1400px) {
|
||||||
|
.movie-grid { grid-template-columns: repeat(5, 1fr); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.movie-card {
|
||||||
|
background: #161616;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid #252525;
|
||||||
|
transition: transform 0.2s, border-color 0.2s;
|
||||||
|
}
|
||||||
|
.movie-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
border-color: #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Format Portrait Rectangulaire (Ratio 2/3) */
|
||||||
|
.poster-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
padding-top: 150%; /* Ratio 2:3 */
|
||||||
|
background: #222;
|
||||||
|
}
|
||||||
|
.poster-img {
|
||||||
|
position: absolute;
|
||||||
|
top: 0; left: 0; width: 100%; height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cinema-label {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px; left: 10px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 800;
|
||||||
|
text-transform: uppercase;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.movie-info {
|
||||||
|
padding: 12px;
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.movie-title {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
min-height: 2.2rem;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #fff;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-group {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5px;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-pill {
|
||||||
|
background: #ffc107;
|
||||||
|
color: #000;
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-status {
|
||||||
|
text-align: center;
|
||||||
|
color: #444;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin-top: 50px;
|
||||||
|
padding-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 { font-weight: 900; letter-spacing: -1px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="main-container">
|
||||||
|
<h1 class="text-center my-4 text-warning"><i class="fas fa-film me-2"></i>CINÉ TOULOUSE</h1>
|
||||||
|
|
||||||
|
<div class="movie-grid">
|
||||||
|
{% for film in films %}
|
||||||
|
<div class="movie-item">
|
||||||
|
<div class="movie-card">
|
||||||
|
<div class="poster-wrapper">
|
||||||
|
{% if film.img %}
|
||||||
|
<img src="{{ film.img }}" class="poster-img" loading="lazy">
|
||||||
|
{% else %}
|
||||||
|
<div class="poster-img d-flex align-items-center justify-content-center bg-dark text-muted small">
|
||||||
|
<i class="fas fa-image fa-2x"></i>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<span class="cinema-label" style="background-color: {{ film.color }}; color: {{ '#000' if film.cinema != 'Le Cratère' else '#fff' }};">
|
||||||
|
{{ film.cinema }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="movie-info">
|
||||||
|
<div class="movie-title">{{ film.title }}</div>
|
||||||
|
<div class="time-group">
|
||||||
|
{% for t in film.times %}
|
||||||
|
<span class="time-pill">{{ t }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer-status">
|
||||||
|
Synchronisation quotidienne à 00:05<br>
|
||||||
|
Dernier relevé : {{ updated }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user