- gRPC service with MusicBrainz provider - PostgreSQL schema with migrations - Service layer with database-first caching - Repository pattern for data access - YAML configuration support - Research documentation for 17 music metadata projects
19 KiB
Harmony - API and Interface Analysis
API Architecture
Harmony is a web UI-first application built on the Fresh framework. It does not provide a traditional REST API or JSON endpoints. All interactions occur through server-side rendered HTML pages with embedded data.
Framework: Fresh 1.6.8
Fresh is a Deno-native web framework with:
- Server-side rendering (SSR): All pages rendered on server
- Islands architecture: Selective client-side interactivity
- File-based routing: Routes defined by file structure
- Zero config: No build step required for development
Route Structure
Main Application Routes
| Route | File | Method | Purpose |
|---|---|---|---|
/ |
routes/index.tsx |
GET | Landing page with documentation |
/release |
routes/release.tsx |
GET | Main lookup and comparison interface |
/release/actions |
routes/release/actions.tsx |
GET | ISRC/cover submission for existing MB releases |
/about |
routes/about.tsx |
GET | Provider documentation and feature matrix |
/settings |
routes/settings.tsx |
GET/POST | User preferences (stored in cookies) |
Static Assets
| Route | Purpose |
|---|---|
/static/* |
CSS, JavaScript, images |
/favicon.ico |
Site favicon |
Primary Route: /release
The main interface for metadata lookup and harmonization.
Query Parameters
Core Lookup Parameters
| Parameter | Type | Required | Description | Example |
|---|---|---|---|---|
gtin |
string | No* | Global Trade Item Number (barcode) | 0602537347377 |
url |
string[] | No* | Provider URL(s), supports multiple | https://open.spotify.com/album/xyz |
*At least one of gtin or url must be provided.
Provider-Specific Parameters
| Parameter | Type | Description | Example |
|---|---|---|---|
[provider_name] |
string | Provider-specific ID or GTIN lookup | spotify=3DiDSNVBRYVzccLn2yqhMJ |
[provider_name]! |
empty | Template mode for provider | musicbrainz! |
Supported Provider Names:
spotifydeezeritunestidalbandcampbeatportmusicbrainzmoraototoy
Filtering Parameters
| Parameter | Type | Default | Description | Values |
|---|---|---|---|---|
region |
string[] | GB,US,DE,JP |
Market regions for lookup | ISO 3166-1 alpha-2 codes |
category |
string | default |
Provider category filter | all, default, preferred |
Permalink Parameters
| Parameter | Type | Description | Example |
|---|---|---|---|
ts |
number | Unix timestamp for cache replay | 1704067200 |
Request Examples
GTIN Lookup (Default Regions)
GET /release?gtin=0602537347377
Queries all GTIN-supporting providers in default regions (GB, US, DE, JP).
GTIN Lookup (Specific Regions)
GET /release?gtin=0602537347377®ion=JP,US
Queries only Japan and US regions.
URL Lookup (Single Provider)
GET /release?url=https://open.spotify.com/album/3DiDSNVBRYVzccLn2yqhMJ
Queries only Spotify using the provided URL.
URL Lookup (Multiple Providers)
GET /release?url=https://open.spotify.com/album/3DiDSNVBRYVzccLn2yqhMJ&url=https://www.deezer.com/album/123456
Queries both Spotify and Deezer.
Provider-Specific ID Lookup
GET /release?spotify=3DiDSNVBRYVzccLn2yqhMJ&deezer=123456
Queries Spotify and Deezer using their native IDs.
Template Mode (MusicBrainz)
GET /release?gtin=0602537347377&musicbrainz!
Uses MusicBrainz as template provider (reference data for merge).
Category Filtering
GET /release?gtin=0602537347377&category=preferred
Queries only preferred providers (Spotify, Tidal, MusicBrainz).
Permalink (Cache Replay)
GET /release?gtin=0602537347377&ts=1704067200
Replays cached lookup from timestamp 1704067200.
Response Format
The /release route returns an HTML page with embedded data, not JSON.
Response Sections
-
Release Header
- Title
- Artist credit
- Release date
- GTIN (if available)
-
Provider Comparison Table
- Side-by-side comparison of all providers
- Color-coded compatibility indicators
- Feature quality ratings
-
Harmonized Metadata Display
- Merged release information
- Track listing with ISRCs
- Label and catalog number information
- Cover art images
- Copyright and availability info
-
MusicBrainz Seeder Form
- Pre-filled form for MB import
- Edit note with provider URLs
- Annotation with extra data
- Copy-to-clipboard functionality
-
Warnings and Messages
- Compatibility conflicts
- Provider errors
- Missing data indicators
- Duplicate detection warnings
-
Permalink
- Timestamp-based URL for reproducibility
- Share button
Example Response Structure (HTML)
<!DOCTYPE html>
<html>
<head>
<title>Album Title - Artist Name | Harmony</title>
<!-- Meta tags, CSS -->
</head>
<body>
<header>
<!-- Navigation -->
</header>
<main>
<!-- Release Header -->
<section class="release-header">
<h1>Album Title</h1>
<p class="artist-credit">Artist Name</p>
<p class="release-date">2014-11-24</p>
<p class="gtin">GTIN: 0602537347377</p>
</section>
<!-- Provider Comparison -->
<section class="provider-comparison">
<table>
<thead>
<tr>
<th>Property</th>
<th>Spotify</th>
<th>Deezer</th>
<th>iTunes</th>
<th>Merged</th>
</tr>
</thead>
<tbody>
<!-- Comparison rows -->
</tbody>
</table>
</section>
<!-- Harmonized Metadata -->
<section class="harmonized-release">
<!-- Track listing, labels, images, etc. -->
</section>
<!-- MusicBrainz Seeder -->
<section class="musicbrainz-seeder">
<form>
<!-- Pre-filled MB import form -->
</form>
</section>
<!-- Warnings -->
<section class="warnings">
<!-- Compatibility warnings, errors -->
</section>
<!-- Permalink -->
<section class="permalink">
<input type="text" readonly value="https://harmony.example.com/release?gtin=0602537347377&ts=1704067200">
<button>Copy</button>
</section>
</main>
<footer>
<!-- Footer content -->
</footer>
<!-- Island hydration scripts -->
<script type="module" src="/islands/LookupForm.js"></script>
<script type="module" src="/islands/SeederForm.js"></script>
</body>
</html>
Error Handling
Errors are displayed inline in the HTML response:
Provider Errors
<div class="provider-error">
<strong>Spotify:</strong> Rate limit exceeded. Retry after 60 seconds.
</div>
Lookup Errors
<div class="lookup-error">
<strong>Error:</strong> No providers found for GTIN 0602537347377 in region CN.
</div>
Compatibility Warnings
<div class="compatibility-warning">
<strong>Warning:</strong> Release date conflict:
<ul>
<li>Spotify: 2014-11-24</li>
<li>iTunes: 2014-11-25</li>
</ul>
Using Spotify value (higher preference).
</div>
Secondary Routes
/ - Landing Page
Purpose: Introduction and quick start guide
Content:
- Project description
- Supported providers
- Usage examples
- Link to
/aboutfor detailed documentation
No query parameters
/release/actions - ISRC/Cover Submission
Purpose: Submit ISRCs or cover art for existing MusicBrainz releases
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
mbid |
string | Yes | MusicBrainz release ID |
action |
string | Yes | isrc or cover |
Example:
GET /release/actions?mbid=12345678-1234-1234-1234-123456789012&action=isrc
Response: Form for submitting ISRCs or cover art to MusicBrainz
/about - Provider Documentation
Purpose: Detailed provider information and feature comparison
Content:
- Provider descriptions
- Feature quality matrix
- Rate limits and authentication requirements
- Supported regions
- Known limitations
No query parameters
Feature Quality Matrix Example:
| Provider | GTIN | Title | Artists | Date | Labels | Tracks | ISRC | Images | Copyright |
|---|---|---|---|---|---|---|---|---|---|
| Spotify | ✓ | ✓ | ✓ | ✓ | ~ | ✓ | ✓ | 2000px | ~ |
| Deezer | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 1400px | ✓ |
| iTunes | ✓ | ✓ | ✓ | ✓ | ~ | ✓ | ~ | Varies | ~ |
| Tidal | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 1280px | ✓ |
| Bandcamp | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | 3000px | ✓ |
Legend:
- ✓ = GOOD quality
- ~ = PRESENT quality
- ✗ = MISSING
/settings - User Preferences
Purpose: Configure user preferences
Method: GET (display form), POST (save preferences)
Preferences:
| Setting | Type | Default | Description |
|---|---|---|---|
defaultRegions |
string[] | ['GB','US','DE','JP'] |
Default regions for lookup |
defaultCategory |
string | default |
Default provider category |
providerPreferences |
string[] | Custom order | Provider preference order for merge |
showCompatibilityWarnings |
boolean | true |
Display compatibility warnings |
cacheStrategy |
string | 24h |
Cache duration |
Storage: Preferences stored in cookies (no server-side storage)
Example Cookie:
harmony_prefs={"defaultRegions":["JP","US"],"defaultCategory":"preferred","providerPreferences":["spotify","tidal","deezer"]}; Max-Age=31536000; Path=/
Islands (Client-Side Interactivity)
Fresh's islands architecture enables selective client-side interactivity.
Island Components
1. LookupForm Island
File: islands/LookupForm.tsx
Purpose: Dynamic lookup form with validation
Features:
- Real-time GTIN validation
- URL parsing and provider detection
- Region multi-select
- Category radio buttons
- Form submission with loading state
Client-Side Logic:
// Conceptual
function LookupForm() {
const [gtin, setGtin] = useState('');
const [urls, setUrls] = useState<string[]>([]);
const [regions, setRegions] = useState(['GB', 'US', 'DE', 'JP']);
const validateGtin = (value: string) => {
// GTIN-13 validation
return /^\d{13}$/.test(value);
};
const handleSubmit = async (e: Event) => {
e.preventDefault();
// Navigate to /release with query params
const params = new URLSearchParams();
if (gtin) params.set('gtin', gtin);
urls.forEach(url => params.append('url', url));
params.set('region', regions.join(','));
window.location.href = `/release?${params}`;
};
return (
<form onSubmit={handleSubmit}>
{/* Form fields */}
</form>
);
}
2. ProviderSelector Island
File: islands/ProviderSelector.tsx
Purpose: Provider category filtering
Features:
- Category selection (all/default/preferred)
- Individual provider checkboxes
- Real-time URL update
3. RegionSelector Island
File: islands/RegionSelector.tsx
Purpose: Multi-region selection
Features:
- Checkbox list of supported regions
- Select all / deselect all
- Common region presets (US+GB, Japan, Europe)
4. PermalinkGenerator Island
File: islands/PermalinkGenerator.tsx
Purpose: Generate timestamp-based permalink
Features:
- Current timestamp capture
- URL generation with
tsparameter - Copy to clipboard
- Share button
Client-Side Logic:
function PermalinkGenerator({ currentUrl }: { currentUrl: string }) {
const [permalink, setPermalink] = useState('');
const generatePermalink = () => {
const url = new URL(currentUrl);
url.searchParams.set('ts', Math.floor(Date.now() / 1000).toString());
setPermalink(url.toString());
};
const copyToClipboard = () => {
navigator.clipboard.writeText(permalink);
};
return (
<div>
<button onClick={generatePermalink}>Generate Permalink</button>
{permalink && (
<>
<input type="text" readonly value={permalink} />
<button onClick={copyToClipboard}>Copy</button>
</>
)}
</div>
);
}
5. SeederForm Island
File: islands/SeederForm.tsx
Purpose: MusicBrainz import form with copy functionality
Features:
- Pre-filled form fields
- Copy individual fields to clipboard
- Copy entire form as JSON
- Open MusicBrainz seeder in new tab
Client-Side Logic:
function SeederForm({ release }: { release: MergedHarmonyRelease }) {
const copyField = (field: string, value: string) => {
navigator.clipboard.writeText(value);
};
const openSeeder = () => {
const mbUrl = `https://musicbrainz.org/release/add`;
const form = document.createElement('form');
form.method = 'POST';
form.action = mbUrl;
form.target = '_blank';
// Add form fields
Object.entries(release).forEach(([key, value]) => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = key;
input.value = JSON.stringify(value);
form.appendChild(input);
});
document.body.appendChild(form);
form.submit();
document.body.removeChild(form);
};
return (
<div>
{/* Form fields with copy buttons */}
<button onClick={openSeeder}>Open in MusicBrainz</button>
</div>
);
}
No REST API
Harmony does not provide a REST API or JSON endpoints. Key implications:
No JSON Responses
All routes return HTML. There is no Accept: application/json support.
Request:
GET /release?gtin=0602537347377
Accept: application/json
Response:
HTTP/1.1 200 OK
Content-Type: text/html
<!DOCTYPE html>
<!-- HTML response, not JSON -->
No Programmatic Access
Clients cannot fetch data programmatically without HTML parsing.
Workaround (not officially supported):
- Fetch HTML response
- Parse HTML with DOM parser
- Extract data from structured elements
Example (conceptual):
const response = await fetch('/release?gtin=0602537347377');
const html = await response.text();
const doc = new DOMParser().parseFromString(html, 'text/html');
const title = doc.querySelector('.release-header h1')?.textContent;
No API Authentication
No API keys, no OAuth2 for API access (OAuth2 only used for provider authentication).
No Rate Limiting on Server
Server does not enforce rate limits (providers have their own limits).
Request/Response Flow
Typical Request Flow
1. User submits lookup form
↓
2. Browser sends GET /release?gtin=...®ion=...
↓
3. Fresh router matches route to routes/release.tsx
↓
4. Route handler executes:
a. Parse query parameters
b. Call CombinedReleaseLookup
c. Parallel provider queries
d. Harmonize responses
e. Merge releases
f. Generate MusicBrainz seeding data
↓
5. Server-side rendering:
a. Render components with data
b. Generate HTML
c. Inject island hydration scripts
↓
6. HTTP response sent to browser
↓
7. Browser renders HTML
↓
8. Island hydration:
a. Load island JavaScript modules
b. Attach event listeners
c. Enable client-side interactivity
Caching Strategy
Server-Side Caching
- snap_storage: Caches HTTP responses from providers
- Cache key: URL + query parameters
- Cache duration: 24 hours (configurable)
- Cache storage: SQLite database (
snaps.db) + file directory (snaps/)
Client-Side Caching
- Browser cache: Standard HTTP caching headers
- localStorage: OAuth2 tokens, MBID mappings (dev mode)
- sessionStorage: MBID mappings (production mode)
- Cookies: User preferences
Permalink Caching
The ts parameter enables cache replay:
- User performs lookup at timestamp T
- Responses cached with timestamp T
- Permalink generated:
/release?gtin=...&ts=T - Future requests with
ts=Treplay cached responses - Ensures reproducible results even if provider data changes
Cache Lookup Logic:
async function getCachedResponse(url: string, timestamp?: number): Promise<Response | null> {
if (timestamp) {
// Permalink mode: lookup by timestamp
return await cache.getByTimestamp(url, timestamp);
} else {
// Normal mode: lookup by recency
return await cache.getRecent(url, MAX_AGE);
}
}
Error Responses
HTTP Status Codes
| Status | Scenario |
|---|---|
| 200 | Success (even with partial provider failures) |
| 400 | Invalid query parameters |
| 404 | Route not found |
| 500 | Server error (unhandled exception) |
Error Display
Errors displayed inline in HTML, not as HTTP error codes.
Example: All providers fail, but response is still 200 OK with error messages in HTML.
Performance Considerations
Parallel Provider Queries
All provider lookups execute in parallel via Promise.allSettled:
const lookups = providers.map(p => p.lookup(input));
const results = await Promise.allSettled(lookups);
Benefits:
- Faster total response time
- Graceful degradation (partial results)
Typical Response Times:
- Single provider: 200-500ms
- Multiple providers (parallel): 500-1500ms
- Cached response: <50ms
Server-Side Rendering Overhead
Fresh SSR adds minimal overhead:
- Component rendering: 10-50ms
- HTML generation: 5-20ms
- Total SSR overhead: <100ms
Island Hydration
Islands load asynchronously after initial page render:
- Initial HTML render: Immediate
- Island JavaScript load: 100-300ms
- Island hydration: 50-100ms
User experience: Page is interactive immediately, islands enhance progressively.
Integration Patterns
Embedding in Other Applications
Since Harmony has no REST API, integration requires:
- iFrame embedding: Embed
/releaseroute in iFrame - Redirect: Redirect users to Harmony for lookup
- HTML parsing: Fetch and parse HTML responses (fragile)
iFrame Example:
<iframe src="https://harmony.example.com/release?gtin=0602537347377" width="100%" height="600"></iframe>
MusicBrainz Integration
Harmony integrates with MusicBrainz via:
- Seeder form: Pre-filled form for MB import
- Edit notes: Include provider URLs and permalink
- Annotations: Extra metadata not in main form
- MBID resolution: Batch URL lookup to detect duplicates
Workflow:
1. User performs lookup in Harmony
↓
2. Harmony displays harmonized release
↓
3. User clicks "Open in MusicBrainz"
↓
4. Seeder form opens in new tab
↓
5. User reviews and submits to MusicBrainz
Summary
Harmony's API design prioritizes:
- Web UI first: No REST API, HTML-only responses
- Server-side rendering: Fast initial load, SEO-friendly
- Islands architecture: Selective client-side interactivity
- Permalink system: Reproducible results via timestamp caching
- Graceful degradation: Partial results on provider failures
- MusicBrainz integration: Seamless seeding workflow
This design is optimized for human users (MusicBrainz editors) rather than programmatic API consumers. For a metadata aggregation system targeting API consumers, a REST API layer would need to be added.