feat(agent): migrate all remaining tools to YAML specs (21/21 covered)
Adds YAML specs for the 14 tools that were still description-from-docstring:
filesystem:
- set_path_for_folder, list_folder, analyze_release, probe_media,
move_media, manage_subtitles, create_seed_links, learn
api:
- find_media_imdb_id, find_torrent, get_torrent_by_index,
add_torrent_to_qbittorrent, add_torrent_by_index
language:
- set_language
Each spec follows the established shape (summary / description /
when_to_use / when_not_to_use / next_steps / parameters with
why_needed + example / returns) and the Python function docstring is
slimmed to a one-line pointer.
Registry now reports: 21 tools, 21 with YAML spec, 0 doc-only.
This commit is contained in:
@@ -14,15 +14,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
def find_media_imdb_id(media_title: str) -> dict[str, Any]:
|
def find_media_imdb_id(media_title: str) -> dict[str, Any]:
|
||||||
"""
|
"""Thin tool wrapper — semantics live in alfred/agent/tools/specs/find_media_imdb_id.yaml."""
|
||||||
Find the IMDb ID for a given media title using TMDB API.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
media_title: Title of the media to search for.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict with IMDb ID and media info, or error details.
|
|
||||||
"""
|
|
||||||
use_case = SearchMovieUseCase(tmdb_client)
|
use_case = SearchMovieUseCase(tmdb_client)
|
||||||
response = use_case.execute(media_title)
|
response = use_case.execute(media_title)
|
||||||
result = response.to_dict()
|
result = response.to_dict()
|
||||||
@@ -45,18 +37,7 @@ def find_media_imdb_id(media_title: str) -> dict[str, Any]:
|
|||||||
|
|
||||||
|
|
||||||
def find_torrent(media_title: str) -> dict[str, Any]:
|
def find_torrent(media_title: str) -> dict[str, Any]:
|
||||||
"""
|
"""Thin tool wrapper — semantics live in alfred/agent/tools/specs/find_torrent.yaml."""
|
||||||
Find torrents for a given media title using Knaben API.
|
|
||||||
|
|
||||||
Results are stored in episodic memory so the user can reference them
|
|
||||||
by index (e.g., "download the 3rd one").
|
|
||||||
|
|
||||||
Args:
|
|
||||||
media_title: Title of the media to search for.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict with torrent list or error details.
|
|
||||||
"""
|
|
||||||
logger.info(f"Searching torrents for: {media_title}")
|
logger.info(f"Searching torrents for: {media_title}")
|
||||||
|
|
||||||
use_case = SearchTorrentsUseCase(knaben_client)
|
use_case = SearchTorrentsUseCase(knaben_client)
|
||||||
@@ -76,17 +57,7 @@ def find_torrent(media_title: str) -> dict[str, Any]:
|
|||||||
|
|
||||||
|
|
||||||
def get_torrent_by_index(index: int) -> dict[str, Any]:
|
def get_torrent_by_index(index: int) -> dict[str, Any]:
|
||||||
"""
|
"""Thin tool wrapper — semantics live in alfred/agent/tools/specs/get_torrent_by_index.yaml."""
|
||||||
Get a torrent from the last search results by its index.
|
|
||||||
|
|
||||||
Allows the user to reference results by number after a search.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
index: 1-based index of the torrent in the search results.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict with torrent data or error if not found.
|
|
||||||
"""
|
|
||||||
logger.info(f"Getting torrent at index: {index}")
|
logger.info(f"Getting torrent at index: {index}")
|
||||||
|
|
||||||
memory = get_memory()
|
memory = get_memory()
|
||||||
@@ -113,15 +84,7 @@ def get_torrent_by_index(index: int) -> dict[str, Any]:
|
|||||||
|
|
||||||
|
|
||||||
def add_torrent_to_qbittorrent(magnet_link: str) -> dict[str, Any]:
|
def add_torrent_to_qbittorrent(magnet_link: str) -> dict[str, Any]:
|
||||||
"""
|
"""Thin tool wrapper — semantics live in alfred/agent/tools/specs/add_torrent_to_qbittorrent.yaml."""
|
||||||
Add a torrent to qBittorrent using a magnet link.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
magnet_link: Magnet link of the torrent to add.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict with success status or error details.
|
|
||||||
"""
|
|
||||||
logger.info("Adding torrent to qBittorrent")
|
logger.info("Adding torrent to qBittorrent")
|
||||||
|
|
||||||
use_case = AddTorrentUseCase(qbittorrent_client)
|
use_case = AddTorrentUseCase(qbittorrent_client)
|
||||||
@@ -157,17 +120,7 @@ def add_torrent_to_qbittorrent(magnet_link: str) -> dict[str, Any]:
|
|||||||
|
|
||||||
|
|
||||||
def add_torrent_by_index(index: int) -> dict[str, Any]:
|
def add_torrent_by_index(index: int) -> dict[str, Any]:
|
||||||
"""
|
"""Thin tool wrapper — semantics live in alfred/agent/tools/specs/add_torrent_by_index.yaml."""
|
||||||
Add a torrent from the last search results by its index.
|
|
||||||
|
|
||||||
Combines get_torrent_by_index and add_torrent_to_qbittorrent.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
index: 1-based index of the torrent in the search results.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict with success status or error details.
|
|
||||||
"""
|
|
||||||
logger.info(f"Adding torrent by index: {index}")
|
logger.info(f"Adding torrent by index: {index}")
|
||||||
|
|
||||||
torrent_result = get_torrent_by_index(index)
|
torrent_result = get_torrent_by_index(index)
|
||||||
|
|||||||
@@ -17,8 +17,14 @@ from alfred.application.filesystem.detect_media_type import detect_media_type
|
|||||||
from alfred.application.filesystem.enrich_from_probe import enrich_from_probe
|
from alfred.application.filesystem.enrich_from_probe import enrich_from_probe
|
||||||
from alfred.application.filesystem.resolve_destination import (
|
from alfred.application.filesystem.resolve_destination import (
|
||||||
resolve_episode_destination as _resolve_episode_destination,
|
resolve_episode_destination as _resolve_episode_destination,
|
||||||
|
)
|
||||||
|
from alfred.application.filesystem.resolve_destination import (
|
||||||
resolve_movie_destination as _resolve_movie_destination,
|
resolve_movie_destination as _resolve_movie_destination,
|
||||||
|
)
|
||||||
|
from alfred.application.filesystem.resolve_destination import (
|
||||||
resolve_season_destination as _resolve_season_destination,
|
resolve_season_destination as _resolve_season_destination,
|
||||||
|
)
|
||||||
|
from alfred.application.filesystem.resolve_destination import (
|
||||||
resolve_series_destination as _resolve_series_destination,
|
resolve_series_destination as _resolve_series_destination,
|
||||||
)
|
)
|
||||||
from alfred.infrastructure.filesystem import FileManager, create_folder, move
|
from alfred.infrastructure.filesystem import FileManager, create_folder, move
|
||||||
@@ -29,19 +35,7 @@ _LEARNED_ROOT = Path(_alfred_pkg.__file__).parent.parent / "data" / "knowledge"
|
|||||||
|
|
||||||
|
|
||||||
def move_media(source: str, destination: str) -> dict[str, Any]:
|
def move_media(source: str, destination: str) -> dict[str, Any]:
|
||||||
"""
|
"""Thin tool wrapper — semantics live in alfred/agent/tools/specs/move_media.yaml."""
|
||||||
Move a media file to a destination path.
|
|
||||||
|
|
||||||
Copies the file safely first (with integrity check), then deletes the source.
|
|
||||||
Use this to organise a downloaded file into the media library.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
source: Absolute path to the source file.
|
|
||||||
destination: Absolute path to the destination file (must not already exist).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict with status, source, destination, filename, and size — or error details.
|
|
||||||
"""
|
|
||||||
file_manager = FileManager()
|
file_manager = FileManager()
|
||||||
use_case = MoveMediaUseCase(file_manager)
|
use_case = MoveMediaUseCase(file_manager)
|
||||||
return use_case.execute(source, destination).to_dict()
|
return use_case.execute(source, destination).to_dict()
|
||||||
@@ -114,72 +108,21 @@ def resolve_series_destination(
|
|||||||
def create_seed_links(
|
def create_seed_links(
|
||||||
library_file: str, original_download_folder: str
|
library_file: str, original_download_folder: str
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""
|
"""Thin tool wrapper — semantics live in alfred/agent/tools/specs/create_seed_links.yaml."""
|
||||||
Prepare a torrent subfolder so qBittorrent can keep seeding after a move.
|
|
||||||
|
|
||||||
Hard-links the video file from the library into torrents/<original_folder_name>/,
|
|
||||||
then copies all remaining files from the original download folder (subtitles,
|
|
||||||
.nfo, .jpg, .txt, …) so the torrent data is complete.
|
|
||||||
|
|
||||||
Call this after move_media when the user wants to keep seeding.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
library_file: Absolute path to the video file now in the library.
|
|
||||||
original_download_folder: Absolute path to the original download folder
|
|
||||||
(may still contain subs, nfo, and other release files).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict with status, torrent_subfolder, linked_file, copied_files,
|
|
||||||
copied_count, skipped — or error details.
|
|
||||||
"""
|
|
||||||
file_manager = FileManager()
|
file_manager = FileManager()
|
||||||
use_case = CreateSeedLinksUseCase(file_manager)
|
use_case = CreateSeedLinksUseCase(file_manager)
|
||||||
return use_case.execute(library_file, original_download_folder).to_dict()
|
return use_case.execute(library_file, original_download_folder).to_dict()
|
||||||
|
|
||||||
|
|
||||||
def manage_subtitles(source_video: str, destination_video: str) -> dict[str, Any]:
|
def manage_subtitles(source_video: str, destination_video: str) -> dict[str, Any]:
|
||||||
"""
|
"""Thin tool wrapper — semantics live in alfred/agent/tools/specs/manage_subtitles.yaml."""
|
||||||
Place subtitle files alongside an organised video file.
|
|
||||||
|
|
||||||
Scans for subtitle files (.srt, .ass, .ssa, .vtt, .sub) next to the source
|
|
||||||
video, filters them according to the user's SubtitlePreferences (languages,
|
|
||||||
min size, SDH, forced), and hard-links the passing files next to the
|
|
||||||
destination video with the correct naming convention:
|
|
||||||
fr.srt / fr.sdh.srt / fr.forced.srt / en.srt …
|
|
||||||
|
|
||||||
Call this right after move_media or copy_media, passing the same source and
|
|
||||||
destination paths. If no subtitles are found, returns ok with placed_count=0.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
source_video: Absolute path to the original video file (in the download folder).
|
|
||||||
destination_video: Absolute path to the placed video file (in the library).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict with status, placed list (source, destination, filename), placed_count,
|
|
||||||
skipped_count — or error details.
|
|
||||||
"""
|
|
||||||
file_manager = FileManager()
|
file_manager = FileManager()
|
||||||
use_case = ManageSubtitlesUseCase(file_manager)
|
use_case = ManageSubtitlesUseCase(file_manager)
|
||||||
return use_case.execute(source_video, destination_video).to_dict()
|
return use_case.execute(source_video, destination_video).to_dict()
|
||||||
|
|
||||||
|
|
||||||
def learn(pack: str, category: str, key: str, values: list[str]) -> dict[str, Any]:
|
def learn(pack: str, category: str, key: str, values: list[str]) -> dict[str, Any]:
|
||||||
"""
|
"""Thin tool wrapper — semantics live in alfred/agent/tools/specs/learn.yaml."""
|
||||||
Teach Alfred a new token mapping and persist it to the learned knowledge pack.
|
|
||||||
|
|
||||||
Use this when a subtitle file contains an unrecognised token — after confirming
|
|
||||||
with the user what the token means, call learn() to persist it so Alfred
|
|
||||||
recognises it in future scans.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
pack: Knowledge pack name. Currently only "subtitles" is supported.
|
|
||||||
category: Category within the pack: "languages", "types", or "formats".
|
|
||||||
key: The entry key — e.g. ISO 639-1 language code ("es"), type id ("sdh").
|
|
||||||
values: List of tokens to add — e.g. ["spanish", "espanol", "spa"].
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict with status, added_count, and the updated token list.
|
|
||||||
"""
|
|
||||||
_VALID_PACKS = {"subtitles"}
|
_VALID_PACKS = {"subtitles"}
|
||||||
_VALID_CATEGORIES = {"languages", "types", "formats"}
|
_VALID_CATEGORIES = {"languages", "types", "formats"}
|
||||||
|
|
||||||
@@ -236,16 +179,7 @@ def learn(pack: str, category: str, key: str, values: list[str]) -> dict[str, An
|
|||||||
|
|
||||||
|
|
||||||
def set_path_for_folder(folder_name: str, path_value: str) -> dict[str, Any]:
|
def set_path_for_folder(folder_name: str, path_value: str) -> dict[str, Any]:
|
||||||
"""
|
"""Thin tool wrapper — semantics live in alfred/agent/tools/specs/set_path_for_folder.yaml."""
|
||||||
Set a folder path in the configuration.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
folder_name: Name of folder to set (download, tvshow, movie, torrent).
|
|
||||||
path_value: Absolute path to the folder.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict with status or error information.
|
|
||||||
"""
|
|
||||||
file_manager = FileManager()
|
file_manager = FileManager()
|
||||||
use_case = SetFolderPathUseCase(file_manager)
|
use_case = SetFolderPathUseCase(file_manager)
|
||||||
response = use_case.execute(folder_name, path_value)
|
response = use_case.execute(folder_name, path_value)
|
||||||
@@ -253,24 +187,8 @@ def set_path_for_folder(folder_name: str, path_value: str) -> dict[str, Any]:
|
|||||||
|
|
||||||
|
|
||||||
def analyze_release(release_name: str, source_path: str) -> dict[str, Any]:
|
def analyze_release(release_name: str, source_path: str) -> dict[str, Any]:
|
||||||
"""
|
"""Thin tool wrapper — semantics live in alfred/agent/tools/specs/analyze_release.yaml."""
|
||||||
Fully analyze a release: parse name, detect media type, probe video with ffprobe.
|
from alfred.domain.release.services import parse_release # noqa: PLC0415
|
||||||
|
|
||||||
Combines parse_release + filesystem type detection + ffprobe in a single call.
|
|
||||||
Use this at the start of any organize workflow to get a complete picture before
|
|
||||||
deciding how to route the release.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
release_name: Raw release folder or file name.
|
|
||||||
source_path: Absolute path to the release folder or file on disk.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict with all parsed fields: media_type, title, year, season, episode,
|
|
||||||
quality, codec, source, group, languages, audio_codec, audio_channels,
|
|
||||||
bit_depth, hdr_format, edition, site_tag, parse_path,
|
|
||||||
and probe_used (bool).
|
|
||||||
"""
|
|
||||||
from alfred.domain.release.services import parse_release
|
|
||||||
|
|
||||||
path = Path(source_path)
|
path = Path(source_path)
|
||||||
parsed = parse_release(release_name)
|
parsed = parse_release(release_name)
|
||||||
@@ -311,21 +229,7 @@ def analyze_release(release_name: str, source_path: str) -> dict[str, Any]:
|
|||||||
|
|
||||||
|
|
||||||
def probe_media(source_path: str) -> dict[str, Any]:
|
def probe_media(source_path: str) -> dict[str, Any]:
|
||||||
"""
|
"""Thin tool wrapper — semantics live in alfred/agent/tools/specs/probe_media.yaml."""
|
||||||
Run ffprobe on a video file and return detailed media information.
|
|
||||||
|
|
||||||
Use this to inspect a specific file for codec, resolution, audio tracks,
|
|
||||||
languages, and embedded subtitles — independently of release name parsing.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
source_path: Absolute path to the video file.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict with video (codec, resolution, width, height, duration, bitrate),
|
|
||||||
audio_tracks (list of codec/channels/language), subtitle_tracks
|
|
||||||
(list of codec/language/forced), audio_languages, is_multi_audio —
|
|
||||||
or error if ffprobe fails.
|
|
||||||
"""
|
|
||||||
path = Path(source_path)
|
path = Path(source_path)
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
return {
|
return {
|
||||||
@@ -379,16 +283,7 @@ def probe_media(source_path: str) -> dict[str, Any]:
|
|||||||
|
|
||||||
|
|
||||||
def list_folder(folder_type: str, path: str = ".") -> dict[str, Any]:
|
def list_folder(folder_type: str, path: str = ".") -> dict[str, Any]:
|
||||||
"""
|
"""Thin tool wrapper — semantics live in alfred/agent/tools/specs/list_folder.yaml."""
|
||||||
List contents of a configured folder.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
folder_type: Type of folder to list (download, tvshow, movie, torrent).
|
|
||||||
path: Relative path within the folder (default: root).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict with folder contents or error information.
|
|
||||||
"""
|
|
||||||
file_manager = FileManager()
|
file_manager = FileManager()
|
||||||
use_case = ListFolderUseCase(file_manager)
|
use_case = ListFolderUseCase(file_manager)
|
||||||
response = use_case.execute(folder_type, path)
|
response = use_case.execute(folder_type, path)
|
||||||
|
|||||||
@@ -9,15 +9,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
def set_language(language: str) -> dict[str, Any]:
|
def set_language(language: str) -> dict[str, Any]:
|
||||||
"""
|
"""Thin tool wrapper — semantics live in alfred/agent/tools/specs/set_language.yaml."""
|
||||||
Set the conversation language.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
language: Language code (e.g., 'en', 'fr', 'es', 'de')
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Status dictionary
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
memory = get_memory()
|
memory = get_memory()
|
||||||
memory.stm.set_language(language)
|
memory.stm.set_language(language)
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
name: add_torrent_by_index
|
||||||
|
|
||||||
|
summary: >
|
||||||
|
Pick a torrent from the last find_torrent results by index and add
|
||||||
|
it to qBittorrent in one call.
|
||||||
|
|
||||||
|
description: |
|
||||||
|
Convenience wrapper that combines get_torrent_by_index +
|
||||||
|
add_torrent_to_qbittorrent. Looks up the torrent at the given
|
||||||
|
1-based index, extracts its magnet link, and sends it to
|
||||||
|
qBittorrent. The result mirrors add_torrent_to_qbittorrent's, with
|
||||||
|
the chosen torrent's name appended on success.
|
||||||
|
|
||||||
|
when_to_use: |
|
||||||
|
The default action after find_torrent when the user picks a hit by
|
||||||
|
number ("download the second one"). One call, two side effects:
|
||||||
|
episodic memory updated + download started.
|
||||||
|
|
||||||
|
when_not_to_use: |
|
||||||
|
- When the user only wants to inspect, not download — use
|
||||||
|
get_torrent_by_index.
|
||||||
|
- When the magnet comes from outside the search results — use
|
||||||
|
add_torrent_to_qbittorrent directly.
|
||||||
|
|
||||||
|
next_steps: |
|
||||||
|
- On status=ok: confirm the download started and end the workflow
|
||||||
|
if not already ended.
|
||||||
|
- On status=error (not_found): the index is out of range; show the
|
||||||
|
available count from episodic memory.
|
||||||
|
- On status=error (no_magnet): the search result was malformed —
|
||||||
|
suggest re-running find_torrent.
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
index:
|
||||||
|
description: 1-based position of the torrent in the last find_torrent results.
|
||||||
|
why_needed: |
|
||||||
|
Identifies which torrent to add. Out-of-range indices return
|
||||||
|
not_found.
|
||||||
|
example: 3
|
||||||
|
|
||||||
|
returns:
|
||||||
|
ok:
|
||||||
|
description: Torrent was added to qBittorrent.
|
||||||
|
fields:
|
||||||
|
status: "'ok'"
|
||||||
|
message: Confirmation message.
|
||||||
|
torrent_name: Name of the torrent that was added.
|
||||||
|
|
||||||
|
error:
|
||||||
|
description: Failed to add.
|
||||||
|
fields:
|
||||||
|
error: Short error code (not_found, no_magnet, ...).
|
||||||
|
message: Human-readable explanation.
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
name: add_torrent_to_qbittorrent
|
||||||
|
|
||||||
|
summary: >
|
||||||
|
Send a magnet link to qBittorrent and start the download.
|
||||||
|
|
||||||
|
description: |
|
||||||
|
Adds a torrent to qBittorrent using its WebUI API. On success, the
|
||||||
|
download is also recorded in episodic memory as an active_download
|
||||||
|
so the agent can track its progress later, the STM topic is set to
|
||||||
|
"downloading", and the current workflow is ended (the user typically
|
||||||
|
leaves the find-and-download scope at this point).
|
||||||
|
|
||||||
|
when_to_use: |
|
||||||
|
When the user provides a raw magnet link, or when chaining manually
|
||||||
|
after get_torrent_by_index. For the common "user picked search hit
|
||||||
|
N" case, prefer add_torrent_by_index — one call instead of two.
|
||||||
|
|
||||||
|
when_not_to_use: |
|
||||||
|
- For .torrent files (not supported by this tool — magnet only).
|
||||||
|
- When qBittorrent is not configured / reachable — the call will
|
||||||
|
fail and the user has to fix the config first.
|
||||||
|
|
||||||
|
next_steps: |
|
||||||
|
- On status=ok: the workflow is already ended; confirm to the user
|
||||||
|
that the download has started.
|
||||||
|
- On status=error: surface the message; common causes are auth
|
||||||
|
failure or qBittorrent being unreachable.
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
magnet_link:
|
||||||
|
description: Magnet URI of the torrent to add (magnet:?xt=urn:btih:...).
|
||||||
|
why_needed: |
|
||||||
|
The actual payload sent to qBittorrent. Must be a full magnet
|
||||||
|
URI, not a hash alone.
|
||||||
|
example: "magnet:?xt=urn:btih:abc123..."
|
||||||
|
|
||||||
|
returns:
|
||||||
|
ok:
|
||||||
|
description: Torrent accepted by qBittorrent.
|
||||||
|
fields:
|
||||||
|
status: "'ok'"
|
||||||
|
message: Confirmation message.
|
||||||
|
|
||||||
|
error:
|
||||||
|
description: qBittorrent rejected the request or is unreachable.
|
||||||
|
fields:
|
||||||
|
error: Short error code.
|
||||||
|
message: Human-readable explanation.
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
name: analyze_release
|
||||||
|
|
||||||
|
summary: >
|
||||||
|
One-shot analyzer that parses a release name, detects its media type
|
||||||
|
from the folder layout, and enriches the result with ffprobe data.
|
||||||
|
|
||||||
|
description: |
|
||||||
|
Combines three steps in a single call so the agent gets a complete
|
||||||
|
picture before routing:
|
||||||
|
1. parse_release(release_name) — extracts title, year, season,
|
||||||
|
episode, quality, source, codec, group, languages, audio info,
|
||||||
|
HDR, edition, site tag.
|
||||||
|
2. detect_media_type(parsed, path) — uses the on-disk layout
|
||||||
|
(single file vs. folder, presence of S01 dirs, episode count)
|
||||||
|
to choose: movie / tv_episode / tv_season / tv_complete /
|
||||||
|
other / unknown.
|
||||||
|
3. ffprobe enrichment — when the media type is recognised, runs
|
||||||
|
ffprobe on the first video file found and fills in audio
|
||||||
|
codec/channels, bit depth, HDR format. Sets probe_used=true.
|
||||||
|
|
||||||
|
when_to_use: |
|
||||||
|
As the very first step of any organize workflow, right after
|
||||||
|
list_folder, on each release the user wants to handle. The output
|
||||||
|
drives which resolve_*_destination to call next.
|
||||||
|
|
||||||
|
when_not_to_use: |
|
||||||
|
- When you only need codec/audio info on a specific video file:
|
||||||
|
use probe_media (no parsing, no media-type detection).
|
||||||
|
- For releases the user has already analyzed earlier in the same
|
||||||
|
workflow — the parse is deterministic, no need to re-run.
|
||||||
|
|
||||||
|
next_steps: |
|
||||||
|
- media_type == movie → resolve_movie_destination
|
||||||
|
- media_type == tv_season → resolve_season_destination
|
||||||
|
- media_type == tv_episode → resolve_episode_destination
|
||||||
|
- media_type == tv_complete → resolve_series_destination
|
||||||
|
- media_type in (other, unknown) → ask the user what to do; do not
|
||||||
|
auto-route.
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
release_name:
|
||||||
|
description: Raw release folder or file name as it appears on disk.
|
||||||
|
why_needed: |
|
||||||
|
Source of all the parsed tokens (quality, codec, group, ...).
|
||||||
|
Don't sanitise it — the parser relies on the exact spelling.
|
||||||
|
example: Breaking.Bad.S01.1080p.BluRay.x265-GROUP
|
||||||
|
|
||||||
|
source_path:
|
||||||
|
description: Absolute path to the release folder or file on disk.
|
||||||
|
why_needed: |
|
||||||
|
Required for layout-based media-type detection and for ffprobe
|
||||||
|
to find a video file inside the release.
|
||||||
|
example: /downloads/Breaking.Bad.S01.1080p.BluRay.x265-GROUP
|
||||||
|
|
||||||
|
returns:
|
||||||
|
ok:
|
||||||
|
description: Release analyzed.
|
||||||
|
fields:
|
||||||
|
status: "'ok'"
|
||||||
|
media_type: "One of: movie, tv_episode, tv_season, tv_complete, other, unknown."
|
||||||
|
parse_path: "Which parser branch was taken (debug)."
|
||||||
|
title: Parsed title.
|
||||||
|
year: Parsed year (int) or null.
|
||||||
|
season: Season number (int) or null.
|
||||||
|
episode: Episode number (int) or null.
|
||||||
|
episode_end: Range end episode (multi-episode releases) or null.
|
||||||
|
quality: Resolution token (e.g. 1080p, 2160p).
|
||||||
|
source: Source token (BluRay, WEB-DL, ...).
|
||||||
|
codec: Video codec token (x264, x265, ...).
|
||||||
|
group: Release group name or null.
|
||||||
|
languages: List of detected language tokens.
|
||||||
|
audio_codec: Audio codec from ffprobe (when probe_used=true).
|
||||||
|
audio_channels: Audio channel count from ffprobe.
|
||||||
|
bit_depth: Bit depth from ffprobe.
|
||||||
|
hdr_format: HDR format from ffprobe (HDR10, DV, ...) or null.
|
||||||
|
edition: Edition tag (Extended, Director's Cut, ...) or null.
|
||||||
|
site_tag: Source-site tag if present.
|
||||||
|
is_season_pack: True when the folder contains a full season.
|
||||||
|
probe_used: True when ffprobe successfully enriched the result.
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
name: create_seed_links
|
||||||
|
|
||||||
|
summary: >
|
||||||
|
Recreate the original torrent folder structure with hard-links so
|
||||||
|
qBittorrent can keep seeding after the library move.
|
||||||
|
|
||||||
|
description: |
|
||||||
|
Hard-links the library video file back into torrents/<original_folder_name>/
|
||||||
|
and copies all remaining files from the original download folder
|
||||||
|
(subtitles, .nfo, .jpg, .txt, …) so the torrent data is complete on
|
||||||
|
disk. qBittorrent then sees the same content at the location it
|
||||||
|
expects and can keep seeding without rehashing the whole torrent.
|
||||||
|
|
||||||
|
when_to_use: |
|
||||||
|
Only when the user has confirmed they want to keep seeding after a
|
||||||
|
move. Call right after manage_subtitles (or after move_media if there
|
||||||
|
are no subs).
|
||||||
|
|
||||||
|
when_not_to_use: |
|
||||||
|
- When the user explicitly answered "no" to "keep seeding?".
|
||||||
|
- When the download was not from a torrent (e.g. direct download).
|
||||||
|
- Before the library file is in place — this tool reads it.
|
||||||
|
|
||||||
|
next_steps: |
|
||||||
|
- After success: optionally call qBittorrent to update the torrent's
|
||||||
|
save path / force a recheck (not yet covered by a tool).
|
||||||
|
- End the workflow.
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
library_file:
|
||||||
|
description: Absolute path to the video file now in the library.
|
||||||
|
why_needed: |
|
||||||
|
The source for the hard-link — same inode means qBittorrent sees
|
||||||
|
identical bytes at the seeding path.
|
||||||
|
example: /tv_shows/Oz.1997.1080p.WEBRip.x265-KONTRAST/Season 03/Oz.S03E01.mkv
|
||||||
|
|
||||||
|
original_download_folder:
|
||||||
|
description: Absolute path to the original download folder.
|
||||||
|
why_needed: |
|
||||||
|
Provides the folder name to recreate under torrents/ and the
|
||||||
|
auxiliary files (subs, nfo, ...) to copy over.
|
||||||
|
example: /downloads/Oz.S03.1080p.WEBRip.x265-KONTRAST
|
||||||
|
|
||||||
|
returns:
|
||||||
|
ok:
|
||||||
|
description: Seeding folder rebuilt.
|
||||||
|
fields:
|
||||||
|
status: "'ok'"
|
||||||
|
torrent_subfolder: Absolute path of the recreated folder under torrents/.
|
||||||
|
linked_file: Absolute path of the hard-linked video.
|
||||||
|
copied_files: List of auxiliary files that were copied.
|
||||||
|
copied_count: Number of auxiliary files copied.
|
||||||
|
skipped: List of files skipped (already present, unreadable, ...).
|
||||||
|
|
||||||
|
error:
|
||||||
|
description: Failed to rebuild the seeding folder.
|
||||||
|
fields:
|
||||||
|
error: Short error code.
|
||||||
|
message: Human-readable explanation.
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
name: find_media_imdb_id
|
||||||
|
|
||||||
|
summary: >
|
||||||
|
Search TMDB for a media title and return its canonical title, year,
|
||||||
|
IMDb id, and TMDB id.
|
||||||
|
|
||||||
|
description: |
|
||||||
|
Looks up a title on TMDB and returns the canonical metadata needed by
|
||||||
|
the resolve_*_destination tools. On success, the result is also
|
||||||
|
stashed in short-term memory under "last_media_search" so later steps
|
||||||
|
in the workflow can read it without re-calling TMDB. The STM topic
|
||||||
|
is set to "searching_media".
|
||||||
|
|
||||||
|
when_to_use: |
|
||||||
|
Right after analyze_release, before calling resolve_*_destination —
|
||||||
|
the resolvers need the canonical title + year and refuse to guess
|
||||||
|
them from the raw release name.
|
||||||
|
|
||||||
|
when_not_to_use: |
|
||||||
|
- When you already have the IMDb id in STM from an earlier step in
|
||||||
|
the same workflow.
|
||||||
|
- For torrent search — use find_torrent instead.
|
||||||
|
|
||||||
|
next_steps: |
|
||||||
|
- On status=ok: call the appropriate resolve_*_destination with
|
||||||
|
tmdb_title and tmdb_year from the result.
|
||||||
|
- On status=error (not_found): show the error and ask the user for
|
||||||
|
a more precise title.
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
media_title:
|
||||||
|
description: Title to search for. Free-form — TMDB does the matching.
|
||||||
|
why_needed: |
|
||||||
|
Drives the TMDB query. Pass a sanitized version (no resolution
|
||||||
|
tokens, no group name) for best results.
|
||||||
|
example: Breaking Bad
|
||||||
|
|
||||||
|
returns:
|
||||||
|
ok:
|
||||||
|
description: Match found.
|
||||||
|
fields:
|
||||||
|
status: "'ok'"
|
||||||
|
title: Canonical title as returned by TMDB.
|
||||||
|
year: Release year (movies) or first-air year (series).
|
||||||
|
media_type: "'movie' or 'tv'."
|
||||||
|
imdb_id: IMDb identifier (ttXXXXXXX) or null.
|
||||||
|
tmdb_id: TMDB numeric id.
|
||||||
|
|
||||||
|
error:
|
||||||
|
description: No match or API failure.
|
||||||
|
fields:
|
||||||
|
error: Short error code (not_found, api_error, ...).
|
||||||
|
message: Human-readable explanation.
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
name: find_torrent
|
||||||
|
|
||||||
|
summary: >
|
||||||
|
Search Knaben for torrents matching a media title; cache results in
|
||||||
|
episodic memory.
|
||||||
|
|
||||||
|
description: |
|
||||||
|
Queries the Knaben aggregator for up to 10 torrents matching the
|
||||||
|
given title, then stores the result list in episodic memory under
|
||||||
|
"last_search_results". The user can then refer to a torrent by
|
||||||
|
1-based index ("download the 3rd one") via get_torrent_by_index or
|
||||||
|
add_torrent_by_index. The STM topic is set to "selecting_torrent".
|
||||||
|
|
||||||
|
when_to_use: |
|
||||||
|
When the user wants to download something new — typically the first
|
||||||
|
step of a "find + download" sub-task. The agent should usually
|
||||||
|
pre-filter the title (canonical name + year) before searching for
|
||||||
|
cleaner results.
|
||||||
|
|
||||||
|
when_not_to_use: |
|
||||||
|
- For TMDB metadata lookup — use find_media_imdb_id.
|
||||||
|
- When a search was already performed in the same session and the
|
||||||
|
user is just picking from the existing list.
|
||||||
|
|
||||||
|
next_steps: |
|
||||||
|
- Present the indexed results to the user.
|
||||||
|
- Once chosen: call add_torrent_by_index(N) — that wraps
|
||||||
|
get_torrent_by_index + add_torrent_to_qbittorrent.
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
media_title:
|
||||||
|
description: Title to search for on Knaben. Free-form.
|
||||||
|
why_needed: |
|
||||||
|
Drives the search query. Use the canonical title (from
|
||||||
|
find_media_imdb_id) plus quality preferences for better hits.
|
||||||
|
example: Inception 2010 1080p
|
||||||
|
|
||||||
|
returns:
|
||||||
|
ok:
|
||||||
|
description: Search returned a list of torrents.
|
||||||
|
fields:
|
||||||
|
status: "'ok'"
|
||||||
|
torrents: "List of {name, size, seeders, leechers, magnet, ...}, up to 10."
|
||||||
|
|
||||||
|
error:
|
||||||
|
description: Search failed.
|
||||||
|
fields:
|
||||||
|
error: Short error code.
|
||||||
|
message: Human-readable explanation.
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
name: get_torrent_by_index
|
||||||
|
|
||||||
|
summary: >
|
||||||
|
Retrieve a torrent from the last find_torrent search by its 1-based
|
||||||
|
index.
|
||||||
|
|
||||||
|
description: |
|
||||||
|
Reads episodic memory's last_search_results and returns the entry at
|
||||||
|
the given 1-based position. Pure lookup — does not start a download.
|
||||||
|
Fails when the search results are missing or the index is out of
|
||||||
|
range.
|
||||||
|
|
||||||
|
when_to_use: |
|
||||||
|
When the user references a search hit by number ("show me the second
|
||||||
|
one") but doesn't yet want to download — e.g. inspection, sharing
|
||||||
|
the magnet, ...
|
||||||
|
|
||||||
|
when_not_to_use: |
|
||||||
|
- When the user wants to start downloading: use add_torrent_by_index
|
||||||
|
instead (one call instead of two).
|
||||||
|
- When no search has been performed yet — the result will be
|
||||||
|
not_found.
|
||||||
|
|
||||||
|
next_steps: |
|
||||||
|
- Display the torrent to the user.
|
||||||
|
- If they then say "add it", call add_torrent_to_qbittorrent with the
|
||||||
|
magnet, or add_torrent_by_index with the same index.
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
index:
|
||||||
|
description: 1-based position in the last find_torrent result list.
|
||||||
|
why_needed: |
|
||||||
|
Maps to a specific torrent entry. Out-of-range values return an
|
||||||
|
error, not a wraparound.
|
||||||
|
example: 3
|
||||||
|
|
||||||
|
returns:
|
||||||
|
ok:
|
||||||
|
description: Torrent found at that index.
|
||||||
|
fields:
|
||||||
|
status: "'ok'"
|
||||||
|
torrent: "Full torrent dict (name, size, seeders, leechers, magnet, ...)."
|
||||||
|
|
||||||
|
error:
|
||||||
|
description: No torrent at that index.
|
||||||
|
fields:
|
||||||
|
error: Short error code (not_found).
|
||||||
|
message: Human-readable explanation, e.g. "Search for torrents first."
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
name: learn
|
||||||
|
|
||||||
|
summary: >
|
||||||
|
Teach Alfred a new token mapping and persist it to the learned
|
||||||
|
knowledge pack so future scans recognise it.
|
||||||
|
|
||||||
|
description: |
|
||||||
|
Appends a new token (or list of tokens) to a key inside a knowledge
|
||||||
|
pack and writes the result to `data/knowledge/<pack>_learned.yaml`.
|
||||||
|
The change is persisted atomically (write-tmp + rename) so a crash
|
||||||
|
cannot corrupt the file. Currently only the `subtitles` pack is
|
||||||
|
supported.
|
||||||
|
|
||||||
|
when_to_use: |
|
||||||
|
When manage_subtitles returns needs_clarification with unresolved
|
||||||
|
tokens, after confirming with the user what the tokens mean. Call
|
||||||
|
once per (category, key) — multiple values can be added in a single
|
||||||
|
call.
|
||||||
|
|
||||||
|
when_not_to_use: |
|
||||||
|
- Without explicit user confirmation of what the token means.
|
||||||
|
- For knowledge that belongs in the static pack
|
||||||
|
(alfred/knowledge/<pack>.yaml) — that's editor territory, not
|
||||||
|
runtime learning.
|
||||||
|
|
||||||
|
next_steps: |
|
||||||
|
- After success: re-run the workflow step that triggered the
|
||||||
|
clarification (typically manage_subtitles) so the new mapping is
|
||||||
|
applied.
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
pack:
|
||||||
|
description: Knowledge pack name. Currently only "subtitles" is supported.
|
||||||
|
why_needed: |
|
||||||
|
Decides which `*_learned.yaml` file under data/knowledge/ gets
|
||||||
|
written. The pack name is namespaced to avoid collisions across
|
||||||
|
domains.
|
||||||
|
example: subtitles
|
||||||
|
|
||||||
|
category:
|
||||||
|
description: Category within the pack — "languages", "types", or "formats".
|
||||||
|
why_needed: |
|
||||||
|
Different categories use different lookup tables at scan time.
|
||||||
|
A wrong category silently has no effect.
|
||||||
|
example: languages
|
||||||
|
|
||||||
|
key:
|
||||||
|
description: Canonical entry id — ISO 639-1 code, type name, format name.
|
||||||
|
why_needed: |
|
||||||
|
The destination bucket for the new tokens. Existing tokens under
|
||||||
|
this key are kept; only new values are appended.
|
||||||
|
example: es
|
||||||
|
|
||||||
|
values:
|
||||||
|
description: List of token spellings to add.
|
||||||
|
why_needed: |
|
||||||
|
Release groups use many spellings for the same language/type;
|
||||||
|
pass them all in one call instead of multiple round-trips.
|
||||||
|
example: '["spanish", "espanol", "spa"]'
|
||||||
|
|
||||||
|
returns:
|
||||||
|
ok:
|
||||||
|
description: Mapping saved.
|
||||||
|
fields:
|
||||||
|
status: "'ok'"
|
||||||
|
pack: Name of the pack that was written to.
|
||||||
|
category: Category that was updated.
|
||||||
|
key: Key that was updated.
|
||||||
|
added_count: Number of values that were actually new (deduplicated).
|
||||||
|
tokens: Full updated token list for that key.
|
||||||
|
|
||||||
|
error:
|
||||||
|
description: Save failed.
|
||||||
|
fields:
|
||||||
|
error: Short error code (unknown_pack, unknown_category, read_failed, write_failed).
|
||||||
|
message: Human-readable explanation.
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
name: list_folder
|
||||||
|
|
||||||
|
summary: >
|
||||||
|
List the contents of a configured folder, optionally below a
|
||||||
|
relative subpath.
|
||||||
|
|
||||||
|
description: |
|
||||||
|
Reads a folder previously configured via set_path_for_folder and
|
||||||
|
returns its entries (files + directories). A relative `path` lets you
|
||||||
|
drill down without re-specifying the absolute root each time. Path
|
||||||
|
traversal is rejected (no `..`, no absolute paths) so the agent
|
||||||
|
cannot escape the configured root.
|
||||||
|
|
||||||
|
when_to_use: |
|
||||||
|
- At the start of an organize workflow to discover what's available
|
||||||
|
in the download folder.
|
||||||
|
- To browse a library collection ("what tv shows do I have?").
|
||||||
|
- As a sanity check before any move to confirm the target exists.
|
||||||
|
|
||||||
|
when_not_to_use: |
|
||||||
|
- For folders that are not configured — call set_path_for_folder
|
||||||
|
first.
|
||||||
|
- To list arbitrary system paths — this tool is intentionally scoped
|
||||||
|
to the known roots.
|
||||||
|
|
||||||
|
next_steps: |
|
||||||
|
- After listing the download folder: typically call analyze_release
|
||||||
|
on a specific entry.
|
||||||
|
- After listing a library folder: use the result to disambiguate a
|
||||||
|
destination during resolve_*_destination.
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
folder_type:
|
||||||
|
description: Logical folder key (download, torrent, movie, tv_show, ...).
|
||||||
|
why_needed: |
|
||||||
|
Resolves to an absolute root through LTM. Must have been set via
|
||||||
|
set_path_for_folder beforehand.
|
||||||
|
example: download
|
||||||
|
|
||||||
|
path:
|
||||||
|
description: Relative subpath inside the root (default ".").
|
||||||
|
why_needed: |
|
||||||
|
Lets you drill into a subfolder without expanding the root. No
|
||||||
|
".." or absolute path is allowed.
|
||||||
|
example: Breaking.Bad.S01.1080p.BluRay.x265-GROUP
|
||||||
|
|
||||||
|
returns:
|
||||||
|
ok:
|
||||||
|
description: Listing returned.
|
||||||
|
fields:
|
||||||
|
status: "'ok'"
|
||||||
|
folder_type: The key that was listed.
|
||||||
|
path: The relative path that was listed.
|
||||||
|
entries: List of {name, type, size?} for each entry.
|
||||||
|
|
||||||
|
error:
|
||||||
|
description: Could not list the folder.
|
||||||
|
fields:
|
||||||
|
error: Short error code (folder_not_configured, path_not_found, path_traversal, ...).
|
||||||
|
message: Human-readable explanation.
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
name: manage_subtitles
|
||||||
|
|
||||||
|
summary: >
|
||||||
|
Detect, filter, and place subtitle tracks next to a video that has just
|
||||||
|
been organised into the library.
|
||||||
|
|
||||||
|
description: |
|
||||||
|
Scans the source video's surroundings for subtitle files
|
||||||
|
(.srt, .ass, .ssa, .vtt, .sub), classifies them by language and type
|
||||||
|
(standard / SDH / forced), filters by the user's SubtitlePreferences
|
||||||
|
(languages, min size, keep_sdh, keep_forced), and hard-links the
|
||||||
|
passing files next to the destination video using the convention
|
||||||
|
`<lang>.<ext>`, `<lang>.sdh.<ext>`, `<lang>.forced.<ext>`.
|
||||||
|
If no subtitles are found, returns status=ok with placed_count=0 — not
|
||||||
|
an error.
|
||||||
|
|
||||||
|
when_to_use: |
|
||||||
|
Always after a successful move_media / move_to_destination, before
|
||||||
|
closing the workflow. Pass the original source path (where subs live)
|
||||||
|
and the new library path (where they should land).
|
||||||
|
|
||||||
|
when_not_to_use: |
|
||||||
|
- Do not call before the video itself has been moved — the destination
|
||||||
|
must exist for hard-links to make sense.
|
||||||
|
- Skip when the user explicitly asks not to handle subtitles.
|
||||||
|
|
||||||
|
next_steps: |
|
||||||
|
- On status=ok: continue with create_seed_links (if seeding) or end
|
||||||
|
the workflow.
|
||||||
|
- On status=needs_clarification: ask the user about the unresolved
|
||||||
|
tokens, then optionally call learn() to teach the new mapping.
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
source_video:
|
||||||
|
description: Absolute path to the original video file (in the download folder).
|
||||||
|
why_needed: |
|
||||||
|
Subtitles typically live next to the source, either as siblings or
|
||||||
|
in a Subs/ subfolder. The scanner walks from this path.
|
||||||
|
example: /downloads/Oz.S03.1080p.WEBRip.x265-KONTRAST/Oz.S03E01.mkv
|
||||||
|
|
||||||
|
destination_video:
|
||||||
|
description: Absolute path to the video file in its library location.
|
||||||
|
why_needed: |
|
||||||
|
Subtitles are hard-linked next to this file so media players pick
|
||||||
|
them up automatically.
|
||||||
|
example: /tv_shows/Oz.1997.1080p.WEBRip.x265-KONTRAST/Season 03/Oz.S03E01.mkv
|
||||||
|
|
||||||
|
returns:
|
||||||
|
ok:
|
||||||
|
description: Subtitles scanned (and possibly placed).
|
||||||
|
fields:
|
||||||
|
status: "'ok'"
|
||||||
|
placed: List of {source, destination, filename} for each linked file.
|
||||||
|
placed_count: Number of subtitle files placed.
|
||||||
|
skipped_count: Number of subtitle files filtered out.
|
||||||
|
|
||||||
|
needs_clarification:
|
||||||
|
description: One or more tokens could not be classified.
|
||||||
|
fields:
|
||||||
|
unresolved: List of unrecognised tokens with their context.
|
||||||
|
question: Human-readable question to relay to the user.
|
||||||
|
|
||||||
|
error:
|
||||||
|
description: Scan or placement failed.
|
||||||
|
fields:
|
||||||
|
error: Short error code.
|
||||||
|
message: Human-readable explanation.
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
name: move_media
|
||||||
|
|
||||||
|
summary: >
|
||||||
|
Safely move a media file with copy + integrity check + delete source.
|
||||||
|
|
||||||
|
description: |
|
||||||
|
Copies the source file to the destination with an integrity check,
|
||||||
|
then deletes the source. Slower than move_to_destination (which is a
|
||||||
|
plain rename) but safer across filesystems where rename is not atomic
|
||||||
|
or when you want a checksum verification.
|
||||||
|
|
||||||
|
when_to_use: |
|
||||||
|
Use to move a single file across filesystems or when paranoia about
|
||||||
|
data integrity is justified — e.g. moving a finished download from a
|
||||||
|
scratch disk to the main library array.
|
||||||
|
|
||||||
|
when_not_to_use: |
|
||||||
|
- For same-filesystem moves where speed matters: use move_to_destination
|
||||||
|
(instant rename on ZFS/ext4 within the same dataset).
|
||||||
|
- For folder-level moves of complete packs: use move_to_destination —
|
||||||
|
move_media is a single-file operation.
|
||||||
|
|
||||||
|
next_steps: |
|
||||||
|
- After a successful move: call manage_subtitles to place any subtitle
|
||||||
|
tracks, then create_seed_links if the user wants to keep seeding.
|
||||||
|
- On error: surface the error code (file_not_found, destination_exists,
|
||||||
|
integrity_check_failed) and ask the user how to proceed.
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
source:
|
||||||
|
description: Absolute path to the source video file.
|
||||||
|
why_needed: |
|
||||||
|
The file being moved. Typically lives under the downloads folder
|
||||||
|
after a torrent completes.
|
||||||
|
example: /downloads/Inception.2010.1080p.BluRay.x265-GROUP/movie.mkv
|
||||||
|
|
||||||
|
destination:
|
||||||
|
description: Absolute path of the destination file — must not already exist.
|
||||||
|
why_needed: |
|
||||||
|
Where the file lands in the library. Comes from a resolve_*_destination
|
||||||
|
call so the naming convention is respected.
|
||||||
|
example: /movies/Inception.2010.1080p.BluRay.x265-GROUP/Inception.2010.1080p.BluRay.x265-GROUP.mkv
|
||||||
|
|
||||||
|
returns:
|
||||||
|
ok:
|
||||||
|
description: Move succeeded.
|
||||||
|
fields:
|
||||||
|
status: "'ok'"
|
||||||
|
source: Absolute path of the source (now gone).
|
||||||
|
destination: Absolute path of the destination (now in place).
|
||||||
|
filename: Basename of the destination file.
|
||||||
|
size: Size in bytes.
|
||||||
|
|
||||||
|
error:
|
||||||
|
description: Move failed.
|
||||||
|
fields:
|
||||||
|
error: Short error code (file_not_found, destination_exists, integrity_check_failed, ...).
|
||||||
|
message: Human-readable explanation.
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
name: probe_media
|
||||||
|
|
||||||
|
summary: >
|
||||||
|
Run ffprobe on a single video file and return its technical details.
|
||||||
|
|
||||||
|
description: |
|
||||||
|
Inspects a specific video file with ffprobe and returns codec,
|
||||||
|
resolution, duration, bitrate, the list of audio tracks (with
|
||||||
|
language and channel layout), and the list of embedded subtitle
|
||||||
|
tracks. Independent of any release-name parsing — works on any file
|
||||||
|
you can point at.
|
||||||
|
|
||||||
|
when_to_use: |
|
||||||
|
- To inspect a file's audio/subtitle tracks before deciding what to
|
||||||
|
do (e.g. choose a default audio language).
|
||||||
|
- To verify a video's resolution / codec when the release name is
|
||||||
|
unreliable.
|
||||||
|
- As a building block when analyze_release is overkill.
|
||||||
|
|
||||||
|
when_not_to_use: |
|
||||||
|
- For full release routing — analyze_release does parsing + media
|
||||||
|
type detection + probe in one call.
|
||||||
|
- On non-video files — ffprobe will return probe_failed.
|
||||||
|
|
||||||
|
next_steps: |
|
||||||
|
- The returned info typically feeds a user-facing decision (e.g.
|
||||||
|
"this is 7.1 DTS, want to keep it?"); rarely chained directly to
|
||||||
|
another tool.
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
source_path:
|
||||||
|
description: Absolute path to the video file to probe.
|
||||||
|
why_needed: |
|
||||||
|
ffprobe needs the exact file (not a folder). For releases use
|
||||||
|
analyze_release; for a known file path, pass it here.
|
||||||
|
example: /downloads/Inception.2010.1080p.BluRay.x265-GROUP/movie.mkv
|
||||||
|
|
||||||
|
returns:
|
||||||
|
ok:
|
||||||
|
description: Probe succeeded.
|
||||||
|
fields:
|
||||||
|
status: "'ok'"
|
||||||
|
video: "Dict with codec, resolution, width, height, duration_seconds, bitrate_kbps."
|
||||||
|
audio_tracks: "List of {index, codec, channels, channel_layout, language, is_default}."
|
||||||
|
subtitle_tracks: "List of {index, codec, language, is_default, is_forced}."
|
||||||
|
audio_languages: List of language codes present in audio tracks.
|
||||||
|
is_multi_audio: True when more than one audio language is present.
|
||||||
|
|
||||||
|
error:
|
||||||
|
description: Probe failed.
|
||||||
|
fields:
|
||||||
|
error: Short error code (not_found, probe_failed).
|
||||||
|
message: Human-readable explanation.
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
name: set_language
|
||||||
|
|
||||||
|
summary: >
|
||||||
|
Set the conversation language so all subsequent assistant messages
|
||||||
|
match it.
|
||||||
|
|
||||||
|
description: |
|
||||||
|
Persists an ISO 639-1 language code in short-term memory under
|
||||||
|
conversation.language. Read by the prompt builder and any tool that
|
||||||
|
needs to localise output. Does not validate the code against an ISO
|
||||||
|
list — the LLM is trusted to pass a sensible value.
|
||||||
|
|
||||||
|
when_to_use: |
|
||||||
|
As the very first call when the user writes in a language different
|
||||||
|
from the current STM language. Doing it before answering avoids a
|
||||||
|
mid-reply switch.
|
||||||
|
|
||||||
|
when_not_to_use: |
|
||||||
|
- On every turn — only when the language actually changes.
|
||||||
|
- To pick a subtitle language — that lives in SubtitlePreferences,
|
||||||
|
not the conversation language.
|
||||||
|
|
||||||
|
next_steps: |
|
||||||
|
- After success: continue the user's request in the newly set
|
||||||
|
language.
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
language:
|
||||||
|
description: ISO 639-1 language code (en, fr, es, de, ...).
|
||||||
|
why_needed: |
|
||||||
|
Identifies the target language unambiguously across the UI and
|
||||||
|
any localisation logic.
|
||||||
|
example: fr
|
||||||
|
|
||||||
|
returns:
|
||||||
|
ok:
|
||||||
|
description: Language saved.
|
||||||
|
fields:
|
||||||
|
status: "'ok'"
|
||||||
|
message: Confirmation message.
|
||||||
|
language: The language code that was saved.
|
||||||
|
|
||||||
|
error:
|
||||||
|
description: Could not save the language.
|
||||||
|
fields:
|
||||||
|
status: "'error'"
|
||||||
|
error: Short error code or exception message.
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
name: set_path_for_folder
|
||||||
|
|
||||||
|
summary: >
|
||||||
|
Configure where a known folder lives on disk (download, torrent, or
|
||||||
|
any library collection).
|
||||||
|
|
||||||
|
description: |
|
||||||
|
Stores an absolute path in long-term memory under a folder key. Two
|
||||||
|
classes of folders exist:
|
||||||
|
- Workspace paths: "download", "torrent" — single-valued each, used
|
||||||
|
by the organize workflows.
|
||||||
|
- Library paths: any other key (e.g. "movie", "tv_show",
|
||||||
|
"documentary") — these are the collections you organise into.
|
||||||
|
The path must exist and be a directory; otherwise the call fails
|
||||||
|
without changing memory.
|
||||||
|
|
||||||
|
when_to_use: |
|
||||||
|
On first run, or when the user moves a folder, or when introducing a
|
||||||
|
new library collection (e.g. "set the documentaries folder to ...").
|
||||||
|
|
||||||
|
when_not_to_use: |
|
||||||
|
- For one-off listings — list_folder works without configuration only
|
||||||
|
if the folder is already set.
|
||||||
|
- To rename or delete an existing folder — this only sets paths.
|
||||||
|
|
||||||
|
next_steps: |
|
||||||
|
- After success: typical follow-ups are list_folder on the same key,
|
||||||
|
or starting a workflow that needs the path.
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
folder_name:
|
||||||
|
description: Logical name of the folder (download, torrent, movie, tv_show, ...).
|
||||||
|
why_needed: |
|
||||||
|
The key the agent uses everywhere afterwards. "download" and
|
||||||
|
"torrent" are reserved for workspace; anything else becomes a
|
||||||
|
library collection.
|
||||||
|
example: tv_show
|
||||||
|
|
||||||
|
path_value:
|
||||||
|
description: Absolute path to the folder on disk.
|
||||||
|
why_needed: |
|
||||||
|
Must exist and be readable. Stored verbatim in LTM — relative
|
||||||
|
paths are rejected.
|
||||||
|
example: /tank/library/tv_shows
|
||||||
|
|
||||||
|
returns:
|
||||||
|
ok:
|
||||||
|
description: Path saved to long-term memory.
|
||||||
|
fields:
|
||||||
|
status: "'ok'"
|
||||||
|
folder_name: The logical name that was set.
|
||||||
|
path_value: The absolute path that was saved.
|
||||||
|
|
||||||
|
error:
|
||||||
|
description: Could not set the path.
|
||||||
|
fields:
|
||||||
|
error: Short error code (path_not_found, not_a_directory, invalid_path, ...).
|
||||||
|
message: Human-readable explanation.
|
||||||
Reference in New Issue
Block a user