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}} artwork + {{.GetTitle}} artwork
-

{{.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}}
  • overview
  • - {{if .Credits}} + {{if .GetCredits}}
  • credits
  • {{end}} {{if .IsSingle}} - {{$Track := index .Tracks 0}} - {{if $Track.Lyrics}} + {{$Track := index .GetTracks 0}} + {{if $Track.GetLyrics}}
  • lyrics
  • {{end}} {{else}} @@ -158,7 +158,7 @@ - + diff --git a/views/music.html b/views/music.html index 03b98b1..f523a09 100644 --- a/views/music.html +++ b/views/music.html @@ -25,19 +25,19 @@
    - {{range $Album := .}} -
    + {{range $Release := .}} +
    - {{$Album.Title}} artwork + {{$Release.GetTitle}} artwork
    -

    {{$Album.Title}}

    -

    {{$Album.PrintPrimaryArtists false true}}

    -

    {{$Album.ResolveType}}

    +

    {{$Release.GetTitle}}

    +

    {{$Release.PrintArtists true true}}

    +

    {{$Release.GetType}}