home to your local SPACEGIRL 💫 arimelody.space
1
fork

Configure Feed

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

HOLY REFACTOR GOOD GRIEF (also finally started some CRUD work)

Signed-off-by: ari melody <ari@arimelody.me>

+1571 -1330
+2 -2
.air.toml
··· 7 7 bin = "./tmp/main" 8 8 cmd = "go build -o ./tmp/main ." 9 9 delay = 1000 10 - exclude_dir = ["assets", "tmp", "vendor", "testdata"] 10 + exclude_dir = ["admin\\static", "public", "uploads"] 11 11 exclude_file = [] 12 12 exclude_regex = ["_test.go"] 13 13 exclude_unchanged = false 14 14 follow_symlink = false 15 15 full_bin = "" 16 - include_dir = [".", "admin", "colour", "db", "discord", "global", "music", "views"] 16 + include_dir = [] 17 17 include_ext = ["go", "tpl", "tmpl"] 18 18 include_file = [] 19 19 kill_delay = "0s"
+1 -1
.gitignore
··· 2 2 .idea/ 3 3 tmp/ 4 4 test/ 5 - data/* 5 + uploads/*
+6 -6
admin/admin.go
··· 9 9 10 10 type ( 11 11 Session struct { 12 - UserID string 13 - Token string 14 - Expires int64 12 + Token string 13 + UserID string 14 + Expires time.Time 15 15 } 16 16 ) 17 17 ··· 28 28 29 29 var sessions []*Session 30 30 31 - func createSession(UserID string) Session { 31 + func createSession(username string, expires time.Time) Session { 32 32 return Session{ 33 - UserID: UserID, 34 33 Token: string(generateToken()), 35 - Expires: time.Now().Add(24 * time.Hour).Unix(), 34 + UserID: username, 35 + Expires: expires, 36 36 } 37 37 } 38 38
+101 -65
admin/http.go
··· 12 12 13 13 "arimelody.me/arimelody.me/discord" 14 14 "arimelody.me/arimelody.me/global" 15 + musicModel "arimelody.me/arimelody.me/music/model" 15 16 ) 16 17 17 18 func Handler() http.Handler { 18 19 mux := http.NewServeMux() 19 20 21 + mux.Handle("/login", LoginHandler()) 22 + mux.Handle("/logout", MustAuthorise(LogoutHandler())) 23 + mux.Handle("/static/", http.StripPrefix("/static", staticHandler())) 20 24 mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 21 - w.WriteHeader(http.StatusOK) 22 - w.Write([]byte("hello /admin!")) 25 + if r.URL.Path != "/" { 26 + http.NotFound(w, r) 27 + return 28 + } 29 + 30 + session := GetSession(r) 31 + if session == nil { 32 + http.Redirect(w, r, "/admin/login", http.StatusFound) 33 + return 34 + } 35 + 36 + type IndexData struct { 37 + Releases []musicModel.Release 38 + Artists []musicModel.Artist 39 + } 40 + 41 + serveTemplate("index.html", IndexData{ 42 + Releases: global.Releases, 43 + Artists: global.Artists, 44 + }).ServeHTTP(w, r) 23 45 })) 24 - mux.Handle("/callback", global.HTTPLog(OAuthCallbackHandler())) 25 - mux.Handle("/login", global.HTTPLog(LoginHandler())) 26 - mux.Handle("/verify", global.HTTPLog(MustAuthorise(VerifyHandler()))) 27 - mux.Handle("/logout", global.HTTPLog(MustAuthorise(LogoutHandler()))) 28 - mux.Handle("/static", global.HTTPLog(MustAuthorise(staticHandler()))) 29 46 30 47 return mux 31 48 } 32 49 33 50 func MustAuthorise(next http.Handler) http.Handler { 34 51 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 35 - auth := r.Header.Get("Authorization") 36 - if strings.HasPrefix(auth, "Bearer ") { 37 - auth = auth[7:] 38 - } else { 39 - cookie, err := r.Cookie("token") 40 - if err != nil { 41 - http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) 42 - return 43 - } 44 - auth = cookie.Value 45 - } 46 - 47 - var session *Session 48 - for _, s := range sessions { 49 - if s.Expires < time.Now().Unix() { 50 - // expired session. remove it from the list! 51 - new_sessions := []*Session{} 52 - for _, ns := range sessions { 53 - if ns.Token == s.Token { 54 - continue 55 - } 56 - new_sessions = append(new_sessions, ns) 57 - } 58 - continue 59 - } 60 - 61 - if s.Token == auth { 62 - session = s 63 - break 64 - } 65 - } 66 - 52 + session := GetSession(r) 67 53 if session == nil { 68 54 http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) 69 55 return 70 56 } 71 57 72 - ctx := context.WithValue(r.Context(), "role", "admin") 58 + ctx := context.WithValue(r.Context(), "session", session) 73 59 next.ServeHTTP(w, r.WithContext(ctx)) 74 60 }) 75 61 } 76 62 63 + func GetSession(r *http.Request) *Session { 64 + // TODO: remove later- this bypasses auth! 65 + return &Session{} 66 + 67 + var token = "" 68 + // is the session token in context? 69 + var ctx_session = r.Context().Value("session") 70 + if ctx_session != nil { 71 + token = ctx_session.(string) 72 + } 73 + // okay, is it in the auth header? 74 + if token == "" { 75 + if strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") { 76 + token = r.Header.Get("Authorization")[7:] 77 + } 78 + } 79 + // finally, is it in the cookie? 80 + if token == "" { 81 + cookie, err := r.Cookie("token") 82 + if err != nil { 83 + return nil 84 + } 85 + token = cookie.Value 86 + } 87 + 88 + var session *Session = nil 89 + for _, s := range sessions { 90 + if s.Expires.Before(time.Now()) { 91 + // expired session. remove it from the list! 92 + new_sessions := []*Session{} 93 + for _, ns := range sessions { 94 + if ns.Token == s.Token { 95 + continue 96 + } 97 + new_sessions = append(new_sessions, ns) 98 + } 99 + sessions = new_sessions 100 + continue 101 + } 102 + 103 + if s.Token == token { 104 + session = s 105 + break 106 + } 107 + } 108 + 109 + return session 110 + } 111 + 77 112 func LoginHandler() http.Handler { 78 113 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 79 - if ADMIN_ID_DISCORD == "" { 114 + if discord.CREDENTIALS_PROVIDED && ADMIN_ID_DISCORD == "" { 80 115 http.Error(w, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable) 81 116 return 82 117 } ··· 84 119 code := r.URL.Query().Get("code") 85 120 86 121 if code == "" { 87 - http.Redirect(w, r, discord.REDIRECT_URI, http.StatusTemporaryRedirect) 122 + serveTemplate("login.html", discord.REDIRECT_URI).ServeHTTP(w, r) 88 123 return 89 124 } 90 125 ··· 109 144 } 110 145 111 146 // login success! 112 - session := createSession(discord_user.Username) 147 + session := createSession(discord_user.Username, time.Now().Add(24 * time.Hour)) 113 148 sessions = append(sessions, &session) 114 149 115 150 cookie := http.Cookie{} ··· 122 157 http.SetCookie(w, &cookie) 123 158 124 159 w.WriteHeader(http.StatusOK) 125 - w.Write([]byte(session.Token)) 160 + w.Header().Add("Content-Type", "text/html") 161 + w.Write([]byte( 162 + "<!DOCTYPE html><html><head>"+ 163 + "<meta http-equiv=\"refresh\" content=\"5;url=/admin/\" />"+ 164 + "</head><body>"+ 165 + "Logged in successfully. "+ 166 + "You should be redirected to <a href=\"/admin/\">/admin/</a> in 5 seconds."+ 167 + "</body></html>"), 168 + ) 126 169 }) 127 170 } 128 171 129 172 func LogoutHandler() http.Handler { 130 173 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 174 + if r.Method != http.MethodGet { 175 + http.NotFound(w, r) 176 + return 177 + } 178 + 131 179 token := r.Context().Value("token").(string) 132 180 133 181 if token == "" { ··· 145 193 }(token) 146 194 147 195 w.WriteHeader(http.StatusOK) 148 - w.Write([]byte("OK")) 196 + w.Write([]byte( 197 + "<meta http-equiv=\"refresh\" content=\"5;url=/\" />"+ 198 + "Logged out successfully. "+ 199 + "You should be redirected to <a href=\"/\">/</a> in 5 seconds."), 200 + ) 149 201 }) 150 202 } 151 203 152 - func OAuthCallbackHandler() http.Handler { 153 - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 154 - }) 155 - } 156 - 157 - func VerifyHandler() http.Handler { 158 - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 159 - // this is an authorised endpoint, so you *must* supply a valid token 160 - // before accessing this route. 161 - w.WriteHeader(http.StatusOK) 162 - w.Write([]byte("OK")) 163 - }) 164 - } 165 - 166 - func ServeTemplate(page string, data any) http.Handler { 204 + func serveTemplate(page string, data any) http.Handler { 167 205 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 168 - lp_layout := filepath.Join("views", "layout.html") 169 - lp_header := filepath.Join("views", "header.html") 170 - lp_footer := filepath.Join("views", "footer.html") 206 + lp_layout := filepath.Join("views", "admin", "layout.html") 171 207 lp_prideflag := filepath.Join("views", "prideflag.html") 172 - fp := filepath.Join("views", filepath.Clean(page)) 208 + fp := filepath.Join("views", "admin", filepath.Clean(page)) 173 209 174 210 info, err := os.Stat(fp) 175 211 if err != nil { ··· 184 220 return 185 221 } 186 222 187 - template, err := template.ParseFiles(lp_layout, lp_header, lp_footer, lp_prideflag, fp) 223 + template, err := template.ParseFiles(lp_layout, lp_prideflag, fp) 188 224 if err != nil { 189 225 fmt.Printf("Error parsing template files: %s\n", err) 190 226 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+187
admin/static/admin.css
··· 1 + @import url("/style/prideflag.css"); 2 + @import url("/font/inter/inter.css"); 3 + 4 + body { 5 + width: 100%; 6 + height: 100vh; 7 + 8 + margin: 0; 9 + padding: 0; 10 + 11 + font-family: "Inter", sans-serif; 12 + font-size: 16px; 13 + 14 + color: #303030; 15 + background: #f0f0f0; 16 + } 17 + 18 + header { 19 + width: min(720px, calc(100% - 2em)); 20 + height: 2em; 21 + margin: 1em auto; 22 + display: flex; 23 + flex-direction: row; 24 + justify-content: center; 25 + 26 + background: #f8f8f8; 27 + border-radius: .5em; 28 + border: 1px solid #808080; 29 + } 30 + header .icon { 31 + height: 100%; 32 + 33 + margin-right: 1em; 34 + } 35 + 36 + header a { 37 + height: 100%; 38 + width: auto; 39 + 40 + margin: 0px; 41 + padding: 0 1em; 42 + 43 + display: flex; 44 + 45 + line-height: 2em; 46 + text-decoration: none; 47 + 48 + color: inherit; 49 + } 50 + 51 + header a:hover { 52 + background: #00000010; 53 + } 54 + 55 + main { 56 + width: min(720px, calc(100% - 2em)); 57 + margin: 0 auto; 58 + padding: 1em; 59 + } 60 + 61 + a { 62 + color: inherit; 63 + text-decoration: none; 64 + } 65 + 66 + a:hover { 67 + text-decoration: underline; 68 + } 69 + 70 + .card h2 { 71 + margin: 0 0 .5em 0; 72 + } 73 + 74 + .card h3, 75 + .card p { 76 + margin: 0; 77 + } 78 + 79 + .release { 80 + padding: 1em; 81 + display: flex; 82 + flex-direction: row; 83 + gap: 1em; 84 + 85 + border-radius: .5em; 86 + background: #f8f8f8f8; 87 + border: 1px solid #808080; 88 + } 89 + 90 + .release-artwork { 91 + width: 96px; 92 + 93 + display: flex; 94 + justify-content: center; 95 + align-items: center; 96 + } 97 + 98 + .release-artwork img { 99 + width: 100%; 100 + aspect-ratio: 1; 101 + } 102 + 103 + .latest-release .release-info { 104 + width: 300px; 105 + flex-direction: column; 106 + } 107 + 108 + .release-title small { 109 + opacity: .75; 110 + } 111 + 112 + .release-links { 113 + margin: .5em 0; 114 + padding: 0; 115 + display: flex; 116 + flex-direction: row; 117 + list-style: none; 118 + flex-wrap: wrap; 119 + gap: .5em; 120 + } 121 + 122 + .release-links li { 123 + flex-grow: 1; 124 + } 125 + 126 + .release-links a { 127 + padding: .5em; 128 + display: block; 129 + 130 + border-radius: .5em; 131 + text-decoration: none; 132 + color: #f0f0f0; 133 + background: #303030; 134 + text-align: center; 135 + 136 + transition: color .1s, background .1s; 137 + } 138 + 139 + .release-links a:hover { 140 + color: #303030; 141 + background: #f0f0f0; 142 + } 143 + 144 + .release-actions { 145 + margin-top: .5em; 146 + } 147 + 148 + .release-actions a { 149 + margin-right: .3em; 150 + padding: .3em .5em; 151 + display: inline-block; 152 + 153 + border-radius: .3em; 154 + background: #e0e0e0; 155 + 156 + transition: color .1s, background .1s; 157 + } 158 + 159 + .release-actions a:hover { 160 + color: #303030; 161 + background: #f0f0f0; 162 + 163 + text-decoration: none; 164 + } 165 + 166 + .artist { 167 + padding: .5em; 168 + display: flex; 169 + flex-direction: row; 170 + align-items: center; 171 + gap: .5em; 172 + 173 + border-radius: .5em; 174 + background: #f8f8f8f8; 175 + border: 1px solid #808080; 176 + } 177 + 178 + .artist:hover { 179 + text-decoration: hover; 180 + } 181 + 182 + .artist-avatar { 183 + width: 32px; 184 + height: 32px; 185 + object-fit: cover; 186 + border-radius: 100%; 187 + }
admin/static/admin.js

This is a binary file and will not be displayed.

+43
api/api.go
··· 1 + package api 2 + 3 + import ( 4 + "net/http" 5 + 6 + "arimelody.me/arimelody.me/admin" 7 + music "arimelody.me/arimelody.me/music/view" 8 + ) 9 + 10 + func Handler() http.Handler { 11 + mux := http.NewServeMux() 12 + 13 + mux.Handle("/v1/artist/", http.StripPrefix("/v1/artist", ServeArtist())) 14 + mux.Handle("/v1/artist", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 15 + switch r.Method { 16 + case http.MethodGet: 17 + ServeAllArtists().ServeHTTP(w, r) 18 + return 19 + case http.MethodPost: 20 + admin.MustAuthorise(CreateArtist()).ServeHTTP(w, r) 21 + return 22 + default: 23 + http.NotFound(w, r) 24 + return 25 + } 26 + })) 27 + mux.Handle("/v1/music/", http.StripPrefix("/v1/music", music.ServeRelease())) 28 + mux.Handle("/v1/music", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 29 + switch r.Method { 30 + case http.MethodGet: 31 + ServeCatalog().ServeHTTP(w, r) 32 + return 33 + case http.MethodPost: 34 + admin.MustAuthorise(CreateRelease()).ServeHTTP(w, r) 35 + return 36 + default: 37 + http.NotFound(w, r) 38 + return 39 + } 40 + })) 41 + 42 + return mux 43 + }
+130
api/artist.go
··· 1 + package api 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + "net/http" 7 + 8 + "arimelody.me/arimelody.me/global" 9 + "arimelody.me/arimelody.me/music/model" 10 + controller "arimelody.me/arimelody.me/music/controller" 11 + ) 12 + 13 + func ServeAllArtists() http.Handler { 14 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 15 + type ( 16 + creditJSON struct { 17 + Role string `json:"role"` 18 + Primary bool `json:"primary"` 19 + } 20 + ) 21 + 22 + var artists = []model.Artist{} 23 + for _, artist := range global.Artists { 24 + artists = append(artists, model.Artist{ 25 + ID: artist.ID, 26 + Name: artist.Name, 27 + Website: artist.Website, 28 + Avatar: artist.Avatar, 29 + }) 30 + } 31 + 32 + w.Header().Add("Content-Type", "application/json") 33 + err := json.NewEncoder(w).Encode(artists) 34 + if err != nil { 35 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 36 + return 37 + } 38 + }) 39 + } 40 + 41 + func ServeArtist() http.Handler { 42 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 43 + if r.URL.Path == "/" { 44 + ServeAllArtists().ServeHTTP(w, r) 45 + return 46 + } 47 + 48 + type ( 49 + creditJSON struct { 50 + Role string `json:"role"` 51 + Primary bool `json:"primary"` 52 + } 53 + artistJSON struct { 54 + model.Artist 55 + Credits map[string]creditJSON `json:"credits"` 56 + } 57 + ) 58 + var res = artistJSON{} 59 + 60 + res.ID = r.URL.Path[1:] 61 + var artist = global.GetArtist(res.ID) 62 + if artist == nil { 63 + http.NotFound(w, r) 64 + return 65 + } 66 + res.Name = artist.Name 67 + res.Website = artist.Website 68 + res.Credits = make(map[string]creditJSON) 69 + 70 + for _, release := range global.Releases { 71 + for _, credit := range release.Credits { 72 + if credit.Artist.ID != res.ID { 73 + continue 74 + } 75 + res.Credits[release.ID] = creditJSON{ 76 + Role: credit.Role, 77 + Primary: credit.Primary, 78 + } 79 + } 80 + } 81 + 82 + w.Header().Add("Content-Type", "application/json") 83 + err := json.NewEncoder(w).Encode(res) 84 + if err != nil { 85 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 86 + return 87 + } 88 + }) 89 + } 90 + 91 + func CreateArtist() http.Handler { 92 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 93 + if r.Method != http.MethodPost { 94 + http.NotFound(w, r) 95 + return 96 + } 97 + 98 + var data model.Artist 99 + err := json.NewDecoder(r.Body).Decode(&data) 100 + if err != nil { 101 + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 102 + return 103 + } 104 + 105 + if global.GetArtist(data.ID) != nil { 106 + http.Error(w, fmt.Sprintf("Artist %s already exists", data.ID), http.StatusBadRequest) 107 + return 108 + } 109 + 110 + var artist = model.Artist{ 111 + ID: data.ID, 112 + Name: data.Name, 113 + Website: data.Website, 114 + Avatar: data.Avatar, 115 + } 116 + 117 + global.Artists = append(global.Artists, artist) 118 + 119 + err = controller.CreateArtistDB(global.DB, &artist) 120 + if err != nil { 121 + fmt.Printf("Failed to create artist %s: %s\n", artist.ID, err) 122 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 123 + return 124 + } 125 + 126 + w.Header().Add("Content-Type", "application/json") 127 + w.WriteHeader(http.StatusCreated) 128 + err = json.NewEncoder(w).Encode(artist) 129 + }) 130 + }
+92
api/music.go
··· 1 + package api 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + "net/http" 7 + "time" 8 + 9 + "arimelody.me/arimelody.me/admin" 10 + "arimelody.me/arimelody.me/global" 11 + "arimelody.me/arimelody.me/music/model" 12 + controller "arimelody.me/arimelody.me/music/controller" 13 + ) 14 + 15 + func ServeCatalog() http.Handler { 16 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 17 + releases := []model.Release{} 18 + authorised := admin.GetSession(r) != nil 19 + for _, release := range global.Releases { 20 + if !release.IsReleased() && !authorised { 21 + continue 22 + } 23 + releases = append(releases, release) 24 + } 25 + 26 + w.Header().Add("Content-Type", "application/json") 27 + err := json.NewEncoder(w).Encode(releases) 28 + if err != nil { 29 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 30 + return 31 + } 32 + }) 33 + } 34 + 35 + func CreateRelease() http.Handler { 36 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 37 + if r.Method != http.MethodPost { 38 + http.NotFound(w, r) 39 + return 40 + } 41 + 42 + type PostReleaseBody struct { 43 + ID string `json:"id"` 44 + Title string `json:"title"` 45 + Description string `json:"description"` 46 + ReleaseType model.ReleaseType `json:"type"` 47 + ReleaseDate time.Time `json:"releaseDate"` 48 + Artwork string `json:"artwork"` 49 + Buyname string `json:"buyname"` 50 + Buylink string `json:"buylink"` 51 + } 52 + 53 + var data PostReleaseBody 54 + err := json.NewDecoder(r.Body).Decode(&data) 55 + if err != nil { 56 + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 57 + return 58 + } 59 + 60 + if global.GetRelease(data.ID) != nil { 61 + http.Error(w, fmt.Sprintf("Release %s already exists", data.ID), http.StatusBadRequest) 62 + return 63 + } 64 + 65 + var release = model.Release{ 66 + ID: data.ID, 67 + Title: data.Title, 68 + Description: data.Description, 69 + ReleaseType: data.ReleaseType, 70 + ReleaseDate: data.ReleaseDate, 71 + Artwork: data.Artwork, 72 + Buyname: data.Buyname, 73 + Buylink: data.Buylink, 74 + Links: []model.Link{}, 75 + Credits: []model.Credit{}, 76 + Tracks: []model.Track{}, 77 + } 78 + 79 + global.Releases = append([]model.Release{release}, global.Releases...) 80 + 81 + err = controller.CreateReleaseDB(global.DB, &release) 82 + if err != nil { 83 + fmt.Printf("Failed to create release %s: %s\n", release.ID, err) 84 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 85 + return 86 + } 87 + 88 + w.Header().Add("Content-Type", "application/json") 89 + w.WriteHeader(http.StatusCreated) 90 + err = json.NewEncoder(w).Encode(release) 91 + }) 92 + }
-24
db/db.go
··· 1 - package db 2 - 3 - import ( 4 - "fmt" 5 - "os" 6 - "time" 7 - 8 - "github.com/jmoiron/sqlx" 9 - _ "github.com/lib/pq" 10 - ) 11 - 12 - func InitDatabase() *sqlx.DB { 13 - db, err := sqlx.Connect("postgres", "user=arimelody dbname=arimelody password=fuckingpassword sslmode=disable") 14 - if err != nil { 15 - fmt.Fprintf(os.Stderr, "unable to create database connection pool: %v\n", err) 16 - os.Exit(1) 17 - } 18 - 19 - db.SetConnMaxLifetime(time.Minute * 3) 20 - db.SetMaxOpenConns(10) 21 - db.SetMaxIdleConns(10) 22 - 23 - return db 24 - }
+24 -19
discord/discord.go
··· 12 12 13 13 const API_ENDPOINT = "https://discord.com/api/v10" 14 14 15 + var CREDENTIALS_PROVIDED = true 15 16 var CLIENT_ID = func() string { 16 17 envvar := os.Getenv("DISCORD_CLIENT_ID") 17 18 if envvar == "" { 18 19 fmt.Printf("DISCORD_CLIENT_ID was not provided. Admin login will be unavailable.\n") 20 + CREDENTIALS_PROVIDED = false 19 21 } 20 22 return envvar 21 23 }() ··· 23 25 envvar := os.Getenv("DISCORD_CLIENT_SECRET") 24 26 if envvar == "" { 25 27 fmt.Printf("DISCORD_CLIENT_SECRET was not provided. Admin login will be unavailable.\n") 28 + CREDENTIALS_PROVIDED = false 26 29 } 27 30 return envvar 28 31 }() ··· 30 33 envvar := os.Getenv("DISCORD_REDIRECT_URI") 31 34 if envvar == "" { 32 35 fmt.Printf("DISCORD_REDIRECT_URI was not provided. Admin login will be unavailable.\n") 36 + CREDENTIALS_PROVIDED = false 33 37 } 34 38 return envvar 35 39 }() ··· 37 41 envvar := os.Getenv("OAUTH_CALLBACK_URI") 38 42 if envvar == "" { 39 43 fmt.Printf("OAUTH_CALLBACK_URI was not provided. Admin login will be unavailable.\n") 44 + CREDENTIALS_PROVIDED = false 40 45 } 41 46 return envvar 42 47 }() 43 48 44 49 type ( 45 50 AccessTokenResponse struct { 51 + AccessToken string `json:"access_token"` 46 52 TokenType string `json:"token_type"` 47 - AccessToken string `json:"access_token"` 48 53 ExpiresIn int `json:"expires_in"` 49 54 RefreshToken string `json:"refresh_token"` 50 55 Scope string `json:"scope"` ··· 52 57 53 58 AuthInfoResponse struct { 54 59 Application struct { 55 - Id string 56 - Name string 57 - Icon string 58 - Description string 59 - Hook bool 60 - BotPublic bool 61 - botRequireCodeGrant bool 62 - VerifyKey bool 63 - } 64 - Scopes []string 65 - Expires string 66 - User DiscordUser 60 + Id string `json:"id"` 61 + Name string `json:"name"` 62 + Icon string `json:"icon"` 63 + Description string `json:"description"` 64 + Hook bool `json:"hook"` 65 + BotPublic bool `json:"bot_public"` 66 + BotRequireCodeGrant bool `json:"bot_require_code_grant"` 67 + VerifyKey string `json:"verify_key"` 68 + } `json:"application"` 69 + Scopes []string `json:"scopes"` 70 + Expires string `json:"expires"` 71 + User DiscordUser `json:"user"` 67 72 } 68 73 69 74 DiscordUser struct { 70 - Id string 71 - Username string 72 - Avatar string 73 - Discriminator string 74 - GlobalName string 75 - PublicFlags int 75 + Id string `json:"id"` 76 + Username string `json:"username"` 77 + Avatar string `json:"avatar"` 78 + Discriminator string `json:"discriminator"` 79 + GlobalName string `json:"global_name"` 80 + PublicFlags int `json:"public_flags"` 76 81 } 77 82 ) 78 83
+40
global/data.go
··· 1 + package global 2 + 3 + import ( 4 + "os" 5 + 6 + "arimelody.me/arimelody.me/music/model" 7 + "github.com/jmoiron/sqlx" 8 + ) 9 + 10 + var HTTP_DOMAIN = func() string { 11 + envvar := os.Getenv("HTTP_DOMAIN") 12 + if envvar != "" { 13 + return envvar 14 + } 15 + return "https://arimelody.me" 16 + } 17 + 18 + var DB *sqlx.DB 19 + 20 + var Releases []model.Release 21 + var Artists []model.Artist 22 + var Tracks []model.Track 23 + 24 + func GetRelease(id string) *model.Release { 25 + for _, release := range Releases { 26 + if release.ID == id { 27 + return &release 28 + } 29 + } 30 + return nil 31 + } 32 + 33 + func GetArtist(id string) *model.Artist { 34 + for _, artist := range Artists { 35 + if artist.ID == id { 36 + return &artist 37 + } 38 + } 39 + return nil 40 + }
+1
global/global.go global/funcs.go
··· 16 16 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 17 17 w.Header().Add("Server", "arimelody.me") 18 18 w.Header().Add("Cache-Control", "max-age=2592000") 19 + next.ServeHTTP(w, r) 19 20 }) 20 21 } 21 22
+33 -24
main.go
··· 6 6 "net/http" 7 7 "os" 8 8 "path/filepath" 9 + "time" 9 10 10 11 "arimelody.me/arimelody.me/admin" 11 - "arimelody.me/arimelody.me/music" 12 - "arimelody.me/arimelody.me/db" 12 + "arimelody.me/arimelody.me/api" 13 13 "arimelody.me/arimelody.me/global" 14 + musicController "arimelody.me/arimelody.me/music/controller" 15 + musicView "arimelody.me/arimelody.me/music/view" 16 + 17 + "github.com/jmoiron/sqlx" 18 + _ "github.com/lib/pq" 14 19 ) 15 20 16 21 const DEFAULT_PORT int = 8080 17 22 18 23 func main() { 19 - db := db.InitDatabase() 20 - defer db.Close() 24 + // initialise database connection 25 + var err error 26 + global.DB, err = sqlx.Connect("postgres", "user=arimelody dbname=arimelody password=fuckingpassword sslmode=disable") 27 + if err != nil { 28 + fmt.Fprintf(os.Stderr, "unable to create database connection pool: %v\n", err) 29 + os.Exit(1) 30 + } 31 + global.DB.SetConnMaxLifetime(time.Minute * 3) 32 + global.DB.SetMaxOpenConns(10) 33 + global.DB.SetMaxIdleConns(10) 34 + defer global.DB.Close() 21 35 22 - var err error 23 - music.Artists, err = music.PullAllArtists(db) 36 + // pull artist data from DB 37 + global.Artists, err = musicController.PullAllArtists(global.DB) 24 38 if err != nil { 25 39 fmt.Printf("Failed to pull artists from database: %v\n", err); 26 40 panic(1) 27 41 } 28 - fmt.Printf("%d artists loaded successfully.\n", len(music.Artists)) 42 + fmt.Printf("%d artists loaded successfully.\n", len(global.Artists)) 29 43 30 - music.Releases, err = music.PullAllReleases(db) 44 + // pull release data from DB 45 + global.Releases, err = musicController.PullAllReleases(global.DB) 31 46 if err != nil { 32 47 fmt.Printf("Failed to pull releases from database: %v\n", err); 33 48 panic(1) 34 49 } 35 - fmt.Printf("%d releases loaded successfully.\n", len(music.Releases)) 50 + fmt.Printf("%d releases loaded successfully.\n", len(global.Releases)) 36 51 52 + // start the web server! 37 53 mux := createServeMux() 38 - 39 54 port := DEFAULT_PORT 40 55 fmt.Printf("now serving at http://127.0.0.1:%d\n", port) 41 - log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), mux)) 56 + log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), global.HTTPLog(mux))) 42 57 } 43 58 44 59 func createServeMux() *http.ServeMux { 45 60 mux := http.NewServeMux() 46 61 47 - mux.Handle("/admin/", global.HTTPLog(http.StripPrefix("/admin", admin.Handler()))) 48 - 49 - mux.Handle("/api/v1/music/artist/", global.HTTPLog(http.StripPrefix("/api/v1/music/artist", music.ServeArtist()))) 50 - mux.Handle("/api/v1/music/", global.HTTPLog(http.StripPrefix("/api/v1/music", music.ServeRelease()))) 51 - mux.Handle("/api/v1/music", global.HTTPLog(music.PostRelease())) 52 - 53 - mux.Handle("/music-artwork/", global.HTTPLog(http.StripPrefix("/music-artwork", music.ServeArtwork()))) 54 - mux.Handle("/music/", global.HTTPLog(http.StripPrefix("/music", music.ServeGateway()))) 55 - mux.Handle("/music", global.HTTPLog(music.ServeCatalog())) 56 - 57 - mux.Handle("/", global.HTTPLog(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 62 + mux.Handle("/admin/", http.StripPrefix("/admin", admin.Handler())) 63 + mux.Handle("/api/", http.StripPrefix("/api", api.Handler())) 64 + mux.Handle("/music/", http.StripPrefix("/music", musicView.Handler())) 65 + mux.Handle("/uploads/", http.StripPrefix("/uploads", staticHandler("uploads"))) 66 + mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 58 67 if r.URL.Path == "/" || r.URL.Path == "/index.html" { 59 68 global.ServeTemplate("index.html", nil).ServeHTTP(w, r) 60 69 return 61 70 } 62 71 staticHandler("public").ServeHTTP(w, r) 63 - }))) 72 + })) 64 73 65 74 return mux 66 75 } ··· 82 91 return 83 92 } 84 93 85 - http.FileServer(http.Dir("./public")).ServeHTTP(w, r) 94 + http.FileServer(http.Dir(directory)).ServeHTTP(w, r) 86 95 }) 87 96 }
-155
music/artist.go
··· 1 - package music 2 - 3 - import ( 4 - "encoding/json" 5 - "net/http" 6 - 7 - "github.com/jmoiron/sqlx" 8 - ) 9 - 10 - type Artist struct { 11 - ID string `json:"id"` 12 - Name string `json:"name"` 13 - Website string `json:"website"` 14 - } 15 - 16 - var Artists []Artist 17 - 18 - func GetArtist(id string) *Artist { 19 - for _, artist := range Artists { 20 - if artist.GetID() == id { 21 - return &artist 22 - } 23 - } 24 - return nil 25 - } 26 - 27 - // GETTERS 28 - 29 - func (artist Artist) GetID() string { 30 - return artist.ID 31 - } 32 - 33 - func (artist Artist) GetName() string { 34 - return artist.Name 35 - } 36 - 37 - func (artist Artist) GetWebsite() string { 38 - return artist.Website 39 - } 40 - 41 - // SETTERS 42 - 43 - func (artist Artist) SetID(id string) error { 44 - artist.ID = id 45 - return nil 46 - } 47 - 48 - func (artist Artist) SetName(name string) error { 49 - artist.Name = name 50 - return nil 51 - } 52 - 53 - func (artist Artist) SetWebsite(website string) error { 54 - artist.Website = website 55 - return nil 56 - } 57 - 58 - // DATABASE 59 - 60 - func (artist Artist) PushToDB(db *sqlx.DB) { 61 - // fmt.Printf("Pushing artist [%s] to database...", artist.Name) 62 - 63 - db.MustExec( 64 - "INSERT INTO artists (id, name, website) "+ 65 - "VALUES ($1, $2, $3) "+ 66 - "ON CONFLICT (id) "+ 67 - "DO UPDATE SET name=$2, website=$3", 68 - artist.ID, 69 - artist.Name, 70 - artist.Website, 71 - ) 72 - 73 - // fmt.Printf("done!\n") 74 - } 75 - 76 - func PullAllArtists(db *sqlx.DB) ([]Artist, error) { 77 - artists := []Artist{} 78 - 79 - rows, err := db.Query("SELECT id, name, website FROM artists") 80 - if err != nil { 81 - return nil, err 82 - } 83 - 84 - for rows.Next() { 85 - var artist = Artist{} 86 - 87 - err = rows.Scan( 88 - &artist.ID, 89 - &artist.Name, 90 - &artist.Website, 91 - ) 92 - if err != nil { 93 - return nil, err 94 - } 95 - 96 - artists = append(artists, artist) 97 - } 98 - 99 - return artists, nil 100 - } 101 - 102 - // HTTP HANDLERS 103 - 104 - func ServeArtist() http.Handler { 105 - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 106 - if r.URL.Path == "/" { 107 - http.NotFound(w, r) 108 - return 109 - } 110 - 111 - type ( 112 - creditJSON struct { 113 - Role string `json:"role"` 114 - Primary bool `json:"primary"` 115 - } 116 - artistJSON struct { 117 - Artist 118 - Credits map[string]creditJSON `json:"credits"` 119 - } 120 - ) 121 - var res = artistJSON{} 122 - 123 - res.ID = r.URL.Path[1:] 124 - var artist = GetArtist(res.ID) 125 - if artist == nil { 126 - http.NotFound(w, r) 127 - return 128 - } 129 - res.Name = artist.Name 130 - res.Website = artist.Website 131 - res.Credits = make(map[string]creditJSON) 132 - 133 - for _, release := range Releases { 134 - for _, credit := range release.Credits { 135 - if credit.Artist.ID != res.ID { 136 - continue 137 - } 138 - res.Credits[release.ID] = creditJSON{ 139 - Role: credit.Role, 140 - Primary: credit.Primary, 141 - } 142 - } 143 - } 144 - 145 - jsonBytes, err := json.Marshal(res) 146 - if err != nil { 147 - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 148 - return 149 - } 150 - 151 - w.Header().Add("Content-Type", "application/json") 152 - w.WriteHeader(http.StatusOK) 153 - w.Write(jsonBytes) 154 - }) 155 - }
+65
music/controller/artist.go
··· 1 + package music 2 + 3 + import ( 4 + "arimelody.me/arimelody.me/music/model" 5 + "github.com/jmoiron/sqlx" 6 + ) 7 + 8 + // DATABASE 9 + 10 + func PullAllArtists(db *sqlx.DB) ([]model.Artist, error) { 11 + var artists = []model.Artist{} 12 + 13 + err := db.Select(&artists, "SELECT * FROM artist") 14 + if err != nil { 15 + return nil, err 16 + } 17 + 18 + return artists, nil 19 + } 20 + 21 + func CreateArtistDB(db *sqlx.DB, artist *model.Artist) error { 22 + _, err := db.Exec( 23 + "INSERT INTO artist (id, name, website, avatar) "+ 24 + "VALUES ($1, $2, $3, $4)", 25 + artist.ID, 26 + artist.Name, 27 + artist.Website, 28 + artist.Avatar, 29 + ) 30 + if err != nil { 31 + return err 32 + } 33 + 34 + return nil 35 + } 36 + 37 + func UpdateArtistDB(db *sqlx.DB, artist *model.Artist) error { 38 + _, err := db.Exec( 39 + "UPDATE artist "+ 40 + "SET name=$2, website=$3, avatar=$4 "+ 41 + "WHERE id=$1", 42 + artist.ID, 43 + artist.Name, 44 + artist.Website, 45 + artist.Avatar, 46 + ) 47 + if err != nil { 48 + return err 49 + } 50 + 51 + return nil 52 + } 53 + 54 + func DeleteArtistDB(db *sqlx.DB, artistID string) error { 55 + _, err := db.Exec( 56 + "DELETE FROM artist "+ 57 + "WHERE id=$1", 58 + artistID, 59 + ) 60 + if err != nil { 61 + return err 62 + } 63 + 64 + return nil 65 + }
+70
music/controller/credit.go
··· 1 + package music 2 + 3 + import ( 4 + "arimelody.me/arimelody.me/music/model" 5 + "github.com/jmoiron/sqlx" 6 + ) 7 + 8 + // DATABASE 9 + 10 + func PullReleaseCredits(db *sqlx.DB, releaseID string) ([]model.Credit, error) { 11 + var credits = []model.Credit{} 12 + 13 + err := db.Select( 14 + &credits, 15 + "SELECT * FROM musiccredit WHERE release=$1", 16 + releaseID, 17 + ) 18 + if err != nil { 19 + return nil, err 20 + } 21 + 22 + return credits, nil 23 + } 24 + 25 + func CreateCreditDB(db *sqlx.DB, releaseID string, artistID string, credit *model.Credit) (error) { 26 + _, err := db.Exec( 27 + "INSERT INTO musiccredit (release, artist, role, is_primary) "+ 28 + "VALUES ($1, $2, $3, $4)", 29 + releaseID, 30 + artistID, 31 + credit.Role, 32 + credit.Primary, 33 + ) 34 + if err != nil { 35 + return err 36 + } 37 + 38 + return nil 39 + } 40 + 41 + func UpdateCreditDB(db *sqlx.DB, releaseID string, artistID string, credit *model.Credit) (error) { 42 + _, err := db.Exec( 43 + "UPDATE musiccredit SET "+ 44 + "role=$3, is_primary=$4 "+ 45 + "WHERE release=$1, artist=$2", 46 + releaseID, 47 + artistID, 48 + credit.Role, 49 + credit.Primary, 50 + ) 51 + if err != nil { 52 + return err 53 + } 54 + 55 + return nil 56 + } 57 + 58 + func DeleteCreditDB(db *sqlx.DB, releaseID string, artistID string) (error) { 59 + _, err := db.Exec( 60 + "DELETE FROM musiccredit "+ 61 + "WHERE release=$1, artist=$2", 62 + releaseID, 63 + artistID, 64 + ) 65 + if err != nil { 66 + return err 67 + } 68 + 69 + return nil 70 + }
+68
music/controller/link.go
··· 1 + package music 2 + 3 + import ( 4 + "arimelody.me/arimelody.me/music/model" 5 + "github.com/jmoiron/sqlx" 6 + ) 7 + 8 + // DATABASE 9 + 10 + func PullReleaseLinks(db *sqlx.DB, releaseID string) ([]model.Link, error) { 11 + var links = []model.Link{} 12 + 13 + err := db.Select( 14 + &links, 15 + "SELECT * FROM musiclink WHERE release=$1", 16 + releaseID, 17 + ) 18 + if err != nil { 19 + return nil, err 20 + } 21 + 22 + return links, nil 23 + } 24 + 25 + func CreateLinkDB(db *sqlx.DB, releaseID string, link *model.Link) (error) { 26 + _, err := db.Exec( 27 + "INSERT INTO musiclink (release, name, url) "+ 28 + "VALUES ($1, $2, $3)", 29 + releaseID, 30 + link.Name, 31 + link.URL, 32 + ) 33 + if err != nil { 34 + return err 35 + } 36 + 37 + return nil 38 + } 39 + 40 + func UpdateLinkDB(db *sqlx.DB, releaseID string, link *model.Link) (error) { 41 + _, err := db.Exec( 42 + "UPDATE musiclink SET "+ 43 + "name=$2, url=$3 "+ 44 + "WHERE release=$1", 45 + releaseID, 46 + link.Name, 47 + link.URL, 48 + ) 49 + if err != nil { 50 + return err 51 + } 52 + 53 + return nil 54 + } 55 + 56 + func DeleteLinkDB(db *sqlx.DB, releaseID string, link *model.Link) (error) { 57 + _, err := db.Exec( 58 + "DELETE FROM musiclink "+ 59 + "WHERE release=$1, name=$2", 60 + releaseID, 61 + link.Name, 62 + ) 63 + if err != nil { 64 + return err 65 + } 66 + 67 + return nil 68 + }
+76
music/controller/release.go
··· 1 + package music 2 + 3 + import ( 4 + "arimelody.me/arimelody.me/music/model" 5 + "github.com/jmoiron/sqlx" 6 + ) 7 + 8 + // DATABASE 9 + 10 + func PullAllReleases(db *sqlx.DB) ([]model.Release, error) { 11 + var releases = []model.Release{} 12 + 13 + err := db.Select(&releases, "SELECT * FROM musicrelease ORDER BY release_date DESC") 14 + if err != nil { 15 + return nil, err 16 + } 17 + 18 + return releases, nil 19 + } 20 + 21 + func CreateReleaseDB(db *sqlx.DB, release *model.Release) error { 22 + _, err := db.Exec( 23 + "INSERT INTO musicrelease "+ 24 + "(id, visible, title, description, type, release_date, artwork, buyname, buylink) "+ 25 + "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", 26 + release.ID, 27 + release.Visible, 28 + release.Title, 29 + release.Description, 30 + release.ReleaseType, 31 + release.ReleaseDate.Format("2-Jan-2006"), 32 + release.Artwork, 33 + release.Buyname, 34 + release.Buylink, 35 + ) 36 + if err != nil { 37 + return err 38 + } 39 + 40 + return nil 41 + } 42 + 43 + func UpdateReleaseDB(db *sqlx.DB, release *model.Release) error { 44 + _, err := db.Exec( 45 + "UPDATE musicrelease SET "+ 46 + "title=$2, description=$3, type=$4, release_date=$5, artwork=$6, buyname=$7, buylink=$8) "+ 47 + "VALUES ($2, $3, $4, $5, $6, $7, $8) "+ 48 + "WHERE id=$1", 49 + release.ID, 50 + release.Title, 51 + release.Description, 52 + release.ReleaseType, 53 + release.ReleaseDate.Format("2-Jan-2006"), 54 + release.Artwork, 55 + release.Buyname, 56 + release.Buylink, 57 + ) 58 + if err != nil { 59 + return err 60 + } 61 + 62 + return nil 63 + } 64 + 65 + func DeleteReleaseDB(db *sqlx.DB, release model.Release) error { 66 + _, err := db.Exec( 67 + "DELETE FROM musicrelease "+ 68 + "WHERE id=$1", 69 + release.ID, 70 + ) 71 + if err != nil { 72 + return err 73 + } 74 + 75 + return nil 76 + }
+69
music/controller/track.go
··· 1 + package music 2 + 3 + import ( 4 + "arimelody.me/arimelody.me/music/model" 5 + "github.com/jmoiron/sqlx" 6 + ) 7 + 8 + // DATABASE 9 + 10 + func PullAllTracks(db *sqlx.DB) ([]model.Track, error) { 11 + var tracks = []model.Track{} 12 + 13 + err := db.Select(&tracks, "SELECT id, title, description, lyrics, preview_url FROM musictrack RETURNING id") 14 + if err != nil { 15 + return nil, err 16 + } 17 + 18 + return tracks, nil 19 + } 20 + 21 + func CreateTrackDB(db *sqlx.DB, track *model.Track) (string, error) { 22 + var trackID string 23 + err := db.QueryRow( 24 + "INSERT INTO musictrack (title, description, lyrics, preview_url) "+ 25 + "VALUES ($1, $2, $3, $4) "+ 26 + "RETURNING id", 27 + track.Title, 28 + track.Description, 29 + track.Lyrics, 30 + track.PreviewURL, 31 + ).Scan(&trackID) 32 + if err != nil { 33 + return "", err 34 + } 35 + 36 + return trackID, nil 37 + } 38 + 39 + func UpdateTrackDB(db *sqlx.DB, track *model.Track) error { 40 + _, err := db.Exec( 41 + "UPDATE musictrack "+ 42 + "SET title=$2, description=$3, lyrics=$4, preview_url=$5 "+ 43 + "WHERE id=$1"+ 44 + "RETURNING id", 45 + track.ID, 46 + track.Title, 47 + track.Description, 48 + track.Lyrics, 49 + track.PreviewURL, 50 + ) 51 + if err != nil { 52 + return err 53 + } 54 + 55 + return nil 56 + } 57 + 58 + func DeleteTrackDB(db *sqlx.DB, trackID string) error { 59 + _, err := db.Exec( 60 + "DELETE FROM musictrack "+ 61 + "WHERE id=$1", 62 + trackID, 63 + ) 64 + if err != nil { 65 + return err 66 + } 67 + 68 + return nil 69 + }
-92
music/credit.go
··· 1 - package music 2 - 3 - import ( 4 - "fmt" 5 - 6 - "github.com/jmoiron/sqlx" 7 - ) 8 - 9 - type ( 10 - Credit struct { 11 - Artist *Artist `json:"artist"` 12 - Role string `json:"role"` 13 - Primary bool `json:"primary"` 14 - } 15 - 16 - PostCreditBody struct { 17 - Artist string `json:"artist"` 18 - Role string `json:"role"` 19 - Primary bool `json:"primary"` 20 - } 21 - ) 22 - 23 - // GETTERS 24 - 25 - func (credit Credit) GetArtist() Artist { 26 - return *credit.Artist 27 - } 28 - 29 - func (credit Credit) GetRole() string { 30 - return credit.Role 31 - } 32 - 33 - func (credit Credit) IsPrimary() bool { 34 - return credit.Primary 35 - } 36 - 37 - // SETTERS 38 - 39 - func (credit Credit) SetArtist(artist *Artist) error { 40 - // TODO: update DB 41 - credit.Artist = artist 42 - return nil 43 - } 44 - 45 - func (credit Credit) SetRole(role string) error { 46 - // TODO: update DB 47 - credit.Role = role 48 - return nil 49 - } 50 - 51 - func (credit Credit) SetPrimary(primary bool) error { 52 - // TODO: update DB 53 - credit.Primary = primary 54 - return nil 55 - } 56 - 57 - // DATABASE 58 - 59 - func PullReleaseCredits(db *sqlx.DB, releaseID string) ([]Credit, error) { 60 - var credits = []Credit{} 61 - 62 - credit_rows, err := db.Query("SELECT artist, role, is_primary FROM musiccredits WHERE release=$1", releaseID) 63 - if err != nil { 64 - return []Credit{}, err 65 - } 66 - 67 - for credit_rows.Next() { 68 - var artistID string 69 - var credit = Credit{} 70 - err = credit_rows.Scan( 71 - &artistID, 72 - &credit.Role, 73 - &credit.Primary, 74 - ) 75 - if err != nil { 76 - fmt.Printf("Error while pulling credit for release %s: %s\n", releaseID, err) 77 - continue 78 - } 79 - 80 - credit.Artist = GetArtist(artistID) 81 - if credit.Artist == nil { 82 - // this should absolutely not happen ever due to foreign key 83 - // constraints, but it doesn't hurt to be sure! 84 - fmt.Printf("Error while pulling credit for release %s: Artist %s does not exist\n", releaseID, artistID) 85 - continue 86 - } 87 - 88 - credits = append(credits, credit) 89 - } 90 - 91 - return credits, nil 92 - }
-73
music/link.go
··· 1 - package music 2 - 3 - import ( 4 - "fmt" 5 - "regexp" 6 - "strings" 7 - 8 - "github.com/jmoiron/sqlx" 9 - ) 10 - 11 - type Link struct { 12 - Name string `json:"name"` 13 - URL string `json:"url"` 14 - } 15 - 16 - // GETTERS 17 - 18 - func (link Link) GetName() string { 19 - return link.Name 20 - } 21 - 22 - func (link Link) GetURL() string { 23 - return link.URL 24 - } 25 - 26 - // SETTERS 27 - 28 - func (link Link) SetName(name string) error { 29 - // TODO: update DB 30 - link.Name = name 31 - return nil 32 - } 33 - 34 - func (link Link) SetURL(url string) error { 35 - // TODO: update DB 36 - link.URL = url 37 - return nil 38 - } 39 - 40 - // MISC 41 - 42 - func (link Link) NormaliseName() string { 43 - rgx := regexp.MustCompile(`[^a-z0-9]`) 44 - return strings.ToLower(rgx.ReplaceAllString(link.Name, "")) 45 - } 46 - 47 - // DATABASE 48 - 49 - func PullReleaseLinks(db *sqlx.DB, releaseID string) ([]Link, error) { 50 - var links = []Link{} 51 - 52 - link_rows, err := db.Query("SELECT name, url FROM musiclinks WHERE release=$1", releaseID); 53 - if err != nil { 54 - return []Link{}, err 55 - } 56 - 57 - for link_rows.Next() { 58 - var link = Link{} 59 - 60 - err = link_rows.Scan( 61 - &link.Name, 62 - &link.URL, 63 - ) 64 - if err != nil { 65 - fmt.Printf("Error while pulling link for release %s: %s\n", releaseID, err) 66 - continue 67 - } 68 - 69 - links = append(links, link) 70 - } 71 - 72 - return links, nil 73 - }
+17
music/model/artist.go
··· 1 + package model 2 + 3 + type ( 4 + Artist struct { 5 + ID string `json:"id"` 6 + Name string `json:"name"` 7 + Website string `json:"website"` 8 + Avatar string `json:"avatar"` 9 + } 10 + ) 11 + 12 + func (artist Artist) GetAvatar() string { 13 + if artist.Avatar == "" { 14 + return "/img/default-avatar.png" 15 + } 16 + return artist.Avatar 17 + }
+7
music/model/credit.go
··· 1 + package model 2 + 3 + type Credit struct { 4 + Artist *Artist `json:"artist"` 5 + Role string `json:"role"` 6 + Primary bool `json:"primary" db:"is_primary"` 7 + }
+16
music/model/link.go
··· 1 + package model 2 + 3 + import ( 4 + "regexp" 5 + "strings" 6 + ) 7 + 8 + type Link struct { 9 + Name string `json:"name"` 10 + URL string `json:"url"` 11 + } 12 + 13 + func (link Link) NormaliseName() string { 14 + rgx := regexp.MustCompile(`[^a-z0-9]`) 15 + return strings.ToLower(rgx.ReplaceAllString(link.Name, "")) 16 + }
+109
music/model/release.go
··· 1 + package model 2 + 3 + import ( 4 + "strings" 5 + "time" 6 + ) 7 + 8 + type ( 9 + ReleaseType string 10 + Release struct { 11 + ID string `json:"id"` 12 + Visible bool `json:"visible"` 13 + Title string `json:"title"` 14 + Description string `json:"description"` 15 + ReleaseType ReleaseType `json:"type" db:"type"` 16 + ReleaseDate time.Time `json:"releaseDate" db:"release_date"` 17 + Artwork string `json:"artwork"` 18 + Buyname string `json:"buyname"` 19 + Buylink string `json:"buylink"` 20 + Links []Link `json:"links"` 21 + Credits []Credit `json:"credits"` 22 + Tracks []Track `json:"tracks"` 23 + } 24 + ) 25 + 26 + const ( 27 + Single ReleaseType = "Single" 28 + Album ReleaseType = "Album" 29 + EP ReleaseType = "EP" 30 + Compilation ReleaseType = "Compilation" 31 + ) 32 + 33 + // GETTERS 34 + 35 + func (release Release) GetArtwork() string { 36 + if release.Artwork == "" { 37 + return "/img/default-cover-art.png" 38 + } 39 + return release.Artwork 40 + } 41 + 42 + func (release Release) PrintReleaseDate() string { 43 + return release.ReleaseDate.Format("02 January 2006") 44 + } 45 + 46 + func (release Release) GetReleaseYear() int { 47 + return release.ReleaseDate.Year() 48 + } 49 + 50 + func (release Release) IsSingle() bool { 51 + return len(release.Tracks) == 1; 52 + } 53 + 54 + func (release Release) IsReleased() bool { 55 + return release.ReleaseDate.Before(time.Now()) 56 + } 57 + 58 + func (release Release) GetUniqueArtists(only_primary bool) []Artist { 59 + var artists = []Artist{} 60 + 61 + for _, credit := range release.Credits { 62 + if only_primary && !credit.Primary { 63 + continue 64 + } 65 + 66 + exists := false 67 + for _, a := range artists { 68 + if a.ID == credit.Artist.ID { 69 + exists = true 70 + break 71 + } 72 + } 73 + 74 + if exists { 75 + continue 76 + } 77 + 78 + artists = append(artists, *credit.Artist) 79 + } 80 + 81 + return artists 82 + } 83 + 84 + func (release Release) GetUniqueArtistNames(only_primary bool) []string { 85 + var names = []string{} 86 + for _, artist := range release.GetUniqueArtists(only_primary) { 87 + names = append(names, artist.Name) 88 + } 89 + 90 + return names 91 + } 92 + 93 + func (release Release) PrintArtists(only_primary bool, ampersand bool) string { 94 + names := release.GetUniqueArtistNames(only_primary) 95 + 96 + if len(names) == 0 { 97 + return "Unknown Artist" 98 + } else if len(names) == 1 { 99 + return names[0] 100 + } 101 + 102 + if ampersand { 103 + res := strings.Join(names[:len(names)-1], ", ") 104 + res += " & " + names[len(names)-1] 105 + return res 106 + } else { 107 + return strings.Join(names[:], ", ") 108 + } 109 + }
+9
music/model/track.go
··· 1 + package model 2 + 3 + type Track struct { 4 + ID string `json:"id"` 5 + Title string `json:"title"` 6 + Description string `json:"description"` 7 + Lyrics string `json:"lyrics"` 8 + PreviewURL string `json:"previewURL" db:"preview_url"` 9 + }
+22 -41
music/music.go music/view/music.go
··· 1 - package music 1 + package view 2 2 3 3 import ( 4 - "fmt" 5 4 "net/http" 6 - "os" 7 - "path/filepath" 8 - "strings" 9 5 10 6 "arimelody.me/arimelody.me/admin" 11 7 "arimelody.me/arimelody.me/global" 8 + "arimelody.me/arimelody.me/music/model" 12 9 ) 13 10 14 11 // HTTP HANDLER METHODS 15 12 13 + func Handler() http.Handler { 14 + mux := http.NewServeMux() 15 + 16 + mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 17 + if r.URL.Path == "/" { 18 + ServeCatalog().ServeHTTP(w, r) 19 + return 20 + } 21 + ServeGateway().ServeHTTP(w, r) 22 + })) 23 + 24 + return mux 25 + } 26 + 16 27 func ServeCatalog() http.Handler { 17 28 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 18 - releases := []Release{} 19 - authorised := r.Context().Value("role") != nil && r.Context().Value("role") == "admin" 20 - for _, release := range Releases { 29 + releases := []model.Release{} 30 + authorised := admin.GetSession(r) != nil 31 + for _, release := range global.Releases { 21 32 if !release.IsReleased() && !authorised { 22 33 continue 23 34 } 24 35 releases = append(releases, release) 25 36 } 26 37 27 - global.ServeTemplate("music.html", Releases).ServeHTTP(w, r) 38 + global.ServeTemplate("music.html", releases).ServeHTTP(w, r) 28 39 }) 29 40 } 30 41 31 - func ServeGateway() http.Handler { 32 - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 33 - if r.URL.Path == "/" { 34 - http.Redirect(w, r, "/music", http.StatusPermanentRedirect) 35 - return 36 - } 37 - 38 - id := r.URL.Path[1:] 39 - release := GetRelease(id) 40 - if release == nil { 41 - http.NotFound(w, r) 42 - return 43 - } 44 - 45 - // only allow authorised users to view unreleased releases 46 - authorised := r.Context().Value("role") != nil && r.Context().Value("role") == "admin" 47 - if !release.IsReleased() && !authorised { 48 - admin.MustAuthorise(ServeGateway()).ServeHTTP(w, r) 49 - return 50 - } 51 - 52 - lrw := global.LoggingResponseWriter{w, http.StatusOK} 53 - 54 - global.ServeTemplate("music-gateway.html", release).ServeHTTP(&lrw, r) 55 - 56 - if lrw.Code != http.StatusOK { 57 - fmt.Printf("Error loading music gateway for %s\n", id) 58 - return 59 - } 60 - }) 61 - } 62 - 42 + /* 63 43 func ServeArtwork() http.Handler { 64 44 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 65 45 if r.URL.Path == "/" { ··· 111 91 w.Write(bytes) 112 92 }) 113 93 } 94 + */
-492
music/release.go
··· 1 - package music 2 - 3 - import ( 4 - "encoding/json" 5 - "errors" 6 - "fmt" 7 - "net/http" 8 - "strings" 9 - "time" 10 - 11 - "arimelody.me/arimelody.me/admin" 12 - "github.com/jmoiron/sqlx" 13 - ) 14 - 15 - type ReleaseType string 16 - 17 - const ( 18 - Single ReleaseType = "Single" 19 - Album ReleaseType = "Album" 20 - EP ReleaseType = "EP" 21 - Compilation ReleaseType = "Compilation" 22 - ) 23 - 24 - type ( 25 - Release struct { 26 - ID string `json:"id"` 27 - Title string `json:"title"` 28 - Description string `json:"description"` 29 - ReleaseType ReleaseType `json:"type"` 30 - ReleaseDate time.Time `json:"releaseDate"` 31 - Artwork string `json:"artwork"` 32 - Buyname string `json:"buyname"` 33 - Buylink string `json:"buylink"` 34 - Links []Link `json:"links"` 35 - Credits []Credit `json:"credits"` 36 - Tracks []Track `json:"tracks"` 37 - } 38 - 39 - PostReleaseBody struct { 40 - ID string `json:"id"` 41 - Title string `json:"title"` 42 - Description string `json:"description"` 43 - ReleaseType ReleaseType `json:"type"` 44 - ReleaseDate time.Time `json:"releaseDate"` 45 - Artwork string `json:"artwork"` 46 - Buyname string `json:"buyname"` 47 - Buylink string `json:"buylink"` 48 - Links []Link `json:"links"` 49 - Credits []PostCreditBody `json:"credits"` 50 - Tracks []Track `json:"tracks"` 51 - } 52 - ) 53 - 54 - var Releases []Release; 55 - 56 - // GETTERS 57 - 58 - func (release Release) GetID() string { 59 - return release.ID 60 - } 61 - 62 - func (release Release) GetTitle() string { 63 - return release.Title 64 - } 65 - 66 - func (release Release) GetDescription() string { 67 - return release.Description 68 - } 69 - 70 - func (release Release) GetType() ReleaseType { 71 - return release.ReleaseType 72 - } 73 - 74 - func (release Release) GetReleaseDate() time.Time { 75 - return release.ReleaseDate 76 - } 77 - 78 - func (release Release) GetArtwork() string { 79 - if release.Artwork == "" { 80 - return "/img/default-cover-art.png" 81 - } 82 - return release.Artwork 83 - } 84 - 85 - func (release Release) GetBuyName() string { 86 - return release.Buyname 87 - } 88 - 89 - func (release Release) GetBuyLink() string { 90 - return release.Buylink 91 - } 92 - 93 - func (release Release) GetLinks() []Link { 94 - return release.Links 95 - } 96 - 97 - func (release Release) GetCredits() []Credit { 98 - return release.Credits 99 - } 100 - 101 - func (release Release) GetTracks() []Track { 102 - return release.Tracks 103 - } 104 - 105 - // SETTERS 106 - 107 - func (release Release) SetID(id string) error { 108 - // TODO: update DB 109 - release.ID = id 110 - return nil 111 - } 112 - 113 - func (release Release) SetTitle(title string) error { 114 - // TODO: update DB 115 - release.Title = title 116 - return nil 117 - } 118 - 119 - func (release Release) SetDescription(description string) error { 120 - // TODO: update DB 121 - release.Description = description 122 - return nil 123 - } 124 - 125 - func (release Release) SetType(releaseType ReleaseType) error { 126 - // TODO: update DB 127 - release.ReleaseType = releaseType 128 - return nil 129 - } 130 - 131 - func (release Release) SetReleaseDate(releaseDate time.Time) error { 132 - // TODO: update DB 133 - release.ReleaseDate = releaseDate 134 - return nil 135 - } 136 - 137 - func (release Release) SetArtwork(artwork string) error { 138 - // TODO: update DB 139 - release.Artwork = artwork 140 - return nil 141 - } 142 - 143 - func (release Release) SetBuyName(buyname string) error { 144 - // TODO: update DB 145 - release.Buyname = buyname 146 - return nil 147 - } 148 - 149 - func (release Release) SetBuyLink(buylink string) error { 150 - // TODO: update DB 151 - release.Buylink = buylink 152 - return nil 153 - } 154 - 155 - func (release Release) SetLinks(links []Link) error { 156 - // TODO: update DB 157 - release.Links = links 158 - return nil 159 - } 160 - 161 - func (release Release) SetCredits(credits []Credit) error { 162 - // TODO: update DB 163 - release.Credits = credits 164 - return nil 165 - } 166 - 167 - func (release Release) SetTracks(tracks []Track) error { 168 - // TODO: update DB 169 - release.Tracks = tracks 170 - return nil 171 - } 172 - 173 - // MISC 174 - 175 - func GetRelease(id string) *Release { 176 - for _, release := range Releases { 177 - if release.GetID() == id { 178 - return &release 179 - } 180 - } 181 - return nil 182 - } 183 - 184 - func (release Release) PrintReleaseDate() string { 185 - return release.ReleaseDate.Format("02 January 2006") 186 - } 187 - 188 - func (release Release) GetReleaseYear() int { 189 - return release.ReleaseDate.Year() 190 - } 191 - 192 - func (release Release) IsSingle() bool { 193 - return len(release.Tracks) == 1; 194 - } 195 - 196 - func (release Release) IsReleased() bool { 197 - return release.ReleaseDate.Before(time.Now()) 198 - } 199 - 200 - func (release Release) GetUniqueArtists(only_primary bool) []Artist { 201 - var artists = []Artist{} 202 - 203 - for _, credit := range release.Credits { 204 - if only_primary && !credit.Primary { 205 - continue 206 - } 207 - 208 - exists := false 209 - for _, a := range artists { 210 - if a.ID == credit.Artist.ID { 211 - exists = true 212 - break 213 - } 214 - } 215 - 216 - if exists { 217 - continue 218 - } 219 - 220 - artists = append(artists, *credit.Artist) 221 - } 222 - 223 - return artists 224 - } 225 - 226 - func (release Release) GetUniqueArtistNames(only_primary bool) []string { 227 - var names = []string{} 228 - for _, artist := range release.GetUniqueArtists(only_primary) { 229 - names = append(names, artist.GetName()) 230 - } 231 - 232 - return names 233 - } 234 - 235 - func (release Release) PrintArtists(only_primary bool, ampersand bool) string { 236 - names := release.GetUniqueArtistNames(only_primary) 237 - 238 - if len(names) == 0 { 239 - return "Unknown Artist" 240 - } else if len(names) == 1 { 241 - return names[0] 242 - } 243 - 244 - if ampersand { 245 - res := strings.Join(names[:len(names)-1], ", ") 246 - res += " & " + names[len(names)-1] 247 - return res 248 - } else { 249 - return strings.Join(names[:], ", ") 250 - } 251 - } 252 - 253 - // DATABASE 254 - 255 - func (release Release) PushToDB(db *sqlx.DB) error { 256 - // fmt.Printf("Pushing release [%s] to database...", release.ID) 257 - 258 - tx, err := db.Begin() 259 - if err != nil { 260 - return errors.New(fmt.Sprintf("Failed to initiate transaction: %s", err)) 261 - } 262 - 263 - _, err = tx.Exec( 264 - "INSERT INTO musicreleases (id, title, description, type, release_date, artwork, buyname, buylink) "+ 265 - "VALUES ($1, $2, $3, $4, $5, $6, $7, $8) "+ 266 - "ON CONFLICT (id) "+ 267 - "DO UPDATE SET title=$2, description=$3, type=$4, release_date=$5, artwork=$6, buyname=$7, buylink=$8", 268 - release.ID, 269 - release.Title, 270 - release.Description, 271 - release.ReleaseType, 272 - release.ReleaseDate.Format("2-Jan-2006"), 273 - release.Artwork, 274 - release.Buyname, 275 - release.Buylink, 276 - ) 277 - 278 - for _, link := range release.Links { 279 - _, err = tx.Exec( 280 - "INSERT INTO musiclinks (release, name, url) "+ 281 - "VALUES ($1, $2, $3) "+ 282 - "ON CONFLICT (release, name) "+ 283 - "DO UPDATE SET url=$3 ", 284 - release.ID, 285 - link.Name, 286 - link.URL, 287 - ) 288 - if err != nil { 289 - return errors.New(fmt.Sprintf("Failed to add music link to transaction: %s", err)) 290 - } 291 - } 292 - for _, credit := range release.Credits { 293 - _, err = tx.Exec( 294 - "INSERT INTO musiccredits (release, artist, role, is_primary) "+ 295 - "VALUES ($1, $2, $3, $4) "+ 296 - "ON CONFLICT (release, artist) "+ 297 - "DO UPDATE SET role=$3, is_primary=$4", 298 - release.ID, 299 - credit.Artist.ID, 300 - credit.Role, 301 - credit.Primary, 302 - ) 303 - if err != nil { 304 - return errors.New(fmt.Sprintf("Failed to add music credit to transaction: %s", err)) 305 - } 306 - } 307 - for _, track := range release.Tracks { 308 - _, err = tx.Exec( 309 - "INSERT INTO musictracks (release, number, title, description, lyrics, preview_url) "+ 310 - "VALUES ($1, $2, $3, $4, $5, $6) "+ 311 - "ON CONFLICT (release, number) "+ 312 - "DO UPDATE SET title=$3, description=$4, lyrics=$5, preview_url=$6", 313 - release.ID, 314 - track.Number, 315 - track.Title, 316 - track.Description, 317 - track.Lyrics, 318 - track.PreviewURL, 319 - ) 320 - if err != nil { 321 - return errors.New(fmt.Sprintf("Failed to add music track to transaction: %s", err)) 322 - } 323 - } 324 - 325 - err = tx.Commit() 326 - if err != nil { 327 - return errors.New(fmt.Sprintf("Failed to commit transaction: %s", err)) 328 - } 329 - 330 - // fmt.Printf("done!\n") 331 - 332 - return nil 333 - } 334 - 335 - func (release Release) DeleteFromDB(db *sqlx.DB) error { 336 - // this probably doesn't need to be a transaction; 337 - // i just felt like making it one 338 - tx, err := db.Begin() 339 - if err != nil { 340 - return errors.New(fmt.Sprintf("Failed to initiate transaction: %s", err)) 341 - } 342 - 343 - _, err = tx.Exec("DELETE FROM musicreleases WHERE id=$1", release.ID) 344 - 345 - err = tx.Commit() 346 - if err != nil { 347 - return errors.New(fmt.Sprintf("Failed to commit transaction: %s", err)) 348 - } 349 - 350 - return nil 351 - } 352 - 353 - func PullAllReleases(db *sqlx.DB) ([]Release, error) { 354 - releases := []Release{} 355 - 356 - rows, err := db.Query("SELECT id, title, description, type, release_date, artwork, buyname, buylink FROM musicreleases ORDER BY release_date DESC") 357 - if err != nil { 358 - return nil, err 359 - } 360 - 361 - for rows.Next() { 362 - var release = Release{} 363 - 364 - err = rows.Scan( 365 - &release.ID, 366 - &release.Title, 367 - &release.Description, 368 - &release.ReleaseType, 369 - &release.ReleaseDate, 370 - &release.Artwork, 371 - &release.Buyname, 372 - &release.Buylink, 373 - ) 374 - if err != nil { 375 - fmt.Printf("Error while pulling a release: %s\n", err) 376 - continue 377 - } 378 - 379 - release.Credits, err = PullReleaseCredits(db, release.ID) 380 - if err != nil { 381 - fmt.Printf("Failed to pull credits for %s: %v\n", release.ID, err) 382 - } 383 - 384 - release.Links, err = PullReleaseLinks(db, release.ID) 385 - if err != nil { 386 - fmt.Printf("Failed to pull links for %s: %v\n", release.ID, err) 387 - } 388 - 389 - release.Tracks, err = PullReleaseTracks(db, release.ID) 390 - if err != nil { 391 - return nil, errors.New(fmt.Sprintf("error pulling tracks for %s: %v\n", release.ID, err)) 392 - } 393 - 394 - releases = append(releases, release) 395 - } 396 - 397 - return releases, nil 398 - } 399 - 400 - // HTTP HANDLERS 401 - 402 - func ServeRelease() http.Handler { 403 - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 404 - if r.URL.Path == "/" { 405 - http.NotFound(w, r) 406 - return 407 - } 408 - 409 - releaseID := r.URL.Path[1:] 410 - var release = GetRelease(releaseID) 411 - if release == nil { 412 - http.NotFound(w, r) 413 - return 414 - } 415 - 416 - // only allow authorised users to view unreleased releases 417 - authorised := r.Context().Value("role") != nil && r.Context().Value("role") == "admin" 418 - if !release.IsReleased() && !authorised { 419 - admin.MustAuthorise(ServeRelease()).ServeHTTP(w, r) 420 - return 421 - } 422 - 423 - jsonBytes, err := json.Marshal(release) 424 - if err != nil { 425 - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 426 - return 427 - } 428 - 429 - w.Header().Add("Content-Type", "application/json") 430 - w.WriteHeader(http.StatusOK) 431 - w.Write(jsonBytes) 432 - }) 433 - } 434 - 435 - func PostRelease() http.Handler { 436 - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 437 - if r.Method != http.MethodPost { 438 - http.NotFound(w, r) 439 - return 440 - } 441 - 442 - var data PostReleaseBody 443 - err := json.NewDecoder(r.Body).Decode(&data) 444 - if err != nil { 445 - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 446 - return 447 - } 448 - 449 - if GetRelease(data.ID) != nil { 450 - http.Error(w, fmt.Sprintf("Release %s already exists", data.ID), http.StatusBadRequest) 451 - return 452 - } 453 - 454 - var credits = []Credit{} 455 - 456 - for _, credit := range data.Credits { 457 - var artist = GetArtist(credit.Artist) 458 - 459 - if artist == nil { 460 - http.Error(w, fmt.Sprintf("Artist %s does not exist", credit.Artist), http.StatusBadRequest) 461 - return 462 - } 463 - 464 - credits = append(credits, Credit{ 465 - Artist: artist, 466 - Role: credit.Role, 467 - Primary: credit.Primary, 468 - }) 469 - } 470 - 471 - var release = Release{ 472 - ID: data.ID, 473 - Title: data.Title, 474 - Description: data.Description, 475 - ReleaseType: data.ReleaseType, 476 - ReleaseDate: data.ReleaseDate, 477 - Artwork: data.Artwork, 478 - Buyname: data.Buyname, 479 - Buylink: data.Buylink, 480 - Links: data.Links, 481 - Credits: credits, 482 - Tracks: data.Tracks, 483 - } 484 - 485 - Releases = append([]Release{release}, Releases...) 486 - 487 - jsonBytes, err := json.Marshal(release) 488 - w.Header().Add("Content-Type", "application/json") 489 - w.WriteHeader(http.StatusCreated) 490 - w.Write(jsonBytes) 491 - }) 492 - }
-100
music/track.go
··· 1 - package music 2 - 3 - import ( 4 - "fmt" 5 - 6 - "github.com/jmoiron/sqlx" 7 - ) 8 - 9 - type Track struct { 10 - Number int `json:"number"` 11 - Title string `json:"title"` 12 - Description string `json:"description"` 13 - Lyrics string `json:"lyrics"` 14 - PreviewURL string `json:"previewURL"` 15 - } 16 - 17 - // GETTERS 18 - 19 - func (track Track) GetNumber() int { 20 - return track.Number 21 - } 22 - 23 - func (track Track) GetTitle() string { 24 - return track.Title 25 - } 26 - 27 - func (track Track) GetDescription() string { 28 - return track.Description 29 - } 30 - 31 - func (track Track) GetLyrics() string { 32 - return track.Lyrics 33 - } 34 - 35 - func (track Track) GetPreviewURL() string { 36 - return track.PreviewURL 37 - } 38 - 39 - // SETTERS 40 - 41 - func (track Track) SetNumber(number int) error { 42 - // TODO: update DB 43 - track.Number = number 44 - return nil 45 - } 46 - 47 - func (track Track) SetTitle(title string) error { 48 - // TODO: update DB 49 - track.Title = title 50 - return nil 51 - } 52 - 53 - func (track Track) SetDescription(description string) error { 54 - // TODO: update DB 55 - track.Description = description 56 - return nil 57 - } 58 - 59 - func (track Track) SetLyrics(lyrics string) error { 60 - // TODO: update DB 61 - track.Lyrics = lyrics 62 - return nil 63 - } 64 - 65 - func (track Track) SetPreviewURL(previewURL string) error { 66 - // TODO: update DB 67 - track.PreviewURL = previewURL 68 - return nil 69 - } 70 - 71 - // DATABASE 72 - 73 - func PullReleaseTracks(db *sqlx.DB, releaseID string) ([]Track, error) { 74 - var tracks = []Track{} 75 - 76 - track_rows, err := db.Query("SELECT number, title, description, lyrics, preview_url FROM musictracks WHERE release=$1", releaseID) 77 - if err != nil { 78 - return []Track{}, err 79 - } 80 - 81 - for track_rows.Next() { 82 - var track = Track{} 83 - 84 - err = track_rows.Scan( 85 - &track.Number, 86 - &track.Title, 87 - &track.Description, 88 - &track.Lyrics, 89 - &track.PreviewURL, 90 - ) 91 - if err != nil { 92 - fmt.Printf("Error while pulling track for release %s: %s\n", releaseID, err) 93 - continue 94 - } 95 - 96 - tracks = append(tracks, track) 97 - } 98 - 99 - return tracks, nil 100 - }
+74
music/view/release.go
··· 1 + package view 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + "net/http" 7 + 8 + "arimelody.me/arimelody.me/admin" 9 + "arimelody.me/arimelody.me/global" 10 + ) 11 + 12 + // HTTP HANDLERS 13 + 14 + func ServeRelease() http.Handler { 15 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 16 + if r.URL.Path == "/" { 17 + http.NotFound(w, r) 18 + return 19 + } 20 + 21 + releaseID := r.URL.Path[1:] 22 + var release = global.GetRelease(releaseID) 23 + if release == nil { 24 + http.NotFound(w, r) 25 + return 26 + } 27 + 28 + // only allow authorised users to view unreleased releases 29 + authorised := admin.GetSession(r) != nil 30 + if !release.IsReleased() && !authorised { 31 + admin.MustAuthorise(ServeRelease()).ServeHTTP(w, r) 32 + return 33 + } 34 + 35 + w.Header().Add("Content-Type", "application/json") 36 + err := json.NewEncoder(w).Encode(release) 37 + if err != nil { 38 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 39 + return 40 + } 41 + }) 42 + } 43 + 44 + func ServeGateway() http.Handler { 45 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 46 + if r.URL.Path == "/" { 47 + http.Redirect(w, r, "/music", http.StatusPermanentRedirect) 48 + return 49 + } 50 + 51 + id := r.URL.Path[1:] 52 + release := global.GetRelease(id) 53 + if release == nil { 54 + http.NotFound(w, r) 55 + return 56 + } 57 + 58 + // only allow authorised users to view unreleased releases 59 + authorised := admin.GetSession(r) != nil 60 + if !release.IsReleased() && !authorised { 61 + admin.MustAuthorise(ServeGateway()).ServeHTTP(w, r) 62 + return 63 + } 64 + 65 + lrw := global.LoggingResponseWriter{w, http.StatusOK} 66 + 67 + global.ServeTemplate("music-gateway.html", release).ServeHTTP(&lrw, r) 68 + 69 + if lrw.Code != http.StatusOK { 70 + fmt.Printf("Error rendering music gateway for %s\n", id) 71 + return 72 + } 73 + }) 74 + }
public/font/inter/Inter-Black.woff2

