diff --git a/.air.toml b/.air.toml
index 940a692..f222806 100644
--- a/.air.toml
+++ b/.air.toml
@@ -14,7 +14,7 @@ tmp_dir = "tmp"
follow_symlink = false
full_bin = ""
include_dir = [".", "views", "api"]
- include_ext = ["go", "tpl", "tmpl", "html"]
+ include_ext = ["go", "tpl", "tmpl"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
diff --git a/api/api.go b/api/api.go
deleted file mode 100644
index ebe0326..0000000
--- a/api/api.go
+++ /dev/null
@@ -1,12 +0,0 @@
-package api
-
-import (
- "net/http"
- "html/template"
-)
-
-func Handle(writer http.ResponseWriter, req *http.Request, root *template.Template) int {
- writer.WriteHeader(501)
- writer.Write([]byte("501 Not Implemented"))
- return 501;
-}
diff --git a/api/v1/admin/admin.go b/api/v1/admin/admin.go
index 69570bb..e952d40 100644
--- a/api/v1/admin/admin.go
+++ b/api/v1/admin/admin.go
@@ -24,14 +24,12 @@ type (
const TOKEN_LENGTH = 64
const TOKEN_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
-// TODO: consider relying *entirely* on env vars instead of hard-coded fallbacks
var ADMIN_ID_DISCORD = func() string {
envvar := os.Getenv("DISCORD_ADMIN_ID")
- if envvar != "" {
- return envvar
- } else {
- return "356210742200107009"
+ if envvar == "" {
+ fmt.Printf("DISCORD_ADMIN_ID was not provided. Admin login will be unavailable.\n")
}
+ return envvar
}()
var sessions []*Session
@@ -48,30 +46,30 @@ func Handler() http.Handler {
mux := http.NewServeMux()
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.WriteHeader(200)
+ w.WriteHeader(http.StatusOK)
w.Write([]byte("hello /admin!"))
}))
mux.Handle("/callback", global.HTTPLog(OAuthCallbackHandler()))
mux.Handle("/login", global.HTTPLog(LoginHandler()))
- mux.Handle("/verify", global.HTTPLog(AuthorisedHandler(VerifyHandler())))
- mux.Handle("/logout", global.HTTPLog(AuthorisedHandler(LogoutHandler())))
+ mux.Handle("/verify", global.HTTPLog(MustAuthorise(VerifyHandler())))
+ mux.Handle("/logout", global.HTTPLog(MustAuthorise(LogoutHandler())))
return mux
}
-func AuthorisedHandler(next http.Handler) http.Handler {
+func MustAuthorise(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
- if auth == "" || !strings.HasPrefix(auth, "Bearer ") {
+ if strings.HasPrefix(auth, "Bearer ") {
+ auth = auth[7:]
+ } else {
cookie, err := r.Cookie("token")
if err != nil {
- w.WriteHeader(401)
- w.Write([]byte("Unauthorized"))
+ http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
auth = cookie.Value
}
- auth = auth[7:]
var session *Session
for _, s := range sessions {
@@ -86,6 +84,7 @@ func AuthorisedHandler(next http.Handler) http.Handler {
}
continue
}
+
if s.Token == auth {
session = s
break
@@ -93,46 +92,47 @@ func AuthorisedHandler(next http.Handler) http.Handler {
}
if session == nil {
- w.WriteHeader(401)
- w.Write([]byte("Unauthorized"))
+ http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), "token", session.Token)
+ ctx = context.WithValue(ctx, "role", "admin")
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func LoginHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if ADMIN_ID_DISCORD == "" {
+ http.Error(w, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable)
+ return
+ }
+
code := r.URL.Query().Get("code")
if code == "" {
- w.Header().Add("Location", discord.REDIRECT_URI)
- w.WriteHeader(307)
+ http.Redirect(w, r, discord.REDIRECT_URI, http.StatusTemporaryRedirect)
return
}
auth_token, err := discord.GetOAuthTokenFromCode(code)
if err != nil {
fmt.Printf("Failed to retrieve discord access token: %s\n", err)
- w.WriteHeader(500)
- w.Write([]byte("Internal server error"))
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
discord_user, err := discord.GetDiscordUserFromAuth(auth_token)
if err != nil {
fmt.Printf("Failed to retrieve discord user information: %s\n", err)
- w.WriteHeader(500)
- w.Write([]byte("Internal server error"))
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if discord_user.Id != ADMIN_ID_DISCORD {
- // TODO: unauthorized user. revoke the token
- w.WriteHeader(401)
- w.Write([]byte("Unauthorized"))
+ // TODO: unauthorized user; revoke the token
+ http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
@@ -149,7 +149,7 @@ func LoginHandler() http.Handler {
cookie.Path = "/"
http.SetCookie(w, &cookie)
- w.WriteHeader(200)
+ w.WriteHeader(http.StatusOK)
w.Write([]byte(session.Token))
})
}
@@ -159,7 +159,7 @@ func LogoutHandler() http.Handler {
token := r.Context().Value("token").(string)
if token == "" {
- w.WriteHeader(401)
+ http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
@@ -172,7 +172,8 @@ func LogoutHandler() http.Handler {
return new_sessions
}(token)
- w.WriteHeader(200)
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte("OK"))
})
}
@@ -185,7 +186,8 @@ func VerifyHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// this is an authorised endpoint, so you *must* supply a valid token
// before accessing this route.
- w.WriteHeader(200)
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte("OK"))
})
}
diff --git a/api/v1/music/artist.go b/api/v1/music/artist.go
new file mode 100644
index 0000000..3fcd740
--- /dev/null
+++ b/api/v1/music/artist.go
@@ -0,0 +1,95 @@
+package music
+
+import (
+ "fmt"
+
+ "github.com/jmoiron/sqlx"
+)
+
+type Artist struct {
+ id string
+ name string
+ website string
+}
+
+var Artists []Artist
+
+func GetArtist(id string) *Artist {
+ for _, artist := range Artists {
+ if artist.GetID() == id {
+ return &artist
+ }
+ }
+ return nil
+}
+
+// GETTERS
+
+func (artist Artist) GetID() string {
+ return artist.id
+}
+
+func (artist Artist) GetName() string {
+ return artist.name
+}
+
+func (artist Artist) GetWebsite() string {
+ return artist.website
+}
+
+// SETTERS
+
+func (artist Artist) SetID(id string) error {
+ artist.id = id
+ return nil
+}
+
+func (artist Artist) SetName(name string) error {
+ artist.name = name
+ return nil
+}
+
+func (artist Artist) SetWebsite(website string) error {
+ artist.website = website
+ return nil
+}
+
+// DATABASE
+
+func (artist Artist) PushToDB(db *sqlx.DB) {
+ fmt.Printf("Pushing artist [%s] to database...", artist.name)
+
+ db.MustExec("INSERT INTO artists (id, name, website) VALUES ($1, $2, $3) ON CONFLICT (id) DO UPDATE SET name=$2, website=$3",
+ artist.id,
+ artist.name,
+ artist.website,
+ )
+
+ fmt.Printf("done!\n")
+}
+
+func PullAllArtists(db *sqlx.DB) ([]Artist, error) {
+ artists := []Artist{}
+
+ rows, err := db.Query("SELECT id, name, website FROM artists")
+ if err != nil {
+ return nil, err
+ }
+
+ for rows.Next() {
+ var artist = Artist{}
+
+ err = rows.Scan(
+ &artist.id,
+ &artist.name,
+ &artist.website,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ artists = append(artists, artist)
+ }
+
+ return artists, nil
+}
diff --git a/api/v1/music/credit.go b/api/v1/music/credit.go
new file mode 100644
index 0000000..a2bd1b6
--- /dev/null
+++ b/api/v1/music/credit.go
@@ -0,0 +1,84 @@
+package music
+
+import (
+ "fmt"
+
+ "github.com/jmoiron/sqlx"
+)
+
+type Credit struct {
+ artist *Artist
+ role string
+ primary bool
+}
+
+// GETTERS
+
+func (credit Credit) GetArtist() Artist {
+ return *credit.artist
+}
+
+func (credit Credit) GetRole() string {
+ return credit.role
+}
+
+func (credit Credit) IsPrimary() bool {
+ return credit.primary
+}
+
+// SETTERS
+
+func (credit Credit) SetArtist(artist *Artist) error {
+ // TODO: update DB
+ credit.artist = artist
+ return nil
+}
+
+func (credit Credit) SetRole(role string) error {
+ // TODO: update DB
+ credit.role = role
+ return nil
+}
+
+func (credit Credit) SetPrimary(primary bool) error {
+ // TODO: update DB
+ credit.primary = primary
+ return nil
+}
+
+// DATABASE
+
+func PullReleaseCredits(db *sqlx.DB, releaseID string) ([]Credit, error) {
+ var credits = []Credit{}
+
+ credit_rows, err := db.Query("SELECT artist, role, is_primary FROM musiccredits WHERE release=$1", releaseID)
+ if err != nil {
+ return []Credit{}, err
+ }
+
+ for credit_rows.Next() {
+ var artistID string
+ var credit = Credit{}
+ err = credit_rows.Scan(
+ &artistID,
+ &credit.role,
+ &credit.primary,
+ )
+ if err != nil {
+ fmt.Printf("Error while pulling credit for release %s: %s\n", releaseID, err)
+ continue
+ }
+
+ credit.artist = GetArtist(artistID)
+ if credit.artist == nil {
+ // this should absolutely not happen ever due to foreign key
+ // constraints, but it doesn't hurt to be sure!
+ fmt.Printf("Error while pulling credit for release %s: Artist %s does not exist\n", releaseID, artistID)
+ continue
+ }
+
+ credits = append(credits, credit)
+ }
+
+ return credits, nil
+}
diff --git a/api/v1/music/link.go b/api/v1/music/link.go
new file mode 100644
index 0000000..3ab74d7
--- /dev/null
+++ b/api/v1/music/link.go
@@ -0,0 +1,73 @@
+package music
+
+import (
+ "fmt"
+ "regexp"
+ "strings"
+
+ "github.com/jmoiron/sqlx"
+)
+
+type Link struct {
+ name string
+ url string
+}
+
+// GETTERS
+
+func (link Link) GetName() string {
+ return link.name
+}
+
+func (link Link) GetURL() string {
+ return link.url
+}
+
+// SETTERS
+
+func (link Link) SetName(name string) error {
+ // TODO: update DB
+ link.name = name
+ return nil
+}
+
+func (link Link) SetURL(url string) error {
+ // TODO: update DB
+ link.url = url
+ return nil
+}
+
+// MISC
+
+func (link Link) NormaliseName() string {
+ rgx := regexp.MustCompile(`[^a-z0-9]`)
+ return strings.ToLower(rgx.ReplaceAllString(link.name, ""))
+}
+
+// DATABASE
+
+func PullReleaseLinks(db *sqlx.DB, releaseID string) ([]Link, error) {
+ var links = []Link{}
+
+ link_rows, err := db.Query("SELECT name, url FROM musiclinks WHERE release=$1", releaseID);
+ if err != nil {
+ return []Link{}, err
+ }
+
+ for link_rows.Next() {
+ var link = Link{}
+
+ err = link_rows.Scan(
+ &link.name,
+ &link.url,
+ )
+ if err != nil {
+ fmt.Printf("Error while pulling link for release %s: %s\n", releaseID, err)
+ continue
+ }
+
+ links = append(links, link)
+ }
+
+ return links, nil
+}
diff --git a/api/v1/music/music.go b/api/v1/music/music.go
index 9a94670..3afb392 100644
--- a/api/v1/music/music.go
+++ b/api/v1/music/music.go
@@ -1,37 +1,62 @@
package music
import (
- "errors"
- "fmt"
- "time"
+ "fmt"
+ "net/http"
+
+ "arimelody.me/arimelody.me/api/v1/admin"
+ "arimelody.me/arimelody.me/global"
)
-var Releases []*MusicRelease;
-var Artists []*Artist;
+// func make_date_work(date string) time.Time {
+// res, err := time.Parse("2-Jan-2006", date)
+// if err != nil {
+// fmt.Printf("somehow we failed to parse %s! falling back to epoch :]\n", date)
+// return time.Unix(0, 0)
+// }
+// return res
+// }
-func make_date_work(date string) time.Time {
- res, err := time.Parse("2-Jan-2006", date)
- if err != nil {
- fmt.Printf("somehow we failed to parse %s! falling back to epoch :]\n", date)
- return time.Unix(0, 0)
- }
- return res
-}
+// HTTP HANDLER METHODS
-func GetRelease(id string) (*MusicRelease, error) {
- for _, release := range Releases {
- if release.Id == id {
- return release, nil
+func ServeCatalog() http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ releases := []Release{}
+ authorised := r.Context().Value("role") != nil && r.Context().Value("role") == "admin"
+ for _, release := range Releases {
+ if !release.IsReleased() && !authorised {
+ continue
+ }
+ releases = append(releases, release)
}
- }
- return nil, errors.New(fmt.Sprintf("Release %s not found", id))
+
+ global.ServeTemplate("music.html", releases).ServeHTTP(w, r)
+ })
}
-func GetArtist(id string) (*Artist, error) {
- for _, artist := range Artists {
- if artist.Id == id {
- return artist, nil
+func ServeGateway() http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ id := r.URL.Path[1:]
+ release := GetRelease(id)
+ if release == nil {
+ http.NotFound(w, r)
+ return
}
- }
- return nil, errors.New(fmt.Sprintf("Artist %s not found", id))
+
+ // only allow authorised users to view unreleased releases
+ authorised := r.Context().Value("role") != nil && r.Context().Value("role") == "admin"
+ if !release.IsReleased() && !authorised {
+ admin.MustAuthorise(ServeGateway()).ServeHTTP(w, r)
+ return
+ }
+
+ lrw := global.LoggingResponseWriter{w, http.StatusOK}
+
+ global.ServeTemplate("music-gateway.html", release).ServeHTTP(&lrw, r)
+
+ if lrw.Code != http.StatusOK {
+ fmt.Printf("Error loading music gateway for %s\n", id)
+ return
+ }
+ })
}
diff --git a/api/v1/music/music_types.go b/api/v1/music/music_types.go
deleted file mode 100644
index e2260a0..0000000
--- a/api/v1/music/music_types.go
+++ /dev/null
@@ -1,133 +0,0 @@
-package music
-
-import (
- "regexp"
- "strings"
- "time"
-)
-
-type (
- Artist struct {
- Id string
- Name string
- Website string
- }
-
- MusicRelease struct {
- Id string
- Title string
- Type string
- ReleaseDate time.Time
- Artwork string
- Buyname string
- Buylink string
- Links []MusicLink
- Description string
- Credits []MusicCredit
- Tracks []MusicTrack
- }
-
- MusicLink struct {
- Name string
- Url string
- }
-
- MusicCredit struct {
- Artist *Artist
- Role string
- Primary bool
- }
-
- MusicTrack struct {
- Number int
- Title string
- Description string
- Lyrics string
- PreviewUrl string
- }
-)
-
-func (release MusicRelease) GetUniqueArtists(include_non_primary bool) []*Artist {
- res := []*Artist{}
- for _, credit := range release.Credits {
- if !include_non_primary && !credit.Primary {
- continue
- }
-
- exists := false
- for _, a := range res {
- if a == credit.Artist {
- exists = true
- break
- }
- }
- if exists {
- continue
- }
-
- res = append(res, credit.Artist)
- }
-
- return res
-}
-
-func (release MusicRelease) GetUniqueArtistNames(include_non_primary bool) []string {
- artists := release.GetUniqueArtists(include_non_primary)
- names := []string{}
- for _, artist := range artists {
- names = append(names, artist.Name)
- }
-
- return names
-}
-
-func (release MusicRelease) PrintPrimaryArtists(include_non_primary bool, ampersand bool) string {
- names := release.GetUniqueArtistNames(include_non_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[:], ", ")
- }
-}
-
-func (release MusicRelease) ResolveType() string {
- if release.Type != "" {
- return release.Type
- }
- return "unknown"
-}
-
-func (release MusicRelease) ResolveArtwork() string {
- if release.Artwork != "" {
- return release.Artwork
- }
- return "/img/music-artwork/default.png"
-}
-
-func (release MusicRelease) PrintReleaseDate() string {
- return release.ReleaseDate.Format("02 January 2006")
-}
-
-func (release MusicRelease) GetReleaseYear() int {
- return release.ReleaseDate.Year()
-}
-
-func (link MusicLink) NormaliseName() string {
- re := regexp.MustCompile(`[^a-z0-9]`)
- return strings.ToLower(re.ReplaceAllString(link.Name, ""))
-}
-
-func (release MusicRelease) IsSingle() bool {
- return len(release.Tracks) == 1;
-}
-
-func (credit MusicCredit) ResolveArtist() Artist {
- return *credit.Artist
-}
diff --git a/api/v1/music/release.go b/api/v1/music/release.go
new file mode 100644
index 0000000..1f5aeff
--- /dev/null
+++ b/api/v1/music/release.go
@@ -0,0 +1,368 @@
+package music
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/jmoiron/sqlx"
+)
+
+type ReleaseType string
+
+const (
+ Single ReleaseType = "Single"
+ Album ReleaseType = "Album"
+ EP ReleaseType = "EP"
+ Compilation ReleaseType = "Compilation"
+)
+
+type Release struct {
+ id string
+ title string
+ releaseType ReleaseType
+ releaseDate time.Time
+ artwork string
+ buyname string
+ buylink string
+ links []Link
+ description string
+ credits []Credit
+ tracks []Track
+}
+
+var Releases []Release;
+
+// GETTERS
+
+func (release Release) GetID() string {
+ return release.id
+}
+
+func (release Release) GetTitle() string {
+ return release.title
+}
+
+func (release Release) GetType() ReleaseType {
+ return release.releaseType
+}
+
+func (release Release) GetReleaseDate() time.Time {
+ return release.releaseDate
+}
+
+func (release Release) GetArtwork() string {
+ if release.artwork == "" {
+ return "/img/music-artwork/default.png"
+ }
+ return release.artwork
+}
+
+func (release Release) GetBuyName() string {
+ return release.buyname
+}
+
+func (release Release) GetBuyLink() string {
+ return release.buylink
+}
+
+func (release Release) GetLinks() []Link {
+ return release.links
+}
+
+func (release Release) GetDescription() string {
+ return release.description
+}
+
+func (release Release) GetCredits() []Credit {
+ return release.credits
+}
+
+func (release Release) GetTracks() []Track {
+ return release.tracks
+}
+
+// SETTERS
+
+func (release Release) SetID(id string) error {
+ // TODO: update DB
+ release.id = id
+ return nil
+}
+
+func (release Release) SetTitle(title string) error {
+ // TODO: update DB
+ release.title = title
+ return nil
+}
+
+func (release Release) SetType(releaseType ReleaseType) error {
+ // TODO: update DB
+ release.releaseType = releaseType
+ return nil
+}
+
+func (release Release) SetReleaseDate(releaseDate time.Time) error {
+ // TODO: update DB
+ release.releaseDate = releaseDate
+ return nil
+}
+
+func (release Release) SetArtwork(artwork string) error {
+ // TODO: update DB
+ release.artwork = artwork
+ return nil
+}
+
+func (release Release) SetBuyName(buyname string) error {
+ // TODO: update DB
+ release.buyname = buyname
+ return nil
+}
+
+func (release Release) SetBuyLink(buylink string) error {
+ // TODO: update DB
+ release.buylink = buylink
+ return nil
+}
+
+func (release Release) SetLinks(links []Link) error {
+ // TODO: update DB
+ release.links = links
+ return nil
+}
+
+func (release Release) SetDescription(description string) error {
+ // TODO: update DB
+ release.description = description
+ return nil
+}
+
+func (release Release) SetCredits(credits []Credit) error {
+ // TODO: update DB
+ release.credits = credits
+ return nil
+}
+
+func (release Release) SetTracks(tracks []Track) error {
+ // TODO: update DB
+ release.tracks = tracks
+ return nil
+}
+
+// MISC
+
+func GetRelease(id string) *Release {
+ for _, release := range Releases {
+ if release.GetID() == id {
+ return &release
+ }
+ }
+ return nil
+}
+
+func (release Release) PrintReleaseDate() string {
+ return release.releaseDate.Format("02 January 2006")
+}
+
+func (release Release) GetReleaseYear() int {
+ return release.releaseDate.Year()
+}
+
+func (release Release) IsSingle() bool {
+ return len(release.tracks) == 1;
+}
+
+func (release Release) IsReleased() bool {
+ return release.releaseDate.Before(time.Now())
+}
+
+func (release Release) 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 Release) GetUniqueArtistNames(only_primary bool) []string {
+ var names = []string{}
+ for _, artist := range release.GetUniqueArtists(only_primary) {
+ names = append(names, artist.GetName())
+ }
+
+ 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[:], ", ")
+ }
+}
+
+// DATABASE
+
+func (release Release) PushToDB(db *sqlx.DB) error {
+ // fmt.Printf("Pushing release [%s] to database...", release.id)
+
+ tx, err := db.Begin()
+ if err != nil {
+ return errors.New(fmt.Sprintf("Failed to initiate transaction: %s", err))
+ }
+
+ _, err = tx.Exec("INSERT INTO musicreleases (id, title, type, release_date, artwork, buyname, buylink) VALUES ($1, $2, $3, $4, $5, $6, $7) "+
+ "ON CONFLICT (id) DO UPDATE SET title=$2, type=$3, release_date=$4, artwork=$5, buyname=$6, buylink=$7",
+ release.id, release.title, release.releaseType, release.releaseDate.Format("2-Jan-2006"), release.artwork, release.buyname, release.buylink)
+
+ for _, link := range release.links {
+ _, err = tx.Exec(
+ "INSERT INTO musiclinks (release, name, url) "+
+ "VALUES ($1, $2, $3) "+
+ "ON CONFLICT (release, name) "+
+ "DO UPDATE SET url=$3 ",
+ release.id,
+ link.name,
+ link.url,
+ )
+ if err != nil {
+ return errors.New(fmt.Sprintf("Failed to add music link to transaction: %s", err))
+ }
+ }
+ for _, credit := range release.credits {
+ _, err = tx.Exec(
+ "INSERT INTO musiccredits (release, artist, role, is_primary) "+
+ "VALUES ($1, $2, $3, $4) "+
+ "ON CONFLICT (release, artist) "+
+ "DO UPDATE SET role=$3, is_primary=$4",
+ release.id,
+ credit.artist.id,
+ credit.role,
+ credit.primary,
+ )
+ if err != nil {
+ return errors.New(fmt.Sprintf("Failed to add music credit to transaction: %s", err))
+ }
+ }
+ for _, track := range release.tracks {
+ _, err = tx.Exec(
+ "INSERT INTO musictracks (release, number, title, description, lyrics, preview_url) "+
+ "VALUES ($1, $2, $3, $4, $5, $6) "+
+ "ON CONFLICT (release, number) "+
+ "DO UPDATE SET title=$3, description=$4, lyrics=$5, preview_url=$6",
+ release.id,
+ track.number,
+ track.title,
+ track.description,
+ track.lyrics,
+ track.previewURL,
+ )
+ if err != nil {
+ return errors.New(fmt.Sprintf("Failed to add music track to transaction: %s", err))
+ }
+ }
+
+ err = tx.Commit()
+ if err != nil {
+ return errors.New(fmt.Sprintf("Failed to commit transaction: %s", err))
+ }
+
+ // fmt.Printf("done!\n")
+
+ return nil
+}
+
+func (release Release) DeleteFromDB(db *sqlx.DB) error {
+ // this probably doesn't need to be a transaction;
+ // i just felt like making it one
+ tx, err := db.Begin()
+ if err != nil {
+ return errors.New(fmt.Sprintf("Failed to initiate transaction: %s", err))
+ }
+
+ _, err = tx.Exec("DELETE FROM musicreleases WHERE id=$1", release.id)
+
+ err = tx.Commit()
+ if err != nil {
+ return errors.New(fmt.Sprintf("Failed to commit transaction: %s", err))
+ }
+
+ return nil
+}
+
+func PullAllReleases(db *sqlx.DB) ([]Release, error) {
+ releases := []Release{}
+
+ rows, err := db.Query("SELECT id, title, description, type, release_date, artwork, buyname, buylink FROM musicreleases")
+ if err != nil {
+ return nil, err
+ }
+
+ for rows.Next() {
+ var release = Release{}
+
+ err = rows.Scan(
+ &release.id,
+ &release.title,
+ &release.description,
+ &release.releaseType,
+ &release.releaseDate,
+ &release.artwork,
+ &release.buyname,
+ &release.buylink,
+ )
+ if err != nil {
+ fmt.Printf("Error while pulling a release: %s\n", err)
+ continue
+ }
+
+ release.credits, err = PullReleaseCredits(db, release.id)
+ if err != nil {
+ fmt.Printf("Failed to pull credits for %s: %v\n", release.id, err)
+ }
+
+ release.links, err = PullReleaseLinks(db, release.id)
+ if err != nil {
+ fmt.Printf("Failed to pull links for %s: %v\n", release.id, err)
+ }
+
+ release.tracks, err = PullReleaseTracks(db, release.id)
+ if err != nil {
+ return nil, errors.New(fmt.Sprintf("error pulling tracks for %s: %v\n", release.id, err))
+ }
+
+ releases = append(releases, release)
+ }
+
+ return releases, nil
+}
diff --git a/api/v1/music/track.go b/api/v1/music/track.go
new file mode 100644
index 0000000..cda34ad
--- /dev/null
+++ b/api/v1/music/track.go
@@ -0,0 +1,100 @@
+package music
+
+import (
+ "fmt"
+
+ "github.com/jmoiron/sqlx"
+)
+
+type Track struct {
+ number int
+ title string
+ description string
+ lyrics string
+ previewURL string
+}
+
+// GETTERS
+
+func (track Track) GetNumber() int {
+ return track.number
+}
+
+func (track Track) GetTitle() string {
+ return track.title
+}
+
+func (track Track) GetDescription() string {
+ return track.description
+}
+
+func (track Track) GetLyrics() string {
+ return track.lyrics
+}
+
+func (track Track) GetPreviewURL() string {
+ return track.previewURL
+}
+
+// SETTERS
+
+func (track Track) SetNumber(number int) error {
+ // TODO: update DB
+ track.number = number
+ return nil
+}
+
+func (track Track) SetTitle(title string) error {
+ // TODO: update DB
+ track.title = title
+ return nil
+}
+
+func (track Track) SetDescription(description string) error {
+ // TODO: update DB
+ track.description = description
+ return nil
+}
+
+func (track Track) SetLyrics(lyrics string) error {
+ // TODO: update DB
+ track.lyrics = lyrics
+ return nil
+}
+
+func (track Track) SetPreviewURL(previewURL string) error {
+ // TODO: update DB
+ track.previewURL = previewURL
+ return nil
+}
+
+// DATABASE
+
+func PullReleaseTracks(db *sqlx.DB, releaseID string) ([]Track, error) {
+ var tracks = []Track{}
+
+ track_rows, err := db.Query("SELECT number, title, description, lyrics, preview_url FROM musictracks WHERE release=$1", releaseID)
+ if err != nil {
+ return []Track{}, err
+ }
+
+ for track_rows.Next() {
+ var track = Track{}
+
+ err = track_rows.Scan(
+ &track.number,
+ &track.title,
+ &track.description,
+ &track.lyrics,
+ &track.previewURL,
+ )
+ if err != nil {
+ fmt.Printf("Error while pulling track for release %s: %s\n", releaseID, err)
+ continue
+ }
+
+ tracks = append(tracks, track)
+ }
+
+ return tracks, nil
+}
diff --git a/db.go b/db.go
deleted file mode 100644
index 117e3e6..0000000
--- a/db.go
+++ /dev/null
@@ -1,204 +0,0 @@
-package main
-
-import (
- "arimelody.me/arimelody.me/api/v1/music"
-
- "fmt"
- "os"
- "time"
-
- "github.com/jmoiron/sqlx"
- _ "github.com/lib/pq"
-)
-
-func PushArtist(db *sqlx.DB, artist music.Artist) {
- fmt.Printf("pushing artist [%s] to database...", artist.Name)
-
- db.MustExec("INSERT INTO artists (id, name, website) VALUES ($1, $2, $3) ON CONFLICT (id) DO UPDATE SET name=$2, website=$3",
- &artist.Id,
- &artist.Name,
- &artist.Website,
- )
-
- fmt.Printf("done!\n")
-}
-
-func PullAllArtists(db *sqlx.DB) ([]*music.Artist, error) {
- artists := []*music.Artist{}
-
- rows, err := db.Query("SELECT id, name, website FROM artists")
- if err != nil {
- return nil, err
- }
-
- for rows.Next() {
- var artist = music.Artist{}
- err = rows.Scan(&artist.Id, &artist.Name, &artist.Website)
- if err != nil {
- return nil, err
- }
- artists = append(artists, &artist)
- }
-
- return artists, nil
-}
-
-func PullArtist(db *sqlx.DB, artistID string) (music.Artist, error) {
- artist := music.Artist{}
-
- err := db.Get(&artist, "SELECT id, name, website FROM artists WHERE id=$1", artistID)
- if err != nil {
- return music.Artist{}, err
- }
-
- return artist, nil
-}
-
-func PushRelease(db *sqlx.DB, release music.MusicRelease) {
- fmt.Printf("pushing release [%s] to database...", release.Id)
-
- tx := db.MustBegin()
- tx.MustExec("INSERT INTO musicreleases (id, title, type, release_date, artwork, buyname, buylink) VALUES ($1, $2, $3, $4, $5, $6, $7) "+
- "ON CONFLICT (id) DO UPDATE SET title=$2, type=$3, release_date=$4, artwork=$5, buyname=$6, buylink=$7",
- &release.Id, &release.Title, &release.Type, release.ReleaseDate.Format("2-Jan-2006"), &release.Artwork, &release.Buyname, &release.Buylink)
-
- for _, link := range release.Links {
- tx.MustExec("INSERT INTO musiclinks (release, name, url) VALUES ($1, $2, $3) ON CONFLICT (release, name) DO UPDATE SET url=$3",
- &release.Id, &link.Name, &link.Url)
- }
- for _, credit := range release.Credits {
- tx.MustExec("INSERT INTO musiccredits (release, artist, role, is_primary) VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING",
- &release.Id, &credit.Artist.Id, &credit.Role, &credit.Primary)
- }
- for _, track := range release.Tracks {
- tx.MustExec("INSERT INTO musictracks (release, number, title, description, lyrics, preview_url) VALUES ($1, $2, $3, $4, $5, $6) "+
- "ON CONFLICT (release, number) DO UPDATE SET title=$3, description=$4, lyrics=$5, preview_url=$6",
- &release.Id, &track.Number, &track.Title, &track.Description, &track.Lyrics, &track.PreviewUrl)
- }
-
- tx.Commit()
-
- fmt.Printf("done!\n")
-}
-
-func PullAllReleases(db *sqlx.DB) ([]*music.MusicRelease, error) {
- releases := []*music.MusicRelease{}
-
- rows, err := db.Query("SELECT id, title, type, release_date, artwork, buyname, buylink FROM musicreleases")
- if err != nil {
- return nil, err
- }
-
- for rows.Next() {
- var release = music.MusicRelease{}
- release.Credits = []music.MusicCredit{}
- release.Links = []music.MusicLink{}
- release.Tracks = []music.MusicTrack{}
-
- err = rows.Scan(
- &release.Id,
- &release.Title,
- &release.Type,
- &release.ReleaseDate,
- &release.Artwork,
- &release.Buyname,
- &release.Buylink)
- if err != nil {
- continue
- }
-
- // pull musiccredits for artist data
- credit_rows, err := db.Query("SELECT artist, role, is_primary FROM musiccredits WHERE release=$1", release.Id)
- if err != nil {
- fmt.Printf("error pulling credits for %s: %v\n", release.Id, err)
- continue
- }
- for credit_rows.Next() {
- var artistID string
- var credit = music.MusicCredit{}
- err = credit_rows.Scan(
- &artistID,
- &credit.Role,
- &credit.Primary)
- if err != nil {
- fmt.Printf("error pulling credit for %s: %v\n", release.Id, err)
- continue
- }
- artist, err := music.GetArtist(artistID)
- if err != nil {
- fmt.Printf("error pulling credit for %s: %v\n", release.Id, err)
- continue
- }
- credit.Artist = artist
- release.Credits = append(release.Credits, credit)
- }
-
- // pull musiclinks for link data
- link_rows, err := db.Query("SELECT name, url FROM musiclinks WHERE release=$1", release.Id);
- if err != nil {
- fmt.Printf("error pulling links for %s: %v\n", release.Id, err)
- continue
- }
- for link_rows.Next() {
- var link = music.MusicLink{}
- err = link_rows.Scan(
- &link.Name,
- &link.Url)
- if err != nil {
- fmt.Printf("error pulling link for %s: %v\n", release.Id, err)
- continue
- }
- release.Links = append(release.Links, link)
- }
-
- // pull musictracks for track data
- track_rows, err := db.Query("SELECT number, title, description, lyrics, preview_url FROM musictracks WHERE release=$1", release.Id);
- if err != nil {
- fmt.Printf("error pulling tracks for %s: %v\n", release.Id, err)
- continue
- }
- for track_rows.Next() {
- var track = music.MusicTrack{}
- err = track_rows.Scan(
- &track.Number,
- &track.Title,
- &track.Description,
- &track.Lyrics,
- &track.PreviewUrl)
- if err != nil {
- fmt.Printf("error pulling track for %s: %v\n", release.Id, err)
- continue
- }
- release.Tracks = append(release.Tracks, track)
- }
-
- releases = append(releases, &release)
- }
-
- return releases, nil
-}
-
-func PullRelease(db *sqlx.DB, releaseID string) (music.MusicRelease, error) {
- release := music.MusicRelease{}
-
- err := db.Get(&release, "SELECT id, title, type, release_date, artwork, buyname, buylink FROM musicreleases WHERE id=$1", releaseID)
- if err != nil {
- return music.MusicRelease{}, err
- }
-
- return release, nil
-}
-
-func InitDatabase() *sqlx.DB {
- db, err := sqlx.Connect("postgres", "user=arimelody dbname=arimelody password=fuckingpassword sslmode=disable")
- if err != nil {
- fmt.Fprintf(os.Stderr, "unable to create database connection pool: %v\n", err)
- os.Exit(1)
- }
-
- db.SetConnMaxLifetime(time.Minute * 3)
- db.SetMaxOpenConns(10)
- db.SetMaxIdleConns(10)
-
- return db
-}
diff --git a/db/db.go b/db/db.go
new file mode 100644
index 0000000..6006f73
--- /dev/null
+++ b/db/db.go
@@ -0,0 +1,24 @@
+package db
+
+import (
+ "fmt"
+ "os"
+ "time"
+
+ "github.com/jmoiron/sqlx"
+ _ "github.com/lib/pq"
+)
+
+func InitDatabase() *sqlx.DB {
+ db, err := sqlx.Connect("postgres", "user=arimelody dbname=arimelody password=fuckingpassword sslmode=disable")
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "unable to create database connection pool: %v\n", err)
+ os.Exit(1)
+ }
+
+ db.SetConnMaxLifetime(time.Minute * 3)
+ db.SetMaxOpenConns(10)
+ db.SetMaxIdleConns(10)
+
+ return db
+}
diff --git a/global/global.go b/global/global.go
index f7a1081..a92f51b 100644
--- a/global/global.go
+++ b/global/global.go
@@ -3,6 +3,9 @@ package global
import (
"fmt"
"net/http"
+ "os"
+ "path/filepath"
+ "html/template"
"strconv"
"time"
)
@@ -19,20 +22,11 @@ var MimeTypes = map[string]string{
"js": "application/javascript",
}
-var LAST_MODIFIED = time.Now()
-
-func IsModified(req *http.Request, last_modified time.Time) bool {
- if len(req.Header["If-Modified-Since"]) == 0 || len(req.Header["If-Modified-Since"][0]) == 0 {
- return true
- }
- request_time, err := time.Parse(http.TimeFormat, req.Header["If-Modified-Since"][0])
- if err != nil {
- return true
- }
- if request_time.Before(last_modified) {
- return true
- }
- return false
+func DefaultHeaders(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Add("Server", "arimelody.me")
+ w.Header().Add("Cache-Control", "max-age=2592000")
+ })
}
type LoggingResponseWriter struct {
@@ -69,3 +63,40 @@ func HTTPLog(next http.Handler) http.Handler {
r.Header["User-Agent"][0])
})
}
+
+func ServeTemplate(page string, data any) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ lp_layout := filepath.Join("views", "layout.html")
+ lp_header := filepath.Join("views", "header.html")
+ lp_footer := filepath.Join("views", "footer.html")
+ lp_prideflag := filepath.Join("views", "prideflag.html")
+ fp := filepath.Join("views", filepath.Clean(page))
+
+ info, err := os.Stat(fp)
+ if err != nil {
+ if os.IsNotExist(err) {
+ http.NotFound(w, r)
+ return
+ }
+ }
+
+ if info.IsDir() {
+ http.NotFound(w, r)
+ return
+ }
+
+ template, err := template.ParseFiles(lp_layout, lp_header, lp_footer, lp_prideflag, fp)
+ if err != nil {
+ fmt.Printf("Error parsing template files: %s\n", err)
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+ return
+ }
+
+ err = template.ExecuteTemplate(w, "layout.html", data)
+ if err != nil {
+ fmt.Printf("Error executing template: %s\n", err)
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+ return
+ }
+ })
+}
diff --git a/main.go b/main.go
index 5758d45..385c1e7 100644
--- a/main.go
+++ b/main.go
@@ -2,7 +2,6 @@ package main
import (
"fmt"
- "html/template"
"log"
"net/http"
"os"
@@ -10,47 +9,57 @@ import (
"arimelody.me/arimelody.me/api/v1/admin"
"arimelody.me/arimelody.me/api/v1/music"
+ "arimelody.me/arimelody.me/db"
"arimelody.me/arimelody.me/global"
)
const DEFAULT_PORT int = 8080
func main() {
- db := InitDatabase()
+ db := db.InitDatabase()
defer db.Close()
var err error
- music.Artists, err = PullAllArtists(db)
+ music.Artists, err = music.PullAllArtists(db)
if err != nil {
fmt.Printf("Failed to pull artists from database: %v\n", err);
panic(1)
}
- music.Releases, err = PullAllReleases(db)
+ fmt.Printf("%d artists loaded successfully.\n", len(music.Artists))
+
+ music.Releases, err = music.PullAllReleases(db)
if err != nil {
fmt.Printf("Failed to pull releases from database: %v\n", err);
panic(1)
}
+ fmt.Printf("%d releases loaded successfully.\n", len(music.Releases))
- mux := http.NewServeMux()
-
- mux.Handle("/api/v1/admin/", global.HTTPLog(http.StripPrefix("/api/v1/admin", admin.Handler())))
-
- mux.Handle("/music/", global.HTTPLog(http.StripPrefix("/music", musicGatewayHandler())))
- mux.Handle("/music", global.HTTPLog(serveTemplate("music.html", music.Releases)))
-
- mux.Handle("/", global.HTTPLog(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- if r.URL.Path == "/" || r.URL.Path == "/index.html" {
- serveTemplate("index.html", nil).ServeHTTP(w, r)
- return
- }
- staticHandler().ServeHTTP(w, r)
- })))
+ mux := createServeMux()
port := DEFAULT_PORT
fmt.Printf("now serving at http://127.0.0.1:%d\n", port)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), mux))
}
+func createServeMux() *http.ServeMux {
+ mux := http.NewServeMux()
+
+ mux.Handle("/api/v1/admin/", global.HTTPLog(http.StripPrefix("/api/v1/admin", admin.Handler())))
+
+ mux.Handle("/music/", global.HTTPLog(http.StripPrefix("/music", music.ServeGateway())))
+ mux.Handle("/music", global.HTTPLog(music.ServeCatalog()))
+
+ mux.Handle("/", global.HTTPLog(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path == "/" || r.URL.Path == "/index.html" {
+ global.ServeTemplate("index.html", nil).ServeHTTP(w, r)
+ return
+ }
+ staticHandler().ServeHTTP(w, r)
+ })))
+
+ return mux
+}
+
func staticHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
info, err := os.Stat(filepath.Join("public", filepath.Clean(r.URL.Path)))
@@ -71,60 +80,3 @@ func staticHandler() http.Handler {
http.FileServer(http.Dir("./public")).ServeHTTP(w, r)
})
}
-
-func musicGatewayHandler() http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- id := r.URL.Path[1:]
- release, err := music.GetRelease(id)
- if err != nil {
- http.NotFound(w, r)
- return
- }
-
- lrw := global.LoggingResponseWriter{w, http.StatusOK}
-
- serveTemplate("music-gateway.html", release).ServeHTTP(&lrw, r)
-
- if lrw.Code != http.StatusOK {
- fmt.Printf("Error loading music gateway for %s\n", id)
- return
- }
- })
-}
-
-func serveTemplate(page string, data any) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- lp_layout := filepath.Join("views", "layout.html")
- lp_header := filepath.Join("views", "header.html")
- lp_footer := filepath.Join("views", "footer.html")
- lp_prideflag := filepath.Join("views", "prideflag.html")
- fp := filepath.Join("views", filepath.Clean(page))
-
- info, err := os.Stat(fp)
- if err != nil {
- if os.IsNotExist(err) {
- http.NotFound(w, r)
- return
- }
- }
-
- if info.IsDir() {
- http.NotFound(w, r)
- return
- }
-
- template, err := template.ParseFiles(lp_layout, lp_header, lp_footer, lp_prideflag, fp)
- if err != nil {
- fmt.Printf("Error parsing template files: %s\n", err)
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
- return
- }
-
- err = template.ExecuteTemplate(w, "layout.html", data)
- if err != nil {
- fmt.Printf("Error executing template: %s\n", err)
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
- return
- }
- })
-}
diff --git a/schema.sql b/schema.sql
index 3a4bf98..7003f5c 100644
--- a/schema.sql
+++ b/schema.sql
@@ -1,3 +1,6 @@
+--
+-- Artists (should be applicable to all art)
+--
CREATE TABLE IF NOT EXISTS artists (
id text NOT NULL,
name text,
@@ -5,9 +8,13 @@ CREATE TABLE IF NOT EXISTS artists (
);
ALTER TABLE artists ADD CONSTRAINT artists_pk PRIMARY KEY (id);
+--
+-- Music releases
+--
CREATE TABLE IF NOT EXISTS musicreleases (
id character varying(64) NOT NULL,
title text NOT NULL,
+ description text,
type text,
release_date DATE NOT NULL,
artwork text,
@@ -16,6 +23,9 @@ CREATE TABLE IF NOT EXISTS musicreleases (
);
ALTER TABLE musicreleases ADD CONSTRAINT musicreleases_pk PRIMARY KEY (id);
+--
+-- Music links (external platform links under a release)
+--
CREATE TABLE IF NOT EXISTS musiclinks (
release character varying(64) NOT NULL,
name text NOT NULL,
@@ -23,6 +33,9 @@ CREATE TABLE IF NOT EXISTS musiclinks (
);
ALTER TABLE musiclinks ADD CONSTRAINT musiclinks_pk PRIMARY KEY (release, name);
+--
+-- Music credits (artist credits under a release)
+--
CREATE TABLE IF NOT EXISTS musiccredits (
release character varying(64) NOT NULL,
artist text NOT NULL,
@@ -31,6 +44,9 @@ CREATE TABLE IF NOT EXISTS musiccredits (
);
ALTER TABLE musiccredits ADD CONSTRAINT musiccredits_pk PRIMARY KEY (release, artist);
+--
+-- Music tracks (tracks under a release)
+--
CREATE TABLE IF NOT EXISTS musictracks (
release character varying(64) NOT NULL,
number integer NOT NULL,
@@ -41,12 +57,14 @@ CREATE TABLE IF NOT EXISTS musictracks (
);
ALTER TABLE musictracks ADD CONSTRAINT musictracks_pk PRIMARY KEY (release, number);
--- foreign keys
+--
+-- Foreign keys
+--
-ALTER TABLE public.musiccredits ADD CONSTRAINT musiccredits_artist_fk FOREIGN KEY (artist) REFERENCES public.artists(id) ON DELETE CASCADE;
+ALTER TABLE public.musiccredits ADD CONSTRAINT IF NOT EXISTS musiccredits_artist_fk FOREIGN KEY (artist) REFERENCES public.artists(id) ON DELETE CASCADE;
-ALTER TABLE public.musiccredits ADD CONSTRAINT musiccredits_release_fk FOREIGN KEY (release) REFERENCES public.musicreleases(id) ON DELETE CASCADE;
+ALTER TABLE public.musiccredits ADD CONSTRAINT IF NOT EXISTS musiccredits_release_fk FOREIGN KEY (release) REFERENCES public.musicreleases(id) ON DELETE CASCADE;
-ALTER TABLE public.musiclinks ADD CONSTRAINT musiclinks_release_fk FOREIGN KEY (release) REFERENCES public.musicreleases(id) ON UPDATE CASCADE ON DELETE CASCADE;
+ALTER TABLE public.musiclinks ADD CONSTRAINT IF NOT EXISTS musiclinks_release_fk FOREIGN KEY (release) REFERENCES public.musicreleases(id) ON UPDATE CASCADE ON DELETE CASCADE;
-ALTER TABLE public.musictracks ADD CONSTRAINT musictracks_release_fk FOREIGN KEY (release) REFERENCES public.musicreleases(id) ON DELETE CASCADE;
+ALTER TABLE public.musictracks ADD CONSTRAINT IF NOT EXISTS musictracks_release_fk FOREIGN KEY (release) REFERENCES public.musicreleases(id) ON DELETE CASCADE;
diff --git a/views/music-gateway.html b/views/music-gateway.html
index d3ad05a..c713412 100644
--- a/views/music-gateway.html
+++ b/views/music-gateway.html
@@ -1,28 +1,28 @@
{{define "head"}}
-
{{.Title}} - {{.PrintPrimaryArtists false true}}
-
+{{.GetTitle}} - {{.PrintArtists true true}}
+
-
-
-
+
+
+
-
+
-
-
-
+
+
+
-
-
-
-
-
+
+
+
+
+
@@ -32,7 +32,7 @@
-
+
<
@@ -47,51 +47,51 @@
-
+
-
{{.Title}}
+ {{.GetTitle}}
{{.GetReleaseYear}}
-
{{.PrintPrimaryArtists false true}}
-
{{.ResolveType}}
+
{{.PrintArtists true true}}
+
{{.GetType}}
- {{if .Description}}
+ {{if .GetDescription}}
- {{.Description}}
+ {{.GetDescription}}
{{end}}
- {{if .Credits}}
+ {{if .GetCredits}}
credits:
- {{range .Credits}}
- {{$Artist := .ResolveArtist}}
- {{if $Artist.Website}}
- - {{$Artist.Name}}: {{.Role}}
+ {{range .GetCredits}}
+ {{$Artist := .GetArtist}}
+ {{if $Artist.GetWebsite}}
+ - {{$Artist.GetName}}: {{.GetRole}}
{{else}}
- - {{$Artist.Name}}: {{.Role}}
+ - {{$Artist.GetName}}: {{.GetRole}}
{{end}}
{{end}}
@@ -99,38 +99,38 @@
{{end}}
{{if .IsSingle}}
- {{$Track := index .Tracks 0}}
- {{if $Track.Lyrics}}
+ {{$Track := index .GetTracks 0}}
+ {{if $Track.GetLyrics}}
lyrics:
-
{{$Track.Lyrics}}
+
{{$Track.GetLyrics}}
{{end}}
{{else}}
tracks:
- {{range .Tracks}}
+ {{range .GetTracks}}
- {{.Title}}
- {{.Lyrics}}
+ {{.GetTitle}}
+ {{.GetLyrics}}
{{end}}
{{end}}
- {{if or .Credits not .IsSingle}}
+ {{if or .GetCredits not .IsSingle}}