mirror of
https://github.com/gabehf/music-importer.git
synced 2026-04-22 11:31:52 -07:00
297 lines
7.5 KiB
Cheetah
297 lines
7.5 KiB
Cheetah
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Music Importer</title>
|
|
<style>
|
|
*, *::before, *::after { box-sizing: border-box; }
|
|
|
|
body {
|
|
font-family: sans-serif;
|
|
background: #111;
|
|
color: #eee;
|
|
text-align: center;
|
|
padding: 60px 24px 80px;
|
|
margin: 0;
|
|
}
|
|
|
|
h1 { margin-bottom: 32px; }
|
|
|
|
button {
|
|
font-size: 32px;
|
|
padding: 20px 40px;
|
|
border-radius: 10px;
|
|
border: none;
|
|
cursor: pointer;
|
|
background: #4CAF50;
|
|
color: white;
|
|
}
|
|
button:disabled {
|
|
background: #555;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
/* ── Last run summary ── */
|
|
.session {
|
|
margin: 48px auto 0;
|
|
max-width: 820px;
|
|
text-align: left;
|
|
}
|
|
|
|
.session-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: baseline;
|
|
border-bottom: 1px solid #333;
|
|
padding-bottom: 8px;
|
|
margin-bottom: 20px;
|
|
}
|
|
.session-header h2 { margin: 0; font-size: 18px; color: #ccc; }
|
|
.session-header .duration { font-size: 13px; color: #666; }
|
|
|
|
.album {
|
|
background: #1a1a1a;
|
|
border: 1px solid #2a2a2a;
|
|
border-radius: 8px;
|
|
padding: 16px 20px;
|
|
margin-bottom: 12px;
|
|
}
|
|
.album-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
margin-bottom: 10px;
|
|
}
|
|
.album-name {
|
|
font-weight: bold;
|
|
font-size: 15px;
|
|
flex: 1;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.badge {
|
|
font-size: 11px;
|
|
font-weight: bold;
|
|
padding: 2px 8px;
|
|
border-radius: 4px;
|
|
white-space: nowrap;
|
|
}
|
|
.badge-ok { background: #1e4d2b; color: #4CAF50; }
|
|
.badge-warn { background: #4d3a00; color: #f0a500; }
|
|
.badge-fatal { background: #4d1a1a; color: #e05050; }
|
|
|
|
/* ── Metadata row ── */
|
|
.metadata {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 14px;
|
|
flex-wrap: wrap;
|
|
font-size: 12px;
|
|
color: #777;
|
|
margin-bottom: 12px;
|
|
}
|
|
.metadata-title {
|
|
color: #aaa;
|
|
font-size: 13px;
|
|
}
|
|
.metadata-pill {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
background: #222;
|
|
border-radius: 4px;
|
|
padding: 2px 7px;
|
|
font-size: 11px;
|
|
}
|
|
.pill-label { color: #555; }
|
|
.pill-beets { color: #7ec8e3; }
|
|
.pill-musicbrainz { color: #c084fc; }
|
|
.pill-file_tags { color: #f0a500; }
|
|
.pill-unknown { color: #888; }
|
|
|
|
/* ── Rich info grid ── */
|
|
.info-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
gap: 6px;
|
|
margin-bottom: 12px;
|
|
}
|
|
.info-card {
|
|
background: #222;
|
|
border-radius: 6px;
|
|
padding: 8px 12px;
|
|
font-size: 12px;
|
|
}
|
|
.info-card-label {
|
|
font-size: 10px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
color: #555;
|
|
margin-bottom: 5px;
|
|
}
|
|
.info-card-value {
|
|
color: #ccc;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
}
|
|
.info-card-sub {
|
|
margin-top: 3px;
|
|
color: #666;
|
|
font-size: 11px;
|
|
line-height: 1.4;
|
|
}
|
|
.info-ok { color: #4CAF50; }
|
|
.info-warn { color: #f0a500; }
|
|
.info-dim { color: #555; }
|
|
|
|
/* ── Pipeline steps ── */
|
|
.steps-label {
|
|
font-size: 10px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
color: #444;
|
|
margin-bottom: 6px;
|
|
}
|
|
.steps {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
gap: 6px;
|
|
}
|
|
.step {
|
|
font-size: 12px;
|
|
padding: 5px 10px;
|
|
border-radius: 5px;
|
|
background: #222;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
.step-label { color: #888; }
|
|
.step-ok { color: #4CAF50; }
|
|
.step-warn { color: #f0a500; }
|
|
.step-fatal { color: #e05050; }
|
|
.step-err { font-size: 11px; color: #c0392b; margin-top: 2px; word-break: break-word; }
|
|
|
|
footer {
|
|
position: fixed;
|
|
bottom: 16px;
|
|
width: 100%;
|
|
font-size: 13px;
|
|
color: #999;
|
|
text-align: center;
|
|
left: 0;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Music Importer</h1>
|
|
|
|
<form action="/run" method="POST">
|
|
<button type="submit" {{if .Running}}disabled{{end}}>
|
|
{{if .Running}}Importer Running...{{else}}Run Importer{{end}}
|
|
</button>
|
|
</form>
|
|
|
|
{{with .Session}}
|
|
<div class="session">
|
|
<div class="session-header">
|
|
<h2>Last Run — {{.StartedAt.Format "Jan 2, 2006 15:04:05"}}</h2>
|
|
<span class="duration">{{duration .StartedAt .FinishedAt}}</span>
|
|
</div>
|
|
|
|
{{range .Albums}}{{$album := .}}
|
|
<div class="album">
|
|
<div class="album-header">
|
|
<span class="album-name" title="{{.Path}}">{{.Name}}</span>
|
|
{{if .Succeeded}}
|
|
{{if .HasWarnings}}
|
|
<span class="badge badge-warn">⚠ warnings</span>
|
|
{{else}}
|
|
<span class="badge badge-ok">✓ ok</span>
|
|
{{end}}
|
|
{{else}}
|
|
<span class="badge badge-fatal">✗ failed at {{.FatalStep}}</span>
|
|
{{end}}
|
|
</div>
|
|
|
|
{{with .Metadata}}
|
|
<div class="metadata">
|
|
<span class="metadata-title">{{.Artist}} — {{.Album}}{{if .Year}} ({{.Year}}){{end}}</span>
|
|
{{if $album.MetadataSource}}
|
|
<span class="metadata-pill">
|
|
<span class="pill-label">via</span>
|
|
{{if eq (print $album.MetadataSource) "beets"}}
|
|
<span class="pill-beets">beets</span>
|
|
{{else if eq (print $album.MetadataSource) "musicbrainz"}}
|
|
<span class="pill-musicbrainz">MusicBrainz</span>
|
|
{{else if eq (print $album.MetadataSource) "file_tags"}}
|
|
<span class="pill-file_tags">file tags</span>
|
|
{{else}}
|
|
<span class="pill-unknown">unknown</span>
|
|
{{end}}
|
|
</span>
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|
|
|
|
{{/* ── Rich info cards ── */}}
|
|
<div class="info-grid">
|
|
{{/* Tracks */}}
|
|
<div class="info-card">
|
|
<div class="info-card-label">Tracks</div>
|
|
<div class="info-card-value">{{.TrackCount}}</div>
|
|
</div>
|
|
|
|
{{/* Lyrics */}}
|
|
<div class="info-card">
|
|
<div class="info-card-label">Lyrics</div>
|
|
{{if eq .LyricsStats.Total 0}}
|
|
<div class="info-card-value info-dim">n/a</div>
|
|
{{else}}
|
|
<div class="info-card-value {{if gt .LyricsStats.Downloaded 0}}info-ok{{else}}info-dim{{end}}">
|
|
{{.LyricsStats.Downloaded}} / {{.LyricsStats.Total}}
|
|
</div>
|
|
<div class="info-card-sub">
|
|
{{if gt .LyricsStats.Synced 0}}<span class="info-ok">{{.LyricsStats.Synced}} synced</span>{{end}}
|
|
{{if and (gt .LyricsStats.Synced 0) (gt .LyricsStats.Plain 0)}} · {{end}}
|
|
{{if gt .LyricsStats.Plain 0}}<span class="info-warn">{{.LyricsStats.Plain}} plain</span>{{end}}
|
|
{{if gt .LyricsStats.AlreadyHad 0}}<span class="info-dim"> {{.LyricsStats.AlreadyHad}} existing</span>{{end}}
|
|
{{if gt .LyricsStats.NotFound 0}}<span class="info-dim"> {{.LyricsStats.NotFound}} missing</span>{{end}}
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
|
|
{{/* Cover art */}}
|
|
<div class="info-card">
|
|
<div class="info-card-label">Cover Art</div>
|
|
{{if .CoverArtStats.Found}}
|
|
{{if .CoverArtStats.Embedded}}
|
|
<div class="info-card-value info-ok">Embedded</div>
|
|
<div class="info-card-sub info-dim">{{.CoverArtStats.Source}}</div>
|
|
{{else}}
|
|
<div class="info-card-value info-warn">Found, not embedded</div>
|
|
<div class="info-card-sub info-dim">{{.CoverArtStats.Source}}</div>
|
|
{{end}}
|
|
{{else}}
|
|
<div class="info-card-value info-dim">Not found</div>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="steps-label">Pipeline</div>
|
|
<div class="steps">
|
|
{{stepCell "Clean Tags" .CleanTags ""}}
|
|
{{stepCell "Metadata" .TagMetadata .FatalStep}}
|
|
{{stepCell "Lyrics" .Lyrics ""}}
|
|
{{stepCell "ReplayGain" .ReplayGain .FatalStep}}
|
|
{{stepCell "Cover Art" .CoverArt .FatalStep}}
|
|
{{stepCell "Move" .Move ""}}
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|
|
|
|
<footer>{{.Version}}</footer>
|
|
</body>
|
|
</html>
|