home to your local SPACEGIRL 💫 arimelody.space
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

add releases page; fix HUGE static file perf regression

+233 -73
+5 -5
admin/accounthttp.go
··· 18 18 func accountHandler(app *model.AppState) http.Handler { 19 19 mux := http.NewServeMux() 20 20 21 - mux.Handle("/totp-setup", totpSetupHandler(app)) 22 - mux.Handle("/totp-confirm", totpConfirmHandler(app)) 23 - mux.Handle("/totp-delete/", http.StripPrefix("/totp-delete", totpDeleteHandler(app))) 21 + mux.Handle("/account/totp-setup", totpSetupHandler(app)) 22 + mux.Handle("/account/totp-confirm", totpConfirmHandler(app)) 23 + mux.Handle("/account/totp-delete/", http.StripPrefix("/totp-delete", totpDeleteHandler(app))) 24 24 25 - mux.Handle("/password", changePasswordHandler(app)) 26 - mux.Handle("/delete", deleteAccountHandler(app)) 25 + mux.Handle("/account/password", changePasswordHandler(app)) 26 + mux.Handle("/account/delete", deleteAccountHandler(app)) 27 27 28 28 return mux 29 29 }
+2 -2
admin/artisthttp.go
··· 10 10 "arimelody-web/model" 11 11 ) 12 12 13 - func serveArtist(app *model.AppState) http.Handler { 13 + func serveArtists(app *model.AppState) http.Handler { 14 14 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 15 - slices := strings.Split(r.URL.Path[1:], "/") 15 + slices := strings.Split(strings.TrimPrefix(r.URL.Path, "/artists")[1:], "/") 16 16 id := slices[0] 17 17 artist, err := controller.GetArtist(app.DB, id) 18 18 if err != nil {
+8 -6
admin/http.go
··· 47 47 mux.Handle("/register", registerAccountHandler(app)) 48 48 49 49 mux.Handle("/account", requireAccount(accountIndexHandler(app))) 50 - mux.Handle("/account/", requireAccount(http.StripPrefix("/account", accountHandler(app)))) 50 + mux.Handle("/account/", requireAccount(accountHandler(app))) 51 51 52 52 mux.Handle("/logs", requireAccount(logsHandler(app))) 53 53 54 - mux.Handle("/release/", requireAccount(http.StripPrefix("/release", serveRelease(app)))) 55 - mux.Handle("/artist/", requireAccount(http.StripPrefix("/artist", serveArtist(app)))) 56 - mux.Handle("/track/", requireAccount(http.StripPrefix("/track", serveTrack(app)))) 54 + mux.Handle("/releases", requireAccount(serveReleases(app))) 55 + mux.Handle("/releases/", requireAccount(serveReleases(app))) 56 + mux.Handle("/artists/", requireAccount(serveArtists(app))) 57 + mux.Handle("/tracks/", requireAccount(serveTracks(app))) 57 58 58 - mux.Handle("/static/", http.StripPrefix("/static", staticHandler())) 59 + mux.Handle("/static/", staticHandler()) 59 60 60 61 mux.Handle("/", requireAccount(AdminIndexHandler(app))) 61 62 ··· 470 471 471 472 func staticHandler() http.Handler { 472 473 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 473 - file, err := staticFS.ReadFile(filepath.Join("static", filepath.Clean(r.URL.Path))) 474 + uri := strings.TrimPrefix(r.URL.Path, "/static") 475 + file, err := staticFS.ReadFile(filepath.Join("static", filepath.Clean(uri))) 474 476 if err != nil { 475 477 http.NotFound(w, r) 476 478 return
+46 -4
admin/releasehttp.go
··· 3 3 import ( 4 4 "fmt" 5 5 "net/http" 6 + "os" 6 7 "strings" 7 8 8 9 "arimelody-web/admin/templates" ··· 10 11 "arimelody-web/model" 11 12 ) 12 13 13 - func serveRelease(app *model.AppState) http.Handler { 14 + func serveReleases(app *model.AppState) http.Handler { 14 15 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 15 - slices := strings.Split(r.URL.Path[1:], "/") 16 + slices := strings.Split(strings.TrimPrefix(r.URL.Path, "/releases")[1:], "/") 16 17 releaseID := slices[0] 17 18 19 + var action string = "" 20 + if len(slices) > 1 { 21 + action = slices[1] 22 + } 23 + 24 + if len(releaseID) > 0 { 25 + serveRelease(app, releaseID, action).ServeHTTP(w, r) 26 + return 27 + } 28 + 29 + session := r.Context().Value("session").(*model.Session) 30 + 31 + type ReleasesData struct { 32 + adminPageData 33 + Releases []*model.Release 34 + } 35 + 36 + releases, err := controller.GetAllReleases(app.DB, false, 0, true) 37 + if err != nil { 38 + fmt.Fprintf(os.Stderr, "WARN: Failed to pull releases: %s\n", err) 39 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 40 + return 41 + } 42 + 43 + err = templates.ReleasesTemplate.Execute(w, ReleasesData{ 44 + adminPageData: adminPageData{ 45 + Path: r.URL.Path, 46 + Session: session, 47 + }, 48 + Releases: releases, 49 + }) 50 + if err != nil { 51 + fmt.Fprintf(os.Stderr, "WARN: Failed to render releases page: %s\n", err) 52 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 53 + return 54 + } 55 + }) 56 + } 57 + 58 + func serveRelease(app *model.AppState, releaseID string, action string) http.Handler { 59 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 18 60 session := r.Context().Value("session").(*model.Session) 19 61 20 62 release, err := controller.GetRelease(app.DB, releaseID, true) ··· 28 70 return 29 71 } 30 72 31 - if len(slices) > 1 { 32 - switch slices[1] { 73 + if len(action) > 0 { 74 + switch action { 33 75 case "editcredits": 34 76 serveEditCredits(release).ServeHTTP(w, r) 35 77 return
+3 -3
admin/static/index.js
··· 14 14 headers: { "Content-Type": "application/json" }, 15 15 body: JSON.stringify({id}) 16 16 }).then(res => { 17 - if (res.ok) location = "/admin/release/" + id; 17 + if (res.ok) location = "/admin/releases/" + id; 18 18 else { 19 19 res.text().then(err => { 20 20 alert("Request failed: " + err); ··· 39 39 }).then(res => { 40 40 res.text().then(text => { 41 41 if (res.ok) { 42 - location = "/admin/artist/" + id; 42 + location = "/admin/artists/" + id; 43 43 } else { 44 44 alert("Request failed: " + text); 45 45 console.error(text); ··· 63 63 }).then(res => { 64 64 res.text().then(text => { 65 65 if (res.ok) { 66 - location = "/admin/track/" + text; 66 + location = "/admin/tracks/" + text; 67 67 } else { 68 68 alert("Request failed: " + text); 69 69 console.error(text);
+30
admin/templates/html/artists.html
··· 1 + {{define "head"}} 2 + <title>Artists - ari melody 💫</title> 3 + <link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon"> 4 + <link rel="stylesheet" href="/admin/static/index.css"> 5 + {{end}} 6 + 7 + {{define "content"}} 8 + <main> 9 + <h1>Artists</h1> 10 + 11 + <div class="card-header"> 12 + <h2><a href="/admin/artists/">Artists</a></h2> 13 + <a class="button new" id="create-artist">Create New</a> 14 + </div> 15 + {{if .Artists}} 16 + <div class="artists-group"> 17 + {{range $Artist := .Artists}} 18 + <div class="artist"> 19 + <img src="{{$Artist.GetAvatar}}" alt="" width="64" loading="lazy" class="artist-avatar"> 20 + <a href="/admin/artists/{{$Artist.ID}}" class="artist-name">{{$Artist.Name}}</a> 21 + </div> 22 + {{end}} 23 + </div> 24 + {{else}} 25 + <p>There are no artists.</p> 26 + {{end}} 27 + </main> 28 + 29 + <script type="module" src="/admin/static/admin.js"></script> 30 + {{end}}
+1 -1
admin/templates/html/components/credits/addcredit.html
··· 7 7 {{range $Artist := .Artists}} 8 8 <li class="new-artist" 9 9 data-id="{{$Artist.ID}}" 10 - hx-get="/admin/release/{{$.ReleaseID}}/newcredit/{{$Artist.ID}}" 10 + hx-get="/admin/releases/{{$.ReleaseID}}/newcredit/{{$Artist.ID}}" 11 11 hx-target="#editcredits ul" 12 12 hx-swap="beforeend" 13 13 >
+2 -2
admin/templates/html/components/credits/editcredits.html
··· 3 3 <h2>Editing: Credits</h2> 4 4 <a id="add-credit" 5 5 class="button new" 6 - href="/admin/release/{{.ID}}/addcredit" 7 - hx-get="/admin/release/{{.ID}}/addcredit" 6 + href="/admin/releases/{{.ID}}/addcredit" 7 + hx-get="/admin/releases/{{.ID}}/addcredit" 8 8 hx-target="body" 9 9 hx-swap="beforeend" 10 10 >Add</a>
+3 -3
admin/templates/html/components/release/release-list-item.html
··· 5 5 </div> 6 6 <div class="release-info"> 7 7 <h3 class="release-title"> 8 - <a href="/admin/release/{{.ID}}">{{.Title}}</a> 8 + <a href="/admin/releases/{{.ID}}">{{.Title}}</a> 9 9 <small> 10 10 <span title="{{.PrintReleaseDate}}">{{.ReleaseDate.Year}}</span> 11 11 {{if not .Visible}}(hidden){{end}} ··· 13 13 </h3> 14 14 <p class="release-artists">{{.PrintArtists true true}}</p> 15 15 <p class="release-type-single">{{.ReleaseType}} 16 - (<a href="/admin/release/{{.ID}}#tracks">{{len .Tracks}} track{{if not (eq (len .Tracks) 1)}}s{{end}}</a>)</p> 16 + (<a href="/admin/releases/{{.ID}}#tracks">{{len .Tracks}} track{{if not (eq (len .Tracks) 1)}}s{{end}}</a>)</p> 17 17 <div class="release-actions"> 18 - <a href="/admin/release/{{.ID}}">Edit</a> 18 + <a href="/admin/releases/{{.ID}}">Edit</a> 19 19 <a href="/music/{{.ID}}" target="_blank">Gateway <img class="icon" src="/img/external-link.svg"/></a> 20 20 </div> 21 21 </div>
+1 -1
admin/templates/html/components/tracks/addtrack.html
··· 8 8 </li> 9 9 <li class="new-track" 10 10 data-id="{{$Track.ID}}" 11 - hx-get="/admin/release/{{$.ReleaseID}}/newtrack/{{$Track.ID}}" 11 + hx-get="/admin/releases/{{$.ReleaseID}}/newtrack/{{$Track.ID}}" 12 12 hx-target="#edittracks ul" 13 13 hx-swap="beforeend" 14 14 >
+2 -2
admin/templates/html/components/tracks/edittracks.html
··· 3 3 <h2>Editing: Tracks</h2> 4 4 <a id="add-track" 5 5 class="button new" 6 - href="/admin/release/{{.Release.ID}}/addtrack" 7 - hx-get="/admin/release/{{.Release.ID}}/addtrack" 6 + href="/admin/releases/{{.Release.ID}}/addtrack" 7 + hx-get="/admin/releases/{{.Release.ID}}/addtrack" 8 8 hx-target="body" 9 9 hx-swap="beforeend" 10 10 >Add</a>
+1 -1
admin/templates/html/edit-artist.html
··· 38 38 <div class="credit"> 39 39 <img src="{{.Release.Artwork}}" alt="" width="64" loading="lazy" class="release-artwork"> 40 40 <div class="credit-info"> 41 - <h3 class="credit-name"><a href="/admin/release/{{.Release.ID}}">{{.Release.Title}}</a></h3> 41 + <h3 class="credit-name"><a href="/admin/releases/{{.Release.ID}}">{{.Release.Title}}</a></h3> 42 42 <p class="credit-artists">{{.Release.PrintArtists true true}}</p> 43 43 <p class="artist-role"> 44 44 Role: {{.Role}}
+8 -8
admin/templates/html/edit-release.html
··· 101 101 <div class="card-header"> 102 102 <h2>Credits <small>({{len .Release.Credits}} total)</small></h2> 103 103 <a class="button edit" 104 - href="/admin/release/{{.Release.ID}}/editcredits" 105 - hx-get="/admin/release/{{.Release.ID}}/editcredits" 104 + href="/admin/releases/{{.Release.ID}}/editcredits" 105 + hx-get="/admin/releases/{{.Release.ID}}/editcredits" 106 106 hx-target="body" 107 107 hx-swap="beforeend" 108 108 >Edit</a> ··· 111 111 <div class="credit"> 112 112 <img src="{{.Artist.GetAvatar}}" alt="" width="64" loading="lazy" class="artist-avatar"> 113 113 <div class="credit-info"> 114 - <p class="artist-name"><a href="/admin/artist/{{.Artist.ID}}">{{.Artist.Name}}</a></p> 114 + <p class="artist-name"><a href="/admin/artists/{{.Artist.ID}}">{{.Artist.Name}}</a></p> 115 115 <p class="artist-role"> 116 116 {{.Role}} 117 117 {{if .Primary}} ··· 130 130 <div class="card-header"> 131 131 <h2>Links ({{len .Release.Links}})</h2> 132 132 <a class="button edit" 133 - href="/admin/release/{{.Release.ID}}/editlinks" 134 - hx-get="/admin/release/{{.Release.ID}}/editlinks" 133 + href="/admin/releases/{{.Release.ID}}/editlinks" 134 + hx-get="/admin/releases/{{.Release.ID}}/editlinks" 135 135 hx-target="body" 136 136 hx-swap="beforeend" 137 137 >Edit</a> ··· 147 147 <div class="card-header" id="tracks"> 148 148 <h2>Tracklist ({{len .Release.Tracks}})</h2> 149 149 <a class="button edit" 150 - href="/admin/release/{{.Release.ID}}/edittracks" 151 - hx-get="/admin/release/{{.Release.ID}}/edittracks" 150 + href="/admin/releases/{{.Release.ID}}/edittracks" 151 + hx-get="/admin/releases/{{.Release.ID}}/edittracks" 152 152 hx-target="body" 153 153 hx-swap="beforeend" 154 154 >Edit</a> ··· 157 157 <div class="track" data-id="{{$track.ID}}"> 158 158 <h2 class="track-title"> 159 159 <span class="track-number">{{.Add $i 1}}</span> 160 - <a href="/admin/track/{{$track.ID}}">{{$track.Title}}</a> 160 + <a href="/admin/tracks/{{$track.ID}}">{{$track.Title}}</a> 161 161 </h2> 162 162 163 163 <h3>Description</h3>
+2 -2
admin/templates/html/index.html
··· 32 32 {{range $Artist := .Artists}} 33 33 <div class="artist"> 34 34 <img src="{{$Artist.GetAvatar}}" alt="" width="64" loading="lazy" class="artist-avatar"> 35 - <a href="/admin/artist/{{$Artist.ID}}" class="artist-name">{{$Artist.Name}}</a> 35 + <a href="/admin/artists/{{$Artist.ID}}" class="artist-name">{{$Artist.Name}}</a> 36 36 </div> 37 37 {{end}} 38 38 </div> ··· 51 51 {{range $Track := .Tracks}} 52 52 <div class="track"> 53 53 <h2 class="track-title"> 54 - <a href="/admin/track/{{$Track.ID}}">{{$Track.Title}}</a> 54 + <a href="/admin/tracks/{{$Track.ID}}">{{$Track.Title}}</a> 55 55 </h2> 56 56 {{if $Track.Description}} 57 57 <p class="track-description">{{$Track.GetDescriptionHTML}}</p>
+6 -6
admin/templates/html/layout.html
··· 28 28 </div> 29 29 <hr> 30 30 <p class="section-label">music</p> 31 - <div class="nav-item{{if eq .Path "/releases"}} active{{end}}"> 32 - <a href="/admin/releases">releases</a> 31 + <div class="nav-item{{if hasPrefix .Path "/releases"}} active{{end}}"> 32 + <a href="/admin/releases/">releases</a> 33 33 </div> 34 - <div class="nav-item{{if eq .Path "/artists"}} active{{end}}"> 35 - <a href="/admin/artists">artists</a> 34 + <div class="nav-item{{if hasPrefix .Path "/artists"}} active{{end}}"> 35 + <a href="/admin/artists/">artists</a> 36 36 </div> 37 - <div class="nav-item{{if eq .Path "/tracks"}} active{{end}}"> 38 - <a href="/admin/tracks">tracks</a> 37 + <div class="nav-item{{if hasPrefix .Path "/tracks"}} active{{end}}"> 38 + <a href="/admin/tracks/">tracks</a> 39 39 </div> 40 40 {{end}} 41 41
+22
admin/templates/html/releases.html
··· 1 + {{define "head"}} 2 + <title>Releases - ari melody 💫</title> 3 + <link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon"> 4 + <link rel="stylesheet" href="/admin/static/index.css"> 5 + {{end}} 6 + 7 + {{define "content"}} 8 + <main> 9 + <div class="card-header"> 10 + <h1><a href="/admin/releases/">Releases</a></h1> 11 + <a class="button new" id="create-release">Create New</a> 12 + </div> 13 + {{range .Releases}} 14 + {{block "release" .}}{{end}} 15 + {{end}} 16 + {{if not .Releases}} 17 + <p>There are no releases.</p> 18 + {{end}} 19 + </main> 20 + 21 + <script type="module" src="/admin/static/admin.js"></script> 22 + {{end}}
+40
admin/templates/html/tracks.html
··· 1 + {{define "head"}} 2 + <title>Releases - ari melody 💫</title> 3 + <link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon"> 4 + <link rel="stylesheet" href="/admin/static/index.css"> 5 + {{end}} 6 + 7 + {{define "content"}} 8 + <main> 9 + <h1>Releases</h1> 10 + 11 + <div class="card-header"> 12 + <h2><a href="/admin/tracks/">Tracks</a></h2> 13 + <a class="button new" id="create-track">Create New</a> 14 + </div> 15 + <p><em>"Orphaned" tracks that have not yet been bound to a release.</em></p> 16 + <br> 17 + {{range $Track := .Tracks}} 18 + <div class="track"> 19 + <h2 class="track-title"> 20 + <a href="/admin/tracks/{{$Track.ID}}">{{$Track.Title}}</a> 21 + </h2> 22 + {{if $Track.Description}} 23 + <p class="track-description">{{$Track.GetDescriptionHTML}}</p> 24 + {{else}} 25 + <p class="track-description empty">No description provided.</p> 26 + {{end}} 27 + {{if $Track.Lyrics}} 28 + <p class="track-lyrics">{{$Track.GetLyricsHTML}}</p> 29 + {{else}} 30 + <p class="track-lyrics empty">There are no lyrics.</p> 31 + {{end}} 32 + </div> 33 + {{end}} 34 + {{if not .Artists}} 35 + <p>There are no artists.</p> 36 + {{end}} 37 + </main> 38 + 39 + <script type="module" src="/admin/static/admin.js"></script> 40 + {{end}}
+37 -11
admin/templates/templates.go
··· 1 1 package templates 2 2 3 3 import ( 4 - "arimelody-web/log" 5 - "fmt" 6 - "html/template" 7 - "strings" 8 - "time" 9 - _ "embed" 4 + "arimelody-web/log" 5 + _ "embed" 6 + "fmt" 7 + "html/template" 8 + "strings" 9 + "time" 10 10 ) 11 11 12 12 //go:embed "html/layout.html" ··· 35 35 36 36 //go:embed "html/edit-account.html" 37 37 var editAccountHTML string 38 - //go:embed "html/edit-artist.html" 39 - var editArtistHTML string 38 + 39 + //go:embed "html/releases.html" 40 + var releasesHTML string 41 + //go:embed "html/artists.html" 42 + var artistsHTML string 43 + //go:embed "html/tracks.html" 44 + var tracksHTML string 45 + 40 46 //go:embed "html/edit-release.html" 41 47 var editReleaseHTML string 48 + //go:embed "html/edit-artist.html" 49 + var editArtistHTML string 42 50 //go:embed "html/edit-track.html" 43 51 var editTrackHTML string 44 52 ··· 62 70 //go:embed "html/components/tracks/edittracks.html" 63 71 var componentEditTracksHTML string 64 72 65 - var BaseTemplate = template.Must(template.New("base").Parse( 66 - strings.Join([]string{ layoutHTML, prideflagHTML }, "\n"), 67 - )) 73 + var BaseTemplate = template.Must( 74 + template.New("base").Funcs( 75 + template.FuncMap{ 76 + "hasPrefix": func(s string, prefix string) bool { 77 + fmt.Printf("does \"%s\" start with \"%s\"?\n", s, prefix) 78 + return strings.HasPrefix(s, prefix) 79 + }, 80 + }, 81 + ).Parse(strings.Join([]string{ 82 + layoutHTML, 83 + prideflagHTML, 84 + }, "\n"))) 68 85 69 86 var IndexTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse( 70 87 strings.Join([]string{ ··· 107 124 return t.Format("02 Jan 2006, 15:04:05") 108 125 }, 109 126 }).Parse(logsHTML)) 127 + 128 + var ReleasesTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse( 129 + strings.Join([]string{ 130 + releasesHTML, 131 + componentReleaseListItemHTML, 132 + }, "\n"), 133 + )) 134 + var ArtistsTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(artistsHTML)) 135 + var TracksTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(tracksHTML)) 110 136 111 137 var EditReleaseTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(editReleaseHTML)) 112 138 var EditArtistTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(editArtistHTML))
+2 -2
admin/trackhttp.go
··· 10 10 "arimelody-web/model" 11 11 ) 12 12 13 - func serveTrack(app *model.AppState) http.Handler { 13 + func serveTracks(app *model.AppState) http.Handler { 14 14 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 15 - slices := strings.Split(r.URL.Path[1:], "/") 15 + slices := strings.Split(strings.TrimPrefix(r.URL.Path, "/tracks")[1:], "/") 16 16 id := slices[0] 17 17 track, err := controller.GetTrack(app.DB, id) 18 18 if err != nil {
+12 -14
view/index.go
··· 16 16 return 17 17 } 18 18 19 - type IndexData struct { 20 - TwitchStatus *model.TwitchStreamInfo 21 - } 22 - 23 - var err error 24 - var twitchStatus *model.TwitchStreamInfo = nil 25 - if app.Twitch != nil && len(app.Config.Twitch.Broadcaster) > 0 { 26 - twitchStatus, err = controller.GetTwitchStatus(app, app.Config.Twitch.Broadcaster) 27 - if err != nil { 28 - fmt.Fprintf(os.Stderr, "WARN: Failed to get Twitch status for %s: %v\n", app.Config.Twitch.Broadcaster, err) 29 - } 30 - } 31 - 32 19 if r.URL.Path == "/" || r.URL.Path == "/index.html" { 33 - err := templates.IndexTemplate.Execute(w, IndexData{ 20 + type IndexData struct { 21 + TwitchStatus *model.TwitchStreamInfo 22 + } 23 + var err error 24 + var twitchStatus *model.TwitchStreamInfo = nil 25 + if app.Twitch != nil && len(app.Config.Twitch.Broadcaster) > 0 { 26 + twitchStatus, err = controller.GetTwitchStatus(app, app.Config.Twitch.Broadcaster) 27 + if err != nil { 28 + fmt.Fprintf(os.Stderr, "WARN: Failed to get Twitch status for %s: %v\n", app.Config.Twitch.Broadcaster, err) 29 + } 30 + } 31 + err = templates.IndexTemplate.Execute(w, IndexData{ 34 32 TwitchStatus: twitchStatus, 35 33 }) 36 34 if err != nil {