Herramienta CLI para la Gestión de Videos 'Ver Más Tarde' de Bilibili con Interfaz Tipo Flashcard

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:

  1. 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.
  2. 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.
  3. 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]")

Etiquetas: Python CLI Bilibili Flashcards MPV

Publicado el 6-8 07:25