ryOS ryOS / Docs
GitHub Launch

Song API

The Song API provides endpoints for managing a music library in ryOS, including CRUD operations for songs, lyrics fetching from KuGou, AI-powered translations, Japanese furigana annotations, and "soramimi" (空耳) phonetic readings.

Refactored song routes use the shared apiHandler pattern with optional auth plus action-specific permission checks.

Overview

Songs are identified by their YouTube video ID (11 characters). The API stores:

  • Metadata (lightweight ~300 bytes): title, artist, album, cover, timestamps
  • Content (heavy ~5-50KB): lyrics, translations, furigana, soramimi annotations

Data is stored in Redis (Upstash) with split storage to avoid exceeding request size limits when listing songs.

Endpoints

MethodPathDescription
GET/api/songsList songs with optional filtering
POST/api/songsCreate song or bulk import
DELETE/api/songsDelete all songs (admin only)
GET/api/songs/{id}Get song by ID
POST/api/songs/{id}Update song or perform action
DELETE/api/songs/{id}Delete song (admin only)

Authentication

Authentication uses a combination of Bearer token and username header:

Authorization: Bearer <auth_token>
X-Username: <username>

Permission Levels

OperationRequired Permission
List songsNone
Create songAuthenticated
Update own songAuthenticated (owner)
Update any songAdmin only
Delete songAdmin only
Bulk importAdmin only
Delete all songsAdmin only

The admin user is ryo (case-insensitive).


Collection Endpoints

GET /api/songs

List all songs or filter by specific criteria.

Query Parameters

ParameterTypeDescription
createdBystringFilter by creator username
idsstringComma-separated list of song IDs for batch fetch
includestringComma-separated: metadata, lyrics, translations, furigana, soramimi (default: metadata)

Response

{
  "songs": [
    {
      "id": "dQw4w9WgXcQ",
      "title": "Never Gonna Give You Up",
      "artist": "Rick Astley",
      "album": "Whenever You Need Somebody",
      "cover": "https://...",
      "lyricOffset": 0,
      "lyricsSource": {
        "hash": "abc123",
        "albumId": "12345",
        "title": "Never Gonna Give You Up",
        "artist": "Rick Astley",
        "album": "Whenever You Need Somebody"
      },
      "createdBy": "ryo",
      "createdAt": 1704067200000,
      "updatedAt": 1704067200000,
      "importOrder": 0
    }
  ]
}

POST /api/songs

Create a single song or perform bulk import.

Single Song Creation

{
  "id": "dQw4w9WgXcQ",
  "title": "Never Gonna Give You Up",
  "artist": "Rick Astley",
  "album": "Whenever You Need Somebody",
  "lyricOffset": 0,
  "lyricsSource": {
    "hash": "abc123",
    "albumId": "12345",
    "title": "Never Gonna Give You Up",
    "artist": "Rick Astley"
  }
}

Bulk Import (Admin Only)

{
  "action": "import",
  "songs": [
    {
      "id": "dQw4w9WgXcQ",
      "title": "Song Title",
      "artist": "Artist Name",
      "album": "Album Name",
      "lyricOffset": 0,
      "lyricsSource": { ... },
      "lyrics": { "lrc": "...", "krc": "..." },
      "translations": { "zh-TW": "..." },
      "furigana": [[{ "text": "歌詞", "reading": "かし" }]],
      "soramimi": [[{ "text": "歌詞", "reading": "割詞" }]],
      "createdBy": "ryo",
      "createdAt": 1704067200000,
      "updatedAt": 1704067200000,
      "importOrder": 0
    }
  ]
}

Content fields (lyrics, translations, furigana, soramimi, soramimiByLang) can be compressed using gzip and base64 encoded with a gzip: prefix.

Response

{
  "success": true,
  "id": "dQw4w9WgXcQ",
  "isUpdate": false,
  "createdBy": "username"
}

For bulk import:

{
  "success": true,
  "imported": 10,
  "updated": 5,
  "withContent": 15,
  "total": 15
}

DELETE /api/songs

Delete all songs (admin only).

Response

{
  "success": true,
  "deleted": 42
}

Individual Song Endpoints

GET /api/songs/{id}

Retrieve a song by its YouTube video ID.

Query Parameters

ParameterTypeDescription
includestringComma-separated: metadata, lyrics, translations, furigana, soramimi (default: metadata)

