Inicial, descargar y conversion basicas
This commit is contained in:
45
.gitignore
vendored
Normal file
45
.gitignore
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# Entornos virtuales
|
||||
venv/
|
||||
.venv/
|
||||
env/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Binarios y ejecutables descargados localmente (FFmpeg/FFprobe)
|
||||
bin/ffmpeg.exe
|
||||
bin/ffprobe.exe
|
||||
bin/ffplay.exe
|
||||
bin/*.exe
|
||||
bin/*.dll
|
||||
|
||||
# Archivos multimedia de pruebas (Entrada y Salida)
|
||||
*.mp4
|
||||
*.mkv
|
||||
*.avi
|
||||
*.mov
|
||||
*.mp3
|
||||
*.wav
|
||||
*.flac
|
||||
*.m4a
|
||||
*.webm
|
||||
|
||||
# Configuración e IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Distribución / Compilación (PyInstaller / CX_Freeze / Nuitka)
|
||||
build/
|
||||
dist/
|
||||
*.spec
|
||||
|
||||
# Archivos de sistema operativo
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
1
README.md
Normal file
1
README.md
Normal file
@@ -0,0 +1 @@
|
||||
Aplicación de escritorio desarrollada en Python y PyQt6 que actúa como una interfaz gráfica sobre FFmpeg para facilitar la conversión rápida de archivos multimedia. Permite exportar vídeo MP4 (estándar y escalado automático a máximo 1080p), extraer audio MP3 estéreo a 192 kbps y aislar los canales izquierdo/derecho (Canal 1 y Canal 2) en pistas monocanal independientes.
|
||||
0
gui/__init__.py
Normal file
0
gui/__init__.py
Normal file
63
gui/main_window.py
Normal file
63
gui/main_window.py
Normal file
@@ -0,0 +1,63 @@
|
||||
from PyQt6.QtWidgets import (QMainWindow, QTabWidget, QProgressBar,
|
||||
QStatusBar, QMessageBox)
|
||||
from gui.tab_youtube import YoutubeTab
|
||||
from gui.tab_converter import ConverterTab # <-- Nueva importación
|
||||
from utils.updater import DependencyChecker
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle("seteTools - Media")
|
||||
self.resize(600, 350)
|
||||
|
||||
self.init_menu()
|
||||
self.init_tabs()
|
||||
self.init_statusbar()
|
||||
self.check_dependencies()
|
||||
|
||||
def init_menu(self):
|
||||
menubar = self.menuBar()
|
||||
menu_actualizaciones = menubar.addMenu("Actualizaciones")
|
||||
|
||||
check_action = menu_actualizaciones.addAction("Buscar actualizaciones de binarios")
|
||||
check_action.triggered.connect(self.force_update_check)
|
||||
|
||||
def init_tabs(self):
|
||||
self.tabs = QTabWidget()
|
||||
|
||||
# Pestaña 1: Descarga YouTube
|
||||
self.youtube_tab = YoutubeTab()
|
||||
self.tabs.addTab(self.youtube_tab, "Descarga YouTube")
|
||||
|
||||
# Pestaña 2: Conversión con FFmpeg
|
||||
self.converter_tab = ConverterTab()
|
||||
self.tabs.addTab(self.converter_tab, "Convertidor FFmpeg")
|
||||
|
||||
self.setCentralWidget(self.tabs)
|
||||
|
||||
def init_statusbar(self):
|
||||
self.statusBar = QStatusBar()
|
||||
self.setStatusBar(self.statusBar)
|
||||
self.progress_bar = QProgressBar()
|
||||
self.progress_bar.setMaximumWidth(200)
|
||||
self.progress_bar.hide()
|
||||
self.statusBar.addPermanentWidget(self.progress_bar)
|
||||
|
||||
def check_dependencies(self):
|
||||
self.thread = DependencyChecker()
|
||||
self.thread.progress_signal.connect(self.update_progress)
|
||||
self.thread.finished_signal.connect(self.on_check_finished)
|
||||
|
||||
self.progress_bar.show()
|
||||
self.thread.start()
|
||||
|
||||
def update_progress(self, message, percent):
|
||||
self.statusBar.showMessage(message)
|
||||
self.progress_bar.setValue(percent)
|
||||
|
||||
def on_check_finished(self):
|
||||
self.progress_bar.hide()
|
||||
|
||||
def force_update_check(self):
|
||||
QMessageBox.information(self, "Actualizaciones", "Verificando componentes...")
|
||||
self.check_dependencies()
|
||||
141
gui/tab_converter.py
Normal file
141
gui/tab_converter.py
Normal file
@@ -0,0 +1,141 @@
|
||||
import os
|
||||
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout,
|
||||
QLabel, QLineEdit, QPushButton, QComboBox,
|
||||
QFileDialog, QMessageBox)
|
||||
from utils.converter import MediaConverter
|
||||
|
||||
class ConverterTab(QWidget):
|
||||
PRESETS = {
|
||||
"MP4 Estándar (H.264 + AAC)": {"key": "mp4_std", "ext": ".mp4", "tag": "_converted"},
|
||||
"MP4 Estándar (Máx. 1080p Vertical)": {"key": "mp4_1080p", "ext": ".mp4", "tag": "_1080p"},
|
||||
"Audio MP3 Estándar (Estéreo 192 kbps)": {"key": "mp3_192k", "ext": ".mp3", "tag": "_audio"},
|
||||
"Audio MP3 - Solo Canal 1 / Izquierdo (192 kbps)": {"key": "mp3_ch1", "ext": ".mp3", "tag": "_ch1"},
|
||||
"Audio MP3 - Solo Canal 2 / Derecho (192 kbps)": {"key": "mp3_ch2", "ext": ".mp3", "tag": "_ch2"}
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.converter_thread = None
|
||||
self.initUI()
|
||||
|
||||
def initUI(self):
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# Archivo de entrada
|
||||
layout.addWidget(QLabel("Archivo de entrada (In):"))
|
||||
in_layout = QHBoxLayout()
|
||||
self.in_input = QLineEdit()
|
||||
self.in_input.setPlaceholderText("Selecciona un archivo de vídeo o audio...")
|
||||
in_layout.addWidget(self.in_input)
|
||||
|
||||
self.in_browse_btn = QPushButton("Examinar")
|
||||
self.in_browse_btn.clicked.connect(self.browse_input_file)
|
||||
in_layout.addWidget(self.in_browse_btn)
|
||||
layout.addLayout(in_layout)
|
||||
|
||||
# Combo de Presets
|
||||
layout.addWidget(QLabel("Preset de conversión:"))
|
||||
self.preset_combo = QComboBox()
|
||||
for label in self.PRESETS.keys():
|
||||
self.preset_combo.addItem(label)
|
||||
self.preset_combo.currentIndexChanged.connect(self.on_preset_changed)
|
||||
layout.addWidget(self.preset_combo)
|
||||
|
||||
# Archivo de salida
|
||||
layout.addWidget(QLabel("Archivo de salida (Out):"))
|
||||
out_layout = QHBoxLayout()
|
||||
self.out_input = QLineEdit()
|
||||
self.out_input.setPlaceholderText("Ruta del archivo de salida...")
|
||||
out_layout.addWidget(self.out_input)
|
||||
|
||||
self.out_browse_btn = QPushButton("Examinar")
|
||||
self.out_browse_btn.clicked.connect(self.browse_output_file)
|
||||
out_layout.addWidget(self.out_browse_btn)
|
||||
layout.addLayout(out_layout)
|
||||
|
||||
# Botón de Conversión
|
||||
self.convert_btn = QPushButton("Iniciar Conversión")
|
||||
self.convert_btn.clicked.connect(self.start_conversion)
|
||||
layout.addWidget(self.convert_btn)
|
||||
|
||||
# Label de estado
|
||||
self.status_label = QLabel("")
|
||||
layout.addWidget(self.status_label)
|
||||
|
||||
layout.addStretch()
|
||||
self.setLayout(layout)
|
||||
|
||||
def browse_input_file(self):
|
||||
file_path, _ = QFileDialog.getOpenFileName(
|
||||
self, "Seleccionar archivo de entrada", "", "Archivos Multimedia (*.mkv *.mp4 *.avi *.mov *.m4a *.mp3 *.wav *.flac *.webm);;Todos los archivos (*.*)"
|
||||
)
|
||||
if file_path:
|
||||
self.in_input.setText(file_path)
|
||||
self.update_output_path_from_input(file_path)
|
||||
|
||||
def browse_output_file(self):
|
||||
selected_preset = self.PRESETS[self.preset_combo.currentText()]
|
||||
ext = selected_preset["ext"]
|
||||
file_path, _ = QFileDialog.getSaveFileName(
|
||||
self, "Guardar archivo como", self.out_input.text(), f"Archivo (*{ext})"
|
||||
)
|
||||
if file_path:
|
||||
self.out_input.setText(file_path)
|
||||
|
||||
def update_output_path_from_input(self, input_path):
|
||||
base_path, _ = os.path.splitext(input_path)
|
||||
selected_preset = self.PRESETS[self.preset_combo.currentText()]
|
||||
new_output = f"{base_path}{selected_preset['tag']}{selected_preset['ext']}"
|
||||
self.out_input.setText(new_output)
|
||||
|
||||
def on_preset_changed(self):
|
||||
input_path = self.in_input.text().strip()
|
||||
if input_path:
|
||||
self.update_output_path_from_input(input_path)
|
||||
else:
|
||||
current_out = self.out_input.text().strip()
|
||||
if current_out:
|
||||
base_path, _ = os.path.splitext(current_out)
|
||||
selected_preset = self.PRESETS[self.preset_combo.currentText()]
|
||||
self.out_input.setText(f"{base_path}{selected_preset['ext']}")
|
||||
|
||||
def start_conversion(self):
|
||||
input_file = self.in_input.text().strip()
|
||||
output_file = self.out_input.text().strip()
|
||||
selected_preset = self.PRESETS[self.preset_combo.currentText()]
|
||||
|
||||
if not input_file or not os.path.exists(input_file):
|
||||
QMessageBox.warning(self, "Atención", "Por favor, selecciona un archivo de entrada válido.")
|
||||
return
|
||||
|
||||
if not output_file:
|
||||
QMessageBox.warning(self, "Atención", "Especifica la ruta del archivo de salida.")
|
||||
return
|
||||
|
||||
self.set_inputs_enabled(False)
|
||||
self.status_label.setText("Iniciando conversión...")
|
||||
|
||||
self.converter_thread = MediaConverter(input_file, output_file, selected_preset["key"])
|
||||
self.converter_thread.progress_signal.connect(self.update_status)
|
||||
self.converter_thread.finished_signal.connect(self.on_conversion_finished)
|
||||
self.converter_thread.start()
|
||||
|
||||
def update_status(self, message):
|
||||
self.status_label.setText(message)
|
||||
|
||||
def on_conversion_finished(self, success, message):
|
||||
self.set_inputs_enabled(True)
|
||||
self.status_label.setText("")
|
||||
|
||||
if success:
|
||||
QMessageBox.information(self, "Éxito", message)
|
||||
else:
|
||||
QMessageBox.critical(self, "Error", message)
|
||||
|
||||
def set_inputs_enabled(self, enabled: bool):
|
||||
self.in_input.setEnabled(enabled)
|
||||
self.in_browse_btn.setEnabled(enabled)
|
||||
self.out_input.setEnabled(enabled)
|
||||
self.out_browse_btn.setEnabled(enabled)
|
||||
self.preset_combo.setEnabled(enabled)
|
||||
self.convert_btn.setEnabled(enabled)
|
||||
90
gui/tab_youtube.py
Normal file
90
gui/tab_youtube.py
Normal file
@@ -0,0 +1,90 @@
|
||||
import os
|
||||
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout,QLabel, QLineEdit, QPushButton, QFileDialog, QMessageBox)
|
||||
from utils.downloader import VideoDownloader
|
||||
|
||||
class YoutubeTab(QWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.downloader_thread = None
|
||||
self.initUI()
|
||||
|
||||
def initUI(self):
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# Input URL
|
||||
layout.addWidget(QLabel("URL del vídeo:"))
|
||||
self.url_input = QLineEdit()
|
||||
self.url_input.setPlaceholderText("https://www.youtube.com/watch?v=...")
|
||||
layout.addWidget(self.url_input)
|
||||
|
||||
# Input Ruta de destino
|
||||
layout.addWidget(QLabel("Ruta de destino:"))
|
||||
path_layout = QHBoxLayout()
|
||||
self.path_input = QLineEdit()
|
||||
# Carpeta por defecto: Descargas del usuario
|
||||
default_path = os.path.join(os.path.expanduser("~"), "Downloads")
|
||||
self.path_input.setText(default_path)
|
||||
path_layout.addWidget(self.path_input)
|
||||
|
||||
self.browse_btn = QPushButton("Examinar")
|
||||
self.browse_btn.clicked.connect(self.browse_folder)
|
||||
path_layout.addWidget(self.browse_btn)
|
||||
layout.addLayout(path_layout)
|
||||
|
||||
# Botón de Descarga
|
||||
self.download_btn = QPushButton("Descargar Vídeo MP4")
|
||||
self.download_btn.clicked.connect(self.start_download)
|
||||
layout.addWidget(self.download_btn)
|
||||
|
||||
# Label de Estado local
|
||||
self.status_label = QLabel("")
|
||||
layout.addWidget(self.status_label)
|
||||
|
||||
layout.addStretch()
|
||||
self.setLayout(layout)
|
||||
|
||||
def browse_folder(self):
|
||||
folder = QFileDialog.getExistingDirectory(self, "Seleccionar carpeta de destino")
|
||||
if folder:
|
||||
self.path_input.setText(folder)
|
||||
|
||||
def start_download(self):
|
||||
url = self.url_input.text().strip()
|
||||
save_path = self.path_input.text().strip()
|
||||
|
||||
if not url:
|
||||
QMessageBox.warning(self, "Atención", "Por favor, introduce una URL válida.")
|
||||
return
|
||||
|
||||
if not os.path.exists(save_path):
|
||||
QMessageBox.warning(self, "Atención", "La ruta de destino seleccionada no existe.")
|
||||
return
|
||||
|
||||
# Bloquear controles para evitar descargas simultáneas
|
||||
self.set_inputs_enabled(False)
|
||||
self.status_label.setText("Iniciando proceso...")
|
||||
|
||||
# Iniciar hilo en segundo plano
|
||||
self.downloader_thread = VideoDownloader(url, save_path)
|
||||
self.downloader_thread.progress_signal.connect(self.update_status)
|
||||
self.downloader_thread.finished_signal.connect(self.on_download_finished)
|
||||
self.downloader_thread.start()
|
||||
|
||||
def update_status(self, message):
|
||||
self.status_label.setText(message)
|
||||
|
||||
def on_download_finished(self, success, message):
|
||||
self.set_inputs_enabled(True)
|
||||
self.status_label.setText("")
|
||||
|
||||
if success:
|
||||
QMessageBox.information(self, "Éxito", message)
|
||||
self.url_input.clear()
|
||||
else:
|
||||
QMessageBox.critical(self, "Error", message)
|
||||
|
||||
def set_inputs_enabled(self, enabled: bool):
|
||||
self.url_input.setEnabled(enabled)
|
||||
self.path_input.setEnabled(enabled)
|
||||
self.browse_btn.setEnabled(enabled)
|
||||
self.download_btn.setEnabled(enabled)
|
||||
9
main.py
Normal file
9
main.py
Normal file
@@ -0,0 +1,9 @@
|
||||
import sys
|
||||
from PyQt6.QtWidgets import QApplication
|
||||
from gui.main_window import MainWindow
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication(sys.argv)
|
||||
window = MainWindow()
|
||||
window.show()
|
||||
sys.exit(app.exec())
|
||||
0
utils/__init__.py
Normal file
0
utils/__init__.py
Normal file
88
utils/converter.py
Normal file
88
utils/converter.py
Normal file
@@ -0,0 +1,88 @@
|
||||
import os
|
||||
import subprocess
|
||||
import re
|
||||
from PyQt6.QtCore import QThread, pyqtSignal
|
||||
|
||||
class MediaConverter(QThread):
|
||||
progress_signal = pyqtSignal(str) # Mensaje de estado
|
||||
finished_signal = pyqtSignal(bool, str) # (Éxito?, Mensaje final)
|
||||
|
||||
def __init__(self, input_file, output_file, preset_key, bin_dir="bin"):
|
||||
super().__init__()
|
||||
self.input_file = input_file
|
||||
self.output_file = output_file
|
||||
self.preset_key = preset_key
|
||||
self.bin_dir = os.path.abspath(bin_dir)
|
||||
|
||||
def run(self):
|
||||
ffmpeg_exe = os.path.join(self.bin_dir, "ffmpeg.exe")
|
||||
|
||||
if not os.path.exists(ffmpeg_exe):
|
||||
self.finished_signal.emit(False, "No se encontró ffmpeg.exe en la carpeta bin.")
|
||||
return
|
||||
|
||||
if not os.path.exists(self.input_file):
|
||||
self.finished_signal.emit(False, "El archivo de entrada no existe.")
|
||||
return
|
||||
|
||||
# Configuración de argumentos según el preset
|
||||
if self.preset_key == "mp4_std":
|
||||
# MP4 Estándar sin reescalado
|
||||
codec_args = ["-c:v", "libx264", "-c:a", "aac"]
|
||||
|
||||
elif self.preset_key == "mp4_1080p":
|
||||
# MP4 Max 1080p Verticales (mantiene aspecto, no escala si es <= 1080)
|
||||
codec_args = ["-vf", "scale=-2:'min(1080,ih)'", "-c:v", "libx264", "-c:a", "aac"]
|
||||
|
||||
elif self.preset_key == "mp3_192k":
|
||||
# Estéreo completo a MP3 192 kbps
|
||||
codec_args = ["-vn", "-c:a", "libmp3lame", "-b:a", "192k"]
|
||||
|
||||
elif self.preset_key == "mp3_ch1":
|
||||
# Solo Canal 1 (Izquierdo / Left) -> Mono MP3 192 kbps
|
||||
codec_args = ["-vn", "-af", "pan=mono|c0=c0", "-c:a", "libmp3lame", "-b:a", "192k"]
|
||||
|
||||
elif self.preset_key == "mp3_ch2":
|
||||
# Solo Canal 2 (Derecho / Right) -> Mono MP3 192 kbps
|
||||
codec_args = ["-vn", "-af", "pan=mono|c0=c1", "-c:a", "libmp3lame", "-b:a", "192k"]
|
||||
|
||||
else:
|
||||
self.finished_signal.emit(False, "Preset no reconocido.")
|
||||
return
|
||||
|
||||
# Sobrescribir archivo de salida si ya existe (-y)
|
||||
command = [ffmpeg_exe, "-y", "-i", self.input_file] + codec_args + [self.output_file]
|
||||
|
||||
try:
|
||||
startupinfo = None
|
||||
if os.name == 'nt':
|
||||
startupinfo = subprocess.STARTUPINFO()
|
||||
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||
|
||||
process = subprocess.Popen(
|
||||
command,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
startupinfo=startupinfo,
|
||||
encoding='utf-8',
|
||||
errors='replace'
|
||||
)
|
||||
|
||||
for line in process.stdout:
|
||||
line_str = line.strip()
|
||||
if line_str:
|
||||
if "time=" in line_str:
|
||||
match = re.search(r'time=(\d{2}:\d{2}:\d{2}\.\d{2})', line_str)
|
||||
if match:
|
||||
self.progress_signal.emit(f"Procesando... Tiempo: {match.group(1)}")
|
||||
|
||||
process.wait()
|
||||
|
||||
if process.returncode == 0:
|
||||
self.finished_signal.emit(True, "¡Conversión completada con éxito!")
|
||||
else:
|
||||
self.finished_signal.emit(False, "Ocurrió un error durante la conversión con FFmpeg.")
|
||||
|
||||
except Exception as e:
|
||||
self.finished_signal.emit(False, f"Error de ejecución: {str(e)}")
|
||||
81
utils/downloader.py
Normal file
81
utils/downloader.py
Normal file
@@ -0,0 +1,81 @@
|
||||
import os
|
||||
import subprocess
|
||||
import re
|
||||
from PyQt6.QtCore import QThread, pyqtSignal
|
||||
|
||||
class VideoDownloader(QThread):
|
||||
progress_signal = pyqtSignal(str)
|
||||
finished_signal = pyqtSignal(bool, str)
|
||||
|
||||
def __init__(self, url, download_path, bin_dir="bin"):
|
||||
super().__init__()
|
||||
self.url = url
|
||||
self.download_path = download_path
|
||||
self.bin_dir = os.path.abspath(bin_dir)
|
||||
|
||||
def run(self):
|
||||
ytdlp_exe = os.path.join(self.bin_dir, "yt-dlp.exe")
|
||||
ffmpeg_exe = os.path.join(self.bin_dir, "ffmpeg.exe")
|
||||
|
||||
if not os.path.exists(ytdlp_exe):
|
||||
self.finished_signal.emit(False, f"No se encontró yt-dlp.exe en: {ytdlp_exe}")
|
||||
return
|
||||
|
||||
if not os.path.exists(ffmpeg_exe):
|
||||
self.finished_signal.emit(False, f"No se encontró ffmpeg.exe en: {ffmpeg_exe}")
|
||||
return
|
||||
|
||||
# Creamos una copia del entorno de sistema y añadimos la carpeta 'bin' al PATH
|
||||
# Esto asegura que tanto yt-dlp como subprocess encuentren ffmpeg y ffprobe sin fallar
|
||||
env = os.environ.copy()
|
||||
env["PATH"] = self.bin_dir + os.pathsep + env.get("PATH", "")
|
||||
|
||||
# Comando idéntico a tu terminal
|
||||
command = [
|
||||
ytdlp_exe,
|
||||
"-f", "bestvideo[ext=mp4]+bestaudio[ext=m4a]/bestvideo+bestaudio",
|
||||
"--merge-output-format", "mp4",
|
||||
"--ffmpeg-location", self.bin_dir,
|
||||
"-P", self.download_path,
|
||||
self.url
|
||||
]
|
||||
|
||||
try:
|
||||
startupinfo = None
|
||||
if os.name == 'nt':
|
||||
startupinfo = subprocess.STARTUPINFO()
|
||||
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||
|
||||
process = subprocess.Popen(
|
||||
command,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
startupinfo=startupinfo,
|
||||
env=env, # Le pasamos el entorno enriquecido con el PATH a bin
|
||||
encoding='utf-8',
|
||||
errors='replace'
|
||||
)
|
||||
|
||||
for line in process.stdout:
|
||||
line_str = line.strip()
|
||||
if line_str:
|
||||
# Imprime en la consola de Python para poder ver cualquier advertencia de yt-dlp
|
||||
print(line_str)
|
||||
|
||||
if "[download]" in line_str and "%" in line_str:
|
||||
match = re.search(r'(\d+\.\d+%)', line_str)
|
||||
if match:
|
||||
self.progress_signal.emit(f"Descargando: {match.group(1)}")
|
||||
elif "[Merger]" in line_str or "Merging" in line_str:
|
||||
self.progress_signal.emit("Uniendo vídeo y audio en MP4...")
|
||||
|
||||
process.wait()
|
||||
|
||||
if process.returncode == 0:
|
||||
self.finished_signal.emit(True, "¡Vídeo descargado y unido correctamente!")
|
||||
else:
|
||||
self.finished_signal.emit(False, "Error durante la descarga o la fusión.")
|
||||
|
||||
except Exception as e:
|
||||
self.finished_signal.emit(False, f"Error de ejecución: {str(e)}")
|
||||
93
utils/updater.py
Normal file
93
utils/updater.py
Normal file
@@ -0,0 +1,93 @@
|
||||
import os
|
||||
import requests
|
||||
import zipfile
|
||||
import shutil
|
||||
from PyQt6.QtCore import QThread, pyqtSignal
|
||||
|
||||
# URL de yt-dlp ejecutable directo
|
||||
URL_YTDLP = "https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe"
|
||||
|
||||
class DependencyChecker(QThread):
|
||||
progress_signal = pyqtSignal(str, int) # (Mensaje, Porcentaje)
|
||||
finished_signal = pyqtSignal()
|
||||
|
||||
def __init__(self, bin_dir="bin"):
|
||||
super().__init__()
|
||||
self.bin_dir = os.path.abspath(bin_dir)
|
||||
|
||||
def run(self):
|
||||
os.makedirs(self.bin_dir, exist_ok=True)
|
||||
|
||||
ytdlp_path = os.path.join(self.bin_dir, "yt-dlp.exe")
|
||||
ffmpeg_path = os.path.join(self.bin_dir, "ffmpeg.exe")
|
||||
ffprobe_path = os.path.join(self.bin_dir, "ffprobe.exe")
|
||||
|
||||
# Limpiar archivos corruptos o de tamaño inferior a 1 MB
|
||||
self.cleanup_invalid_files([ytdlp_path, ffmpeg_path, ffprobe_path])
|
||||
|
||||
# 1. Verificar / Descargar yt-dlp.exe
|
||||
if not os.path.exists(ytdlp_path):
|
||||
self.download_file("yt-dlp.exe", URL_YTDLP, ytdlp_path)
|
||||
|
||||
# 2. Verificar / Descargar FFmpeg y FFprobe reales desde la API de GitHub
|
||||
if not os.path.exists(ffmpeg_path) or not os.path.exists(ffprobe_path):
|
||||
self.download_ffmpeg_suite()
|
||||
|
||||
self.progress_signal.emit("Todos los componentes están listos.", 100)
|
||||
self.finished_signal.emit()
|
||||
|
||||
def cleanup_invalid_files(self, paths):
|
||||
for p in paths:
|
||||
if os.path.exists(p) and os.path.getsize(p) < 1000000: # Menor a ~1 MB
|
||||
try:
|
||||
os.remove(p)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def download_file(self, name, url, save_path):
|
||||
headers = {"User-Agent": "Mozilla/5.0"}
|
||||
response = requests.get(url, headers=headers, stream=True, allow_redirects=True)
|
||||
total_size = int(response.headers.get('content-length', 0))
|
||||
downloaded = 0
|
||||
|
||||
with open(save_path, 'wb') as file:
|
||||
for chunk in response.iter_content(chunk_size=16384):
|
||||
if chunk:
|
||||
file.write(chunk)
|
||||
downloaded += len(chunk)
|
||||
if total_size > 0:
|
||||
percent = int((downloaded / total_size) * 100)
|
||||
self.progress_signal.emit(f"Descargando {name}...", percent)
|
||||
|
||||
def download_ffmpeg_suite(self):
|
||||
self.progress_signal.emit("Buscando versión de FFmpeg...", 5)
|
||||
headers = {"User-Agent": "Mozilla/5.0"}
|
||||
|
||||
# Consultar la API de GitHub de GyanD/codexffmpeg para obtener la URL directa del ZIP
|
||||
api_url = "https://api.github.com/repos/GyanD/codexffmpeg/releases/latest"
|
||||
r = requests.get(api_url, headers=headers).json()
|
||||
|
||||
download_url = None
|
||||
for asset in r.get("assets", []):
|
||||
if asset["name"].endswith("essentials_build.zip") or asset["name"].endswith("full_build.zip"):
|
||||
download_url = asset["browser_download_url"]
|
||||
break
|
||||
|
||||
if not download_url:
|
||||
# URL de respaldo estática si falla la API
|
||||
download_url = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip"
|
||||
|
||||
zip_path = os.path.join(self.bin_dir, "ffmpeg.zip")
|
||||
self.download_file("FFmpeg Suite (ZIP)", download_url, zip_path)
|
||||
|
||||
self.progress_signal.emit("Extrayendo FFmpeg y FFprobe...", 90)
|
||||
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||||
for member in zip_ref.namelist():
|
||||
filename = os.path.basename(member)
|
||||
if filename in ["ffmpeg.exe", "ffprobe.exe"]:
|
||||
target_path = os.path.join(self.bin_dir, filename)
|
||||
with zip_ref.open(member) as source, open(target_path, "wb") as target:
|
||||
shutil.copyfileobj(source, target)
|
||||
|
||||
if os.path.exists(zip_path):
|
||||
os.remove(zip_path)
|
||||
Reference in New Issue
Block a user