Metadata-Version: 2.4
Name: emby-curator
Version: 3.4.2
Summary: Curate Emby libraries: deduplicate, localize genres & descriptions, clean up stale media, and analyze missing episodes
Author-email: Barlog951 <9975572+Barlog951@users.noreply.github.com>
License-Expression: Apache-2.0
Project-URL: Homepage, https://github.com/Barlog951/emby-curator
Project-URL: Repository, https://github.com/Barlog951/emby-curator
Project-URL: Original Project, https://github.com/troykelly/emby-dedupe
Project-URL: Changelog, https://github.com/Barlog951/emby-curator/blob/main/CHANGELOG.md
Keywords: emby,media,library,curation,deduplication,genres,metadata,cleanup
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: End Users/Desktop
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Multimedia :: Video
Classifier: Topic :: Utilities
Classifier: Environment :: Console
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: NOTICE
Requires-Dist: httpx>=0.28.1
Requires-Dist: anyio>=4.14.2
Requires-Dist: tqdm>=4.70.1
Requires-Dist: backoff>=2.2.1
Requires-Dist: jinja2>=3.1.6
Requires-Dist: pyyaml>=6.0.3
Requires-Dist: rank-torrent-name>=1.11.1
Requires-Dist: python-dotenv>=1.2.3
Requires-Dist: typer>=0.27.2
Requires-Dist: lingua-language-detector>=2.2.0
Provides-Extra: claude
Requires-Dist: anthropic>=1.12.0; extra == "claude"
Provides-Extra: dev
Requires-Dist: anthropic>=1.12.0; extra == "dev"
Requires-Dist: pytest>=9.1.1; extra == "dev"
Requires-Dist: pytest-cov>=7.0.0; extra == "dev"
Requires-Dist: pytest-mock>=3.15.1; extra == "dev"
Requires-Dist: pytest-xdist>=3.8.0; extra == "dev"
Requires-Dist: mypy>=2.3.1; extra == "dev"
Requires-Dist: types-PyYAML>=6.0.12; extra == "dev"
Requires-Dist: ruff>=0.16.8; extra == "dev"
Requires-Dist: vulture>=2.14; extra == "dev"
Requires-Dist: requests>=2.32.5; extra == "dev"
Dynamic: license-file

# Emby Curator

**Emby Curator** keeps your Emby media libraries in order. It deduplicates media, localizes genres and descriptions (SK/CZ from TMDB/OMDb), cleans up stale unwatched content with rating-decay protection, and analyzes missing episodes and franchise gaps — with terminal and HTML reports throughout.

