simkl 0.1.0

Library to build queries for SIMKL and decoding JSON responses using Serde
Documentation
# Simkl Crate β€” Roadmap

Issues are grouped by severity and topic, derived from a cross-review of the source against
`docs/simkl.apib`.

---

## πŸ”΄ Critical β€” Broken at Runtime

### URL Construction

- [x] **Pervasive missing `/` separator** (`search.rs`, `lib.rs`, `pin.rs`, `sync.rs`,
  `calendar.rs`, `user.rs`)
  Fixed: prefixed every path segment with `"/"` in all standalone `get_*_request()`
  functions to match the convention established by `SimklRequest::build_url()`.

- [x] **Wrong domain in `get_monthly_request`** (`calendar.rs`)
  Fixed: now uses the new `CALENDAR_BASE_URL = "https://data.simkl.in/calendar/"` constant
  instead of `API_URL`.

- [x] **Wrong OAuth authorize domain in `get_auth_url`** (`lib.rs`)
  Fixed: now uses `OAUTH_URL` (`https://simkl.com/oauth/authorize`) and appends the query
  string directly, rather than starting from `API_URL`.

- [x] **`get_search_request` writes the literal string `"type"` in the path**
  (`search.rs`)
  Fixed: path is now `/search/{type}` with the `type` parameter interpolated; `None` type
  falls back to `/search/`.

### Image URLs

- [x] **`get_avatar_url` uses `"episodes/"` instead of `"avatars/"`** (`images.rs:113`)
  Fixed: path prefix changed to `"avatars/"`.

### API Endpoints

- [x] **`EpisodesRequest` has the wrong path structure** (`request.rs:301-305`)
  Fixed: endpoint is now `/tv/episodes/{show_id}`; `season` moved from the URL path to a
  query parameter, matching the spec.

### Unimplemented Stubs

- [x] **`get_rating()` panics unconditionally** (`lib.rs:351`)
  Removed: the stub had no parameters, wrong return type, and no implementation. Replaced
  with a `// TODO` comment pointing to the Ratings API roadmap item.

- [x] **`FindRandomPayload::to_url_param()` panics unconditionally** (`search.rs:181`)
  Fixed: removed `todo!()`, implemented `&type=` serialization (mapping `MediaType::Show`
  to spec value `"tv"`). Genre serialization left as a `TODO` comment pending idiomatic
  genre enum naming (see Low section).

---

## 🟠 High β€” Tests Fail / Correctness

### Failing Tests

- [x] **`test_avatar_urls`** (`images.rs:256-278`)
  Fixed: all assertions updated to use `avatars/` prefix; `AvatarSize::Big` corrected to
  `_256` and `AvatarSize::VeryBig` corrected to `_512`, both matching the spec.

- [x] **`test_episode_urls` β€” `M112x63` case** (`images.rs:246-252`)
  Fixed: assertion corrected from `_s.webp` to `_m.webp` per spec (`episodes | _m | 112Γ—63`).

- [x] **`test_id_request` wrong expected URL** (`search.rs:225-235`)
  Fixed: assertion updated to `"https://api.simkl.com/search/id?…"` as part of the URL
  construction fix.

### Duplicated Types

- [ ] **`SimklError` is defined in both `lib.rs` and `error.rs`** (`lib.rs:22-56`,
  `error.rs:6-40`)
  Identical variants, identical `Display` / `From` impls. Remove the copy in `lib.rs`; the
  canonical definition with the `Result<T>` alias belongs in `error.rs`.

---

## 🟑 Medium β€” Missing Coverage / API Mismatch

### Missing API Modules

- [x] **Ratings API** (`/ratings`, `/ratings/{type}`)
  Added `ratings.rs`: `RatingsById` and `RatingsByWatchlist` request structs (both implement
  `SimklRequest`), `RatingsResponse` / `WatchlistRatingsItem` response types, `WatchlistType`
  enum, `fields` constants module. 8 unit tests + 1 doc-test.

- [x] **Redirect API** (`/redirect`)
  Added `redirect.rs`: `RedirectRequest` builder (implements `SimklRequest`), `RedirectTarget`
  enum (`Simkl`, `Trailer`, `Twitter`, `Watched`). All 16 ID fields present, including new
  `traktslug` and `letterboxd`. 8 unit tests + 1 doc-test.

- [x] **Checkin API** (`/checkin`)
  Added `checkin.rs`: `CheckinPayload` (movie/show constructors), `CheckinEpisode`,
  `ShowCheckinItem`, `CheckinResponse`, `CheckinConflict`. 4 unit tests.

### Incomplete Sync Module

