Compare commits
12 Commits
98d4f308ba
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b087660721 | |||
| 6e1ad80f33 | |||
| 4af947b440 | |||
| 8941057f0e | |||
| 6ebd69422d | |||
| 9052e04d6c | |||
| 99fbe9d515 | |||
| d050afff13 | |||
| e656893c68 | |||
| 07ecae67fe | |||
| ad8ccb183e | |||
| 511fdcbc26 |
34
.gitea/workflows/deploy.yml
Normal file
34
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
name: Deploy Backend
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*.*.*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
env:
|
||||||
|
TMDB_API_KEY: ${{ secrets.TMDB_API_KEY }}
|
||||||
|
TMDB_ACCESS_TOKEN: ${{ secrets.TMDB_ACCESS_TOKEN }}
|
||||||
|
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Extract version tag
|
||||||
|
run: echo "VERSION_TAG=${GITHUB_REF_NAME}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Create shared network if missing
|
||||||
|
run: docker network create poster-picker || true
|
||||||
|
|
||||||
|
- name: Build Docker image
|
||||||
|
run: docker compose build
|
||||||
|
|
||||||
|
- name: Stop old container (if exists)
|
||||||
|
run: docker compose down --remove-orphans || true
|
||||||
|
|
||||||
|
- name: Run compose
|
||||||
|
run: docker compose up -d
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,2 +1,5 @@
|
|||||||
.env
|
.env
|
||||||
.venv
|
.venv
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
|||||||
30
Dockerfile
Normal file
30
Dockerfile
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# backend/Dockerfile
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
RUN addgroup --system app && adduser --system --ingroup app --home /home/app app
|
||||||
|
|
||||||
|
WORKDIR /srv/app
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Assurer permissions
|
||||||
|
RUN chown -R app:app /srv/app
|
||||||
|
|
||||||
|
USER app
|
||||||
|
|
||||||
|
# Port interne
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
# Variable d'environnement par défaut
|
||||||
|
ENV FLASK_ENV=production
|
||||||
|
ENV GUNICORN_WORKERS=2
|
||||||
|
ENV GUNICORN_THREADS=2
|
||||||
|
ENV GUNICORN_WORKER_CLASS=gevent
|
||||||
|
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} --worker-tmp-dir /dev/shm --bind 0.0.0.0:5000 --access-logfile - --error-logfile - 'main:create_app()'"]
|
||||||
Binary file not shown.
@@ -1,21 +1,14 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
# Configuration générale
|
|
||||||
SECRET_KEY = os.environ.get('SECRET_KEY', 'dev_key')
|
SECRET_KEY = os.environ.get('SECRET_KEY', 'dev_key')
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
TESTING = False
|
TESTING = False
|
||||||
REDIS_URL = os.environ.get("REDIS_URL", "redis://localhost:6379")
|
|
||||||
|
|
||||||
class DevelopmentConfig(Config):
|
class DevelopmentConfig(Config):
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
# Configuration pour le développement local
|
|
||||||
REDIS_URL = "redis://localhost:6379"
|
|
||||||
|
|
||||||
class ProductionConfig(Config):
|
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
|
DEBUG = False
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
|
|||||||
18
docker-compose.yml
Normal file
18
docker-compose.yml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
services:
|
||||||
|
backend:
|
||||||
|
build: .
|
||||||
|
image: poster-picker-backend:latest
|
||||||
|
environment:
|
||||||
|
- FLASK_ENV=production
|
||||||
|
- TMDB_API_KEY=${TMDB_API_KEY}
|
||||||
|
- TMDB_ACCESS_TOKEN=${TMDB_ACCESS_TOKEN}
|
||||||
|
- SECRET_KEY=${SECRET_KEY}
|
||||||
|
expose:
|
||||||
|
- "5000"
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- poster-picker
|
||||||
|
|
||||||
|
networks:
|
||||||
|
poster-picker:
|
||||||
|
external: true
|
||||||
14
main.py
14
main.py
@@ -2,7 +2,6 @@ import os
|
|||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
from src.api.routes import api_bp
|
from src.api.routes import api_bp
|
||||||
from flask_sse import sse
|
|
||||||
from config import config
|
from config import config
|
||||||
|
|
||||||
|
|
||||||
@@ -13,17 +12,18 @@ def create_app():
|
|||||||
"http://localhost:3000",
|
"http://localhost:3000",
|
||||||
"http://127.0.0.1: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)
|
CORS(app, supports_credentials=True, origins=origins)
|
||||||
|
|
||||||
app.config.update(
|
|
||||||
SESSION_COOKIE_SAMESITE="None", # necessary for cross-site cookies
|
|
||||||
SESSION_COOKIE_SECURE=False # True in prod (HTTPS)
|
|
||||||
)
|
|
||||||
|
|
||||||
env = os.environ.get('FLASK_ENV', 'default')
|
env = os.environ.get('FLASK_ENV', 'default')
|
||||||
|
|
||||||
|
if env == "production":
|
||||||
|
app.config.update(SESSION_COOKIE_SAMESITE="None", SESSION_COOKIE_SECURE=True)
|
||||||
|
|
||||||
app.config.from_object(config[env])
|
app.config.from_object(config[env])
|
||||||
|
|
||||||
app.register_blueprint(sse, url_prefix='/stream')
|
|
||||||
app.register_blueprint(api_bp, url_prefix='/api')
|
app.register_blueprint(api_bp, url_prefix='/api')
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|||||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
Binary file not shown.
@@ -1,21 +1,25 @@
|
|||||||
import io
|
import io
|
||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
import zipfile
|
import uuid
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import uuid
|
from flask import Blueprint, jsonify, request, session
|
||||||
from flask import Blueprint, jsonify, request, session, send_file
|
|
||||||
from werkzeug.utils import secure_filename
|
|
||||||
from src.services.tmdb_service import TMDBService
|
from src.services.tmdb_service import TMDBService
|
||||||
from src.services.image_service import ImageService
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
|
|
||||||
api_bp = Blueprint('api', __name__)
|
api_bp = Blueprint('api', __name__)
|
||||||
|
|
||||||
tmdb_service = TMDBService()
|
tmdb_service = TMDBService()
|
||||||
image_service = ImageService()
|
|
||||||
|
|
||||||
|
def get_user_id():
|
||||||
|
user_id = request.headers.get('X-User-ID') or session.get('user_id')
|
||||||
|
if not user_id:
|
||||||
|
user_id = str(uuid.uuid4())
|
||||||
|
session['user_id'] = user_id
|
||||||
|
return user_id
|
||||||
|
|
||||||
ALLOWED_EXTENSIONS = {'csv'}
|
ALLOWED_EXTENSIONS = {'csv'}
|
||||||
|
|
||||||
@@ -32,27 +36,45 @@ def get_poster_url(movie_name, movie_year):
|
|||||||
|
|
||||||
@api_bp.route('/movies', methods=['GET'])
|
@api_bp.route('/movies', methods=['GET'])
|
||||||
def get_movies():
|
def get_movies():
|
||||||
user_id = session.get('user_id') # Récupérer l'ID de l'utilisateur de la session
|
user_id = get_user_id()
|
||||||
|
|
||||||
if user_id not in user_csvs:
|
if user_id not in user_csvs:
|
||||||
return jsonify({'movies': [], 'message': 'No movies found. Please upload a CSV file.'}), 200
|
return jsonify({'movies': [], 'message': 'No movies found. Please upload a CSV file.'}), 200
|
||||||
|
|
||||||
# Lire le DataFrame à partir du cache
|
|
||||||
csv_content = user_csvs[user_id]
|
csv_content = user_csvs[user_id]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Si csv_content est une chaîne, il n'est pas nécessaire de décoder
|
|
||||||
if isinstance(csv_content, bytes):
|
if isinstance(csv_content, bytes):
|
||||||
df = pd.read_csv(io.StringIO(csv_content.decode('utf-8')))
|
df = pd.read_csv(io.StringIO(csv_content.decode('utf-8')))
|
||||||
else:
|
else:
|
||||||
df = pd.read_csv(io.StringIO(csv_content))
|
df = pd.read_csv(io.StringIO(csv_content))
|
||||||
|
|
||||||
# Reste de votre code pour la pagination et le traitement...
|
month = request.args.get('month') # e.g. "2024-01"
|
||||||
page = int(request.args.get('page', 1))
|
|
||||||
limit = int(request.args.get('limit', 10))
|
|
||||||
|
|
||||||
df = df.iloc[::-1].reset_index(drop=True)
|
df = df.iloc[::-1].reset_index(drop=True)
|
||||||
total_movies = len(df)
|
total_movies = len(df)
|
||||||
|
|
||||||
|
if month:
|
||||||
|
df['_date_parsed'] = pd.to_datetime(df['Watched Date'], errors='coerce')
|
||||||
|
df = df[df['_date_parsed'].dt.strftime('%Y-%m') == month]
|
||||||
|
df = df.sort_values('_date_parsed')
|
||||||
|
df = df.drop(columns=['_date_parsed'])
|
||||||
|
movies = df.to_dict('records')
|
||||||
|
movies_with_posters = [
|
||||||
|
{
|
||||||
|
'id': i + 1,
|
||||||
|
'Watched Date': movie['Watched Date'],
|
||||||
|
'Name': movie['Name'],
|
||||||
|
'Year': movie['Year'],
|
||||||
|
'Poster': get_poster_url(movie['Name'], movie['Year']),
|
||||||
|
}
|
||||||
|
for i, movie in enumerate(movies)
|
||||||
|
]
|
||||||
|
return jsonify({'movies': movies_with_posters, 'total': len(movies_with_posters)})
|
||||||
|
|
||||||
|
page = int(request.args.get('page', 1))
|
||||||
|
limit = int(request.args.get('limit', 10))
|
||||||
|
|
||||||
total_pages = math.ceil(total_movies / limit)
|
total_pages = math.ceil(total_movies / limit)
|
||||||
|
|
||||||
start_index = (page - 1) * limit
|
start_index = (page - 1) * limit
|
||||||
@@ -86,8 +108,6 @@ def get_movies():
|
|||||||
|
|
||||||
@api_bp.route('/posters/<movie_name>/<movie_year>', methods=['GET'])
|
@api_bp.route('/posters/<movie_name>/<movie_year>', methods=['GET'])
|
||||||
def get_movie_posters(movie_name, movie_year):
|
def get_movie_posters(movie_name, movie_year):
|
||||||
tmdb_service = TMDBService()
|
|
||||||
|
|
||||||
movie = tmdb_service.search_movie(movie_name, movie_year)
|
movie = tmdb_service.search_movie(movie_name, movie_year)
|
||||||
|
|
||||||
if movie:
|
if movie:
|
||||||
@@ -103,11 +123,7 @@ def allowed_file(filename):
|
|||||||
|
|
||||||
@api_bp.route('/upload-csv', methods=['POST'])
|
@api_bp.route('/upload-csv', methods=['POST'])
|
||||||
def upload_csv():
|
def upload_csv():
|
||||||
user_id = session.get('user_id')
|
user_id = get_user_id()
|
||||||
|
|
||||||
if not user_id:
|
|
||||||
user_id = str(uuid.uuid4())
|
|
||||||
session['user_id'] = user_id
|
|
||||||
|
|
||||||
if 'file' not in request.files:
|
if 'file' not in request.files:
|
||||||
return jsonify({'error': 'No file part'}), 400
|
return jsonify({'error': 'No file part'}), 400
|
||||||
@@ -120,11 +136,16 @@ def upload_csv():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
df = pd.read_csv(io.StringIO(content.decode('utf-8')))
|
df = pd.read_csv(io.StringIO(content.decode('utf-8')))
|
||||||
df = df.rename(columns={
|
|
||||||
'Date': 'Watched Date',
|
if 'Watched Date' in df.columns:
|
||||||
'Name': 'Name',
|
pass
|
||||||
'Year': 'Year'
|
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)
|
user_csvs[user_id] = df.to_csv(index=False)
|
||||||
|
|
||||||
@@ -136,7 +157,7 @@ def upload_csv():
|
|||||||
|
|
||||||
@api_bp.route('/check-csv', methods=['GET'])
|
@api_bp.route('/check-csv', methods=['GET'])
|
||||||
def check_csv():
|
def check_csv():
|
||||||
user_id = session.get('user_id')
|
user_id = get_user_id()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if user_id in user_csvs:
|
if user_id in user_csvs:
|
||||||
@@ -150,7 +171,7 @@ def check_csv():
|
|||||||
|
|
||||||
@api_bp.route('/delete-csv', methods=['DELETE'])
|
@api_bp.route('/delete-csv', methods=['DELETE'])
|
||||||
def delete_csv():
|
def delete_csv():
|
||||||
user_id = session.get('user_id')
|
user_id = get_user_id()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if user_id in user_csvs:
|
if user_id in user_csvs:
|
||||||
@@ -163,106 +184,71 @@ def delete_csv():
|
|||||||
return jsonify({'error': 'An error occurred while deleting the CSV file'}), 500
|
return jsonify({'error': 'An error occurred while deleting the CSV file'}), 500
|
||||||
|
|
||||||
|
|
||||||
@api_bp.route('/fetch-diary', methods=['POST'])
|
def _parse_letterboxd_rss(xml_bytes):
|
||||||
def fetch_diary_from_username():
|
import xml.etree.ElementTree as ET
|
||||||
"""
|
root = ET.fromstring(xml_bytes)
|
||||||
Récupère le diary via l'API publique (api.hugo-pierret.be), transforme en CSV et
|
entries = []
|
||||||
le stocke dans user_csvs[user_id] pour que le front continue à utiliser le même flux que l'upload.
|
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:
|
try:
|
||||||
data = request.get_json() or {}
|
data = request.get_json() or {}
|
||||||
username = data.get('username')
|
username = data.get('username', '').strip()
|
||||||
if not username:
|
if not username:
|
||||||
return jsonify({'error': 'username required'}), 400
|
return jsonify({'error': 'username required'}), 400
|
||||||
|
|
||||||
user_id = session.get('user_id')
|
user_id = get_user_id()
|
||||||
if not user_id:
|
|
||||||
user_id = str(uuid.uuid4())
|
|
||||||
session['user_id'] = user_id
|
|
||||||
|
|
||||||
api_url = f"https://api.hugo-pierret.be/letterboxd/diary?username={username}"
|
rss_url = f"https://letterboxd.com/{username}/rss/"
|
||||||
resp = requests.get(api_url)
|
resp = requests.get(rss_url, headers={'User-Agent': 'Mozilla/5.0'}, timeout=10)
|
||||||
if resp.status_code != 200:
|
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()
|
rss_entries = _parse_letterboxd_rss(resp.content)
|
||||||
entries = payload.get('entries', [])
|
if not rss_entries:
|
||||||
if not entries:
|
return jsonify({'error': 'No diary entries found in RSS feed'}), 404
|
||||||
return jsonify({'error': 'No entries returned by upstream API'}), 404
|
|
||||||
|
|
||||||
rows = []
|
if user_id not in user_csvs:
|
||||||
import re
|
df = pd.DataFrame(rss_entries)
|
||||||
for e in entries:
|
df = df.sort_values('Watched Date').reset_index(drop=True)
|
||||||
title = e.get('title') or ""
|
user_csvs[user_id] = df.to_csv(index=False)
|
||||||
date = e.get('date') or ""
|
return jsonify({'success': True, 'fresh': True, 'added': len(rss_entries), 'total': len(rss_entries)}), 200
|
||||||
|
|
||||||
m = re.match(r"^(?P<name>.+?)\s*\((?P<year>\d{4})\)\s*$", title)
|
existing_content = user_csvs[user_id]
|
||||||
if m:
|
df_existing = pd.read_csv(
|
||||||
name = m.group('name').strip()
|
io.StringIO(existing_content.decode('utf-8') if isinstance(existing_content, bytes) else existing_content)
|
||||||
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_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:
|
except Exception as e:
|
||||||
print(f"Error in download_posters: {str(e)}")
|
print("Error in sync_rss:", str(e))
|
||||||
return jsonify({'error': 'An error occurred while fetching movie posters'}), 500
|
return jsonify({'error': 'An internal error occurred'}), 500
|
||||||
|
|||||||
Binary file not shown.
@@ -1,10 +0,0 @@
|
|||||||
import re
|
|
||||||
import os
|
|
||||||
|
|
||||||
|
|
||||||
def clean_filename(filename):
|
|
||||||
return re.sub(r'[\\/*?:"<>|]', '_', filename)
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_directory(directory):
|
|
||||||
os.makedirs(directory, exist_ok=True)
|
|
||||||
@@ -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
|
|
||||||
@@ -24,7 +24,7 @@ class TMDBService:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def get_movie_posters(self, movie_id):
|
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 = {
|
headers = {
|
||||||
"accept": "application/json",
|
"accept": "application/json",
|
||||||
"Authorization": f"Bearer {self.access_token}"
|
"Authorization": f"Bearer {self.access_token}"
|
||||||
|
|||||||
Reference in New Issue
Block a user