patch
This commit is contained in:
parent
d68fa30c39
commit
77f8e433b2
294
animecli.py
294
animecli.py
@ -6,6 +6,7 @@ import yt_dlp
|
|||||||
import curses
|
import curses
|
||||||
import re
|
import re
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
|
import concurrent.futures
|
||||||
|
|
||||||
DB_DIR = Path.home() / ".animecli"
|
DB_DIR = Path.home() / ".animecli"
|
||||||
DB_PATH = DB_DIR / "animes.db"
|
DB_PATH = DB_DIR / "animes.db"
|
||||||
@ -13,6 +14,7 @@ DOWNLOAD_DIR = Path.home() / "MesAnimes"
|
|||||||
|
|
||||||
CONFIG = {
|
CONFIG = {
|
||||||
"vitesse_max": 0, # 0 = illimité, sinon en Ko/s
|
"vitesse_max": 0, # 0 = illimité, sinon en Ko/s
|
||||||
|
"telechargements_simultanes": 1 # Nombre de téléchargements simultanés
|
||||||
}
|
}
|
||||||
|
|
||||||
def init_db():
|
def init_db():
|
||||||
@ -131,120 +133,151 @@ def format_dernier(dernier_url, saison):
|
|||||||
ep = m.group(1) if m else "?"
|
ep = m.group(1) if m else "?"
|
||||||
return f"Saison {saison}, épisode {ep}, {dernier_url or 'aucun'}"
|
return f"Saison {saison}, épisode {ep}, {dernier_url or 'aucun'}"
|
||||||
|
|
||||||
def download_anime(conn, stdscr, anime_id=None):
|
def telecharger_episode(url, saison_folder, filename, qualite):
|
||||||
c = conn.cursor()
|
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"
|
qualite = "1080p"
|
||||||
if anime_id:
|
base_folder = DOWNLOAD_DIR / titre
|
||||||
rows = c.execute(
|
download_queue = []
|
||||||
"SELECT id, titre, url, saison, dernier_episode FROM animes WHERE id = ?", (anime_id,)
|
for idx in to_download:
|
||||||
).fetchall()
|
sources, _ = episode_data[idx]
|
||||||
if not rows:
|
infos = []
|
||||||
return False, "Anime introuvable."
|
for src in sources:
|
||||||
else:
|
info = get_source_info(src)
|
||||||
rows = c.execute(
|
if info:
|
||||||
"SELECT id, titre, url, saison, dernier_episode FROM animes"
|
infos.append(info)
|
||||||
).fetchall()
|
if not infos:
|
||||||
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}")
|
|
||||||
continue
|
continue
|
||||||
if not episode_data:
|
sel_src = choisir_source(stdscr, infos) if len(infos) > 1 else 0
|
||||||
messages.append("Aucun lien d'épisode trouvé.")
|
chosen_url = infos[sel_src]['url']
|
||||||
continue
|
saison_str = f"S{int(saison):02d}"
|
||||||
nouveaux = []
|
ep_str = f"E{idx+1:02d}"
|
||||||
for sources, _ in episode_data:
|
saison_folder = base_folder / f"Saison {int(saison):02d}"
|
||||||
if not sources:
|
saison_folder.mkdir(parents=True, exist_ok=True)
|
||||||
continue
|
filename = f"{titre.replace(' ', '_')} - {saison_str}{ep_str}.%(ext)s"
|
||||||
if dernier and dernier in sources:
|
download_queue.append((chosen_url, saison_folder, filename, qualite))
|
||||||
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
|
|
||||||
|
|
||||||
# Préparer les infos de sources pour tous les épisodes
|
curses.endwin()
|
||||||
all_sources_infos = []
|
with concurrent.futures.ThreadPoolExecutor(max_workers=CONFIG.get("telechargements_simultanes", 2)) as executor:
|
||||||
for sources in nouveaux:
|
futures = [
|
||||||
infos = []
|
executor.submit(telecharger_episode, url, folder, fname, qualite)
|
||||||
for src in sources:
|
for url, folder, fname, qualite in download_queue
|
||||||
info = get_source_info(src)
|
]
|
||||||
if info:
|
for future in concurrent.futures.as_completed(futures):
|
||||||
infos.append(info)
|
success, url = future.result()
|
||||||
all_sources_infos.append(infos)
|
if success:
|
||||||
|
print(f"Téléchargement réussi: {url}")
|
||||||
# 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
|
|
||||||
else:
|
else:
|
||||||
sel = chosen_idx
|
print(f"Echec téléchargement: {url}")
|
||||||
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
|
|
||||||
|
|
||||||
def curses_menu(stdscr, conn):
|
def curses_menu(stdscr, conn):
|
||||||
curses.curs_set(0)
|
curses.curs_set(0)
|
||||||
cursor_y = 2
|
|
||||||
menu = [
|
menu = [
|
||||||
"Ajouter un anime",
|
"Ajouter un anime",
|
||||||
"Lister les animes",
|
"Lister les animes",
|
||||||
"Télécharger épisodes",
|
"Télécharger plusieurs épisodes",
|
||||||
"Supprimer un anime",
|
"Supprimer un anime",
|
||||||
"Configuration",
|
"Configuration",
|
||||||
"Quitter"]
|
"Quitter"
|
||||||
|
]
|
||||||
|
cursor_y = 0
|
||||||
while True:
|
while True:
|
||||||
stdscr.clear()
|
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):
|
for idx, item in enumerate(menu):
|
||||||
x = 4
|
x = 4
|
||||||
y = 2 + idx
|
y = 2 + idx
|
||||||
if y == cursor_y:
|
if y - 2 == cursor_y:
|
||||||
stdscr.attron(curses.color_pair(1))
|
stdscr.attron(curses.color_pair(1))
|
||||||
stdscr.addstr(y, x, item)
|
stdscr.addstr(y, x, item)
|
||||||
stdscr.attroff(curses.color_pair(1))
|
stdscr.attroff(curses.color_pair(1))
|
||||||
@ -252,23 +285,22 @@ def curses_menu(stdscr, conn):
|
|||||||
stdscr.addstr(y, x, item)
|
stdscr.addstr(y, x, item)
|
||||||
stdscr.refresh()
|
stdscr.refresh()
|
||||||
k = stdscr.getch()
|
k = stdscr.getch()
|
||||||
if k == curses.KEY_UP and cursor_y > 2:
|
if k == curses.KEY_UP and cursor_y > 0:
|
||||||
cursor_y -= 1
|
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
|
cursor_y += 1
|
||||||
elif k in [curses.KEY_ENTER, 10, 13]:
|
elif k in [curses.KEY_ENTER, 10, 13]:
|
||||||
choice = cursor_y - 2
|
if menu[cursor_y] == "Ajouter un anime":
|
||||||
if menu[choice] == "Ajouter un anime":
|
|
||||||
handle_add(stdscr, conn)
|
handle_add(stdscr, conn)
|
||||||
elif menu[choice] == "Lister les animes":
|
elif menu[cursor_y] == "Lister les animes":
|
||||||
handle_list(stdscr, conn)
|
handle_list(stdscr, conn)
|
||||||
elif menu[choice] == "Télécharger épisodes":
|
elif menu[cursor_y] == "Télécharger plusieurs épisodes":
|
||||||
handle_download(stdscr, conn)
|
handle_multi_download(stdscr, conn)
|
||||||
elif menu[choice] == "Supprimer un anime":
|
elif menu[cursor_y] == "Supprimer un anime":
|
||||||
handle_delete(stdscr, conn)
|
handle_delete(stdscr, conn)
|
||||||
elif menu[choice] == "Configuration":
|
elif menu[cursor_y] == "Configuration":
|
||||||
handle_config(stdscr)
|
handle_config(stdscr)
|
||||||
elif menu[choice] == "Quitter":
|
elif menu[cursor_y] == "Quitter":
|
||||||
break
|
break
|
||||||
|
|
||||||
def get_input(stdscr, prompt):
|
def get_input(stdscr, prompt):
|
||||||
@ -344,6 +376,12 @@ def handle_config(stdscr):
|
|||||||
if speed_idx is not None:
|
if speed_idx is not None:
|
||||||
CONFIG["vitesse_max"] = int(speed_options[speed_idx].split()[0])
|
CONFIG["vitesse_max"] = int(speed_options[speed_idx].split()[0])
|
||||||
options[0] = f"Vitesse max: {CONFIG['vitesse_max']} KB/s (0 = illimité)"
|
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')]:
|
elif k in [ord('q'), ord('Q')]:
|
||||||
break
|
break
|
||||||
|
|
||||||
@ -408,48 +446,6 @@ def handle_delete(stdscr, conn):
|
|||||||
stdscr.addstr(2, 0, "Appuyez sur une touche pour revenir au menu.")
|
stdscr.addstr(2, 0, "Appuyez sur une touche pour revenir au menu.")
|
||||||
stdscr.getch()
|
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():
|
def main():
|
||||||
conn = init_db()
|
conn = init_db()
|
||||||
curses.wrapper(setup_and_run, conn)
|
curses.wrapper(setup_and_run, conn)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user