This is a binary file and will not be displayed.

public/font/inter/Inter-BlackItalic.woff2

This is a binary file and will not be displayed.

public/font/inter/Inter-Bold.woff2

This is a binary file and will not be displayed.

public/font/inter/Inter-BoldItalic.woff2

This is a binary file and will not be displayed.

public/font/inter/Inter-ExtraBold.woff2

This is a binary file and will not be displayed.

public/font/inter/Inter-ExtraBoldItalic.woff2

This is a binary file and will not be displayed.

public/font/inter/Inter-ExtraLight.woff2

This is a binary file and will not be displayed.

public/font/inter/Inter-ExtraLightItalic.woff2

This is a binary file and will not be displayed.

public/font/inter/Inter-Italic.woff2

This is a binary file and will not be displayed.

public/font/inter/Inter-Light.woff2

This is a binary file and will not be displayed.

public/font/inter/Inter-LightItalic.woff2

This is a binary file and will not be displayed.

public/font/inter/Inter-Medium.woff2

This is a binary file and will not be displayed.

public/font/inter/Inter-MediumItalic.woff2

This is a binary file and will not be displayed.

public/font/inter/Inter-Regular.woff2

This is a binary file and will not be displayed.

public/font/inter/Inter-SemiBold.woff2

This is a binary file and will not be displayed.

