Initial commit
This commit is contained in:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal 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
180
README.md
Normal 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
428
app.py
Normal 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
274
downloader.py
Normal 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
5
settings.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"default_output_folder": "downloads",
|
||||
"default_video_quality": "720",
|
||||
"default_audio_quality": "192K"
|
||||
}
|
||||
Reference in New Issue
Block a user