Categories & Tags
Two complementary grouping primitives — categories are 1:N with save-path inheritance, tags are M:N labels.
Mosaic models two orthogonal kinds of organization:
- Categories — exactly one per torrent (or none). Carries an optional
default_save_path. Mental model: a folder. - Tags — many per torrent. Pure labels. Mental model: a hashtag.
Both are first-class in the UI and the API.
Categories
A category is {id, name, default_save_path, color}. The save path is what makes categories more than a label: when you add a torrent and a category is selected, the desktop Add modal consults the category’s default_save_path and pre-fills the save-path field — the UI is the layer that does this lookup. The AddMagnet / AddTorrentFile / AddTorrentBytes API calls do not take a category_id, so save-path inheritance from category at add-time is a UI affordance, not a server-side automatic.
Examples:
| Category | default_save_path |
|---|---|
| Movies | /media/movies |
| TV Shows | /media/tv |
| ISOs | /srv/iso |
| Games | /srv/games |
A torrent can be uncategorized (category_id = null), but a torrent cannot have two categories. The UI renders the chip with the category’s color for at-a-glance scanning.
Operations
| Action | Endpoint |
|---|---|
| List | GET /api/categories |
| Create | POST /api/categories |
| Update | PUT /api/categories |
| Delete | DELETE /api/categories/{id} |
| Set on torrent | POST /api/torrents/category (infohash, category_id or null) |
Deleting a category does not remove the torrents in it; their category_id becomes null.
Tags
A tag is {id, name, color}. Tags can be assigned to as many torrents as you like, and a torrent can carry as many tags as you like. The relationship lives in a join table (torrent_tags).
Tags are good for cross-cutting concerns categories don’t capture:
to-watch,seed-forever,archivedlinux-iso,documentary,4ktemp,friend-shared
Operations
| Action | Endpoint |
|---|---|
| List | GET /api/tags |
| Create | POST /api/tags |
| Delete | DELETE /api/tags/{id} |
| Assign | POST /api/tags/assign (infohash, tag_id) |
| Unassign | POST /api/tags/unassign (infohash, tag_id) |
Each TorrentDTO carries a tags: TagDTO[] field with the full per-torrent tag set, so you don’t need a second round-trip to render tag chips.
When to use which
- If the thing implies a save path → category.
- If the thing is a label that doesn’t dictate location → tag.
- If you find yourself wanting two categories for the same torrent → that’s a tag.
A torrent can absolutely have a category and tags simultaneously, and that’s the common case.
Persistence
Categories live in the categories SQLite table; tags in tags; assignment in torrent_tags. Foreign keys cascade on delete:
- Deleting a tag drops its assignments.
- Deleting a category sets the affected torrents’
category_idto null.
Both are stable across restarts and survive RestoreOnStartup.