import hashlib
import os
import requests
import time
from bs4 import BeautifulSoup
from concurrent.futures import ThreadPoolExecutor, as_completed
from urllib.parse import urlparse, urljoin
import uuid
import re
import threading
class BunkrDownloader:
def __init__(self, download_folder, log_callback=None, enable_widgets_callback=None, update_progress_callback=None, update_global_progress_callback=None, headers=None, max_workers=5, translations=None):
self.download_folder = download_folder
self.log_callback = log_callback
self.enable_widgets_callback = enable_widgets_callback
self.update_progress_callback = update_progress_callback
self.update_global_progress_callback = update_global_progress_callback
self.session = requests.Session()
self.headers = headers or {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36',
'Referer': 'https://bunkr.site/',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'Accept-Language': 'en-US,en;q=0.9',
}
self.cancel_requested = False # Flag to indicate if a cancellation request has been made
self.executor = ThreadPoolExecutor(max_workers=max_workers) # Thread pool executor for concurrent downloads
self.total_files = 0
self.completed_files = 0
self.max_downloads = 5 # Valor por defecto
self.log_messages = [] # Cola para almacenar mensajes de log
self.notification_interval = 10 # Intervalo de notificación en segundos
self.start_notification_thread()
self.translations = translations or {}
def start_notification_thread(self):
def notify_user():
while not self.cancel_requested:
if self.log_messages:
# Enviar todos los mensajes acumulados
if self.log_callback:
self.log_callback("\n".join(self.log_messages))
self.log_messages.clear()
time.sleep(self.notification_interval)
# Iniciar un hilo para notificaciones periódicas
notification_thread = threading.Thread(target=notify_user, daemon=True)
notification_thread.start()
def tr(self, key):
# Obtener la traducción para la clave dada
return self.translations.get(key, key)
def log(self, message_key, url=None):
message = self.tr(message_key)
domain = urlparse(url).netloc if url else "General"
full_message = f"{domain}: {message}"
self.log_messages.append(full_message) # Agregar mensaje a la cola
def request_cancel(self):
self.cancel_requested = True
self.log("Download has been cancelled.")
self.shutdown_executor()
def shutdown_executor(self):
self.executor.shutdown(wait=False)
self.log("Executor shut down.")
def clean_filename(self, filename):
return re.sub(r'[<>:"/\\|?*\u200b]', '_', filename)
def get_consistent_folder_name(self, url, default_name):
# Genera un hash de la URL para crear un nombre único y consistente
url_hash = hashlib.md5(url.encode()).hexdigest()[:8]
folder_name = f"{default_name}_{url_hash}"
return self.clean_filename(folder_name)
def download_file(self, url_media, ruta_carpeta, file_id):
if self.cancel_requested:
self.log("Descarga cancelada", url=url_media)
return
file_name = os.path.basename(urlparse(url_media).path)
file_path = os.path.join(ruta_carpeta, file_name)
if os.path.exists(file_path):
self.log(f"El archivo ya existe, omitiendo: {file_path}")
self.completed_files += 1
if self.update_global_progress_callback:
self.update_global_progress_callback(self.completed_files, self.total_files)
return
max_attempts = 3
delay = 1
for attempt in range(max_attempts):
try:
self.log(f"Intentando descargar {url_media} (Intento {attempt + 1}/{max_attempts})")
response = self.session.get(url_media, headers=self.headers, stream=True)
response.raise_for_status()
total_size = int(response.headers.get('content-length', 0))
downloaded_size = 0
# Descargar el archivo en fragmentos
with open(file_path, 'wb') as file:
for chunk in response.iter_content(chunk_size=65536): # Fragmentos de 64KB
if self.cancel_requested:
self.log("Descarga cancelada durante la descarga del archivo.", url=url_media)
file.close()
os.remove(file_path)
return
file.write(chunk)
downloaded_size += len(chunk)
if self.update_progress_callback:
self.update_progress_callback(downloaded_size, total_size, file_id=file_id, file_path=file_path)
self.log("Archivo descargado", url=url_media)
# Notificar al usuario al completar la descarga
if self.log_callback:
self.log_callback(f"Descarga completada: {file_name}")
self.completed_files += 1
if self.update_global_progress_callback:
self.update_global_progress_callback(self.completed_files, self.total_files)
break
except requests.RequestException as e:
if response.status_code == 429:
self.log(f"Límite de tasa excedido. Reintentando después de {delay} segundos.")
time.sleep(delay)
delay *= 2 # Retroceso exponencial para limitación de tasa
else:
self.log(f"Error al descargar de {url_media}: {e}. Intento {attempt + 1} de {max_attempts}", url=url_media)
if attempt < max_attempts - 1:
time.sleep(3)
def descargar_post_bunkr(self, url_post):
try:
self.log(f"Iniciando descarga para el post: {url_post}")
# Si se trata de una URL tipo '/f/', seguimos el flujo en dos pasos:
if '/f/' in url_post:
self.log("Detectado URL tipo '/f/'. Procediendo a extraer el enlace intermedio.")
# Paso 1: Accedemos a la URL original para obtener el primer enlace (intermedio)
response = self.session.get(url_post, headers=self.headers)
if response.status_code != 200:
self.log(f"Error al acceder al post {url_post}: Estado {response.status_code}")
return
soup = BeautifulSoup(response.text, 'html.parser')
first_anchor = soup.find('a', {
'class': 'btn btn-main btn-lg rounded-full px-6 font-semibold flex-1 ic-download-01 ic-before before:text-lg'
})
if not first_anchor or 'href' not in first_anchor.attrs:
self.log("No se encontró el primer enlace de descarga en la página original.")
return
intermediate_url = first_anchor['href']
self.log(f"Enlace intermedio encontrado: {intermediate_url}")
# Paso 2: Accedemos a la URL intermedia para extraer el enlace final de descarga
intermediate_response = self.session.get(intermediate_url, headers=self.headers)
if intermediate_response.status_code != 200:
self.log(f"Error al acceder a la URL intermedia: {intermediate_url} (Estado {intermediate_response.status_code})")
return
soup2 = BeautifulSoup(intermediate_response.text, 'html.parser')
# Buscamos la etiqueta