AnimeLibrarian sends the filenames in an incoming folder and the existing folders in a media library to a Dify workflow. It validates the returned move plan, shows exactly what will change, and moves the files only after confirmation.
Before After
Downloads/ Media/
└── [SubsPlease] Frieren - 01.mkv → └── Frieren/
└── Season 01/
└── Frieren - S01E01.mkv
- A polished Rich table for people, minimal plain text, and script-safe JSON Lines
- A true dry run that never prompts or changes the filesystem
- One confirmation for the whole plan, defaulting to no when files would be overwritten
- Strict validation of AI-proposed sources, destinations, extensions, and paths
- Clean pipelines: plans go to stdout; status, progress, warnings, and prompts go to stderr
Requires Python 3.13, uv, a published Dify workflow, and a Dify API key.
git clone https://github.com/laipz8200/anime-librarian.git
cd anime-librarian
uv sync
cp .env.example .env
# Edit .env and set ANIMELIBRARIAN_DIFY_API_KEY.
mkdir -p ~/Downloads ~/Media/Frieren
uv run anime-librarian \
--source ~/Downloads \
--target ~/Media \
--dry-runThe source and target must already be directories. The source scan is non-recursive. The target must contain at least one direct child folder so Dify has an existing library catalog to work from.
When the plan looks right, rerun without --dry-run.
- In Dify Studio, import
Dify DSL File/Anime Librarian.yml. - Configure the imported OpenAI model nodes.
- Optionally set the workflow secret
TMDB_API_TOKENfor richer TMDB metadata; the workflow falls back to model-only matching when it is absent. - Publish the workflow, open API Access, and create an API key.
- Put that key in
.env. Keep the default workflow endpoint unless your Dify deployment uses another URL.
The CLI supplies the workflow's files and directories inputs and expects its
published text output to contain {"result": [...]} filename pairs.
CLI paths override environment paths. .env is loaded automatically.
| Variable | Required? | Default / purpose |
|---|---|---|
ANIMELIBRARIAN_DIFY_API_KEY |
Yes | Dify workflow API key |
ANIMELIBRARIAN_SOURCE_PATH |
Unless --source is used |
Existing incoming directory |
ANIMELIBRARIAN_TARGET_PATH |
Unless --target is used |
Existing media-library root |
ANIMELIBRARIAN_DIFY_WORKFLOW_RUN_ENDPOINT |
No | https://api.dify.ai/v1/workflows/run |
ANIMELIBRARIAN_API_TIMEOUT |
No | Positive timeout in seconds; 300 |
ANIMELIBRARIAN_USER_NAME |
No | Dify request user; Anime Librarian |
| Option | What it does |
|---|---|
-s DIR, --source DIR |
Use an incoming directory instead of the environment value |
-t DIR, --target DIR |
Use a library root instead of the environment value |
-n, --dry-run |
Print the plan without prompting or moving files |
--format table |
Render the default Rich move table |
--format plain |
Print one source -> target line per move |
--format json |
Print one JSON object per move (JSON Lines) |
-V, --version |
Print the installed version |
-h, --help |
Show all options |
For example, stream a machine-readable preview while keeping status messages separate:
uv run anime-librarian --dry-run --format json > move-plan.jsonlAnimeLibrarian considers only direct, regular files in the source directory. It ignores symlinks, other extensions, and nested directories.
- Video:
.mkv,.mp4,.avi,.mov,.wmv,.flv,.webm,.m4v,.mpg,.mpeg - Subtitle:
.srt,.ass,.ssa,.sub,.idx,.vtt
Every Dify suggestion must name a scanned source exactly, preserve its extension, stay within the target root, and have a unique source and destination. Absolute paths, traversal, duplicate moves, source-as-target moves, and directory destinations are rejected. Source and destination identities are checked again before each move, so late file swaps are refused. Missing destination folders are listed before the prompt and created only after approval.
A normal run asks once and defaults to yes. If any destination already exists, the conflict list is shown and that same prompt defaults to no. A dry run never asks.
The application is intentionally small and linear:
| Module | Responsibility |
|---|---|
main.py |
argparse interface, dependency setup, and plan → review → move workflow |
config.py |
Load and validate immutable runtime Settings |
dify.py |
httpx adapter for the blocking Dify workflow API |
file_renamer.py |
FileOrganizer scanning, validated FileMove plans, and filesystem changes |
console.py |
One Rich Terminal boundary for stdout, stderr, prompts, and progress |
models.py |
Validated Dify NamePair values and immutable FileMove values |
make install # Sync dependencies
make lint # Format and autofix Python, then run Ruff
make check # Run Ruff without fixes
make type-check # Run strict basedpyright
make test # Run the test suite
make clean # Remove generated Python caches and coverage files
make help # List every targetThe focused suite exercises Dify with httpx.MockTransport, so tests do not
open network sockets.
Changes should follow red → green → refactor, then pass:
make lint
make type-check
make testContributions are welcome under the MIT License.