From 4af947b440ca3d9aedce953f4a552140a5c1635a Mon Sep 17 00:00:00 2001 From: Hugo Pierret Date: Sun, 26 Apr 2026 17:25:21 +0200 Subject: [PATCH] Cleanup: remove unused code, add CI/CD workflow - Remove flask-sse, Redis (unused) - Remove ImageService, file_utils (unused) - Remove /download-posters route (frontend handles download client-side) - Remove LETTERBOXD_API_URL, secure_filename (dead imports) - Fix duplicate TMDBService instantiation - Simplify config.py (remove REDIS_URL) - Fix gunicorn user home dir for control socket - Add .gitignore for __pycache__ - Add .gitea/workflows/deploy.yml for tag-based CI/CD --- .gitea/workflows/deploy.yml | 33 +++ .gitignore | 3 + Dockerfile | 4 +- __pycache__/config.cpython-312.pyc | Bin 1178 -> 926 bytes config.py | 7 - main.py | 9 +- src/api/__pycache__/routes.cpython-312.pyc | Bin 12785 -> 12800 bytes src/api/routes.py | 220 ++++++++---------- .../__pycache__/tmdb_service.cpython-312.pyc | Bin 1960 -> 1977 bytes src/services/file_utils.py | 10 - src/services/image_service.py | 14 -- src/services/tmdb_service.py | 2 +- 12 files changed, 145 insertions(+), 157 deletions(-) create mode 100644 .gitea/workflows/deploy.yml delete mode 100644 src/services/file_utils.py delete mode 100644 src/services/image_service.py diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..97f8605 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,33 @@ +name: Deploy Backend + +on: + push: + tags: + - 'v*.*.*' + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Extract version tag + run: echo "VERSION_TAG=${GITHUB_REF_NAME}" >> $GITHUB_ENV + + - name: Build Docker image + run: | + docker build \ + -t poster-picker-backend:latest \ + -t poster-picker-backend:${{ env.VERSION_TAG }} . + + - name: Stop old container + run: | + cd ~/dev-server/poster-picker + docker compose stop backend || true + + - name: Start new container + run: | + cd ~/dev-server/poster-picker + docker compose up -d --no-build backend diff --git a/.gitignore b/.gitignore index 85c55eb..315f677 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ .env .venv +__pycache__/ +*.pyc +*.pyo diff --git a/Dockerfile b/Dockerfile index a54e14e..ca3791c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # backend/Dockerfile FROM python:3.11-slim -RUN addgroup --system app && adduser --system --ingroup app app +RUN addgroup --system app && adduser --system --ingroup app --home /home/app app WORKDIR /srv/app @@ -27,4 +27,4 @@ ENV GUNICORN_TIMEOUT=300 # Commande de démarrage via gunicorn (attache l'app Flask : app:app) # Utilise timeout raisonnable, bind sur 0.0.0.0:5000 -CMD ["sh","-c","gunicorn --workers ${GUNICORN_WORKERS} --threads ${GUNICORN_THREADS} --worker-class ${GUNICORN_WORKER_CLASS} --timeout ${GUNICORN_TIMEOUT} --bind 0.0.0.0:5000 --access-logfile - --error-logfile - 'main:create_app()'"] \ No newline at end of file +CMD ["sh","-c","gunicorn --workers ${GUNICORN_WORKERS} --threads ${GUNICORN_THREADS} --worker-class ${GUNICORN_WORKER_CLASS} --timeout ${GUNICORN_TIMEOUT} --worker-tmp-dir /dev/shm --bind 0.0.0.0:5000 --access-logfile - --error-logfile - 'main:create_app()'"] \ No newline at end of file diff --git a/__pycache__/config.cpython-312.pyc b/__pycache__/config.cpython-312.pyc index e86d50970c148636d0351d7011ad902ad970f9c1..b1b02d7e7ce09e15e01c9d371891a924681e38a3 100644 GIT binary patch delta 461 zcmbQmIgg$1G%qg~0}#C8e3u!{IFWA>qtC>JzATk2nyizx80DD#G&v{dF?vtl$C$>h z094n)aAUGIlQc`Cdy&B8cqX}e0ifV?hE#?q#uSDqrWD2~=1L|_=39a;sb#4-`31SD zc_q&Id1;yHA$}`air9gwi#R|8Cy3w$5&R$mWK5ADh!BPmAZ5iM4M0121R7l-YCzJ- zKnw--3=E7==4TvE5eO*CPbp0<$;{7#xWvt`NCc<{-C{A27>M8oVz|YEFpEJlK#y^! zAUt-9t6&m~Xgw$Zi&#Je*zF*Nx7d=wo<_6d7l%zQ&@DNsc16-aRUnra+W?6V%#4hT zw;4FUvoJG?d}II;U$__<1!u5*VE|HJ*ccgwCX|8LLLWF7L{x7GDc#^#_{h$}#ni}F I!~*g+0C#*|VgLXD delta 627 zcmbQoK8us@G%qg~0}x2P6HY(OG?8x-W6Q*az7yAJhq9!wu4V#BF)&1NRKx=667TIAd5b+IwJbh6wbJbtXOOFlXK;LIkk2o%qSTbkVk>?9oc!d( zoQ(Y95-T%fb4x!>uF1lT-mFFJKr<)DF{ZJb0vRm~4xLg)+ZS*V>n*54Uu3iMFaymmQLlk2QLljdAV-#}=Qxr=jvnItj}%}7h2Y@W`uB~*nm!B2Rlt5peR43G`S=*KM&$Eu;p%k znjGjZ16f(b3nKVH1lWyW*P=O893%@OK$69hKmr(k;Bb?G2Ha%WJc5m`5dVo! z{>Y?N4>q3#q#5j0kO{ZglELwb78buaY;u8)&PlZ^k^!m$d8*hBNPJ*sWMsU}!1?} diff --git a/config.py b/config.py index c1c59ee..2897426 100644 --- a/config.py +++ b/config.py @@ -1,21 +1,14 @@ import os class Config: - # Configuration générale SECRET_KEY = os.environ.get('SECRET_KEY', 'dev_key') DEBUG = False TESTING = False - REDIS_URL = os.environ.get("REDIS_URL", "redis://localhost:6379") class DevelopmentConfig(Config): DEBUG = True - # Configuration pour le développement local - REDIS_URL = "redis://localhost:6379" class ProductionConfig(Config): - # Configuration pour la production (Render, etc.) - REDIS_URL = os.environ.get("REDIS_URL") - # Peut désactiver le debug mode pour la prod DEBUG = False config = { diff --git a/main.py b/main.py index c9a8760..dc69ac8 100644 --- a/main.py +++ b/main.py @@ -2,7 +2,6 @@ import os from flask import Flask from flask_cors import CORS from src.api.routes import api_bp -from flask_sse import sse from config import config @@ -13,18 +12,18 @@ def create_app(): "http://localhost:3000", "http://127.0.0.1:3000" ] + frontend_url = os.environ.get("FRONTEND_URL") + if frontend_url: + origins.append(frontend_url) CORS(app, supports_credentials=True, origins=origins) - + env = os.environ.get('FLASK_ENV', 'default') if env == "production": app.config.update(SESSION_COOKIE_SAMESITE="None", SESSION_COOKIE_SECURE=True) - else: - app.config.update(SESSION_COOKIE_SAMESITE="Lax", SESSION_COOKIE_SECURE=False) app.config.from_object(config[env]) - app.register_blueprint(sse, url_prefix='/stream') app.register_blueprint(api_bp, url_prefix='/api') return app diff --git a/src/api/__pycache__/routes.cpython-312.pyc b/src/api/__pycache__/routes.cpython-312.pyc index 600a2b7145f37f4b3eafd3bb100c9d7523ca2e46..95b89b5ac6f65f4dc39c7ca38dd9b7a28a9827d2 100644 GIT binary patch literal 12800 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 Sdyo>kYa~16?&Per-~R*N)ju`> literal 12785 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/', methods=['GET']) def get_movie_posters(movie_name, movie_year): - tmdb_service = TMDBService() - movie = tmdb_service.search_movie(movie_name, movie_year) if movie: @@ -105,11 +123,7 @@ def allowed_file(filename): @api_bp.route('/upload-csv', methods=['POST']) def upload_csv(): - user_id = session.get('user_id') - - if not user_id: - user_id = str(uuid.uuid4()) - session['user_id'] = user_id + user_id = get_user_id() if 'file' not in request.files: return jsonify({'error': 'No file part'}), 400 @@ -122,11 +136,16 @@ def upload_csv(): try: df = pd.read_csv(io.StringIO(content.decode('utf-8'))) - df = df.rename(columns={ - 'Date': 'Watched Date', - 'Name': 'Name', - 'Year': 'Year' - }) + + if 'Watched Date' in df.columns: + pass + elif 'Date' in df.columns: + df = df.rename(columns={'Date': 'Watched Date'}) + else: + return jsonify({'error': 'CSV must contain a "Watched Date" or "Date" column'}), 400 + + if 'Name' not in df.columns or 'Year' not in df.columns: + return jsonify({'error': 'CSV must contain "Name" and "Year" columns'}), 400 user_csvs[user_id] = df.to_csv(index=False) @@ -138,7 +157,7 @@ def upload_csv(): @api_bp.route('/check-csv', methods=['GET']) def check_csv(): - user_id = session.get('user_id') + user_id = get_user_id() try: if user_id in user_csvs: @@ -152,7 +171,7 @@ def check_csv(): @api_bp.route('/delete-csv', methods=['DELETE']) def delete_csv(): - user_id = session.get('user_id') + user_id = get_user_id() try: if user_id in user_csvs: @@ -165,106 +184,71 @@ def delete_csv(): return jsonify({'error': 'An error occurred while deleting the CSV file'}), 500 -@api_bp.route('/fetch-diary', methods=['POST']) -def fetch_diary_from_username(): - """ - Récupère le diary via l'API publique (api.hugo-pierret.be), transforme en CSV et - le stocke dans user_csvs[user_id] pour que le front continue à utiliser le même flux que l'upload. - """ +def _parse_letterboxd_rss(xml_bytes): + import xml.etree.ElementTree as ET + root = ET.fromstring(xml_bytes) + entries = [] + for item in root.findall('.//item'): + watched_date = film_title = film_year = None + for child in item: + tag = child.tag.split('}')[-1] if '}' in child.tag else child.tag + if tag == 'watchedDate': + watched_date = child.text + elif tag == 'filmTitle': + film_title = child.text + elif tag == 'filmYear': + film_year = child.text + if watched_date and film_title: + entries.append({ + 'Watched Date': watched_date, + 'Name': film_title, + 'Year': film_year or '', + }) + return entries + + +@api_bp.route('/sync-rss', methods=['POST']) +def sync_rss(): try: data = request.get_json() or {} - username = data.get('username') + username = data.get('username', '').strip() if not username: return jsonify({'error': 'username required'}), 400 - user_id = session.get('user_id') - if not user_id: - user_id = str(uuid.uuid4()) - session['user_id'] = user_id + user_id = get_user_id() - api_url = f"{LETTERBOXD_API_URL}/letterboxd/diary?username={username}" - resp = requests.get(api_url) + rss_url = f"https://letterboxd.com/{username}/rss/" + resp = requests.get(rss_url, headers={'User-Agent': 'Mozilla/5.0'}, timeout=10) if resp.status_code != 200: - return jsonify({'error': 'Failed to fetch diary from upstream API'}), 502 + return jsonify({'error': f'Could not fetch RSS feed (HTTP {resp.status_code})'}), 502 - payload = resp.json() - entries = payload.get('entries', []) - if not entries: - return jsonify({'error': 'No entries returned by upstream API'}), 404 + rss_entries = _parse_letterboxd_rss(resp.content) + if not rss_entries: + return jsonify({'error': 'No diary entries found in RSS feed'}), 404 - rows = [] - import re - for e in entries: - title = e.get('title') or "" - date = e.get('date') or "" + if user_id not in user_csvs: + df = pd.DataFrame(rss_entries) + df = df.sort_values('Watched Date').reset_index(drop=True) + user_csvs[user_id] = df.to_csv(index=False) + return jsonify({'success': True, 'fresh': True, 'added': len(rss_entries), 'total': len(rss_entries)}), 200 - m = re.match(r"^(?P.+?)\s*\((?P\d{4})\)\s*$", title) - if m: - name = m.group('name').strip() - year = m.group('year') - else: - year_match = re.search(r"(\d{4})", title) - year = year_match.group(1) if year_match else "" - name = re.sub(r"\(\d{4}\)", "", title).strip() - - rows.append({ - "Watched Date": date, - "Name": name, - "Year": year - }) - - import pandas as pd, io - rows.reverse() - df = pd.DataFrame(rows) - csv_str = df.to_csv(index=False) - - user_csvs[user_id] = csv_str - - return jsonify({'success': True, 'message': 'Diary fetched and stored', 'total': len(rows)}), 200 - - except Exception as e: - print("Error in fetch_diary_from_username:", str(e)) - return jsonify({'error': 'An internal error occurred'}), 500 - - -@api_bp.route('/download-posters', methods=['POST']) -def download_posters(): - try: - data = request.get_json() - selected_posters = data.get('posters', []) - - if not selected_posters: - return jsonify({'error': 'No posters selected'}), 400 - - zip_buffer = io.BytesIO() - with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file: - # Télécharger chaque poster - for poster in selected_posters: - movie_id = poster['movieId'] - poster_id = poster['posterId'] - watched_date = poster.get('watchedDate', 'unknown-date') - - poster_url = f"https://image.tmdb.org/t/p/original{poster_id}" - - try: - response = requests.get(poster_url) - if response.status_code == 200: - file_name = f"{watched_date}_{movie_id}.jpg" - zip_file.writestr(file_name, response.content) - else: - print(f"Failed to download poster for movie ID {movie_id}") - except Exception as e: - print(f"Error downloading poster for movie ID {movie_id}: {str(e)}") - - # Retourner le fichier ZIP - zip_buffer.seek(0) - return send_file( - zip_buffer, - mimetype='application/zip', - as_attachment=True, - download_name='posters.zip' + existing_content = user_csvs[user_id] + df_existing = pd.read_csv( + io.StringIO(existing_content.decode('utf-8') if isinstance(existing_content, bytes) else existing_content) ) + existing_keys = set(zip(df_existing['Name'].astype(str), df_existing['Watched Date'].astype(str))) + new_rows = [e for e in rss_entries if (e['Name'], e['Watched Date']) not in existing_keys] + + if not new_rows: + return jsonify({'success': True, 'fresh': False, 'added': 0, 'total': len(df_existing)}), 200 + + df_merged = pd.concat([df_existing, pd.DataFrame(new_rows)], ignore_index=True) + df_merged = df_merged.sort_values('Watched Date').reset_index(drop=True) + user_csvs[user_id] = df_merged.to_csv(index=False) + + return jsonify({'success': True, 'fresh': False, 'added': len(new_rows), 'total': len(df_merged)}), 200 + except Exception as e: - print(f"Error in download_posters: {str(e)}") - return jsonify({'error': 'An error occurred while fetching movie posters'}), 500 + print("Error in sync_rss:", str(e)) + return jsonify({'error': 'An internal error occurred'}), 500 diff --git a/src/services/__pycache__/tmdb_service.cpython-312.pyc b/src/services/__pycache__/tmdb_service.cpython-312.pyc index f23a0b50326ea4704d290944057583946e66ff86..fec64db9c99975afb5dd5f36c2f88d58e318726d 100644 GIT binary patch delta 60 zcmZ3%zmuQ$G%qg~0}zzSzRe8T$eYQ^Xf?T delta 42 xcmdnVzk;9lG%qg~0}y!aP0VoK$eYQ^s5!ZuRdw=O)+a2H7x|]', '_', filename) - - -def ensure_directory(directory): - os.makedirs(directory, exist_ok=True) diff --git a/src/services/image_service.py b/src/services/image_service.py deleted file mode 100644 index 8f6e516..0000000 --- a/src/services/image_service.py +++ /dev/null @@ -1,14 +0,0 @@ -import requests -from PIL import Image -import io - - -class ImageService: - def download_image(self, image_url, save_path=None): - response = requests.get(image_url, stream=True) - if response.status_code == 200: - image = Image.open(io.BytesIO(response.content)) - if save_path: - image.save(save_path) - return image - return None diff --git a/src/services/tmdb_service.py b/src/services/tmdb_service.py index f3b2d48..6ab3c90 100644 --- a/src/services/tmdb_service.py +++ b/src/services/tmdb_service.py @@ -24,7 +24,7 @@ class TMDBService: return None def get_movie_posters(self, movie_id): - url = f"{self.base_url}/movie/{movie_id}/images?include_image_language=en,fr,null" + url = f"{self.base_url}/movie/{movie_id}/images?include_image_language=en,fr,ja,null&language=null" headers = { "accept": "application/json", "Authorization": f"Bearer {self.access_token}"