Initial commit

This commit is contained in:
2026-05-30 14:11:33 +01:00
commit 84d23b739f
5 changed files with 911 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
@"
.venv/
__pycache__/
*.pyc
*.pyo
*.pyd
build/
dist/
*.spec
installer_output/
downloads/
*.part
*.tmp
*.log
.idea/
.vscode/
Thumbs.db
.DS_Store
"@ | Out-File -Encoding utf8 .gitignore

180
README.md Normal file
View File

@@ -0,0 +1,180 @@
# YouTube Downloader
**Version : 1.0.0**
**Développé par : IM TECH**
**Plateforme : Windows**
YouTube Downloader est une application Windows simple et pratique permettant de télécharger, découper et convertir des contenus audio ou vidéo à partir d'une URL.
L'application permet de télécharger une vidéo complète, un extrait vidéo, un fichier audio MP3 complet ou un extrait audio MP3, avec choix de la qualité, du nom du fichier et du dossier de sortie.
---
## Fonctionnalités
- Télécharger une vidéo complète en MP4
- Télécharger un extrait vidéo en MP4
- Télécharger un audio complet en MP3
- Télécharger un extrait audio en MP3
- Choisir la qualité vidéo : 480p, 720p, 1080p ou meilleure qualité disponible
- Choisir la qualité audio : 128K, 192K, 256K ou 320K
- Choisir le dossier de sortie
- Nommer le fichier de sortie
- Afficher le journal de téléchargement
- Annuler un téléchargement en cours
- Écran “À propos / Disclaimer” intégré
---
## Utilisation
1. Lancez l'application `YoutubeDownloader.exe`.
2. Collez l'URL dans le champ prévu.
3. Choisissez le mode de téléchargement :
- Vidéo complète MP4
- Extrait vidéo MP4
- Audio complet MP3
- Extrait audio MP3
4. Si vous choisissez un extrait, indiquez le temps de début et le temps de fin.
5. Choisissez le nom du fichier.
6. Sélectionnez le dossier de sortie.
7. Cliquez sur **Télécharger**.
---
## Exemple pour télécharger un extrait vidéo
Pour télécharger un extrait vidéo de 29 secondes :
```text
Mode : Extrait vidéo MP4
Début : 00:05:00
Fin : 00:05:29
Qualité vidéo : 720p
Nom du fichier : extrait_video
Exemple pour télécharger un extrait audio MP3
Pour télécharger un extrait audio MP3 de 29 secondes :
Mode : Extrait audio MP3
Début : 00:05:00
Fin : 00:05:29
Qualité audio : 192K
Nom du fichier : extrait_audio
Le fichier généré sera enregistré dans le dossier de sortie choisi.
Dossier de téléchargement par défaut
Par défaut, les fichiers téléchargés sont enregistrés dans :
C:\Users\VotreNom\Downloads\Youtube_Downloader
L'utilisateur peut choisir un autre dossier directement depuis l'application.
Version portable
Si vous utilisez la version portable, ne lancez pas uniquement le fichier .exe isolé.
Il faut conserver tout le dossier complet :
YoutubeDownloader/
├── YoutubeDownloader.exe
├── _internal/
└── autres fichiers nécessaires
L'application a besoin de ses fichiers internes pour fonctionner correctement.
Version installable
Si vous utilisez la version installable :
Lancez le fichier :
YoutubeDownloader_Setup_v1.0.0.exe
Suivez les étapes de l'installation.
Lancez l'application depuis le menu Démarrer ou depuis le raccourci bureau si vous l'avez créé.
Outils utilisés
Cette application utilise des outils externes pour effectuer les opérations de téléchargement, découpage, conversion et fusion de fichiers média :
yt-dlp pour la récupération des contenus audio et vidéo
FFmpeg pour la conversion, la fusion et le découpage des fichiers média
FFprobe, si disponible, pour l'analyse des fichiers média
Ces outils restent la propriété de leurs auteurs respectifs.
Disclaimer important
Ce logiciel est fourni uniquement comme outil technique local.
L'utilisateur est seul responsable :
des liens qu'il utilise ;
des contenus qu'il télécharge ;
des fichiers qu'il convertit ;
de l'utilisation, publication ou redistribution des fichiers obtenus.
Ce logiciel doit être utilisé uniquement pour :
vos propres vidéos ;
des contenus libres de droits ;
des contenus sous licence autorisant le téléchargement ou la réutilisation ;
des contenus pour lesquels vous avez une autorisation explicite ;
des usages permis par la loi applicable dans votre pays.
Ce logiciel n'est pas destiné à :
violer les droits d'auteur ;
contourner des protections techniques ;
redistribuer illégalement des vidéos, musiques ou extraits ;
télécharger des contenus sans autorisation lorsque cela est interdit.
L'utilisateur doit respecter les conditions d'utilisation des plateformes concernées, les droits d'auteur et les lois applicables dans son pays.
Affiliation
Ce logiciel n'est pas affilié, associé, approuvé ou sponsorisé par YouTube, Google, yt-dlp ou FFmpeg.
Les marques, logos et noms cités appartiennent à leurs propriétaires respectifs.
Responsabilité
IM TECH ne peut être tenu responsable d'une utilisation illégale, abusive ou non autorisée de ce logiciel.
L'application est fournie en l'état, sans garantie de fonctionnement permanent.
Certaines plateformes peuvent modifier leur fonctionnement, leurs restrictions ou leurs méthodes techniques, ce qui peut affecter temporairement ou définitivement le téléchargement de certains contenus.
Limites connues
Certains liens peuvent ne pas fonctionner selon les restrictions de la plateforme.
Certaines vidéos peuvent nécessiter une authentification ou être indisponibles selon le pays.
La vitesse de téléchargement dépend de la connexion Internet, de la taille du fichier et du format choisi.
La conversion MP3 ou MP4 peut prendre plus de temps selon la durée du fichier et la puissance de l'ordinateur.
Structure de la version portable
La version portable doit être conservée sous forme de dossier complet :
YoutubeDownloader/
├── YoutubeDownloader.exe
├── _internal/
│ ├── bin/
│ │ ├── yt-dlp.exe
│ │ ├── ffmpeg.exe
│ │ └── ffprobe.exe
│ └── fichiers internes
Ne supprimez pas le dossier _internal.
Informations de version
Nom : YouTube Downloader
Version : 1.0.0
Développeur : IM TECH
Plateforme : Windows
Type : Application de téléchargement et conversion média
Support
Pour toute remarque, correction ou amélioration, contactez le développeur du logiciel.
Droits
© IM TECH - Tous droits réservés.

428
app.py Normal file
View File

@@ -0,0 +1,428 @@
import queue
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from downloader import (
DOWNLOADS_DIR,
build_command,
load_settings,
validate_tools,
CommandRunner,
)
APP_VERSION = "1.0.0"
ABOUT_TEXT = f"""
YouTube Downloader
Version {APP_VERSION}
Développé par IM TECH
Cet outil permet de télécharger, découper et convertir des contenus audio ou vidéo
à partir d'une URL, selon les options choisies par l'utilisateur.
Fonctions principales :
- Télécharger une vidéo complète en MP4
- Télécharger un extrait vidéo en MP4
- Télécharger un audio complet en MP3
- Télécharger un extrait audio en MP3
- Choisir la qualité vidéo et audio
- Choisir le dossier de sortie
IMPORTANT - UTILISATION RESPONSABLE
Ce logiciel est fourni uniquement comme outil technique local.
L'utilisateur est seul responsable :
- des liens qu'il utilise ;
- des contenus qu'il télécharge ;
- des fichiers qu'il convertit ;
- de l'utilisation, publication ou redistribution des fichiers obtenus.
Ce logiciel doit être utilisé uniquement pour :
- vos propres vidéos ;
- des contenus libres de droits ;
- des contenus sous licence autorisant le téléchargement ou la réutilisation ;
- des contenus pour lesquels vous avez une autorisation explicite ;
- des usages permis par la loi applicable dans votre pays.
Ce logiciel n'est pas destiné à :
- violer les droits d'auteur ;
- contourner des protections techniques ;
- redistribuer illégalement des vidéos, musiques ou extraits ;
- télécharger des contenus sans autorisation lorsque cela est interdit.
Ce logiciel n'est pas affilié, associé, approuvé ou sponsorisé par YouTube,
Google, yt-dlp ou FFmpeg.
Les marques, logos et noms cités appartiennent à leurs propriétaires respectifs.
En utilisant ce logiciel, vous acceptez d'en faire un usage légal,
personnel et responsable.
"""
MODE_LABELS = {
"Vidéo complète MP4": "video_full",
"Extrait vidéo MP4": "video_extract",
"Audio complet MP3": "audio_full",
"Extrait audio MP3": "audio_extract",
}
class YouTubeDownloaderApp:
def __init__(self, root: tk.Tk):
self.root = root
self.root.title("YouTube Downloader")
self.root.geometry("900x650")
self.root.minsize(820, 600)
self.settings = load_settings()
self.runner = CommandRunner()
self.log_queue = queue.Queue()
self.url_var = tk.StringVar()
self.mode_var = tk.StringVar(value="Extrait audio MP3")
self.start_var = tk.StringVar(value="00:00:00")
self.end_var = tk.StringVar(value="00:00:29")
self.filename_var = tk.StringVar(value="extrait_YT_Downloader")
self.output_var = tk.StringVar(value=str(DOWNLOADS_DIR))
self.video_quality_var = tk.StringVar(
value=self.settings.get("default_video_quality", "720")
)
self.audio_quality_var = tk.StringVar(
value=self.settings.get("default_audio_quality", "192K")
)
self.build_menu()
self.build_ui()
self.check_tools_on_start()
self.update_extract_fields()
self.poll_log_queue()
def build_menu(self):
menu_bar = tk.Menu(self.root)
help_menu = tk.Menu(menu_bar, tearoff=0)
help_menu.add_command(label="À propos / Disclaimer", command=self.show_about)
menu_bar.add_cascade(label="Aide", menu=help_menu)
self.root.config(menu=menu_bar)
def build_ui(self):
main = ttk.Frame(self.root, padding=20)
main.pack(fill="both", expand=True)
header = ttk.Frame(main)
header.pack(fill="x", pady=(0, 20))
title_block = ttk.Frame(header)
title_block.pack(side="left", fill="x", expand=True)
title = ttk.Label(
title_block,
text="YouTube Downloader",
font=("Segoe UI", 20, "bold"),
)
title.pack(anchor="w")
subtitle = ttk.Label(
title_block,
text="Télécharger une vidéo complète, un extrait, un MP4 ou un MP3.",
font=("Segoe UI", 10),
)
subtitle.pack(anchor="w")
about_button = ttk.Button(
header,
text="À propos",
command=self.show_about,
)
about_button.pack(side="right", padx=(15, 0))
form = ttk.Frame(main)
form.pack(fill="x")
ttk.Label(form, text="URL YouTube").grid(row=0, column=0, sticky="w")
url_entry = ttk.Entry(form, textvariable=self.url_var)
url_entry.grid(row=1, column=0, columnspan=4, sticky="ew", pady=(5, 15))
ttk.Label(form, text="Mode").grid(row=2, column=0, sticky="w")
mode_combo = ttk.Combobox(
form,
textvariable=self.mode_var,
values=list(MODE_LABELS.keys()),
state="readonly",
)
mode_combo.grid(row=3, column=0, sticky="ew", pady=(5, 15))
mode_combo.bind(
"<<ComboboxSelected>>",
lambda event: self.update_extract_fields(),
)
ttk.Label(form, text="Début").grid(row=2, column=1, sticky="w", padx=(15, 0))
self.start_entry = ttk.Entry(form, textvariable=self.start_var)
self.start_entry.grid(row=3, column=1, sticky="ew", padx=(15, 0), pady=(5, 15))
ttk.Label(form, text="Fin").grid(row=2, column=2, sticky="w", padx=(15, 0))
self.end_entry = ttk.Entry(form, textvariable=self.end_var)
self.end_entry.grid(row=3, column=2, sticky="ew", padx=(15, 0), pady=(5, 15))
ttk.Label(form, text="Nom du fichier").grid(row=4, column=0, sticky="w")
filename_entry = ttk.Entry(form, textvariable=self.filename_var)
filename_entry.grid(row=5, column=0, columnspan=2, sticky="ew", pady=(5, 15))
ttk.Label(form, text="Qualité vidéo").grid(row=4, column=2, sticky="w", padx=(15, 0))
video_quality_combo = ttk.Combobox(
form,
textvariable=self.video_quality_var,
values=["480", "720", "1080", "best"],
state="readonly",
)
video_quality_combo.grid(row=5, column=2, sticky="ew", padx=(15, 0), pady=(5, 15))
ttk.Label(form, text="Qualité audio").grid(row=4, column=3, sticky="w", padx=(15, 0))
audio_quality_combo = ttk.Combobox(
form,
textvariable=self.audio_quality_var,
values=["128K", "192K", "256K", "320K"],
state="readonly",
)
audio_quality_combo.grid(row=5, column=3, sticky="ew", padx=(15, 0), pady=(5, 15))
ttk.Label(form, text="Dossier de sortie").grid(row=6, column=0, sticky="w")
output_entry = ttk.Entry(form, textvariable=self.output_var)
output_entry.grid(row=7, column=0, columnspan=3, sticky="ew", pady=(5, 15))
browse_button = ttk.Button(
form,
text="Choisir...",
command=self.choose_output_folder,
)
browse_button.grid(row=7, column=3, sticky="ew", padx=(15, 0), pady=(5, 15))
form.columnconfigure(0, weight=2)
form.columnconfigure(1, weight=1)
form.columnconfigure(2, weight=1)
form.columnconfigure(3, weight=1)
buttons = ttk.Frame(main)
buttons.pack(fill="x", pady=(5, 15))
self.download_button = ttk.Button(
buttons,
text="Télécharger",
command=self.start_download,
)
self.download_button.pack(side="left")
self.cancel_button = ttk.Button(
buttons,
text="Annuler",
command=self.cancel_download,
state="disabled",
)
self.cancel_button.pack(side="left", padx=(10, 0))
self.status_label = ttk.Label(
buttons,
text="Prêt.",
)
self.status_label.pack(side="left", padx=(20, 0))
log_frame = ttk.LabelFrame(main, text="Journal")
log_frame.pack(fill="both", expand=True)
self.log_text = tk.Text(
log_frame,
height=15,
wrap="word",
font=("Consolas", 10),
)
self.log_text.pack(side="left", fill="both", expand=True)
scrollbar = ttk.Scrollbar(
log_frame,
orient="vertical",
command=self.log_text.yview,
)
scrollbar.pack(side="right", fill="y")
self.log_text.configure(yscrollcommand=scrollbar.set)
def show_about(self):
about_window = tk.Toplevel(self.root)
about_window.title("À propos - YouTube Downloader")
about_window.geometry("720x560")
about_window.minsize(620, 460)
about_window.transient(self.root)
about_window.grab_set()
container = ttk.Frame(about_window, padding=20)
container.pack(fill="both", expand=True)
title = ttk.Label(
container,
text="À propos / Disclaimer",
font=("Segoe UI", 16, "bold"),
)
title.pack(anchor="w", pady=(0, 10))
text_frame = ttk.Frame(container)
text_frame.pack(fill="both", expand=True)
about_text = tk.Text(
text_frame,
wrap="word",
font=("Segoe UI", 10),
height=20,
)
about_text.pack(side="left", fill="both", expand=True)
scrollbar = ttk.Scrollbar(
text_frame,
orient="vertical",
command=about_text.yview,
)
scrollbar.pack(side="right", fill="y")
about_text.configure(yscrollcommand=scrollbar.set)
about_text.insert("1.0", ABOUT_TEXT.strip())
about_text.configure(state="disabled")
close_button = ttk.Button(
container,
text="Fermer",
command=about_window.destroy,
)
close_button.pack(anchor="e", pady=(15, 0))
def check_tools_on_start(self):
DOWNLOADS_DIR.mkdir(parents=True, exist_ok=True)
errors = validate_tools()
if errors:
self.add_log("Outils manquants :")
for error in errors:
self.add_log(f"- {error}")
self.add_log("")
self.add_log("Ajoute yt-dlp.exe et ffmpeg.exe dans le dossier bin.")
else:
self.add_log("yt-dlp et FFmpeg détectés correctement.")
def update_extract_fields(self):
mode = MODE_LABELS[self.mode_var.get()]
is_extract = mode in ["video_extract", "audio_extract"]
state = "normal" if is_extract else "disabled"
self.start_entry.configure(state=state)
self.end_entry.configure(state=state)
def choose_output_folder(self):
folder = filedialog.askdirectory()
if folder:
self.output_var.set(folder)
def start_download(self):
url = self.url_var.get().strip()
mode_label = self.mode_var.get()
mode = MODE_LABELS[mode_label]
if not url:
messagebox.showerror("Erreur", "Colle d'abord une URL YouTube.")
return
errors = validate_tools()
if errors:
messagebox.showerror("Outils manquants", "\n".join(errors))
return
try:
command = build_command(
url=url,
mode=mode,
output_folder=self.output_var.get().strip(),
filename=self.filename_var.get().strip(),
start_time=self.start_var.get().strip(),
end_time=self.end_var.get().strip(),
video_quality=self.video_quality_var.get(),
audio_quality=self.audio_quality_var.get(),
)
except Exception as error:
messagebox.showerror("Erreur", str(error))
return
self.download_button.configure(state="disabled")
self.cancel_button.configure(state="normal")
self.status_label.configure(text="Téléchargement en cours...")
self.add_log("")
self.add_log("Commande lancée :")
self.add_log(" ".join(command))
self.add_log("")
self.runner.run(
command=command,
on_line=self.queue_log,
on_finish=self.queue_finish,
)
def cancel_download(self):
self.runner.cancel()
self.status_label.configure(text="Annulation...")
def queue_log(self, line: str):
self.log_queue.put(("log", line))
def queue_finish(self, success: bool, message: str):
self.log_queue.put(("finish", success, message))
def poll_log_queue(self):
try:
while True:
item = self.log_queue.get_nowait()
if item[0] == "log":
self.add_log(item[1])
elif item[0] == "finish":
success, message = item[1], item[2]
self.add_log("")
self.add_log(message)
self.download_button.configure(state="normal")
self.cancel_button.configure(state="disabled")
if success:
self.status_label.configure(text="Terminé.")
else:
self.status_label.configure(text="Arrêté ou erreur.")
except queue.Empty:
pass
self.root.after(100, self.poll_log_queue)
def add_log(self, text: str):
self.log_text.insert("end", text + "\n")
self.log_text.see("end")
def main():
root = tk.Tk()
YouTubeDownloaderApp(root)
root.mainloop()
if __name__ == "__main__":
main()

274
downloader.py Normal file
View File

@@ -0,0 +1,274 @@
from __future__ import annotations
import json
import os
import subprocess
import sys
import threading
from pathlib import Path
from typing import Callable
# ------------------------------------------------------------
# Gestion des chemins
# ------------------------------------------------------------
# En mode développement :
# APP_DIR = dossier du projet
# RESOURCE_DIR = dossier du projet
#
# En mode .exe PyInstaller :
# APP_DIR = dossier où se trouve YoutubeDownloader.exe
# RESOURCE_DIR = dossier temporaire/interne utilisé par PyInstaller
# ------------------------------------------------------------
if getattr(sys, "frozen", False):
APP_DIR = Path(sys.executable).resolve().parent
RESOURCE_DIR = Path(getattr(sys, "_MEIPASS", APP_DIR))
else:
APP_DIR = Path(__file__).resolve().parent
RESOURCE_DIR = APP_DIR
BASE_DIR = APP_DIR
BIN_DIR = RESOURCE_DIR / "bin"
# Les téléchargements iront dans le dossier Downloads de l'utilisateur.
# C'est plus propre pour une app installée.
DOWNLOADS_DIR = Path.home() / "Downloads" / "Youtube_Downloader"
SETTINGS_FILE = RESOURCE_DIR / "settings.json"
YTDLP_EXE = BIN_DIR / "yt-dlp.exe"
FFMPEG_EXE = BIN_DIR / "ffmpeg.exe"
FFPROBE_EXE = BIN_DIR / "ffprobe.exe"
def load_settings() -> dict:
"""
Charge les paramètres depuis settings.json.
Si le fichier est absent ou cassé, on utilise des valeurs par défaut.
"""
default_settings = {
"default_output_folder": "downloads",
"default_video_quality": "720",
"default_audio_quality": "192K",
}
if not SETTINGS_FILE.exists():
return default_settings
try:
with open(SETTINGS_FILE, "r", encoding="utf-8") as file:
settings = json.load(file)
return {
"default_output_folder": settings.get(
"default_output_folder",
default_settings["default_output_folder"],
),
"default_video_quality": settings.get(
"default_video_quality",
default_settings["default_video_quality"],
),
"default_audio_quality": settings.get(
"default_audio_quality",
default_settings["default_audio_quality"],
),
}
except Exception:
return default_settings
def validate_tools() -> list[str]:
"""
Vérifie que yt-dlp.exe et ffmpeg.exe existent dans le dossier bin.
"""
errors = []
if not YTDLP_EXE.exists():
errors.append("yt-dlp.exe est introuvable dans le dossier bin.")
if not FFMPEG_EXE.exists():
errors.append("ffmpeg.exe est introuvable dans le dossier bin.")
# ffprobe est utile mais pas toujours obligatoire.
# On ne bloque pas l'application s'il est absent.
return errors
def clean_filename(filename: str) -> str:
"""
Nettoie le nom du fichier pour éviter les caractères interdits sous Windows.
"""
forbidden_chars = '<>:"/\\|?*'
cleaned = "".join("_" if char in forbidden_chars else char for char in filename)
cleaned = cleaned.strip()
if not cleaned:
return "telechargement"
return cleaned
def resolve_output_folder(output_folder: str) -> Path:
"""
Prépare le dossier de sortie.
Si le chemin est relatif, il est créé dans le dossier de l'application.
"""
if not output_folder.strip():
folder = DOWNLOADS_DIR
else:
folder = Path(output_folder)
if not folder.is_absolute():
folder = BASE_DIR / folder
folder.mkdir(parents=True, exist_ok=True)
return folder
def build_command(
url: str,
mode: str,
output_folder: str,
filename: str,
start_time: str = "",
end_time: str = "",
video_quality: str = "720",
audio_quality: str = "192K",
) -> list[str]:
"""
Construit la commande yt-dlp selon le mode choisi.
"""
if not url.strip():
raise ValueError("L'URL YouTube est obligatoire.")
filename = clean_filename(filename)
folder = resolve_output_folder(output_folder)
output_template = str(folder / f"{filename}.%(ext)s")
cmd = [
str(YTDLP_EXE),
url.strip(),
"--newline",
"--no-playlist",
"--ffmpeg-location",
str(BIN_DIR),
]
# Gestion des extraits
if mode in ["video_extract", "audio_extract"]:
if not start_time.strip() or not end_time.strip():
raise ValueError("Pour un extrait, il faut indiquer le début et la fin.")
cmd += [
"--download-sections",
f"*{start_time.strip()}-{end_time.strip()}",
]
# Vidéo MP4
if mode in ["video_full", "video_extract"]:
if video_quality == "best":
format_selector = "bv*[ext=mp4]+ba[ext=m4a]/b[ext=mp4]/best"
else:
format_selector = (
f"bv*[height<={video_quality}][ext=mp4]+ba[ext=m4a]/"
f"b[height<={video_quality}][ext=mp4]/best"
)
cmd += [
"-f",
format_selector,
"--merge-output-format",
"mp4",
]
# Audio MP3
elif mode in ["audio_full", "audio_extract"]:
cmd += [
"-f",
"ba/bestaudio",
"-x",
"--audio-format",
"mp3",
"--audio-quality",
audio_quality,
]
else:
raise ValueError("Mode de téléchargement inconnu.")
cmd += [
"-o",
output_template,
]
return cmd
class CommandRunner:
"""
Lance yt-dlp dans un thread séparé pour éviter de bloquer l'interface.
"""
def __init__(self):
self.process: subprocess.Popen | None = None
self.cancelled = False
def run(
self,
command: list[str],
on_line: Callable[[str], None],
on_finish: Callable[[bool, str], None],
) -> None:
self.cancelled = False
def worker():
try:
creation_flags = 0
if os.name == "nt":
creation_flags = subprocess.CREATE_NO_WINDOW
self.process = subprocess.Popen(
command,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
encoding="utf-8",
errors="replace",
creationflags=creation_flags,
)
if self.process.stdout:
for line in self.process.stdout:
on_line(line.rstrip())
return_code = self.process.wait()
if self.cancelled:
on_finish(False, "Téléchargement annulé.")
elif return_code == 0:
on_finish(True, "Téléchargement terminé avec succès.")
else:
on_finish(False, f"Erreur pendant le téléchargement. Code : {return_code}")
except FileNotFoundError:
on_finish(False, "Impossible de lancer yt-dlp ou FFmpeg.")
except Exception as error:
on_finish(False, f"Erreur : {error}")
thread = threading.Thread(target=worker, daemon=True)
thread.start()
def cancel(self) -> None:
self.cancelled = True
if self.process and self.process.poll() is None:
try:
self.process.terminate()
except Exception:
pass

5
settings.json Normal file
View File

@@ -0,0 +1,5 @@
{
"default_output_folder": "downloads",
"default_video_quality": "720",
"default_audio_quality": "192K"
}