diff --git a/fastanime/cli/commands/downloads.py b/fastanime/cli/commands/downloads.py index 5d9ad8e..ec5d9a4 100644 --- a/fastanime/cli/commands/downloads.py +++ b/fastanime/cli/commands/downloads.py @@ -1,7 +1,9 @@ +import logging from typing import TYPE_CHECKING import click +logger = logging.getLogger(__name__) if TYPE_CHECKING: from ..config import Config @@ -10,9 +12,16 @@ if TYPE_CHECKING: help="View and watch your downloads using mpv", short_help="Watch downloads" ) @click.option("--path", "-p", help="print the downloads folder and exit", is_flag=True) +@click.option("--view-episodes", "-v", help="View individual episodes", is_flag=True) +@click.option( + "--ffmpegthumbnailer-seek-time", + "--time-to-seek", + "-t", + type=click.IntRange(-1, 100), + help="ffmpegthumbnailer seek time [0-100]", +) @click.pass_obj -def downloads(config: "Config", path: bool): - import logging +def downloads(config: "Config", path: bool, view_episodes, ffmpegthumbnailer_seek_time): import os from ...cli.utils.mpv import run_mpv @@ -21,8 +30,8 @@ def downloads(config: "Config", path: bool): from ..utils.tools import exit_app from ..utils.utils import fuzzy_inquirer - logger = logging.getLogger(__name__) - + if not ffmpegthumbnailer_seek_time: + ffmpegthumbnailer_seek_time = config.ffmpegthumbnailer_seek_time USER_VIDEOS_DIR = config.downloads_dir if path: print(USER_VIDEOS_DIR) @@ -43,15 +52,29 @@ def downloads(config: "Config", path: bool): return out = os.path.join(downloads_thumbnail_cache_dir, anime_title) - completed_process = subprocess.run( - [FFMPEG_THUMBNAILER, "-i", video_path, "-o", out], stderr=subprocess.PIPE - ) - if completed_process.returncode == 0: - logger.info(f"Success in creating {anime_title} thumbnail") - else: - logger.warn(f"Failed in creating {anime_title} thumbnail") + if ffmpegthumbnailer_seek_time == -1: + import random - def get_previews(workers=None): + seektime = str(random.randrange(0, 100)) + else: + seektime = str(ffmpegthumbnailer_seek_time) + _ = subprocess.run( + [ + FFMPEG_THUMBNAILER, + "-i", + video_path, + "-o", + out, + "-s", + "0", + "-t", + seektime, + ], + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + + def get_previews_anime(workers=None, bg=True): import concurrent.futures import shutil from pathlib import Path @@ -66,38 +89,52 @@ def downloads(config: "Config", path: bool): downloads_thumbnail_cache_dir = os.path.join(APP_CACHE_DIR, "video_thumbnails") Path(downloads_thumbnail_cache_dir).mkdir(parents=True, exist_ok=True) - # use concurrency to download the images as fast as possible - with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor: - # load the jobs - future_to_url = {} - for anime_title in anime_downloads: - anime_path = os.path.join(USER_VIDEOS_DIR, anime_title) - if not os.path.isdir(anime_path): - continue - playlist = os.listdir(anime_path) - if playlist: - # actual link to download image from - video_path = os.path.join(anime_path, playlist[0]) - future_to_url[ - executor.submit( - create_thumbnails, - video_path, - anime_title, - downloads_thumbnail_cache_dir, - ) - ] = anime_title - # execute the jobs - for future in concurrent.futures.as_completed(future_to_url): - url = future_to_url[future] - try: - future.result() - except Exception as e: - logger.error("%r generated an exception: %s" % (url, e)) + def _worker(): + # use concurrency to download the images as fast as possible + with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor: + # load the jobs + future_to_url = {} + for anime_title in anime_downloads: + anime_path = os.path.join(USER_VIDEOS_DIR, anime_title) + if not os.path.isdir(anime_path): + continue + playlist = os.listdir(anime_path) + if playlist: + # actual link to download image from + video_path = os.path.join(anime_path, playlist[0]) + future_to_url[ + executor.submit( + create_thumbnails, + video_path, + anime_title, + downloads_thumbnail_cache_dir, + ) + ] = anime_title + + # execute the jobs + for future in concurrent.futures.as_completed(future_to_url): + url = future_to_url[future] + try: + future.result() + except Exception as e: + logger.error("%r generated an exception: %s" % (url, e)) + + if bg: + from threading import Thread + + worker = Thread(target=_worker) + worker.daemon = True + worker.start() + else: + _worker() os.environ["SHELL"] = shutil.which("bash") or "bash" preview = """ %s - if [ -s %s/{} ]; then fzf-preview %s/{} + if [ -s %s/{} ]; then + if ! fzf-preview %s/{} 2>/dev/null; then + echo Loading... + fi else echo Loading... fi """ % ( @@ -107,7 +144,115 @@ def downloads(config: "Config", path: bool): ) return preview - def stream(): + def get_previews_episodes(anime_playlist_path, workers=None, bg=True): + import shutil + from pathlib import Path + + from ...constants import APP_CACHE_DIR + from ..utils.scripts import fzf_preview + + if not shutil.which("ffmpegthumbnailer"): + print("ffmpegthumbnailer not found") + logger.error("ffmpegthumbnailer not found") + return + + downloads_thumbnail_cache_dir = os.path.join(APP_CACHE_DIR, "video_thumbnails") + Path(downloads_thumbnail_cache_dir).mkdir(parents=True, exist_ok=True) + + def _worker(): + import concurrent.futures + + # use concurrency to download the images as fast as possible + # anime_playlist_path = os.path.join(USER_VIDEOS_DIR, anime_playlist_path) + if not os.path.isdir(anime_playlist_path): + return + anime_episodes = os.listdir(anime_playlist_path) + with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor: + # load the jobs + future_to_url = {} + for episode_title in anime_episodes: + episode_path = os.path.join(anime_playlist_path, episode_title) + + # actual link to download image from + future_to_url[ + executor.submit( + create_thumbnails, + episode_path, + episode_title, + downloads_thumbnail_cache_dir, + ) + ] = episode_title + + # execute the jobs + for future in concurrent.futures.as_completed(future_to_url): + url = future_to_url[future] + try: + future.result() + except Exception as e: + logger.error("%r generated an exception: %s" % (url, e)) + + if bg: + from threading import Thread + + worker = Thread(target=_worker) + worker.daemon = True + worker.start() + else: + _worker() + os.environ["SHELL"] = shutil.which("bash") or "bash" + preview = """ + %s + if [ -s %s/{} ]; then + if ! fzf-preview %s/{} 2>/dev/null; then + echo Loading... + fi + else echo Loading... + fi + """ % ( + fzf_preview, + downloads_thumbnail_cache_dir, + downloads_thumbnail_cache_dir, + ) + return preview + + def stream_episode( + anime_playlist_path, + ): + if view_episodes: + if not os.path.isdir(anime_playlist_path): + print(anime_playlist_path, "is not dir") + exit_app(1) + return + episodes = os.listdir(anime_playlist_path) + downloaded_episodes = [*episodes, "Back"] + if config.use_fzf: + if not config.preview: + episode_title = fzf.run( + downloaded_episodes, + "Enter Episode ", + ) + else: + preview = get_previews_episodes(anime_playlist_path) + episode_title = fzf.run( + downloaded_episodes, + "Enter Episode ", + preview=preview, + ) + elif config.use_rofi: + episode_title = Rofi.run(downloaded_episodes, "Enter Episode") + else: + episode_title = fuzzy_inquirer( + downloaded_episodes, + "Enter Playlist Name: ", + ) + if episode_title == "Back": + stream_anime() + return + episode_path = os.path.join(anime_playlist_path, episode_title) + run_mpv(episode_path) + stream_episode(anime_playlist_path) + + def stream_anime(): if config.use_fzf: if not config.preview: playlist_name = fzf.run( @@ -115,7 +260,7 @@ def downloads(config: "Config", path: bool): "Enter Playlist Name", ) else: - preview = get_previews() + preview = get_previews_anime() playlist_name = fzf.run( anime_downloads, "Enter Playlist Name", @@ -132,7 +277,12 @@ def downloads(config: "Config", path: bool): exit_app() return playlist = os.path.join(USER_VIDEOS_DIR, playlist_name) - run_mpv(playlist) - stream() + if view_episodes: + stream_episode( + playlist, + ) + else: + run_mpv(playlist) + stream_anime() - stream() + stream_anime() diff --git a/fastanime/cli/config.py b/fastanime/cli/config.py index dce14e8..81be731 100644 --- a/fastanime/cli/config.py +++ b/fastanime/cli/config.py @@ -94,6 +94,7 @@ class Config(object): "rofi_theme": "", "rofi_theme_input": "", "rofi_theme_confirm": "", + "ffmpegthumnailer_seek_time": "-1", } ) self.configparser.add_section("stream") @@ -133,6 +134,7 @@ class Config(object): Rofi.rofi_theme_input = self.rofi_theme_input self.rofi_theme_confirm = self.get_rofi_theme_confirm() Rofi.rofi_theme_confirm = self.rofi_theme_confirm + self.ffmpegthumbnailer_seek_time = self.get_ffmpegthumnailer_seek_time() # ---- setup user data ------ self.watch_history: dict = self.user_data.get("watch_history", {}) self.anime_list: list = self.user_data.get("animelist", []) @@ -178,6 +180,9 @@ class Config(object): def get_provider(self): return self.configparser.get("general", "provider") + def get_ffmpegthumnailer_seek_time(self): + return self.configparser.getint("general", "ffmpegthumnailer_seek_time") + def get_preferred_language(self): return self.configparser.get("general", "preferred_language")