From d175ce494403de04b10e0c2e3f7016a13f8252d4 Mon Sep 17 00:00:00 2001 From: Hugo Pierret Date: Thu, 5 Sep 2024 18:34:12 +0200 Subject: [PATCH] first commit --- .gitignore | 2 + data/diary.csv | 66 ++++++++ main.py | 18 +++ reorderPosters.py | 37 +++++ requirements.txt | 0 src/api/__pycache__/routes.cpython-312.pyc | Bin 0 -> 7261 bytes src/api/routes.py | 137 +++++++++++++++++ .../poster_selector.cpython-312.pyc | Bin 0 -> 2207 bytes .../__pycache__/movie_listbox.cpython-312.pyc | Bin 0 -> 2284 bytes .../__pycache__/poster_frame.cpython-312.pyc | Bin 0 -> 10078 bytes src/gui/components/movie_listbox.py | 28 ++++ src/gui/components/poster_frame.py | 144 ++++++++++++++++++ src/gui/poster_selector.py | 28 ++++ .../__pycache__/image_service.cpython-312.pyc | Bin 0 -> 970 bytes .../__pycache__/tmdb_service.cpython-312.pyc | Bin 0 -> 1931 bytes src/services/file_utils.py | 8 + src/services/image_service.py | 13 ++ src/services/tmdb_service.py | 33 ++++ 18 files changed, 514 insertions(+) create mode 100644 .gitignore create mode 100644 data/diary.csv create mode 100644 main.py create mode 100644 reorderPosters.py create mode 100644 requirements.txt create mode 100644 src/api/__pycache__/routes.cpython-312.pyc create mode 100644 src/api/routes.py create mode 100644 src/gui/__pycache__/poster_selector.cpython-312.pyc create mode 100644 src/gui/components/__pycache__/movie_listbox.cpython-312.pyc create mode 100644 src/gui/components/__pycache__/poster_frame.cpython-312.pyc create mode 100644 src/gui/components/movie_listbox.py create mode 100644 src/gui/components/poster_frame.py create mode 100644 src/gui/poster_selector.py create mode 100644 src/services/__pycache__/image_service.cpython-312.pyc create mode 100644 src/services/__pycache__/tmdb_service.cpython-312.pyc create mode 100644 src/services/file_utils.py create mode 100644 src/services/image_service.py create mode 100644 src/services/tmdb_service.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..85c55eb --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.env +.venv diff --git a/data/diary.csv b/data/diary.csv new file mode 100644 index 0000000..496fac2 --- /dev/null +++ b/data/diary.csv @@ -0,0 +1,66 @@ +Watched Date,Name,Year,Letterboxd URI,Rating,Rewatch,Tags,Watched Date +2023-12-15,Ghost in the Shell,1995,https://boxd.it/5k8vT3,3.5,,,2023-12-13 +2024-01-03,Baby Driver,2017,https://boxd.it/5tDbaP,4.0,,,2024-01-02 +2024-01-04,The Shawshank Redemption,1994,https://boxd.it/5ugTgP,4.5,,,2024-01-03 +2024-01-05,Léon: The Professional,1994,https://boxd.it/5uRr6B,4.0,,,2024-01-04 +2024-01-06,Brothers,2009,https://boxd.it/5vuRQN,3.5,,,2024-01-05 +2024-01-07,"Crazy, Stupid, Love.",2011,https://boxd.it/5wgeFn,4.0,,,2024-01-06 +2024-01-08,There Will Be Blood,2007,https://boxd.it/5x4B37,4.5,,,2024-01-07 +2024-01-09,(500) Days of Summer,2009,https://boxd.it/5xDHe1,4.0,,,2024-01-08 +2024-01-20,Dallas Buyers Club,2013,https://boxd.it/5DrLPb,4.0,,,2024-01-19 +2024-01-22,Boogie Nights,1997,https://boxd.it/5EEjCN,4.0,,,2024-01-21 +2024-01-24,Saltburn,2023,https://boxd.it/5FxIYT,4.0,,,2024-01-23 +2024-01-25,Under the Skin,2013,https://boxd.it/5G0SG9,2.5,,,2024-01-24 +2024-01-26,Anatomy of a Fall,2023,https://boxd.it/5GuroR,4.0,,,2024-01-25 +2024-01-31,Poor Things,2023,https://boxd.it/5J4AI7,4.5,,,2024-01-30 +2024-02-01,In the Mood for Love,2000,https://boxd.it/5JweK7,4.5,,,2024-01-31 +2024-02-07,Black Swan,2010,https://boxd.it/5MhygD,4.0,,,2024-02-06 +2024-02-13,Rain Man,1988,https://boxd.it/5OXE5j,4.0,,,2024-02-12 +2024-02-20,The 40 Year Old Virgin,2005,https://boxd.it/5Sgvxj,3.5,,,2024-02-19 +2024-02-21,Captain Fantastic,2016,https://boxd.it/5SHgol,4.0,,,2024-02-20 +2024-02-23,The Prestige,2006,https://boxd.it/5TwaTp,4.5,,,2024-02-22 +2024-02-29,The Shining,1980,https://boxd.it/5WgsV9,4.0,,,2024-02-28 +2024-03-02,Dune: Part Two,2024,https://boxd.it/5X4Ep5,5.0,,,2024-03-01 +2024-03-12,Fantastic Mr. Fox,2009,https://boxd.it/622Xk5,4.5,,,2024-03-11 +2024-03-13,Past Lives,2023,https://boxd.it/62uP1F,4.5,,,2024-03-12 +2024-03-19,Little Women,2019,https://boxd.it/65jkNX,3.0,,,2024-03-18 +2024-03-21,The Perks of Being a Wallflower,2012,https://boxd.it/668ca1,4.0,,,2024-03-20 +2024-03-28,Amélie,2001,https://boxd.it/69lEoN,4.5,,,2024-03-27 +2024-03-29,Lost in Translation,2003,https://boxd.it/69Kn1n,4.0,,,2024-03-28 +2024-04-03,No Country for Old Men,2007,https://boxd.it/6clZZd,4.0,,,2024-04-02 +2024-04-05,Fargo,1996,https://boxd.it/6dadIv,3.5,,,2024-04-04 +2024-04-09,No Hard Feelings,2023,https://boxd.it/6f0cwL,2.5,,,2024-04-08 +2024-04-17,"OSS 117: Cairo, Nest of Spies",2006,https://boxd.it/6i7AOl,4.0,,,2024-04-16 +2024-04-20,The Neon Demon,2016,https://boxd.it/6j9IF5,3.0,,,2024-04-19 +2024-04-23,Taxi Driver,1976,https://boxd.it/6ktjeR,3.5,,,2024-04-22 +2024-04-30,The Theory of Everything,2014,https://boxd.it/6nlzHL,4.5,,,2024-04-29 +2024-05-09,Civil War,2024,https://boxd.it/6rbidl,4.5,,,2024-05-08 +2024-05-10,Scott Pilgrim vs. the World,2010,https://boxd.it/6rxEAl,4.0,,,2024-05-09 +2024-05-15,Asteroid City,2023,https://boxd.it/6txkqD,3.5,,,2024-05-14 +2024-05-22,Battle of the Sexes,2017,https://boxd.it/6wiCsp,3.5,,,2024-05-22 +2024-05-26,War Dogs,2016,https://boxd.it/6xS8YB,3.5,,,2024-05-25 +2024-05-28,The Creator,2023,https://boxd.it/6yPVwT,4.5,,,2024-05-27 +2024-05-30,Full Metal Jacket,1987,https://boxd.it/6zEwVV,4.0,,,2024-05-29 +2024-06-04,Dead Poets Society,1989,https://boxd.it/6BNjNv,3.5,,,2024-06-03 +2024-06-05,Perfect Days,2023,https://boxd.it/6C9zSb,4.0,,,2024-06-04 +2024-06-07,Burn After Reading,2008,https://boxd.it/6CQVG3,3.0,,,2024-06-06 +2024-06-12,Before Sunrise,1995,https://boxd.it/6EWJMD,4.0,,,2024-06-11 +2024-06-20,OSS 117: Lost in Rio,2009,https://boxd.it/6I1d3j,3.5,,,2024-06-19 +2024-06-23,Inside Out 2,2024,https://boxd.it/6J9mU7,4.0,,,2024-06-22 +2024-06-26,May December,2023,https://boxd.it/6KqGP5,2.5,,,2024-06-25 +2024-06-29,Ferrari,2023,https://boxd.it/6Lv8pV,3.0,,,2024-06-28 +2024-06-30,Sicario,2015,https://boxd.it/6LUq2b,4.0,,,2024-06-29 +2024-07-21,Gladiator,2000,https://boxd.it/6Vcoij,4.5,,,2024-07-20 +2024-07-22,Perfect Blue,1997,https://boxd.it/6VIhi1,4.0,,,2024-07-21 +2024-07-23,Paprika,2006,https://boxd.it/6WcVWb,4.5,,,2024-07-22 +2024-07-24,Dream Scenario,2023,https://boxd.it/6WCG0t,4.0,,,2024-07-23 +2024-07-29,Yes Man,2008,https://boxd.it/6ZbFWJ,3.0,,,2024-07-29 +2024-07-30,Deadpool & Wolverine,2024,https://boxd.it/6Zo8Sb,3.5,,,2024-07-29 +2024-07-31,Source Code,2011,https://boxd.it/6ZPH0H,3.5,,,2024-07-30 +2024-08-04,Almost Famous,2000,https://boxd.it/71w9tb,4.0,,,2024-08-03 +2024-08-05,The Master,2012,https://boxd.it/72e6pN,3.0,,,2024-08-05 +2024-08-10,Millennium Actress,2001,https://boxd.it/747e7V,4.0,,,2024-08-10 +2024-08-14,The King of Comedy,1982,https://boxd.it/75VeGl,4.5,,,2024-08-13 +2024-08-15,American Fiction,2023,https://boxd.it/76lZ2J,4.0,,,2024-08-14 +2024-08-18,The Diary of a Teenage Girl,2015,https://boxd.it/77HSDB,3.5,,,2024-08-17 +2024-08-20,Punch-Drunk Love,2002,https://boxd.it/78Mk2P,4.0,,,2024-08-19 diff --git a/main.py b/main.py new file mode 100644 index 0000000..0b47046 --- /dev/null +++ b/main.py @@ -0,0 +1,18 @@ +from flask import Flask +from flask_cors import CORS +from src.api.routes import api_bp +from flask_sse import sse + +def create_app(): + app = Flask(__name__) + CORS(app) + + app.config["REDIS_URL"] = "redis://localhost:6379" + app.register_blueprint(sse, url_prefix='/stream') + app.register_blueprint(api_bp, url_prefix='/api') + + return app + +if __name__ == "__main__": + app = create_app() + app.run(debug=True) \ No newline at end of file diff --git a/reorderPosters.py b/reorderPosters.py new file mode 100644 index 0000000..add6d57 --- /dev/null +++ b/reorderPosters.py @@ -0,0 +1,37 @@ +import os +import pandas as pd +import re +from pathlib import Path + +def clean_filename(filename): + return re.sub(r'[\\/*?:"<>|]', '_', filename) + +csv_file = './data/diary.csv' +movies = pd.read_csv(csv_file) + +output_folder = 'posters' + +poster_files = [f for f in os.listdir(output_folder) if os.path.isfile(os.path.join(output_folder, f))] + +# Parcourir les films dans l'ordre du fichier CSV +for index, row in movies.iterrows(): + title = row['Name'] + year = row['Year'] + safe_title = clean_filename(f"{title}_{year}") + + # Trouver le fichier poster correspondant + for poster_file in poster_files: + if safe_title in poster_file: + # Déterminer le nouveau nom du fichier avec un préfixe basé sur l'index du film + new_name = f"{index+1:03d}_{poster_file}" + old_path = os.path.join(output_folder, poster_file) + new_path = os.path.join(output_folder, new_name) + + # Renommer le fichier + os.rename(old_path, new_path) + print(f"Renommé : {poster_file} -> {new_name}") + break + else: + print(f"Poster non trouvé pour : {title} ({year})") + +print("Tri terminé.") \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/api/__pycache__/routes.cpython-312.pyc b/src/api/__pycache__/routes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a7e92a83c2bd7454368b64276ed3486d91e43c40 GIT binary patch literal 7261 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 literal 0 HcmV?d00001 diff --git a/src/api/routes.py b/src/api/routes.py new file mode 100644 index 0000000..795a0bf --- /dev/null +++ b/src/api/routes.py @@ -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//', 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 \ No newline at end of file diff --git a/src/gui/__pycache__/poster_selector.cpython-312.pyc b/src/gui/__pycache__/poster_selector.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b9ef7eac6ea5999510bed3aa44d291560e382965 GIT binary patch literal 2207 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 literal 0 HcmV?d00001 diff --git a/src/gui/components/__pycache__/poster_frame.cpython-312.pyc b/src/gui/components/__pycache__/poster_frame.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5e02bff6ccac8a2aec622cb1a625a4c13e215e08 GIT binary patch literal 10078 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 Ee`cq|qyPW_ literal 0 HcmV?d00001 diff --git a/src/gui/components/movie_listbox.py b/src/gui/components/movie_listbox.py new file mode 100644 index 0000000..644fdbb --- /dev/null +++ b/src/gui/components/movie_listbox.py @@ -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("<>", 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) \ No newline at end of file diff --git a/src/gui/components/poster_frame.py b/src/gui/components/poster_frame.py new file mode 100644 index 0000000..97d29bc --- /dev/null +++ b/src/gui/components/poster_frame.py @@ -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("", self.on_frame_configure) + self.master.bind("", self.on_window_resize) + self.master.bind_all("", 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("", 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") \ No newline at end of file diff --git a/src/gui/poster_selector.py b/src/gui/poster_selector.py new file mode 100644 index 0000000..efe9d03 --- /dev/null +++ b/src/gui/poster_selector.py @@ -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) \ No newline at end of file diff --git a/src/services/__pycache__/image_service.cpython-312.pyc b/src/services/__pycache__/image_service.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8135912153670a2fc888ec5f19d9a23747ccfa1b GIT binary patch literal 970 zcmY*X%}*0S6rb4-`UL`MQ49t=s9`y9(Zq<22Mr-5q>_M_%_f_*GnCf-C^J*Eq$D0V za3vn9iHRIFc<|3KF)=|%r~`WO#LZX}6HdypkbL0SL-1YKZCR9xsOrUIig)l?NQ2avi2khZ5pS*F&2hJGrIq5-*H@SHL( zMZQRP13@Pz>=wYlgb0`lQjxL+O;x08ygy7^!eqTt!o`w$E?*a1+MVxVIup}JQjH78 z2LhWA!S_l$+osx7tMTp5MIrUcBmiWNCE~+sGG;wwnV47mW7b1a`9jZ%*@%)WXB`{5K!F{@C%-D?Z1%y7^@w6Zt<`!ts9B37C=ZP|vM4E8B2KQ4 zT2Ys-IbCb;3n+N%yMcr3(6upEkaKj`J^jD4Pc)~?LOlm3*|AoBC%>EeaP2UAdpp_A zWREfv`me$(xN=8*e9%re^l1W)3Fq9^~@dvtI{C+e0I*^iKNa zox`E)?d<6GLVGaV5p{t9i{i^AZQFM|Y}-6-+g^a`uH^l;y;gVJ$U`oP=y!mO3LqDy zk}ne~NvQljTi~~n&~s6YJT-KZ62R5P!a}$oG9b(-^NM8us+}eCQQv!HTsY*{q0;$N Yg%BQt!Ea#X7^MDaNtpWugbFkN0sxELOaK4? literal 0 HcmV?d00001 diff --git a/src/services/__pycache__/tmdb_service.cpython-312.pyc b/src/services/__pycache__/tmdb_service.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2e0cac17630092aed350d38c17dfee99977d90ee GIT binary patch literal 1931 zcmah~-ESL35a0W<-Po=hoRE(uAq3H4RmA-e(1rj?(tx6tP)HHNvRGZbTgR^Lb9c|7 zw$7ptK{5gad2*{XsviUCAHe^>i$fySx)uq<6K^!Ggw!Wy?|eyAs&*vL&fI+L?96Z1 zzlOtI2m4#k3e^*Jq=485k#aMO1P_(kU*Dna$ZR&5|R;BHaqS@`v4JT z1rc>qc0VT6vJ}()7Rr1~vV%E8Czu!}Ei9)xVa_6Tl=cV+bwrWaCLt4P1tku&zq|J`qLU_;qoEZ5)jM&W~ zqCf_(Yp9H3>bxBiHR7pDbNIsS=XSTN&CJX$Ea1x@U6`G(3@w>vkx#_qdNDI@F44TP zoS|fK++gYW891bDaNC!rqE)sZeDGCTwnKUEzg^L4? zKE60Jk^GcX#*<5>v{6i6Vn&+j`Mhpsj6(7vHBHJEjq4=2sHY$gNphA-g1_h|PnvnM zh&$SNv1|u0&J;2x#+9A}eINI}24G8m0@w}opl_gFUM)99w);-j)bBzMhDPqp-I{A2 zJGVVFQB!vU1FilyYwGvmwrE>wuYaJ3?g_p9Gq5_cP!`rFX$_FLE?1%3Pn)MDSu&At zD^=ChyvVH2llE6jvDCYYhtR5KY1u>jR!{O!UG-T$q7J}k7VspZ34K9K+BaVb$2rwm zYANnaFs85|7;WuZiL$b-mca(&@fCHL0Jr^&@>0&^OaLyXv9L(&1Oym`5N>i?fvMYS zmK%kbpB;9RnHe)jS&xtvm^bSc5=8K#QQ*`LdlDwPsdGPAM_pnO;Tdu(#7>2k{)1yY zXc5KmFM!=Zt?u6X@api7k(<@#;n!=*AALvb)~eO&9c=aVKlH0Fbk%&1x=_zheR6fO zp=`Xr6FpN4wE9Qygl~n{BDHWU*ne~2x8N(S=xAeLD|%vMYAZV442`!UQPvGxr1owc zD0|cY1(at^-;w|W^151;i9*z;<6sS~%S5{kIC%~ZWJ|g0+rxossn5beRy6eqBK|b; z;ulaXFkkVzAc$99j%V_En)3HDg;cIYD0Wnw(+lYme1C@)Do6BUF_%d>mohG%c4S?idqB?=AZTpJ`hkMLD5QU&P)+boMU;?3Fa^Mqi z=8iLj9TlcA!G?fwaZenaxG?Dk;JFyVo#d6_gIM{Wc~B(5zzzcD%f|Iawt4d7=HX8O zH!r@{dg)lhym$QU*739O5;TV=9%@RotLA?c*oV(&JJB})K9Rm3s^3(<{pwz1Y%4O> zIJdF59XZtuoN^E|AM=T~sG#R5#&#EmN=Ydv|p6aORZRbW3MC)hb4 z6X$i97$z4mTtU0CqP?}-71Vm#gMRI_>n$9id*WSidTRUJKP@KSC^t<^NFs%k`f*^= qE$3Y(-a_#X^I>5BD3T=IM~CjC$nR+6PZV4U|Ems4r*{!BC;UG+%e|-o literal 0 HcmV?d00001 diff --git a/src/services/file_utils.py b/src/services/file_utils.py new file mode 100644 index 0000000..bb81a0e --- /dev/null +++ b/src/services/file_utils.py @@ -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) \ No newline at end of file diff --git a/src/services/image_service.py b/src/services/image_service.py new file mode 100644 index 0000000..d40992e --- /dev/null +++ b/src/services/image_service.py @@ -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 \ No newline at end of file diff --git a/src/services/tmdb_service.py b/src/services/tmdb_service.py new file mode 100644 index 0000000..7593a31 --- /dev/null +++ b/src/services/tmdb_service.py @@ -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', []) \ No newline at end of file