Response

{
  "id": "dQw4w9WgXcQ",
  "title": "Song Title",
  "artist": "Artist Name",
  "lyrics": {
    "lrc": "[00:00.00]First line...",
    "krc": "[0,1000]<0,500,0>First<500,500,0>line...",
    "parsedLines": [
      {
        "startTimeMs": "0",
        "words": "First line",
        "wordTimings": [
          { "text": "First", "startTimeMs": 0, "durationMs": 500 },
          { "text": "line", "startTimeMs": 500, "durationMs": 500 }
        ]
      }
    ]
  },
  "translations": {
    "zh-TW": "[00:00.00]第一行..."
  },
  "furigana": [
    [{ "text": "歌詞", "reading": "かし" }, { "text": "です" }]
  ],
  "soramimi": [
    [{ "text": "歌詞", "reading": "割詞" }]
  ],
  "soramimiByLang": {
    "zh-TW": [[{ "text": "歌詞", "reading": "割詞" }]],
    "en": [[{ "text": "歌詞", "reading": "ka shi" }]]
  }
}

POST /api/songs/{id}

Update song metadata or perform an action.

Actions

ActionDescriptionAuth Required
(none)Update metadataYes (owner)
search-lyricsSearch KuGou for lyricsNo
fetch-lyricsFetch lyrics from KuGouFirst fetch: No, Force refresh: Yes
translateGenerate full translation (non-streaming JSON)First: No, Force: Yes
translate-streamGenerate translation (SSE)First: No, Force: Yes
furigana-streamGenerate furigana (SSE)First: No, Force: Yes
soramimi-streamGenerate soramimi (SSE)First: No, Force: Yes
clear-cached-dataClear cached annotationsNo
unshareClear createdBy fieldAdmin only

Action: search-lyrics

Search for matching lyrics on KuGou.

Request

{
  "action": "search-lyrics",
  "query": "周杰倫 晴天"
}

If query is omitted, uses the song's title and artist.

Response

{
  "results": [
    {
      "title": "晴天",
      "artist": "周杰倫",
      "album": "葉惠美",
      "hash": "abc123def456",
      "albumId": "12345",
      "score": 0.95
    }
  ]
}

Action: fetch-lyrics

Fetch lyrics from KuGou using a lyrics source.

Request

{
  "action": "fetch-lyrics",
  "lyricsSource": {
    "hash": "abc123def456",
    "albumId": "12345",
    "title": "晴天",
    "artist": "周杰倫"
  },
  "force": false,
  "title": "Song Title",
  "artist": "Artist Name",
  "returnMetadata": true,
  "translateTo": "en",
  "includeFurigana": true,
  "includeSoramimi": true,
  "soramimiTargetLanguage": "zh-TW"
}
FieldTypeDescription
lyricsSourceobjectKuGou source (from search results)
forcebooleanForce refresh even if cached
titlestringOptional title for auto-search
artiststringOptional artist for auto-search
returnMetadatabooleanInclude metadata in response
translateTostringLanguage code to check translation status
includeFuriganabooleanInclude furigana status in response
includeSoramimibooleanInclude soramimi status in response
soramimiTargetLanguagestringzh-TW or en (default: zh-TW)

Response

{
  "lyrics": {
    "parsedLines": [
      { "startTimeMs": "0", "words": "First line" }
    ]
  },
  "cached": true,
  "translation": {
    "totalLines": 50,
    "cached": true,
    "lrc": "[00:00.00]Translated line..."
  },
  "furigana": {
    "totalLines": 50,
    "cached": false
  },
  "soramimi": {
    "totalLines": 50,
    "cached": false,
    "targetLanguage": "zh-TW"
  },
  "metadata": {
    "title": "晴天",
    "artist": "周杰倫",
    "album": "葉惠美",
    "cover": "https://...",
    "lyricsSource": { ... }
  }
}

Action: translate

Generate full translated LRC in one JSON response (non-streaming).

Request

{
  "action": "translate",
  "language": "en",
  "force": false
}

Response

{
  "translation": "[00:00.00]First translated line...",
  "cached": true
}

Action: translate-stream

Generate AI translation with Server-Sent Events (SSE) streaming.

Request

{
  "action": "translate-stream",
  "language": "en",
  "force": false
}

Response (SSE)