> **Maintained fork.** Emby Curator is a fork of [emby-dedupe](https://github.com/troykelly/emby-dedupe) by Troy Kelly, which has been inactive since May 2024. It has been substantially rewritten and extended (genre management, description/title localization, library cleanup, missing-episode analysis, analytics dashboards, a 1000+ test suite, and CI/CD). Distributed under the Apache License 2.0 — see [NOTICE](https://github.com/Barlog951/emby-curator/blob/main/NOTICE) and [CHANGELOG.md](https://github.com/Barlog951/emby-curator/blob/main/CHANGELOG.md).

## Build Status

![Python Tests](https://github.com/Barlog951/emby-curator/actions/workflows/python-test.yaml/badge.svg)
![Security Scan](https://github.com/Barlog951/emby-curator/actions/workflows/security-scan.yaml/badge.svg)
![Edge Container Build](https://github.com/Barlog951/emby-curator/actions/workflows/edge.yaml/badge.svg)

The following architectures are supported in the latest Docker version:

| Architecture    | Supported          |
|-----------------|--------------------|
| `amd64`         | :white_check_mark: |
| `arm64`         | :white_check_mark: |
| `arm/v7`        | :x:                |
| `arm/v6`        | :x:                |
| `i386`          | :x:                |

## Table of Contents

- [Features](#features)
- [Installation](#installation)
  - [Using Docker](#using-docker)
  - [Using Python](#using-python)
- [Usage](#usage)
- [Environment Variables](#environment-variables)
- [Command-line Arguments](#command-line-arguments)
- [Examples](#examples)
- [API Key Requirement](#api-key-requirement)
- [Project Structure](#project-structure)
- [Contributing](#contributing)
- [License](#license)

## Features

- Identifies duplicates in Emby media libraries (supports scanning multiple libraries at once)
- Generates detailed Markdown reports
- Optional HTML report with movie/episode images and detailed metadata
- Smart handling of both movies and TV shows with proper season/episode identification
- Support for multiple provider IDs (IMDB, TVDB, TMDB)
- Intelligent quality ranking to keep the best version
- Language prioritization to keep items with preferred audio tracks
- Dolby Vision **Profile 5** ("green/pink") awareness: detects single-layer DV P5 files
  (which render with a green/magenta tint on non-DV players) and ranks them *below* a
  clean HDR10 copy of the same item, so the watchable version is the one kept. The HTML
  report flags these with a "DV Profile 5 — green/pink" badge.
- Optional DV P5 → HDR10 converter (`scripts/dv/`, NVENC/libplacebo) to produce the clean
  copy in the first place — see [`scripts/dv/README.md`](scripts/dv/README.md)
- **Fold-delete safety guard:** Emby's `DELETE /Items/{id}` removes a media file's *owning
  folder* (a movie folder or per-episode subfolder), not just the file. The deletion path
  refuses any delete that would fold-delete a folder containing the item being **kept**, so
  a duplicate co-located with its keeper is never destroyed together with it.
- **ČSFD metadata** (`csfd fill`): for titles TMDb/TVDb/IMDb can't identify, fills only *empty*
  fields (Slovak overview, genres, year, rating, directors and cast, 1080px poster) from
  [csfd.sk](https://www.csfd.sk), accepting only unambiguous title + year matches. Optional
  `--ai-match` asks an AI to pick among the plausible candidates when the rules can't decide:
  Claude Haiku proposes and [TypeSafe](https://typesafe.ai)'s Jev cross-checks, or either one alone.
  It is review-only by default.
- Multi-platform support (Docker and Python)

## Installation

### Using Docker

To install the Docker container, pull the image from the GitHub Container Registry:

```shell
docker pull ghcr.io/barlog951/emby-curator:latest
```

The `latest` tag is the newest stable release; `edge` tracks the latest build from `main`.

### Using Python

You can also install the tool directly using pip:

```shell
pip install emby-curator
```

Or clone the repository and install locally:

```shell
git clone https://github.com/Barlog951/emby-curator.git
cd emby-dedupe
pip install -e .
```

## Usage

The `emby-dedupe` tool uses a subcommand structure. Shared connection options go before the subcommand; subcommand-specific options go after.

```
emby-curator [shared options] SUBCOMMAND [subcommand options]
```

### Subcommands

| Subcommand | Description |
|---|---|
| `dedupe` | Find and remove duplicate media |
| `genres audit` | Read-only report of genre health |
| `genres normalize` | Fix variant genre names |
| `genres fix` | Fill missing genres from TMDB/OMDb |
| `genres process` | Normalize + fix in one pass (used by the webhook listener) |
| `genres origin` | Add a "Česko-slovenský" genre to Czech/Slovak movies and TV series |
| `cleanup` | Find stale, unwatched media (dynamic rating-decay protection) |
| `descriptions fill` | Localize Overview/Tagline/Name (SK/CZ) and backfill ProductionYear |
| `csfd fill` | Fill metadata, posters and cast from ČSFD for titles TMDb/TVDb can't identify |
| `csfd people` | ČSFD portraits, biographies and birth/death dates for actors |
| `check` | Check if media should be downloaded |
| `missing-episodes` | Find missing TV episodes |

### Shared Options (before subcommand)

| Option | Short | Env Var | Description |
|---|---|---|---|
| `--host` | `-H` | `DEDUPE_EMBY_HOST` | Emby server URL |
| `--port` | `-p` | `DEDUPE_EMBY_PORT` | Emby server port |
| `--api-key` | `-a` | `DEDUPE_EMBY_API_KEY` | Emby API key |
| `--library` | `-l` | `DEDUPE_EMBY_LIBRARY` | Library to scan (repeatable) |
| `--doit` | | `DEDUPE_DOIT` | Actually perform write actions |
| `--lock/--no-lock` | | `DEDUPE_LOCK` | File lock to prevent concurrent runs |
| `-v` | | | Verbosity (repeat for more: `-vv`, `-vvv`) |

### Basic Usage

```shell
# Dry-run duplicate scan
emby-curator --host http://your-emby-server --api-key your_api_key --library "Your Library" dedupe

# With multiple libraries
emby-curator --host http://your-emby-server --api-key your_api_key \
  --library "Movies" --library "TV Shows" \
  dedupe
```

## Environment Variables

The following environment variables can be used to configure the tool:

- `DEDUPE_EMBY_HOST`: The hostname or IP of the Emby server.
- `DEDUPE_EMBY_PORT`: The port for the Emby server (defaults to 8096 if not specified).
- `DEDUPE_EMBY_LIBRARY`: The name(s) of the libraries on the Emby server you want to deduplicate. For multiple libraries, separate with commas.
- `DEDUPE_EMBY_API_KEY`: The API key for the Emby server with appropriate permissions.
- `DEDUPE_DOIT`: Set to 'true' to perform deduplication deletion actions (defaults to 'false').
- `DEDUPE_LOGGING`: The logging level (e.g., ERROR, WARNING, INFO, DEBUG).
- `DEDUPE_EMBY_USERNAME`: Emby username for server access, required if `DEDUPE_DOIT` is 'true'.
- `DEDUPE_EMBY_PASSWORD`: Emby password for server access, required if `DEDUPE_DOIT` is 'true'.
- `DEDUPE_HTML_REPORT`: Set to 'true' to generate an HTML report.
- `DEDUPE_HTML_ONLY`: Set to 'true' to generate only the HTML report without terminal output.
- `DEDUPE_LANG_PRIO`: Comma-separated list of language codes in priority order (e.g., 'slo,cze,eng'). Media items with higher priority languages will be kept.
- `DEDUPE_EXCLUDE_IDS`: Comma-separated list of provider IDs to exclude from deduplication (e.g., 'tt1234567,123456'). Works with IMDB (tt prefix), TMDB, and TVDB IDs.
- `DEDUPE_EXCLUDE`: Comma-separated list of terms to exclude from deduplication. If a movie title contains any of these terms, it will be skipped.
- `DEDUPE_FLARESOLVERR_URL`: FlareSolverr endpoint used by the `csfd` commands to get past ČSFD's bot check (default `http://localhost:8191/v1`).
- `DEDUPE_TYPESAFE_API_KEY`: TypeSafe API key for `csfd fill --ai-match` (`TYPESAFE_API_KEY` is accepted too). Never pass it on the command line.
- `DEDUPE_ANTHROPIC_API_KEY`: Anthropic API key for the Claude backend of `csfd fill --ai-match` (needs `pip install 'emby-curator[claude]'`). The generic `ANTHROPIC_API_KEY` is deliberately ignored, so a key set for another tool doesn't switch it on.

## Command-line Arguments

The CLI uses a two-level structure: shared options first, then a subcommand with its own options.

### Shared options (before subcommand)

```
emby-curator [OPTIONS] SUBCOMMAND [SUBCOMMAND-OPTIONS]

  -H, --host TEXT        Emby server URL          [env: DEDUPE_EMBY_HOST]
  -p, --port INTEGER     Emby server port          [env: DEDUPE_EMBY_PORT]
  -a, --api-key TEXT     Emby API key              [env: DEDUPE_EMBY_API_KEY]
  -l, --library TEXT     Library to scan (repeatable) [env: DEDUPE_EMBY_LIBRARY]
  --doit                 Perform write/delete actions [env: DEDUPE_DOIT]
  --lock/--no-lock       Enable file lock           [env: DEDUPE_LOCK]
  -v                     Verbosity (-v, -vv, -vvv)
```

### dedupe subcommand options

```
emby-curator ... dedupe [OPTIONS]

  --username TEXT        Emby username             [env: DEDUPE_EMBY_USERNAME]
  --password TEXT        Emby password             [env: DEDUPE_EMBY_PASSWORD]
  --lang-prio TEXT       Language priority order, comma-separated (e.g. slo,cze,eng) [env: DEDUPE_LANG_PRIO]
  --exclude-ids TEXT     Provider IDs to exclude (IMDB/TMDB/TVDB, comma-separated) [env: DEDUPE_EXCLUDE_IDS]
  --html-report          Generate HTML report and open in browser
  --no-open              Generate HTML report without opening browser
  --html-only            HTML report only, no terminal output
```

### genres subcommand options

```
emby-curator ... genres audit [--suggest]
emby-curator ... genres normalize [--doit] [--repair-dupes] [--item-ids IDS]
emby-curator ... genres fix [--doit] [--gaps-only | --validate] [--item-ids IDS]
emby-curator ... genres origin [--doit] [--genre-name NAME] [--item-ids IDS]
```

## Examples

### Generating a List of Duplicates (Dry Run)

The following command simulates the deduplication process without making any changes:

Using Docker:

```shell
docker run \
  -e DEDUPE_EMBY_HOST="http://your-emby-server" \
  -e DEDUPE_EMBY_LIBRARY="Your Library Name" \
  -e DEDUPE_EMBY_API_KEY="your_api_key" \
  ghcr.io/barlog951/emby-curator:latest

# For multiple libraries
docker run \
  -e DEDUPE_EMBY_HOST="http://your-emby-server" \
  -e DEDUPE_EMBY_LIBRARY="Movies,TV Shows" \
  -e DEDUPE_EMBY_API_KEY="your_api_key" \
  ghcr.io/barlog951/emby-curator:latest
```

Using Python:

```shell
emby-curator --host "http://your-emby-server" --library "Your Library Name" --api-key "your_api_key" dedupe
```

To scan multiple libraries:

```shell
emby-curator --host "http://your-emby-server" --library "Movies" --library "TV Shows" --api-key "your_api_key" dedupe
```

### Generating an HTML Report

To generate an HTML report with images and detailed metadata:

```shell
emby-curator --host "http://your-emby-server" --library "Your Library Name" --api-key "your_api_key" \
  dedupe --html-report
```

Or to generate only the HTML report without terminal output:

```shell
emby-curator --host "http://your-emby-server" --library "Your Library Name" --api-key "your_api_key" \
  dedupe --html-only
```

### Performing Deduplication Actions

To perform the deletion of duplicates:

Using Docker:

```shell
docker run \
  -e DEDUPE_EMBY_HOST="http://your-emby-server" \
  -e DEDUPE_EMBY_LIBRARY="Your Library Name" \
  -e DEDUPE_EMBY_API_KEY="your_api_key" \
  -e DEDUPE_EMBY_USERNAME="your_emby_username" \
  -e DEDUPE_EMBY_PASSWORD="your_emby_password" \
  -e DEDUPE_DOIT="true" \
  ghcr.io/barlog951/emby-curator:latest
```

Using Python:

```shell
emby-curator --host "http://your-emby-server" --library "Your Library Name" --api-key "your_api_key" \
  --doit dedupe --username "your_emby_username" --password "your_emby_password"
```

### Full Deduplication with All Options

```shell
python -m emby_dedupe \
  --host "http://your-emby-server" \
  --api-key "your_api_key" \
  --library "Movies" \
  --library "TV Shows" \
  --doit \
  dedupe \
  --username "your_username" \
  --password "your_password" \
  --lang-prio "slo,cze,eng" \
  --exclude-ids "tt0468569,tt0080684,550" \
  --html-report \
  --html-only
```

### Using Language Prioritization

To prioritize keeping media items with specific languages over others:

Using Docker:

```shell
docker run \
  -e DEDUPE_EMBY_HOST="http://your-emby-server" \
  -e DEDUPE_EMBY_LIBRARY="Movies" \
  -e DEDUPE_EMBY_API_KEY="your_api_key" \
  -e DEDUPE_LANG_PRIO="slo,cze,eng" \
  ghcr.io/barlog951/emby-curator:latest
```

Using Python:

```shell
emby-curator --host "http://your-emby-server" --library "Movies" --api-key "your_api_key" \
  dedupe --lang-prio "slo,cze,eng"
```

This will prioritize keeping files with Slovak audio tracks first, then Czech, then English. When multiple files have the same highest-priority language, the one with better quality will be kept.

### Excluding Media by Provider IDs

To exclude specific media items from deduplication using their provider IDs:

Using Docker:

```shell
docker run \
  -e DEDUPE_EMBY_HOST="http://your-emby-server" \
  -e DEDUPE_EMBY_LIBRARY="Movies" \
  -e DEDUPE_EMBY_API_KEY="your_api_key" \
  -e DEDUPE_EXCLUDE_IDS="tt0468569,tt0080684,550" \
  ghcr.io/barlog951/emby-curator:latest
```

Using Python:

```shell
emby-curator --host "http://your-emby-server" --library "Movies" --api-key "your_api_key" \
  dedupe --exclude-ids "tt0468569,tt0080684,550"
```

This will exclude The Dark Knight (tt0468569), The Empire Strikes Back (tt0080684), and Fight Club (TMDB ID 550) from deduplication. Useful for preserving intentional multiple versions.

The excluded IDs can be:
- IMDB IDs (start with "tt" followed by 7-8 digits)
- TMDB IDs (numeric values)
- TVDB IDs (numeric values)

### Genre Management

```shell
# Audit genre health (read-only)
emby-curator --host "http://your-emby-server" --api-key "your_api_key" --library "Movies" genres audit

# Preview variant name normalization
emby-curator --host "http://your-emby-server" --api-key "your_api_key" --library "Movies" genres normalize

# Apply normalization
emby-curator --host "http://your-emby-server" --api-key "your_api_key" --library "Movies" --doit genres normalize

# Fill missing genres from TMDB/OMDb (validate mode — only items with no genres)
emby-curator --host "http://your-emby-server" --api-key "your_api_key" --library "Movies" --doit genres fix --validate

# Target specific items (used by webhook listener)
emby-curator --host "http://your-emby-server" --api-key "your_api_key" --doit genres normalize --item-ids 123,456

# Preview which movies and series are Czech/Slovak, then give them the extra genre
emby-curator --host "http://your-emby-server" --api-key "your_api_key" genres origin --all-libraries
emby-curator --host "http://your-emby-server" --api-key "your_api_key" genres origin --all-libraries --doit
```

`genres origin` adds one extra genre (default `Česko-slovenský`, change it with `--genre-name`) to
Czech and Slovak movies and TV series, so viewers can browse them in the Genres view. The English
genres stay, and the command never removes the genre, so one you add by hand is kept. A title counts
when TMDb gives its original language as Czech or Slovak, or when it is a non-English co-production
with Czechia, Slovakia or Czechoslovakia (e.g. *A Royal Affair*). English-language films that TMDb
lists as Czech only because they were shot in Prague (*The Bourne Identity*) don't count. For titles
TMDb doesn't know, the first country of origin on ČSFD decides (that lookup uses FlareSolverr; see
below). It needs `DEDUPE_TMDB_API_KEY`.

Every run also re-checks the titles that already have the genre and lists the ones the rules no
longer back, for example an item Emby has since re-identified. It doesn't remove the genre from them.
For automatic upkeep, run it monthly with `--all-libraries --doit` for the whole library, and have the
webhook listener (`scripts/genre-webhook-listener.py`) run it with `--item-ids` right after `genres
process` on newly added items.

### ČSFD Metadata

The `csfd` commands reach csfd.sk through a [FlareSolverr](https://github.com/FlareSolverr/FlareSolverr)
container (`docker run -d -p 8191:8191 ghcr.io/flaresolverr/flaresolverr:latest`). They only fill
**empty** fields, and they are dry runs unless `--doit` is given. A matched item gets a `Csfd`
provider id, so later runs skip it.

```shell
# Preview what ČSFD would fill (dry run) and write a TSV report
emby-curator --host "http://your-emby-server" --api-key "your_api_key" csfd fill --all-libraries --report csfd.tsv

# Apply, with a hand-made map for titles the search can't resolve
emby-curator --host "http://your-emby-server" --api-key "your_api_key" csfd fill --all-libraries --map csfd-map.tsv --doit

# Portraits and biographies for actors with no photo (most-used actors first)
emby-curator --host "http://your-emby-server" --api-key "your_api_key" csfd people --all-libraries --limit 500 --doit
```

The map file (`--map`) has one `<emby_id><TAB><csfd_url>` per line, and `#` starts a comment.
A line `<emby_id><TAB>-` records that a title has no ČSFD entry, so it is never searched or suggested again.

**AI match fallback (`--ai-match`).** When the strict rules find no unambiguous match, an AI chooses
among the year-plausible candidates, or answers "none". Each candidate is described with its country and
original titles. Only the item's title, year, type and folder name are sent. By default the picks are
**suggestions only**: they are written to `--ai-review-file` (default `csfd-ai-review.tsv`) in map format.
Delete the lines you reject and append the rest to your map.

`--ai-provider` picks the backend. `auto` (the default) uses every backend that has a key:
- `claude`: Claude Haiku 5.5, asked 5 times. The confidence is the share of votes. It finds the most
  matches, but a unanimous vote can still be wrong, so on its own it **only suggests** (`--ai-auto` is refused).
- `jev`: TypeSafe's Jev, with its own confidence score. It is more conservative and finds fewer matches.
- `both`: Claude proposes and Jev checks. Each review line is tagged `both-agree`, `claude-only`,
  `jev-only` or `conflict`. In a test on 43 hand-checked titles, every agreed pick was right.

`--ai-auto` applies picks at or above `--ai-threshold` (default 0.9): with `jev` alone, or with `both` when
the two agree. With `both`, the threshold applies to Claude's vote share, so 0.9 means all 5 votes.
Run review-only for a while before enabling it.

```shell
pip install 'emby-curator[claude]'
export DEDUPE_ANTHROPIC_API_KEY=...  # from https://console.anthropic.com
export DEDUPE_TYPESAFE_API_KEY=...   # from https://console.typesafe.ai
emby-curator --host "http://your-emby-server" --api-key "your_api_key" csfd fill --all-libraries --map csfd-map.tsv --ai-match --doit
```

## Python API

Besides the CLI, `emby-curator` exposes a small stable Python API for embedding the
quality check in your own tools (e.g. a downloader deciding whether an existing copy
is good enough). Import the supported symbols from the top-level package:

```python
from emby_dedupe import EmbyChecker, CheckConfig

# Reads ~/.emby-dedupe/config.yaml (raises EmbyConfigMissingError if absent).
with EmbyChecker.from_config() as checker:
    # Fast connectivity/credential probe (real GET /System/Info, never cached).
    checker.test_connection()

    result = checker.check(
        name="Inception", year=2010,
        resolution="2160p", codec="hevc", hdr="HDR10", size_mb=15000,
    )
    if result.should_download:
        print(f"grab it — {result.reason}")
```

Supported surface (re-exported from `emby_dedupe`, safe to depend on):
`EmbyChecker`, `CheckConfig`, `ComparisonResult`, `ExistingQuality`,
`ProposedQuality`, `Config`. `check()` accepts either a `CheckConfig` or keyword
arguments matching its fields; an unknown keyword raises `TypeError`. Configuration
and connection problems raise the `EmbyDedupeError` subclasses in
`emby_dedupe.utils.exceptions` (`EmbyConfigError` — also a `ValueError`;
`EmbyConfigMissingError` — also a `FileNotFoundError`; `EmbyServerConnectionError`).

## API Key Requirement

A valid API key with enough permissions to access the necessary operations on the Emby server must be provided. This API key is used to authenticate the script with the Emby server for read and list actions. Deletion operations require username and password credentials for additional authentication.

## Project Structure

The codebase is organized into modules according to their functionality:

```
emby_dedupe/
├── api/              # Emby API client and deduplication logic
├── cli/              # Command-line interface
├── models/           # Data models
├── reports/          # Report generation (Markdown, HTML)
└── utils/            # Utility functions and helpers
```

## Development and Testing

For development, install the package with development dependencies:

```shell
git clone https://github.com/Barlog951/emby-curator.git
cd emby-dedupe
pip install -e ".[dev]"
```

### Development Containers

#### VS Code Dev Container

This project includes a development container configuration for Visual Studio Code, which provides a consistent development environment with all necessary tools pre-installed:

1. Install [Visual Studio Code](https://code.visualstudio.com/) and the [Remote - Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension
2. Clone the repository and open it in VS Code
3. When prompted to "Reopen in Container", click "Reopen in Container"
   - Alternatively, use the Command Palette (F1) and select "Remote-Containers: Reopen in Container"
4. The container will build and start with Python 3.12, required dependencies, and useful extensions installed

The development container includes:
- Python 3.12 environment
- All project dependencies
- Development tools (ruff, mypy, pytest)
- Docker-in-Docker support for testing container builds
- GitHub CLI for managing issues and PRs
- VSCode extensions for Python development and GitHub integration

#### PyCharm Docker Integration

You can also use PyCharm with the included development Dockerfile:

1. Install [PyCharm Professional](https://www.jetbrains.com/pycharm/) (required for Docker integration)
2. Install Docker on your system
3. In PyCharm, go to File → Settings → Build, Execution, Deployment → Docker and set up Docker integration
4. Open the project in PyCharm
5. Set up a Docker-based Python interpreter:
   - Go to File → Settings → Project → Python Interpreter
   - Click the gear icon → Add
   - Select "Docker" and use the included `Dockerfile.dev`
   - Set the Python interpreter path to `/usr/local/bin/python`

For services and run configurations:
1. Go to Run → Edit Configurations
2. Add a new Python configuration
3. Set "Run with Docker" in the "Python interpreter" dropdown
4. Set the module to run to the package entry point (`emby_dedupe`, i.e. `python -m emby_dedupe` — routes to the typer CLI in `emby_dedupe/cli/app.py`)

The `Dockerfile.dev` includes:
- Python 3.12 environment
- Development dependencies 
- Common utilities needed for development
- The package installed in development mode

This provides a consistent environment for development and testing, regardless of your local setup.

### Running Tests

The project has a comprehensive test suite with 1288 tests covering 89% of the codebase. The tests are organized in the same structure as the main package:

```
tests/
├── unit/
    ├── api/              # Tests for API client and deduplication
    ├── cli/              # Tests for CLI functionality
    ├── models/           # Tests for data models
    ├── reports/          # Tests for report generation
    └── utils/            # Tests for utility functions
```

To run the tests:

```shell
# Run all tests
pytest

# Run tests with coverage report
pytest --cov=emby_dedupe

# Run tests with detailed coverage report
pytest --cov-report term-missing --cov=emby_dedupe

# Run a specific test file
pytest tests/unit/models/test_disjoint_set.py
```

You can use the provided Makefile for common development tasks:

```shell
# Run all tests
make test

# Run tests with coverage report
make coverage

# Run linting checks
make lint

# Run type checking
make mypy

# Auto-fix linting issues
make allfx

# Clean build artifacts
make clean

# Run all checks (clean, install dev dependencies, lint, type check, test)
make all
```

### Continuous Integration

The project uses GitHub Actions for continuous integration with these workflows:

1. **Python Tests**: Runs linting, type checking, and tests on every push and PR
2. **Security Scan**: Performs security analysis with Bandit and CodeQL
3. **Docker Builds**: Creates multi-architecture containers for edge, pre-release, and release tags

#### Test Coverage

Current test coverage by module (89% overall):
- API Client: 93%
- Deduplication Logic: 89%
- Metadata Handling: 91%
- HTML Reports: 84%
- Markdown Reports: 71%
- Common Report Functions: 90%
- HTTP Utilities: 100%
- Logging: 100%
- CLI: 55-66%

The tests cover all key functionality including:
- Duplicate identification and rationalization
- Media quality rating
- HTML and Markdown report generation
- Language prioritization features
- Exclusion term processing
- Error handling and edge cases

## Contributing

We welcome your contributions. If you encounter bugs or have suggestions for improvement, please feel free to open an issue on the [GitHub repository](https://github.com/Barlog951/emby-curator). Pull requests are also greatly appreciated.

## License

This project is licensed under the Apache License 2.0 - see the [LICENSE](https://github.com/Barlog951/emby-curator/blob/main/LICENSE) file for details.
