Spaces:
Paused
Paused
Update app.py
Browse files
app.py
CHANGED
|
@@ -5,6 +5,7 @@ from urllib3.util.retry import Retry
|
|
| 5 |
from bs4 import BeautifulSoup
|
| 6 |
from urllib.parse import urljoin, urlparse
|
| 7 |
import os
|
|
|
|
| 8 |
|
| 9 |
|
| 10 |
def build_session():
|
|
@@ -30,9 +31,8 @@ def extract_media(url):
|
|
| 30 |
"Accept-Encoding": "gzip, deflate, br",
|
| 31 |
"Connection": "keep-alive",
|
| 32 |
"Upgrade-Insecure-Requests": "1",
|
|
|
|
| 33 |
}
|
| 34 |
-
# Timeout separado: (conexión, lectura). Antes era un único valor de 50s,
|
| 35 |
-
# lo que dejaba la UI colgada mucho tiempo si el sitio nunca respondía.
|
| 36 |
response = session.get(url, headers=headers, timeout=(10, 20))
|
| 37 |
response.raise_for_status()
|
| 38 |
|
|
@@ -42,32 +42,20 @@ def extract_media(url):
|
|
| 42 |
images = []
|
| 43 |
seen = set()
|
| 44 |
|
| 45 |
-
# Buscar imágenes en <img>
|
| 46 |
for img in soup.find_all("img"):
|
| 47 |
src = img.get("src") or img.get("data-src") or img.get("data-original")
|
| 48 |
if not src:
|
| 49 |
continue
|
| 50 |
-
|
| 51 |
-
# Convertir URLs relativas a absolutas
|
| 52 |
img_url = urljoin(base_url, src)
|
| 53 |
-
|
| 54 |
-
# Filtrar duplicados y URLs inválidas
|
| 55 |
if img_url in seen or not img_url.startswith(("http://", "https://")):
|
| 56 |
continue
|
| 57 |
seen.add(img_url)
|
| 58 |
-
|
| 59 |
-
# Obtener nombre del archivo
|
| 60 |
parsed = urlparse(img_url)
|
| 61 |
filename = os.path.basename(parsed.path) or "imagen.jpg"
|
| 62 |
if not any(filename.lower().endswith(ext) for ext in [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".bmp"]):
|
| 63 |
filename += ".jpg"
|
|
|
|
| 64 |
|
| 65 |
-
images.append({
|
| 66 |
-
"image": img_url,
|
| 67 |
-
"caption": filename
|
| 68 |
-
})
|
| 69 |
-
|
| 70 |
-
# Buscar imágenes de fondo en <div> o elementos con style
|
| 71 |
for tag in soup.find_all(style=True):
|
| 72 |
style = tag["style"]
|
| 73 |
if "url(" in style:
|
|
@@ -79,13 +67,9 @@ def extract_media(url):
|
|
| 79 |
seen.add(bg_url)
|
| 80 |
images.append({"image": bg_url, "caption": "background.jpg"})
|
| 81 |
|
| 82 |
-
# =======================================================
|
| 83 |
-
# NUEVA SECCIÓN: Buscar videos
|
| 84 |
-
# =======================================================
|
| 85 |
videos = []
|
| 86 |
video_exts = [".mp4", ".webm", ".avi", ".mov", ".mkv", ".m4v", ".ogg"]
|
| 87 |
-
|
| 88 |
-
# 1. Buscar en etiquetas <video> y <source>
|
| 89 |
for tag in soup.find_all(["video", "source"]):
|
| 90 |
src = tag.get("src") or tag.get("data-src")
|
| 91 |
if not src:
|
|
@@ -98,8 +82,7 @@ def extract_media(url):
|
|
| 98 |
if not any(filename.lower().endswith(ext) for ext in video_exts):
|
| 99 |
filename += ".mp4"
|
| 100 |
videos.append({"url": vid_url, "filename": filename})
|
| 101 |
-
|
| 102 |
-
# 2. Buscar enlaces directos a videos en etiquetas <a>
|
| 103 |
for a in soup.find_all("a", href=True):
|
| 104 |
href = a["href"]
|
| 105 |
if any(href.lower().endswith(ext) for ext in video_exts):
|
|
@@ -110,61 +93,55 @@ def extract_media(url):
|
|
| 110 |
filename = os.path.basename(parsed.path) or "video.mp4"
|
| 111 |
videos.append({"url": vid_url, "filename": filename})
|
| 112 |
|
| 113 |
-
#
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
for vid in videos:
|
| 117 |
-
video_html_content += f'<li>🎬 <a href="{vid["url"]}" target="_blank" download="{vid["filename"]}" style="color: #007bff; text-decoration: none; font-weight: bold;">⬇️ Descargar {vid["filename"]}</a></li>'
|
| 118 |
-
video_html_content += "</ul>"
|
| 119 |
-
else:
|
| 120 |
-
video_html_content = "<p style='color: gray; font-style: italic;'>No se encontraron videos directos en el HTML de esta página. (Nota: Sitios como YouTube usan transmisión fragmentada y no exponen un enlace .mp4 directo).</p>"
|
| 121 |
-
|
| 122 |
-
if not images and not videos:
|
| 123 |
-
return [], video_html_content, "No se encontraron imágenes ni videos en esta página."
|
| 124 |
-
|
| 125 |
-
status_msg = f"Se encontraron {len(images)} imágenes y {len(videos)} videos."
|
| 126 |
-
return [img["image"] for img in images], video_html_content, status_msg
|
| 127 |
-
|
| 128 |
-
except requests.exceptions.Timeout:
|
| 129 |
-
return [], "", (
|
| 130 |
-
"El sitio no respondió a tiempo tras reintentar. Suele pasar cuando "
|
| 131 |
-
"el dominio está filtrado a nivel de red/DNS (algunos hosts de archivos "
|
| 132 |
-
"terminan en listas de bloqueo por abuso) o el servidor está caído/lento. "
|
| 133 |
-
"Prueba el mismo enlace en el navegador desde esta misma red para confirmar."
|
| 134 |
-
)
|
| 135 |
-
except requests.exceptions.ConnectionError as e:
|
| 136 |
-
# requests envuelve un ReadTimeout que agotó los reintentos como
|
| 137 |
-
# ConnectionError (no como Timeout) — es un detalle de la librería,
|
| 138 |
-
# no un tipo de fallo distinto al de arriba.
|
| 139 |
-
if "ReadTimeoutError" in str(e) or "Read timed out" in str(e):
|
| 140 |
-
return [], "", (
|
| 141 |
-
"El sitio acepta la conexión pero nunca termina de responder, "
|
| 142 |
-
"ni siquiera tras reintentar. Es el patrón de un bloqueo de "
|
| 143 |
-
"red/firewall o de un mecanismo anti-bots que corta la respuesta "
|
| 144 |
-
"a mitad de camino, no el de un timeout demasiado corto."
|
| 145 |
-
)
|
| 146 |
-
return [], "", f"No se pudo conectar con el sitio: {str(e)}"
|
| 147 |
-
except requests.exceptions.RequestException as e:
|
| 148 |
-
return [], "", f"Error al acceder a la URL: {str(e)}"
|
| 149 |
except Exception as e:
|
| 150 |
-
return [],
|
| 151 |
finally:
|
| 152 |
session.close()
|
| 153 |
|
| 154 |
|
| 155 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
with gr.Blocks(title="Extractor de Imágenes y Videos Web") as demo:
|
| 157 |
-
gr.Markdown(""
|
| 158 |
-
# 🖼️ 🎬 Extractor de Imágenes y Videos
|
| 159 |
-
Pega el enlace de cualquier página web y extrae todas las imágenes y videos disponibles directamente del HTML.
|
| 160 |
-
""")
|
| 161 |
|
| 162 |
with gr.Row():
|
| 163 |
-
url_input = gr.Textbox(
|
| 164 |
-
label="URL de la página",
|
| 165 |
-
placeholder="https://ejemplo.com",
|
| 166 |
-
scale=4
|
| 167 |
-
)
|
| 168 |
submit_btn = gr.Button("🔍 Extraer contenido", scale=1, variant="primary")
|
| 169 |
|
| 170 |
status_text = gr.Textbox(label="Estado", interactive=False)
|
|
@@ -173,7 +150,6 @@ with gr.Blocks(title="Extractor de Imágenes y Videos Web") as demo:
|
|
| 173 |
with gr.Column(scale=2):
|
| 174 |
gallery = gr.Gallery(
|
| 175 |
label="Imágenes encontradas",
|
| 176 |
-
show_label=True,
|
| 177 |
columns=4,
|
| 178 |
rows=4,
|
| 179 |
height="auto",
|
|
@@ -181,32 +157,36 @@ with gr.Blocks(title="Extractor de Imágenes y Videos Web") as demo:
|
|
| 181 |
allow_preview=True
|
| 182 |
)
|
| 183 |
with gr.Column(scale=1):
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
)
|
| 188 |
|
| 189 |
-
#
|
| 190 |
-
gr.
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
|
|
|
| 195 |
|
| 196 |
submit_btn.click(
|
| 197 |
-
fn=
|
| 198 |
inputs=url_input,
|
| 199 |
-
outputs=[gallery,
|
| 200 |
)
|
| 201 |
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
]
|
| 208 |
-
|
| 209 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
)
|
| 211 |
|
| 212 |
if __name__ == "__main__":
|
|
|
|
| 5 |
from bs4 import BeautifulSoup
|
| 6 |
from urllib.parse import urljoin, urlparse
|
| 7 |
import os
|
| 8 |
+
import tempfile
|
| 9 |
|
| 10 |
|
| 11 |
def build_session():
|
|
|
|
| 31 |
"Accept-Encoding": "gzip, deflate, br",
|
| 32 |
"Connection": "keep-alive",
|
| 33 |
"Upgrade-Insecure-Requests": "1",
|
| 34 |
+
"Referer": "https://www.erome.com/",
|
| 35 |
}
|
|
|
|
|
|
|
| 36 |
response = session.get(url, headers=headers, timeout=(10, 20))
|
| 37 |
response.raise_for_status()
|
| 38 |
|
|
|
|
| 42 |
images = []
|
| 43 |
seen = set()
|
| 44 |
|
|
|
|
| 45 |
for img in soup.find_all("img"):
|
| 46 |
src = img.get("src") or img.get("data-src") or img.get("data-original")
|
| 47 |
if not src:
|
| 48 |
continue
|
|
|
|
|
|
|
| 49 |
img_url = urljoin(base_url, src)
|
|
|
|
|
|
|
| 50 |
if img_url in seen or not img_url.startswith(("http://", "https://")):
|
| 51 |
continue
|
| 52 |
seen.add(img_url)
|
|
|
|
|
|
|
| 53 |
parsed = urlparse(img_url)
|
| 54 |
filename = os.path.basename(parsed.path) or "imagen.jpg"
|
| 55 |
if not any(filename.lower().endswith(ext) for ext in [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".bmp"]):
|
| 56 |
filename += ".jpg"
|
| 57 |
+
images.append({"image": img_url, "caption": filename})
|
| 58 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
for tag in soup.find_all(style=True):
|
| 60 |
style = tag["style"]
|
| 61 |
if "url(" in style:
|
|
|
|
| 67 |
seen.add(bg_url)
|
| 68 |
images.append({"image": bg_url, "caption": "background.jpg"})
|
| 69 |
|
|
|
|
|
|
|
|
|
|
| 70 |
videos = []
|
| 71 |
video_exts = [".mp4", ".webm", ".avi", ".mov", ".mkv", ".m4v", ".ogg"]
|
| 72 |
+
|
|
|
|
| 73 |
for tag in soup.find_all(["video", "source"]):
|
| 74 |
src = tag.get("src") or tag.get("data-src")
|
| 75 |
if not src:
|
|
|
|
| 82 |
if not any(filename.lower().endswith(ext) for ext in video_exts):
|
| 83 |
filename += ".mp4"
|
| 84 |
videos.append({"url": vid_url, "filename": filename})
|
| 85 |
+
|
|
|
|
| 86 |
for a in soup.find_all("a", href=True):
|
| 87 |
href = a["href"]
|
| 88 |
if any(href.lower().endswith(ext) for ext in video_exts):
|
|
|
|
| 93 |
filename = os.path.basename(parsed.path) or "video.mp4"
|
| 94 |
videos.append({"url": vid_url, "filename": filename})
|
| 95 |
|
| 96 |
+
# Guardamos la lista de videos para poder descargarlos después
|
| 97 |
+
return [img["image"] for img in images], videos, f"Se encontraron {len(images)} imágenes y {len(videos)} videos."
|
| 98 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
except Exception as e:
|
| 100 |
+
return [], [], f"Error: {str(e)}"
|
| 101 |
finally:
|
| 102 |
session.close()
|
| 103 |
|
| 104 |
|
| 105 |
+
def download_video(video_url, filename):
|
| 106 |
+
"""Descarga el video con headers correctos y lo entrega al usuario"""
|
| 107 |
+
if not video_url:
|
| 108 |
+
return None
|
| 109 |
+
|
| 110 |
+
session = build_session()
|
| 111 |
+
headers = {
|
| 112 |
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
| 113 |
+
"Referer": "https://www.erome.com/",
|
| 114 |
+
"Accept": "*/*",
|
| 115 |
+
"Accept-Language": "es-ES,es;q=0.9",
|
| 116 |
+
"Connection": "keep-alive",
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
try:
|
| 120 |
+
with session.get(video_url, headers=headers, stream=True, timeout=(15, 60)) as r:
|
| 121 |
+
r.raise_for_status()
|
| 122 |
+
|
| 123 |
+
# Guardamos temporalmente el archivo
|
| 124 |
+
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(filename)[1] or ".mp4")
|
| 125 |
+
for chunk in r.iter_content(chunk_size=8192):
|
| 126 |
+
if chunk:
|
| 127 |
+
tmp.write(chunk)
|
| 128 |
+
tmp.close()
|
| 129 |
+
|
| 130 |
+
return tmp.name # Gradio lo convertirá en archivo descargable
|
| 131 |
+
|
| 132 |
+
except Exception as e:
|
| 133 |
+
print(f"Error descargando video: {e}")
|
| 134 |
+
return None
|
| 135 |
+
finally:
|
| 136 |
+
session.close()
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
# Interfaz
|
| 140 |
with gr.Blocks(title="Extractor de Imágenes y Videos Web") as demo:
|
| 141 |
+
gr.Markdown("# 🖼️ 🎬 Extractor de Imágenes y Videos")
|
|
|
|
|
|
|
|
|
|
| 142 |
|
| 143 |
with gr.Row():
|
| 144 |
+
url_input = gr.Textbox(label="URL de la página", placeholder="https://ejemplo.com", scale=4)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
submit_btn = gr.Button("🔍 Extraer contenido", scale=1, variant="primary")
|
| 146 |
|
| 147 |
status_text = gr.Textbox(label="Estado", interactive=False)
|
|
|
|
| 150 |
with gr.Column(scale=2):
|
| 151 |
gallery = gr.Gallery(
|
| 152 |
label="Imágenes encontradas",
|
|
|
|
| 153 |
columns=4,
|
| 154 |
rows=4,
|
| 155 |
height="auto",
|
|
|
|
| 157 |
allow_preview=True
|
| 158 |
)
|
| 159 |
with gr.Column(scale=1):
|
| 160 |
+
video_dropdown = gr.Dropdown(label="Videos encontrados", choices=[], interactive=True)
|
| 161 |
+
download_btn = gr.Button("⬇️ Descargar video seleccionado", variant="primary")
|
| 162 |
+
video_file = gr.File(label="Archivo descargado")
|
|
|
|
| 163 |
|
| 164 |
+
# Estado oculto para guardar la lista de videos
|
| 165 |
+
videos_state = gr.State([])
|
| 166 |
+
|
| 167 |
+
def process(url):
|
| 168 |
+
images, videos, status = extract_media(url)
|
| 169 |
+
choices = [f"{v['filename']} | {v['url']}" for v in videos]
|
| 170 |
+
return images, gr.update(choices=choices, value=choices[0] if choices else None), videos, status
|
| 171 |
|
| 172 |
submit_btn.click(
|
| 173 |
+
fn=process,
|
| 174 |
inputs=url_input,
|
| 175 |
+
outputs=[gallery, video_dropdown, videos_state, status_text]
|
| 176 |
)
|
| 177 |
|
| 178 |
+
def handle_download(selected, videos_list):
|
| 179 |
+
if not selected or not videos_list:
|
| 180 |
+
return None
|
| 181 |
+
# Extraemos la URL del texto del dropdown
|
| 182 |
+
url = selected.split(" | ")[-1]
|
| 183 |
+
filename = selected.split(" | ")[0]
|
| 184 |
+
return download_video(url, filename)
|
| 185 |
+
|
| 186 |
+
download_btn.click(
|
| 187 |
+
fn=handle_download,
|
| 188 |
+
inputs=[video_dropdown, videos_state],
|
| 189 |
+
outputs=video_file
|
| 190 |
)
|
| 191 |
|
| 192 |
if __name__ == "__main__":
|