podpull 1.0.0

A fast, minimal CLI tool for downloading and synchronizing podcasts from RSS feeds
Documentation
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

podpull is a Rust CLI tool for downloading and synchronizing podcasts from RSS feeds. It's designed for local backup with no external services, accounts, or databases — the output directory itself serves as the state.

**CLI**: `podpull [OPTIONS] <feed> <output-dir>`
- `feed` - RSS feed URL or local file path
- `output_dir` - Directory for downloaded episodes
- `-c, --concurrent <N>` - Max concurrent downloads (default: 3)
- `-l, --limit <N>` - Download only N most recent undownloaded episodes
- `-q, --quiet` - Suppress progress output

## Development Commands

```bash
cargo build              # Build debug version
cargo build --release    # Build optimized release version
cargo run -- <args>      # Run with arguments
cargo test               # Run all tests
cargo clippy             # Run linter
cargo fmt                # Format code
```

## Code Architecture

```
src/
├── main.rs          # CLI entry, argument parsing, progress UI (indicatif)
├── lib.rs           # Public API exports
├── error.rs         # Error types: FeedError, DownloadError, MetadataError, StateError, SyncError
├── sync.rs          # Main orchestration: fetch → parse → scan → plan → download
├── state.rs         # Output directory scanning, sync planning, episode sorting
├── http.rs          # HttpClient trait + reqwest implementation
├── progress.rs      # ProgressEvent enum + ProgressReporter trait
├── feed/
│   ├── fetch.rs     # Fetch from URL or local file
│   └── parse.rs     # RSS parsing → Podcast/Episode structs
├── episode/
│   ├── download.rs  # Stream to .partial file, SHA-256 hash, atomic rename
│   └── filename.rs  # Generate filenames from metadata
└── metadata/
    ├── episode.rs   # Per-episode JSON metadata
    └── podcast.rs   # Feed-level podcast.json
```

### Key Design Patterns

- **Stateless**: No database; state derived from output directory files
- **Trait abstraction**: `HttpClient` and `ProgressReporter` traits enable testing with mocks
- **Atomic downloads**: Write to `.partial` file, rename on completion
- **Content integrity**: SHA-256 hash stored in episode metadata
- **GUID deduplication**: Track episodes by RSS GUID (fallback to URL if missing)
- **Concurrent slots**: Downloads run in parallel but respect `--limit` ordering

### Output Structure

```
output-dir/
├── podcast.json                 # Feed metadata
├── 2024-01-15-episode-title.mp3 # Audio file
└── 2024-01-15-episode-title.json # Episode metadata (guid, content_hash, downloaded_at)
```

## Testing

Tests use `tempfile::tempdir()` for isolation and mock `HttpClient` implementations.

```bash
cargo test                    # Run all tests
cargo test sync::tests        # Run sync module tests
cargo test state::tests       # Run state module tests
```

Key test files: `sync.rs` (3 tests), `state.rs` (8 tests), `feed/parse.rs`, `episode/download.rs`

## Code Quality

Before committing:
1. `cargo fmt` - Format code
2. `cargo clippy` - Check for lint issues
3. `cargo test` - Ensure tests pass

## Architecture Decision Records (ADRs)

ADRs are stored in `doc/adr/` (16 ADRs documenting key decisions). Use the `adrs` tool:

**CRITICAL: Always set `EDITOR=true` to prevent hanging:**
```bash
EDITOR=true adrs new "Title of decision"
EDITOR=true adrs list
EDITOR=true adrs link <source> <link-type> <target>
```

## Commit Style

- **Atomic commits**: One logical change per commit
- **Working state**: Every commit must build and pass tests
- **No AI attribution**: No mentions of AI/Claude in commit messages, no `Co-Authored-By` lines
- **Message format**: Imperative subject line, optional body explaining "why"

Example:
```
Add retry logic for failed downloads

Transient network errors are common with podcast feeds. Retrying
up to 3 times with exponential backoff improves reliability.
```