diff --git a/.air.toml b/.air.toml index 04ea619..3f45b18 100644 --- a/.air.toml +++ b/.air.toml @@ -7,13 +7,13 @@ tmp_dir = "tmp" bin = "./tmp/main" cmd = "go build -o ./tmp/main ." delay = 1000 - exclude_dir = ["assets", "tmp", "vendor", "testdata"] + exclude_dir = ["admin\\static", "public", "uploads"] exclude_file = [] exclude_regex = ["_test.go"] exclude_unchanged = false follow_symlink = false full_bin = "" - include_dir = [".", "admin", "colour", "db", "discord", "global", "music", "views"] + include_dir = [] include_ext = ["go", "tpl", "tmpl"] include_file = [] kill_delay = "0s" diff --git a/.gitignore b/.gitignore index 8329c31..77222bb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ .idea/ tmp/ test/ -data/* +uploads/* diff --git a/admin/admin.go b/admin/admin.go index 4a34d7f..d5ab6e7 100644 --- a/admin/admin.go +++ b/admin/admin.go @@ -9,9 +9,9 @@ import ( type ( Session struct { - UserID string - Token string - Expires int64 + Token string + UserID string + Expires time.Time } ) @@ -28,11 +28,11 @@ var ADMIN_ID_DISCORD = func() string { var sessions []*Session -func createSession(UserID string) Session { +func createSession(username string, expires time.Time) Session { return Session{ - UserID: UserID, Token: string(generateToken()), - Expires: time.Now().Add(24 * time.Hour).Unix(), + UserID: username, + Expires: expires, } } diff --git a/admin/http.go b/admin/http.go index ff6d48a..219e62e 100644 --- a/admin/http.go +++ b/admin/http.go @@ -12,71 +12,106 @@ import ( "arimelody.me/arimelody.me/discord" "arimelody.me/arimelody.me/global" + musicModel "arimelody.me/arimelody.me/music/model" ) func Handler() http.Handler { mux := http.NewServeMux() + mux.Handle("/login", LoginHandler()) + mux.Handle("/logout", MustAuthorise(LogoutHandler())) + mux.Handle("/static/", http.StripPrefix("/static", staticHandler())) mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte("hello /admin!")) + if r.URL.Path != "/" { + http.NotFound(w, r) + return + } + + session := GetSession(r) + if session == nil { + http.Redirect(w, r, "/admin/login", http.StatusFound) + return + } + + type IndexData struct { + Releases []musicModel.Release + Artists []musicModel.Artist + } + + serveTemplate("index.html", IndexData{ + Releases: global.Releases, + Artists: global.Artists, + }).ServeHTTP(w, r) })) - mux.Handle("/callback", global.HTTPLog(OAuthCallbackHandler())) - mux.Handle("/login", global.HTTPLog(LoginHandler())) - mux.Handle("/verify", global.HTTPLog(MustAuthorise(VerifyHandler()))) - mux.Handle("/logout", global.HTTPLog(MustAuthorise(LogoutHandler()))) - mux.Handle("/static", global.HTTPLog(MustAuthorise(staticHandler()))) return mux } func MustAuthorise(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - auth := r.Header.Get("Authorization") - if strings.HasPrefix(auth, "Bearer ") { - auth = auth[7:] - } else { - cookie, err := r.Cookie("token") - if err != nil { - http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) - return - } - auth = cookie.Value - } - - var session *Session - for _, s := range sessions { - if s.Expires < time.Now().Unix() { - // expired session. remove it from the list! - new_sessions := []*Session{} - for _, ns := range sessions { - if ns.Token == s.Token { - continue - } - new_sessions = append(new_sessions, ns) - } - continue - } - - if s.Token == auth { - session = s - break - } - } - + session := GetSession(r) if session == nil { http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) return } - ctx := context.WithValue(r.Context(), "role", "admin") + ctx := context.WithValue(r.Context(), "session", session) next.ServeHTTP(w, r.WithContext(ctx)) }) } +func GetSession(r *http.Request) *Session { + // TODO: remove later- this bypasses auth! + return &Session{} + + var token = "" + // is the session token in context? + var ctx_session = r.Context().Value("session") + if ctx_session != nil { + token = ctx_session.(string) + } + // okay, is it in the auth header? + if token == "" { + if strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") { + token = r.Header.Get("Authorization")[7:] + } + } + // finally, is it in the cookie? + if token == "" { + cookie, err := r.Cookie("token") + if err != nil { + return nil + } + token = cookie.Value + } + + var session *Session = nil + for _, s := range sessions { + if s.Expires.Before(time.Now()) { + // expired session. remove it from the list! + new_sessions := []*Session{} + for _, ns := range sessions { + if ns.Token == s.Token { + continue + } + new_sessions = append(new_sessions, ns) + } + sessions = new_sessions + continue + } + + if s.Token == token { + session = s + break + } + } + + return session +} + func LoginHandler() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if ADMIN_ID_DISCORD == "" { + if discord.CREDENTIALS_PROVIDED && ADMIN_ID_DISCORD == "" { http.Error(w, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable) return } @@ -84,7 +119,7 @@ func LoginHandler() http.Handler { code := r.URL.Query().Get("code") if code == "" { - http.Redirect(w, r, discord.REDIRECT_URI, http.StatusTemporaryRedirect) + serveTemplate("login.html", discord.REDIRECT_URI).ServeHTTP(w, r) return } @@ -109,7 +144,7 @@ func LoginHandler() http.Handler { } // login success! - session := createSession(discord_user.Username) + session := createSession(discord_user.Username, time.Now().Add(24 * time.Hour)) sessions = append(sessions, &session) cookie := http.Cookie{} @@ -122,12 +157,25 @@ func LoginHandler() http.Handler { http.SetCookie(w, &cookie) w.WriteHeader(http.StatusOK) - w.Write([]byte(session.Token)) + w.Header().Add("Content-Type", "text/html") + w.Write([]byte( + ""+ + ""+ + ""+ + "Logged in successfully. "+ + "You should be redirected to /admin/ in 5 seconds."+ + ""), + ) }) } func LogoutHandler() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.NotFound(w, r) + return + } + token := r.Context().Value("token").(string) if token == "" { @@ -145,31 +193,19 @@ func LogoutHandler() http.Handler { }(token) w.WriteHeader(http.StatusOK) - w.Write([]byte("OK")) + w.Write([]byte( + ""+ + "Logged out successfully. "+ + "You should be redirected to / in 5 seconds."), + ) }) } -func OAuthCallbackHandler() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - }) -} - -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(http.StatusOK) - w.Write([]byte("OK")) - }) -} - -func ServeTemplate(page string, data any) http.Handler { +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_layout := filepath.Join("views", "admin", "layout.html") lp_prideflag := filepath.Join("views", "prideflag.html") - fp := filepath.Join("views", filepath.Clean(page)) + fp := filepath.Join("views", "admin", filepath.Clean(page)) info, err := os.Stat(fp) if err != nil { @@ -184,7 +220,7 @@ func ServeTemplate(page string, data any) http.Handler { return } - template, err := template.ParseFiles(lp_layout, lp_header, lp_footer, lp_prideflag, fp) + template, err := template.ParseFiles(lp_layout, lp_prideflag, fp) if err != nil { fmt.Printf("Error parsing template files: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) diff --git a/admin/static/admin.css b/admin/static/admin.css new file mode 100644 index 0000000..453c4d6 --- /dev/null +++ b/admin/static/admin.css @@ -0,0 +1,187 @@ +@import url("/style/prideflag.css"); +@import url("/font/inter/inter.css"); + +body { + width: 100%; + height: 100vh; + + margin: 0; + padding: 0; + + font-family: "Inter", sans-serif; + font-size: 16px; + + color: #303030; + background: #f0f0f0; +} + +header { + width: min(720px, calc(100% - 2em)); + height: 2em; + margin: 1em auto; + display: flex; + flex-direction: row; + justify-content: center; + + background: #f8f8f8; + border-radius: .5em; + border: 1px solid #808080; +} +header .icon { + height: 100%; + + margin-right: 1em; +} + +header a { + height: 100%; + width: auto; + + margin: 0px; + padding: 0 1em; + + display: flex; + + line-height: 2em; + text-decoration: none; + + color: inherit; +} + +header a:hover { + background: #00000010; +} + +main { + width: min(720px, calc(100% - 2em)); + margin: 0 auto; + padding: 1em; +} + +a { + color: inherit; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +.card h2 { + margin: 0 0 .5em 0; +} + +.card h3, +.card p { + margin: 0; +} + +.release { + padding: 1em; + display: flex; + flex-direction: row; + gap: 1em; + + border-radius: .5em; + background: #f8f8f8f8; + border: 1px solid #808080; +} + +.release-artwork { + width: 96px; + + display: flex; + justify-content: center; + align-items: center; +} + +.release-artwork img { + width: 100%; + aspect-ratio: 1; +} + +.latest-release .release-info { + width: 300px; + flex-direction: column; +} + +.release-title small { + opacity: .75; +} + +.release-links { + margin: .5em 0; + padding: 0; + display: flex; + flex-direction: row; + list-style: none; + flex-wrap: wrap; + gap: .5em; +} + +.release-links li { + flex-grow: 1; +} + +.release-links a { + padding: .5em; + display: block; + + border-radius: .5em; + text-decoration: none; + color: #f0f0f0; + background: #303030; + text-align: center; + + transition: color .1s, background .1s; +} + +.release-links a:hover { + color: #303030; + background: #f0f0f0; +} + +.release-actions { + margin-top: .5em; +} + +.release-actions a { + margin-right: .3em; + padding: .3em .5em; + display: inline-block; + + border-radius: .3em; + background: #e0e0e0; + + transition: color .1s, background .1s; +} + +.release-actions a:hover { + color: #303030; + background: #f0f0f0; + + text-decoration: none; +} + +.artist { + padding: .5em; + display: flex; + flex-direction: row; + align-items: center; + gap: .5em; + + border-radius: .5em; + background: #f8f8f8f8; + border: 1px solid #808080; +} + +.artist:hover { + text-decoration: hover; +} + +.artist-avatar { + width: 32px; + height: 32px; + object-fit: cover; + border-radius: 100%; +} diff --git a/admin/static/admin.js b/admin/static/admin.js new file mode 100644 index 0000000..e69de29 diff --git a/api/api.go b/api/api.go new file mode 100644 index 0000000..e44c261 --- /dev/null +++ b/api/api.go @@ -0,0 +1,43 @@ +package api + +import ( + "net/http" + + "arimelody.me/arimelody.me/admin" + music "arimelody.me/arimelody.me/music/view" +) + +func Handler() http.Handler { + mux := http.NewServeMux() + + mux.Handle("/v1/artist/", http.StripPrefix("/v1/artist", ServeArtist())) + mux.Handle("/v1/artist", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + ServeAllArtists().ServeHTTP(w, r) + return + case http.MethodPost: + admin.MustAuthorise(CreateArtist()).ServeHTTP(w, r) + return + default: + http.NotFound(w, r) + return + } + })) + mux.Handle("/v1/music/", http.StripPrefix("/v1/music", music.ServeRelease())) + mux.Handle("/v1/music", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + ServeCatalog().ServeHTTP(w, r) + return + case http.MethodPost: + admin.MustAuthorise(CreateRelease()).ServeHTTP(w, r) + return + default: + http.NotFound(w, r) + return + } + })) + + return mux +} diff --git a/api/artist.go b/api/artist.go new file mode 100644 index 0000000..7011ff5 --- /dev/null +++ b/api/artist.go @@ -0,0 +1,130 @@ +package api + +import ( + "encoding/json" + "fmt" + "net/http" + + "arimelody.me/arimelody.me/global" + "arimelody.me/arimelody.me/music/model" + controller "arimelody.me/arimelody.me/music/controller" +) + +func ServeAllArtists() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + type ( + creditJSON struct { + Role string `json:"role"` + Primary bool `json:"primary"` + } + ) + + var artists = []model.Artist{} + for _, artist := range global.Artists { + artists = append(artists, model.Artist{ + ID: artist.ID, + Name: artist.Name, + Website: artist.Website, + Avatar: artist.Avatar, + }) + } + + w.Header().Add("Content-Type", "application/json") + err := json.NewEncoder(w).Encode(artists) + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + }) +} + +func ServeArtist() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" { + ServeAllArtists().ServeHTTP(w, r) + return + } + + type ( + creditJSON struct { + Role string `json:"role"` + Primary bool `json:"primary"` + } + artistJSON struct { + model.Artist + Credits map[string]creditJSON `json:"credits"` + } + ) + var res = artistJSON{} + + res.ID = r.URL.Path[1:] + var artist = global.GetArtist(res.ID) + if artist == nil { + http.NotFound(w, r) + return + } + res.Name = artist.Name + res.Website = artist.Website + res.Credits = make(map[string]creditJSON) + + for _, release := range global.Releases { + for _, credit := range release.Credits { + if credit.Artist.ID != res.ID { + continue + } + res.Credits[release.ID] = creditJSON{ + Role: credit.Role, + Primary: credit.Primary, + } + } + } + + w.Header().Add("Content-Type", "application/json") + err := json.NewEncoder(w).Encode(res) + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + }) +} + +func CreateArtist() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.NotFound(w, r) + return + } + + var data model.Artist + err := json.NewDecoder(r.Body).Decode(&data) + if err != nil { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + if global.GetArtist(data.ID) != nil { + http.Error(w, fmt.Sprintf("Artist %s already exists", data.ID), http.StatusBadRequest) + return + } + + var artist = model.Artist{ + ID: data.ID, + Name: data.Name, + Website: data.Website, + Avatar: data.Avatar, + } + + global.Artists = append(global.Artists, artist) + + err = controller.CreateArtistDB(global.DB, &artist) + if err != nil { + fmt.Printf("Failed to create artist %s: %s\n", artist.ID, err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + err = json.NewEncoder(w).Encode(artist) + }) +} diff --git a/api/music.go b/api/music.go new file mode 100644 index 0000000..22695cb --- /dev/null +++ b/api/music.go @@ -0,0 +1,92 @@ +package api + +import ( + "encoding/json" + "fmt" + "net/http" + "time" + + "arimelody.me/arimelody.me/admin" + "arimelody.me/arimelody.me/global" + "arimelody.me/arimelody.me/music/model" + controller "arimelody.me/arimelody.me/music/controller" +) + +func ServeCatalog() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + releases := []model.Release{} + authorised := admin.GetSession(r) != nil + for _, release := range global.Releases { + if !release.IsReleased() && !authorised { + continue + } + releases = append(releases, release) + } + + w.Header().Add("Content-Type", "application/json") + err := json.NewEncoder(w).Encode(releases) + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + }) +} + +func CreateRelease() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.NotFound(w, r) + return + } + + type PostReleaseBody struct { + ID string `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + ReleaseType model.ReleaseType `json:"type"` + ReleaseDate time.Time `json:"releaseDate"` + Artwork string `json:"artwork"` + Buyname string `json:"buyname"` + Buylink string `json:"buylink"` + } + + var data PostReleaseBody + err := json.NewDecoder(r.Body).Decode(&data) + if err != nil { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + if global.GetRelease(data.ID) != nil { + http.Error(w, fmt.Sprintf("Release %s already exists", data.ID), http.StatusBadRequest) + return + } + + var release = model.Release{ + ID: data.ID, + Title: data.Title, + Description: data.Description, + ReleaseType: data.ReleaseType, + ReleaseDate: data.ReleaseDate, + Artwork: data.Artwork, + Buyname: data.Buyname, + Buylink: data.Buylink, + Links: []model.Link{}, + Credits: []model.Credit{}, + Tracks: []model.Track{}, + } + + global.Releases = append([]model.Release{release}, global.Releases...) + + err = controller.CreateReleaseDB(global.DB, &release) + if err != nil { + fmt.Printf("Failed to create release %s: %s\n", release.ID, err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + err = json.NewEncoder(w).Encode(release) + }) +} diff --git a/db/db.go b/db/db.go deleted file mode 100644 index 6006f73..0000000 --- a/db/db.go +++ /dev/null @@ -1,24 +0,0 @@ -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/discord/discord.go b/discord/discord.go index c6a1d7a..5190b09 100644 --- a/discord/discord.go +++ b/discord/discord.go @@ -12,10 +12,12 @@ import ( const API_ENDPOINT = "https://discord.com/api/v10" +var CREDENTIALS_PROVIDED = true var CLIENT_ID = func() string { envvar := os.Getenv("DISCORD_CLIENT_ID") if envvar == "" { fmt.Printf("DISCORD_CLIENT_ID was not provided. Admin login will be unavailable.\n") + CREDENTIALS_PROVIDED = false } return envvar }() @@ -23,6 +25,7 @@ var CLIENT_SECRET = func() string { envvar := os.Getenv("DISCORD_CLIENT_SECRET") if envvar == "" { fmt.Printf("DISCORD_CLIENT_SECRET was not provided. Admin login will be unavailable.\n") + CREDENTIALS_PROVIDED = false } return envvar }() @@ -30,6 +33,7 @@ var REDIRECT_URI = func() string { envvar := os.Getenv("DISCORD_REDIRECT_URI") if envvar == "" { fmt.Printf("DISCORD_REDIRECT_URI was not provided. Admin login will be unavailable.\n") + CREDENTIALS_PROVIDED = false } return envvar }() @@ -37,14 +41,15 @@ var OAUTH_CALLBACK_URI = func() string { envvar := os.Getenv("OAUTH_CALLBACK_URI") if envvar == "" { fmt.Printf("OAUTH_CALLBACK_URI was not provided. Admin login will be unavailable.\n") + CREDENTIALS_PROVIDED = false } return envvar }() type ( AccessTokenResponse struct { - TokenType string `json:"token_type"` AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` ExpiresIn int `json:"expires_in"` RefreshToken string `json:"refresh_token"` Scope string `json:"scope"` @@ -52,27 +57,27 @@ type ( AuthInfoResponse struct { Application struct { - Id string - Name string - Icon string - Description string - Hook bool - BotPublic bool - botRequireCodeGrant bool - VerifyKey bool - } - Scopes []string - Expires string - User DiscordUser + Id string `json:"id"` + Name string `json:"name"` + Icon string `json:"icon"` + Description string `json:"description"` + Hook bool `json:"hook"` + BotPublic bool `json:"bot_public"` + BotRequireCodeGrant bool `json:"bot_require_code_grant"` + VerifyKey string `json:"verify_key"` + } `json:"application"` + Scopes []string `json:"scopes"` + Expires string `json:"expires"` + User DiscordUser `json:"user"` } DiscordUser struct { - Id string - Username string - Avatar string - Discriminator string - GlobalName string - PublicFlags int + Id string `json:"id"` + Username string `json:"username"` + Avatar string `json:"avatar"` + Discriminator string `json:"discriminator"` + GlobalName string `json:"global_name"` + PublicFlags int `json:"public_flags"` } ) diff --git a/global/data.go b/global/data.go new file mode 100644 index 0000000..df5a3b0 --- /dev/null +++ b/global/data.go @@ -0,0 +1,40 @@ +package global + +import ( + "os" + + "arimelody.me/arimelody.me/music/model" + "github.com/jmoiron/sqlx" +) + +var HTTP_DOMAIN = func() string { + envvar := os.Getenv("HTTP_DOMAIN") + if envvar != "" { + return envvar + } + return "https://arimelody.me" +} + +var DB *sqlx.DB + +var Releases []model.Release +var Artists []model.Artist +var Tracks []model.Track + +func GetRelease(id string) *model.Release { + for _, release := range Releases { + if release.ID == id { + return &release + } + } + return nil +} + +func GetArtist(id string) *model.Artist { + for _, artist := range Artists { + if artist.ID == id { + return &artist + } + } + return nil +} diff --git a/global/global.go b/global/funcs.go similarity index 99% rename from global/global.go rename to global/funcs.go index f796d03..5d3f8fc 100644 --- a/global/global.go +++ b/global/funcs.go @@ -16,6 +16,7 @@ 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") + next.ServeHTTP(w, r) }) } diff --git a/main.go b/main.go index 9270dcb..453e992 100644 --- a/main.go +++ b/main.go @@ -6,61 +6,70 @@ import ( "net/http" "os" "path/filepath" + "time" "arimelody.me/arimelody.me/admin" - "arimelody.me/arimelody.me/music" - "arimelody.me/arimelody.me/db" + "arimelody.me/arimelody.me/api" "arimelody.me/arimelody.me/global" + musicController "arimelody.me/arimelody.me/music/controller" + musicView "arimelody.me/arimelody.me/music/view" + + "github.com/jmoiron/sqlx" + _ "github.com/lib/pq" ) const DEFAULT_PORT int = 8080 func main() { - db := db.InitDatabase() - defer db.Close() - + // initialise database connection var err error - music.Artists, err = music.PullAllArtists(db) + global.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) + } + global.DB.SetConnMaxLifetime(time.Minute * 3) + global.DB.SetMaxOpenConns(10) + global.DB.SetMaxIdleConns(10) + defer global.DB.Close() + + // pull artist data from DB + global.Artists, err = musicController.PullAllArtists(global.DB) if err != nil { fmt.Printf("Failed to pull artists from database: %v\n", err); panic(1) } - fmt.Printf("%d artists loaded successfully.\n", len(music.Artists)) + fmt.Printf("%d artists loaded successfully.\n", len(global.Artists)) - music.Releases, err = music.PullAllReleases(db) + // pull release data from DB + global.Releases, err = musicController.PullAllReleases(global.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)) + fmt.Printf("%d releases loaded successfully.\n", len(global.Releases)) + // start the web server! 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)) + log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), global.HTTPLog(mux))) } func createServeMux() *http.ServeMux { mux := http.NewServeMux() - mux.Handle("/admin/", global.HTTPLog(http.StripPrefix("/admin", admin.Handler()))) - - mux.Handle("/api/v1/music/artist/", global.HTTPLog(http.StripPrefix("/api/v1/music/artist", music.ServeArtist()))) - mux.Handle("/api/v1/music/", global.HTTPLog(http.StripPrefix("/api/v1/music", music.ServeRelease()))) - mux.Handle("/api/v1/music", global.HTTPLog(music.PostRelease())) - - mux.Handle("/music-artwork/", global.HTTPLog(http.StripPrefix("/music-artwork", music.ServeArtwork()))) - 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) { + mux.Handle("/admin/", http.StripPrefix("/admin", admin.Handler())) + mux.Handle("/api/", http.StripPrefix("/api", api.Handler())) + mux.Handle("/music/", http.StripPrefix("/music", musicView.Handler())) + mux.Handle("/uploads/", http.StripPrefix("/uploads", staticHandler("uploads"))) + mux.Handle("/", 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("public").ServeHTTP(w, r) - }))) + })) return mux } @@ -82,6 +91,6 @@ func staticHandler(directory string) http.Handler { return } - http.FileServer(http.Dir("./public")).ServeHTTP(w, r) + http.FileServer(http.Dir(directory)).ServeHTTP(w, r) }) } diff --git a/music/artist.go b/music/artist.go deleted file mode 100644 index 59e5555..0000000 --- a/music/artist.go +++ /dev/null @@ -1,155 +0,0 @@ -package music - -import ( - "encoding/json" - "net/http" - - "github.com/jmoiron/sqlx" -) - -type Artist struct { - ID string `json:"id"` - Name string `json:"name"` - Website string `json:"website"` -} - -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 -} - -// HTTP HANDLERS - -func ServeArtist() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/" { - http.NotFound(w, r) - return - } - - type ( - creditJSON struct { - Role string `json:"role"` - Primary bool `json:"primary"` - } - artistJSON struct { - Artist - Credits map[string]creditJSON `json:"credits"` - } - ) - var res = artistJSON{} - - res.ID = r.URL.Path[1:] - var artist = GetArtist(res.ID) - if artist == nil { - http.NotFound(w, r) - return - } - res.Name = artist.Name - res.Website = artist.Website - res.Credits = make(map[string]creditJSON) - - for _, release := range Releases { - for _, credit := range release.Credits { - if credit.Artist.ID != res.ID { - continue - } - res.Credits[release.ID] = creditJSON{ - Role: credit.Role, - Primary: credit.Primary, - } - } - } - - jsonBytes, err := json.Marshal(res) - if err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - w.Header().Add("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - w.Write(jsonBytes) - }) -} diff --git a/music/controller/artist.go b/music/controller/artist.go new file mode 100644 index 0000000..17f8ddd --- /dev/null +++ b/music/controller/artist.go @@ -0,0 +1,65 @@ +package music + +import ( + "arimelody.me/arimelody.me/music/model" + "github.com/jmoiron/sqlx" +) + +// DATABASE + +func PullAllArtists(db *sqlx.DB) ([]model.Artist, error) { + var artists = []model.Artist{} + + err := db.Select(&artists, "SELECT * FROM artist") + if err != nil { + return nil, err + } + + return artists, nil +} + +func CreateArtistDB(db *sqlx.DB, artist *model.Artist) error { + _, err := db.Exec( + "INSERT INTO artist (id, name, website, avatar) "+ + "VALUES ($1, $2, $3, $4)", + artist.ID, + artist.Name, + artist.Website, + artist.Avatar, + ) + if err != nil { + return err + } + + return nil +} + +func UpdateArtistDB(db *sqlx.DB, artist *model.Artist) error { + _, err := db.Exec( + "UPDATE artist "+ + "SET name=$2, website=$3, avatar=$4 "+ + "WHERE id=$1", + artist.ID, + artist.Name, + artist.Website, + artist.Avatar, + ) + if err != nil { + return err + } + + return nil +} + +func DeleteArtistDB(db *sqlx.DB, artistID string) error { + _, err := db.Exec( + "DELETE FROM artist "+ + "WHERE id=$1", + artistID, + ) + if err != nil { + return err + } + + return nil +} diff --git a/music/controller/credit.go b/music/controller/credit.go new file mode 100644 index 0000000..4ea987e --- /dev/null +++ b/music/controller/credit.go @@ -0,0 +1,70 @@ +package music + +import ( + "arimelody.me/arimelody.me/music/model" + "github.com/jmoiron/sqlx" +) + +// DATABASE + +func PullReleaseCredits(db *sqlx.DB, releaseID string) ([]model.Credit, error) { + var credits = []model.Credit{} + + err := db.Select( + &credits, + "SELECT * FROM musiccredit WHERE release=$1", + releaseID, + ) + if err != nil { + return nil, err + } + + return credits, nil +} + +func CreateCreditDB(db *sqlx.DB, releaseID string, artistID string, credit *model.Credit) (error) { + _, err := db.Exec( + "INSERT INTO musiccredit (release, artist, role, is_primary) "+ + "VALUES ($1, $2, $3, $4)", + releaseID, + artistID, + credit.Role, + credit.Primary, + ) + if err != nil { + return err + } + + return nil +} + +func UpdateCreditDB(db *sqlx.DB, releaseID string, artistID string, credit *model.Credit) (error) { + _, err := db.Exec( + "UPDATE musiccredit SET "+ + "role=$3, is_primary=$4 "+ + "WHERE release=$1, artist=$2", + releaseID, + artistID, + credit.Role, + credit.Primary, + ) + if err != nil { + return err + } + + return nil +} + +func DeleteCreditDB(db *sqlx.DB, releaseID string, artistID string) (error) { + _, err := db.Exec( + "DELETE FROM musiccredit "+ + "WHERE release=$1, artist=$2", + releaseID, + artistID, + ) + if err != nil { + return err + } + + return nil +} diff --git a/music/controller/link.go b/music/controller/link.go new file mode 100644 index 0000000..b1d399a --- /dev/null +++ b/music/controller/link.go @@ -0,0 +1,68 @@ +package music + +import ( + "arimelody.me/arimelody.me/music/model" + "github.com/jmoiron/sqlx" +) + +// DATABASE + +func PullReleaseLinks(db *sqlx.DB, releaseID string) ([]model.Link, error) { + var links = []model.Link{} + + err := db.Select( + &links, + "SELECT * FROM musiclink WHERE release=$1", + releaseID, + ) + if err != nil { + return nil, err + } + + return links, nil +} + +func CreateLinkDB(db *sqlx.DB, releaseID string, link *model.Link) (error) { + _, err := db.Exec( + "INSERT INTO musiclink (release, name, url) "+ + "VALUES ($1, $2, $3)", + releaseID, + link.Name, + link.URL, + ) + if err != nil { + return err + } + + return nil +} + +func UpdateLinkDB(db *sqlx.DB, releaseID string, link *model.Link) (error) { + _, err := db.Exec( + "UPDATE musiclink SET "+ + "name=$2, url=$3 "+ + "WHERE release=$1", + releaseID, + link.Name, + link.URL, + ) + if err != nil { + return err + } + + return nil +} + +func DeleteLinkDB(db *sqlx.DB, releaseID string, link *model.Link) (error) { + _, err := db.Exec( + "DELETE FROM musiclink "+ + "WHERE release=$1, name=$2", + releaseID, + link.Name, + ) + if err != nil { + return err + } + + return nil +} diff --git a/music/controller/release.go b/music/controller/release.go new file mode 100644 index 0000000..4806d03 --- /dev/null +++ b/music/controller/release.go @@ -0,0 +1,76 @@ +package music + +import ( + "arimelody.me/arimelody.me/music/model" + "github.com/jmoiron/sqlx" +) + +// DATABASE + +func PullAllReleases(db *sqlx.DB) ([]model.Release, error) { + var releases = []model.Release{} + + err := db.Select(&releases, "SELECT * FROM musicrelease ORDER BY release_date DESC") + if err != nil { + return nil, err + } + + return releases, nil +} + +func CreateReleaseDB(db *sqlx.DB, release *model.Release) error { + _, err := db.Exec( + "INSERT INTO musicrelease "+ + "(id, visible, title, description, type, release_date, artwork, buyname, buylink) "+ + "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + release.ID, + release.Visible, + release.Title, + release.Description, + release.ReleaseType, + release.ReleaseDate.Format("2-Jan-2006"), + release.Artwork, + release.Buyname, + release.Buylink, + ) + if err != nil { + return err + } + + return nil +} + +func UpdateReleaseDB(db *sqlx.DB, release *model.Release) error { + _, err := db.Exec( + "UPDATE musicrelease SET "+ + "title=$2, description=$3, type=$4, release_date=$5, artwork=$6, buyname=$7, buylink=$8) "+ + "VALUES ($2, $3, $4, $5, $6, $7, $8) "+ + "WHERE id=$1", + release.ID, + release.Title, + release.Description, + release.ReleaseType, + release.ReleaseDate.Format("2-Jan-2006"), + release.Artwork, + release.Buyname, + release.Buylink, + ) + if err != nil { + return err + } + + return nil +} + +func DeleteReleaseDB(db *sqlx.DB, release model.Release) error { + _, err := db.Exec( + "DELETE FROM musicrelease "+ + "WHERE id=$1", + release.ID, + ) + if err != nil { + return err + } + + return nil +} diff --git a/music/controller/track.go b/music/controller/track.go new file mode 100644 index 0000000..0a352ca --- /dev/null +++ b/music/controller/track.go @@ -0,0 +1,69 @@ +package music + +import ( + "arimelody.me/arimelody.me/music/model" + "github.com/jmoiron/sqlx" +) + +// DATABASE + +func PullAllTracks(db *sqlx.DB) ([]model.Track, error) { + var tracks = []model.Track{} + + err := db.Select(&tracks, "SELECT id, title, description, lyrics, preview_url FROM musictrack RETURNING id") + if err != nil { + return nil, err + } + + return tracks, nil +} + +func CreateTrackDB(db *sqlx.DB, track *model.Track) (string, error) { + var trackID string + err := db.QueryRow( + "INSERT INTO musictrack (title, description, lyrics, preview_url) "+ + "VALUES ($1, $2, $3, $4) "+ + "RETURNING id", + track.Title, + track.Description, + track.Lyrics, + track.PreviewURL, + ).Scan(&trackID) + if err != nil { + return "", err + } + + return trackID, nil +} + +func UpdateTrackDB(db *sqlx.DB, track *model.Track) error { + _, err := db.Exec( + "UPDATE musictrack "+ + "SET title=$2, description=$3, lyrics=$4, preview_url=$5 "+ + "WHERE id=$1"+ + "RETURNING id", + track.ID, + track.Title, + track.Description, + track.Lyrics, + track.PreviewURL, + ) + if err != nil { + return err + } + + return nil +} + +func DeleteTrackDB(db *sqlx.DB, trackID string) error { + _, err := db.Exec( + "DELETE FROM musictrack "+ + "WHERE id=$1", + trackID, + ) + if err != nil { + return err + } + + return nil +} diff --git a/music/credit.go b/music/credit.go deleted file mode 100644 index 232b5af..0000000 --- a/music/credit.go +++ /dev/null @@ -1,92 +0,0 @@ -package music - -import ( - "fmt" - - "github.com/jmoiron/sqlx" -) - -type ( - Credit struct { - Artist *Artist `json:"artist"` - Role string `json:"role"` - Primary bool `json:"primary"` - } - - PostCreditBody struct { - Artist string `json:"artist"` - Role string `json:"role"` - Primary bool `json:"primary"` - } -) - -// 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/music/link.go b/music/link.go deleted file mode 100644 index 9e834c0..0000000 --- a/music/link.go +++ /dev/null @@ -1,73 +0,0 @@ -package music - -import ( - "fmt" - "regexp" - "strings" - - "github.com/jmoiron/sqlx" -) - -type Link struct { - Name string `json:"name"` - URL string `json:"url"` -} - -// 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/music/model/artist.go b/music/model/artist.go new file mode 100644 index 0000000..1956e3a --- /dev/null +++ b/music/model/artist.go @@ -0,0 +1,17 @@ +package model + +type ( + Artist struct { + ID string `json:"id"` + Name string `json:"name"` + Website string `json:"website"` + Avatar string `json:"avatar"` + } +) + +func (artist Artist) GetAvatar() string { + if artist.Avatar == "" { + return "/img/default-avatar.png" + } + return artist.Avatar +} diff --git a/music/model/credit.go b/music/model/credit.go new file mode 100644 index 0000000..cb12e7d --- /dev/null +++ b/music/model/credit.go @@ -0,0 +1,7 @@ +package model + +type Credit struct { + Artist *Artist `json:"artist"` + Role string `json:"role"` + Primary bool `json:"primary" db:"is_primary"` +} diff --git a/music/model/link.go b/music/model/link.go new file mode 100644 index 0000000..fd38556 --- /dev/null +++ b/music/model/link.go @@ -0,0 +1,16 @@ +package model + +import ( + "regexp" + "strings" +) + +type Link struct { + Name string `json:"name"` + URL string `json:"url"` +} + +func (link Link) NormaliseName() string { + rgx := regexp.MustCompile(`[^a-z0-9]`) + return strings.ToLower(rgx.ReplaceAllString(link.Name, "")) +} diff --git a/music/model/release.go b/music/model/release.go new file mode 100644 index 0000000..5cd6099 --- /dev/null +++ b/music/model/release.go @@ -0,0 +1,109 @@ +package model + +import ( + "strings" + "time" +) + +type ( + ReleaseType string + Release struct { + ID string `json:"id"` + Visible bool `json:"visible"` + Title string `json:"title"` + Description string `json:"description"` + ReleaseType ReleaseType `json:"type" db:"type"` + ReleaseDate time.Time `json:"releaseDate" db:"release_date"` + Artwork string `json:"artwork"` + Buyname string `json:"buyname"` + Buylink string `json:"buylink"` + Links []Link `json:"links"` + Credits []Credit `json:"credits"` + Tracks []Track `json:"tracks"` + } +) + +const ( + Single ReleaseType = "Single" + Album ReleaseType = "Album" + EP ReleaseType = "EP" + Compilation ReleaseType = "Compilation" +) + +// GETTERS + +func (release Release) GetArtwork() string { + if release.Artwork == "" { + return "/img/default-cover-art.png" + } + return release.Artwork +} + +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.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 new file mode 100644 index 0000000..176056e --- /dev/null +++ b/music/model/track.go @@ -0,0 +1,9 @@ +package model + +type Track struct { + ID string `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + Lyrics string `json:"lyrics"` + PreviewURL string `json:"previewURL" db:"preview_url"` +} diff --git a/music/release.go b/music/release.go deleted file mode 100644 index ed409f7..0000000 --- a/music/release.go +++ /dev/null @@ -1,492 +0,0 @@ -package music - -import ( - "encoding/json" - "errors" - "fmt" - "net/http" - "strings" - "time" - - "arimelody.me/arimelody.me/admin" - "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 `json:"id"` - Title string `json:"title"` - Description string `json:"description"` - ReleaseType ReleaseType `json:"type"` - ReleaseDate time.Time `json:"releaseDate"` - Artwork string `json:"artwork"` - Buyname string `json:"buyname"` - Buylink string `json:"buylink"` - Links []Link `json:"links"` - Credits []Credit `json:"credits"` - Tracks []Track `json:"tracks"` - } - - PostReleaseBody struct { - ID string `json:"id"` - Title string `json:"title"` - Description string `json:"description"` - ReleaseType ReleaseType `json:"type"` - ReleaseDate time.Time `json:"releaseDate"` - Artwork string `json:"artwork"` - Buyname string `json:"buyname"` - Buylink string `json:"buylink"` - Links []Link `json:"links"` - Credits []PostCreditBody `json:"credits"` - Tracks []Track `json:"tracks"` - } -) - -var Releases []Release; - -// GETTERS - -func (release Release) GetID() string { - return release.ID -} - -func (release Release) GetTitle() string { - return release.Title -} - -func (release Release) GetDescription() string { - return release.Description -} - -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/default-cover-art.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) 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) SetDescription(description string) error { - // TODO: update DB - release.Description = description - 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) 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, description, type, release_date, artwork, buyname, buylink) "+ - "VALUES ($1, $2, $3, $4, $5, $6, $7, $8) "+ - "ON CONFLICT (id) "+ - "DO UPDATE SET title=$2, description=$3, type=$4, release_date=$5, artwork=$6, buyname=$7, buylink=$8", - release.ID, - release.Title, - release.Description, - 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 ORDER BY release_date DESC") - 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 -} - -// HTTP HANDLERS - -func ServeRelease() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/" { - http.NotFound(w, r) - return - } - - releaseID := r.URL.Path[1:] - var release = GetRelease(releaseID) - if release == nil { - http.NotFound(w, r) - return - } - - // 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(ServeRelease()).ServeHTTP(w, r) - return - } - - jsonBytes, err := json.Marshal(release) - if err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - w.Header().Add("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - w.Write(jsonBytes) - }) -} - -func PostRelease() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.NotFound(w, r) - return - } - - var data PostReleaseBody - err := json.NewDecoder(r.Body).Decode(&data) - if err != nil { - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) - return - } - - if GetRelease(data.ID) != nil { - http.Error(w, fmt.Sprintf("Release %s already exists", data.ID), http.StatusBadRequest) - return - } - - var credits = []Credit{} - - for _, credit := range data.Credits { - var artist = GetArtist(credit.Artist) - - if artist == nil { - http.Error(w, fmt.Sprintf("Artist %s does not exist", credit.Artist), http.StatusBadRequest) - return - } - - credits = append(credits, Credit{ - Artist: artist, - Role: credit.Role, - Primary: credit.Primary, - }) - } - - var release = Release{ - ID: data.ID, - Title: data.Title, - Description: data.Description, - ReleaseType: data.ReleaseType, - ReleaseDate: data.ReleaseDate, - Artwork: data.Artwork, - Buyname: data.Buyname, - Buylink: data.Buylink, - Links: data.Links, - Credits: credits, - Tracks: data.Tracks, - } - - Releases = append([]Release{release}, Releases...) - - jsonBytes, err := json.Marshal(release) - w.Header().Add("Content-Type", "application/json") - w.WriteHeader(http.StatusCreated) - w.Write(jsonBytes) - }) -} diff --git a/music/track.go b/music/track.go deleted file mode 100644 index bb804e4..0000000 --- a/music/track.go +++ /dev/null @@ -1,100 +0,0 @@ -package music - -import ( - "fmt" - - "github.com/jmoiron/sqlx" -) - -type Track struct { - Number int `json:"number"` - Title string `json:"title"` - Description string `json:"description"` - Lyrics string `json:"lyrics"` - PreviewURL string `json:"previewURL"` -} - -// 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/music/music.go b/music/view/music.go similarity index 61% rename from music/music.go rename to music/view/music.go index 5ff6833..50b9d36 100644 --- a/music/music.go +++ b/music/view/music.go @@ -1,65 +1,45 @@ -package music +package view import ( - "fmt" "net/http" - "os" - "path/filepath" - "strings" "arimelody.me/arimelody.me/admin" "arimelody.me/arimelody.me/global" + "arimelody.me/arimelody.me/music/model" ) // HTTP HANDLER METHODS +func Handler() http.Handler { + mux := http.NewServeMux() + + mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" { + ServeCatalog().ServeHTTP(w, r) + return + } + ServeGateway().ServeHTTP(w, r) + })) + + return mux +} + 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 { + releases := []model.Release{} + authorised := admin.GetSession(r) != nil + for _, release := range global.Releases { if !release.IsReleased() && !authorised { continue } releases = append(releases, release) } - global.ServeTemplate("music.html", Releases).ServeHTTP(w, r) - }) -} - -func ServeGateway() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/" { - http.Redirect(w, r, "/music", http.StatusPermanentRedirect) - return - } - - id := r.URL.Path[1:] - release := GetRelease(id) - if release == nil { - http.NotFound(w, r) - return - } - - // 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 - } + global.ServeTemplate("music.html", releases).ServeHTTP(w, r) }) } +/* func ServeArtwork() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { @@ -111,3 +91,4 @@ func ServeArtwork() http.Handler { w.Write(bytes) }) } +*/ diff --git a/music/view/release.go b/music/view/release.go new file mode 100644 index 0000000..0b480ce --- /dev/null +++ b/music/view/release.go @@ -0,0 +1,74 @@ +package view + +import ( + "encoding/json" + "fmt" + "net/http" + + "arimelody.me/arimelody.me/admin" + "arimelody.me/arimelody.me/global" +) + +// HTTP HANDLERS + +func ServeRelease() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" { + http.NotFound(w, r) + return + } + + releaseID := r.URL.Path[1:] + var release = global.GetRelease(releaseID) + if release == nil { + http.NotFound(w, r) + return + } + + // only allow authorised users to view unreleased releases + authorised := admin.GetSession(r) != nil + if !release.IsReleased() && !authorised { + admin.MustAuthorise(ServeRelease()).ServeHTTP(w, r) + return + } + + w.Header().Add("Content-Type", "application/json") + err := json.NewEncoder(w).Encode(release) + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + }) +} + +func ServeGateway() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" { + http.Redirect(w, r, "/music", http.StatusPermanentRedirect) + return + } + + id := r.URL.Path[1:] + release := global.GetRelease(id) + if release == nil { + http.NotFound(w, r) + return + } + + // only allow authorised users to view unreleased releases + authorised := admin.GetSession(r) != nil + 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 rendering music gateway for %s\n", id) + return + } + }) +} diff --git a/public/font/inter/Inter-Black.woff2 b/public/font/inter/Inter-Black.woff2 new file mode 100644 index 0000000..18b35db Binary files /dev/null and b/public/font/inter/Inter-Black.woff2 differ diff --git a/public/font/inter/Inter-BlackItalic.woff2 b/public/font/inter/Inter-BlackItalic.woff2 new file mode 100644 index 0000000..02c9d8e Binary files /dev/null and b/public/font/inter/Inter-BlackItalic.woff2 differ diff --git a/public/font/inter/Inter-Bold.woff2 b/public/font/inter/Inter-Bold.woff2 new file mode 100644 index 0000000..0f1b157 Binary files /dev/null and b/public/font/inter/Inter-Bold.woff2 differ diff --git a/public/font/inter/Inter-BoldItalic.woff2 b/public/font/inter/Inter-BoldItalic.woff2 new file mode 100644 index 0000000..bc50f24 Binary files /dev/null and b/public/font/inter/Inter-BoldItalic.woff2 differ diff --git a/public/font/inter/Inter-ExtraBold.woff2 b/public/font/inter/Inter-ExtraBold.woff2 new file mode 100644 index 0000000..b113368 Binary files /dev/null and b/public/font/inter/Inter-ExtraBold.woff2 differ diff --git a/public/font/inter/Inter-ExtraBoldItalic.woff2 b/public/font/inter/Inter-ExtraBoldItalic.woff2 new file mode 100644 index 0000000..a5b76ca Binary files /dev/null and b/public/font/inter/Inter-ExtraBoldItalic.woff2 differ diff --git a/public/font/inter/Inter-ExtraLight.woff2 b/public/font/inter/Inter-ExtraLight.woff2 new file mode 100644 index 0000000..1d77ae8 Binary files /dev/null and b/public/font/inter/Inter-ExtraLight.woff2 differ diff --git a/public/font/inter/Inter-ExtraLightItalic.woff2 b/public/font/inter/Inter-ExtraLightItalic.woff2 new file mode 100644 index 0000000..8c68492 Binary files /dev/null and b/public/font/inter/Inter-ExtraLightItalic.woff2 differ diff --git a/public/font/inter/Inter-Italic.woff2 b/public/font/inter/Inter-Italic.woff2 new file mode 100644 index 0000000..4c24ce2 Binary files /dev/null and b/public/font/inter/Inter-Italic.woff2 differ diff --git a/public/font/inter/Inter-Light.woff2 b/public/font/inter/Inter-Light.woff2 new file mode 100644 index 0000000..dbe6143 Binary files /dev/null and b/public/font/inter/Inter-Light.woff2 differ diff --git a/public/font/inter/Inter-LightItalic.woff2 b/public/font/inter/Inter-LightItalic.woff2 new file mode 100644 index 0000000..a40d042 Binary files /dev/null and b/public/font/inter/Inter-LightItalic.woff2 differ diff --git a/public/font/inter/Inter-Medium.woff2 b/public/font/inter/Inter-Medium.woff2 new file mode 100644 index 0000000..0fd2ee7 Binary files /dev/null and b/public/font/inter/Inter-Medium.woff2 differ diff --git a/public/font/inter/Inter-MediumItalic.woff2 b/public/font/inter/Inter-MediumItalic.woff2 new file mode 100644 index 0000000..9676715 Binary files /dev/null and b/public/font/inter/Inter-MediumItalic.woff2 differ diff --git a/public/font/inter/Inter-Regular.woff2 b/public/font/inter/Inter-Regular.woff2 new file mode 100644 index 0000000..b8699af Binary files /dev/null and b/public/font/inter/Inter-Regular.woff2 differ diff --git a/public/font/inter/Inter-SemiBold.woff2 b/public/font/inter/Inter-SemiBold.woff2 new file mode 100644 index 0000000..95c48b1 Binary files /dev/null and b/public/font/inter/Inter-SemiBold.woff2 differ diff --git a/public/font/inter/Inter-SemiBoldItalic.woff2 b/public/font/inter/Inter-SemiBoldItalic.woff2 new file mode 100644 index 0000000..ddfe19e Binary files /dev/null and b/public/font/inter/Inter-SemiBoldItalic.woff2 differ diff --git a/public/font/inter/Inter-Thin.woff2 b/public/font/inter/Inter-Thin.woff2 new file mode 100644 index 0000000..0790960 Binary files /dev/null and b/public/font/inter/Inter-Thin.woff2 differ diff --git a/public/font/inter/Inter-ThinItalic.woff2 b/public/font/inter/Inter-ThinItalic.woff2 new file mode 100644 index 0000000..a7bf213 Binary files /dev/null and b/public/font/inter/Inter-ThinItalic.woff2 differ diff --git a/public/font/inter/InterDisplay-Black.woff2 b/public/font/inter/InterDisplay-Black.woff2 new file mode 100644 index 0000000..8138123 Binary files /dev/null and b/public/font/inter/InterDisplay-Black.woff2 differ diff --git a/public/font/inter/InterDisplay-BlackItalic.woff2 b/public/font/inter/InterDisplay-BlackItalic.woff2 new file mode 100644 index 0000000..735ba21 Binary files /dev/null and b/public/font/inter/InterDisplay-BlackItalic.woff2 differ diff --git a/public/font/inter/InterDisplay-Bold.woff2 b/public/font/inter/InterDisplay-Bold.woff2 new file mode 100644 index 0000000..11c6719 Binary files /dev/null and b/public/font/inter/InterDisplay-Bold.woff2 differ diff --git a/public/font/inter/InterDisplay-BoldItalic.woff2 b/public/font/inter/InterDisplay-BoldItalic.woff2 new file mode 100644 index 0000000..5b6a1fb Binary files /dev/null and b/public/font/inter/InterDisplay-BoldItalic.woff2 differ diff --git a/public/font/inter/InterDisplay-ExtraBold.woff2 b/public/font/inter/InterDisplay-ExtraBold.woff2 new file mode 100644 index 0000000..9058e98 Binary files /dev/null and b/public/font/inter/InterDisplay-ExtraBold.woff2 differ diff --git a/public/font/inter/InterDisplay-ExtraBoldItalic.woff2 b/public/font/inter/InterDisplay-ExtraBoldItalic.woff2 new file mode 100644 index 0000000..4cd61c0 Binary files /dev/null and b/public/font/inter/InterDisplay-ExtraBoldItalic.woff2 differ diff --git a/public/font/inter/InterDisplay-ExtraLight.woff2 b/public/font/inter/InterDisplay-ExtraLight.woff2 new file mode 100644 index 0000000..8621b29 Binary files /dev/null and b/public/font/inter/InterDisplay-ExtraLight.woff2 differ diff --git a/public/font/inter/InterDisplay-ExtraLightItalic.woff2 b/public/font/inter/InterDisplay-ExtraLightItalic.woff2 new file mode 100644 index 0000000..689c8d9 Binary files /dev/null and b/public/font/inter/InterDisplay-ExtraLightItalic.woff2 differ diff --git a/public/font/inter/InterDisplay-Italic.woff2 b/public/font/inter/InterDisplay-Italic.woff2 new file mode 100644 index 0000000..11f20bc Binary files /dev/null and b/public/font/inter/InterDisplay-Italic.woff2 differ diff --git a/public/font/inter/InterDisplay-Light.woff2 b/public/font/inter/InterDisplay-Light.woff2 new file mode 100644 index 0000000..446301c Binary files /dev/null and b/public/font/inter/InterDisplay-Light.woff2 differ diff --git a/public/font/inter/InterDisplay-LightItalic.woff2 b/public/font/inter/InterDisplay-LightItalic.woff2 new file mode 100644 index 0000000..f688196 Binary files /dev/null and b/public/font/inter/InterDisplay-LightItalic.woff2 differ diff --git a/public/font/inter/InterDisplay-Medium.woff2 b/public/font/inter/InterDisplay-Medium.woff2 new file mode 100644 index 0000000..29160b2 Binary files /dev/null and b/public/font/inter/InterDisplay-Medium.woff2 differ diff --git a/public/font/inter/InterDisplay-MediumItalic.woff2 b/public/font/inter/InterDisplay-MediumItalic.woff2 new file mode 100644 index 0000000..ef1bcbe Binary files /dev/null and b/public/font/inter/InterDisplay-MediumItalic.woff2 differ diff --git a/public/font/inter/InterDisplay-Regular.woff2 b/public/font/inter/InterDisplay-Regular.woff2 new file mode 100644 index 0000000..a6c04f6 Binary files /dev/null and b/public/font/inter/InterDisplay-Regular.woff2 differ diff --git a/public/font/inter/InterDisplay-SemiBold.woff2 b/public/font/inter/InterDisplay-SemiBold.woff2 new file mode 100644 index 0000000..2b4db23 Binary files /dev/null and b/public/font/inter/InterDisplay-SemiBold.woff2 differ diff --git a/public/font/inter/InterDisplay-SemiBoldItalic.woff2 b/public/font/inter/InterDisplay-SemiBoldItalic.woff2 new file mode 100644 index 0000000..59091db Binary files /dev/null and b/public/font/inter/InterDisplay-SemiBoldItalic.woff2 differ diff --git a/public/font/inter/InterDisplay-Thin.woff2 b/public/font/inter/InterDisplay-Thin.woff2 new file mode 100644 index 0000000..dc0b948 Binary files /dev/null and b/public/font/inter/InterDisplay-Thin.woff2 differ diff --git a/public/font/inter/InterDisplay-ThinItalic.woff2 b/public/font/inter/InterDisplay-ThinItalic.woff2 new file mode 100644 index 0000000..96439c0 Binary files /dev/null and b/public/font/inter/InterDisplay-ThinItalic.woff2 differ diff --git a/public/font/inter/InterVariable-Italic.woff2 b/public/font/inter/InterVariable-Italic.woff2 new file mode 100644 index 0000000..f22ec25 Binary files /dev/null and b/public/font/inter/InterVariable-Italic.woff2 differ diff --git a/public/font/inter/InterVariable.woff2 b/public/font/inter/InterVariable.woff2 new file mode 100644 index 0000000..22a12b0 Binary files /dev/null and b/public/font/inter/InterVariable.woff2 differ diff --git a/public/font/inter/LICENSE.txt b/public/font/inter/LICENSE.txt new file mode 100644 index 0000000..9b2ca37 --- /dev/null +++ b/public/font/inter/LICENSE.txt @@ -0,0 +1,92 @@ +Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION AND CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/public/font/inter/inter.css b/public/font/inter/inter.css new file mode 100644 index 0000000..413e197 --- /dev/null +++ b/public/font/inter/inter.css @@ -0,0 +1,57 @@ +/* Variable fonts usage: +:root { font-family: "Inter", sans-serif; } +@supports (font-variation-settings: normal) { + :root { font-family: "InterVariable", sans-serif; font-optical-sizing: auto; } +} */ +@font-face { + font-family: InterVariable; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: url("InterVariable.woff2") format("woff2"); +} +@font-face { + font-family: InterVariable; + font-style: italic; + font-weight: 100 900; + font-display: swap; + src: url("InterVariable-Italic.woff2") format("woff2"); +} + +/* static fonts */ +@font-face { font-family: "Inter"; font-style: normal; font-weight: 100; font-display: swap; src: url("Inter-Thin.woff2") format("woff2"); } +@font-face { font-family: "Inter"; font-style: italic; font-weight: 100; font-display: swap; src: url("Inter-ThinItalic.woff2") format("woff2"); } +@font-face { font-family: "Inter"; font-style: normal; font-weight: 200; font-display: swap; src: url("Inter-ExtraLight.woff2") format("woff2"); } +@font-face { font-family: "Inter"; font-style: italic; font-weight: 200; font-display: swap; src: url("Inter-ExtraLightItalic.woff2") format("woff2"); } +@font-face { font-family: "Inter"; font-style: normal; font-weight: 300; font-display: swap; src: url("Inter-Light.woff2") format("woff2"); } +@font-face { font-family: "Inter"; font-style: italic; font-weight: 300; font-display: swap; src: url("Inter-LightItalic.woff2") format("woff2"); } +@font-face { font-family: "Inter"; font-style: normal; font-weight: 400; font-display: swap; src: url("Inter-Regular.woff2") format("woff2"); } +@font-face { font-family: "Inter"; font-style: italic; font-weight: 400; font-display: swap; src: url("Inter-Italic.woff2") format("woff2"); } +@font-face { font-family: "Inter"; font-style: normal; font-weight: 500; font-display: swap; src: url("Inter-Medium.woff2") format("woff2"); } +@font-face { font-family: "Inter"; font-style: italic; font-weight: 500; font-display: swap; src: url("Inter-MediumItalic.woff2") format("woff2"); } +@font-face { font-family: "Inter"; font-style: normal; font-weight: 600; font-display: swap; src: url("Inter-SemiBold.woff2") format("woff2"); } +@font-face { font-family: "Inter"; font-style: italic; font-weight: 600; font-display: swap; src: url("Inter-SemiBoldItalic.woff2") format("woff2"); } +@font-face { font-family: "Inter"; font-style: normal; font-weight: 700; font-display: swap; src: url("Inter-Bold.woff2") format("woff2"); } +@font-face { font-family: "Inter"; font-style: italic; font-weight: 700; font-display: swap; src: url("Inter-BoldItalic.woff2") format("woff2"); } +@font-face { font-family: "Inter"; font-style: normal; font-weight: 800; font-display: swap; src: url("Inter-ExtraBold.woff2") format("woff2"); } +@font-face { font-family: "Inter"; font-style: italic; font-weight: 800; font-display: swap; src: url("Inter-ExtraBoldItalic.woff2") format("woff2"); } +@font-face { font-family: "Inter"; font-style: normal; font-weight: 900; font-display: swap; src: url("Inter-Black.woff2") format("woff2"); } +@font-face { font-family: "Inter"; font-style: italic; font-weight: 900; font-display: swap; src: url("Inter-BlackItalic.woff2") format("woff2"); } +@font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 100; font-display: swap; src: url("InterDisplay-Thin.woff2") format("woff2"); } +@font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 100; font-display: swap; src: url("InterDisplay-ThinItalic.woff2") format("woff2"); } +@font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 200; font-display: swap; src: url("InterDisplay-ExtraLight.woff2") format("woff2"); } +@font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 200; font-display: swap; src: url("InterDisplay-ExtraLightItalic.woff2") format("woff2"); } +@font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 300; font-display: swap; src: url("InterDisplay-Light.woff2") format("woff2"); } +@font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 300; font-display: swap; src: url("InterDisplay-LightItalic.woff2") format("woff2"); } +@font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 400; font-display: swap; src: url("InterDisplay-Regular.woff2") format("woff2"); } +@font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 400; font-display: swap; src: url("InterDisplay-Italic.woff2") format("woff2"); } +@font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 500; font-display: swap; src: url("InterDisplay-Medium.woff2") format("woff2"); } +@font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 500; font-display: swap; src: url("InterDisplay-MediumItalic.woff2") format("woff2"); } +@font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 600; font-display: swap; src: url("InterDisplay-SemiBold.woff2") format("woff2"); } +@font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 600; font-display: swap; src: url("InterDisplay-SemiBoldItalic.woff2") format("woff2"); } +@font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 700; font-display: swap; src: url("InterDisplay-Bold.woff2") format("woff2"); } +@font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 700; font-display: swap; src: url("InterDisplay-BoldItalic.woff2") format("woff2"); } +@font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 800; font-display: swap; src: url("InterDisplay-ExtraBold.woff2") format("woff2"); } +@font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 800; font-display: swap; src: url("InterDisplay-ExtraBoldItalic.woff2") format("woff2"); } +@font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 900; font-display: swap; src: url("InterDisplay-Black.woff2") format("woff2"); } +@font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 900; font-display: swap; src: url("InterDisplay-BlackItalic.woff2") format("woff2"); } diff --git a/public/style/admin.css b/public/style/admin.css deleted file mode 100644 index 3cf6004..0000000 --- a/public/style/admin.css +++ /dev/null @@ -1,152 +0,0 @@ -@import url("/style/main.css"); - -main { - width: min(calc(100% - 4rem), 720px); - min-height: calc(100vh - 10.3rem); - margin: 0 auto 2rem auto; - padding-top: 4rem; -} - -main h1 { - line-height: 3rem; - color: var(--primary); -} - -main h2 { - color: var(--secondary); -} - -main h3 { - color: var(--tertiary); -} - -div#me_irl { - width: fit-content; - height: fit-content; - border: 2px solid white; -} - -div#me_irl img { - display: block; -} - -div#me_irl::before { - content: ""; - position: absolute; - width: 104px; - height: 104px; - transform: translate(2px, 2px); - background-image: linear-gradient(to top right, - var(--primary), - var(--secondary)); - z-index: -1; -} - -h1, -h2, -h3, -h4, -h5, -h6, -p, -small, -blockquote { - transition: background-color 0.1s; -} - -h1 a, -h2 a, -h3 a, -h4 a, -h5 a, -h6 a { - color: inherit; -} - -h1 a:hover, -h2 a:hover, -h3 a:hover, -h4 a:hover, -h5 a:hover, -h6 a:hover { - text-decoration: none; -} - -main h1:hover, -main h2:hover, -main h3:hover, -main h4:hover, -main h5:hover, -main h6:hover, -main p:hover, -main small:hover, -main blockquote:hover { - background-color: #fff1; -} - -blockquote { - margin: 1rem 0; - padding: 0 2.5rem; -} - -hr { - text-align: center; - line-height: 0px; - border-width: 1px 0 0 0; - border-color: #888f; - margin: 1.5em 0; - overflow: visible; -} - -ul.links { - display: flex; - gap: 1em .5em; - flex-wrap: wrap; -} - -ul.links li { - list-style: none; -} - -ul.links li a { - padding: .2em .5em; - border: 1px solid var(--links); - color: var(--links); - border-radius: 2px; - background-color: transparent; - transition-property: color, border-color, background-color; - transition-duration: .2s; - animation-delay: 0s; - animation: list-item-fadein .2s forwards; - opacity: 0; -} - -ul.links li a:hover { - color: #eee; - border-color: #eee; - background-color: var(--links) !important; - text-decoration: none; - box-shadow: 0 0 1em var(--links); -} - -div#web-buttons { - margin: 2rem 0; -} - -#web-buttons a { - text-decoration: none; -} - -#web-buttons img { - image-rendering: auto; - image-rendering: crisp-edges; - image-rendering: pixelated; -} - -#web-buttons img:hover { - margin: -1px; - border: 1px solid #eee; - transform: translate(-2px, -2px); - box-shadow: 1px 1px 0 #eee, 2px 2px 0 #eee; -} - diff --git a/schema.sql b/schema.sql index e0ada43..ab75620 100644 --- a/schema.sql +++ b/schema.sql @@ -1,19 +1,20 @@ -- -- Artists (should be applicable to all art) -- -CREATE TABLE artist ( - id uuid DEFAULT gen_random_uuid(), +CREATE TABLE public.artist ( + id character varying(64) DEFAULT gen_random_uuid(), name text NOT NULL, website text, avatar text ); -ALTER TABLE artist ADD CONSTRAINT artist_pk PRIMARY KEY (id); +ALTER TABLE public.artist ADD CONSTRAINT artist_pk PRIMARY KEY (id); -- -- Music releases -- -CREATE TABLE musicrelease ( +CREATE TABLE public.musicrelease ( id character varying(64) NOT NULL, + visible bool DEFAULT false, title text NOT NULL, description text, type text, @@ -22,56 +23,56 @@ CREATE TABLE musicrelease ( buyname text, buylink text ); -ALTER TABLE musicrelease ADD CONSTRAINT musicrelease_pk PRIMARY KEY (id); +ALTER TABLE public.musicrelease ADD CONSTRAINT musicrelease_pk PRIMARY KEY (id); -- -- Music links (external platform links under a release) -- -CREATE TABLE musiclink ( +CREATE TABLE public.musiclink ( release character varying(64) NOT NULL, name text NOT NULL, url text NOT NULL ); -ALTER TABLE musiclink ADD CONSTRAINT musiclink_pk PRIMARY KEY (release, name); +ALTER TABLE public.musiclink ADD CONSTRAINT musiclink_pk PRIMARY KEY (release, name); -- -- Music credits (artist credits under a release) -- -CREATE TABLE musiccredit ( +CREATE TABLE public.musiccredit ( release character varying(64) NOT NULL, - artist uuid NOT NULL, + artist character varying(64) NOT NULL, role text NOT NULL, is_primary boolean DEFAULT false ); -ALTER TABLE musiccredit ADD CONSTRAINT musiccredit_pk PRIMARY KEY (release, artist); +ALTER TABLE public.musiccredit ADD CONSTRAINT musiccredit_pk PRIMARY KEY (release, artist); -- -- Music tracks (tracks under a release) -- -CREATE TABLE musictrack ( +CREATE TABLE public.musictrack ( id uuid DEFAULT gen_random_uuid(), title text NOT NULL, description text, lyrics text, preview_url text ); -ALTER TABLE musictrack ADD CONSTRAINT musictrack_pk PRIMARY KEY (id); +ALTER TABLE public.musictrack ADD CONSTRAINT musictrack_pk PRIMARY KEY (id); -- -- Music release/track pairs -- -CREATE TABLE musicreleasetrack ( +CREATE TABLE public.musicreleasetrack ( release character varying(64) NOT NULL, track uuid NOT NULL, number integer NOT NULL ); -ALTER TABLE musicreleasetrack ADD CONSTRAINT musictrack_pk PRIMARY KEY (release, track); +ALTER TABLE public.musicreleasetrack ADD CONSTRAINT musicreleasetrack_pk PRIMARY KEY (release, track); -- -- Foreign keys -- -ALTER TABLE musiccredit ADD CONSTRAINT musiccredit_artist_fk FOREIGN KEY (artist) REFERENCES artist(id) ON DELETE CASCADE ON UPDATE CASCADE; -ALTER TABLE musiccredit ADD CONSTRAINT musiccredit_release_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON DELETE CASCADE; -ALTER TABLE musiclink ADD CONSTRAINT musiclink_release_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON UPDATE CASCADE ON DELETE CASCADE; -ALTER TABLE musicreleasetrack ADD CONSTRAINT music_pair_trackref_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON DELETE CASCADE; -ALTER TABLE musicreleasetrack ADD CONSTRAINT music_pair_releaseref_fk FOREIGN KEY (track) REFERENCES musictrack(id) ON DELETE CASCADE; +ALTER TABLE public.musiccredit ADD CONSTRAINT musiccredit_artist_fk FOREIGN KEY (artist) REFERENCES public.artist(id) ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE public.musiccredit ADD CONSTRAINT musiccredit_release_fk FOREIGN KEY (release) REFERENCES public.musicrelease(id) ON DELETE CASCADE; +ALTER TABLE public.musiclink ADD CONSTRAINT musiclink_release_fk FOREIGN KEY (release) REFERENCES public.musicrelease(id) ON UPDATE CASCADE ON DELETE CASCADE; +ALTER TABLE public.musicreleasetrack ADD CONSTRAINT music_pair_trackref_fk FOREIGN KEY (release) REFERENCES public.musicrelease(id) ON DELETE CASCADE; +ALTER TABLE public.musicreleasetrack ADD CONSTRAINT music_pair_releaseref_fk FOREIGN KEY (track) REFERENCES public.musictrack(id) ON DELETE CASCADE; diff --git a/views/admin.html b/views/admin.html deleted file mode 100644 index 5276a9a..0000000 --- a/views/admin.html +++ /dev/null @@ -1,22 +0,0 @@ -{{define "head"}} -admin - ari melody 💫 - - - -{{end}} - -{{define "content"}} -
- - -

