diff --git a/.air.toml b/.air.toml index 4900634..ea2b4d5 100644 --- a/.air.toml +++ b/.air.toml @@ -7,7 +7,7 @@ tmp_dir = "tmp" bin = "./tmp/main" cmd = "go build -o ./tmp/main ." delay = 1000 - exclude_dir = ["admin\\static", "public", "uploads"] + exclude_dir = ["admin\\static", "public", "uploads", "test"] exclude_file = [] exclude_regex = ["_test.go"] exclude_unchanged = false diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0e34f0f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +**/.DS_Store +.git/ +.air.toml/ +.gitattributes +.gitignore +uploads/* +test/ +tmp/ +docker-compose.yml +Dockerfile +schema.sql diff --git a/.gitignore b/.gitignore index 77222bb..aa2f104 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ tmp/ test/ uploads/* +docker-compose-test.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..278f01a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM golang:1.22 AS build-stage + +WORKDIR /app + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +RUN CGO_ENABLED=0 GOOS=linux go build -o /arimelody-web + +# --- + +FROM build-stage AS build-release-stage + +WORKDIR /app + +COPY --from=build-stage /arimelody-web /arimelody-web +COPY . . + +EXPOSE 8080 + +CMD ["/arimelody-web"] diff --git a/admin/admin.go b/admin/admin.go index 1f1fc60..3c2aaae 100644 --- a/admin/admin.go +++ b/admin/admin.go @@ -3,9 +3,10 @@ package admin import ( "fmt" "math/rand" + "os" "time" - "arimelody.me/arimelody.me/global" + "arimelody-web/global" ) type ( @@ -28,9 +29,9 @@ var ADMIN_BYPASS = func() bool { }() var ADMIN_ID_DISCORD = func() string { - id := global.Args["discordAdmin"] + id := os.Getenv("DISCORD_ADMIN") if id == "" { - fmt.Printf("WARN: Discord admin ID (-discordAdmin) was not provided. Admin login will be unavailable.\n") + fmt.Printf("WARN: Discord admin ID (DISCORD_ADMIN) was not provided. Admin login will be unavailable.\n") } return id }() diff --git a/admin/artisthttp.go b/admin/artisthttp.go new file mode 100644 index 0000000..31a7c9c --- /dev/null +++ b/admin/artisthttp.go @@ -0,0 +1,47 @@ +package admin + +import ( + "fmt" + "net/http" + "strings" + + "arimelody-web/global" + "arimelody-web/music/model" + "arimelody-web/music/controller" +) + +func serveArtist() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + slices := strings.Split(r.URL.Path[1:], "/") + id := slices[0] + artist, err := music.GetArtist(global.DB, id) + if err != nil { + if artist == nil { + http.NotFound(w, r) + return + } + fmt.Printf("Error rendering admin artist page for %s: %s\n", id, err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + credits, err := music.GetArtistCredits(global.DB, artist.ID) + if err != nil { + fmt.Printf("Error rendering admin track page for %s: %s\n", id, err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + type Artist struct { + *model.Artist + Credits []*model.Credit + } + + err = pages["artist"].Execute(w, Artist{ Artist: artist, Credits: credits }) + if err != nil { + fmt.Printf("Error rendering admin track page for %s: %s\n", id, err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } + }) +} + diff --git a/admin/components/credits/editcredits.html b/admin/components/credits/editcredits.html index cf743e1..999740a 100644 --- a/admin/components/credits/editcredits.html +++ b/admin/components/credits/editcredits.html @@ -61,6 +61,7 @@ el.remove(); }); + el.draggable = true; el.addEventListener("dragstart", () => { el.classList.add("moving") }); el.addEventListener("dragend", () => { el.classList.remove("moving") }); } diff --git a/admin/http.go b/admin/http.go index 820eddf..35ff3f6 100644 --- a/admin/http.go +++ b/admin/http.go @@ -3,17 +3,16 @@ package admin import ( "context" "fmt" - "html/template" "net/http" "os" "path/filepath" "strings" "time" - "arimelody.me/arimelody.me/discord" - "arimelody.me/arimelody.me/global" - musicModel "arimelody.me/arimelody.me/music/model" - musicDB "arimelody.me/arimelody.me/music/controller" + "arimelody-web/discord" + "arimelody-web/global" + musicDB "arimelody-web/music/controller" + musicModel "arimelody-web/music/model" ) type loginData struct { @@ -28,6 +27,7 @@ func Handler() http.Handler { mux.Handle("/logout", MustAuthorise(LogoutHandler())) mux.Handle("/static/", http.StripPrefix("/static", staticHandler())) mux.Handle("/release/", MustAuthorise(http.StripPrefix("/release", serveRelease()))) + mux.Handle("/artist/", MustAuthorise(http.StripPrefix("/artist", serveArtist()))) mux.Handle("/track/", MustAuthorise(http.StripPrefix("/track", serveTrack()))) mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { @@ -41,30 +41,12 @@ func Handler() http.Handler { return } - type ( - IndexData struct { - Releases []musicModel.FullRelease - Artists []*musicModel.Artist - Tracks []musicModel.DisplayTrack - } - ) - - dbReleases, err := musicDB.GetAllReleases(global.DB) + releases, err := musicDB.GetAllReleases(global.DB, false, 0, true) if err != nil { fmt.Printf("FATAL: Failed to pull releases: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - releases := []musicModel.FullRelease{} - for _, release := range dbReleases { - fullRelease, err := musicDB.GetFullRelease(global.DB, release.ID) - if err != nil { - fmt.Printf("FATAL: Failed to pull full release data for %s: %s\n", release.ID, err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - releases = append(releases, *fullRelease) - } artists, err := musicDB.GetAllArtists(global.DB) if err != nil { @@ -73,19 +55,17 @@ func Handler() http.Handler { return } - dbTracks, err := musicDB.GetOrphanTracks(global.DB) + tracks, err := musicDB.GetOrphanTracks(global.DB) if err != nil { fmt.Printf("FATAL: Failed to pull orphan tracks: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - var tracks = []musicModel.DisplayTrack{} - for _, track := range dbTracks { - tracks = append(tracks, musicModel.DisplayTrack{ - Track: track, - Lyrics: template.HTML(strings.Replace(track.Lyrics, "\n", "
", -1)), - }) + type IndexData struct { + Releases []*musicModel.Release + Artists []*musicModel.Artist + Tracks []*musicModel.Track } err = pages["index"].Execute(w, IndexData{ @@ -125,8 +105,9 @@ func GetSession(r *http.Request) *Session { // is the session token in context? var ctx_session = r.Context().Value("session") if ctx_session != nil { - token = ctx_session.(string) + token = ctx_session.(*Session).Token } + // okay, is it in the auth header? if token == "" { if strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") { @@ -168,11 +149,15 @@ func GetSession(r *http.Request) *Session { func LoginHandler() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if discord.CREDENTIALS_PROVIDED && ADMIN_ID_DISCORD == "" { + if !discord.CREDENTIALS_PROVIDED || ADMIN_ID_DISCORD == "" { http.Error(w, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable) return } + fmt.Println(discord.CLIENT_ID) + fmt.Println(discord.API_ENDPOINT) + fmt.Println(discord.REDIRECT_URI) + code := r.URL.Query().Get("code") if code == "" { @@ -209,8 +194,9 @@ func LoginHandler() http.Handler { cookie.Name = "token" cookie.Value = session.Token cookie.Expires = time.Now().Add(24 * time.Hour) - // TODO: uncomment this probably that might be nice i think - // cookie.Secure = true + if strings.HasPrefix(global.HTTP_DOMAIN, "https") { + cookie.Secure = true + } cookie.HttpOnly = true cookie.Path = "/" http.SetCookie(w, &cookie) diff --git a/admin/releasehttp.go b/admin/releasehttp.go index c7b6528..bacaf01 100644 --- a/admin/releasehttp.go +++ b/admin/releasehttp.go @@ -5,9 +5,9 @@ import ( "net/http" "strings" - "arimelody.me/arimelody.me/global" - db "arimelody.me/arimelody.me/music/controller" - "arimelody.me/arimelody.me/music/model" + "arimelody-web/global" + db "arimelody-web/music/controller" + "arimelody-web/music/model" ) func serveRelease() http.Handler { @@ -15,13 +15,13 @@ func serveRelease() http.Handler { slices := strings.Split(r.URL.Path[1:], "/") releaseID := slices[0] - release, err := db.GetFullRelease(global.DB, releaseID) + release, err := db.GetRelease(global.DB, releaseID, true) if err != nil { if strings.Contains(err.Error(), "no rows") { http.NotFound(w, r) return } - fmt.Printf("FATAL: Failed to pull full release data for %s: %s\n", release.ID, err) + fmt.Printf("FATAL: Failed to pull full release data for %s: %s\n", releaseID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } @@ -68,7 +68,7 @@ func serveRelease() http.Handler { }) } -func serveEditCredits(release *model.FullRelease) http.Handler { +func serveEditCredits(release *model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") err := components["editcredits"].Execute(w, release) @@ -79,9 +79,9 @@ func serveEditCredits(release *model.FullRelease) http.Handler { }) } -func serveAddCredit(release *model.FullRelease) http.Handler { +func serveAddCredit(release *model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - artists, err := db.GetArtistsNotOnRelease(global.DB, release.Release.ID) + artists, err := db.GetArtistsNotOnRelease(global.DB, release.ID) if err != nil { fmt.Printf("FATAL: Failed to pull artists not on %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -128,7 +128,7 @@ func serveNewCredit() http.Handler { }) } -func serveEditLinks(release *model.FullRelease) http.Handler { +func serveEditLinks(release *model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") err := components["editlinks"].Execute(w, release) @@ -139,7 +139,7 @@ func serveEditLinks(release *model.FullRelease) http.Handler { }) } -func serveEditTracks(release *model.FullRelease) http.Handler { +func serveEditTracks(release *model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") err := components["edittracks"].Execute(w, release) @@ -150,9 +150,9 @@ func serveEditTracks(release *model.FullRelease) http.Handler { }) } -func serveAddTrack(release *model.FullRelease) http.Handler { +func serveAddTrack(release *model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - tracks, err := db.GetTracksNotOnRelease(global.DB, release.Release.ID) + tracks, err := db.GetTracksNotOnRelease(global.DB, release.ID) if err != nil { fmt.Printf("FATAL: Failed to pull tracks not on %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) diff --git a/admin/static/edit-artist.css b/admin/static/edit-artist.css new file mode 100644 index 0000000..b96d2ce --- /dev/null +++ b/admin/static/edit-artist.css @@ -0,0 +1,151 @@ +h1 { + margin: 0 0 1em 0; +} + +#artist { + margin-bottom: 1em; + padding: 1.5em; + display: flex; + flex-direction: row; + gap: 1.2em; + + border-radius: .5em; + background: #f8f8f8f8; + border: 1px solid #808080; +} + +.artist-avatar { + width: 200px; + text-align: center; +} +.artist-avatar img { + width: 100%; + aspect-ratio: 1; +} +.artist-avatar img:hover { + outline: 1px solid #808080; + cursor: pointer; +} +.artist-avatar #remove-avatar { + padding: .3em .4em; +} + +.artist-info { + margin: -1em 0 0 0; + flex-grow: 1; + display: flex; + flex-direction: column; +} + +.attribute-header { + margin: 1em 0 .2em 0; + opacity: .5; +} + +.artist-name { + margin: 0; +} + +input[type="text"] { + width: calc(100% - .4em); + padding: .1em .2em; + font-size: inherit; + font-family: inherit; + font-weight: inherit; + color: inherit; + background: #ffffff; + border: 1px solid transparent; + border-radius: 4px; + outline: none; +} +input[type="text"]:hover { + border-color: #80808080; +} +input[type="text"]:active, +input[type="text"]:focus { + border-color: #808080; +} + +button, .button { + padding: .5em .8em; + font-family: inherit; + font-size: inherit; + border-radius: .5em; + border: 1px solid #a0a0a0; + background: #f0f0f0; + color: inherit; +} +button:hover, .button:hover { + background: #fff; + border-color: #d0d0d0; +} +button:active, .button:active { + background: #d0d0d0; + border-color: #808080; +} + +button { + color: inherit; +} +button.save { + background: #6fd7ff; + border-color: #6f9eb0; +} +button.delete { + background: #ff7171; + border-color: #7d3535; +} +button:hover { + background: #fff; + border-color: #d0d0d0; +} +button:active { + background: #d0d0d0; + border-color: #808080; +} +button[disabled] { + background: #d0d0d0 !important; + border-color: #808080 !important; + opacity: .5; + cursor: not-allowed !important; +} + +a.delete { + color: #d22828; +} + +.artist-actions { + margin-top: auto; + display: flex; + gap: .5em; + flex-direction: row; + justify-content: right; +} + +.card-title a.button { + text-decoration: none; +} + +.credit { + margin: 1em 0; + padding: .5em; + display: flex; + flex-direction: row; + gap: 1em; + align-items: center; + background: #f8f8f8; + border-radius: 8px; + border: 1px solid #808080; +} + +.release-artwork { + width: 64px; + height: min-content; + border-radius: 4px; +} + +.credit-info h3, +.credit-info p { + margin: 0; + font-size: .9em; +} diff --git a/admin/static/edit-artist.js b/admin/static/edit-artist.js new file mode 100644 index 0000000..8f1bb2b --- /dev/null +++ b/admin/static/edit-artist.js @@ -0,0 +1,79 @@ +const artistID = document.getElementById("artist").dataset.id; +const nameInput = document.getElementById("name"); +const avatarImg = document.getElementById("avatar"); +const removeAvatarBtn = document.getElementById("remove-avatar"); +const avatarInput = document.getElementById("avatar-file"); +const websiteInput = document.getElementById("website"); +const saveBtn = document.getElementById("save"); +const deleteBtn = document.getElementById("delete"); + +saveBtn.addEventListener("click", () => { + fetch("/api/v1/artist/" + artistID, { + method: "PUT", + body: JSON.stringify({ + name: nameInput.value, + website: websiteInput.value, + avatar: avatarImg.src, + }), + headers: { "Content-Type": "application/json" } + }).then(res => { + if (!res.ok) { + res.text().then(error => { + console.error(error); + alert("Failed to update release: " + error); + }); + return; + } + + location = location; + }); +}); + +deleteBtn.addEventListener("click", () => { + if (artistID != prompt( + "You are about to permanently delete " + artistID + ". " + + "This action is irreversible. " + + "Please enter \"" + artistID + "\" to continue.")) return; + fetch("/api/v1/artist/" + artistID, { + method: "DELETE", + }).then(res => { + if (!res.ok) { + res.text().then(error => { + console.error(error); + alert("Failed to delete release: " + error); + }); + return; + } + + location = "/admin"; + }); +}); + +[nameInput, websiteInput].forEach(input => { + input.addEventListener("change", () => { + saveBtn.disabled = false; + }); + input.addEventListener("keypress", () => { + saveBtn.disabled = false; + }); +}); + +avatarImg.addEventListener("click", () => { + avatarInput.addEventListener("change", () => { + if (avatarInput.files.length > 0) { + const reader = new FileReader(); + reader.onload = e => { + const data = e.target.result; + avatarImg.src = data; + saveBtn.disabled = false; + }; + reader.readAsDataURL(avatarInput.files[0]); + } + }); + avatarInput.click(); +}); + +removeAvatarBtn.addEventListener("click", () => { + avatarImg.src = "/img/default-avatar.png" + saveBtn.disabled = false; +}); diff --git a/admin/static/edit-release.js b/admin/static/edit-release.js index 2de1c66..11d21f0 100644 --- a/admin/static/edit-release.js +++ b/admin/static/edit-release.js @@ -1,5 +1,3 @@ -import Stateful from "/script/silver.min.js" - const releaseID = document.getElementById("release").dataset.id; const titleInput = document.getElementById("title"); const artworkImg = document.getElementById("artwork"); diff --git a/admin/static/edit-track.css b/admin/static/edit-track.css index ed359ae..6e87397 100644 --- a/admin/static/edit-track.css +++ b/admin/static/edit-track.css @@ -6,7 +6,7 @@ h1 { #track { margin-bottom: 1em; - padding: 1.5em; + padding: .5em 1.5em 1.5em 1.5em; display: flex; flex-direction: row; gap: 1.2em; @@ -34,7 +34,7 @@ h1 { } #title { - width: 100%; + width: calc(100% - .4em); padding: .1em .2em; } diff --git a/admin/static/index.js b/admin/static/index.js index 8eec511..e251802 100644 --- a/admin/static/index.js +++ b/admin/static/index.js @@ -1,4 +1,5 @@ const newReleaseBtn = document.getElementById("create-release"); +const newArtistBtn = document.getElementById("create-artist"); const newTrackBtn = document.getElementById("create-track"); newReleaseBtn.addEventListener("click", event => { @@ -24,6 +25,30 @@ newReleaseBtn.addEventListener("click", event => { }); }); +newArtistBtn.addEventListener("click", event => { + event.preventDefault(); + const id = prompt("Enter an ID for this artist:"); + if (id == null || id == "") return; + + fetch("/api/v1/artist", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({id}) + }).then(res => { + res.text().then(text => { + if (res.ok) { + location = "/admin/artist/" + id; + } else { + alert("Request failed: " + text); + console.error(text); + } + }) + }).catch(err => { + alert("Failed to create artist. Check the console for details."); + console.error(err); + }); +}); + newTrackBtn.addEventListener("click", event => { event.preventDefault(); const title = prompt("Enter an title for this track:"); @@ -43,7 +68,7 @@ newTrackBtn.addEventListener("click", event => { } }) }).catch(err => { - alert("Failed to create release. Check the console for details."); + alert("Failed to create track. Check the console for details."); console.error(err); }); }); diff --git a/admin/templates.go b/admin/templates.go index af69aba..b7aaf9e 100644 --- a/admin/templates.go +++ b/admin/templates.go @@ -29,6 +29,11 @@ var pages = map[string]*template.Template{ filepath.Join("views", "prideflag.html"), filepath.Join("admin", "views", "edit-release.html"), )), + "artist": template.Must(template.ParseFiles( + filepath.Join("admin", "views", "layout.html"), + filepath.Join("views", "prideflag.html"), + filepath.Join("admin", "views", "edit-artist.html"), + )), "track": template.Must(template.ParseFiles( filepath.Join("admin", "views", "layout.html"), filepath.Join("views", "prideflag.html"), diff --git a/admin/trackhttp.go b/admin/trackhttp.go index bd32636..732d34a 100644 --- a/admin/trackhttp.go +++ b/admin/trackhttp.go @@ -5,9 +5,9 @@ import ( "net/http" "strings" - "arimelody.me/arimelody.me/global" - "arimelody.me/arimelody.me/music/model" - "arimelody.me/arimelody.me/music/controller" + "arimelody-web/global" + "arimelody-web/music/model" + "arimelody-web/music/controller" ) func serveTrack() http.Handler { @@ -25,26 +25,16 @@ func serveTrack() http.Handler { return } - dbReleases, err := music.GetTrackReleases(global.DB, track.ID) + releases, err := music.GetTrackReleases(global.DB, track.ID, true) if err != nil { - fmt.Printf("Error rendering admin track page for %s: %s\n", id, err) + fmt.Printf("FATAL: Failed to pull releases for %s: %s\n", id, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - releases := []model.FullRelease{} - for _, release := range dbReleases { - fullRelease, err := music.GetFullRelease(global.DB, release.ID) - if err != nil { - fmt.Printf("FATAL: Failed to pull full release data for %s: %s\n", release.ID, err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - releases = append(releases, *fullRelease) - } type Track struct { *model.Track - Releases []model.FullRelease + Releases []*model.Release } err = pages["track"].Execute(w, Track{ Track: track, Releases: releases }) diff --git a/admin/views/edit-artist.html b/admin/views/edit-artist.html new file mode 100644 index 0000000..b54361a --- /dev/null +++ b/admin/views/edit-artist.html @@ -0,0 +1,72 @@ +{{define "head"}} +Editing {{.Name}} - ari melody 💫 + + +{{end}} + +{{define "content"}} +
+

Editing Artist

+ +
+
+ + + +
+
+

Name

+

+ +

+ +

Website

+ + +
+ +
+
+
+ +
+

Featured in

+
+
+ {{if .Credits}} + {{range .Credits}} +
+ +
+

{{.Release.Title}}

+

{{.Release.PrintArtists true true}}

+

+ Role: {{.Role}} + {{if .Primary}} + (Primary) + {{end}} +

+
+
+ {{end}} + {{else}} +

This artist has no credits.

+ {{end}} +
+ +
+

Danger Zone

+
+
+

+ Clicking the button below will delete this artist. + This action is irreversible. + You will be prompted to confirm this decision. +

+ +
+ +
+ + +{{end}} diff --git a/admin/views/edit-release.html b/admin/views/edit-release.html index 0c31d2b..d106bb5 100644 --- a/admin/views/edit-release.html +++ b/admin/views/edit-release.html @@ -112,7 +112,7 @@
-

{{.Artist.Name}}

+

{{.Artist.Name}}

{{.Role}} {{if .Primary}} @@ -152,23 +152,23 @@ >Edit

- {{range .Tracks}} -
+ {{range $i, $track := .Tracks}} +

- {{.Number}} - {{.Title}} + {{.Add $i 1}} + {{$track.Title}}

Description

- {{if .Description}} -

{{.Description}}

+ {{if $track.Description}} +

{{$track.GetDescriptionHTML}}

{{else}}

No description provided.

{{end}}

Lyrics

- {{if .Lyrics}} -

{{.Lyrics}}

+ {{if $track.Lyrics}} +

{{$track.GetLyricsHTML}}

{{else}}

There are no lyrics.

{{end}} diff --git a/admin/views/index.html b/admin/views/index.html index f88576d..8f42e0e 100644 --- a/admin/views/index.html +++ b/admin/views/index.html @@ -22,13 +22,13 @@

Artists

- Create New + Create New
{{range $Artist := .Artists}} {{end}} {{if not .Artists}} @@ -49,12 +49,12 @@ {{$Track.Title}} {{if $Track.Description}} -

{{$Track.Description}}

+

{{$Track.GetDescriptionHTML}}

{{else}}

No description provided.

{{end}} {{if $Track.Lyrics}} -

{{$Track.Lyrics}}

+

{{$Track.GetLyricsHTML}}

{{else}}

There are no lyrics.

{{end}} diff --git a/api/api.go b/api/api.go index 443904f..699cf1c 100644 --- a/api/api.go +++ b/api/api.go @@ -5,10 +5,10 @@ import ( "net/http" "strings" - "arimelody.me/arimelody.me/admin" - "arimelody.me/arimelody.me/global" - "arimelody.me/arimelody.me/music/model" - music "arimelody.me/arimelody.me/music/view" + "arimelody-web/admin" + "arimelody-web/global" + music "arimelody-web/music/controller" + musicView "arimelody-web/music/view" ) func Handler() http.Handler { @@ -18,8 +18,7 @@ func Handler() http.Handler { mux.Handle("/v1/artist/", http.StripPrefix("/v1/artist", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var artistID = strings.Split(r.URL.Path[1:], "/")[0] - var artist model.Artist - err := global.DB.Get(&artist, "SELECT * FROM artist WHERE id=$1", artistID) + artist, err := music.GetArtist(global.DB, artistID) if err != nil { if strings.Contains(err.Error(), "no rows") { http.NotFound(w, r) @@ -61,8 +60,7 @@ func Handler() http.Handler { mux.Handle("/v1/music/", http.StripPrefix("/v1/music", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var releaseID = strings.Split(r.URL.Path[1:], "/")[0] - var release model.Release - err := global.DB.Get(&release, "SELECT * FROM musicrelease WHERE id=$1", releaseID) + release, err := music.GetRelease(global.DB, releaseID, true) if err != nil { if strings.Contains(err.Error(), "no rows") { http.NotFound(w, r) @@ -76,7 +74,7 @@ func Handler() http.Handler { switch r.Method { case http.MethodGet: // GET /api/v1/music/{id} - music.ServeRelease(release).ServeHTTP(w, r) + musicView.ServeRelease(release).ServeHTTP(w, r) case http.MethodPut: // PUT /api/v1/music/{id} (admin) admin.MustAuthorise(UpdateRelease(release)).ServeHTTP(w, r) @@ -104,8 +102,7 @@ func Handler() http.Handler { mux.Handle("/v1/track/", http.StripPrefix("/v1/track", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var trackID = strings.Split(r.URL.Path[1:], "/")[0] - var track model.Track - err := global.DB.Get(&track, "SELECT * FROM musictrack WHERE id=$1", trackID) + track, err := music.GetTrack(global.DB, trackID) if err != nil { if strings.Contains(err.Error(), "no rows") { http.NotFound(w, r) diff --git a/api/artist.go b/api/artist.go index 164e535..2d5f5e6 100644 --- a/api/artist.go +++ b/api/artist.go @@ -3,21 +3,18 @@ package api import ( "encoding/json" "fmt" + "io/fs" "net/http" + "os" + "path/filepath" "strings" - "arimelody.me/arimelody.me/global" - "arimelody.me/arimelody.me/music/model" - db "arimelody.me/arimelody.me/music/controller" + "arimelody-web/global" + db "arimelody-web/music/controller" + music "arimelody-web/music/controller" + "arimelody-web/music/model" ) -type artistJSON struct { - ID string `json:"id"` - Name *string `json:"name"` - Website *string `json:"website"` - Avatar *string `json:"avatar"` -} - func ServeAllArtists() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var artists = []*model.Artist{} @@ -36,7 +33,7 @@ func ServeAllArtists() http.Handler { }) } -func ServeArtist(artist model.Artist) http.Handler { +func ServeArtist(artist *model.Artist) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { type ( creditJSON struct { @@ -44,7 +41,7 @@ func ServeArtist(artist model.Artist) http.Handler { Primary bool `json:"primary"` } artistJSON struct { - model.Artist + *model.Artist Credits map[string]creditJSON `json:"credits"` } ) @@ -78,39 +75,23 @@ func ServeArtist(artist model.Artist) http.Handler { func CreateArtist() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var data artistJSON - err := json.NewDecoder(r.Body).Decode(&data) + var artist model.Artist + err := json.NewDecoder(r.Body).Decode(&artist) if err != nil { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } - if data.ID == "" { + if artist.ID == "" { http.Error(w, "Artist ID cannot be blank\n", http.StatusBadRequest) return } - if data.Name == nil || *data.Name == "" { - http.Error(w, "Artist name cannot be blank\n", http.StatusBadRequest) - return - } + if artist.Name == "" { artist.Name = artist.ID } - var artist = model.Artist{ - ID: data.ID, - Name: *data.Name, - Website: *data.Website, - Avatar: *data.Avatar, - } - - _, err = global.DB.Exec( - "INSERT INTO artist (id, name, website, avatar) "+ - "VALUES ($1, $2, $3, $4)", - artist.ID, - artist.Name, - artist.Website, - artist.Avatar) + err = music.CreateArtist(global.DB, &artist) if err != nil { if strings.Contains(err.Error(), "duplicate key") { - http.Error(w, fmt.Sprintf("Artist %s already exists\n", data.ID), http.StatusBadRequest) + http.Error(w, fmt.Sprintf("Artist %s already exists\n", artist.ID), http.StatusBadRequest) return } fmt.Printf("FATAL: Failed to create artist %s: %s\n", artist.ID, err) @@ -122,43 +103,59 @@ func CreateArtist() http.Handler { }) } -func UpdateArtist(artist model.Artist) http.Handler { +func UpdateArtist(artist *model.Artist) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var data artistJSON - err := json.NewDecoder(r.Body).Decode(&data) + err := json.NewDecoder(r.Body).Decode(&artist) if err != nil { fmt.Printf("FATAL: Failed to update artist: %s\n", err) http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } - if data.ID != "" { artist.ID = data.ID } - if data.Name != nil { artist.Name = *data.Name } - if data.Website != nil { artist.Website = *data.Website } - if data.Avatar != nil { artist.Avatar = *data.Avatar } + if artist.Avatar == "" { + artist.Avatar = "/img/default-avatar.png" + } else { + if strings.Contains(artist.Avatar, ";base64,") { + var artworkDirectory = filepath.Join("uploads", "avatar") + filename, err := HandleImageUpload(&artist.Avatar, artworkDirectory, artist.ID) - _, err = global.DB.Exec( - "UPDATE artist "+ - "SET name=$2, website=$3, avatar=$4 "+ - "WHERE id=$1", - artist.ID, - artist.Name, - artist.Website, - artist.Avatar) + // clean up files with this ID and different extensions + err = filepath.Walk(artworkDirectory, func(path string, info fs.FileInfo, err error) error { + if path == filepath.Join(artworkDirectory, filename) { return nil } + + withoutExt := strings.TrimSuffix(path, filepath.Ext(path)) + if withoutExt != filepath.Join(artworkDirectory, artist.ID) { return nil } + + return os.Remove(path) + }) + if err != nil { + fmt.Printf("WARN: Error while cleaning up avatar files: %s\n", err) + } + + artist.Avatar = fmt.Sprintf("/uploads/avatar/%s", filename) + } + } + + err = music.UpdateArtist(global.DB, artist) if err != nil { + if strings.Contains(err.Error(), "no rows") { + http.NotFound(w, r) + return + } fmt.Printf("FATAL: Failed to update artist %s: %s\n", artist.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) } -func DeleteArtist(artist model.Artist) http.Handler { +func DeleteArtist(artist *model.Artist) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := global.DB.Exec( - "DELETE FROM artist "+ - "WHERE id=$1", - artist.ID) + err := music.DeleteArtist(global.DB, artist.ID) if err != nil { + if strings.Contains(err.Error(), "no rows") { + http.NotFound(w, r) + return + } fmt.Printf("FATAL: Failed to delete artist %s: %s\n", artist.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } diff --git a/api/release.go b/api/release.go index fb1ea54..763383f 100644 --- a/api/release.go +++ b/api/release.go @@ -10,35 +10,44 @@ import ( "strings" "time" - "arimelody.me/arimelody.me/admin" - "arimelody.me/arimelody.me/global" - music "arimelody.me/arimelody.me/music/controller" - "arimelody.me/arimelody.me/music/model" + "arimelody-web/admin" + "arimelody-web/global" + music "arimelody-web/music/controller" + "arimelody-web/music/model" ) func ServeCatalog() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - - releases := []*model.Release{} - err := global.DB.Select(&releases, "SELECT * FROM musicrelease ORDER BY release_date DESC") + releases, err := music.GetAllReleases(global.DB, false, 0, true) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - catalog := []model.ReleaseShorthand{} + type Release struct { + ID string `json:"id"` + Title string `json:"title"` + ReleaseType model.ReleaseType `json:"type" db:"type"` + ReleaseDate time.Time `json:"releaseDate" db:"release_date"` + Artwork string `json:"artwork"` + Buylink string `json:"buylink"` + Copyright string `json:"copyright" db:"copyright"` + } + + catalog := []Release{} authorised := admin.GetSession(r) != nil for _, release := range releases { if !release.Visible && !authorised { continue } - catalog = append(catalog, model.ReleaseShorthand{ + catalog = append(catalog, Release{ ID: release.ID, Title: release.Title, ReleaseType: release.ReleaseType, ReleaseDate: release.ReleaseDate, Artwork: release.Artwork, Buylink: release.Buylink, + Copyright: release.Copyright, }) } @@ -85,7 +94,7 @@ func CreateRelease() http.Handler { http.Error(w, fmt.Sprintf("Release %s already exists\n", release.ID), http.StatusBadRequest) return } - fmt.Printf("Failed to create release %s: %s\n", release.ID, err) + fmt.Printf("FATAL: Failed to create release %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } @@ -100,7 +109,7 @@ func CreateRelease() http.Handler { }) } -func UpdateRelease(release model.Release) http.Handler { +func UpdateRelease(release *model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { http.NotFound(w, r) @@ -157,15 +166,19 @@ func UpdateRelease(release model.Release) http.Handler { } } - err = music.UpdateRelease(global.DB, &release) + err = music.UpdateRelease(global.DB, release) if err != nil { + if strings.Contains(err.Error(), "no rows") { + http.NotFound(w, r) + return + } fmt.Printf("FATAL: Failed to update release %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) } -func UpdateReleaseTracks(release model.Release) http.Handler { +func UpdateReleaseTracks(release *model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var trackIDs = []string{} err := json.NewDecoder(r.Body).Decode(&trackIDs) @@ -174,15 +187,19 @@ func UpdateReleaseTracks(release model.Release) http.Handler { return } - err = music.UpdateReleaseTracks(global.DB, &release, trackIDs) + err = music.UpdateReleaseTracks(global.DB, release.ID, trackIDs) if err != nil { - fmt.Printf("Failed to update tracks for %s: %s\n", release.ID, err) + if strings.Contains(err.Error(), "no rows") { + http.NotFound(w, r) + return + } + fmt.Printf("FATAL: Failed to update tracks for %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) } -func UpdateReleaseCredits(release model.Release) http.Handler { +func UpdateReleaseCredits(release *model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { type creditJSON struct { Artist string @@ -196,9 +213,9 @@ func UpdateReleaseCredits(release model.Release) http.Handler { return } - var credits []model.Credit + var credits []*model.Credit for _, credit := range data { - credits = append(credits, model.Credit{ + credits = append(credits, &model.Credit{ Artist: model.Artist{ ID: credit.Artist, }, @@ -207,19 +224,23 @@ func UpdateReleaseCredits(release model.Release) http.Handler { }) } - err = music.UpdateReleaseCredits(global.DB, &release, credits) + err = music.UpdateReleaseCredits(global.DB, release.ID, credits) if err != nil { if strings.Contains(err.Error(), "duplicate key") { http.Error(w, "Artists may only be credited once\n", http.StatusBadRequest) return } - fmt.Printf("Failed to update links for %s: %s\n", release.ID, err) + if strings.Contains(err.Error(), "no rows") { + http.NotFound(w, r) + return + } + fmt.Printf("FATAL: Failed to update links for %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) } -func UpdateReleaseLinks(release model.Release) http.Handler { +func UpdateReleaseLinks(release *model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPut { http.NotFound(w, r) @@ -233,30 +254,27 @@ func UpdateReleaseLinks(release model.Release) http.Handler { return } - tx := global.DB.MustBegin() - tx.MustExec("DELETE FROM musiclink WHERE release=$1", release.ID) - for _, link := range links { - tx.MustExec( - "INSERT INTO musiclink "+ - "(release, name, url) "+ - "VALUES ($1, $2, $3)", - release.ID, - link.Name, - link.URL) - } - err = tx.Commit() + err = music.UpdateReleaseLinks(global.DB, release.ID, links) if err != nil { - fmt.Printf("Failed to update links for %s: %s\n", release.ID, err) + if strings.Contains(err.Error(), "no rows") { + http.NotFound(w, r) + return + } + fmt.Printf("FATAL: Failed to update links for %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) } -func DeleteRelease(release model.Release) http.Handler { +func DeleteRelease(release *model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := global.DB.Exec("DELETE FROM musicrelease WHERE id=$1", release.ID) + err := music.DeleteRelease(global.DB, release.ID) if err != nil { - fmt.Printf("Failed to delete release %s: %s\n", release.ID, err) + if strings.Contains(err.Error(), "no rows") { + http.NotFound(w, r) + return + } + fmt.Printf("FATAL: Failed to delete release %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) diff --git a/api/track.go b/api/track.go index 75080c5..fd4018b 100644 --- a/api/track.go +++ b/api/track.go @@ -5,15 +5,15 @@ import ( "fmt" "net/http" - "arimelody.me/arimelody.me/global" - music "arimelody.me/arimelody.me/music/controller" - "arimelody.me/arimelody.me/music/model" + "arimelody-web/global" + music "arimelody-web/music/controller" + "arimelody-web/music/model" ) type ( Track struct { - model.Track - Releases []model.ReleaseShorthand + *model.Track + Releases []string `json:"releases"` } ) @@ -48,13 +48,18 @@ func ServeAllTracks() http.Handler { }) } -func ServeTrack(track model.Track) http.Handler { +func ServeTrack(track *model.Track) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - releases, err := music.GetTrackReleases(global.DB, track.ID) + dbReleases, err := music.GetTrackReleases(global.DB, track.ID, false) if err != nil { fmt.Printf("FATAL: Failed to pull track releases for %s from DB: %s\n", track.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } + + releases := []string{} + for _, release := range dbReleases { + releases = append(releases, release.ID) + } w.Header().Add("Content-Type", "application/json") err = json.NewEncoder(w).Encode(Track{ track, releases }) @@ -97,7 +102,7 @@ func CreateTrack() http.Handler { }) } -func UpdateTrack(track model.Track) http.Handler { +func UpdateTrack(track *model.Track) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPut || r.URL.Path == "/" { http.NotFound(w, r) @@ -115,7 +120,7 @@ func UpdateTrack(track model.Track) http.Handler { return } - err = music.UpdateTrack(global.DB, &track) + err = music.UpdateTrack(global.DB, track) if err != nil { fmt.Printf("Failed to update track %s: %s\n", track.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -130,7 +135,7 @@ func UpdateTrack(track model.Track) http.Handler { }) } -func DeleteTrack(track model.Track) http.Handler { +func DeleteTrack(track *model.Track) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodDelete || r.URL.Path == "/" { http.NotFound(w, r) diff --git a/api/uploads.go b/api/uploads.go index 23479b7..f2fe297 100644 --- a/api/uploads.go +++ b/api/uploads.go @@ -35,6 +35,8 @@ func HandleImageUpload(data *string, directory string, filename string) (string, } defer file.Close() + // TODO: generate compressed versions of image (512x512?) + buffer := bufio.NewWriter(file) _, err = buffer.Write(imageData) if err != nil { diff --git a/discord/discord.go b/discord/discord.go index e749daa..c3b3cec 100644 --- a/discord/discord.go +++ b/discord/discord.go @@ -6,26 +6,27 @@ import ( "fmt" "net/http" "net/url" + "os" "strings" - "arimelody.me/arimelody.me/global" + "arimelody-web/global" ) const API_ENDPOINT = "https://discord.com/api/v10" var CREDENTIALS_PROVIDED = true var CLIENT_ID = func() string { - id := global.Args["discordClient"] + id := os.Getenv("DISCORD_CLIENT") if id == "" { - fmt.Printf("WARN: Discord client ID (-discordClient) was not provided. Admin login will be unavailable.\n") + fmt.Printf("WARN: Discord client ID (DISCORD_CLIENT) was not provided. Admin login will be unavailable.\n") CREDENTIALS_PROVIDED = false } return id }() var CLIENT_SECRET = func() string { - secret := global.Args["discordSecret"] - if secret== "" { - fmt.Printf("WARN: Discord secret (-discordSecret) was not provided. Admin login will be unavailable.\n") + secret := os.Getenv("DISCORD_SECRET") + if secret == "" { + fmt.Printf("WARN: Discord secret (DISCORD_SECRET) was not provided. Admin login will be unavailable.\n") CREDENTIALS_PROVIDED = false } return secret @@ -107,7 +108,6 @@ func GetDiscordUserFromAuth(token string) (DiscordUser, error) { } auth_info := AuthInfoResponse{} - err = json.NewDecoder(res.Body).Decode(&auth_info) if err != nil { return DiscordUser{}, errors.New(fmt.Sprintf("Failed to parse auth information from discord: %s\n", err)) diff --git a/docker-compose-db.yml b/docker-compose-db.yml deleted file mode 100644 index 4722286..0000000 --- a/docker-compose-db.yml +++ /dev/null @@ -1,18 +0,0 @@ -version: '3.9' - -services: - db: - image: postgres:16.1-alpine3.18 - container_name: arimelody.me-db - ports: - - 5432:5432 - volumes: - - arimelody-db:/var/lib/postgresql/data - environment: - POSTGRES_DB: arimelody - POSTGRES_USER: arimelody - POSTGRES_PASSWORD: fuckingpassword - -volumes: - arimelody-db: - external: true diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e29b006 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,22 @@ +services: + web: + image: docker.arimelody.me/arimelody.me:latest + build: . + ports: + - 8080:8080 + volumes: + - ./uploads:/app/uploads + environment: + HTTP_DOMAIN: "https://arimelody.me" + ARIMELODY_DB_HOST: db + DISCORD_ADMIN: # your discord user ID. + DISCORD_CLIENT: # your discord OAuth client ID. + DISCORD_SECRET: # your discord OAuth secret. + db: + image: postgres:16.1-alpine3.18 + volumes: + - ./db:/var/lib/postgresql/data + environment: + POSTGRES_DB: arimelody + POSTGRES_USER: arimelody + POSTGRES_PASSWORD: fuckingpassword diff --git a/global/data.go b/global/data.go index b8d0c02..fb641fc 100644 --- a/global/data.go +++ b/global/data.go @@ -35,11 +35,11 @@ var Args = func() map[string]string { }() var HTTP_DOMAIN = func() string { - domain := Args["httpDomain"] - if domain != "" { - return domain + domain := os.Getenv("HTTP_DOMAIN") + if domain == "" { + return "https://arimelody.me" } - return "https://arimelody.me" + return domain }() var DB *sqlx.DB diff --git a/global/funcs.go b/global/funcs.go index f0f3633..33198ab 100644 --- a/global/funcs.go +++ b/global/funcs.go @@ -6,7 +6,7 @@ import ( "strconv" "time" - "arimelody.me/arimelody.me/colour" + "arimelody-web/colour" ) func DefaultHeaders(next http.Handler) http.Handler { diff --git a/go.mod b/go.mod index 1f74015..e2beaf4 100644 --- a/go.mod +++ b/go.mod @@ -1,19 +1,8 @@ -module arimelody.me/arimelody.me +module arimelody-web go 1.22 require ( - github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47 - github.com/jmoiron/sqlx v1.3.5 + github.com/jmoiron/sqlx v1.4.0 github.com/lib/pq v1.10.9 ) - -require ( - github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect - github.com/jackc/pgx/v5 v5.5.5 // indirect - github.com/jackc/puddle/v2 v2.2.1 // indirect - golang.org/x/crypto v0.17.0 // indirect - golang.org/x/sync v0.1.0 // indirect - golang.org/x/text v0.14.0 // indirect -) diff --git a/go.sum b/go.sum index 83d5b57..f4ce337 100644 --- a/go.sum +++ b/go.sum @@ -1,30 +1,10 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47 h1:k4Tw0nt6lwro3Uin8eqoET7MDA4JnT8YgbCjc/g5E3k= -github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= -github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= -github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= -github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= -github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= -github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= -github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= diff --git a/main.go b/main.go index a60524f..c103696 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "errors" "fmt" "log" "net/http" @@ -8,11 +9,11 @@ import ( "path/filepath" "time" - "arimelody.me/arimelody.me/admin" - "arimelody.me/arimelody.me/api" - "arimelody.me/arimelody.me/global" - musicView "arimelody.me/arimelody.me/music/view" - "arimelody.me/arimelody.me/templates" + "arimelody-web/admin" + "arimelody-web/api" + "arimelody-web/global" + musicView "arimelody-web/music/view" + "arimelody-web/templates" "github.com/jmoiron/sqlx" _ "github.com/lib/pq" @@ -22,8 +23,11 @@ const DEFAULT_PORT int = 8080 func main() { // initialise database connection + var dbHost = os.Getenv("ARIMELODY_DB_HOST") + if dbHost == "" { dbHost = "127.0.0.1" } + var err error - global.DB, err = sqlx.Connect("postgres", "user=arimelody dbname=arimelody password=fuckingpassword sslmode=disable") + global.DB, err = sqlx.Connect("postgres", "host=" + dbHost + " user=arimelody dbname=arimelody password=fuckingpassword sslmode=disable") if err != nil { fmt.Fprintf(os.Stderr, "FATAL: Unable to create database connection pool: %v\n", err) os.Exit(1) @@ -64,9 +68,10 @@ func createServeMux() *http.ServeMux { func staticHandler(directory string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { info, err := os.Stat(filepath.Join(directory, filepath.Clean(r.URL.Path))) + // does the file exist? if err != nil { - if os.IsNotExist(err) { + if errors.Is(err, os.ErrNotExist) { http.NotFound(w, r) return } diff --git a/music/controller/artist.go b/music/controller/artist.go index b6a48f5..2ae92e7 100644 --- a/music/controller/artist.go +++ b/music/controller/artist.go @@ -1,7 +1,7 @@ package music import ( - "arimelody.me/arimelody.me/music/model" + "arimelody-web/music/model" "github.com/jmoiron/sqlx" ) @@ -45,27 +45,54 @@ func GetArtistsNotOnRelease(db *sqlx.DB, releaseID string) ([]*model.Artist, err } func GetArtistCredits(db *sqlx.DB, artistID string) ([]*model.Credit, error) { - type DBCredit struct { - Release string - Artist string - Role string - Primary bool `db:"is_primary"` - } - var dbCredits []DBCredit - - err := db.Select(&dbCredits, "SELECT * FROM musiccredit WHERE artist=$1", artistID) + rows, err := db.Query( + "SELECT release.id,release.title,release.artwork,artist.id,artist.name,artist.website,artist.avatar,role,is_primary "+ + "FROM musiccredit "+ + "JOIN musicrelease AS release ON release=release.id "+ + "JOIN artist ON artist=artist.id "+ + "WHERE artist=$1 "+ + "ORDER BY release_date DESC", + artistID) if err != nil { return nil, err } - + defer rows.Close() + + type NamePrimary struct { + Name string `json:"name"` + Primary bool `json:"primary" db:"is_primary"` + } var credits []*model.Credit - for _, credit := range dbCredits { - credits = append(credits, &model.Credit{ - Release: model.Release{ ID: credit.Release }, - Artist: model.Artist{ ID: credit.Artist }, - Role: credit.Role, - Primary: credit.Primary, - }) + for rows.Next() { + var credit model.Credit + err = rows.Scan( + &credit.Release.ID, + &credit.Release.Title, + &credit.Release.Artwork, + &credit.Artist.ID, + &credit.Artist.Name, + &credit.Artist.Website, + &credit.Artist.Avatar, + &credit.Role, + &credit.Primary, + ) + + otherArtists := []NamePrimary{} + err = db.Select(&otherArtists, + "SELECT name,is_primary FROM artist "+ + "JOIN musiccredit ON artist=id "+ + "WHERE release=$1", + credit.Release.ID) + for _, otherCredit := range otherArtists { + credit.Release.Credits = append(credit.Release.Credits, &model.Credit{ + Artist: model.Artist{ + Name: otherCredit.Name, + }, + Primary: otherCredit.Primary, + }) + } + + credits = append(credits, &credit) } return credits, nil @@ -104,11 +131,11 @@ func UpdateArtist(db *sqlx.DB, artist *model.Artist) error { return nil } -func DeleteArtist(db *sqlx.DB, artist *model.Artist) error { +func DeleteArtist(db *sqlx.DB, artistID string) error { _, err := db.Exec( "DELETE FROM artist "+ "WHERE id=$1", - artist.ID, + artistID, ) if err != nil { return err diff --git a/music/controller/release.go b/music/controller/release.go index c314109..4bd2cd6 100644 --- a/music/controller/release.go +++ b/music/controller/release.go @@ -4,29 +4,99 @@ import ( "errors" "fmt" - "arimelody.me/arimelody.me/music/model" + "arimelody-web/music/model" "github.com/jmoiron/sqlx" ) -func GetRelease(db *sqlx.DB, id string) (*model.Release, error) { - var releases = model.Release{} +func GetRelease(db *sqlx.DB, id string, full bool) (*model.Release, error) { + var release = model.Release{} - err := db.Get(&releases, "SELECT * FROM musicrelease WHERE id=$1", id) + err := db.Get(&release, "SELECT * FROM musicrelease WHERE id=$1", id) if err != nil { return nil, err } - return &releases, nil + if full { + // get credits + credits, err := GetReleaseCredits(db, id) + if err != nil { + return nil, errors.New(fmt.Sprintf("Credits: %s", err)) + } + for _, credit := range credits { + release.Credits = append(release.Credits, credit) + } + + // get tracks + tracks, err := GetReleaseTracks(db, id) + if err != nil { + return nil, errors.New(fmt.Sprintf("Tracks: %s", err)) + } + for _, track := range tracks { + release.Tracks = append(release.Tracks, track) + } + + // get links + links, err := GetReleaseLinks(db, id) + if err != nil { + return nil, errors.New(fmt.Sprintf("Links: %s", err)) + } + for _, link := range links { + release.Links = append(release.Links, link) + } + } + + return &release, nil } -func GetAllReleases(db *sqlx.DB) ([]*model.Release, error) { +func GetAllReleases(db *sqlx.DB, onlyVisible bool, limit int, full bool) ([]*model.Release, error) { var releases = []*model.Release{} - err := db.Select(&releases, "SELECT * FROM musicrelease ORDER BY release_date DESC") + query := "SELECT * FROM musicrelease" + if onlyVisible { + query += " WHERE visible=true" + } + query += " ORDER BY release_date DESC" + var err error + if limit > 0 { + err = db.Select(&releases, query + " LIMIT $1", limit) + } else { + err = db.Select(&releases, query) + } if err != nil { return nil, err } + if full { + for _, release := range releases { + // get credits + credits, err := GetReleaseCredits(db, release.ID) + if err != nil { + return nil, errors.New(fmt.Sprintf("Credits: %s", err)) + } + for _, credit := range credits { + release.Credits = append(release.Credits, credit) + } + + // get tracks + tracks, err := GetReleaseTracks(db, release.ID) + if err != nil { + return nil, errors.New(fmt.Sprintf("Tracks: %s", err)) + } + for _, track := range tracks { + release.Tracks = append(release.Tracks, track) + } + + // get links + links, err := GetReleaseLinks(db, release.ID) + if err != nil { + return nil, errors.New(fmt.Sprintf("Links: %s", err)) + } + for _, link := range links { + release.Links = append(release.Links, link) + } + } + } + return releases, nil } @@ -78,49 +148,53 @@ func UpdateRelease(db *sqlx.DB, release *model.Release) error { return nil } -func UpdateReleaseTracks(db *sqlx.DB, release *model.Release, new_tracks []string) error { - _, err := db.Exec( - "DELETE FROM musicreleasetrack "+ - "WHERE release=$1", - release.ID, - ) +func UpdateReleaseTracks(db *sqlx.DB, releaseID string, new_tracks []string) error { + tx, err := db.Begin() if err != nil { return err } + _, err = tx.Exec("DELETE FROM musicreleasetrack WHERE release=$1", releaseID) + if err != nil { + return err + } for i, trackID := range new_tracks { - _, err = db.Exec( + _, err = tx.Exec( "INSERT INTO musicreleasetrack "+ "(release, track, number) "+ "VALUES ($1, $2, $3)", - release.ID, + releaseID, trackID, - i, - ) + i) if err != nil { return err } } - return nil -} - -func UpdateReleaseCredits(db *sqlx.DB, release *model.Release, new_credits []model.Credit) error { - _, err := db.Exec( - "DELETE FROM musiccredit "+ - "WHERE release=$1", - release.ID, - ) + err = tx.Commit() if err != nil { return err } + return nil +} + +func UpdateReleaseCredits(db *sqlx.DB, releaseID string, new_credits []*model.Credit) error { + tx, err := db.Begin() + if err != nil { + return err + } + + _, err = tx.Exec("DELETE FROM musiccredit WHERE release=$1", releaseID) + if err != nil { + return err + } for _, credit := range new_credits { - _, err = db.Exec( + _, err = tx.Exec( "INSERT INTO musiccredit "+ "(release, artist, role, is_primary) "+ "VALUES ($1, $2, $3, $4)", - release.ID, + releaseID, credit.Artist.ID, credit.Role, credit.Primary, @@ -130,25 +204,31 @@ func UpdateReleaseCredits(db *sqlx.DB, release *model.Release, new_credits []mod } } - return nil -} - -func UpdateReleaseLinks(db *sqlx.DB, release *model.Release, new_links []*model.Link) error { - _, err := db.Exec( - "DELETE FROM musiclink "+ - "WHERE release=$1", - release.ID, - ) + err = tx.Commit() if err != nil { return err } + return nil +} + +func UpdateReleaseLinks(db *sqlx.DB, releaseID string, new_links []*model.Link) error { + tx, err := db.Begin() + if err != nil { + return err + } + + _, err = tx.Exec("DELETE FROM musiclink WHERE release=$1", releaseID) + if err != nil { + return err + } for _, link := range new_links { - _, err = db.Exec( + fmt.Printf("%s: %s\n", link.Name, link.URL) + _, err := tx.Exec( "INSERT INTO musiclink "+ "(release, name, url) "+ "VALUES ($1, $2, $3)", - release.ID, + releaseID, link.Name, link.URL, ) @@ -157,14 +237,19 @@ func UpdateReleaseLinks(db *sqlx.DB, release *model.Release, new_links []*model. } } + err = tx.Commit() + if err != nil { + return err + } + return nil } -func DeleteRelease(db *sqlx.DB, release *model.Release) error { +func DeleteRelease(db *sqlx.DB, releaseID string) error { _, err := db.Exec( "DELETE FROM musicrelease "+ "WHERE id=$1", - release.ID, + releaseID, ) if err != nil { return err @@ -173,54 +258,8 @@ func DeleteRelease(db *sqlx.DB, release *model.Release) error { return nil } -func GetFullRelease(db *sqlx.DB, releaseID string) (*model.FullRelease, error) { - // get release - release, err := GetRelease(db, releaseID) - if err != nil { - return nil, err - } - - // get credits - credits, err := GetReleaseCredits(db, releaseID) - if err != nil { - return nil, errors.New(fmt.Sprintf("Credits: %s", err)) - } - - // get artists - for i, credit := range credits { - artist, err := GetArtist(db, credit.Artist.ID) - if err != nil { - return nil, errors.New(fmt.Sprintf("FATAL: Failed to serve release %s: Artists: %s\n", release.ID, err)) - } - credits[i].Artist = *artist - } - - // get tracks - dbTracks, err := GetReleaseTracks(db, releaseID) - if err != nil { - return nil, errors.New(fmt.Sprintf("Tracks: %s", err)) - } - tracks := []model.DisplayTrack{} - for i, track := range dbTracks { - tracks = append(tracks, track.MakeDisplay(i + 1)) - } - - // get links - links, err := GetReleaseLinks(db, releaseID) - if err != nil { - return nil, errors.New(fmt.Sprintf("Links: %s", err)) - } - - return &model.FullRelease{ - Release: release, - Tracks: tracks, - Credits: credits, - Links: links, - }, nil -} - -func GetReleaseTracks(db *sqlx.DB, releaseID string) ([]model.Track, error) { - var tracks = []model.Track{} +func GetReleaseTracks(db *sqlx.DB, releaseID string) ([]*model.Track, error) { + var tracks = []*model.Track{} err := db.Select(&tracks, "SELECT musictrack.* FROM musictrack "+ @@ -236,40 +275,38 @@ func GetReleaseTracks(db *sqlx.DB, releaseID string) ([]model.Track, error) { return tracks, nil } -func GetReleaseCredits(db *sqlx.DB, releaseID string) ([]model.Credit, error) { - type DBCredit struct { - Release string - Artist string - Role string - Primary bool `db:"is_primary"` - } - var dbCredits []DBCredit - - err := db.Select(&dbCredits, - "SELECT musiccredit.* FROM musiccredit "+ - "JOIN artist ON artist=id "+ - "WHERE release=$1", +func GetReleaseCredits(db *sqlx.DB, releaseID string) ([]*model.Credit, error) { + rows, err := db.Query( + "SELECT artist.id,artist.name,artist.website,artist.avatar,role,is_primary "+ + "FROM musiccredit "+ + "JOIN artist ON artist=artist.id "+ + "JOIN musicrelease ON release=musicrelease.id "+ + "WHERE musicrelease.id=$1 "+ + "ORDER BY is_primary DESC", releaseID, ) if err != nil { return nil, err } - var credits []model.Credit - for _, credit := range dbCredits { - credits = append(credits, model.Credit{ - Release: model.Release{ ID: credit.Release }, - Artist: model.Artist{ ID: credit.Artist }, - Role: credit.Role, - Primary: credit.Primary, - }) + var credits []*model.Credit + for rows.Next() { + credit := model.Credit{} + rows.Scan( + &credit.Artist.ID, + &credit.Artist.Name, + &credit.Artist.Website, + &credit.Artist.Avatar, + &credit.Role, + &credit.Primary) + credits = append(credits, &credit) } return credits, nil } -func GetReleaseLinks(db *sqlx.DB, releaseID string) ([]model.Link, error) { - var links = []model.Link{} +func GetReleaseLinks(db *sqlx.DB, releaseID string) ([]*model.Link, error) { + var links = []*model.Link{} err := db.Select(&links, "SELECT name,url FROM musiclink WHERE release=$1", releaseID) if err != nil { diff --git a/music/controller/track.go b/music/controller/track.go index a070bff..3187553 100644 --- a/music/controller/track.go +++ b/music/controller/track.go @@ -1,7 +1,7 @@ package music import ( - "arimelody.me/arimelody.me/music/model" + "arimelody-web/music/model" "github.com/jmoiron/sqlx" ) @@ -55,11 +55,12 @@ func GetTracksNotOnRelease(db *sqlx.DB, releaseID string) ([]*model.Track, error return tracks, nil } -func GetTrackReleases(db *sqlx.DB, trackID string) ([]model.ReleaseShorthand, error) { - var releases = []model.ReleaseShorthand{} +func GetTrackReleases(db *sqlx.DB, trackID string, full bool) ([]*model.Release, error) { + var releases = []*model.Release{} err := db.Select(&releases, - "SELECT id,title,type,release_date,artwork,buylink FROM musicrelease "+ + "SELECT id,title,type,release_date,artwork,buylink "+ + "FROM musicrelease "+ "JOIN musicreleasetrack ON release=id "+ "WHERE track=$1 "+ "ORDER BY release_date", @@ -69,6 +70,43 @@ func GetTrackReleases(db *sqlx.DB, trackID string) ([]model.ReleaseShorthand, er return nil, err } + type NamePrimary struct { + Name string `json:"name"` + Primary bool `json:"primary" db:"is_primary"` + } + for _, release := range releases { + // get artists + credits := []NamePrimary{} + err := db.Select(&credits, + "SELECT name,is_primary FROM artist "+ + "JOIN musiccredit ON artist=artist.id "+ + "JOIN musicrelease ON release=musicrelease.id "+ + "WHERE musicrelease.id=$1", release.ID) + if err != nil { + return nil, err + } + for _, credit := range credits { + release.Credits = append(release.Credits, &model.Credit{ + Artist: model.Artist{ + Name: credit.Name, + }, + Primary: credit.Primary, + }) + } + + // get tracks + tracks := []string{} + err = db.Select(&tracks, "SELECT track FROM musicreleasetrack WHERE release=$1", release.ID) + if err != nil { + return nil, err + } + for _, trackID := range tracks { + release.Tracks = append(release.Tracks, &model.Track{ + ID: trackID, + }) + } + } + return releases, nil } diff --git a/music/model/artist.go b/music/model/artist.go index 837aee8..733e537 100644 --- a/music/model/artist.go +++ b/music/model/artist.go @@ -1,7 +1,5 @@ package model -import "strings" - type ( Artist struct { ID string `json:"id"` @@ -21,56 +19,3 @@ func (artist Artist) GetAvatar() string { } return artist.Avatar } - -func (release FullRelease) GetUniqueArtists(only_primary bool) []*Artist { - var artists = []*Artist{} - - for _, credit := range release.Credits { - if only_primary && !credit.Primary { - continue - } - - exists := false - for _, a := range artists { - if a.ID == credit.Artist.ID { - exists = true - break - } - } - - if exists { - continue - } - - artists = append(artists, &credit.Artist) - } - - return artists -} - -func (release FullRelease) GetUniqueArtistNames(only_primary bool) []string { - var names = []string{} - for _, artist := range release.GetUniqueArtists(only_primary) { - names = append(names, artist.Name) - } - - return names -} - -func (release FullRelease) PrintArtists(only_primary bool, ampersand bool) string { - names := release.GetUniqueArtistNames(only_primary) - - if len(names) == 0 { - return "Unknown Artist" - } else if len(names) == 1 { - return names[0] - } - - if ampersand { - res := strings.Join(names[:len(names)-1], ", ") - res += " & " + names[len(names)-1] - return res - } else { - return strings.Join(names[:], ", ") - } -} diff --git a/music/model/credit.go b/music/model/credit.go index 9b62b54..2080d83 100644 --- a/music/model/credit.go +++ b/music/model/credit.go @@ -1,8 +1,10 @@ package model -type Credit struct { - Release Release `json:"release"` - Artist Artist `json:"artist"` - Role string `json:"role"` - Primary bool `json:"primary" db:"is_primary"` -} +type ( + Credit struct { + Release Release `json:"release"` + Artist Artist `json:"artist"` + Role string `json:"role"` + Primary bool `json:"primary" db:"is_primary"` + } +) diff --git a/music/model/release.go b/music/model/release.go index d9840a4..7b0c8d5 100644 --- a/music/model/release.go +++ b/music/model/release.go @@ -1,6 +1,8 @@ package model import ( + "html/template" + "strings" "time" ) @@ -19,22 +21,9 @@ type ( Buylink string `json:"buylink"` Copyright string `json:"copyright" db:"copyright"` CopyrightURL string `json:"copyrightURL" db:"copyrighturl"` - } - - FullRelease struct { - *Release - Tracks []DisplayTrack `json:"tracks"` - Credits []Credit `json:"credits"` - Links []Link `json:"links"` - } - - ReleaseShorthand struct { - ID string `json:"id"` - Title string `json:"title"` - ReleaseType ReleaseType `json:"type" db:"type"` - ReleaseDate time.Time `json:"releaseDate" db:"release_date"` - Artwork string `json:"artwork"` - Buylink string `json:"buylink"` + Tracks []*Track `json:"tracks"` + Credits []*Credit `json:"credits"` + Links []*Link `json:"links"` } ) @@ -48,6 +37,10 @@ const ( // GETTERS +func (release Release) GetDescriptionHTML() template.HTML { + return template.HTML(strings.Replace(release.Description, "\n", "
", -1)) +} + func (release Release) TextReleaseDate() string { return release.ReleaseDate.Format("2006-01-02T15:04") } @@ -67,10 +60,39 @@ func (release Release) GetArtwork() string { return release.Artwork } -func (release FullRelease) IsSingle() bool { +func (release Release) IsSingle() bool { return len(release.Tracks) == 1; } func (release Release) IsReleased() bool { return release.ReleaseDate.Before(time.Now()) } + +func (release Release) GetUniqueArtistNames(only_primary bool) []string { + names := []string{} + + for _, credit := range release.Credits { + if only_primary && !credit.Primary { continue } + names = append(names, credit.Artist.Name) + } + + return names +} + +func (release Release) PrintArtists(only_primary bool, ampersand bool) string { + names := release.GetUniqueArtistNames(only_primary) + + if len(names) == 0 { + return "Unknown Artist" + } else if len(names) == 1 { + return names[0] + } + + if ampersand { + res := strings.Join(names[:len(names)-1], ", ") + res += " & " + names[len(names)-1] + return res + } else { + return strings.Join(names[:], ", ") + } +} diff --git a/music/model/track.go b/music/model/track.go index 47b2c46..d44c224 100644 --- a/music/model/track.go +++ b/music/model/track.go @@ -7,24 +7,23 @@ import ( type ( Track struct { - ID string `json:"id"` - Title string `json:"title"` - Description string `json:"description"` - Lyrics string `json:"lyrics" db:"lyrics"` - PreviewURL string `json:"previewURL" db:"preview_url"` - } - - DisplayTrack struct { - *Track - Lyrics template.HTML `json:"lyrics"` - Number int `json:"-"` + ID string `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + Lyrics string `json:"lyrics" db:"lyrics"` + PreviewURL string `json:"previewURL" db:"preview_url"` } ) -func (track Track) MakeDisplay(number int) DisplayTrack { - return DisplayTrack{ - Track: &track, - Lyrics: template.HTML(strings.Replace(track.Lyrics, "\n", "
", -1)), - Number: number, - } +func (track Track) GetDescriptionHTML() template.HTML { + return template.HTML(strings.Replace(track.Description, "\n", "
", -1)) +} + +func (track Track) GetLyricsHTML() template.HTML { + return template.HTML(strings.Replace(track.Lyrics, "\n", "
", -1)) +} + +// this function is stupid and i hate that i need it +func (track Track) Add(a int, b int) int { + return a + b } diff --git a/music/view/music.go b/music/view/music.go index 041680e..6297a42 100644 --- a/music/view/music.go +++ b/music/view/music.go @@ -4,10 +4,10 @@ import ( "fmt" "net/http" - "arimelody.me/arimelody.me/global" - music "arimelody.me/arimelody.me/music/controller" - "arimelody.me/arimelody.me/music/model" - "arimelody.me/arimelody.me/templates" + "arimelody-web/global" + music "arimelody-web/music/controller" + "arimelody-web/music/model" + "arimelody-web/templates" ) // HTTP HANDLER METHODS @@ -21,8 +21,7 @@ func Handler() http.Handler { return } - var release model.Release - err := global.DB.Get(&release, "SELECT * FROM musicrelease WHERE id=$1", r.URL.Path[1:]) + release, err := music.GetRelease(global.DB, r.URL.Path[1:], true) if err != nil { http.NotFound(w, r) return @@ -36,25 +35,17 @@ func Handler() http.Handler { func ServeCatalog() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - dbReleases, err := music.GetAllReleases(global.DB) + releases, err := music.GetAllReleases(global.DB, true, 0, true) if err != nil { fmt.Printf("FATAL: Failed to pull releases for catalog: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - releases := []*model.FullRelease{} - for _, dbRelease := range dbReleases { - if !dbRelease.Visible { continue } - if !dbRelease.IsReleased() { - dbRelease.ReleaseType = model.Upcoming + + for _, release := range releases { + if !release.IsReleased() { + release.ReleaseType = model.Upcoming } - release, err := music.GetFullRelease(global.DB, dbRelease.ID) - if err != nil { - fmt.Printf("FATAL: Failed to pull full release for %s: %s\n", dbRelease.ID, err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - releases = append(releases, release) } err = templates.Pages["music"].Execute(w, releases) diff --git a/music/view/release.go b/music/view/release.go index 306c97e..fcb2b29 100644 --- a/music/view/release.go +++ b/music/view/release.go @@ -5,11 +5,11 @@ import ( "fmt" "net/http" - "arimelody.me/arimelody.me/admin" - "arimelody.me/arimelody.me/global" - "arimelody.me/arimelody.me/music/model" - db "arimelody.me/arimelody.me/music/controller" - "arimelody.me/arimelody.me/templates" + "arimelody-web/admin" + "arimelody-web/global" + "arimelody-web/music/model" + db "arimelody-web/music/controller" + "arimelody-web/templates" ) type ( @@ -26,14 +26,14 @@ type ( } Release struct { - model.Release + *model.Release Tracks []Track `json:"tracks"` Credits []Credit `json:"credits"` Links map[string]string `json:"links"` } ) -func ServeRelease(release model.Release) http.Handler { +func ServeRelease(release *model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // only allow authorised users to view hidden releases authorised := admin.GetSession(r) != nil @@ -108,7 +108,7 @@ func ServeRelease(release model.Release) http.Handler { }) } -func ServeGateway(release model.Release) http.Handler { +func ServeGateway(release *model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // only allow authorised users to view hidden releases authorised := admin.GetSession(r) != nil @@ -117,21 +117,15 @@ func ServeGateway(release model.Release) http.Handler { return } - fullRelease := &model.FullRelease{ - Release: &release, - } + response := *release if authorised || release.IsReleased() { - fullerRelease, err := db.GetFullRelease(global.DB, release.ID) - if err != nil { - fmt.Printf("FATAL: Failed to pull full release data for %s: %s\n", release.ID, err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - fullRelease = fullerRelease + response.Tracks = release.Tracks + response.Credits = release.Credits + response.Links = release.Links } - err := templates.Pages["music-gateway"].Execute(w, fullRelease) + err := templates.Pages["music-gateway"].Execute(w, response) if err != nil { fmt.Printf("Error rendering music gateway for %s: %s\n", release.ID, err) diff --git a/public/style/music-gateway.css b/public/style/music-gateway.css index 68ad65c..9eff4e4 100644 --- a/public/style/music-gateway.css +++ b/public/style/music-gateway.css @@ -296,6 +296,13 @@ div#info p { display: inline-block; } +#upcoming-release { + width: fit-content; + padding: .3em 1em; + font-size: 1em; + background: #101010; +} + ul#links { width: 100%; margin: 1rem 0; @@ -357,7 +364,12 @@ ul#links a:hover { } #description { - font-size: 1.2em; + font-size: 1.1em; +} + +#copyright { + margin-bottom: 0; + font-size: .8em; } #share { diff --git a/views/music-gateway.html b/views/music-gateway.html index 93a6950..1bf3a2f 100644 --- a/views/music-gateway.html +++ b/views/music-gateway.html @@ -61,7 +61,7 @@

{{.ReleaseType}}

{{else}}

upcoming

-

Releases: {{.PrintReleaseDate}}

+

Releases: {{.PrintReleaseDate}}

{{end}} {{if .IsReleased}} @@ -79,7 +79,7 @@ {{if .Description}} -

{{.Description}}

+

{{.GetDescriptionHTML}}

{{else if .IsSingle}} @@ -90,6 +90,10 @@ {{end}} + {{if and .Copyright .CopyrightURL}} + + {{end}} +
@@ -118,7 +122,7 @@

LYRICS

{{if $Track.Lyrics}} - {{$Track.Lyrics}} + {{$Track.GetLyricsHTML}} {{else}} No lyrics. {{end}} @@ -130,7 +134,7 @@

TRACKS

{{range $i, $track := .Tracks}}
- {{$track.Number}}. {{$track.Title}} + {{$track.Add $i 1}}. {{$track.Title}} {{if $track.Description}}

DESCRIPTION

@@ -139,7 +143,7 @@

LYRICS

{{if $track.Lyrics}} - {{$track.Lyrics}} + {{$track.GetLyricsHTML}} {{else}} No lyrics. {{end}}