first commit
This commit is contained in:
BIN
src/api/__pycache__/routes.cpython-312.pyc
Normal file
BIN
src/api/__pycache__/routes.cpython-312.pyc
Normal file
Binary file not shown.
137
src/api/routes.py
Normal file
137
src/api/routes.py
Normal file
@@ -0,0 +1,137 @@
|
||||
import math
|
||||
import os
|
||||
from flask import Blueprint, jsonify, request
|
||||
from werkzeug.utils import secure_filename
|
||||
from src.services.tmdb_service import TMDBService
|
||||
from src.services.image_service import ImageService
|
||||
import pandas as pd
|
||||
from functools import lru_cache
|
||||
|
||||
api_bp = Blueprint('api', __name__)
|
||||
|
||||
tmdb_service = TMDBService()
|
||||
image_service = ImageService()
|
||||
|
||||
UPLOAD_FOLDER = './data'
|
||||
ALLOWED_EXTENSIONS = {'csv'}
|
||||
|
||||
csv_file = os.path.join(UPLOAD_FOLDER, 'diary.csv')
|
||||
|
||||
@lru_cache(maxsize=1000)
|
||||
def get_poster_url(movie_name, movie_year):
|
||||
movie = tmdb_service.search_movie(movie_name, movie_year)
|
||||
if movie:
|
||||
posters = tmdb_service.get_movie_posters(movie['id'])
|
||||
if posters:
|
||||
return posters[0]
|
||||
return None
|
||||
|
||||
@api_bp.route('/movies', methods=['GET'])
|
||||
def get_movies():
|
||||
try:
|
||||
if not os.path.exists(csv_file):
|
||||
return jsonify({'movies': [], 'message': 'No movies found. Please upload a CSV file.'}), 200
|
||||
|
||||
page = int(request.args.get('page', 1))
|
||||
limit = int(request.args.get('limit', 10))
|
||||
|
||||
df = pd.read_csv(csv_file)
|
||||
|
||||
df = df.iloc[::-1].reset_index(drop=True)
|
||||
|
||||
total_movies = len(df)
|
||||
total_pages = math.ceil(total_movies / limit)
|
||||
|
||||
start_index = (page - 1) * limit
|
||||
end_index = start_index + limit
|
||||
|
||||
df_page = df.iloc[start_index:end_index]
|
||||
|
||||
movies = df_page.to_dict('records')
|
||||
|
||||
movies_with_posters = [
|
||||
{
|
||||
'id': start_index + 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': total_movies,
|
||||
'page': page,
|
||||
'total_pages': total_pages
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"Error in get_movies: {str(e)}")
|
||||
return jsonify({'error': 'An error occurred while fetching movies'}), 500
|
||||
|
||||
@api_bp.route('/posters/<movie_name>/<movie_year>', methods=['GET'])
|
||||
def get_movie_posters(movie_name, movie_year):
|
||||
|
||||
tmdb_service = TMDBService()
|
||||
|
||||
movie = tmdb_service.search_movie(movie_name, movie_year)
|
||||
|
||||
if movie:
|
||||
posters = tmdb_service.get_movie_posters(movie['id'])
|
||||
return jsonify({'posters': posters})
|
||||
else:
|
||||
return jsonify({'error': 'Movie not found'}), 404
|
||||
|
||||
def allowed_file(filename):
|
||||
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
||||
|
||||
@api_bp.route('/upload-csv', methods=['POST'])
|
||||
def upload_csv():
|
||||
if 'file' not in request.files:
|
||||
return jsonify({'error': 'No file part'}), 400
|
||||
file = request.files['file']
|
||||
if file.filename == '':
|
||||
return jsonify({'error': 'No selected file'}), 400
|
||||
if file and allowed_file(file.filename):
|
||||
filename = secure_filename(file.filename)
|
||||
file_path = os.path.join(UPLOAD_FOLDER, filename)
|
||||
file.save(file_path)
|
||||
|
||||
try:
|
||||
df = pd.read_csv(file_path)
|
||||
df = df.rename(columns={
|
||||
'Date': 'Watched Date',
|
||||
'Name': 'Name',
|
||||
'Year': 'Year'
|
||||
})
|
||||
|
||||
df.to_csv(csv_file, index=False)
|
||||
|
||||
return jsonify({'success': True, 'message': 'File uploaded and processed successfully'}), 200
|
||||
except Exception as e:
|
||||
return jsonify({'error': f'Error processing file: {str(e)}'}), 500
|
||||
return jsonify({'error': 'File type not allowed'}), 400
|
||||
|
||||
@api_bp.route('/check-csv', methods=['GET'])
|
||||
def check_csv():
|
||||
try:
|
||||
if os.path.exists(csv_file):
|
||||
return jsonify({'fileExists': True}), 200
|
||||
else:
|
||||
return jsonify({'fileExists': False}), 200
|
||||
except Exception as e:
|
||||
print(f"Error in check_csv: {str(e)}")
|
||||
return jsonify({'error': 'An error occurred while checking the CSV file'}), 500
|
||||
|
||||
@api_bp.route('/delete-csv', methods=['DELETE'])
|
||||
def delete_csv():
|
||||
try:
|
||||
if os.path.exists(csv_file):
|
||||
os.remove(csv_file)
|
||||
return jsonify({'success': True, 'message': 'CSV file deleted successfully'}), 200
|
||||
else:
|
||||
return jsonify({'error': 'CSV file not found'}), 404
|
||||
except Exception as e:
|
||||
print(f"Error in delete_csv: {str(e)}")
|
||||
return jsonify({'error': 'An error occurred while deleting the CSV file'}), 500
|
||||
BIN
src/gui/__pycache__/poster_selector.cpython-312.pyc
Normal file
BIN
src/gui/__pycache__/poster_selector.cpython-312.pyc
Normal file
Binary file not shown.
BIN
src/gui/components/__pycache__/movie_listbox.cpython-312.pyc
Normal file
BIN
src/gui/components/__pycache__/movie_listbox.cpython-312.pyc
Normal file
Binary file not shown.
BIN
src/gui/components/__pycache__/poster_frame.cpython-312.pyc
Normal file
BIN
src/gui/components/__pycache__/poster_frame.cpython-312.pyc
Normal file
Binary file not shown.
28
src/gui/components/movie_listbox.py
Normal file
28
src/gui/components/movie_listbox.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from tkinter import Listbox, SINGLE
|
||||
from services.file_utils import clean_filename
|
||||
import os
|
||||
|
||||
class MovieListbox(Listbox):
|
||||
def __init__(self, master, movies, on_select_callback):
|
||||
super().__init__(master, selectmode=SINGLE, bg='#23272a', fg='white',
|
||||
selectbackground='#7289da', width=50)
|
||||
self.movies = movies
|
||||
self.on_select_callback = on_select_callback
|
||||
self.populate_listbox()
|
||||
self.bind("<<ListboxSelect>>", self.on_movie_select)
|
||||
|
||||
def populate_listbox(self):
|
||||
for index, row in self.movies.iterrows():
|
||||
movie_title = f"{row['Name']} ({row['Year']})"
|
||||
safe_title = clean_filename(f"{row['Name']}_{row['Year']}")
|
||||
poster_path = os.path.join('posters', f"{safe_title}.jpg")
|
||||
|
||||
self.insert(index, movie_title)
|
||||
if os.path.exists(poster_path):
|
||||
self.itemconfig(index, {'fg': 'green'})
|
||||
|
||||
def on_movie_select(self, event):
|
||||
selected_index = self.curselection()
|
||||
if selected_index:
|
||||
selected_movie = self.movies.iloc[selected_index[0]]
|
||||
self.on_select_callback(selected_movie)
|
||||
144
src/gui/components/poster_frame.py
Normal file
144
src/gui/components/poster_frame.py
Normal file
@@ -0,0 +1,144 @@
|
||||
from tkinter import Frame, Label, Canvas, NW, StringVar, messagebox
|
||||
from tkinter.ttk import Progressbar
|
||||
from PIL import ImageTk
|
||||
from threading import Thread
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import os
|
||||
|
||||
from src.services.image_service import ImageService
|
||||
from services.file_utils import clean_filename
|
||||
|
||||
class PosterFrame(Frame):
|
||||
def __init__(self, master):
|
||||
super().__init__(master, bg='#2c2f33')
|
||||
self.canvas = Canvas(self, bg='#2c2f33', highlightthickness=0)
|
||||
self.canvas.pack(side="right", fill="both", expand=True)
|
||||
|
||||
self.poster_frame = Frame(self.canvas, bg='#2c2f33')
|
||||
self.canvas.create_window((0, 0), window=self.poster_frame, anchor=NW)
|
||||
|
||||
self.poster_frame.bind("<Configure>", self.on_frame_configure)
|
||||
self.master.bind("<Configure>", self.on_window_resize)
|
||||
self.master.bind_all("<MouseWheel>", self.on_mouse_wheel)
|
||||
|
||||
self.loading_label = None
|
||||
self.progress_bar = None
|
||||
self.resizing = False
|
||||
self.current_movie = None
|
||||
|
||||
self.initial_message = Label(self.poster_frame, text="Aucun poster à afficher. Veuillez sélectionner un film.", bg='#2c2f33', fg='white')
|
||||
self.initial_message.pack(pady=20)
|
||||
|
||||
def show_posters(self, posters, movie, image_service):
|
||||
self.clear_poster_frame()
|
||||
self.current_movie = movie
|
||||
|
||||
self.loading_label = Label(self.poster_frame, text="Chargement des posters...",
|
||||
bg='#2c2f33', fg='white')
|
||||
self.loading_label.grid(row=0, column=0, pady=20)
|
||||
|
||||
self.progress_bar = Progressbar(self.poster_frame, orient="horizontal",
|
||||
mode="determinate", length=400)
|
||||
self.progress_bar.grid(row=1, column=0, pady=10)
|
||||
|
||||
Thread(target=self.download_and_display_posters,
|
||||
args=(posters, image_service)).start()
|
||||
|
||||
def download_and_display_posters(self, posters, image_service):
|
||||
max_columns = max(1, self.canvas.winfo_width() // 200)
|
||||
images = []
|
||||
|
||||
with ThreadPoolExecutor(max_workers=10) as executor:
|
||||
future_to_url = {executor.submit(image_service.download_image,
|
||||
f"https://image.tmdb.org/t/p/w200{poster['file_path']}"):
|
||||
poster['file_path'] for poster in posters}
|
||||
|
||||
total = len(future_to_url)
|
||||
for i, future in enumerate(future_to_url):
|
||||
image = future.result()
|
||||
if image:
|
||||
images.append(image)
|
||||
self.update_progress_bar(i + 1, total)
|
||||
|
||||
self.display_posters(images, max_columns)
|
||||
|
||||
def display_posters(self, images, max_columns):
|
||||
self.clear_poster_frame()
|
||||
for idx, image in enumerate(images):
|
||||
image.thumbnail((200, 300))
|
||||
photo = ImageTk.PhotoImage(image)
|
||||
label = Label(self.poster_frame, image=photo, bg='#2c2f33')
|
||||
label.image = photo
|
||||
row = idx // max_columns
|
||||
col = idx % max_columns
|
||||
label.grid(row=row, column=col, padx=5, pady=5)
|
||||
label.bind("<Button-1>", lambda e, idx=idx: self.download_selected_poster(idx))
|
||||
|
||||
def download_selected_poster(self, idx):
|
||||
poster = self.posters[idx]
|
||||
poster_path = poster['file_path']
|
||||
poster_url = f"https://image.tmdb.org/t/p/original{poster_path}"
|
||||
|
||||
title = self.current_movie['Name']
|
||||
year = self.current_movie['Year']
|
||||
safe_title = clean_filename(f"{title}_{year}")
|
||||
|
||||
output_folder = 'posters'
|
||||
os.makedirs(output_folder, exist_ok=True)
|
||||
save_path = os.path.join(output_folder, f"{safe_title}.jpg")
|
||||
|
||||
if os.path.exists(save_path):
|
||||
response = messagebox.askyesno("Poster déjà existant",
|
||||
f"Le poster pour {title} ({year}) existe déjà. Voulez-vous l'écraser?")
|
||||
if not response:
|
||||
return
|
||||
|
||||
# Téléchargement du poster en haute qualité
|
||||
image = self.image_service.download_image(poster_url, save_path)
|
||||
|
||||
if image:
|
||||
messagebox.showinfo("Téléchargement réussi", f"Poster téléchargé et enregistré : {save_path}")
|
||||
# Mettre à jour la liste des films pour refléter le nouveau téléchargement
|
||||
self.master.movie_listbox.populate_listbox()
|
||||
else:
|
||||
messagebox.showerror("Erreur", "Échec du téléchargement du poster.")
|
||||
|
||||
def update_progress_bar(self, value, total):
|
||||
if self.progress_bar:
|
||||
progress = (value / total) * 100
|
||||
self.master.after(0, lambda: self.safe_update_progress(progress))
|
||||
|
||||
def safe_update_progress(self, progress):
|
||||
if self.progress_bar and self.progress_bar.winfo_exists():
|
||||
self.progress_bar['value'] = progress
|
||||
self.update_idletasks()
|
||||
|
||||
def clear_poster_frame(self):
|
||||
for widget in self.poster_frame.winfo_children():
|
||||
widget.destroy()
|
||||
|
||||
def on_frame_configure(self, event):
|
||||
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
|
||||
|
||||
def on_window_resize(self, event):
|
||||
if not self.resizing:
|
||||
self.resizing = True
|
||||
self.after(100, self.resize_done)
|
||||
|
||||
def resize_done(self):
|
||||
self.resizing = False
|
||||
self.update_poster_layout()
|
||||
|
||||
def update_poster_layout(self):
|
||||
for widget in self.poster_frame.winfo_children():
|
||||
widget.grid_forget()
|
||||
|
||||
max_columns = max(1, self.poster_frame.winfo_width() // 200)
|
||||
|
||||
for idx, widget in enumerate(self.poster_frame.winfo_children()):
|
||||
row = idx // max_columns
|
||||
col = idx % max_columns
|
||||
widget.grid(row=row, column=col, padx=5, pady=5)
|
||||
|
||||
def on_mouse_wheel(self, event):
|
||||
self.canvas.yview_scroll(-1 * (event.delta // 120), "units")
|
||||
28
src/gui/poster_selector.py
Normal file
28
src/gui/poster_selector.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from tkinter import Tk, Frame
|
||||
from .components.movie_listbox import MovieListbox
|
||||
from .components.poster_frame import PosterFrame
|
||||
from src.services.tmdb_service import TMDBService
|
||||
from src.services.image_service import ImageService
|
||||
|
||||
class PosterSelector(Tk):
|
||||
def __init__(self, movies):
|
||||
super().__init__()
|
||||
self.movies = movies
|
||||
self.tmdb_service = TMDBService()
|
||||
self.image_service = ImageService()
|
||||
|
||||
self.title("Sélectionnez un Poster")
|
||||
self.geometry("1200x600")
|
||||
self.configure(bg='#2c2f33')
|
||||
|
||||
self.movie_listbox = MovieListbox(self, movies, self.on_movie_select)
|
||||
self.poster_frame = PosterFrame(self)
|
||||
|
||||
self.movie_listbox.pack(side="left", fill="y")
|
||||
self.poster_frame.pack(side="right", fill="both", expand=True)
|
||||
|
||||
def on_movie_select(self, selected_movie):
|
||||
movie = self.tmdb_service.search_movie(selected_movie['Name'], selected_movie['Year'])
|
||||
if movie:
|
||||
posters = self.tmdb_service.get_movie_posters(movie['id'])
|
||||
self.poster_frame.show_posters(posters, selected_movie, self.image_service)
|
||||
BIN
src/services/__pycache__/image_service.cpython-312.pyc
Normal file
BIN
src/services/__pycache__/image_service.cpython-312.pyc
Normal file
Binary file not shown.
BIN
src/services/__pycache__/tmdb_service.cpython-312.pyc
Normal file
BIN
src/services/__pycache__/tmdb_service.cpython-312.pyc
Normal file
Binary file not shown.
8
src/services/file_utils.py
Normal file
8
src/services/file_utils.py
Normal file
@@ -0,0 +1,8 @@
|
||||
import re
|
||||
import os
|
||||
|
||||
def clean_filename(filename):
|
||||
return re.sub(r'[\\/*?:"<>|]', '_', filename)
|
||||
|
||||
def ensure_directory(directory):
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
13
src/services/image_service.py
Normal file
13
src/services/image_service.py
Normal file
@@ -0,0 +1,13 @@
|
||||
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
|
||||
33
src/services/tmdb_service.py
Normal file
33
src/services/tmdb_service.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import os
|
||||
import requests
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
class TMDBService:
|
||||
def __init__(self):
|
||||
self.api_key = os.getenv('TMDB_API_KEY')
|
||||
self.access_token = os.getenv('TMDB_ACCESS_TOKEN')
|
||||
self.base_url = 'https://api.themoviedb.org/3'
|
||||
|
||||
def search_movie(self, title, year):
|
||||
url = f"{self.base_url}/search/movie"
|
||||
params = {
|
||||
'api_key': self.api_key,
|
||||
'query': title,
|
||||
'year': year
|
||||
}
|
||||
response = requests.get(url, params=params)
|
||||
response_data = response.json()
|
||||
if response_data['results']:
|
||||
return response_data['results'][0]
|
||||
return None
|
||||
|
||||
def get_movie_posters(self, movie_id):
|
||||
url = f"{self.base_url}/movie/{movie_id}/images?include_image_language=en"
|
||||
headers = {
|
||||
"accept": "application/json",
|
||||
"Authorization": f"Bearer {self.access_token}"
|
||||
}
|
||||
response = requests.get(url, headers=headers)
|
||||
return response.json().get('posters', [])
|
||||
Reference in New Issue
Block a user