- # admin panel -

- -

- whapow! nothing here. -
- nice try, though. -

-
-{{end}} diff --git a/views/admin/index.html b/views/admin/index.html new file mode 100644 index 0000000..ab7e407 --- /dev/null +++ b/views/admin/index.html @@ -0,0 +1,55 @@ +{{define "head"}} +admin - ari melody 💫 + +{{end}} + +{{define "content"}} +
+ + home + releases + artists +
+ +
+ +

Releases

+
+ {{range $Release := .Releases}} +
+
+ +
+
+

{{$Release.Title}} {{$Release.GetReleaseYear}}

+

{{$Release.PrintArtists true true}}

+

{{$Release.ReleaseType}}

+
+ Edit + Gateway +
+
+
+ {{end}} + {{if not .Releases}} +

There are no releases.

+ {{end}} +
+ +

Artists

+
+ {{range $Artist := .Artists}} +
+ + ari melody +
+ {{end}} + {{if not .Artists}} +

There are no artists.

+ {{end}} +
+ +
+ + +{{end}} diff --git a/views/admin/layout.html b/views/admin/layout.html new file mode 100644 index 0000000..06c15c9 --- /dev/null +++ b/views/admin/layout.html @@ -0,0 +1,22 @@ + + + + + + + + + + {{block "head" .}}{{end}} + + + + + + {{block "content" .}} + {{end}} + + {{template "prideflag"}} + + + diff --git a/views/admin/login.html b/views/admin/login.html new file mode 100644 index 0000000..af35e2e --- /dev/null +++ b/views/admin/login.html @@ -0,0 +1,20 @@ +{{define "head"}} +admin - ari melody 💫 + + + +{{end}} + +{{define "content"}} +
+ +

