Files
Simon DiesenreiterandClaude Opus 5.5 1e65864c8f Song server: search and download songs, mobile web UI
- Sources: "free" (Creative Commons songs with media from
  github.com/UltraStar-Deluxe/songs) and "usdb" (usdb.animux.de with an
  account; audio/video via yt-dlp from the song's #VIDEO hints or YouTube
  links in the comments, covers from fanart.tv/YouTube)
- Aggregated search (all sources in parallel, in-library flag), download
  queue into <library>/Downloads/<source>/<Artist - Title>, then rescan
- Web UI in wwwroot (plain JS, no build): search, downloads with progress,
  library; phone first, dark mode, installable
- Docker image with yt-dlp, ffmpeg and deno; library mounted writable
- Tests for matching, USDB parsing and song rewriting

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 23:15:48 +01:00

70 lines
2.7 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#7c3aed">
<title>Karaoke Songs</title>
<link rel="manifest" href="manifest.webmanifest">
<link rel="icon" href="icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="icon.svg">
<link rel="stylesheet" href="app.css">
</head>
<body>
<header class="top">
<h1><img src="icon.svg" alt="" width="28" height="28"> Karaoke Songs</h1>
<span id="libraryCount" class="muted"></span>
</header>
<main>
<section id="tab-search" class="tab active" aria-label="Search">
<form id="searchForm" class="searchbar" role="search">
<input id="query" type="search" placeholder="Artist or title" autocomplete="off" enterkeyhint="search" aria-label="Search songs">
</form>
<div id="sources" class="chips"></div>
<div id="searchStatus" class="status"></div>
<ul id="results" class="list"></ul>
</section>
<section id="tab-downloads" class="tab" aria-label="Downloads">
<div id="downloadsEmpty" class="status">No downloads yet. Find songs under Search.</div>
<ul id="downloads" class="list"></ul>
</section>
<section id="tab-library" class="tab" aria-label="Library">
<form class="searchbar" onsubmit="return false">
<input id="libraryFilter" type="search" placeholder="Filter the library" autocomplete="off" aria-label="Filter the library">
</form>
<div id="libraryStatus" class="status"></div>
<ul id="library" class="list compact"></ul>
</section>
</main>
<nav class="tabs" aria-label="Sections">
<button data-tab="search" class="active">
<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-4-4"/></svg>
Search
</button>
<button data-tab="downloads">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4v11m0 0-4-4m4 4 4-4M5 20h14"/></svg>
Downloads <span id="activeBadge" class="badge" hidden></span>
</button>
<button data-tab="library">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 18V6l11-2v12"/><circle cx="6" cy="18" r="3"/><circle cx="17" cy="16" r="3"/></svg>
Library
</button>
</nav>
<dialog id="keyDialog">
<form method="dialog">
<h2>Access key</h2>
<p class="muted">This song server needs an access key.</p>
<input id="keyInput" type="password" autocomplete="current-password" aria-label="Access key">
<menu><button value="ok" class="primary">Save</button></menu>
</form>
</dialog>
<script src="app.js"></script>
</body>
</html>