- [x] **Sync POST endpoints have URL stubs but no payload types** (`sync.rs`)
  Added full payload types: `WatchedSeason`, `WatchedEpisode`, `MovieHistoryItem`,
  `ShowHistoryItem`, `AnimeHistoryItem`, `SyncHistoryPayload`, `MovieRatingItem`,
  `ShowRatingItem`, `AnimeRatingItem`, `SyncRatingPayload`, `MovieListItem`, `ShowListItem`,
  `AnimeListItem`, `SyncListPayload`. Response types: `SyncAddedCounts`, `SyncNotFound`,
  `SyncResult`. 8 unit tests.

### Missing ID Fields

- [x] **`IdLookup` is missing `traktslug` and `letterboxd`** (`search.rs:15-37`)
  Added `traktslug: Option<String>` and `letterboxd: Option<String>` to `IdLookup`; URL
  builder `get_search_by_id_request` now appends `&traktslug=` / `&letterboxd=` when set.

### Data Model Mismatches

- [x] **`PaginatedResponse<T>` doesn't match the API** (`pagination.rs:59-73`)
  Added `SimklResponse::pagination_info()` (`response.rs`) that parses the four
  `X-Pagination-*` response headers and returns `Option<PaginationInfo>`. The
  `PaginatedResponse<T>` wrapper is kept as an optional in-memory helper; the doc comment
  now clarifies that pagination comes from headers. 4 unit tests.

- [x] **`Extended::default()` sets `full = true`** (`lib.rs:83-96`)
  Changed to `#[derive(Default)]` β€” all `bool` fields default to `false`, which is the
  neutral "no extra data" mode. Callers opt in by setting `full = true` or individual flags.

---

## 🟒 Low β€” Code Quality / Style

### Naming

- [x] **Non-idiomatic enum variants in genre enums** (`anime.rs`, `movie.rs`, `show.rs`)
  Removed `#[allow(non_camel_case_types)]`. All variants renamed to PascalCase
  (`MartialArts`, `SciFi`, `SliceOfLife`, `AwardsShow`, `ScienceFiction`, etc.).
  Added `as_str()` methods returning the kebab-case API values (`"sci-fi"`,
  `"slice-of-life"`, etc.) and `Display` impls. Genre URL param now implemented in
  `FindRandomPayload::to_url_param()`. Tests added for all three enums.

- [x] **`MovieGenre::Family` is PascalCase; all other variants are lowercase** (`movie.rs:13`)
  Fixed: all variants are now PascalCase consistently. `Family` maps to API string `"family"`.

- [x] **`FanartSize::Mobile950x540` has wrong dimensions in its name** (`images.rs:36`)
  Renamed to `Mobile960x540`. CDN suffix `_mobile` is unchanged.

### Error Handling

- [x] **Error messages are in French** (`error.rs:19-24`)
  `"Erreur JSON"` → `"JSON error"`, `"Paramètres invalides"` → `"invalid parameters"`,
  `"Erreur de parsing"` β†’ `"parse error"`. Doc comments on variants also translated.

- [x] **`MediaType::from_str` returns `Err(())`** (`lib.rs`)
  Added `pub struct ParseMediaTypeError(pub String)` with `Display` + `std::error::Error`.
  `FromStr::Err` changed from `()` to `ParseMediaTypeError`; error carries the unknown string.

### Field Visibility

- [x] **`User::joined_at` is private while all other `User` fields are public** (`user.rs`)
  Made `pub`.

- [x] **`WatchedLastWeek`, `Count`, `TvOrAnime::total_mins` fields are all private** (`user.rs`)
  All fields made `pub`.

- [x] **`Rank`, `Ratings` in `lib.rs` have private fields**
  All fields in `Rank` and `Ratings` made `pub`.

### Redundant / Dead Code

- [x] **`get_extended_parameter`: redundant `.collect()`** (`lib.rs`)
  `selected.into_iter().collect::<Vec<&str>>().join(",")` β†’ `selected.join(",")`.

- [x] **`RateLimiter` is never used** (`rate_limit.rs`)
  Added module-level doc comment with usage example, documenting it as an exported helper
  for callers. The doc-test also runs as a test.

- [x] **Webp support for avatars contradicts the spec** (`images.rs`)
  Added doc comment on `get_avatar_url` explaining the `webp` parameter is a deliberate
  client-side extension via the `wsrv.nl` proxy; pass `false` to stay spec-compliant.

- [x] **Commented-out dead code removed**
  - `show.rs` β€” removed the 47-line commented-out `Show` impl block
  - `search.rs` β€” removed the commented-out `extended` snippet in `get_search_request`
  - `pagination.rs` β€” removed commented-out offset support throughout