commit 7fb82310e70928fa34e39a26d2ccb0fce8573c25 Author: theo Date: Thu Feb 19 14:06:46 2026 +0100 Version stable : Synthèse 3 cinémas avec affiches et horaires diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10701c9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# Cache des données (ne pas versionner) +data/ +*.json + +# Python +__pycache__/ +*.py[cod] +*$py.class +venv/ +.env + +# Docker +.DS_Store \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ea3c85b --- /dev/null +++ b/Dockerfile @@ -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"] \ No newline at end of file diff --git a/Dockerfile~ b/Dockerfile~ new file mode 100644 index 0000000..624a64e --- /dev/null +++ b/Dockerfile~ @@ -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"] \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..fff61f5 --- /dev/null +++ b/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) diff --git a/app.py~ b/app.py~ new file mode 100644 index 0000000..f4d62ab --- /dev/null +++ b/app.py~ @@ -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) diff --git a/app_abc.py b/app_abc.py new file mode 100644 index 0000000..4a346cb --- /dev/null +++ b/app_abc.py @@ -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) diff --git a/app_cosmo.py b/app_cosmo.py new file mode 100644 index 0000000..14c0ede --- /dev/null +++ b/app_cosmo.py @@ -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) diff --git a/app_cratere.py b/app_cratere.py new file mode 100644 index 0000000..40e2f83 --- /dev/null +++ b/app_cratere.py @@ -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) diff --git a/debug.py b/debug.py new file mode 100644 index 0000000..9ccfb84 --- /dev/null +++ b/debug.py @@ -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") diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..930200e --- /dev/null +++ b/docker-compose.yml @@ -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 \ No newline at end of file diff --git a/docker-compose.yml~ b/docker-compose.yml~ new file mode 100644 index 0000000..0fa5e70 --- /dev/null +++ b/docker-compose.yml~ @@ -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 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3b2b2c6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +flask +requests +beautifulsoup4 +thefuzz +gunicorn +apscheduler diff --git a/requirements.txt~ b/requirements.txt~ new file mode 100644 index 0000000..62c1945 --- /dev/null +++ b/requirements.txt~ @@ -0,0 +1,5 @@ +flask +requests +beautifulsoup4 +thefuzz +gunicorn diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..ed7a443 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,117 @@ + + + + + + + Ciné Toulouse + + + + + +
+

CINÉ TOULOUSE

+ +
+ {% for film in films %} +
+
+ {% if film.img %} + + {% else %} +
IMAGE NON DISPONIBLE
+ {% endif %} + + {{ film.cinema }} + +
+ +
+
{{ film.title }}
+
+ {% for t in film.times %} + {{ t }} + {% endfor %} +
+
+
+ {% endfor %} +
+ + +
+ + diff --git a/templates/index.html~ b/templates/index.html~ new file mode 100644 index 0000000..8a92db4 --- /dev/null +++ b/templates/index.html~ @@ -0,0 +1,158 @@ + + + + + + + Ciné Toulouse + + + + + +
+

CINÉ TOULOUSE

+ +
+ {% for film in films %} +
+
+
+ {% if film.img %} + + {% else %} +
+ +
+ {% endif %} + + + {{ film.cinema }} + +
+ +
+
{{ film.title }}
+
+ {% for t in film.times %} + {{ t }} + {% endfor %} +
+
+
+
+ {% endfor %} +
+ + +
+ +