Log in with Discord.

+ +
+ + +{{end}} diff --git a/views/music-gateway.html b/views/music-gateway.html index 531b6d3..193134b 100644 --- a/views/music-gateway.html +++ b/views/music-gateway.html @@ -1,28 +1,28 @@ {{define "head"}} -{{.GetTitle}} - {{.PrintArtists true true}} +{{.Title}} - {{.PrintArtists true true}} - + - + - + - - + + - - - + + + - + @@ -47,51 +47,51 @@
- {{.GetTitle}} artwork + {{.Title}} artwork
-

{{.GetTitle}}

+

{{.Title}}

{{.GetReleaseYear}}

{{.PrintArtists true true}}

-

{{.GetType}}

+

{{.ReleaseType}}

- {{if .GetDescription}} + {{if .Description}}

- {{.GetDescription}} + {{.Description}}

{{end}}
- {{if .GetCredits}} + {{if .Credits}}

credits:

@@ -99,20 +99,20 @@ {{end}} {{if .IsSingle}} - {{$Track := index .GetTracks 0}} - {{if $Track.GetLyrics}} + {{$Track := index .Tracks 0}} + {{if $Track.Lyrics}}

lyrics:

-

{{$Track.GetLyrics}}

+

{{$Track.Lyrics}}

{{end}} {{else}}

tracks:

- {{range $i, $track := .GetTracks}} + {{range $i, $track := .Tracks}}
- {{$track.GetNumber}}. {{$track.GetTitle}} - {{$track.GetLyrics}} + {{$track.Number}}. {{$track.Title}} + {{$track.Lyrics}}
{{end}}
@@ -123,13 +123,13 @@