public/font/inter/Inter-SemiBoldItalic.woff2

This is a binary file and will not be displayed.

public/font/inter/Inter-Thin.woff2

This is a binary file and will not be displayed.

public/font/inter/Inter-ThinItalic.woff2

This is a binary file and will not be displayed.

public/font/inter/InterDisplay-Black.woff2

This is a binary file and will not be displayed.

public/font/inter/InterDisplay-BlackItalic.woff2

This is a binary file and will not be displayed.

public/font/inter/InterDisplay-Bold.woff2

This is a binary file and will not be displayed.

public/font/inter/InterDisplay-BoldItalic.woff2

This is a binary file and will not be displayed.

public/font/inter/InterDisplay-ExtraBold.woff2

This is a binary file and will not be displayed.

public/font/inter/InterDisplay-ExtraBoldItalic.woff2

This is a binary file and will not be displayed.

public/font/inter/InterDisplay-ExtraLight.woff2

This is a binary file and will not be displayed.

public/font/inter/InterDisplay-ExtraLightItalic.woff2

This is a binary file and will not be displayed.

public/font/inter/InterDisplay-Italic.woff2

This is a binary file and will not be displayed.

public/font/inter/InterDisplay-Light.woff2

This is a binary file and will not be displayed.

public/font/inter/InterDisplay-LightItalic.woff2

