feat(feedback-service): make it configurable

This commit is contained in:
Benexl
2025-07-29 14:00:44 +03:00
parent 87372e41be
commit 7f52d8cb39
17 changed files with 292 additions and 200 deletions

View File

@@ -16,7 +16,7 @@ def auth(config: AppConfig, status: bool, logout: bool):
from ....service.feedback import FeedbackService
auth_service = AuthService("anilist")
feedback = FeedbackService(config.general.icons)
feedback = FeedbackService(config)
selector = create_selector(config)
feedback.clear_console()

View File

@@ -123,7 +123,7 @@ def download(config: AppConfig, **options: "Unpack[DownloadOptions]"):
from fastanime.libs.selectors import create_selector
from rich.progress import Progress
feedback = FeedbackService(config.general.icons)
feedback = FeedbackService(config)
selector = create_selector(config)
media_api = create_api_client(config.general.media_api, config)
provider = create_provider(config.general.provider)

View File

@@ -16,7 +16,7 @@ def notifications(config: AppConfig):
from ....service.auth import AuthService
feedback = FeedbackService(config.general.icons)
feedback = FeedbackService(config)
console = Console()
auth = AuthService(config.general.media_api)
api_client = create_api_client(config.general.media_api, config)

View File

@@ -19,6 +19,7 @@ from .. import examples
if TYPE_CHECKING:
from typing import TypedDict
from typing_extensions import Unpack
class SearchOptions(TypedDict, total=False):
@@ -196,7 +197,7 @@ def search(config: AppConfig, **options: "Unpack[SearchOptions]"):
from .....libs.media_api.params import MediaSearchParams
from ....service.feedback import FeedbackService
feedback = FeedbackService(config.general.icons)
feedback = FeedbackService(config)
try:
# Create API client

View File

