# Greyscale

Greyscale is a JavaScript API/runtime project whose primary source lives in [`src`](src), while [`greyscale-app`](greyscale-app) is retained as a legacy demo.

## Project Scope

- Primary runtime source: [`src`](src)
- Primary API module: [`src/api.js`](src/api.js)
- Legacy demo (kept for reference): [`greyscale-app`](greyscale-app)

## Current Structure

```text
.
├── src/
│   ├── api.js
│   ├── app.js
│   ├── cache.js
│   ├── downloads.js
│   ├── lyrics.js
│   ├── player.js
│   └── utils.js
├── greyscale-app/         # legacy demo
├── monochrome/            # upstream reference submodule
└── server.js
```

## Run

```bash
node server.js
# open http://127.0.0.1:12345
```

---

## Testing

Run unit tests (offline/mocked only):

```bash
node tests/test.js
```

Run unit + live integration tests (real service calls):

```bash
node tests/test.js --live
```

Notes:
- The `--live` mode calls real uptime workers, real API instances, and `lrclib.net`.
- Some live checks may be skipped when upstream data is unavailable (for example missing IDs from search results).
- The summary at the end reports pass/fail/skip totals for unit and live suites separately.

---

## API Documentation

### `GreyscaleAPI` ([`src/api.js`](src/api.js))

Create an API instance:

```js
import { GreyscaleAPI } from './src/api.js';

const api = new GreyscaleAPI();
await api.initInstances();
```

#### Instance + transport

- `initInstances()`
  - Loads healthy API and streaming instance pools from uptime workers.
  - Falls back to static instance lists when workers fail.

- `fetchFromPool(poolType, path, options?)`
  - Internal transport with failover across instance pool.
  - `poolType`: `'api' | 'streaming'`

#### Search endpoints

- `searchTracks(query)`
  - Endpoint: `/search/?s={query}`
  - Returns paged object:
    - `items: Track[]`
    - `limit`, `offset`, `totalNumberOfItems`

- `searchAlbums(query)`
  - Endpoint: `/search/?al={query}`
  - Returns paged object with deduplicated album results.

- `searchArtists(query)`
  - Endpoint: `/search/?a={query}`
  - Returns paged object of artists.

#### Detail endpoints

- `getAlbum(id)`
  - Endpoint: `/album/?id={id}`
  - Returns:
    - `{ album, tracks }`
  - Handles album track pagination (`offset/limit`) for large albums.

- `getArtist(artistId, options?)`
  - Endpoints:
    - `/artist/?id={id}`
    - `/artist/?f={id}&skip_tracks=true`
  - Returns normalized artist profile including:
    - `albums`
    - `eps`
    - `tracks` (top tracks)

- `getTrackMetadata(id)`
  - Endpoint: `/info/?id={id}`
  - Returns a normalized track object.

- `getTrackRecommendations(id)`
  - Endpoint: `/recommendations/?id={id}`
  - Returns: `Track[]`

#### Streaming + media

- `getTrackStream(id, quality = 'HI_RES_LOSSLESS')`
  - Endpoint: `/track/?id={id}&quality={quality}`
  - Returns:
    - `{ streamUrl, isDash }`
  - Handles direct URLs and manifest decoding.

- `downloadTrackBlob(id, quality = 'LOSSLESS', onProgress?)`
  - Downloads stream into a `Blob`.
  - Returns:
    - `{ blob, filename, track }`
  - Progress callback shape:
    - `{ receivedBytes, totalBytes? }`

#### Artwork helpers

- `getCoverUrl(coverId, size = 320)`
  - Builds Tidal image URL for album/track cover ids.

- `getArtistPictureUrl(id, size = 320)`
  - Builds Tidal image URL for artist image ids.

#### Cache helpers

- `clearCache()`
  - Clears API cache.

- `getCacheStats()`
  - Returns cache stats object from `APICache`.

---

### `APICache` ([`src/cache.js`](src/cache.js))

```js
import { APICache } from './src/cache.js';
```

- Constructor:
  - `new APICache({ maxSize = 250, ttl = 20min } = {})`

- Methods:
  - `get(namespace, key)`
  - `set(namespace, key, value, ttl?)`
  - `clearExpired()`
  - `clear()`
  - `getCacheStats()`

Uses namespace-prefixed keys and timestamp-based TTL expiration.

---

### `DownloadManager` ([`src/downloads.js`](src/downloads.js))

```js
import { DownloadManager } from './src/downloads.js';
```

- Constructor:
  - `new DownloadManager(api)`

- Methods:
  - `setOnChange(callback)`
  - `getTasks()`
  - `clearFinished()`
  - `cancel(taskId)`
  - `downloadTrack(track, quality?)`
  - `downloadAlbum(album, tracks, quality?)`

Task status lifecycle:
- `queued -> downloading -> done`
- error path: `error`
- cancel path: `cancelled`

---

### `LyricsManager` ([`src/lyrics.js`](src/lyrics.js))

```js
import { LyricsManager } from './src/lyrics.js';
```

- Constructor:
  - `new LyricsManager()`

- Methods:
  - `fetchLyrics(track)`
    - Fetches from LRCLIB and caches by track key.
  - `getCurrentLineIndex(currentTime, lines)`
    - Binary search for active synced line.
  - `renderLyrics(payload, activeIndex?)`
    - Produces renderable HTML for synced/plain lyrics.

Internal helper:
- LRC parsing into `{ time, text }[]`.

---

### `SimplePlayer` ([`src/player.js`](src/player.js))

```js
import { SimplePlayer } from './src/player.js';
```

- Constructor:
  - `new SimplePlayer({ audio, progressEl, currentTimeEl, durationEl })`

- Core methods:
  - `setResolver(resolveStreamFn)`
  - `setQueue(tracks, startIndex?)`
  - `getCurrentTrack()`
  - `playCurrent(resolverOverride?)`
  - `playNext(resolverOverride?)`
  - `playPrev(resolverOverride?)`
  - `togglePlayPause()`

- Events/callbacks:
  - `onTrackChanged(track)`
  - `onTimeUpdate(current, duration, track)`

Supports direct audio URLs and DASH playback via `dash.js`.

---

## Notes

- [`src/app.js`](src/app.js) is the integration shell (routing + UI wiring) for runtime modules.
- [`greyscale-app`](greyscale-app) is intentionally preserved as demo history.