If cached:

{"type": "cached", "translation": "[00:00.00]First line..."}

If generating:

{"type":"start","totalLines":50,"message":"Translation started"}
{"type":"line","lineIndex":0,"translation":"First line","progress":2}
{"type":"line","lineIndex":1,"translation":"Second line","progress":4}
...
{"type":"complete","totalLines":50,"successCount":50,"translations":[...],"success":true}


Action: furigana-stream

Generate furigana (Japanese reading annotations) with SSE streaming.

Request

{
  "action": "furigana-stream",
  "force": false
}

Response (SSE)

If cached:

{"type": "cached", "furigana": [[{"text": "歌詞", "reading": "かし"}]]}

If generating:

{"type":"start","totalLines":50,"message":"Furigana generation started"}
{"type":"line","lineIndex":0,"furigana":[{"text":"私","reading":"わたし"}],"progress":2}
...
{"type":"complete","totalLines":50,"successCount":50,"furigana":[...],"success":true}


Action: soramimi-stream

Generate soramimi (空耳 - phonetic misheard lyrics) with SSE streaming.

Soramimi creates Chinese or English phonetic readings that sound like the original lyrics but can be read as meaningful text in the target language.

Request

{
  "action": "soramimi-stream",
  "force": false,
  "targetLanguage": "zh-TW",
  "furigana": [[{"text": "私", "reading": "わたし"}]]
}
FieldTypeDescription
forcebooleanForce regeneration
targetLanguagestringzh-TW (Chinese) or en (English)
furiganaarrayOptional furigana to help AI know kanji pronunciation

Response (SSE)

{"type":"start","totalLines":50,"message":"AI processing started"}
{"type":"line","lineIndex":0,"soramimi":[{"text":"私","reading":"娃她惜"}],"progress":2}
...
{"type":"complete","totalLines":50,"successCount":50,"soramimi":[...],"success":true}
Note: Chinese soramimi is automatically skipped for Chinese lyrics (returns {"skipped": true, "skipReason": "chinese_lyrics"}).

Action: clear-cached-data

Clear cached translations, furigana, and/or soramimi.

Request

{
  "action": "clear-cached-data",
  "clearTranslations": true,
  "clearFurigana": true,
  "clearSoramimi": true
}

Response

{
  "success": true,
  "cleared": ["translations", "furigana", "soramimi"]
}

Update Metadata

Default POST action (no action field) updates song metadata.

Request

{
  "title": "New Title",
  "artist": "New Artist",
  "album": "New Album",
  "lyricOffset": 1000,
  "lyricsSource": { ... },
  "clearTranslations": false,
  "clearFurigana": false,
  "clearSoramimi": false,
  "clearLyrics": false,
  "isShare": true
}
FieldTypeDescription
lyricOffsetnumberOffset in ms (-300000 to 300000)
clearTranslationsbooleanClear cached translations
clearFuriganabooleanClear cached furigana
clearSoramimibooleanClear cached soramimi
clearLyricsbooleanClear lyrics content
isSharebooleanSet createdBy to current user

Response

{
  "success": true,
  "id": "dQw4w9WgXcQ",
  "isUpdate": true,
  "createdBy": "username"
}

DELETE /api/songs/{id}

Delete a song (admin only).

Response

{
  "success": true,
  "deleted": true
}

Lyrics Processing

Lyrics Formats

The API supports two lyrics formats from KuGou:

FormatDescription
LRCStandard timestamped lyrics: [00:00.00]Lyrics text
KRCWord-level timing: [0,1000]<0,500,0>Word<500,500,0>Word

KRC format provides karaoke-style word-by-word timing with embedded translations.

Line Filtering

Lines are automatically filtered to remove:

  • Credit/metadata lines (作词, 作曲, Produced by, etc.)
  • Title/artist header lines
  • Empty parenthetical content

Chinese Conversion

  • Lyrics from KuGou are in Simplified Chinese
  • Automatically converted to Traditional Chinese for display
  • Japanese lyrics are detected and preserved without conversion

Embedded Translations

KRC files may contain embedded Chinese translations in a base64-encoded [language:] tag. These are automatically extracted for Traditional Chinese translation requests, bypassing AI generation.


Furigana Generation

Furigana (振り仮名) are reading annotations for Japanese kanji.

