weee
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
{{define "admin_album_edit"}}{{template "header" .}}
|
||||
<div class="card" style="max-width: 500px;">
|
||||
<div class="card-body">
|
||||
<h2 class="h6">Edit album</h2>
|
||||
<form method="post" action="/admin/albums/{{.Data.ID}}/edit" enctype="multipart/form-data">
|
||||
{{template "csrf_field" .CSRFToken}}
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Title</label>
|
||||
<input type="text" name="title" class="form-control form-control-sm" value="{{.Data.Title}}" required>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Sort title</label>
|
||||
<input type="text" name="sort_title" class="form-control form-control-sm" value="{{.Data.SortTitle}}">
|
||||
</div>
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label class="form-label small">Year</label>
|
||||
<input type="number" name="year" class="form-control form-control-sm" value="{{if .Data.Year.Valid}}{{.Data.Year.Int64}}{{end}}">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label small">Original year</label>
|
||||
<input type="number" name="original_year" class="form-control form-control-sm" value="{{if .Data.OriginalYear.Valid}}{{.Data.OriginalYear.Int64}}{{end}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small">Record label</label>
|
||||
<input type="text" name="record_label" class="form-control form-control-sm" value="{{.Data.RecordLabel}}">
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<h3 class="h6">Cover art</h3>
|
||||
{{if .Data.CoverArtID.Valid}}
|
||||
<img src="/cover-art/{{.Data.CoverArtID.String}}?size=150" style="width:120px;" class="mb-2" alt="">
|
||||
{{end}}
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Upload a cover</label>
|
||||
<input type="file" name="cover_file" class="form-control form-control-sm" accept="image/*">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small">...or set a cover URL</label>
|
||||
<input type="url" name="cover_url" class="form-control form-control-sm" placeholder="https://example.com/cover.jpg">
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<button type="submit" class="btn btn-primary btn-sm">Save</button>
|
||||
<a href="/albums/{{.Data.ID}}" class="btn btn-sm btn-outline-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{template "footer" .}}{{end}}
|
||||
@@ -0,0 +1,50 @@
|
||||
{{define "admin_artist_edit"}}{{template "header" .}}
|
||||
<div class="card" style="max-width: 500px;">
|
||||
<div class="card-body">
|
||||
<h2 class="h6">Edit artist</h2>
|
||||
<form method="post" action="/admin/artists/{{.Data.ID}}/edit" enctype="multipart/form-data">
|
||||
{{template "csrf_field" .CSRFToken}}
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Name</label>
|
||||
<input type="text" name="name" class="form-control form-control-sm" value="{{.Data.Name}}" required>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Sort name</label>
|
||||
<input type="text" name="sort_name" class="form-control form-control-sm" value="{{.Data.SortName}}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small">Biography</label>
|
||||
<textarea name="bio" class="form-control form-control-sm" rows="4">{{.Data.Bio}}</textarea>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<h3 class="h6">Photo</h3>
|
||||
{{if .Data.CoverArtID.Valid}}
|
||||
<img src="/cover-art/{{.Data.CoverArtID.String}}?size=150" style="width:100px;height:100px;border-radius:50%;object-fit:cover;" class="mb-2" alt="">
|
||||
{{end}}
|
||||
{{if .Data.PhotoManual}}
|
||||
<p class="small text-body-secondary">This photo was set manually and will not be replaced by auto-fetch.</p>
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input" type="checkbox" name="remove_photo" value="1" id="remove_photo">
|
||||
<label class="form-check-label" for="remove_photo">Remove custom photo (revert to auto-fetch)</label>
|
||||
</div>
|
||||
{{else}}
|
||||
<p class="small text-body-secondary">No custom photo set{{if not .Data.MusicBrainzID}} (this artist has no MusicBrainz ID, so auto-fetch cannot run){{end}}.</p>
|
||||
{{end}}
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Upload a photo</label>
|
||||
<input type="file" name="photo_file" class="form-control form-control-sm" accept="image/*">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small">...or set a photo URL</label>
|
||||
<input type="url" name="photo_url" class="form-control form-control-sm" placeholder="https://example.com/photo.jpg">
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<button type="submit" class="btn btn-primary btn-sm">Save</button>
|
||||
<a href="/artists/{{.Data.ID}}" class="btn btn-sm btn-outline-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{template "footer" .}}{{end}}
|
||||
@@ -22,6 +22,13 @@
|
||||
{{if .Channel.ErrorMessage}}<div class="text-danger small">{{.Channel.ErrorMessage}}</div>{{end}}
|
||||
</div>
|
||||
<div>
|
||||
{{if .Episodes}}
|
||||
<form method="post" action="/podcasts/{{.Channel.ID}}/download-all" class="d-inline">
|
||||
{{template "csrf_field" $.CSRFToken}}
|
||||
<input type="hidden" name="next" value="/admin/podcasts">
|
||||
<button type="submit" class="btn btn-sm btn-outline-primary"><i class="bi bi-download"></i> Download all</button>
|
||||
</form>
|
||||
{{end}}
|
||||
<form method="post" action="/admin/podcasts/{{.Channel.ID}}/refresh" class="d-inline">
|
||||
{{template "csrf_field" $.CSRFToken}}
|
||||
<button type="submit" class="btn btn-sm btn-outline-primary">Refresh</button>
|
||||
|
||||
@@ -21,6 +21,43 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3" style="max-width: 600px;">
|
||||
<div class="card-body">
|
||||
<h2 class="h6">Artist photos</h2>
|
||||
{{if .Data.ArtistPhotosEnabled}}
|
||||
<p class="text-secondary small mb-2">
|
||||
Fetch a photo (fanart.tv when an artist has a tagged MusicBrainz ID and a key is configured, otherwise a direct name search against Wikidata/Wikimedia Commons) for every artist with no manually-set photo. This never calls MusicBrainz itself, so it isn't affected by its rate limits — matching by name alone is less precise, though. This bypasses any previously-cached "not found" result, so it's a good way to retry after fixing a config or network issue.
|
||||
</p>
|
||||
{{else}}
|
||||
<p class="text-secondary small mb-2">
|
||||
Automatic background fetching is disabled (<code>VIBESONIC_ENABLE_ARTIST_PHOTOS=false</code>), but this button still runs a one-off fetch — useful for checking why photos aren't showing up.
|
||||
</p>
|
||||
{{end}}
|
||||
<form method="post" action="/admin/settings/artist-photos/refresh">
|
||||
{{template "csrf_field" .CSRFToken}}
|
||||
<button type="submit" class="btn btn-sm btn-outline-primary">Fetch all artist photos now</button>
|
||||
</form>
|
||||
{{if .Data.ArtistPhotoResults}}
|
||||
<table class="table table-sm vs-table mt-3 mb-0">
|
||||
<thead><tr><th>Artist</th><th>Status</th><th>Error</th></tr></thead>
|
||||
<tbody>
|
||||
{{range .Data.ArtistPhotoResults}}
|
||||
<tr>
|
||||
<td>{{.ArtistName}}</td>
|
||||
<td>
|
||||
{{if .Error}}<span class="text-danger">{{.Status}}</span>
|
||||
{{else if eq .Status "fetched"}}<span class="text-success">{{.Status}}</span>
|
||||
{{else}}<span class="text-secondary">{{.Status}}</span>{{end}}
|
||||
</td>
|
||||
<td class="text-danger small">{{.Error}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3" style="max-width: 600px;">
|
||||
<div class="card-body">
|
||||
<h2 class="h6">External authentication</h2>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
{{define "admin_song_edit"}}{{template "header" .}}
|
||||
<div class="card" style="max-width: 500px;">
|
||||
<div class="card-body">
|
||||
<h2 class="h6">Edit song</h2>
|
||||
<form method="post" action="/admin/songs/{{.Data.ID}}/edit">
|
||||
{{template "csrf_field" .CSRFToken}}
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Title</label>
|
||||
<input type="text" name="title" class="form-control form-control-sm" value="{{.Data.Title}}" required>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Sort title</label>
|
||||
<input type="text" name="sort_title" class="form-control form-control-sm" value="{{.Data.SortTitle}}">
|
||||
</div>
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label class="form-label small">Track #</label>
|
||||
<input type="number" name="track_number" class="form-control form-control-sm" value="{{if .Data.TrackNumber.Valid}}{{.Data.TrackNumber.Int64}}{{end}}">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label small">Disc #</label>
|
||||
<input type="number" name="disc_number" class="form-control form-control-sm" value="{{if .Data.DiscNumber.Valid}}{{.Data.DiscNumber.Int64}}{{end}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Composer</label>
|
||||
<input type="text" name="composer" class="form-control form-control-sm" value="{{.Data.Composer}}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small">Conductor</label>
|
||||
<input type="text" name="conductor" class="form-control form-control-sm" value="{{.Data.Conductor}}">
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" name="metadata_locked" value="1" id="metadata_locked" {{if .Data.MetadataLocked}}checked{{end}}>
|
||||
<label class="form-check-label" for="metadata_locked">Lock metadata (prevent library rescans from overwriting these fields)</label>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<button type="submit" class="btn btn-primary btn-sm">Save</button>
|
||||
<a href="/songs" class="btn btn-sm btn-outline-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{template "footer" .}}{{end}}
|
||||
@@ -11,6 +11,9 @@
|
||||
{{template "csrf_field" .CSRFToken}}
|
||||
<button type="submit" class="btn btn-sm btn-outline-secondary"><i class="bi bi-share"></i> Share</button>
|
||||
</form>
|
||||
{{if and .User .User.IsAdmin}}
|
||||
<a class="btn btn-sm btn-outline-secondary" href="/admin/albums/{{.Data.Album.ID}}/edit"><i class="bi bi-pencil"></i> Edit album</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{template "song_rows" .Data.Songs}}
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
{{define "artist"}}{{template "header" .}}
|
||||
<div class="d-flex gap-3 mb-4">
|
||||
<img class="vs-card-art" style="width:180px;border-radius:50%;" src="{{if .Data.Artist.CoverArtID}}/cover-art/{{.Data.Artist.CoverArtID}}?size=400{{else}}/static/img/default-art.svg{{end}}" alt="">
|
||||
<div>
|
||||
<h2 class="h4 mb-1">{{.Data.Artist.Name}}</h2>
|
||||
{{if .Data.Artist.Bio}}<p class="text-secondary mb-2">{{.Data.Artist.Bio}}</p>{{end}}
|
||||
{{if and .User .User.IsAdmin}}
|
||||
<a class="btn btn-sm btn-outline-secondary" href="/admin/artists/{{.Data.Artist.ID}}/edit"><i class="bi bi-pencil"></i> Edit artist</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="vs-grid mb-3">
|
||||
{{range .Data.Albums}}{{template "album_card" .}}{{end}}
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
{{define "artist_card"}}
|
||||
<a href="/artists/{{.ID}}" class="vs-card">
|
||||
<img class="vs-card-art" style="border-radius:50%;" src="/static/img/default-art.svg" alt="" loading="lazy">
|
||||
<img class="vs-card-art" style="border-radius:50%;" src="{{if .CoverArtID}}/cover-art/{{.CoverArtID}}?size=300{{else}}/static/img/default-art.svg{{end}}" alt="" loading="lazy">
|
||||
<div class="vs-card-title">{{.Name}}</div>
|
||||
</a>
|
||||
{{end}}
|
||||
@@ -26,11 +26,17 @@
|
||||
data-cover="{{$s.CoverArtID}}"
|
||||
data-starred="{{if $s.Starred}}1{{else}}0{{end}}">
|
||||
<td class="vs-track-num">{{if $s.TrackNumber}}{{$s.TrackNumber}}{{else}}{{add $i 1}}{{end}}</td>
|
||||
<td class="vs-song-art-cell">
|
||||
<img class="vs-song-art" src="{{if $s.CoverArtID}}/cover-art/{{$s.CoverArtID}}?size=80{{else}}/static/img/default-art.svg{{end}}" alt="" loading="lazy">
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-semibold">{{$s.Title}}</div>
|
||||
{{if $s.AlbumTitle}}<div class="text-secondary small">{{$s.ArtistName}} — {{$s.AlbumTitle}}</div>{{else}}<div class="text-secondary small">{{$s.ArtistName}}</div>{{end}}
|
||||
</td>
|
||||
<td class="text-end text-secondary small">{{fmtDuration $s.DurationSeconds}}</td>
|
||||
<td class="text-end text-secondary small">
|
||||
{{fmtDuration $s.DurationSeconds}}
|
||||
{{if $s.IsAdmin}}<a href="/admin/songs/{{$s.ID}}/edit" title="Edit song" class="text-secondary ms-2"><i class="bi bi-pencil"></i></a>{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<script>window.vsCSRFToken = {{.CSRFToken}};</script>
|
||||
<script data-vs-csrf="{{.CSRFToken}}">window.vsCSRFToken = {{.CSRFToken}};</script>
|
||||
<header class="vs-topbar d-flex align-items-center justify-content-between">
|
||||
<a href="/" class="vs-brand d-flex align-items-center gap-2">
|
||||
<i class="bi bi-vinyl-fill"></i>
|
||||
|
||||
@@ -3,7 +3,15 @@
|
||||
{{range .Data}}
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<h2 class="h6">{{.Channel.Title}}</h2>
|
||||
<div class="d-flex align-items-center justify-content-between gap-2">
|
||||
<h2 class="h6 mb-0">{{.Channel.Title}}</h2>
|
||||
{{if .Episodes}}
|
||||
<form method="post" action="/podcasts/{{.Channel.ID}}/download-all">
|
||||
{{template "csrf_field" $.CSRFToken}}
|
||||
<button type="submit" class="btn btn-sm btn-outline-primary"><i class="bi bi-download"></i> Download all episodes</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
<p class="text-secondary small">{{.Channel.Description}}</p>
|
||||
<table class="table table-sm vs-table mb-0">
|
||||
<tbody>
|
||||
|
||||
Reference in New Issue
Block a user