feat: initial implementation of metadata aggregator
- 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
This commit is contained in:
@@ -0,0 +1,832 @@
|
||||
# Harmony - Codebase and Implementation Analysis
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
harmony/
|
||||
├── cli.ts # CLI entry point
|
||||
├── config.ts # Configuration management (36 lines)
|
||||
├── deno.json # Deno configuration and tasks
|
||||
├── deno.lock # Dependency lock file
|
||||
├── .env.example # Environment variable template
|
||||
├── .github/
|
||||
│ └── workflows/
|
||||
│ └── deno.yml # CI/CD pipeline
|
||||
├── components/ # UI components (22 static)
|
||||
│ ├── Header.tsx
|
||||
│ ├── Footer.tsx
|
||||
│ ├── ReleaseInfo.tsx
|
||||
│ ├── TrackList.tsx
|
||||
│ ├── ProviderTable.tsx
|
||||
│ └── ...
|
||||
├── islands/ # Interactive components (5 islands)
|
||||
│ ├── LookupForm.tsx
|
||||
│ ├── ProviderSelector.tsx
|
||||
│ ├── RegionSelector.tsx
|
||||
│ ├── PermalinkGenerator.tsx
|
||||
│ └── SeederForm.tsx
|
||||
├── routes/ # Fresh routes
|
||||
│ ├── index.tsx # Landing page
|
||||
│ ├── release.tsx # Main lookup interface
|
||||
│ ├── about.tsx # Provider documentation
|
||||
│ ├── settings.tsx # User preferences
|
||||
│ └── release/
|
||||
│ └── actions.tsx # ISRC/cover submission
|
||||
├── static/ # Static assets
|
||||
│ ├── styles.css
|
||||
│ └── favicon.ico
|
||||
├── server/ # Server entry points
|
||||
│ ├── main.ts # Production server
|
||||
│ └── dev.ts # Development server
|
||||
├── providers/ # Provider implementations
|
||||
│ ├── base.ts # MetadataProvider abstract class
|
||||
│ ├── api_base.ts # MetadataApiProvider (OAuth2)
|
||||
│ ├── release_lookup.ts # ReleaseLookup interface
|
||||
│ ├── release_api_lookup.ts # ReleaseApiLookup (multi-region)
|
||||
│ ├── registry.ts # ProviderRegistry
|
||||
│ ├── spotify.ts # Spotify provider
|
||||
│ ├── deezer.ts # Deezer provider
|
||||
│ ├── itunes.ts # iTunes provider
|
||||
│ ├── tidal.ts # Tidal provider
|
||||
│ ├── musicbrainz.ts # MusicBrainz provider
|
||||
│ ├── bandcamp.ts # Bandcamp provider
|
||||
│ ├── beatport.ts # Beatport provider
|
||||
│ ├── mora.ts # Mora provider
|
||||
│ └── ototoy.ts # Ototoy provider
|
||||
├── harmonizer/ # Harmonization modules
|
||||
│ ├── types.ts # HarmonyRelease schema (273 lines)
|
||||
│ ├── combined_lookup.ts # CombinedReleaseLookup
|
||||
│ ├── merge.ts # 3-phase merge algorithm
|
||||
│ ├── compatibility.ts # Compatibility checking
|
||||
│ ├── deduplicate.ts # Deduplication
|
||||
│ ├── isrc.ts # ISRC validation
|
||||
│ ├── language_script.ts # Language/script detection
|
||||
│ ├── release_label.ts # Label normalization
|
||||
│ ├── release_types.ts # Release type inference
|
||||
│ └── tracklist_gap.ts # Track gap detection
|
||||
├── musicbrainz/ # MusicBrainz integration
|
||||
│ ├── seeding.ts # MB format conversion
|
||||
│ ├── mbid_mapping.ts # MBID resolution (batch 100)
|
||||
│ ├── api_client.ts # MB API client
|
||||
│ ├── annotation.ts # Annotation builder
|
||||
│ └── edit_link.ts # Edit link generation
|
||||
├── utils/ # Utility modules
|
||||
│ ├── config.ts # Config helpers
|
||||
│ ├── logger.ts # Logging setup
|
||||
│ ├── rate_limiter.ts # Rate limiting
|
||||
│ ├── cache.ts # Cache utilities
|
||||
│ └── errors.ts # Error classes
|
||||
├── testdata/ # Test fixtures (43 cached responses)
|
||||
│ ├── spotify/
|
||||
│ ├── deezer/
|
||||
│ ├── itunes/
|
||||
│ └── ...
|
||||
└── tests/ # Test files (38 total)
|
||||
├── providers/
|
||||
│ ├── spotify_test.ts
|
||||
│ ├── deezer_test.ts
|
||||
│ └── ...
|
||||
├── harmonizer/
|
||||
│ ├── merge_test.ts
|
||||
│ ├── compatibility_test.ts
|
||||
│ └── ...
|
||||
└── musicbrainz/
|
||||
├── seeding_test.ts
|
||||
└── mbid_mapping_test.ts
|
||||
```
|
||||
|
||||
## Configuration Management
|
||||
|
||||
### config.ts (36 lines)
|
||||
|
||||
**Location**: `config.ts`
|
||||
|
||||
**Purpose**: Centralized configuration with environment variable loading
|
||||
|
||||
**Structure**:
|
||||
|
||||
```typescript
|
||||
export const config = {
|
||||
// OAuth2 Credentials
|
||||
spotify: {
|
||||
clientId: getFromEnv('HARMONY_SPOTIFY_CLIENT_ID'),
|
||||
clientSecret: getFromEnv('HARMONY_SPOTIFY_CLIENT_SECRET')
|
||||
},
|
||||
tidal: {
|
||||
clientId: getFromEnv('HARMONY_TIDAL_CLIENT_ID'),
|
||||
clientSecret: getFromEnv('HARMONY_TIDAL_CLIENT_SECRET')
|
||||
},
|
||||
|
||||
// MusicBrainz Configuration
|
||||
musicbrainz: {
|
||||
apiUrl: getUrlFromEnv('HARMONY_MB_API_URL', 'https://musicbrainz.org/ws/2'),
|
||||
targetUrl: getUrlFromEnv('HARMONY_MB_TARGET_URL', 'https://musicbrainz.org')
|
||||
},
|
||||
|
||||
// Data Storage
|
||||
dataDir: getFromEnv('HARMONY_DATA_DIR', './'),
|
||||
|
||||
// Server Configuration
|
||||
port: parseInt(getFromEnv('PORT', '8000')),
|
||||
forwardProto: getFromEnv('FORWARD_PROTO'),
|
||||
deploymentId: getFromEnv('DENO_DEPLOYMENT_ID')
|
||||
};
|
||||
```
|
||||
|
||||
### utils/config.ts
|
||||
|
||||
**Configuration Helpers**:
|
||||
|
||||
```typescript
|
||||
export function getFromEnv(key: string, defaultValue?: string): string {
|
||||
const value = Deno.env.get(key);
|
||||
if (value === undefined) {
|
||||
if (defaultValue !== undefined) {
|
||||
return defaultValue;
|
||||
}
|
||||
throw new Error(`Environment variable ${key} is required but not set`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function getBooleanFromEnv(key: string, defaultValue: boolean): boolean {
|
||||
const value = Deno.env.get(key);
|
||||
if (value === undefined) return defaultValue;
|
||||
return value.toLowerCase() === 'true' || value === '1';
|
||||
}
|
||||
|
||||
export function getUrlFromEnv(key: string, defaultValue?: string): string {
|
||||
const value = getFromEnv(key, defaultValue);
|
||||
try {
|
||||
new URL(value); // Validate URL format
|
||||
return value;
|
||||
} catch {
|
||||
throw new Error(`Environment variable ${key} is not a valid URL: ${value}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### .env.example
|
||||
|
||||
**Template**:
|
||||
|
||||
```bash
|
||||
# OAuth2 Credentials
|
||||
# Get from: https://developer.spotify.com/dashboard
|
||||
HARMONY_SPOTIFY_CLIENT_ID=
|
||||
HARMONY_SPOTIFY_CLIENT_SECRET=
|
||||
|
||||
# Get from: https://developer.tidal.com/
|
||||
HARMONY_TIDAL_CLIENT_ID=
|
||||
HARMONY_TIDAL_CLIENT_SECRET=
|
||||
|
||||
# MusicBrainz Configuration
|
||||
HARMONY_MB_API_URL=https://musicbrainz.org/ws/2
|
||||
HARMONY_MB_TARGET_URL=https://musicbrainz.org
|
||||
|
||||
# Data Storage
|
||||
HARMONY_DATA_DIR=/var/lib/harmony
|
||||
|
||||
# Server Configuration
|
||||
PORT=8000
|
||||
FORWARD_PROTO=https
|
||||
```
|
||||
|
||||
## Logging System
|
||||
|
||||
### utils/logger.ts
|
||||
|
||||
**Logger Setup**:
|
||||
|
||||
```typescript
|
||||
import * as log from 'std/log/mod.ts';
|
||||
|
||||
export async function setupLogging() {
|
||||
await log.setup({
|
||||
handlers: {
|
||||
console: new log.handlers.ConsoleHandler('DEBUG', {
|
||||
formatter: (record) => {
|
||||
const timestamp = new Date(record.datetime).toISOString();
|
||||
const level = record.levelName.padEnd(7);
|
||||
const logger = record.loggerName.padEnd(20);
|
||||
return `${timestamp} ${level} ${logger} ${record.msg}`;
|
||||
},
|
||||
useColors: true
|
||||
})
|
||||
},
|
||||
loggers: {
|
||||
'harmony.lookup': {
|
||||
level: 'INFO',
|
||||
handlers: ['console']
|
||||
},
|
||||
'harmony.mbid': {
|
||||
level: 'DEBUG',
|
||||
handlers: ['console']
|
||||
},
|
||||
'harmony.provider': {
|
||||
level: 'INFO',
|
||||
handlers: ['console']
|
||||
},
|
||||
'harmony.server': {
|
||||
level: 'INFO',
|
||||
handlers: ['console']
|
||||
},
|
||||
'requests': {
|
||||
level: 'INFO',
|
||||
handlers: ['console']
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Logger Usage
|
||||
|
||||
**Get logger**:
|
||||
```typescript
|
||||
import * as log from 'std/log/mod.ts';
|
||||
|
||||
const logger = log.getLogger('harmony.provider');
|
||||
```
|
||||
|
||||
**Log levels**:
|
||||
```typescript
|
||||
logger.debug('Debug message');
|
||||
logger.info('Info message');
|
||||
logger.warning('Warning message');
|
||||
logger.error('Error message');
|
||||
logger.critical('Critical message');
|
||||
```
|
||||
|
||||
**Structured logging**:
|
||||
```typescript
|
||||
logger.info(`Fetching album ${albumId} from ${providerName}`);
|
||||
logger.warning(`Rate limit exceeded, retrying after ${retryAfter}s`);
|
||||
logger.error(`Provider ${providerName} failed: ${error.message}`);
|
||||
```
|
||||
|
||||
### Color Formatting
|
||||
|
||||
**Console output** (with ANSI colors):
|
||||
|
||||
```
|
||||
2024-01-01T12:00:00.000Z INFO harmony.lookup Looking up GTIN 0602537347377
|
||||
2024-01-01T12:00:00.123Z INFO harmony.provider Spotify: Fetching album 3DiDSNVBRYVzccLn2yqhMJ
|
||||
2024-01-01T12:00:00.456Z DEBUG harmony.provider Spotify: Using cached response
|
||||
2024-01-01T12:00:00.789Z WARN harmony.provider iTunes: Rate limit exceeded
|
||||
2024-01-01T12:00:01.234Z INFO harmony.lookup Merge complete: 3 providers
|
||||
```
|
||||
|
||||
**Color scheme**:
|
||||
- DEBUG: Gray
|
||||
- INFO: Blue
|
||||
- WARNING: Yellow
|
||||
- ERROR: Red
|
||||
- CRITICAL: Red + bold
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Error Hierarchy
|
||||
|
||||
**File**: `utils/errors.ts`
|
||||
|
||||
```typescript
|
||||
// Base error
|
||||
export class LookupError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'LookupError';
|
||||
}
|
||||
}
|
||||
|
||||
// Provider errors
|
||||
export class ProviderError extends LookupError {
|
||||
constructor(
|
||||
public provider: string,
|
||||
message: string
|
||||
) {
|
||||
super(`${provider}: ${message}`);
|
||||
this.name = 'ProviderError';
|
||||
}
|
||||
}
|
||||
|
||||
// HTTP/API errors
|
||||
export class ResponseError extends ProviderError {
|
||||
constructor(
|
||||
provider: string,
|
||||
public status: number,
|
||||
message: string
|
||||
) {
|
||||
super(provider, `HTTP ${status}: ${message}`);
|
||||
this.name = 'ResponseError';
|
||||
}
|
||||
}
|
||||
|
||||
// Data compatibility errors
|
||||
export class CompatibilityError extends LookupError {
|
||||
constructor(
|
||||
public property: string,
|
||||
public values: any[]
|
||||
) {
|
||||
super(`Incompatible values for ${property}: ${JSON.stringify(values)}`);
|
||||
this.name = 'CompatibilityError';
|
||||
}
|
||||
}
|
||||
|
||||
// Cache errors
|
||||
export class CacheMissError extends LookupError {
|
||||
constructor(
|
||||
public key: string
|
||||
) {
|
||||
super(`Cache miss for key: ${key}`);
|
||||
this.name = 'CacheMissError';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling Patterns
|
||||
|
||||
#### Graceful Degradation
|
||||
|
||||
```typescript
|
||||
// Use Promise.allSettled for parallel provider queries
|
||||
const lookupPromises = providers.map(provider =>
|
||||
provider.lookup(input).catch(error => {
|
||||
logger.warning(`Provider ${provider.name} failed: ${error.message}`);
|
||||
return null; // Return null on error
|
||||
})
|
||||
);
|
||||
|
||||
const results = await Promise.allSettled(lookupPromises);
|
||||
|
||||
// Filter successful results
|
||||
const releases = results
|
||||
.filter(r => r.status === 'fulfilled' && r.value !== null)
|
||||
.map(r => r.value);
|
||||
|
||||
if (releases.length === 0) {
|
||||
throw new LookupError('All providers failed');
|
||||
}
|
||||
```
|
||||
|
||||
#### Rate Limit Handling
|
||||
|
||||
```typescript
|
||||
async function fetchWithRetry(url: string, maxRetries = 3): Promise<Response> {
|
||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||
const response = await fetch(url);
|
||||
|
||||
if (response.status === 429) {
|
||||
// Rate limit exceeded
|
||||
const retryAfter = parseInt(response.headers.get('Retry-After') || '60');
|
||||
|
||||
if (retryAfter > 300) {
|
||||
// Don't wait more than 5 minutes
|
||||
throw new ResponseError('provider', 429, `Rate limit exceeded, retry after ${retryAfter}s (too long)`);
|
||||
}
|
||||
|
||||
logger.warning(`Rate limit exceeded, retrying after ${retryAfter}s`);
|
||||
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new ResponseError('provider', response.status, response.statusText);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
throw new ResponseError('provider', 429, 'Rate limit exceeded after max retries');
|
||||
}
|
||||
```
|
||||
|
||||
#### Error Propagation
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const release = await provider.lookup(input);
|
||||
return provider.harmonize(release);
|
||||
} catch (error) {
|
||||
if (error instanceof ProviderError) {
|
||||
// Log and re-throw provider errors
|
||||
logger.error(error.message);
|
||||
throw error;
|
||||
} else {
|
||||
// Wrap unexpected errors
|
||||
throw new ProviderError(provider.name, error.message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Infrastructure
|
||||
|
||||
### Test Framework
|
||||
|
||||
**Deno built-in testing** + `@std/testing`:
|
||||
|
||||
```typescript
|
||||
import { assertEquals, assertExists } from '@std/testing/asserts';
|
||||
import { describe, it } from '@std/testing/bdd';
|
||||
```
|
||||
|
||||
### Test Structure
|
||||
|
||||
**38 test files** organized by module:
|
||||
|
||||
```
|
||||
tests/
|
||||
├── providers/
|
||||
│ ├── spotify_test.ts
|
||||
│ ├── deezer_test.ts
|
||||
│ ├── itunes_test.ts
|
||||
│ ├── tidal_test.ts
|
||||
│ ├── musicbrainz_test.ts
|
||||
│ ├── bandcamp_test.ts
|
||||
│ ├── beatport_test.ts
|
||||
│ ├── mora_test.ts
|
||||
│ └── ototoy_test.ts
|
||||
├── harmonizer/
|
||||
│ ├── merge_test.ts
|
||||
│ ├── compatibility_test.ts
|
||||
│ ├── deduplicate_test.ts
|
||||
│ ├── isrc_test.ts
|
||||
│ ├── language_script_test.ts
|
||||
│ ├── release_label_test.ts
|
||||
│ ├── release_types_test.ts
|
||||
│ └── tracklist_gap_test.ts
|
||||
└── musicbrainz/
|
||||
├── seeding_test.ts
|
||||
├── mbid_mapping_test.ts
|
||||
├── annotation_test.ts
|
||||
└── edit_link_test.ts
|
||||
```
|
||||
|
||||
### Declarative Provider Tests
|
||||
|
||||
**File**: `tests/utils/describe_provider.ts`
|
||||
|
||||
**Purpose**: Consistent provider testing with minimal boilerplate
|
||||
|
||||
**Usage**:
|
||||
|
||||
```typescript
|
||||
import { describeProvider } from '../utils/describe_provider.ts';
|
||||
|
||||
describeProvider({
|
||||
name: 'Spotify',
|
||||
provider: new SpotifyProvider(),
|
||||
tests: {
|
||||
urlMatching: [
|
||||
{ url: 'https://open.spotify.com/album/3DiDSNVBRYVzccLn2yqhMJ', shouldMatch: true },
|
||||
{ url: 'https://www.deezer.com/album/123456', shouldMatch: false }
|
||||
],
|
||||
gtinLookup: {
|
||||
gtin: '0602537347377',
|
||||
expectedTitle: 'Album Title',
|
||||
expectedArtists: ['Artist Name']
|
||||
},
|
||||
urlLookup: {
|
||||
url: 'https://open.spotify.com/album/3DiDSNVBRYVzccLn2yqhMJ',
|
||||
expectedTitle: 'Album Title'
|
||||
},
|
||||
harmonization: {
|
||||
input: spotifyAlbumFixture,
|
||||
expectedFields: ['title', 'artists', 'gtin', 'media', 'images']
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Generated tests**:
|
||||
- URL pattern matching
|
||||
- GTIN lookup
|
||||
- URL lookup
|
||||
- Harmonization
|
||||
- Feature quality validation
|
||||
|
||||
### Snapshot Testing
|
||||
|
||||
**Purpose**: Verify output stability across changes
|
||||
|
||||
**Example**:
|
||||
|
||||
```typescript
|
||||
import { assertSnapshot } from '@std/testing/snapshot';
|
||||
|
||||
Deno.test('Spotify harmonization snapshot', async (t) => {
|
||||
const provider = new SpotifyProvider();
|
||||
const spotifyAlbum = await loadFixture('spotify/album.json');
|
||||
const harmonyRelease = provider.harmonize(spotifyAlbum);
|
||||
|
||||
await assertSnapshot(t, harmonyRelease);
|
||||
});
|
||||
```
|
||||
|
||||
**Snapshot file** (auto-generated):
|
||||
|
||||
```typescript
|
||||
// __snapshots__/spotify_test.ts.snap
|
||||
export const snapshot = {
|
||||
"Spotify harmonization snapshot": {
|
||||
title: "Album Title",
|
||||
artists: [{ name: "Artist Name" }],
|
||||
gtin: "0602537347377",
|
||||
// ... full object
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Offline Testing
|
||||
|
||||
**Test data**: 43 cached responses in `testdata/`
|
||||
|
||||
**Structure**:
|
||||
|
||||
```
|
||||
testdata/
|
||||
├── spotify/
|
||||
│ ├── album_3DiDSNVBRYVzccLn2yqhMJ.json
|
||||
│ ├── album_search_upc_0602537347377.json
|
||||
│ └── ...
|
||||
├── deezer/
|
||||
│ ├── album_123456.json
|
||||
│ └── ...
|
||||
├── itunes/
|
||||
│ ├── lookup_us_123456.json
|
||||
│ └── ...
|
||||
└── ...
|
||||
```
|
||||
|
||||
**Loading fixtures**:
|
||||
|
||||
```typescript
|
||||
async function loadFixture(path: string): Promise<any> {
|
||||
const content = await Deno.readTextFile(`testdata/${path}`);
|
||||
return JSON.parse(content);
|
||||
}
|
||||
```
|
||||
|
||||
**Offline mode** (default):
|
||||
|
||||
```bash
|
||||
deno test -A
|
||||
```
|
||||
|
||||
Uses cached responses from `testdata/`, no network requests.
|
||||
|
||||
**Download mode** (fetch fresh data):
|
||||
|
||||
```bash
|
||||
deno test -A --download
|
||||
```
|
||||
|
||||
Fetches fresh responses from providers and updates `testdata/`.
|
||||
|
||||
### Test Coverage
|
||||
|
||||
**Run tests with coverage**:
|
||||
|
||||
```bash
|
||||
deno test -A --coverage=coverage
|
||||
deno coverage coverage
|
||||
```
|
||||
|
||||
**Coverage report**:
|
||||
|
||||
```
|
||||
file:///opt/harmony/providers/spotify.ts 95.2%
|
||||
file:///opt/harmony/harmonizer/merge.ts 88.7%
|
||||
file:///opt/harmony/musicbrainz/seeding.ts 92.3%
|
||||
...
|
||||
```
|
||||
|
||||
## Code Style
|
||||
|
||||
### Formatting Rules
|
||||
|
||||
**File**: `deno.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"fmt": {
|
||||
"useTabs": true,
|
||||
"lineWidth": 120,
|
||||
"indentWidth": 4,
|
||||
"singleQuote": true,
|
||||
"proseWrap": "preserve"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Rules**:
|
||||
- **Tabs**: Use tabs for indentation (not spaces)
|
||||
- **Line width**: 120 characters maximum
|
||||
- **Quotes**: Single quotes for strings
|
||||
- **Semicolons**: Required
|
||||
- **Trailing commas**: Allowed
|
||||
|
||||
**Format code**:
|
||||
|
||||
```bash
|
||||
deno fmt
|
||||
```
|
||||
|
||||
**Check formatting**:
|
||||
|
||||
```bash
|
||||
deno fmt --check
|
||||
```
|
||||
|
||||
### Linting Rules
|
||||
|
||||
**File**: `deno.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"lint": {
|
||||
"rules": {
|
||||
"tags": ["recommended"],
|
||||
"exclude": ["no-explicit-any"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Lint code**:
|
||||
|
||||
```bash
|
||||
deno lint
|
||||
```
|
||||
|
||||
**Common lint errors**:
|
||||
- Unused variables
|
||||
- Missing return types
|
||||
- Unreachable code
|
||||
- Prefer `const` over `let`
|
||||
|
||||
### Type Checking
|
||||
|
||||
**Strict mode** enabled:
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Type check**:
|
||||
|
||||
```bash
|
||||
deno check **/*.ts
|
||||
```
|
||||
|
||||
## Dependency Management
|
||||
|
||||
### deno.json
|
||||
|
||||
**Import map**:
|
||||
|
||||
```json
|
||||
{
|
||||
"imports": {
|
||||
"$fresh/": "https://deno.land/x/fresh@1.6.8/",
|
||||
"preact": "https://esm.sh/preact@10.19.6",
|
||||
"preact/": "https://esm.sh/preact@10.19.6/",
|
||||
"@preact/signals": "https://esm.sh/@preact/signals@1.2.2",
|
||||
"@kellnerd/musicbrainz": "https://deno.land/x/musicbrainz@v0.5.0/mod.ts",
|
||||
"snap-storage": "https://deno.land/x/snap_storage@v0.2.0/mod.ts",
|
||||
"@std/": "https://deno.land/std@0.208.0/"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key dependencies**:
|
||||
|
||||
| Dependency | Version | Purpose |
|
||||
|------------|---------|---------|
|
||||
| Fresh | 1.6.8 | Web framework |
|
||||
| Preact | 10.19.6 | UI library |
|
||||
| @kellnerd/musicbrainz | 0.5.0 | MusicBrainz API client |
|
||||
| snap-storage | 0.2.0 | HTTP response caching |
|
||||
| @std/* | 0.208.0 | Deno standard library |
|
||||
|
||||
### Lock File
|
||||
|
||||
**deno.lock**: Dependency integrity verification
|
||||
|
||||
**Update lock file**:
|
||||
|
||||
```bash
|
||||
deno cache --reload --lock=deno.lock --lock-write deps.ts
|
||||
```
|
||||
|
||||
## Tasks
|
||||
|
||||
### deno.json Tasks
|
||||
|
||||
```json
|
||||
{
|
||||
"tasks": {
|
||||
"check": "deno fmt --check && deno lint && deno check **/*.ts",
|
||||
"ok": "deno fmt && deno lint && deno check **/*.ts && deno test -A",
|
||||
"cli": "deno run -A cli.ts",
|
||||
"dev": "deno run -A --watch=static/,routes/ server/dev.ts",
|
||||
"build": "deno run -A server/dev.ts build",
|
||||
"server": "DENO_DEPLOYMENT_ID=$(git describe --tags --always) deno run -A server/main.ts"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Task descriptions**:
|
||||
|
||||
| Task | Purpose | Usage |
|
||||
|------|---------|-------|
|
||||
| `check` | Verify code quality (format, lint, type check) | `deno task check` |
|
||||
| `ok` | Format, lint, check, and test | `deno task ok` |
|
||||
| `cli` | Run CLI | `deno task cli --gtin 0602537347377` |
|
||||
| `dev` | Start development server | `deno task dev` |
|
||||
| `build` | Build static assets | `deno task build` |
|
||||
| `server` | Start production server | `deno task server` |
|
||||
|
||||
## No External Tooling
|
||||
|
||||
Harmony **does not use**:
|
||||
- **Sentry**: No error tracking
|
||||
- **Prometheus**: No metrics collection
|
||||
- **Datadog/New Relic**: No APM
|
||||
- **Webpack/Vite**: Fresh handles bundling
|
||||
- **ESLint**: Deno lint built-in
|
||||
- **Prettier**: Deno fmt built-in
|
||||
- **Jest/Mocha**: Deno test built-in
|
||||
|
||||
**Rationale**: Deno provides all necessary tooling out-of-the-box.
|
||||
|
||||
## Performance Optimizations
|
||||
|
||||
### Parallel Provider Queries
|
||||
|
||||
```typescript
|
||||
const lookups = providers.map(p => p.lookup(input));
|
||||
const results = await Promise.allSettled(lookups);
|
||||
```
|
||||
|
||||
**Benefit**: Reduce total response time from sum of provider latencies to max of provider latencies.
|
||||
|
||||
### HTTP Response Caching
|
||||
|
||||
```typescript
|
||||
const cached = await cache.get(url);
|
||||
if (cached) return cached;
|
||||
|
||||
const response = await fetch(url);
|
||||
await cache.set(url, response);
|
||||
return response;
|
||||
```
|
||||
|
||||
**Benefit**: Avoid redundant API calls, comply with rate limits.
|
||||
|
||||
### OAuth2 Token Caching
|
||||
|
||||
```typescript
|
||||
const cached = localStorage.getItem('spotify_token');
|
||||
if (cached && !isExpired(cached)) {
|
||||
return cached.access_token;
|
||||
}
|
||||
```
|
||||
|
||||
**Benefit**: Reduce token requests, faster authentication.
|
||||
|
||||
### Server-Side Rendering
|
||||
|
||||
Fresh SSR generates HTML on server, reducing client-side JavaScript.
|
||||
|
||||
**Benefit**: Faster initial page load, better SEO.
|
||||
|
||||
### Islands Architecture
|
||||
|
||||
Only interactive components load JavaScript on client.
|
||||
|
||||
**Benefit**: Minimal JavaScript bundle size, faster page interactivity.
|
||||
|
||||
## Summary
|
||||
|
||||
Harmony's codebase demonstrates:
|
||||
|
||||
1. **Clean architecture**: Clear separation of concerns (providers, harmonizer, MusicBrainz)
|
||||
2. **Type safety**: Full TypeScript coverage with strict mode
|
||||
3. **Comprehensive testing**: 38 test files with declarative provider specs
|
||||
4. **Offline testing**: 43 cached responses for reproducible tests
|
||||
5. **Logging system**: 5 specialized loggers with color formatting
|
||||
6. **Error hierarchy**: Structured error handling with graceful degradation
|
||||
7. **Configuration management**: Environment variables with validation
|
||||
8. **Code quality**: Deno fmt, lint, and type check enforced
|
||||
9. **No external tooling**: Deno provides all necessary tools
|
||||
10. **Performance optimizations**: Parallel queries, caching, SSR, islands
|
||||
|
||||
This codebase is production-ready and serves as an excellent reference for building type-safe, well-tested metadata aggregation systems.
|
||||
Reference in New Issue
Block a user