This is a binary file and will not be displayed.

public/font/inter/InterDisplay-Medium.woff2

This is a binary file and will not be displayed.

public/font/inter/InterDisplay-MediumItalic.woff2

This is a binary file and will not be displayed.

public/font/inter/InterDisplay-Regular.woff2

This is a binary file and will not be displayed.

public/font/inter/InterDisplay-SemiBold.woff2

This is a binary file and will not be displayed.

public/font/inter/InterDisplay-SemiBoldItalic.woff2

This is a binary file and will not be displayed.

public/font/inter/InterDisplay-Thin.woff2

This is a binary file and will not be displayed.

public/font/inter/InterDisplay-ThinItalic.woff2

This is a binary file and will not be displayed.

public/font/inter/InterVariable-Italic.woff2

This is a binary file and will not be displayed.

public/font/inter/InterVariable.woff2

This is a binary file and will not be displayed.

+92
public/font/inter/LICENSE.txt
··· 1 + Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter) 2 + 3 + This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 + This license is copied below, and is also available with a FAQ at: 5 + http://scripts.sil.org/OFL 6 + 7 + ----------------------------------------------------------- 8 + SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 9 + ----------------------------------------------------------- 10 + 11 + PREAMBLE 12 + The goals of the Open Font License (OFL) are to stimulate worldwide 13 + development of collaborative font projects, to support the font creation 14 + efforts of academic and linguistic communities, and to provide a free and 15 + open framework in which fonts may be shared and improved in partnership 16 + with others. 17 + 18 + The OFL allows the licensed fonts to be used, studied, modified and 19 + redistributed freely as long as they are not sold by themselves. The 20 + fonts, including any derivative works, can be bundled, embedded, 21 + redistributed and/or sold with any software provided that any reserved 22 + names are not used by derivative works. The fonts and derivatives, 23 + however, cannot be released under any other type of license. The 24 + requirement for fonts to remain under this license does not apply 25 + to any document created using the fonts or their derivatives. 26 + 27 + DEFINITIONS 28 + "Font Software" refers to the set of files released by the Copyright 29 + Holder(s) under this license and clearly marked as such. This may 30 + include source files, build scripts and documentation. 31 + 32 + "Reserved Font Name" refers to any names specified as such after the 33 + copyright statement(s). 34 + 35 + "Original Version" refers to the collection of Font Software components as 36 + distributed by the Copyright Holder(s). 37 + 38 + "Modified Version" refers to any derivative made by adding to, deleting, 39 + or substituting -- in part or in whole -- any of the components of the 40 + Original Version, by changing formats or by porting the Font Software to a 41 + new environment. 42 + 43 + "Author" refers to any designer, engineer, programmer, technical 44 + writer or other person who contributed to the Font Software. 45 + 46 + PERMISSION AND CONDITIONS 47 + Permission is hereby granted, free of charge, to any person obtaining 48 + a copy of the Font Software, to use, study, copy, merge, embed, modify, 49 + redistribute, and sell modified and unmodified copies of the Font 50 + Software, subject to the following conditions: 51 + 52 + 1) Neither the Font Software nor any of its individual components, 53 + in Original or Modified Versions, may be sold by itself. 54 + 55 + 2) Original or Modified Versions of the Font Software may be bundled, 56 + redistributed and/or sold with any software, provided that each copy 57 + contains the above copyright notice and this license. These can be 58 + included either as stand-alone text files, human-readable headers or 59 + in the appropriate machine-readable metadata fields within text or 60 + binary files as long as those fields can be easily viewed by the user. 61 + 62 + 3) No Modified Version of the Font Software may use the Reserved Font 63 + Name(s) unless explicit written permission is granted by the corresponding 64 + Copyright Holder. This restriction only applies to the primary font name as 65 + presented to the users. 66 + 67 + 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 68 + Software shall not be used to promote, endorse or advertise any 69 + Modified Version, except to acknowledge the contribution(s) of the 70 + Copyright Holder(s) and the Author(s) or with their explicit written 71 + permission. 72 + 73 + 5) The Font Software, modified or unmodified, in part or in whole, 74 + must be distributed entirely under this license, and must not be 75 + distributed under any other license. The requirement for fonts to 76 + remain under this license does not apply to any document created 77 + using the Font Software. 78 + 79 + TERMINATION 80 + This license becomes null and void if any of the above conditions are 81 + not met. 82 + 83 + DISCLAIMER 84 + THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 85 + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 86 + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 87 + OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 88 + COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 89 + INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 90 + DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 91 + FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 92 + OTHER DEALINGS IN THE FONT SOFTWARE.
+57
public/font/inter/inter.css
··· 1 + /* Variable fonts usage: 2 + :root { font-family: "Inter", sans-serif; } 3 + @supports (font-variation-settings: normal) { 4 + :root { font-family: "InterVariable", sans-serif; font-optical-sizing: auto; } 5 + } */ 6 + @font-face { 7 + font-family: InterVariable; 8 + font-style: normal; 9 + font-weight: 100 900; 10 + font-display: swap; 11 + src: url("InterVariable.woff2") format("woff2"); 12 + } 13 + @font-face { 14 + font-family: InterVariable; 15 + font-style: italic; 16 + font-weight: 100 900; 17 + font-display: swap; 18 + src: url("InterVariable-Italic.woff2") format("woff2"); 19 + } 20 + 21 + /* static fonts */ 22 + @font-face { font-family: "Inter"; font-style: normal; font-weight: 100; font-display: swap; src: url("Inter-Thin.woff2") format("woff2"); } 23 + @font-face { font-family: "Inter"; font-style: italic; font-weight: 100; font-display: swap; src: url("Inter-ThinItalic.woff2") format("woff2"); } 24 + @font-face { font-family: "Inter"; font-style: normal; font-weight: 200; font-display: swap; src: url("Inter-ExtraLight.woff2") format("woff2"); } 25 + @font-face { font-family: "Inter"; font-style: italic; font-weight: 200; font-display: swap; src: url("Inter-ExtraLightItalic.woff2") format("woff2"); } 26 + @font-face { font-family: "Inter"; font-style: normal; font-weight: 300; font-display: swap; src: url("Inter-Light.woff2") format("woff2"); } 27 + @font-face { font-family: "Inter"; font-style: italic; font-weight: 300; font-display: swap; src: url("Inter-LightItalic.woff2") format("woff2"); } 28 + @font-face { font-family: "Inter"; font-style: normal; font-weight: 400; font-display: swap; src: url("Inter-Regular.woff2") format("woff2"); } 29 + @font-face { font-family: "Inter"; font-style: italic; font-weight: 400; font-display: swap; src: url("Inter-Italic.woff2") format("woff2"); } 30 + @font-face { font-family: "Inter"; font-style: normal; font-weight: 500; font-display: swap; src: url("Inter-Medium.woff2") format("woff2"); } 31 + @font-face { font-family: "Inter"; font-style: italic; font-weight: 500; font-display: swap; src: url("Inter-MediumItalic.woff2") format("woff2"); } 32 + @font-face { font-family: "Inter"; font-style: normal; font-weight: 600; font-display: swap; src: url("Inter-SemiBold.woff2") format("woff2"); } 33 + @font-face { font-family: "Inter"; font-style: italic; font-weight: 600; font-display: swap; src: url("Inter-SemiBoldItalic.woff2") format("woff2"); } 34 + @font-face { font-family: "Inter"; font-style: normal; font-weight: 700; font-display: swap; src: url("Inter-Bold.woff2") format("woff2"); } 35 + @font-face { font-family: "Inter"; font-style: italic; font-weight: 700; font-display: swap; src: url("Inter-BoldItalic.woff2") format("woff2"); } 36 + @font-face { font-family: "Inter"; font-style: normal; font-weight: 800; font-display: swap; src: url("Inter-ExtraBold.woff2") format("woff2"); } 37 + @font-face { font-family: "Inter"; font-style: italic; font-weight: 800; font-display: swap; src: url("Inter-ExtraBoldItalic.woff2") format("woff2"); } 38 + @font-face { font-family: "Inter"; font-style: normal; font-weight: 900; font-display: swap; src: url("Inter-Black.woff2") format("woff2"); } 39 + @font-face { font-family: "Inter"; font-style: italic; font-weight: 900; font-display: swap; src: url("Inter-BlackItalic.woff2") format("woff2"); } 40 + @font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 100; font-display: swap; src: url("InterDisplay-Thin.woff2") format("woff2"); } 41 + @font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 100; font-display: swap; src: url("InterDisplay-ThinItalic.woff2") format("woff2"); } 42 + @font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 200; font-display: swap; src: url("InterDisplay-ExtraLight.woff2") format("woff2"); } 43 + @font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 200; font-display: swap; src: url("InterDisplay-ExtraLightItalic.woff2") format("woff2"); } 44 + @font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 300; font-display: swap; src: url("InterDisplay-Light.woff2") format("woff2"); } 45 + @font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 300; font-display: swap; src: url("InterDisplay-LightItalic.woff2") format("woff2"); } 46 + @font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 400; font-display: swap; src: url("InterDisplay-Regular.woff2") format("woff2"); } 47 + @font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 400; font-display: swap; src: url("InterDisplay-Italic.woff2") format("woff2"); } 48 + @font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 500; font-display: swap; src: url("InterDisplay-Medium.woff2") format("woff2"); } 49 + @font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 500; font-display: swap; src: url("InterDisplay-MediumItalic.woff2") format("woff2"); } 50 + @font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 600; font-display: swap; src: url("InterDisplay-SemiBold.woff2") format("woff2"); } 51 + @font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 600; font-display: swap; src: url("InterDisplay-SemiBoldItalic.woff2") format("woff2"); } 52 + @font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 700; font-display: swap; src: url("InterDisplay-Bold.woff2") format("woff2"); } 53 + @font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 700; font-display: swap; src: url("InterDisplay-BoldItalic.woff2") format("woff2"); } 54 + @font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 800; font-display: swap; src: url("InterDisplay-ExtraBold.woff2") format("woff2"); } 55 + @font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 800; font-display: swap; src: url("InterDisplay-ExtraBoldItalic.woff2") format("woff2"); } 56 + @font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 900; font-display: swap; src: url("InterDisplay-Black.woff2") format("woff2"); } 57 + @font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 900; font-display: swap; src: url("InterDisplay-BlackItalic.woff2") format("woff2"); }
-152
public/style/admin.css
··· 1 - @import url("/style/main.css"); 2 - 3 - main { 4 - width: min(calc(100% - 4rem), 720px); 5 - min-height: calc(100vh - 10.3rem); 6 - margin: 0 auto 2rem auto; 7 - padding-top: 4rem; 8 - } 9 - 10 - main h1 { 11 - line-height: 3rem; 12 - color: var(--primary); 13 - } 14 - 15 - main h2 { 16 - color: var(--secondary); 17 - } 18 - 19 - main h3 { 20 - color: var(--tertiary); 21 - } 22 - 23 - div#me_irl { 24 - width: fit-content; 25 - height: fit-content; 26 - border: 2px solid white; 27 - } 28 - 29 - div#me_irl img { 30 - display: block; 31 - } 32 - 33 - div#me_irl::before { 34 - content: ""; 35 - position: absolute; 36 - width: 104px; 37 - height: 104px; 38 - transform: translate(2px, 2px); 39 - background-image: linear-gradient(to top right, 40 - var(--primary), 41 - var(--secondary)); 42 - z-index: -1; 43 - } 44 - 45 - h1, 46 - h2, 47 - h3, 48 - h4, 49 - h5, 50 - h6, 51 - p, 52 - small, 53 - blockquote { 54 - transition: background-color 0.1s; 55 - } 56 - 57 - h1 a, 58 - h2 a, 59 - h3 a, 60 - h4 a, 61 - h5 a, 62 - h6 a { 63 - color: inherit; 64 - } 65 - 66 - h1 a:hover, 67 - h2 a:hover, 68 - h3 a:hover, 69 - h4 a:hover, 70 - h5 a:hover, 71 - h6 a:hover { 72 - text-decoration: none; 73 - } 74 - 75 - main h1:hover, 76 - main h2:hover, 77 - main h3:hover, 78 - main h4:hover, 79 - main h5:hover, 80 - main h6:hover, 81 - main p:hover, 82 - main small:hover, 83 - main blockquote:hover { 84 - background-color: #fff1; 85 - } 86 - 87 - blockquote { 88 - margin: 1rem 0; 89 - padding: 0 2.5rem; 90 - } 91 - 92 - hr { 93 - text-align: center; 94 - line-height: 0px; 95 - border-width: 1px 0 0 0; 96 - border-color: #888f; 97 - margin: 1.5em 0; 98 - overflow: visible; 99 - } 100 - 101 - ul.links { 102 - display: flex; 103 - gap: 1em .5em; 104 - flex-wrap: wrap; 105 - } 106 - 107 - ul.links li { 108 - list-style: none; 109 - } 110 - 111 - ul.links li a { 112 - padding: .2em .5em; 113 - border: 1px solid var(--links); 114 - color: var(--links); 115 - border-radius: 2px; 116 - background-color: transparent; 117 - transition-property: color, border-color, background-color; 118 - transition-duration: .2s; 119 - animation-delay: 0s; 120 - animation: list-item-fadein .2s forwards; 121 - opacity: 0; 122 - } 123 - 124 - ul.links li a:hover { 125 - color: #eee; 126 - border-color: #eee; 127 - background-color: var(--links) !important; 128 - text-decoration: none; 129 - box-shadow: 0 0 1em var(--links); 130 - } 131 - 132 - div#web-buttons { 133 - margin: 2rem 0; 134 - } 135 - 136 - #web-buttons a { 137 - text-decoration: none; 138 - } 139 - 140 - #web-buttons img { 141 - image-rendering: auto; 142 - image-rendering: crisp-edges; 143 - image-rendering: pixelated; 144 - } 145 - 146 - #web-buttons img:hover { 147 - margin: -1px; 148 - border: 1px solid #eee; 149 - transform: translate(-2px, -2px); 150 - box-shadow: 1px 1px 0 #eee, 2px 2px 0 #eee; 151 - } 152 -
+20 -19
schema.sql
··· 1 1 -- 2 2 -- Artists (should be applicable to all art) 3 3 -- 4 - CREATE TABLE artist ( 5 - id uuid DEFAULT gen_random_uuid(), 4 + CREATE TABLE public.artist ( 5 + id character varying(64) DEFAULT gen_random_uuid(), 6 6 name text NOT NULL, 7 7 website text, 8 8 avatar text 9 9 ); 10 - ALTER TABLE artist ADD CONSTRAINT artist_pk PRIMARY KEY (id); 10 + ALTER TABLE public.artist ADD CONSTRAINT artist_pk PRIMARY KEY (id); 11 11 12 12 -- 13 13 -- Music releases 14 14 -- 15 - CREATE TABLE musicrelease ( 15 + CREATE TABLE public.musicrelease ( 16 16 id character varying(64) NOT NULL, 17 + visible bool DEFAULT false, 17 18 title text NOT NULL, 18 19 description text, 19 20 type text, ··· 22 23 buyname text, 23 24 buylink text 24 25 ); 25 - ALTER TABLE musicrelease ADD CONSTRAINT musicrelease_pk PRIMARY KEY (id); 26 + ALTER TABLE public.musicrelease ADD CONSTRAINT musicrelease_pk PRIMARY KEY (id); 26 27 27 28 -- 28 29 -- Music links (external platform links under a release) 29 30 -- 30 - CREATE TABLE musiclink ( 31 + CREATE TABLE public.musiclink ( 31 32 release character varying(64) NOT NULL, 32 33 name text NOT NULL, 33 34 url text NOT NULL 34 35 ); 35 - ALTER TABLE musiclink ADD CONSTRAINT musiclink_pk PRIMARY KEY (release, name); 36 + ALTER TABLE public.musiclink ADD CONSTRAINT musiclink_pk PRIMARY KEY (release, name); 36 37 37 38 -- 38 39 -- Music credits (artist credits under a release) 39 40 -- 40 - CREATE TABLE musiccredit ( 41 + CREATE TABLE public.musiccredit ( 41 42 release character varying(64) NOT NULL, 42 - artist uuid NOT NULL, 43 + artist character varying(64) NOT NULL, 43 44 role text NOT NULL, 44 45 is_primary boolean DEFAULT false 45 46 ); 46 - ALTER TABLE musiccredit ADD CONSTRAINT musiccredit_pk PRIMARY KEY (release, artist); 47 + ALTER TABLE public.musiccredit ADD CONSTRAINT musiccredit_pk PRIMARY KEY (release, artist); 47 48 48 49 -- 49 50 -- Music tracks (tracks under a release) 50 51 -- 51 - CREATE TABLE musictrack ( 52 + CREATE TABLE public.musictrack ( 52 53 id uuid DEFAULT gen_random_uuid(), 53 54 title text NOT NULL, 54 55 description text, 55 56 lyrics text, 56 57 preview_url text 57 58 ); 58 - ALTER TABLE musictrack ADD CONSTRAINT musictrack_pk PRIMARY KEY (id); 59 + ALTER TABLE public.musictrack ADD CONSTRAINT musictrack_pk PRIMARY KEY (id); 59 60 60 61 -- 61 62 -- Music release/track pairs 62 63 -- 63 - CREATE TABLE musicreleasetrack ( 64 + CREATE TABLE public.musicreleasetrack ( 64 65 release character varying(64) NOT NULL, 65 66 track uuid NOT NULL, 66 67 number integer NOT NULL 67 68 ); 68 - ALTER TABLE musicreleasetrack ADD CONSTRAINT musictrack_pk PRIMARY KEY (release, track); 69 + ALTER TABLE public.musicreleasetrack ADD CONSTRAINT musicreleasetrack_pk PRIMARY KEY (release, track); 69 70 70 71 -- 71 72 -- Foreign keys 72 73 -- 73 - ALTER TABLE musiccredit ADD CONSTRAINT musiccredit_artist_fk FOREIGN KEY (artist) REFERENCES artist(id) ON DELETE CASCADE ON UPDATE CASCADE; 74 - ALTER TABLE musiccredit ADD CONSTRAINT musiccredit_release_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON DELETE CASCADE; 75 - ALTER TABLE musiclink ADD CONSTRAINT musiclink_release_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON UPDATE CASCADE ON DELETE CASCADE; 76 - ALTER TABLE musicreleasetrack ADD CONSTRAINT music_pair_trackref_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON DELETE CASCADE; 77 - ALTER TABLE musicreleasetrack ADD CONSTRAINT music_pair_releaseref_fk FOREIGN KEY (track) REFERENCES musictrack(id) ON DELETE CASCADE; 74 + ALTER TABLE public.musiccredit ADD CONSTRAINT musiccredit_artist_fk FOREIGN KEY (artist) REFERENCES public.artist(id) ON DELETE CASCADE ON UPDATE CASCADE; 75 + ALTER TABLE public.musiccredit ADD CONSTRAINT musiccredit_release_fk FOREIGN KEY (release) REFERENCES public.musicrelease(id) ON DELETE CASCADE; 76 + ALTER TABLE public.musiclink ADD CONSTRAINT musiclink_release_fk FOREIGN KEY (release) REFERENCES public.musicrelease(id) ON UPDATE CASCADE ON DELETE CASCADE; 77 + ALTER TABLE public.musicreleasetrack ADD CONSTRAINT music_pair_trackref_fk FOREIGN KEY (release) REFERENCES public.musicrelease(id) ON DELETE CASCADE; 78 + ALTER TABLE public.musicreleasetrack ADD CONSTRAINT music_pair_releaseref_fk FOREIGN KEY (track) REFERENCES public.musictrack(id) ON DELETE CASCADE;
-22
views/admin.html
··· 1 - {{define "head"}} 2 - <title>admin - ari melody 💫</title> 3 - <link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon"> 4 - 5 - <link rel="stylesheet" href="/style/admin.css"> 6 - {{end}} 7 - 8 - {{define "content"}} 9 - <main> 10 - <script type="module" src="/script/admin.js" defer></script> 11 - 12 - <h1> 13 - # admin panel 14 - </h1> 15 - 16 - <p> 17 - whapow! nothing here. 18 - <br> 19 - nice try, though. 20 - </p> 21 - </main> 22 - {{end}}
+55
views/admin/index.html
··· 1 + {{define "head"}} 2 + <title>admin - ari melody 💫</title> 3 + <link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon"> 4 + {{end}} 5 + 6 + {{define "content"}} 7 + <header> 8 + <img src="/img/favicon.png" alt="" class="icon"> 9 + <a href="/admin">home</a> 10 + <a href="/admin/releases">releases</a> 11 + <a href="/admin/artists">artists</a> 12 + </header> 13 + 14 + <main> 15 + 16 + <h1>Releases</h1> 17 + <div class="card releases"> 18 + {{range $Release := .Releases}} 19 + <div class="release"> 20 + <div class="release-artwork"> 21 + <img src="{{$Release.Artwork}}" alt="" width="128" loading="lazy"> 22 + </div> 23 + <div class="release-info"> 24 + <h3 class="release-title">{{$Release.Title}} <small>{{$Release.GetReleaseYear}}</small></h3> 25 + <p class="release-artists">{{$Release.PrintArtists true true}}</p> 26 + <p class="release-type-single">{{$Release.ReleaseType}}</p> 27 + <div class="release-actions"> 28 + <a href="/admin/releases/{{$Release.ID}}">Edit</a> 29 + <a href="/music/{{$Release.ID}}" target="_blank">Gateway</a> 30 + </div> 31 + </div> 32 + </div> 33 + {{end}} 34 + {{if not .Releases}} 35 + <p>There are no releases.</p> 36 + {{end}} 37 + </div> 38 + 39 + <h1>Artists</h1> 40 + <div class="card artists"> 41 + {{range $Artist := .Artists}} 42 + <div class="artist"> 43 + <img src="https://arimelody.me/img/favicon.png" alt="" width="64" loading="lazy" class="artist-avatar"> 44 + <a href="/admin/artists/arimelody" class="artist-name">ari melody</a> 45 + </div> 46 + {{end}} 47 + {{if not .Artists}} 48 + <p>There are no artists.</p> 49 + {{end}} 50 + </div> 51 + 52 + </main> 53 + 54 + <script type="module" src="/admin/static/admin.js" defer></script> 55 + {{end}}
+22
views/admin/layout.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + 4 + <head> 5 + <meta http-equiv="content-type" content="text/html; charset=UTF-8"> 6 + <meta charset="UTF-8"> 7 + <meta http-equiv="X-UA-Compatible" content="IE=edge"> 8 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 9 + 10 + {{block "head" .}}{{end}} 11 + 12 + <link rel="stylesheet" href="/admin/static/admin.css"> 13 + </head> 14 + 15 + <body> 16 + {{block "content" .}} 17 + {{end}} 18 + 19 + {{template "prideflag"}} 20 + </body> 21 + 22 + </html>
+20
views/admin/login.html
··· 1 + {{define "head"}} 2 + <title>admin - ari melody 💫</title> 3 + <link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon"> 4 + 5 + <style> 6 + .discord { 7 + color: #5865F2; 8 + } 9 + </style> 10 + {{end}} 11 + 12 + {{define "content"}} 13 + <main> 14 + 15 + <p>Log in with <a href="{{.}}" class="discord">Discord</a>.</p> 16 + 17 + </main> 18 + 19 + <script type="module" src="/admin/static/admin.js" defer></script> 20 + {{end}}
+35 -35
views/music-gateway.html
··· 1 1 {{define "head"}} 2 - <title>{{.GetTitle}} - {{.PrintArtists true true}}</title> 2 + <title>{{.Title}} - {{.PrintArtists true true}}</title> 3 3 <link rel="icon" type="image/png" href="{{.GetArtwork}}"> 4 4 5 - <meta name="description" content="Stream &quot;{{.GetTitle}}&quot; by {{.PrintArtists true true}} on all platforms!"> 5 + <meta name="description" content="Stream &quot;{{.Title}}&quot; by {{.PrintArtists true true}} on all platforms!"> 6 6 <meta name="author" content="{{.PrintArtists true true}}"> 7 - <meta name="keywords" content="{{.PrintArtists true false}}, music, {{.GetTitle}}, {{.GetID}}, {{.GetReleaseYear}}"> 7 + <meta name="keywords" content="{{.PrintArtists true false}}, music, {{.Title}}, {{.ID}}, {{.GetReleaseYear}}"> 8 8 9 - <meta property="og:url" content="https://arimelody.me/music/{{.GetID}}"> 9 + <meta property="og:url" content="https://arimelody.me/music/{{.ID}}"> 10 10 <meta property="og:type" content="website"> 11 11 <meta property="og:locale" content="en_IE"> 12 12 <meta property="og:site_name" content="ari melody music"> 13 - <meta property="og.Title" content="{{.GetTitle}} - {{.PrintArtists true true}}"> 14 - <meta property="og:description" content="Stream &quot;{{.GetTitle}}&quot; by {{.PrintArtists true true}} on all platforms!"> 13 + <meta property="og.Title" content="{{.Title}} - {{.PrintArtists true true}}"> 14 + <meta property="og:description" content="Stream &quot;{{.Title}}&quot; by {{.PrintArtists true true}} on all platforms!"> 15 15 <meta property="og:image" content="https://arimelody.me{{.GetArtwork}}"> 16 16 17 17 <meta name="twitter:card" content="summary_large_image"> 18 18 <meta name="twitter:site" content="@funniduck"> 19 19 <meta name="twitter:creator" content="@funniduck"> 20 20 <meta property="twitter:domain" content="arimelody.me"> 21 - <meta property="twitter:url" content="https://arimelody.me/music/{{.GetID}}"> 22 - <meta name="twitter.Title" content="{{.PrintArtists true true}} - {{.GetTitle}}"> 23 - <meta name="twitter:description" content="Stream &quot;{{.GetTitle}}&quot; by {{.PrintArtists true true}} on all platforms!"> 21 + <meta property="twitter:url" content="https://arimelody.me/music/{{.ID}}"> 22 + <meta name="twitter.Title" content="{{.PrintArtists true true}} - {{.Title}}"> 23 + <meta name="twitter:description" content="Stream &quot;{{.Title}}&quot; by {{.PrintArtists true true}} on all platforms!"> 24 24 <meta name="twitter:image" content="https://arimelody.me{{.GetArtwork}}"> 25 - <meta name="twitter:image:alt" content="Cover art for &quot;{{.GetTitle}}&quot;"> 25 + <meta name="twitter:image:alt" content="Cover art for &quot;{{.Title}}&quot;"> 26 26 27 27 <link rel="stylesheet" href="/style/main.css"> 28 28 <link rel="stylesheet" href="/style/music-gateway.css"> ··· 47 47 <div class="tilt-bottom"></div> 48 48 <div class="tilt-bottomleft"></div> 49 49 <div class="tilt-left"></div> 50 - <img id="artwork" src="{{.GetArtwork}}" alt="{{.GetTitle}} artwork" width=240 height=240> 50 + <img id="artwork" src="{{.GetArtwork}}" alt="{{.Title}} artwork" width=240 height=240> 51 51 </div> 52 52 <div id="vertical-line"></div> 53 53 <div id="info"> 54 54 <div id="overview"> 55 55 <div id="title-container"> 56 - <h1 id="title">{{.GetTitle}}</h1> 56 + <h1 id="title">{{.Title}}</h1> 57 57 <span id="year" title="{{.PrintReleaseDate}}">{{.GetReleaseYear}}</span> 58 58 </div> 59 59 <p id="artist">{{.PrintArtists true true}}</p> 60 - <p id="type" class="{{.GetType}}">{{.GetType}}</p> 60 + <p id="type" class="{{.ReleaseType}}">{{.ReleaseType}}</p> 61 61 62 62 <ul id="links"> 63 - {{if .GetBuyLink}} 63 + {{if .Buylink}} 64 64 <li> 65 - <a href="{{.GetBuyLink}}" class="buy">{{or .GetBuyName "buy"}}</a> 65 + <a href="{{.Buylink}}" class="buy">{{or .Buyname "buy"}}</a> 66 66 </li> 67 67 {{end}} 68 68 69 - {{range .GetLinks}} 69 + {{range .Links}} 70 70 <li> 71 - <a class="{{.NormaliseName}}" href="{{.GetURL}}">{{.GetName}}</a> 71 + <a class="{{.NormaliseName}}" href="{{.URL}}">{{.Name}}</a> 72 72 </li> 73 73 {{end}} 74 74 </ul> 75 75 76 - {{if .GetDescription}} 76 + {{if .Description}} 77 77 <p id="description"> 78 - {{.GetDescription}} 78 + {{.Description}} 79 79 </p> 80 80 {{end}} 81 81 82 82 <button id="share">share</button> 83 83 </div> 84 84 85 - {{if .GetCredits}} 85 + {{if .Credits}} 86 86 <div id="credits"> 87 87 <h2>credits:</h2> 88 88 <ul> 89 - {{range .GetCredits}} 90 - {{$Artist := .GetArtist}} 91 - {{if $Artist.GetWebsite}} 92 - <li><strong><a href="{{$Artist.GetWebsite}}">{{$Artist.GetName}}</a></strong>: {{.GetRole}}</li> 89 + {{range .Credits}} 90 + {{$Artist := .Artist}} 91 + {{if $Artist.Website}} 92 + <li><strong><a href="{{$Artist.Website}}">{{$Artist.Name}}</a></strong>: {{.Role}}</li> 93 93 {{else}} 94 - <li><strong>{{$Artist.GetName}}</strong>: {{.GetRole}}</li> 94 + <li><strong>{{$Artist.Name}}</strong>: {{.Role}}</li> 95 95 {{end}} 96 96 {{end}} 97 97 </ul> ··· 99 99 {{end}} 100 100 101 101 {{if .IsSingle}} 102 - {{$Track := index .GetTracks 0}} 103 - {{if $Track.GetLyrics}} 102 + {{$Track := index .Tracks 0}} 103 + {{if $Track.Lyrics}} 104 104 <div id="lyrics"> 105 105 <h2>lyrics:</h2> 106 - <p>{{$Track.GetLyrics}}</p> 106 + <p>{{$Track.Lyrics}}</p> 107 107 </div> 108 108 {{end}} 109 109 {{else}} 110 110 <div id="tracks"> 111 111 <h2>tracks:</h2> 112 - {{range $i, $track := .GetTracks}} 112 + {{range $i, $track := .Tracks}} 113 113 <details> 114 - <summary class="album-track-title">{{$track.GetNumber}}. {{$track.GetTitle}}</summary> 115 - {{$track.GetLyrics}} 114 + <summary class="album-track-title">{{$track.Number}}. {{$track.Title}}</summary> 115 + {{$track.Lyrics}} 116 116 </details> 117 117 {{end}} 118 118 </div> ··· 123 123 <ul> 124 124 <li><a href="#overview">overview</a></li> 125 125 126 - {{if .GetCredits}} 126 + {{if .Credits}} 127 127 <li><a href="#credits">credits</a></li> 128 128 {{end}} 129 129 130 130 {{if .IsSingle}} 131 - {{$Track := index .GetTracks 0}} 132 - {{if $Track.GetLyrics}} 131 + {{$Track := index .Tracks 0}} 132 + {{if $Track.Lyrics}} 133 133 <li><a href="#lyrics">lyrics</a></li> 134 134 {{end}} 135 135 {{else}} ··· 156 156 <!-- <% } else { %> --> 157 157 <!-- <div class="track-preview" id="preview-<%= data.id %>"> --> 158 158 <!-- <i class="fa-solid fa-play play"></i> --> 159 - <!-- <p>{{.GetTitle}}</p> --> 159 + <!-- <p>{{.Title}}</p> --> 160 160 <!-- <audio src="<%= file %>"></audio> --> 161 161 <!-- </div> --> 162 162 <!-- <% } %> -->
+7 -7
views/music.html
··· 26 26 27 27 <div id="music-container"> 28 28 {{range $Release := .}} 29 - <div class="music" id="{{$Release.GetID}}" swap-url="/music/{{$Release.GetID}}"> 29 + <div class="music" id="{{$Release.ID}}" swap-url="/music/{{$Release.ID}}"> 30 30 <div class="music-artwork"> 31 - <img src="{{$Release.GetArtwork}}" alt="{{$Release.GetTitle}} artwork" width="128" loading="lazy"> 31 + <img src="{{$Release.GetArtwork}}" alt="{{$Release.Title}} artwork" width="128" loading="lazy"> 32 32 </div> 33 33 <div class="music-details"> 34 34 <h1 class="music-title"> 35 - <a href="/music/{{$Release.GetID}}"> 36 - {{$Release.GetTitle}} 35 + <a href="/music/{{$Release.ID}}"> 36 + {{$Release.Title}} 37 37 </a> 38 38 </h1> 39 39 <h2 class="music-artist">{{$Release.PrintArtists true true}}</h2> 40 - <h3 class="music-type-{{$Release.GetType}}">{{$Release.GetType}}</h3> 40 + <h3 class="music-type-{{$Release.ReleaseType}}">{{$Release.ReleaseType}}</h3> 41 41 <ul class="music-links"> 42 - {{range $Link := $Release.GetLinks}} 42 + {{range $Link := $Release.Links}} 43 43 <li> 44 - <a href="{{$Link.GetURL}}" class="link-button">{{$Link.GetName}}</a> 44 + <a href="{{$Link.URL}}" class="link-button">{{$Link.Name}}</a> 45 45 </li> 46 46 {{end}} 47 47 </ul>
+1 -1
views/prideflag.html
··· 1 1 {{define "prideflag"}} 2 - <a href="https://github.com/mellodoot/prideflag" target="_blank" id="prideflag"> 2 + <a href="https://git.arimelody.me/ari/prideflag" target="_blank" id="prideflag"> 3 3 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120" hx-preserve="true"> 4 4 <path id="red" d="M120,80 L100,100 L120,120 Z" style="fill:#d20605"/> 5 5 <path id="orange" d="M120,80 V40 L80,80 L100,100 Z" style="fill:#ef9c00"/>