mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-15 09:00:51 -08:00
feat(downloads command): improve local downloads experience
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
|
import logging
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..config import Config
|
from ..config import Config
|
||||||
|
|
||||||
@@ -10,9 +12,16 @@ if TYPE_CHECKING:
|
|||||||
help="View and watch your downloads using mpv", short_help="Watch downloads"
|
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("--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
|
@click.pass_obj
|
||||||
def downloads(config: "Config", path: bool):
|
def downloads(config: "Config", path: bool, view_episodes, ffmpegthumbnailer_seek_time):
|
||||||
import logging
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from ...cli.utils.mpv import run_mpv
|
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.tools import exit_app
|
||||||
from ..utils.utils import fuzzy_inquirer
|
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
|
USER_VIDEOS_DIR = config.downloads_dir
|
||||||
if path:
|
if path:
|
||||||
print(USER_VIDEOS_DIR)
|
print(USER_VIDEOS_DIR)
|
||||||
@@ -43,15 +52,29 @@ def downloads(config: "Config", path: bool):
|
|||||||
return
|
return
|
||||||
|
|
||||||
out = os.path.join(downloads_thumbnail_cache_dir, anime_title)
|
out = os.path.join(downloads_thumbnail_cache_dir, anime_title)
|
||||||
completed_process = subprocess.run(
|
if ffmpegthumbnailer_seek_time == -1:
|
||||||
[FFMPEG_THUMBNAILER, "-i", video_path, "-o", out], stderr=subprocess.PIPE
|
import random
|
||||||
)
|
|
||||||
if completed_process.returncode == 0:
|
|
||||||
logger.info(f"Success in creating {anime_title} thumbnail")
|
|
||||||
else:
|
|
||||||
logger.warn(f"Failed in creating {anime_title} thumbnail")
|
|
||||||
|
|
||||||
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 concurrent.futures
|
||||||
import shutil
|
import shutil
|
||||||
from pathlib import Path
|
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")
|
downloads_thumbnail_cache_dir = os.path.join(APP_CACHE_DIR, "video_thumbnails")
|
||||||
Path(downloads_thumbnail_cache_dir).mkdir(parents=True, exist_ok=True)
|
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
|
def _worker():
|
||||||
for future in concurrent.futures.as_completed(future_to_url):
|
# use concurrency to download the images as fast as possible
|
||||||
url = future_to_url[future]
|
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
|
||||||
try:
|
# load the jobs
|
||||||
future.result()
|
future_to_url = {}
|
||||||
except Exception as e:
|
for anime_title in anime_downloads:
|
||||||
logger.error("%r generated an exception: %s" % (url, e))
|
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"
|
os.environ["SHELL"] = shutil.which("bash") or "bash"
|
||||||
preview = """
|
preview = """
|
||||||
%s
|
%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...
|
else echo Loading...
|
||||||
fi
|
fi
|
||||||
""" % (
|
""" % (
|
||||||
@@ -107,7 +144,115 @@ def downloads(config: "Config", path: bool):
|
|||||||
)
|
)
|
||||||
return preview
|
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 config.use_fzf:
|
||||||
if not config.preview:
|
if not config.preview:
|
||||||
playlist_name = fzf.run(
|
playlist_name = fzf.run(
|
||||||
@@ -115,7 +260,7 @@ def downloads(config: "Config", path: bool):
|
|||||||
"Enter Playlist Name",
|
"Enter Playlist Name",
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
preview = get_previews()
|
preview = get_previews_anime()
|
||||||
playlist_name = fzf.run(
|
playlist_name = fzf.run(
|
||||||
anime_downloads,
|
anime_downloads,
|
||||||
"Enter Playlist Name",
|
"Enter Playlist Name",
|
||||||
@@ -132,7 +277,12 @@ def downloads(config: "Config", path: bool):
|
|||||||
exit_app()
|
exit_app()
|
||||||
return
|
return
|
||||||
playlist = os.path.join(USER_VIDEOS_DIR, playlist_name)
|
playlist = os.path.join(USER_VIDEOS_DIR, playlist_name)
|
||||||
run_mpv(playlist)
|
if view_episodes:
|
||||||
stream()
|
stream_episode(
|
||||||
|
playlist,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
run_mpv(playlist)
|
||||||
|
stream_anime()
|
||||||
|
|
||||||
stream()
|
stream_anime()
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ class Config(object):
|
|||||||
"rofi_theme": "",
|
"rofi_theme": "",
|
||||||
"rofi_theme_input": "",
|
"rofi_theme_input": "",
|
||||||
"rofi_theme_confirm": "",
|
"rofi_theme_confirm": "",
|
||||||
|
"ffmpegthumnailer_seek_time": "-1",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
self.configparser.add_section("stream")
|
self.configparser.add_section("stream")
|
||||||
@@ -133,6 +134,7 @@ class Config(object):
|
|||||||
Rofi.rofi_theme_input = self.rofi_theme_input
|
Rofi.rofi_theme_input = self.rofi_theme_input
|
||||||
self.rofi_theme_confirm = self.get_rofi_theme_confirm()
|
self.rofi_theme_confirm = self.get_rofi_theme_confirm()
|
||||||
Rofi.rofi_theme_confirm = self.rofi_theme_confirm
|
Rofi.rofi_theme_confirm = self.rofi_theme_confirm
|
||||||
|
self.ffmpegthumbnailer_seek_time = self.get_ffmpegthumnailer_seek_time()
|
||||||
# ---- setup user data ------
|
# ---- setup user data ------
|
||||||
self.watch_history: dict = self.user_data.get("watch_history", {})
|
self.watch_history: dict = self.user_data.get("watch_history", {})
|
||||||
self.anime_list: list = self.user_data.get("animelist", [])
|
self.anime_list: list = self.user_data.get("animelist", [])
|
||||||
@@ -178,6 +180,9 @@ class Config(object):
|
|||||||
def get_provider(self):
|
def get_provider(self):
|
||||||
return self.configparser.get("general", "provider")
|
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):
|
def get_preferred_language(self):
|
||||||
return self.configparser.get("general", "preferred_language")
|
return self.configparser.get("general", "preferred_language")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user