diff --git a/animecli.py b/animecli.py index bc3375f..91c8571 100644 --- a/animecli.py +++ b/animecli.py @@ -6,6 +6,7 @@ import yt_dlp import curses import re from urllib.parse import urljoin +import concurrent.futures DB_DIR = Path.home() / ".animecli" DB_PATH = DB_DIR / "animes.db" @@ -13,6 +14,7 @@ DOWNLOAD_DIR = Path.home() / "MesAnimes" CONFIG = { "vitesse_max": 0, # 0 = illimité, sinon en Ko/s + "telechargements_simultanes": 1 # Nombre de téléchargements simultanés } def init_db(): @@ -131,120 +133,151 @@ def format_dernier(dernier_url, saison): ep = m.group(1) if m else "?" return f"Saison {saison}, épisode {ep}, {dernier_url or 'aucun'}" -def download_anime(conn, stdscr, anime_id=None): - c = conn.cursor() +def telecharger_episode(url, saison_folder, filename, qualite): + ydl_opts = { + "outtmpl": str(saison_folder / filename), + "format": f"bestvideo[height<={qualite.rstrip('p')}]+bestaudio/best", + "downloader": "ffmpeg", + "downloader_args": {"hls": ["--hls-use-mpegts"]} + } + if CONFIG["vitesse_max"] > 0: + ydl_opts["ratelimit"] = CONFIG["vitesse_max"] * 1024 + try: + with yt_dlp.YoutubeDL(ydl_opts) as ydl: + ydl.download([url]) + return True, url + except Exception as e: + print(f"Erreur download {url}: {e}") + return False, url + +def handle_multi_download(stdscr, conn): + animes = get_all_animes(conn) + if not animes: + stdscr.clear() + stdscr.addstr(0, 0, "Aucun anime disponible.") + stdscr.addstr(2, 0, "Appuyez sur une touche pour revenir au menu.") + stdscr.getch() + return + + sel = 0 + while True: + stdscr.clear() + stdscr.addstr(0, 0, "Sélectionnez un anime :") + for idx, (_, titre, _, saison, dernier) in enumerate(animes): + line = f"{titre} (Saison {saison}, dernier: {format_dernier(dernier, saison)})" + if idx == sel: + stdscr.attron(curses.color_pair(1)) + stdscr.addstr(2 + idx, 2, line) + stdscr.attroff(curses.color_pair(1)) + else: + stdscr.addstr(2 + idx, 2, line) + stdscr.refresh() + k = stdscr.getch() + if k == curses.KEY_UP and sel > 0: + sel -= 1 + elif k == curses.KEY_DOWN and sel < len(animes) - 1: + sel += 1 + elif k in [curses.KEY_ENTER, 10, 13]: + break + + anime_id, titre, url_page, saison, dernier = animes[sel] + try: + episode_data = extract_episode_sources(url_page) + except Exception as e: + stdscr.clear() + stdscr.addstr(0, 0, f"Erreur récupération des sources: {e}") + stdscr.getch() + return + if not episode_data: + stdscr.clear() + stdscr.addstr(0, 0, "Aucun épisode trouvé.") + stdscr.getch() + return + + selected = [False] * len(episode_data) + cursor = 0 + while True: + stdscr.clear() + stdscr.addstr(0, 0, "Sélectionnez les épisodes à télécharger (Espace pour cocher, Entrée pour valider) :") + for idx, (sources, _) in enumerate(episode_data): + mark = "[X]" if selected[idx] else "[ ]" + line = f"{mark} Épisode {idx+1}" + if idx == cursor: + stdscr.attron(curses.color_pair(1)) + stdscr.addstr(2 + idx, 2, line) + stdscr.attroff(curses.color_pair(1)) + else: + stdscr.addstr(2 + idx, 2, line) + stdscr.refresh() + k = stdscr.getch() + if k == curses.KEY_UP and cursor > 0: + cursor -= 1 + elif k == curses.KEY_DOWN and cursor < len(episode_data) - 1: + cursor += 1 + elif k == ord(' '): + selected[cursor] = not selected[cursor] + elif k in [curses.KEY_ENTER, 10, 13]: + break + + to_download = [i for i, sel_ in enumerate(selected) if sel_] + if not to_download: + stdscr.clear() + stdscr.addstr(0, 0, "Aucun épisode sélectionné.") + stdscr.getch() + return + qualite = "1080p" - if anime_id: - rows = c.execute( - "SELECT id, titre, url, saison, dernier_episode FROM animes WHERE id = ?", (anime_id,) - ).fetchall() - if not rows: - return False, "Anime introuvable." - else: - rows = c.execute( - "SELECT id, titre, url, saison, dernier_episode FROM animes" - ).fetchall() - messages = [] - for anime_id, titre_anime, url_page, saison, dernier in rows: - messages.append(f"--- Mise à jour '{titre_anime}' ---") - try: - episode_data = extract_episode_sources(url_page) - except Exception as e: - messages.append(f"Erreur récupération des sources : {e}") + base_folder = DOWNLOAD_DIR / titre + download_queue = [] + for idx in to_download: + sources, _ = episode_data[idx] + infos = [] + for src in sources: + info = get_source_info(src) + if info: + infos.append(info) + if not infos: continue - if not episode_data: - messages.append("Aucun lien d'épisode trouvé.") - continue - nouveaux = [] - for sources, _ in episode_data: - if not sources: - continue - if dernier and dernier in sources: - nouveaux = [] - else: - nouveaux.append(sources) - if not nouveaux: - messages.append("Pas de nouvel épisode.") - continue - messages.append(f"Nouveaux épisodes à télécharger ({len(nouveaux)}) :") - base_folder = DOWNLOAD_DIR / titre_anime + sel_src = choisir_source(stdscr, infos) if len(infos) > 1 else 0 + chosen_url = infos[sel_src]['url'] + saison_str = f"S{int(saison):02d}" + ep_str = f"E{idx+1:02d}" + saison_folder = base_folder / f"Saison {int(saison):02d}" + saison_folder.mkdir(parents=True, exist_ok=True) + filename = f"{titre.replace(' ', '_')} - {saison_str}{ep_str}.%(ext)s" + download_queue.append((chosen_url, saison_folder, filename, qualite)) - # Préparer les infos de sources pour tous les épisodes - all_sources_infos = [] - for sources in nouveaux: - infos = [] - for src in sources: - info = get_source_info(src) - if info: - infos.append(info) - all_sources_infos.append(infos) - - # Choix de la source pour tous les épisodes - chosen_idx = None - apply_all = False - for idx, sources_infos in enumerate(all_sources_infos): - if not sources_infos: - messages.append("Aucune info trouvée pour les sources") - continue - if not apply_all: - sel = choisir_source(stdscr, sources_infos) - chosen_idx = sel - # Proposer d'appliquer à tous - stdscr.clear() - stdscr.addstr(0, 0, "Utiliser ce choix pour tous les épisodes suivants ? (o/n)") - stdscr.refresh() - k = stdscr.getch() - if k in [ord('o'), ord('O')]: - apply_all = True + curses.endwin() + with concurrent.futures.ThreadPoolExecutor(max_workers=CONFIG.get("telechargements_simultanes", 2)) as executor: + futures = [ + executor.submit(telecharger_episode, url, folder, fname, qualite) + for url, folder, fname, qualite in download_queue + ] + for future in concurrent.futures.as_completed(futures): + success, url = future.result() + if success: + print(f"Téléchargement réussi: {url}") else: - sel = chosen_idx - chosen_url = sources_infos[sel]['url'] - saison_str = f"S{int(saison):02d}" - ep_str = f"E{idx+1:02d}" - saison_folder = base_folder / f"Saison {int(saison):02d}" - saison_folder.mkdir(parents=True, exist_ok=True) - filename = f"{titre_anime.replace(' ', '_')} - {saison_str}{ep_str}.%(ext)s" - ydl_opts = { - "outtmpl": str(saison_folder / filename), - "format": f"bestvideo[height<={qualite.rstrip('p')}]+bestaudio/best", - "downloader_args": {"hls": ["--hls-use-mpegts"]} - } - if CONFIG["vitesse_max"] > 0: - ydl_opts['ratelimit'] = CONFIG["vitesse_max"] * 1024 - curses.endwin() - try: - with yt_dlp.YoutubeDL(ydl_opts) as ydl: - print(f"Téléchargement: {chosen_url}") - ydl.download([chosen_url]) - dernier = chosen_url - except Exception as e: - print(f"Erreur download {chosen_url}: {e}") - stdscr.refresh() - c.execute( - "UPDATE animes SET dernier_episode = ? WHERE id = ?", (dernier, anime_id) - ) - conn.commit() - messages.append(f"Mise à jour terminée '{titre_anime}'.") - return True, messages + print(f"Echec téléchargement: {url}") def curses_menu(stdscr, conn): curses.curs_set(0) - cursor_y = 2 menu = [ "Ajouter un anime", "Lister les animes", - "Télécharger épisodes", + "Télécharger plusieurs épisodes", "Supprimer un anime", "Configuration", - "Quitter"] - + "Quitter" + ] + cursor_y = 0 while True: stdscr.clear() - stdscr.addstr(0, 2, "animecli - Gestion d'animes (Terminal GUI)", curses.A_BOLD) + stdscr.addstr(0, 2, "animecli - Gestion d'animes", curses.A_BOLD) for idx, item in enumerate(menu): x = 4 y = 2 + idx - if y == cursor_y: + if y - 2 == cursor_y: stdscr.attron(curses.color_pair(1)) stdscr.addstr(y, x, item) stdscr.attroff(curses.color_pair(1)) @@ -252,23 +285,22 @@ def curses_menu(stdscr, conn): stdscr.addstr(y, x, item) stdscr.refresh() k = stdscr.getch() - if k == curses.KEY_UP and cursor_y > 2: + if k == curses.KEY_UP and cursor_y > 0: cursor_y -= 1 - elif k == curses.KEY_DOWN and cursor_y < 2 + len(menu) - 1: + elif k == curses.KEY_DOWN and cursor_y < len(menu) - 1: cursor_y += 1 elif k in [curses.KEY_ENTER, 10, 13]: - choice = cursor_y - 2 - if menu[choice] == "Ajouter un anime": + if menu[cursor_y] == "Ajouter un anime": handle_add(stdscr, conn) - elif menu[choice] == "Lister les animes": + elif menu[cursor_y] == "Lister les animes": handle_list(stdscr, conn) - elif menu[choice] == "Télécharger épisodes": - handle_download(stdscr, conn) - elif menu[choice] == "Supprimer un anime": + elif menu[cursor_y] == "Télécharger plusieurs épisodes": + handle_multi_download(stdscr, conn) + elif menu[cursor_y] == "Supprimer un anime": handle_delete(stdscr, conn) - elif menu[choice] == "Configuration": + elif menu[cursor_y] == "Configuration": handle_config(stdscr) - elif menu[choice] == "Quitter": + elif menu[cursor_y] == "Quitter": break def get_input(stdscr, prompt): @@ -344,6 +376,12 @@ def handle_config(stdscr): if speed_idx is not None: CONFIG["vitesse_max"] = int(speed_options[speed_idx].split()[0]) options[0] = f"Vitesse max: {CONFIG['vitesse_max']} KB/s (0 = illimité)" + elif sel == 1: + sim_options = ["1", "2", "3", "4"] + sim_idx = popup_menu(stdscr, "Téléchargements simultanés :", sim_options) + if sim_idx is not None: + CONFIG["telechargements_simultanes"] = int(sim_options[sim_idx]) + options[1] = f"Téléchargements simultanés: {CONFIG['telechargements_simultanes']}" elif k in [ord('q'), ord('Q')]: break @@ -408,48 +446,6 @@ def handle_delete(stdscr, conn): stdscr.addstr(2, 0, "Appuyez sur une touche pour revenir au menu.") stdscr.getch() -def handle_download(stdscr, conn): - animes = get_all_animes(conn) - options = ["Tous les animes"] + [titre for (_, titre, _, _, _) in animes] - sel = 0 - while True: - stdscr.clear() - stdscr.addstr(0, 0, "Sélectionnez un anime pour mise à jour:") - for idx, title in enumerate(options): - if idx == sel: - stdscr.attron(curses.color_pair(1)) - stdscr.addstr(2 + idx, 2, title) - stdscr.attroff(curses.color_pair(1)) - else: - stdscr.addstr(2 + idx, 2, title) - stdscr.refresh() - k = stdscr.getch() - if k == curses.KEY_UP and sel > 0: - sel -= 1 - elif k == curses.KEY_DOWN and sel < len(options) - 1: - sel += 1 - elif k in [curses.KEY_ENTER, 10, 13]: - break - if sel == 0: - anime_id = None - else: - anime_id = animes[sel - 1][0] - stdscr.clear() - stdscr.addstr(0, 0, "Lancement du téléchargement...") - stdscr.refresh() - success, messages = download_anime(conn, stdscr, anime_id) - stdscr.clear() - if not success: - stdscr.addstr(0, 0, messages) - else: - for idx, line in enumerate(messages, start=0): - if idx >= curses.LINES - 2: - stdscr.addstr(curses.LINES - 1, 0, "--Plus de lignes--") - break - stdscr.addstr(idx, 0, line) - stdscr.addstr(curses.LINES - 1, 0, "Appuyez sur une touche pour revenir au menu.") - stdscr.getch() - def main(): conn = init_db() curses.wrapper(setup_and_run, conn)