La acumulación de videos en la lista "Ver Más Tarde" de Bilibili es un desafío común para muchos usuarios. Esta herramienta de línea de comandos (CLI) ofrece una solución para organizar y consumir este contenido de manera más estructurada, utilizando un enfoque similar a las tarjetas didácticas (flashcards).
Características Principales
El gestor de videos "Ver Más Tarde" de Bilibili proporciona las siguientes funcionalidades:
- Reorganización de la Lista Remota: Permite "barajar" la lista de videos pendientes en el servidor de Bilibili. Un número aleatorio de videos (típicamente entre 5 y 20) se extrae y se vuelve a añadir al inicio de la lista, simulando un sorteo para traer contenido antiguo a la vista.
- Modo Flashcard Interactivo: Presenta los videos de la lista uno por uno, mostrando información clave como título, autor, duración, descripción y un enlace directo. El usuario tiene varias opciones para cada video:
- Eliminar: Quitar el video de la lista "Ver Más Tarde".
- Guardar y Eliminar: Añadir el video a una carpeta de favoritos predeterminada y luego eliminarlo de la lista "Ver Más Tarde".
- Reproducir Video: Abrir el video en el reproductor MPV externo.
- Reproducir Audio: Escuchar solo el audio del video a través de MPV.
- Omitir: Pasar al siguiente video sin realizar ninguna acción.
- Salir: Finalizar la sesión de organización.
- Reproducción Avanzada con MPV: La reproducción de video y audio se gestiona a través de MPV. Para videos con múltiples partes (p. ej., "P1", "P2"), la herramienta permite al usuario seleccionar la parte específica a reproducir. Si hay una gran cantidad de partes, se utiliza un paginador del sistema para facilitar la navegación.
Requisitos e Implementación Técnica
La herramienta está desarrollada en Python y requiere la instalación de las siguientes bibliotecas:
requests: Para realizar peticiones HTTP a la API de Bilibili.browser_cookie3: Para acceder a las cookies de sesión de Bilibili guardadas en el navegador del usuario, lo que permite la autenticación sin necesidad de credenciales explícitas.rich: Para una interfaz de consola rica y atractiva, incluyendo paneles, tablas y barras de progreso.
Además, es indispensable tener instalado el reproductor multimedia MPV en el sistema operativo. MPV, a su vez, suele integrarse con yt-dlp para la extracción de flujos de video, lo que garantiaz la compatibilidad con una amplia gama de videos de Bilibili (incluyendo series y contenido regular).
La interacción con Bilibili se realiza a través de su API pública, consultando la documentación disponible en proyectos como Bilibili-API-Collect. Las cookies del navegador (especialmente SESSDATA y bili\_jct para CSRF) son cruciales para mantener la sesión y realizar operaciones de modificación en la lista del usuario.
Entorno de Prueba: openSUSE Tumbleweed Python3.13, Fedora 43 Python3.14. Dependencias:
# pyproject.toml
[project]
name = "bili-watchlater-tool"
version = "0.1.0"
description = "Herramienta CLI para organizar videos 'Ver Más Tarde' de Bilibili"
readme = "README.md"
requires-python = ">=3.14"
dependencies = [
"browser-cookie3>=0.20.1",
"requests>=2.32.5",
"rich>=14.3.1",
]
Ejemplo de Uso y Código
El siguiente código Python implementa las funcionalidades descritas, utilizando las bibliotecas mencionadas para interactuar con Bilibili y MPV.
import time
import random
import browser_cookie3
import requests
import subprocess
import os
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich.live import Live
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn
# Inicializa la consola Rich para una salida atractiva
consola = Console()
class ReproductorMpv:
"""Clase envoltorio para el reproductor multimedia MPV."""
def __init__(self):
self.ruta_mpv = self._detectar_ejecutable_mpv()
self.proceso_actual = None
def _detectar_ejecutable_mpv(self):
"""Busca la ruta del ejecutable de MPV en ubicaciones comunes."""
posibles_rutas = [
"mpv",
"mpv.exe",
os.path.join(os.environ.get("PROGRAMFILES", "C:\\Program Files"), "mpv", "mpv.exe"),
os.path.join(os.environ.get("CHOCOLATEYINSTALL", "C:\\ProgramData\\chocolatey"), "bin", "mpv.exe"),
os.path.join(os.environ.get("USERPROFILE", "C:\\Users\\default"), "scoop", "shims", "mpv.exe"),
"/usr/bin/mpv",
"/usr/local/bin/mpv",
"/opt/homebrew/bin/mpv"
]
for ruta in posibles_rutas:
ruta_expandida = os.path.expandvars(ruta)
if os.path.isfile(ruta_expandida) or self._comando_existe(ruta):
return ruta if ruta in ["mpv", "mpv.exe"] else ruta_expandida
return None
def _comando_existe(self, comando):
"""Verifica si un comando del sistema existe."""
try:
subprocess.run([comando, "--version"], capture_output=True, check=False)
return True
except FileNotFoundError:
return False
def esta_disponible(self):
"""Indica si el reproductor MPV fue detectado y está listo para usar."""
return self.ruta_mpv is not None
def obtener_url_video_stream(self, bvid, pagina=1):
"""
Intenta obtener la URL de streaming real de un video de Bilibili.
Prioriza yt-dlp, si está disponible.
"""
url_base_bili = f"https://www.bilibili.com/video/{bvid}"
url_con_pagina = f"{url_base_bili}?p={pagina}" if pagina > 1 else url_base_bili
try:
import yt_dlp
ydl_opts = {
'format': 'best[height<=1080]', # Limita a 1080p para mejor rendimiento
'quiet': True,
'no_warnings': True,
'skip_download': True,
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
info = ydl.extract_info(url_con_pagina, download=False)
if info and 'url' in info:
return info['url'], info.get('title', 'Título Desconocido')
elif info and 'formats' in info:
# Intenta encontrar el mejor formato si 'url' directa no está presente
mejor_formato = next((f for f in info['formats'] if 'url' in f), None)
if mejor_formato:
return mejor_formato['url'], info.get('title', 'Título Desconocido')
except ImportError:
consola.print("[yellow]Sugerencia: Instala yt-dlp para una mejor experiencia de reproducción (pip install yt-dlp)[/yellow]")
except Exception as e:
consola.print(f"[yellow]Fallo al obtener enlace de video con yt-dlp: {e}[/yellow]")
# Retorno a la URL de la página de Bilibili si yt-dlp falla o no está instalado
return url_con_pagina, None
def reproducir_contenido(self, bvid, pagina=1, solo_audio=False, titulo_video=None):
"""
Inicia la reproducción de un video o solo su audio usando MPV.
"""
if not self.esta_disponible():
consola.print("[red]Error: Reproductor MPV no encontrado. Por favor, instálalo.[/red]")
consola.print("[dim]Descarga: https://mpv.io/installation/[/dim]")
return False
url_stream, titulo_extraido = self.obtener_url_video_stream(bvid, pagina=pagina)
titulo_mostrar = titulo_video or titulo_extraido or bvid
comando_mpv = [self.ruta_mpv]
if solo_audio:
comando_mpv.extend(["--no-video", "--force-window=immediate"])
consola.print(f"[cyan]🎵 Reproduciendo audio: {titulo_mostrar[:40]}...[/cyan]")
else:
comando_mpv.extend(["--force-window=immediate"])
consola.print(f"[cyan]▶️ Reproduciendo video: {titulo_mostrar[:40]}...[/cyan]")
# Parámetros de optimización comunes para MPV
comando_mpv.extend([
"--cache=yes",
"--cache-secs=30",
"--demuxer-max-bytes=50M",
"--demuxer-max-back-bytes=25M",
"--geometry=50%:50%",
])
# Simular encabezados de navegador para evitar bloqueos
comando_mpv.extend([
"--http-header-fields-append=Referer: https://www.bilibili.com",
"--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
])
comando_mpv.append(url_stream)
try:
# Iniciar MPV en un proceso separado
if os.name == 'nt': # Windows
self.proceso_actual = subprocess.Popen(
comando_mpv,
creationflags=subprocess.CREATE_NEW_CONSOLE
)
else: # Linux/Mac
self.proceso_actual = subprocess.Popen(
comando_mpv,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
consola.print(f"[green]MPV iniciado (PID: {self.proceso_actual.pid})[/green]")
return True
except Exception as e:
consola.print(f"[red]Fallo al iniciar MPV: {e}[/red]")
return False
def esperar_finalizacion(self):
"""Espera a que el proceso de MPV finalice (bloqueante)."""
if self.proceso_actual:
try:
self.proceso_actual.wait()
except Exception:
pass # Ignora errores si el proceso ya no existe
finally:
self.proceso_actual = None
def esta_reproduciendo(self):
"""Verifica si MPV está actualmente reproduciendo contenido."""
if self.proceso_actual:
return self.proceso_actual.poll() is None
return False
def detener_reproduccion(self):
"""Detiene forzosamente cualquier reproducción activa de MPV."""
if self.proceso_actual and self.esta_reproduciendo():
try:
self.proceso_actual.terminate()
self.proceso_actual.wait(timeout=2)
except Exception:
try:
self.proceso_actual.kill() # Último recurso
except Exception:
pass
finally:
self.proceso_actual = None
class AdministradorBilibili:
"""Gestor de videos 'Ver Más Tarde' y favoritos de Bilibili."""
def __init__(self):
self.cookies = self.cargar_cookies_bilibili()
if not self.cookies:
raise Exception("No se pudieron cargar las cookies de Bilibili. Asegúrate de estar logueado en el navegador y que esté cerrado.")
self.csrf_token = next((c.value for c in self.cookies if c.name == 'bili_jct'), None)
if not self.csrf_token:
raise Exception("No se encontró el token CSRF (bili_jct) en las cookies.")
self.sesion = requests.Session()
self.sesion.cookies = self.cookies
self.sesion.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Referer": "https://www.bilibili.com/watchlater/",
"Origin": "https://www.bilibili.com"
})
self.url_base_pendientes = "https://api.bilibili.com/x/v2/history/toview"
self.id_favoritos_predeterminado = self.obtener_id_favoritos_predeterminado()
if not self.id_favoritos_predeterminado:
consola.print("[yellow]Advertencia: No se pudo obtener el ID de la carpeta de favoritos predeterminada. La función de guardar podría estar limitada.[/yellow]")
self.reproductor = ReproductorMpv()
if self.reproductor.esta_disponible():
consola.print("[green]✓ Reproductor MPV listo[/green]")
else:
consola.print("[yellow]⚠ Reproductor MPV no detectado. Las funciones de reproducción no estarán disponibles.[/yellow]")
def cargar_cookies_bilibili(self):
"""Intenta cargar las cookies de Bilibili desde varios navegadores."""
navegadores_a_probar = [browser_cookie3.chrome, browser_cookie3.edge, browser_cookie3.firefox]
for funcion_navegador in navegadores_a_probar:
try:
coleccion_cookies = funcion_navegador(domain_name='.bilibili.com')
# SESSDATA es una cookie clave para la autenticación en Bilibili
if any(c.name == 'SESSDATA' for c in coleccion_cookies):
return coleccion_cookies
except Exception:
continue # Prueba con el siguiente navegador si falla
return None
def _obtener_id_usuario(self):
"""Recupera el ID de usuario (UID) del usuario actualmente logueado."""
url_nav = "https://api.bilibili.com/x/web-interface/nav"
respuesta = self.sesion.get(url_nav).json()
if respuesta['code'] == 0:
return respuesta['data']['mid']
raise Exception("Fallo al obtener el UID. Verifica tu estado de sesión.")
def obtener_id_favoritos_predeterminado(self):
"""Obtiene el ID de la primera carpeta de favoritos del usuario (generalmente la predeterminada)."""
uid = self._obtener_id_usuario()
url_fav_list = f"https://api.bilibili.com/x/v3/fav/folder/created/list-all?up_mid={uid}"
respuesta = self.sesion.get(url_fav_list).json()
if respuesta['code'] == 0 and respuesta['data']['list']:
# La primera carpeta en la lista suele ser la predeterminada o "Mi lista de favoritos"
return respuesta['data']['list'][0]['id']
return None
def agregar_a_favoritos(self, aid_video, id_carpeta_fav):
"""Añade un video a la carpeta de favoritos especificada."""
url_deal_fav = "https://api.bilibili.com/x/v3/fav/resource/deal"
datos = {
'rid': aid_video,
'type': '2', # Tipo 2 para videos
'add_media_ids': id_carpeta_fav,
'csrf': self.csrf_token
}
return self.sesion.post(url_deal_fav, data=datos).json()
def obtener_lista_pendientes(self):
"""Recupera la lista completa de videos 'Ver Más Tarde' del usuario."""
respuesta = self.sesion.get(self.url_base_pendientes).json()
if respuesta['code'] != 0:
consola.print(f"[red]Error al obtener la lista de pendientes: {respuesta['message']}[/red]")
return []
return respuesta['data']['list']
def obtener_segmentos_video(self, bvid):
"""
Obtiene información sobre las partes (segmentos o 'P') de un video.
Retorna: Una lista de tuplas (numero_pagina, titulo_parte, duracion) o una lista vacía.
"""
url_view_info = f"https://api.bilibili.com/x/web-interface/view?bvid={bvid}"
try:
respuesta = self.sesion.get(url_view_info).json()
if respuesta['code'] == 0:
datos = respuesta['data']
segmentos = datos.get('pages', [])
if len(segmentos) <= 1:
return [] # No hay múltiples partes
info_segmentos = []
for p in segmentos:
info_segmentos.append((
p['page'],
p['part'] or f"P{p['page']}",
p['duration']
))
return info_segmentos
except Exception as e:
consola.print(f"[yellow]Fallo al obtener info de segmentos: {e}[/yellow]")
return []
def eliminar_video_pendiente(self, aid_video):
"""Elimina un video de la lista 'Ver Más Tarde'."""
url_del_toview = "https://api.bilibili.com/x/v2/history/toview/del"
datos = {'aid': aid_video, 'csrf': self.csrf_token}
return self.sesion.post(url_del_toview, data=datos).json()
def agregar_a_pendientes(self, bvid):
"""Añade un video a la lista 'Ver Más Tarde'."""
url_add_toview = "https://api.bilibili.com/x/v2/history/toview/add"
datos = {'bvid': bvid, 'csrf': self.csrf_token}
return self.sesion.post(url_add_toview, data=datos).json()
def reordenar_lista_remota(self):
"""
"Baraja" la lista 'Ver Más Tarde' en el servidor, moviendo videos seleccionados aleatoriamente al principio.
"""
lista_videos_actual = self.obtener_lista_pendientes()
total_videos = len(lista_videos_actual)
if total_videos < 10:
consola.print("[yellow]Pocos videos (menos de 10) para un reordenamiento significativo.[/yellow]")
return
# Selecciona aleatoriamente un porcentaje de videos para reordenar, con un límite
num_a_reordenar = min(max(int(total_videos * 0.15), 5), 20)
videos_seleccionados = random.sample(lista_videos_actual, num_a_reordenar)
consola.print(f"[bold cyan]Iniciando reordenamiento de {total_videos} videos (moviendo {num_a_reordenar})...[/bold cyan]")
with Progress(
SpinnerColumn("dots"),
TextColumn("[progress.description]{task.description}"),
BarColumn(),
TaskProgressColumn(),
console=consola
) as progreso:
tarea = progreso.add_task("[cyan]Procesando videos...", total=num_a_reordenar)
for video in videos_seleccionados:
try:
# La lógica es eliminar y volver a añadir; esto los mueve al principio de la lista
self.eliminar_video_pendiente(video['aid'])
time.sleep(0.3)
self.agregar_a_pendientes(video['bvid'])
progreso.update(tarea, advance=1, description=f"[cyan]Movido: {video['title'][:25]}...")
time.sleep(0.4)
except Exception as e:
consola.print(f"[red]Error al reordenar el video '{video['title']}': {e}[/red]")
consola.print("[green]✨ Reordenamiento completado en la nube.[/green]\n")
def _mostrar_opciones_accion(self, tiene_reproductor):
"""
Muestra el panel de acciones disponibles para el usuario.
Retorna las acciones como una cadena formateada y un diccionario de mapeo.
"""
acciones = []
acciones.append(("Enter", "Omitir", "white"))
acciones.append(("D", "Eliminar", "red"))
acciones.append(("S", "Guardar y Eliminar", "yellow"))
if tiene_reproductor:
acciones.append(("P", "Reproducir Video", "green"))
acciones.append(("A", "Reproducir Audio", "cyan"))
acciones.append(("Q", "Salir", "blue"))
partes_accion = []
for tecla, descripcion, color in acciones:
partes_accion.append(
f"[bold reverse {color}] {tecla.upper()} [/bold reverse {color}] [dim]{descripcion}[/dim]"
)
return " ".join(partes_accion), {a[0].lower(): a[1] for a in acciones}
def _manejar_seleccion_segmento(self, bvid, titulo_video):
"""
Permite al usuario seleccionar un segmento (parte P) de un video.
Soporta visualización paginada si hay muchos segmentos.
"""
segmentos_video = self.obtener_segmentos_video(bvid)
if not segmentos_video:
return 1 # Si no hay segmentos, asume la parte 1
total_segmentos = len(segmentos_video)
while True:
consola.print(f"\n[yellow]El video '{titulo_video[:40]}...' tiene {total_segmentos} partes:[/yellow]")
tabla_segmentos = Table(show_header=True, header_style="bold magenta")
tabla_segmentos.add_column("No.", style="cyan", width=6)
tabla_segmentos.add_column("Título de Parte", style="white")
tabla_segmentos.add_column("Duración", style="green", width=10)
# Muestra solo los primeros 10 segmentos
for num_p, titulo_p, duracion_s in segmentos_video[:10]:
minutos, segundos = divmod(duracion_s, 60)
tabla_segmentos.add_row(str(num_p), titulo_p[:40], f"{minutos}:{segundos:02d}")
if total_segmentos > 10:
tabla_segmentos.add_row("...", f"({total_segmentos-10} partes más)", "")
consola.print(tabla_segmentos)
sugerencias = [f"1-{total_segmentos} para seleccionar"]
if total_segmentos > 10:
sugerencias.append("[cyan]l/lista[/cyan] para ver todas")
sugerencias.append("Enter para reproducir la parte 1")
consola.print(f"\n[dim]{' | '.join(sugerencias)}[/dim]")
try:
eleccion_usuario = input("Tu elección > ").strip().lower()
if eleccion_usuario in ('l', 'lista') and total_segmentos > 10:
self._mostrar_lista_segmentos_completa(segmentos_video)
continue # Vuelve a mostrar las opciones después de ver la lista completa
if eleccion_usuario == "":
return 1 # Predeterminado a la parte 1
if eleccion_usuario.isdigit():
num_parte = int(eleccion_usuario)
if 1 <= num_parte <= total_segmentos:
return num_parte
else:
consola.print(f"[red]Por favor, ingresa un número entre 1 y {total_segmentos}.[/red]")
input("Presiona Enter para continuar...")
continue
consola.print("[red]Entrada no válida.[/red]")
input("Presiona Enter para continuar...")
except KeyboardInterrupt:
consola.print("\n[yellow]Selección cancelada.[/yellow]")
return 1
except Exception as e:
consola.print(f"[red]Error en la selección de segmento: {e}[/red]")
return 1
def _mostrar_lista_segmentos_completa(self, segmentos):
"""
Muestra la lista completa de segmentos usando un paginador del sistema.
"""
lineas_contenido = []
lineas_contenido.append(f"Lista completa de partes de video (Total: {len(segmentos)})\n")
lineas_contenido.append("=" * 60)
for num_p, titulo_p, duracion_s in segmentos:
minutos, segundos = divmod(duracion_s, 60)
duracion_str = f"{minutos}:{segundos:02d}"
lineas_contenido.append(f"{num_p:>3}. [{duracion_str}] {titulo_p}")
contenido_texto = "\n".join(lineas_contenido)
consola.print("\n[yellow]Abriendo lista completa (presiona 'q' para salir del paginador)...[/yellow]")
with consola.pager():
consola.print(contenido_texto)
def iniciar_sesion_flashcard(self, orden="aleatorio"):
"""Inicia el modo flashcard para organizar videos."""
lista_videos = self.obtener_lista_pendientes()
if not lista_videos:
consola.print("[yellow]Tu lista 'Ver Más Tarde' está vacía.[/yellow]")
return
if orden == "aleatorio": random.shuffle(lista_videos)
elif orden == "inverso": lista_videos.reverse()
num_total_videos = len(lista_videos)
indice_actual = 0
while indice_actual < num_total_videos:
video_actual = lista_videos[indice_actual]
consola.clear()
# Construye la interfaz de la flashcard
tabla_info = Table.grid(expand=True)
tabla_info.add_column(style="cyan", justify="right")
tabla_info.add_column(style="white")
tabla_info.add_row("Título: ", f"[bold]{video_actual['title']}[/bold]")
tabla_info.add_row("UP: ", video_actual['owner']['name'])
minutos_duracion, segundos_duracion = divmod(video_actual['duration'], 60)
tabla_info.add_row("Duración: ", f"{minutos_duracion}min {segundos_duracion}s")
desc_corta = (video_actual['desc'][:100] + '...') if len(video_actual['desc']) > 100 else video_actual['desc']
tabla_info.add_row("Descripción: ", desc_corta)
tabla_info.add_row("Enlace: ", f"https://www.bilibili.com/video/{video_actual['bvid']}")
segmentos = self.obtener_segmentos_video(video_actual['bvid'])
if segmentos:
tabla_info.add_row("Partes: ", f"[yellow]Este video tiene {len(segmentos)} partes[/yellow]")
consola.print(Panel(tabla_info, title=f"Flashcard {indice_actual+1}/{num_total_videos}", border_style="bright_blue"))
# Muestra las opciones de acción al usuario
hint_acciones, mapa_acciones = self._mostrar_opciones_accion(self.reproductor.esta_disponible())
consola.print(Panel(hint_acciones, border_style="dim"))
entrada_usuario = consola.input("Ingresa tu comando > ").lower().strip()
if entrada_usuario == 'q':
consola.print("[bold]Sesión de organización finalizada. ¡Hasta pronto![/bold]")
break
elif entrada_usuario == 'd':
resultado = self.eliminar_video_pendiente(video_actual['aid'])
if resultado.get('code') == 0:
consola.print("[bold red]Eliminado de 'Ver Más Tarde' ✓[/bold red]")
indice_actual += 1
else:
consola.print(f"[red]Fallo al eliminar: {resultado.get('message')}[/red]")
time.sleep(0.5)
elif entrada_usuario == 's':
if not self.id_favoritos_predeterminado:
consola.print("[red]Error: ID de carpeta de favoritos no disponible.[/red]")
else:
res_fav = self.agregar_a_favoritos(video_actual['aid'], self.id_favoritos_predeterminado)
if res_fav.get('code') == 0:
self.eliminar_video_pendiente(video_actual['aid'])
consola.print(f"[bold yellow]Guardado en favoritos y eliminado de 'Ver Más Tarde' ✓[/bold yellow]")
indice_actual += 1
else:
consola.print(f"[red]Fallo al guardar en favoritos: {res_fav.get('message')}[/red]")
time.sleep(0.5)
elif entrada_usuario in ['p', 'a'] and self.reproductor.esta_disponible():
es_solo_audio = (entrada_usuario == 'a')
pagina_seleccionada = 1
if segmentos:
pagina_seleccionada = self._manejar_seleccion_segmento(video_actual['bvid'], video_actual['title'])
exito_reproduccion = self.reproductor.reproducir_contenido(
video_actual['bvid'],
pagina=pagina_seleccionada,
solo_audio=es_solo_audio,
titulo_video=video_actual['title']
)
if exito_reproduccion:
consola.print("[dim]MPV reproduciendo. Cierra el reproductor para volver...[/dim]")
self.reproductor.esperar_finalizacion()
consola.print("[green]Reproducción finalizada. Volviendo a la interfaz de organización.[/green]")
time.sleep(0.5)
else:
# Cualquier otra entrada o Enter simplemente omite el video
indice_actual += 1
if __name__ == "__main__":
try:
gestor = AdministradorBilibili()
# Pregunta al usuario si desea reordenar la lista
if consola.input("¿Deseas reordenar aleatoriamente tu lista de 'Ver Más Tarde' en Bilibili? (s/n): ").lower() == 's':
gestor.reordenar_lista_remota()
# Inicia el modo flashcard
modo_orden = consola.input("Selecciona el orden de la organización (1:Aleatorio / 2:Normal / 3:Inverso): ")
mapeo_modos = {"1": "aleatorio", "2": "normal", "3": "inverso"}
gestor.iniciar_sesion_flashcard(orden=mapeo_modos.get(modo_orden, "aleatorio"))
except Exception as e:
consola.print(f"[bold red]Error fatal: {e}[/bold red]")