Process

  1. Lines containing kanji are identified
  1. Non-kanji lines get plain text segments immediately
  2. AI generates readings in <kanji:reading> format
  3. Readings are parsed into segments: [{text: "私", reading: "わたし"}]

Output Format

[
  [
    { "text": "私", "reading": "わたし" },
    { "text": "は" },
    { "text": "走", "reading": "はし" },
    { "text": "る" }
  ]
]

Soramimi Generation

Soramimi (空耳, "sky ear") creates phonetic readings in Chinese or English that sound like foreign lyrics.

Target Languages

LanguageDescriptionExample
zh-TWTraditional Chinese characters사랑 → 思浪 (sounds like "sarang")
enEnglish phonetics사랑 → "saw wrong"

Process

  1. Non-English lines are identified
  1. English lines are passed through unchanged
  2. AI generates readings considering:
    • Phonetic accuracy (sound must be close)
    • Semantic meaning (prefer meaningful characters)
    • Furigana annotations (if provided for Japanese)

Output Format

[
  [
    { "text": "사랑", "reading": "思浪" },
    { "text": "해요", "reading": "海喲" }
  ]
]

Data Types

LyricsSource

interface LyricsSource {
  hash: string;           // KuGou song hash
  albumId: string | number;
  title: string;
  artist: string;
  album?: string;
}

SongMetadata

interface SongMetadata {
  id: string;              // YouTube video ID (11 chars)
  title: string;
  artist?: string;
  album?: string;
  cover?: string;          // Cover image URL
  lyricOffset?: number;    // Timing offset in ms
  lyricsSource?: LyricsSource;
  createdBy?: string;
  createdAt: number;       // Unix timestamp
  updatedAt: number;
  importOrder?: number;    // For stable sorting
}

FuriganaSegment

interface FuriganaSegment {
  text: string;            // Original text
  reading?: string;        // Hiragana reading (furigana) or phonetic reading (soramimi)
}

ParsedLyricLine

interface ParsedLyricLine {
  startTimeMs: string;
  words: string;
  wordTimings?: WordTiming[];
}

interface WordTiming {
  text: string;
  startTimeMs: number;
  durationMs: number;
}

Error Responses

All errors follow this format:

{
  "error": "Error message"
}
StatusDescription
400Bad request (invalid parameters)
401Unauthorized (auth required)
403Forbidden (permission denied)
404Song not found
405Method not allowed
500Internal server error

Example Usage

Create a song and fetch lyrics

// 1. Create the song
const createRes = await fetch('/api/songs', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${token}`,
    'X-Username': username
  },
  body: JSON.stringify({
    id: 'dQw4w9WgXcQ',
    title: 'Never Gonna Give You Up',
    artist: 'Rick Astley'
  })
});

// 2. Search for lyrics
const searchRes = await fetch('/api/songs/dQw4w9WgXcQ', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    action: 'search-lyrics',
    query: 'Rick Astley Never Gonna Give You Up'
  })
});
const { results } = await searchRes.json();

// 3. Fetch lyrics with the best match
const lyricsRes = await fetch('/api/songs/dQw4w9WgXcQ', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    action: 'fetch-lyrics',
    lyricsSource: results[0],
    returnMetadata: true
  })
});

Stream translation with SSE

const response = await fetch('/api/songs/dQw4w9WgXcQ', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    action: 'translate-stream',
    language: 'zh-TW'
  })
});

const reader = response.body.getReader();
const decoder = new TextDecoder();

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  
  const chunk = decoder.decode(value);
  const lines = chunk.split('\n').filter(l => l.startsWith('data: '));
  
  for (const line of lines) {
    const data = JSON.parse(line.slice(6));
    
    if (data.type === 'line') {
      console.log(`Line ${data.lineIndex}: ${data.translation}`);
    } else if (data.type === 'complete') {
      console.log('Translation complete!');
    }
  }
}

Bulk export and import

// Export all songs with full content
const exportRes = await fetch('/api/songs?include=metadata,lyrics,translations,furigana,soramimi');
const { songs } = await exportRes.json();

// Import songs (admin only)
const importRes = await fetch('/api/songs', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${adminToken}`,
    'X-Username': 'ryo'
  },
  body: JSON.stringify({
    action: 'import',
    songs: songs
  })
});

Related Endpoints

  • Media API - YouTube search and audio features