Mixtape Manager¶
The mixtape_manager package defines the MixtapeManager class, This class acts as the core data access and management layer for mixtape-related features in the larger system, abstracting away file system operations and data consistency concerns.
It is responsible for managing the storage, retrieval, and organization of mixtape data and their associated cover images on disk. It provides a high-level interface for saving, deleting, listing, and loading mixtape metadata, handling both JSON data and image files. The class ensures that mixtape data is consistently stored, cover images are processed from base64, and metadata is maintained for easy retrieval and display.
🏛️ Architecture Overview¶
classDiagram
class MixtapeManager {
-Path path_mixtapes
-Path path_cover
-MusicCollection collection
-Logger _logger
+MixtapeManager(path_mixtapes, collection, logger=None)
+save(mixtape_data) str
+update(slug, updated_data) str
+get(slug) dict|None
+list_all() list[dict]
+delete(slug) None
-_sanitize_title(title) str
-_generate_unique_slug(base_slug, current_slug=None) str
-_save_with_slug(mixtape_data, title, slug) str
-_process_cover(cover_data, slug) str|None
-_cover_resize(image, new_width=1200) Image
-_find_by_client_id(client_id) dict|None
-_verify_against_collection(data) (dict, bool|None)
-_verify_mixtape_metadata(data) dict
-_normalize_timestamps(data) dict
-_convert_old_mixtape(data) dict
}
- Storage root –
path_mixtapes(provided by the caller). - Cover sub‑directory –
path_cover = path_mixtapes / "covers"(created automatically). - Dependency –
Pillow(PIL.Image) is required for cover‑image resizing.
📂 Filesystem Layout & JSON Schema¶
/path/to/mixtapes/
│
├─ my‑awesome‑mix.json ← Mixtape metadata (UTF‑8 JSON)
├─ another‑mix.json
│
└─ covers/
├─ my‑awesome‑mix.jpg ← JPEG cover (max width 1200 px, quality 100)
└─ another‑mix.jpg
Minimal JSON structure (all fields are optional unless noted)¶
| Field | Type | Description |
|---|---|---|
title |
str |
Human-readable title (used for slug generation). |
client_id |
str |
Optional identifier that ties a mixtape to a specific client/device. |
created_at |
ISO-8601 str |
Set on first save; auto-filled if missing. |
updated_at |
ISO-8601 str |
Refreshed on every save or update. |
cover |
str |
Relative path to the JPEG cover (covers/<slug>.jpg) or a data-URI (data:image/...). |
liner_notes |
str |
Free-form notes; defaults to "". |
tracks |
list[dict] |
Each entry must contain at least path (relative to the music root). Other keys (artist, album, track, duration, filename, cover) are filled/validated on read. |
slug |
str (added on output) |
Filesystem-safe identifier derived from title, added by the manager on output. |
⚙️ Behavior Details¶
Slug generation & uniqueness¶
_sanitize_titleremoves any character that isn’t alphanumeric, hyphen, underscore, or space, then strips surrounding whitespace._generate_unique_slugcheckspath_mixtapes/*.json. If a conflict exists it appends-1,-2, … until a free filename is found.- When updating an existing mixtape you may pass
current_slugso the method can reuse the same filename.
Client‑ID reuse on save¶
- If
mixtape_datacontains aclient_id,save()first calls_find_by_client_id. - The first mixtape that matches that
client_idis updated in‑place (the same slug is kept). - If no match is found a brand‑new mixtape is created.
Timestamp handling & legacy migration¶
- On first creation both
created_atandupdated_atare set todatetime.now().isoformat(). - On every
updateonlyupdated_atis refreshed. _normalize_timestampsmigrates a historicsaved_atfield toupdated_atand guarantees thatcreated_atandupdated_atare present (fills missing values withNoneor the current time).
Cover image handling¶
| Helper | What it does | Important details |
|---|---|---|
_process_cover(cover_data: str, slug: str) → str \| None |
Decodes a data:image/...;base64,… string, resizes the image, saves it as a JPEG under covers/<slug>.jpg, and returns the relative path (covers/<slug>.jpg). |
Returns None on any error; the calling code keeps the original cover value. |
_cover_resize(image: Image, new_width: int = 1200) → Image |
Resizes the Pillow Image to a maximum width of new_width while preserving aspect ratio. |
Uses Image.LANCZOS for high-quality down-sampling. |
_save_with_slug |
Handles cover persistence during save operations. | If cover starts with data:image, delegates to _process_cover. If it is a plain string (already a relative path), it is stored unchanged. Errors during cover processing are logged but do not abort the mixtape save. |
- The JPEG is saved with quality = 100 (lossless as far as JPEG permits).
- The filename on disk is exactly
covers/<slug>.jpg– the method builds the relative path string for the JSON field.
Verification against the music collection¶
get(slug)loads the JSON, then calls_verify_against_collection.- For each track it fetches the canonical record from the injected MusicCollection (
self.collection.get_track(path=Path(track* ["path"]))). - Missing or stale fields (
artist,album,track,filename,duration,cover) are refreshed. - If the collection cannot be accessed (e.g., DB corruption) the method falls back to the raw JSON and logs a warning.
Legacy field conversion¶
- Older mixtapes stored a track title under the key
title._convert_old_mixtaperenames that key totrackso the rest of the code can rely on a uniform schema.
Error handling & robustness¶
- Corrupted JSON files – both
list_alland_find_by_client_idcatch json.JSONDecodeError / OSError, log a warning, and simply skip the offending file. - Cover‑image failures –
_process_covercatches any exception, logs an error, and returns None. The mixtape is still saved; thecoverfield will be omitted or retain its previous value. - Missing slug on update –
updateraisesFileNotFoundErrorif the target JSON does not exist, making the failure explicit to the caller.
🔁 Sequence Diagram¶
sequenceDiagram
participant Client
participant MixtapeManager
participant Disk
participant MusicCollection
Client->>MixtapeManager: save(mixtape_data)
MixtapeManager->>Disk: Write JSON file (<slug>.json)
alt cover is a data‑uri
MixtapeManager->>Disk: Decode + resize + JPEG → covers/<slug>.jpg
end
MixtapeManager-->>Client: slug
Client->>MixtapeManager: get(slug)
MixtapeManager->>Disk: Read JSON file
MixtapeManager->>MusicCollection: get_track for each track
MusicCollection-->>MixtapeManager: track metadata
MixtapeManager-->>Client: enriched mixtape dict
Client->>MixtapeManager: list_all()
MixtapeManager->>Disk: glob *.json, read each
MixtapeManager-->>Client: sorted list of mixtapes
Client->>MixtapeManager: delete(slug)
MixtapeManager->>Disk: Delete JSON + optional cover
MixtapeManager-->>Client: (none)
🔌 API¶
MixtapeManager(path_mixtapes, collection, logger=None)
¶
Manages mixtape files and their associated cover images.
Provides functionality to create, update, delete, list, and retrieve mixtapes stored on disk.
Initializes the MixtapeManager with paths for mixtapes and covers.
Sets up the directory structure for storing mixtape JSON files and cover images.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path_mixtapes
|
Path
|
Path to the directory where mixtapes are stored. |
required |
collection
|
MusicCollection
|
Access to the collection's metadata |
required |
logger
|
Logger
|
Optional logger instance for logging actions. |
None
|
Methods:
| Name | Description |
|---|---|
save |
Creates a new mixtape or updates an existing one based on client identity. |
update |
Updates an existing mixtape's data while preserving key metadata. |
delete |
Deletes a mixtape and its associated cover image by slug. |
list_all |
Lists all mixtapes stored on disk. |
get |
Retrieves a single mixtape by its slug if it exists and is valid. |
Source code in src/mixtape_manager/mixtape_manager.py
save(mixtape_data)
¶
Creates a new mixtape or updates an existing one based on client identity.
Reuses an existing mixtape when a matching client_id is found, otherwise generates a fresh mixtape entry.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mixtape_data
|
dict
|
Dictionary containing mixtape information, including optional client_id, title, and tracks. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
str |
str
|
The slug of the created or updated mixtape. |
Source code in src/mixtape_manager/mixtape_manager.py
update(slug, updated_data)
¶
Updates an existing mixtape's data while preserving key metadata.
Loads the stored mixtape, applies changes only to allowed fields, maintains backward compatibility, and refreshes the update timestamp before saving.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
slug
|
str
|
The slug of the mixtape to update. |
required |
updated_data
|
dict
|
A dictionary of fields to update on the mixtape. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
str |
str
|
The slug of the updated mixtape. |
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
If a mixtape with the given slug does not exist. |
Source code in src/mixtape_manager/mixtape_manager.py
delete(slug)
¶
Deletes a mixtape and its associated cover image by slug.
Removes the mixtape JSON file and cover image from disk if they exist.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
slug
|
str
|
The slug of the mixtape to delete. |
required |
Source code in src/mixtape_manager/mixtape_manager.py
list_all()
¶
Lists all mixtapes stored on disk.
Returns a list of dictionaries containing mixtape data, sorted by update or save time.
Returns:
| Type | Description |
|---|---|
list[dict]
|
list[dict]: List of all mixtape data dictionaries. |
Source code in src/mixtape_manager/mixtape_manager.py
get(slug)
¶
Retrieves a single mixtape by its slug if it exists and is valid.
Loads the mixtape from disk, validates its tracks against the collection, and normalizes optional fields.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
slug
|
str
|
The slug identifier of the mixtape to retrieve. |
required |
Returns:
| Type | Description |
|---|---|
dict | None
|
dict | None: The validated and normalized mixtape data, or None if the mixtape is missing or invalid. |