@@ -23,7 +23,7 @@ def stats(config: "AppConfig"):
console = Console()
feedback = FeedbackService(config.general.icons)
feedback = FeedbackService(config)
auth = AuthService(config.general.media_api)
registry_service = MediaRegistryService(
config.general.media_api, config.media_registry

View File

@@ -11,6 +11,7 @@ if TYPE_CHECKING:
from pathlib import Path
from typing import TypedDict
from fastanime.cli.service.feedback.service import FeedbackService
from typing_extensions import Unpack
from ...libs.provider.anime.base import BaseAnimeProvider
@@ -102,8 +103,7 @@ if TYPE_CHECKING:
)
@click.pass_obj
def download(config: AppConfig, **options: "Unpack[Options]"):
from rich import print
from rich.progress import Progress
from fastanime.cli.service.feedback.service import FeedbackService
from ...core.exceptions import FastAnimeError
from ...libs.provider.anime.params import (
@@ -113,16 +113,16 @@ def download(config: AppConfig, **options: "Unpack[Options]"):
from ...libs.provider.anime.provider import create_provider
from ...libs.selectors.selector import create_selector
feedback = FeedbackService(config)
provider = create_provider(config.general.provider)
selector = create_selector(config)
anime_titles = options["anime_title"]
print(f"[green bold]Streaming:[/] {anime_titles}")
feedback.info(f"[green bold]Streaming:[/] {anime_titles}")
for anime_title in anime_titles:
# ---- search for anime ----
print(f"[green bold]Searching for:[/] {anime_title}")
with Progress() as progress:
progress.add_task("Fetching Search Results...", total=None)
feedback.info(f"[green bold]Searching for:[/] {anime_title}")
with feedback.progress(f"Fetching anime search results for {anime_title}"):
search_results = provider.search(
SearchParams(
query=anime_title, translation_type=config.stream.translation_type
@@ -144,8 +144,7 @@ def download(config: AppConfig, **options: "Unpack[Options]"):
anime_result = _search_results[selected_anime_title]
# ---- fetch selected anime ----
with Progress() as progress:
progress.add_task("Fetching Anime...", total=None)
with feedback.progress(f"Fetching {anime_result.title}"):
anime = provider.get(AnimeParams(id=anime_result.id, query=anime_title))
if not anime:
@@ -165,7 +164,14 @@ def download(config: AppConfig, **options: "Unpack[Options]"):
for episode in episodes_range:
download_anime(
config, options, provider, selector, anime, anime_title, episode
config,
options,
provider,
selector,
feedback,
anime,
anime_title,
episode,
)
except (ValueError, IndexError) as e:
raise FastAnimeError(f"Invalid episode range: {e}") from e
@@ -177,7 +183,14 @@ def download(config: AppConfig, **options: "Unpack[Options]"):
if not episode:
raise FastAnimeError("No episode selected")
download_anime(
config, options, provider, selector, anime, anime_title, episode
config,
options,
provider,
selector,
feedback,
anime,
anime_title,
episode,
)
@@ -186,35 +199,19 @@ def download_anime(
download_options: "Options",
provider: "BaseAnimeProvider",
selector: "BaseSelector",
feedback: "FeedbackService",
anime: "Anime",
anime_title: str,
episode: str,
):
from rich import print
from rich.progress import Progress
from ...core.downloader import DownloadParams, create_downloader
from ...libs.provider.anime.params import EpisodeStreamsParams
downloader = create_downloader(config.downloads)
with Progress() as progress:
progress.add_task("Fetching Episode Streams...", total=None)
streams = provider.episode_streams(
EpisodeStreamsParams(
anime_id=anime.id,
episode=episode,
query=anime_title,
translation_type=config.stream.translation_type,
)
)
if not streams:
raise FastAnimeError(
f"Failed to get streams for anime: {anime.title}, episode: {episode}"
)
with Progress() as progress:
progress.add_task("Fetching Episode Streams...", total=None)
with feedback.progress(f"Fetching episode streams"):
streams = provider.episode_streams(
EpisodeStreamsParams(
anime_id=anime.id,
@@ -229,16 +226,14 @@ def download_anime(
)
if config.stream.server.value == "TOP":
with Progress() as progress:
progress.add_task("Fetching top server...", total=None)
with feedback.progress(f"Fetching top server"):
server = next(streams, None)
if not server:
raise FastAnimeError(
f"Failed to get server for anime: {anime.title}, episode: {episode}"
)
else:
with Progress() as progress:
progress.add_task("Fetching servers", total=None)
with feedback.progress(f"Fetching servers"):
servers = {server.name: server for server in streams}
servers_names = list(servers.keys())
if config.stream.server in servers_names:
@@ -253,7 +248,7 @@ def download_anime(
raise FastAnimeError(
f"Failed to get stream link for anime: {anime.title}, episode: {episode}"
)
print(f"[green bold]Now Downloading:[/] {anime.title} Episode: {episode}")
feedback.info(f"[green bold]Now Downloading:[/] {anime.title} Episode: {episode}")
downloader.download(
DownloadParams(
url=stream_link,

View File

@@ -1,11 +1,16 @@
import click
from fastanime.core.config import AppConfig
from fastanime.libs.media_api.params import MediaSearchParams
from fastanime.core.exceptions import FastAnimeError
from fastanime.libs.media_api.params import MediaSearchParams
@click.command(help="Queue episodes for the background worker to download.")
@click.option("--title", "-t", required=True, multiple=True, help="Anime title to queue.")
@click.option("--episode-range", "-r", required=True, help="Range of episodes (e.g., '1-10').")
@click.option(
"--title", "-t", required=True, multiple=True, help="Anime title to queue."
)
@click.option(
"--episode-range", "-r", required=True, help="Range of episodes (e.g., '1-10')."
)
@click.pass_obj
def queue(config: AppConfig, title: tuple, episode_range: str):
"""
@@ -14,12 +19,12 @@ def queue(config: AppConfig, title: tuple, episode_range: str):
"""
from fastanime.cli.service.download.service import DownloadService
from fastanime.cli.service.feedback import FeedbackService
from fastanime.cli.service.registry import MediaRegistryService
from fastanime.cli.utils.parser import parse_episode_range
from fastanime.libs.media_api.api import create_api_client
from fastanime.libs.provider.anime.provider import create_provider
from fastanime.cli.service.registry import MediaRegistryService
feedback = FeedbackService(config.general.icons)
feedback = FeedbackService(config)
media_api = create_api_client(config.general.media_api, config)
provider = create_provider(config.general.provider)
registry = MediaRegistryService(config.general.media_api, config.media_registry)
@@ -28,7 +33,9 @@ def queue(config: AppConfig, title: tuple, episode_range: str):
for anime_title in title:
try:
feedback.info(f"Searching for '{anime_title}'...")
search_result = media_api.search_media(MediaSearchParams(query=anime_title, per_page=1))
search_result = media_api.search_media(
MediaSearchParams(query=anime_title, per_page=1)
)
if not search_result or not search_result.media:
feedback.warning(f"Could not find '{anime_title}' on AniList.")
@@ -36,14 +43,18 @@ def queue(config: AppConfig, title: tuple, episode_range: str):
media_item = search_result.media[0]
available_episodes = [str(i + 1) for i in range(media_item.episodes or 0)]
episodes_to_queue = list(parse_episode_range(episode_range, available_episodes))
episodes_to_queue = list(
parse_episode_range(episode_range, available_episodes)
)
queued_count = 0
for ep in episodes_to_queue:
if download_service.add_to_queue(media_item, ep):
queued_count += 1
feedback.success(f"Successfully queued {queued_count} episodes for '{media_item.title.english}'.")
feedback.success(
f"Successfully queued {queued_count} episodes for '{media_item.title.english}'."
)
except FastAnimeError as e:
feedback.error(f"Failed to queue '{anime_title}'", str(e))

View File

@@ -42,7 +42,7 @@ def registry(ctx: click.Context, api: str):
from ...service.registry import MediaRegistryService
config: AppConfig = ctx.obj
feedback = FeedbackService()
feedback = FeedbackService(config)
if ctx.invoked_subcommand is None:
# Show registry overview and statistics

View File

@@ -3,6 +3,7 @@ Registry sync command - synchronize local registry with remote media API
"""
import click
from fastanime.cli.service.feedback.service import FeedbackService
from fastanime.cli.service.registry.service import MediaRegistryService
from rich.progress import Progress
@@ -60,7 +61,7 @@ def sync(
from ....service.feedback import FeedbackService
from ....service.registry import MediaRegistryService
feedback = FeedbackService(config.general.icons)
feedback = FeedbackService(config)
auth = AuthService(config.general.media_api)
registry_service = MediaRegistryService(api, config.media_registry)
@@ -100,93 +101,91 @@ def sync(
statuses_to_sync = [status_map[s] for s in status_list]
with Progress() as progress:
if download:
_sync_download(
media_api_client,
registry_service,
statuses_to_sync,
feedback,
progress,
dry_run,
force,
)
if download:
_sync_download(
media_api_client,
registry_service,
statuses_to_sync,
feedback,
dry_run,
force,
)
if upload:
_sync_upload(
media_api_client,
registry_service,
statuses_to_sync,
feedback,
progress,
dry_run,
force,
)
if upload:
_sync_upload(
media_api_client,
registry_service,
statuses_to_sync,
feedback,
dry_run,
force,
)
feedback.success("Sync Complete", "Registry synchronization finished successfully")
def _sync_download(
api_client, registry_service, statuses, feedback, progress, dry_run, force
api_client, registry_service, statuses, feedback: "FeedbackService", dry_run, force
):
"""Download remote media list to local registry."""
from .....libs.media_api.params import UserMediaListSearchParams
feedback.info("Starting Download", "Fetching remote media lists...")
download_task = progress.add_task("Downloading media lists...", total=len(statuses))
total_downloaded = 0
total_updated = 0
with feedback.progress("Downloading media lists...", total=len(statuses)) as (
task_id,
progress,
):
for status in statuses:
try:
# Fetch all pages for this status
page = 1
while True:
params = UserMediaListSearchParams(
status=status, page=page, per_page=50
)
for status in statuses:
try:
# Fetch all pages for this status
page = 1
while True:
params = UserMediaListSearchParams(
status=status, page=page, per_page=50
)
result = api_client.search_media_list(params)
if not result or not result.media:
break
result = api_client.search_media_list(params)
if not result or not result.media:
break
for media_item in result.media:
if dry_run:
feedback.info(
"Would download",
f"{media_item.title.english or media_item.title.romaji} ({status.value})",
)
else:
# Get or create record and update with user status
record = registry_service.get_or_create_record(media_item)
# Update index entry with latest status
if media_item.user_status:
registry_service.update_media_index_entry(
media_item.id,
media_item=media_item,
status=media_item.user_status.status,
progress=str(media_item.user_status.progress or 0),
score=media_item.user_status.score,
repeat=media_item.user_status.repeat,
notes=media_item.user_status.notes,
for media_item in result.media:
if dry_run:
feedback.info(
"Would download",
f"{media_item.title.english or media_item.title.romaji} ({status.value})",
)
total_updated += 1
else:
# Get or create record and update with user status
record = registry_service.get_or_create_record(media_item)
registry_service.save_media_record(record)
total_downloaded += 1
# Update index entry with latest status
if media_item.user_status:
registry_service.update_media_index_entry(
media_item.id,
media_item=media_item,
status=media_item.user_status.status,
progress=str(media_item.user_status.progress or 0),
score=media_item.user_status.score,
repeat=media_item.user_status.repeat,
notes=media_item.user_status.notes,
)
total_updated += 1
if not result.page_info.has_next_page:
break
page += 1
registry_service.save_media_record(record)
total_downloaded += 1
except Exception as e:
feedback.error(f"Download Error ({status.value})", str(e))
continue
if not result.page_info.has_next_page:
break
page += 1
progress.advance(download_task)
except Exception as e:
feedback.error(f"Download Error ({status.value})", str(e))
continue
progress.advance(task_id) # type:ignore
if not dry_run:
feedback.success(
@@ -200,76 +199,72 @@ def _sync_upload(
registry_service: MediaRegistryService,
statuses,
feedback,
progress,
dry_run,
force,
):
"""Upload local registry changes to remote API."""
feedback.info("Starting Upload", "Syncing local changes to remote...")
upload_task = progress.add_task("Uploading changes...", total=None)
total_uploaded = 0
total_errors = 0
try:
# Get all media records from registry
all_records = registry_service.get_all_media_records()
with feedback.progress("Uploading changes..."):
try:
# Get all media records from registry
all_records = registry_service.get_all_media_records()
for record in all_records:
try:
# Get the index entry for this media
index_entry = registry_service.get_media_index_entry(
record.media_item.id
)
if not index_entry or not index_entry.status:
continue
# Only sync if status is in our target list
if index_entry.status.value not in statuses:
continue
if dry_run:
feedback.info(
"Would upload",
f"{record.media_item.title.english or record.media_item.title.romaji} "
f"({index_entry.status.value}, progress: {index_entry.progress or 0})",
)
else:
# Update remote list entry
from .....libs.media_api.params import (
UpdateUserMediaListEntryParams,
for record in all_records:
try:
# Get the index entry for this media
index_entry = registry_service.get_media_index_entry(
record.media_item.id
)
if not index_entry or not index_entry.status:
continue
update_params = UpdateUserMediaListEntryParams(
media_id=record.media_item.id,
status=index_entry.status,
progress=index_entry.progress,
score=index_entry.score,
)
# Only sync if status is in our target list
if index_entry.status.value not in statuses:
continue
if api_client.update_list_entry(update_params):
total_uploaded += 1
if dry_run:
feedback.info(
"Would upload",
f"{record.media_item.title.english or record.media_item.title.romaji} "
f"({index_entry.status.value}, progress: {index_entry.progress or 0})",
)
else:
total_errors += 1
feedback.warning(
"Upload Failed",
f"Failed to upload {record.media_item.title.english or record.media_item.title.romaji}",
# Update remote list entry
from .....libs.media_api.params import (
UpdateUserMediaListEntryParams,
)
except Exception as e:
total_errors += 1
feedback.error(
"Upload Error",
f"Failed to upload media {record.media_item.id}: {e}",
)
continue
update_params = UpdateUserMediaListEntryParams(
media_id=record.media_item.id,
status=index_entry.status,
progress=index_entry.progress,
score=index_entry.score,
)
except Exception as e:
feedback.error("Upload Error", f"Failed to get local records: {e}")
return
if api_client.update_list_entry(update_params):
total_uploaded += 1
else:
total_errors += 1
feedback.warning(
"Upload Failed",
f"Failed to upload {record.media_item.title.english or record.media_item.title.romaji}",
)
progress.remove_task(upload_task)
except Exception as e:
total_errors += 1
feedback.error(
"Upload Error",
f"Failed to upload media {record.media_item.id}: {e}",
)
continue
except Exception as e:
feedback.error("Upload Error", f"Failed to get local records: {e}")
return
if not dry_run:
feedback.success(

View File

@@ -1,7 +1,6 @@
from typing import TYPE_CHECKING
import click
from fastanime.cli.service.player.service import PlayerService
from ...core.config import AppConfig
from ...core.exceptions import FastAnimeError
@@ -11,6 +10,7 @@ from . import examples
if TYPE_CHECKING:
from typing import TypedDict
from fastanime.cli.service.feedback.service import FeedbackService
from typing_extensions import Unpack
from ...libs.provider.anime.base import BaseAnimeProvider
@@ -42,8 +42,7 @@ if TYPE_CHECKING:
)
@click.pass_obj
def search(config: AppConfig, **options: "Unpack[Options]"):
from rich import print
from rich.progress import Progress
from fastanime.cli.service.feedback.service import FeedbackService
from ...core.exceptions import FastAnimeError
from ...libs.provider.anime.params import (
@@ -53,16 +52,16 @@ def search(config: AppConfig, **options: "Unpack[Options]"):
from ...libs.provider.anime.provider import create_provider
from ...libs.selectors.selector import create_selector
feedback = FeedbackService(config)
provider = create_provider(config.general.provider)
selector = create_selector(config)
anime_titles = options["anime_title"]
print(f"[green bold]Streaming:[/] {anime_titles}")
feedback.info(f"[green bold]Streaming:[/] {anime_titles}")
for anime_title in anime_titles:
# ---- search for anime ----
print(f"[green bold]Searching for:[/] {anime_title}")
with Progress() as progress:
progress.add_task("Fetching Search Results...", total=None)
feedback.info(f"[green bold]Searching for:[/] {anime_title}")
with feedback.progress(f"Fetching anime search results for {anime_title}"):
search_results = provider.search(
SearchParams(
query=anime_title, translation_type=config.stream.translation_type
@@ -84,8 +83,7 @@ def search(config: AppConfig, **options: "Unpack[Options]"):
anime_result = _search_results[selected_anime_title]
# ---- fetch selected anime ----
with Progress() as progress:
progress.add_task("Fetching Anime...", total=None)
with feedback.progress(f"Fetching {anime_result.title}"):
anime = provider.get(AnimeParams(id=anime_result.id, query=anime_title))
if not anime:
@@ -105,7 +103,13 @@ def search(config: AppConfig, **options: "Unpack[Options]"):
for episode in episodes_range:
stream_anime(
config, provider, selector, anime, episode, anime_title
config,
provider,
selector,
feedback,
anime,
episode,
anime_title,
)
except (ValueError, IndexError) as e:
raise FastAnimeError(f"Invalid episode range: {e}") from e
@@ -116,27 +120,28 @@ def search(config: AppConfig, **options: "Unpack[Options]"):
)
if not episode:
raise FastAnimeError("No episode selected")
stream_anime(config, provider, selector, anime, episode, anime_title)
stream_anime(
config, provider, selector, feedback, anime, episode, anime_title
)
def stream_anime(
config: AppConfig,
provider: "BaseAnimeProvider",
selector: "BaseSelector",
feedback: "FeedbackService",
anime: "Anime",
episode: str,
anime_title: str,
):
from rich import print
from rich.progress import Progress
from fastanime.cli.service.player.service import PlayerService
from ...libs.player.params import PlayerParams
from ...libs.provider.anime.params import EpisodeStreamsParams
player_service = PlayerService(config, provider)
with Progress() as progress:
progress.add_task("Fetching Episode Streams...", total=None)
with feedback.progress(f"Fetching episode streams"):
streams = provider.episode_streams(
EpisodeStreamsParams(
anime_id=anime.id,
@@ -151,16 +156,14 @@ def stream_anime(
)
if config.stream.server.value == "TOP":
with Progress() as progress:
progress.add_task("Fetching top server...", total=None)
with feedback.progress(f"Fetching top server"):
server = next(streams, None)
if not server:
raise FastAnimeError(
f"Failed to get server for anime: {anime.title}, episode: {episode}"
)
else:
with Progress() as progress:
progress.add_task("Fetching servers", total=None)
with feedback.progress(f"Fetching servers"):
servers = {server.name: server for server in streams}
servers_names = list(servers.keys())
if config.stream.server.value in servers_names:
@@ -175,7 +178,7 @@ def stream_anime(
raise FastAnimeError(
f"Failed to get stream link for anime: {anime.title}, episode: {episode}"
)
print(f"[green bold]Now Streaming:[/] {anime.title} Episode: {episode}")
feedback.info(f"[green bold]Now Streaming:[/] {anime.title} Episode: {episode}")
player_service.play(
PlayerParams(

View File

@@ -19,7 +19,7 @@ def worker(config: AppConfig):
from fastanime.libs.media_api.api import create_api_client
from fastanime.libs.provider.anime.provider import create_provider
feedback = FeedbackService(config.general.icons)
feedback = FeedbackService(config)
if not config.worker.enabled:
feedback.warning("Worker is disabled in the configuration. Exiting.")
return