home to your local SPACEGIRL 💫 arimelody.space
1
fork

Configure Feed

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

added stuff, broke some other stuff, made admin auth!

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

+1594 -1380
+1
.gitignore
··· 1 1 **/.DS_Store 2 2 .idea/ 3 3 tmp/ 4 + test/
+5 -13
api/api.go
··· 1 1 package api 2 2 3 3 import ( 4 - "net/http" 5 - "html/template" 6 - 7 - "arimelody.me/arimelody.me/api/v1/admin" 4 + "net/http" 5 + "html/template" 8 6 ) 9 7 10 8 func Handle(writer http.ResponseWriter, req *http.Request, root *template.Template) int { 11 - code := 404; 12 - if req.URL.Path == "/api/v1/admin/login" { 13 - code = admin.HandleLogin(writer, req, root) 14 - } 15 - 16 - if code == 404 { 17 - writer.Write([]byte("404 not found")) 18 - } 19 - return code; 9 + writer.WriteHeader(501) 10 + writer.Write([]byte("501 Not Implemented")) 11 + return 501; 20 12 }
+205 -20
api/v1/admin/admin.go
··· 1 1 package admin 2 2 3 3 import ( 4 + "context" 5 + "encoding/json" 4 6 "fmt" 5 - "html/template" 6 - "io" 7 + "math/rand" 7 8 "net/http" 9 + "net/url" 10 + "os" 11 + "strings" 12 + "time" 13 + 14 + "arimelody.me/arimelody.me/discord" 8 15 ) 9 16 10 17 type ( 11 - State struct { 12 - Token string 18 + Session struct { 19 + UserID string 20 + Token string 21 + } 22 + 23 + loginData struct { 24 + UserID string 25 + Password string 26 + } 27 + ) 28 + 29 + const TOKEN_LENGTH = 64 30 + const TOKEN_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 31 + 32 + // TODO: consider relying *entirely* on env vars instead of hard-coded fallbacks 33 + var ADMIN_ID_DISCORD = func() string { 34 + envvar := os.Getenv("DISCORD_ADMIN_ID") 35 + if envvar != "" { 36 + return envvar 37 + } else { 38 + return "356210742200107009" 39 + } 40 + }() 41 + 42 + var sessions []*Session 43 + 44 + func CreateSession(UserID string) Session { 45 + return Session{ 46 + UserID: UserID, 47 + Token: string(generateToken()), 48 + } 49 + } 50 + 51 + func Handler() http.Handler { 52 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 53 + fmt.Println(r.URL.Path) 54 + 55 + w.WriteHeader(200) 56 + w.Write([]byte("hello admin!")) 57 + }) 58 + } 59 + 60 + func AuthorisedHandler(next http.Handler) http.Handler { 61 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 62 + auth := r.Header.Get("Authorization") 63 + if auth == "" || !strings.HasPrefix(auth, "Bearer ") { 64 + cookie, err := r.Cookie("token") 65 + if err != nil { 66 + w.WriteHeader(401) 67 + w.Write([]byte("Unauthorized")) 68 + return 69 + } 70 + auth = cookie.Value 13 71 } 14 - ) 72 + auth = auth[7:] 15 73 16 - func CreateState() *State { 17 - return &State{ 18 - Token: "you are the WINRAR!!", 74 + var session *Session 75 + for _, s := range sessions { 76 + if s.Token == auth { 77 + session = s 78 + break 79 + } 80 + } 81 + 82 + if session == nil { 83 + w.WriteHeader(401) 84 + w.Write([]byte("Unauthorized")) 85 + return 19 86 } 87 + 88 + ctx := context.WithValue(r.Context(), "token", session.Token) 89 + next.ServeHTTP(w, r.WithContext(ctx)) 90 + }) 20 91 } 21 92 22 - func HandleLogin(writer http.ResponseWriter, req *http.Request, root *template.Template) int { 23 - if req.Method != "POST" { 24 - return 404; 93 + func LoginHandler() http.Handler { 94 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 95 + code := r.URL.Query().Get("code") 96 + 97 + if code == "" { 98 + w.Header().Add("Location", discord.REDIRECT_URI) 99 + w.WriteHeader(307) 100 + return 101 + } 102 + 103 + // let's get an oauth token! 104 + req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/oauth2/token", discord.API_ENDPOINT), 105 + strings.NewReader(url.Values{ 106 + "client_id": {discord.CLIENT_ID}, 107 + "client_secret": {discord.CLIENT_SECRET}, 108 + "grant_type": {"authorization_code"}, 109 + "code": {code}, 110 + "redirect_uri": {discord.MY_REDIRECT_URI}, 111 + }.Encode())) 112 + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 113 + 114 + res, err := http.DefaultClient.Do(req) 115 + if err != nil { 116 + fmt.Printf("Failed to retrieve OAuth token: %s\n", err) 117 + w.WriteHeader(500) 118 + w.Write([]byte("Internal server error")) 119 + return 25 120 } 26 121 27 - body, err := io.ReadAll(req.Body) 122 + oauth := discord.AccessTokenResponse{} 123 + 124 + err = json.NewDecoder(res.Body).Decode(&oauth) 28 125 if err != nil { 29 - fmt.Printf("failed to parse request body!\n"); 30 - return 500; 126 + fmt.Printf("Failed to parse OAuth response data from discord: %s\n", err) 127 + w.WriteHeader(500) 128 + w.Write([]byte("Internal server error")) 129 + return 31 130 } 131 + res.Body.Close() 32 132 33 - if string(body) != "super epic mega gaming password" { 34 - return 400; 133 + discord_access_token := oauth.AccessToken 134 + 135 + // let's get authorisation information! 136 + req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%s/oauth2/@me", discord.API_ENDPOINT), nil) 137 + req.Header.Add("Authorization", "Bearer " + discord_access_token) 138 + 139 + res, err = http.DefaultClient.Do(req) 140 + if err != nil { 141 + fmt.Printf("Failed to retrieve discord auth information: %s\n", err) 142 + w.WriteHeader(500) 143 + w.Write([]byte("Internal server error")) 144 + return 35 145 } 36 146 37 - state := CreateState(); 147 + auth_info := discord.AuthInfoResponse{} 38 148 39 - writer.WriteHeader(200); 40 - writer.Write([]byte(state.Token)) 149 + err = json.NewDecoder(res.Body).Decode(&auth_info) 150 + if err != nil { 151 + fmt.Printf("Failed to parse auth information from discord: %s\n", err) 152 + w.WriteHeader(500) 153 + w.Write([]byte("Internal server error")) 154 + return 155 + } 156 + res.Body.Close() 41 157 42 - return 200; 158 + discord_user_id := auth_info.User.Id 159 + 160 + if discord_user_id != ADMIN_ID_DISCORD { 161 + // TODO: unauthorized user. revoke the token 162 + w.WriteHeader(401) 163 + w.Write([]byte("Unauthorized")) 164 + return 165 + } 166 + 167 + // login success! 168 + session := CreateSession(auth_info.User.Username) 169 + sessions = append(sessions, &session) 170 + 171 + cookie := http.Cookie{} 172 + cookie.Name = "token" 173 + cookie.Value = session.Token 174 + cookie.Expires = time.Now().Add(24 * time.Hour) 175 + // cookie.Secure = true 176 + cookie.HttpOnly = true 177 + cookie.Path = "/" 178 + http.SetCookie(w, &cookie) 179 + 180 + w.WriteHeader(200) 181 + w.Write([]byte(session.Token)) 182 + }) 183 + } 184 + 185 + func LogoutHandler() http.Handler { 186 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 187 + token := r.Context().Value("token").(string) 188 + 189 + if token == "" { 190 + w.WriteHeader(401) 191 + return 192 + } 193 + 194 + // remove this session from the list 195 + sessions = func (token string) []*Session { 196 + new_sessions := []*Session{} 197 + for _, session := range sessions { 198 + new_sessions = append(new_sessions, session) 199 + } 200 + return new_sessions 201 + }(token) 202 + 203 + w.WriteHeader(200) 204 + }) 205 + } 206 + 207 + func OAuthCallbackHandler() http.Handler { 208 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 209 + }) 210 + } 211 + 212 + func VerifyHandler() http.Handler { 213 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 214 + // this is an authorised endpoint, so you *must* supply a valid token 215 + // before accessing this route. 216 + w.WriteHeader(200) 217 + }) 218 + } 219 + 220 + func generateToken() string { 221 + var token []byte 222 + 223 + for i := 0; i < TOKEN_LENGTH; i++ { 224 + token = append(token, TOKEN_CHARS[rand.Intn(len(TOKEN_CHARS))]) 225 + } 226 + 227 + return string(token) 43 228 }
+25 -21
api/v1/music/music.go
··· 1 1 package music 2 2 3 3 import ( 4 - "fmt" 5 - "time" 4 + "errors" 5 + "fmt" 6 + "time" 6 7 ) 7 8 9 + var Releases []*MusicRelease; 10 + var Artists []*Artist; 11 + 8 12 func make_date_work(date string) time.Time { 9 - res, err := time.Parse("2-Jan-2006", date) 10 - if err != nil { 11 - fmt.Printf("somehow we failed to parse %s! falling back to epoch :]\n", date) 12 - return time.Unix(0, 0) 13 - } 14 - return res 13 + res, err := time.Parse("2-Jan-2006", date) 14 + if err != nil { 15 + fmt.Printf("somehow we failed to parse %s! falling back to epoch :]\n", date) 16 + return time.Unix(0, 0) 17 + } 18 + return res 15 19 } 16 20 17 - func GetRelease(id string) (MusicRelease, bool) { 18 - for _, album := range placeholders { 19 - if album.Id == id { 20 - return album, true 21 - } 21 + func GetRelease(id string) (*MusicRelease, error) { 22 + for _, release := range Releases { 23 + if release.Id == id { 24 + return release, nil 22 25 } 23 - return MusicRelease{}, false 26 + } 27 + return nil, errors.New(fmt.Sprintf("Release %s not found", id)) 24 28 } 25 29 26 - func QueryAllMusic() ([]MusicRelease) { 27 - return placeholders 30 + func GetArtist(id string) (*Artist, error) { 31 + for _, artist := range Artists { 32 + if artist.Id == id { 33 + return artist, nil 34 + } 35 + } 36 + return nil, errors.New(fmt.Sprintf("Artist %s not found", id)) 28 37 } 29 - 30 - func QueryAllArtists() ([]Artist) { 31 - return []Artist{ ari, mellodoot, zaire, mae, loudar, red } 32 - } 33 -
-161
api/v1/music/music_placeholders.go
··· 1 - package music 2 - 3 - var ari = Artist{ 4 - Id: "arimelody", 5 - Name: "ari melody", 6 - Website: "https://arimelody.me", 7 - } 8 - var mellodoot = Artist{ 9 - Id: "mellodoot", 10 - Name: "mellodoot", 11 - Website: "https://mellodoot.com", 12 - } 13 - var zaire = Artist{ 14 - Id: "zaire", 15 - Name: "zaire", 16 - Website: "https://supitszaire.com", 17 - } 18 - var mae = Artist{ 19 - Id: "maetaylor", 20 - Name: "mae taylor", 21 - Website: "https://mae.wtf", 22 - } 23 - var loudar = Artist{ 24 - Id: "loudar", 25 - Name: "Loudar", 26 - Website: "https://alex.targoninc.com", 27 - } 28 - var red = Artist { 29 - Id: "smoljorb", 30 - Name: "smoljorb", 31 - } 32 - 33 - var placeholders = []MusicRelease{ 34 - { 35 - Id: "test", 36 - Title: "test album", 37 - Type: "album", 38 - ReleaseDate: make_date_work("18-Mar-2024"), 39 - Buyname: "go get it!!", 40 - Buylink: "https://arimelody.me/", 41 - Links: []MusicLink{ 42 - { 43 - Name: "youtube", 44 - Url: "https://youtu.be/dQw4w9WgXcQ", 45 - }, 46 - }, 47 - Description: 48 - `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas viverra ligula interdum, tempor metus venenatis, tempus est. Praesent semper vulputate nulla, a venenatis libero elementum id. Proin maximus aliquet accumsan. Integer eu orci congue, ultrices leo sed, maximus risus. Integer laoreet non urna non accumsan. Cras ut sollicitudin justo. Vivamus eu orci tempus, aliquet est rhoncus, tempus neque. Aliquam tempor sit amet nibh sed tempus. Nulla vitae bibendum purus. Sed in mi enim. Nam pharetra enim lorem, vel tristique diam malesuada a. Duis dignissim nunc mi, id semper ex tincidunt a. Sed laoreet consequat lacus a consectetur. Nulla est diam, tempus eget lacus ullamcorper, tincidunt faucibus ex. Duis consectetur felis sit amet ante fermentum interdum. Sed pulvinar laoreet tellus.`, 49 - Credits: []MusicCredit{ 50 - { 51 - Artist: &ari, 52 - Role: "having the swag", 53 - }, 54 - { 55 - Artist: &zaire, 56 - Role: "having the swag", 57 - }, 58 - { 59 - Artist: &mae, 60 - Role: "having the swag", 61 - }, 62 - { 63 - Artist: &loudar, 64 - Role: "having the swag", 65 - }, 66 - }, 67 - Tracks: []MusicTrack{ 68 - { 69 - Number: 0, 70 - Title: "track 1", 71 - Description: "sample track description", 72 - Lyrics: "sample lyrics for track 1!", 73 - PreviewUrl: "https://mellodoot.com/audio/preview/dream.webm", 74 - }, 75 - { 76 - Number: 1, 77 - Title: "track 2", 78 - Description: "sample track description", 79 - Lyrics: "sample lyrics for track 2!", 80 - PreviewUrl: "https://mellodoot.com/audio/preview/dream.webm", 81 - }, 82 - }, 83 - }, 84 - { 85 - Id: "dream", 86 - Title: "Dream", 87 - Type: "single", 88 - ReleaseDate: make_date_work("11-Nov-2022"), 89 - Artwork: "https://mellodoot.com/img/music_artwork/mellodoot_-_Dream.webp", 90 - Buylink: "https://arimelody.bandcamp.com/track/dream", 91 - Links: []MusicLink{ 92 - { 93 - Name: "spotify", 94 - Url: "https://open.spotify.com/album/5talRpqzjExP1w6j5LFIAh", 95 - }, 96 - { 97 - Name: "apple music", 98 - Url: "https://music.apple.com/ie/album/dream-single/1650037132", 99 - }, 100 - { 101 - Name: "soundcloud", 102 - Url: "https://soundcloud.com/arimelody/dream2022", 103 - }, 104 - { 105 - Name: "youtube", 106 - Url: "https://www.youtube.com/watch?v=nfFgtMuYAx8", 107 - }, 108 - }, 109 - Description: "living the dream 🌌 ✨", 110 - Credits: []MusicCredit{ 111 - { 112 - Artist: &mellodoot, 113 - Role: "vocals", 114 - }, 115 - { 116 - Artist: &mellodoot, 117 - Role: "production", 118 - }, 119 - { 120 - Artist: &mellodoot, 121 - Role: "artwork", 122 - }, 123 - }, 124 - Tracks: []MusicTrack{ 125 - { 126 - Number: 0, 127 - Title: "Dream", 128 - Description: "no description here!", 129 - Lyrics: 130 - `the truth is what you make of it 131 - in the end, what you spend, is the end of it 132 - when you're lost in the life 133 - the life that you created on your own 134 - i'm becoming one 135 - with the soul that i see in the mirror 136 - blending one and whole 137 - this time, i'm real 138 - 139 - i'm living the dream 140 - i'm living my best life 141 - running out of time 142 - i gotta make this right 143 - whenever you rise 144 - whenever you come down 145 - fall away from the light 146 - and then fall into our arms 147 - 148 - the truth is what you make of it 149 - in the end, what you spend, is the end of it 150 - when you're lost in the life 151 - the life that you created on your own 152 - i'm becoming one 153 - with the soul that i see in the mirror 154 - blending one and whole 155 - this time, i'm real`, 156 - PreviewUrl: "https://mellodoot.com/audio/preview/dream.webm", 157 - }, 158 - }, 159 - }, 160 - } 161 -
+91 -94
api/v1/music/music_types.go
··· 1 1 package music 2 2 3 3 import ( 4 - "regexp" 5 - "strings" 6 - "time" 4 + "regexp" 5 + "strings" 6 + "time" 7 7 ) 8 8 9 9 type ( 10 - Artist struct { 11 - Id string 12 - Name string 13 - Website string 14 - } 10 + Artist struct { 11 + Id string 12 + Name string 13 + Website string 14 + } 15 15 16 - MusicRelease struct { 17 - Id string 18 - Title string 19 - Type string 20 - ReleaseDate time.Time 21 - Artwork string 22 - Buyname string 23 - Buylink string 24 - Links []MusicLink 25 - Description string 26 - Credits []MusicCredit 27 - Tracks []MusicTrack 28 - } 16 + MusicRelease struct { 17 + Id string 18 + Title string 19 + Type string 20 + ReleaseDate time.Time 21 + Artwork string 22 + Buyname string 23 + Buylink string 24 + Links []MusicLink 25 + Description string 26 + Credits []MusicCredit 27 + Tracks []MusicTrack 28 + } 29 29 30 - MusicLink struct { 31 - Name string 32 - Url string 33 - } 30 + MusicLink struct { 31 + Name string 32 + Url string 33 + } 34 34 35 - MusicCredit struct { 36 - Artist *Artist 37 - Role string 38 - Meta bool // for "meta" contributors (i.e. not credited for the musical work, but other related assets) 39 - } 35 + MusicCredit struct { 36 + Artist *Artist 37 + Role string 38 + Primary bool 39 + } 40 40 41 - MusicTrack struct { 42 - Number int 43 - Title string 44 - Description string 45 - Lyrics string 46 - PreviewUrl string 47 - } 41 + MusicTrack struct { 42 + Number int 43 + Title string 44 + Description string 45 + Lyrics string 46 + PreviewUrl string 47 + } 48 48 ) 49 49 50 - func (release MusicRelease) GetUniqueArtists(include_meta bool) []*Artist { 51 - res := []*Artist{} 52 - for _, credit := range release.Credits { 53 - if !include_meta && credit.Meta { 54 - continue 55 - } 50 + func (release MusicRelease) GetUniqueArtists(include_non_primary bool) []*Artist { 51 + res := []*Artist{} 52 + for _, credit := range release.Credits { 53 + if !include_non_primary && !credit.Primary { 54 + continue 55 + } 56 56 57 - exists := false 58 - for _, a := range res { 59 - if a == credit.Artist { 60 - exists = true 61 - break 62 - } 63 - } 64 - if exists { 65 - continue 66 - } 57 + exists := false 58 + for _, a := range res { 59 + if a == credit.Artist { 60 + exists = true 61 + break 62 + } 63 + } 64 + if exists { 65 + continue 66 + } 67 67 68 - res = append(res, credit.Artist) 69 - } 68 + res = append(res, credit.Artist) 69 + } 70 70 71 - // now create the actual array to send 72 - return res 71 + return res 73 72 } 74 73 75 - func (release MusicRelease) GetUniqueArtistNames(include_meta bool) []string { 76 - artists := release.GetUniqueArtists(include_meta) 77 - names := []string{} 78 - for _, artist := range artists { 79 - names = append(names, artist.Name) 80 - } 74 + func (release MusicRelease) GetUniqueArtistNames(include_non_primary bool) []string { 75 + artists := release.GetUniqueArtists(include_non_primary) 76 + names := []string{} 77 + for _, artist := range artists { 78 + names = append(names, artist.Name) 79 + } 81 80 82 - return names 81 + return names 83 82 } 84 83 85 - func (album MusicRelease) PrintPrimaryArtists(include_meta bool) string { 86 - names := album.GetUniqueArtistNames(include_meta) 87 - if len(names) == 1 { 88 - return names[0] 89 - } 84 + func (release MusicRelease) PrintPrimaryArtists(include_non_primary bool, ampersand bool) string { 85 + names := release.GetUniqueArtistNames(include_non_primary) 86 + if len(names) == 0 { 87 + return "Unknown Artist" 88 + } else if len(names) == 1 { 89 + return names[0] 90 + } 91 + if ampersand { 90 92 res := strings.Join(names[:len(names)-1], ", ") 91 93 res += " & " + names[len(names)-1] 92 94 return res 93 - } 94 - 95 - func (album MusicRelease) PrintCommaPrimaryArtists(include_meta bool) string { 96 - names := album.GetUniqueArtistNames(include_meta) 97 - if len(names) == 1 { 98 - return names[0] 99 - } 95 + } else { 100 96 return strings.Join(names[:], ", ") 97 + } 101 98 } 102 99 103 - func (album MusicRelease) ResolveType() string { 104 - if album.Type != "" { 105 - return album.Type 106 - } 107 - return "unknown" 100 + func (release MusicRelease) ResolveType() string { 101 + if release.Type != "" { 102 + return release.Type 103 + } 104 + return "unknown" 108 105 } 109 106 110 - func (album MusicRelease) ResolveArtwork() string { 111 - if album.Artwork != "" { 112 - return album.Artwork 113 - } 114 - return "/img/music-artwork/default.png" 107 + func (release MusicRelease) ResolveArtwork() string { 108 + if release.Artwork != "" { 109 + return release.Artwork 110 + } 111 + return "/img/music-artwork/default.png" 115 112 } 116 113 117 - func (album MusicRelease) PrintReleaseDate() string { 118 - return album.ReleaseDate.Format("02 January 2006") 114 + func (release MusicRelease) PrintReleaseDate() string { 115 + return release.ReleaseDate.Format("02 January 2006") 119 116 } 120 117 121 - func (album MusicRelease) GetReleaseYear() int { 122 - return album.ReleaseDate.Year() 118 + func (release MusicRelease) GetReleaseYear() int { 119 + return release.ReleaseDate.Year() 123 120 } 124 121 125 122 func (link MusicLink) NormaliseName() string { 126 - re := regexp.MustCompile(`[^a-z0-9]`) 127 - return strings.ToLower(re.ReplaceAllString(link.Name, "")) 123 + re := regexp.MustCompile(`[^a-z0-9]`) 124 + return strings.ToLower(re.ReplaceAllString(link.Name, "")) 128 125 } 129 126 130 127 func (release MusicRelease) IsSingle() bool { 131 - return len(release.Tracks) == 1; 128 + return len(release.Tracks) == 1; 132 129 } 133 130 134 131 func (credit MusicCredit) ResolveArtist() Artist { 135 - return *credit.Artist 132 + return *credit.Artist 136 133 }
+151 -57
db.go
··· 1 1 package main 2 2 3 3 import ( 4 - "arimelody.me/arimelody.me/api/v1/music" 4 + "arimelody.me/arimelody.me/api/v1/music" 5 5 6 - "fmt" 7 - "os" 8 - "time" 6 + "fmt" 7 + "os" 8 + "time" 9 9 10 - _ "github.com/lib/pq" 11 - "github.com/jmoiron/sqlx" 10 + "github.com/jmoiron/sqlx" 11 + _ "github.com/lib/pq" 12 12 ) 13 13 14 - var schema = 15 - `CREATE TABLE IF NOT EXISTS artists ( 16 - id TEXT PRIMARY KEY, 17 - name TEXT, 18 - website TEXT 19 - ); 14 + func PushArtist(db *sqlx.DB, artist music.Artist) { 15 + fmt.Printf("pushing artist [%s] to database...", artist.Name) 20 16 21 - CREATE TABLE IF NOT EXISTS musicreleases ( 22 - id VARCHAR(64) PRIMARY KEY, 23 - title TEXT NOT NULL, 24 - type TEXT, 25 - release_date DATE NOT NULL, 26 - artwork TEXT, 27 - buyname TEXT, 28 - buylink TEXT 29 - ); 17 + db.MustExec("INSERT INTO artists (id, name, website) VALUES ($1, $2, $3) ON CONFLICT (id) DO UPDATE SET name=$2, website=$3", 18 + &artist.Id, 19 + &artist.Name, 20 + &artist.Website, 21 + ) 30 22 31 - CREATE TABLE IF NOT EXISTS musiclinks ( 32 - release VARCHAR(64) REFERENCES musicreleases(id) ON DELETE CASCADE ON UPDATE CASCADE, 33 - name TEXT, 34 - url TEXT, 35 - CONSTRAINT musiclinks_pk PRIMARY KEY (release, name) 36 - ); 23 + fmt.Printf("done!\n") 24 + } 37 25 38 - CREATE TABLE IF NOT EXISTS musiccredits ( 39 - release VARCHAR(64) REFERENCES musicreleases(ID) ON DELETE CASCADE, 40 - artist TEXT REFERENCES artists(id) ON DELETE CASCADE, 41 - role TEXT, 42 - meta BOOLEAN, 43 - constraint musiccredits_pk PRIMARY KEY (release, artist) 44 - ); 26 + func PullAllArtists(db *sqlx.DB) ([]*music.Artist, error) { 27 + artists := []*music.Artist{} 45 28 46 - CREATE TABLE IF NOT EXISTS musictracks ( 47 - release VARCHAR(64) REFERENCES musicreleases(ID) ON DELETE CASCADE, 48 - number INT NOT NULL, 49 - title TEXT NOT NULL, 50 - description TEXT, 51 - lyrics TEXT, 52 - preview_url TEXT, 53 - CONSTRAINT musictracks_pk PRIMARY KEY (release, number) 54 - );` 29 + rows, err := db.Query("SELECT id, name, website FROM artists") 30 + if err != nil { 31 + return nil, err 32 + } 55 33 56 - func PushArtist(db *sqlx.DB, artist music.Artist) { 57 - fmt.Printf("syncing artist [%s] to database...", artist.Name) 34 + for rows.Next() { 35 + var artist = music.Artist{} 36 + err = rows.Scan(&artist.Id, &artist.Name, &artist.Website) 37 + if err != nil { 38 + return nil, err 39 + } 40 + artists = append(artists, &artist) 41 + } 58 42 59 - db.MustExec("INSERT INTO artists (id, name, website) VALUES ($1, $2, $3) ON CONFLICT (id) DO UPDATE SET name=$2, website=$3", 60 - &artist.Id, 61 - &artist.Name, 62 - &artist.Website, 63 - ) 43 + return artists, nil 44 + } 45 + 46 + func PullArtist(db *sqlx.DB, artistID string) (music.Artist, error) { 47 + artist := music.Artist{} 48 + 49 + err := db.Get(&artist, "SELECT id, name, website FROM artists WHERE id=$1", artistID) 50 + if err != nil { 51 + return music.Artist{}, err 52 + } 64 53 65 - fmt.Printf("done!\n") 54 + return artist, nil 66 55 } 67 56 68 57 func PushRelease(db *sqlx.DB, release music.MusicRelease) { 69 - fmt.Printf("syncing release [%s] to database...", release.Id) 58 + fmt.Printf("pushing release [%s] to database...", release.Id) 70 59 71 60 tx := db.MustBegin() 72 61 tx.MustExec("INSERT INTO musicreleases (id, title, type, release_date, artwork, buyname, buylink) VALUES ($1, $2, $3, $4, $5, $6, $7) "+ ··· 78 67 &release.Id, &link.Name, &link.Url) 79 68 } 80 69 for _, credit := range release.Credits { 81 - tx.MustExec("INSERT INTO musiccredits (release, artist, role, meta) VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING", 82 - &release.Id, &credit.Artist.Id, &credit.Role, &credit.Meta) 70 + tx.MustExec("INSERT INTO musiccredits (release, artist, role, is_primary) VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING", 71 + &release.Id, &credit.Artist.Id, &credit.Role, &credit.Primary) 83 72 } 84 73 for _, track := range release.Tracks { 85 74 tx.MustExec("INSERT INTO musictracks (release, number, title, description, lyrics, preview_url) VALUES ($1, $2, $3, $4, $5, $6) "+ ··· 92 81 fmt.Printf("done!\n") 93 82 } 94 83 84 + func PullAllReleases(db *sqlx.DB) ([]*music.MusicRelease, error) { 85 + releases := []*music.MusicRelease{} 86 + 87 + rows, err := db.Query("SELECT id, title, type, release_date, artwork, buyname, buylink FROM musicreleases") 88 + if err != nil { 89 + return nil, err 90 + } 91 + 92 + for rows.Next() { 93 + var release = music.MusicRelease{} 94 + release.Credits = []music.MusicCredit{} 95 + release.Links = []music.MusicLink{} 96 + release.Tracks = []music.MusicTrack{} 97 + 98 + err = rows.Scan( 99 + &release.Id, 100 + &release.Title, 101 + &release.Type, 102 + &release.ReleaseDate, 103 + &release.Artwork, 104 + &release.Buyname, 105 + &release.Buylink) 106 + if err != nil { 107 + continue 108 + } 109 + 110 + // pull musiccredits for artist data 111 + credit_rows, err := db.Query("SELECT artist, role, is_primary FROM musiccredits WHERE release=$1", release.Id) 112 + if err != nil { 113 + fmt.Printf("error pulling credits for %s: %v\n", release.Id, err) 114 + continue 115 + } 116 + for credit_rows.Next() { 117 + var artistID string 118 + var credit = music.MusicCredit{} 119 + err = credit_rows.Scan( 120 + &artistID, 121 + &credit.Role, 122 + &credit.Primary) 123 + if err != nil { 124 + fmt.Printf("error pulling credit for %s: %v\n", release.Id, err) 125 + continue 126 + } 127 + artist, err := music.GetArtist(artistID) 128 + if err != nil { 129 + fmt.Printf("error pulling credit for %s: %v\n", release.Id, err) 130 + continue 131 + } 132 + credit.Artist = artist 133 + release.Credits = append(release.Credits, credit) 134 + } 135 + 136 + // pull musiclinks for link data 137 + link_rows, err := db.Query("SELECT name, url FROM musiclinks WHERE release=$1", release.Id); 138 + if err != nil { 139 + fmt.Printf("error pulling links for %s: %v\n", release.Id, err) 140 + continue 141 + } 142 + for link_rows.Next() { 143 + var link = music.MusicLink{} 144 + err = link_rows.Scan( 145 + &link.Name, 146 + &link.Url) 147 + if err != nil { 148 + fmt.Printf("error pulling link for %s: %v\n", release.Id, err) 149 + continue 150 + } 151 + release.Links = append(release.Links, link) 152 + } 153 + 154 + // pull musictracks for track data 155 + track_rows, err := db.Query("SELECT number, title, description, lyrics, preview_url FROM musictracks WHERE release=$1", release.Id); 156 + if err != nil { 157 + fmt.Printf("error pulling tracks for %s: %v\n", release.Id, err) 158 + continue 159 + } 160 + for track_rows.Next() { 161 + var track = music.MusicTrack{} 162 + err = track_rows.Scan( 163 + &track.Number, 164 + &track.Title, 165 + &track.Description, 166 + &track.Lyrics, 167 + &track.PreviewUrl) 168 + if err != nil { 169 + fmt.Printf("error pulling track for %s: %v\n", release.Id, err) 170 + continue 171 + } 172 + release.Tracks = append(release.Tracks, track) 173 + } 174 + 175 + releases = append(releases, &release) 176 + } 177 + 178 + return releases, nil 179 + } 180 + 181 + func PullRelease(db *sqlx.DB, releaseID string) (music.MusicRelease, error) { 182 + release := music.MusicRelease{} 183 + 184 + err := db.Get(&release, "SELECT id, title, type, release_date, artwork, buyname, buylink FROM musicreleases WHERE id=$1", releaseID) 185 + if err != nil { 186 + return music.MusicRelease{}, err 187 + } 188 + 189 + return release, nil 190 + } 191 + 95 192 func InitDatabase() *sqlx.DB { 96 193 db, err := sqlx.Connect("postgres", "user=arimelody dbname=arimelody password=fuckingpassword sslmode=disable") 97 194 if err != nil { ··· 102 199 db.SetConnMaxLifetime(time.Minute * 3) 103 200 db.SetMaxOpenConns(10) 104 201 db.SetMaxIdleConns(10) 105 - 106 - db.MustExec(schema) 107 - fmt.Printf("database schema synchronised.\n") 108 202 109 203 return db 110 204 }
+44
discord/discord.go
··· 1 + package discord 2 + 3 + const API_ENDPOINT = "https://discord.com/api/v10" 4 + const CLIENT_ID = "1268013769578119208" 5 + // TODO: good GOD change this later please i beg you. we've already broken 6 + // the rules by doing this at all 7 + const CLIENT_SECRET = "JUEZnixhN7BxmLIHmbECiKETMP85VT0E" 8 + const REDIRECT_URI = "https://discord.com/oauth2/authorize?client_id=1268013769578119208&response_type=code&redirect_uri=http%3A%2F%2F127.0.0.1%3A8080%2Fapi%2Fv1%2Fadmin%2Flogin&scope=identify" 9 + // TODO: change before prod 10 + const MY_REDIRECT_URI = "http://127.0.0.1:8080/api/v1/admin/login" 11 + 12 + type ( 13 + AccessTokenResponse struct { 14 + TokenType string `json:"token_type"` 15 + AccessToken string `json:"access_token"` 16 + ExpiresIn int `json:"expires_in"` 17 + RefreshToken string `json:"refresh_token"` 18 + Scope string `json:"scope"` 19 + } 20 + 21 + AuthInfoResponse struct { 22 + Application struct { 23 + Id string 24 + Name string 25 + Icon string 26 + Description string 27 + Hook bool 28 + BotPublic bool 29 + botRequireCodeGrant bool 30 + VerifyKey bool 31 + } 32 + Scopes []string 33 + Expires string 34 + User struct { 35 + Id string 36 + Username string 37 + Avatar string 38 + Discriminator string 39 + GlobalName string 40 + PublicFlags int 41 + } 42 + } 43 + ) 44 +
+34
global/global.go
··· 1 + package global 2 + 3 + import ( 4 + "net/http" 5 + "time" 6 + ) 7 + 8 + var LAST_MODIFIED = time.Now() 9 + 10 + var MimeTypes = map[string]string{ 11 + "css": "text/css; charset=utf-8", 12 + "png": "image/png", 13 + "jpg": "image/jpg", 14 + "webp": "image/webp", 15 + "html": "text/html", 16 + "asc": "text/plain", 17 + "pub": "text/plain", 18 + "js": "application/javascript", 19 + } 20 + 21 + func IsModified(req *http.Request, last_modified time.Time) bool { 22 + if len(req.Header["If-Modified-Since"]) == 0 || len(req.Header["If-Modified-Since"][0]) == 0 { 23 + return true 24 + } 25 + request_time, err := time.Parse(http.TimeFormat, req.Header["If-Modified-Since"][0]) 26 + if err != nil { 27 + return true 28 + } 29 + if request_time.Before(last_modified) { 30 + return true 31 + } 32 + return false 33 + } 34 +
+190 -256
main.go
··· 5 5 "html/template" 6 6 "log" 7 7 "net/http" 8 - "os" 9 8 "strconv" 10 - "strings" 11 9 "time" 12 10 13 - "arimelody.me/arimelody.me/api" 11 + "arimelody.me/arimelody.me/api/v1/admin" 14 12 "arimelody.me/arimelody.me/api/v1/music" 15 - 16 - "github.com/gomarkdown/markdown" 17 - "github.com/gomarkdown/markdown/html" 18 - "github.com/gomarkdown/markdown/parser" 19 13 ) 20 14 21 - const PORT int = 8080 22 - var LAST_MODIFIED = time.Now() 23 - 24 - var mime_types = map[string]string{ 25 - "css": "text/css; charset=utf-8", 26 - "png": "image/png", 27 - "jpg": "image/jpg", 28 - "webp": "image/webp", 29 - "html": "text/html", 30 - "asc": "text/plain", 31 - "pub": "text/plain", 32 - "js": "application/javascript", 33 - } 15 + const DEFAULT_PORT int = 8080 34 16 35 17 var base_template = template.Must(template.ParseFiles( 36 - "views/base.html", 37 - "views/header.html", 38 - "views/footer.html", 39 - "views/prideflag.html", 18 + "views/base.html", 19 + "views/header.html", 20 + "views/footer.html", 21 + "views/prideflag.html", 40 22 )) 41 - // var htmx_template = template.Must(template.New("root").Parse(`<head>{{block "head" .}}{{end}}</head>{{block "content" .}}{{end}}`)) 42 23 43 24 func log_request(req *http.Request, code int, start_time time.Time) { 44 - now := time.Now() 45 - difference := (now.Nanosecond() - start_time.Nanosecond()) / 1_000_000 46 - elapsed := "<1" 47 - if difference >= 1 { 48 - elapsed = strconv.Itoa(difference) 49 - } 50 - 51 - fmt.Printf("[%s] %s %s - %d (%sms) (%s)\n", 52 - now.Format(time.UnixDate), 53 - req.Method, 54 - req.URL.Path, 55 - code, 56 - elapsed, 57 - req.Header["User-Agent"][0], 58 - ) 59 - } 60 - 61 - func handle_request(writer http.ResponseWriter, req *http.Request) { 62 - uri := req.URL.Path 63 - start_time := time.Now() 64 - 65 - // hx_request := len(req.Header["Hx-Request"]) > 0 && req.Header["Hx-Request"][0] == "true" 66 - // 67 - // // don't bother fulfilling requests to a page that's already loaded on the client! 68 - // if hx_request && len(req.Header["Referer"]) > 0 && len(req.Header["Hx-Current-Url"]) > 0 { 69 - // regex := regexp.MustCompile(`https?:\/\/[^\/]+`) 70 - // current_location := regex.ReplaceAllString(req.Header["Hx-Current-Url"][0], "") 71 - // if current_location == req.URL.Path { 72 - // writer.WriteHeader(204); 73 - // return 74 - // } 75 - // } 76 - 77 - writer.Header().Set("Server", "arimelody.me") 78 - writer.Header().Set("Cache-Control", "max-age=86400") 79 - writer.Header().Set("Last-Modified", LAST_MODIFIED.Format(http.TimeFormat)) 80 - 81 - code := func(writer http.ResponseWriter, req *http.Request) int { 82 - // var root *template.Template 83 - // if hx_request { 84 - // root = template.Must(htmx_template.Clone()) 85 - // } else { 86 - // root = template.Must(base_template.Clone()) 87 - // } 88 - 89 - var root = template.Must(base_template.Clone()) 90 - 91 - if req.URL.Path == "/" { 92 - return handle_index(writer, req, root) 93 - } 94 - 95 - if uri == "/music" || uri == "/music/" { 96 - return handle_music(writer, req, root) 97 - } 98 - 99 - if strings.HasPrefix(uri, "/music/") { 100 - return handle_music_gateway(writer, req, root) 101 - } 102 - 103 - if strings.HasPrefix(uri, "/admin") { 104 - return handle_admin(writer, req, root) 105 - } 106 - 107 - if strings.HasPrefix(uri, "/api") { 108 - return api.Handle(writer, req, root) 109 - } 110 - 111 - return static_handler(writer, req, root) 112 - }(writer, req) 113 - 114 - log_request(req, code, start_time) 115 - } 116 - 117 - func handle_index(writer http.ResponseWriter, req *http.Request, root *template.Template) int { 118 - if !was_modified(req, LAST_MODIFIED) { 119 - writer.WriteHeader(304) 120 - return 304 121 - } 122 - 123 - index_template := template.Must(root.ParseFiles("views/index.html")) 124 - err := index_template.Execute(writer, nil) 125 - if err != nil { 126 - http.Error(writer, err.Error(), http.StatusInternalServerError) 127 - return 500 128 - } 129 - return 200 130 - } 131 - 132 - func handle_music(writer http.ResponseWriter, req *http.Request, root *template.Template) int { 133 - if !was_modified(req, LAST_MODIFIED) { 134 - writer.WriteHeader(304) 135 - return 304 136 - } 137 - 138 - music_template := template.Must(root.ParseFiles("views/music.html")) 139 - music := music.QueryAllMusic() 140 - err := music_template.Execute(writer, music) 141 - if err != nil { 142 - http.Error(writer, err.Error(), http.StatusInternalServerError) 143 - return 500 144 - } 145 - return 200 146 - } 147 - 148 - func handle_music_gateway(writer http.ResponseWriter, req *http.Request, root *template.Template) int { 149 - if !was_modified(req, LAST_MODIFIED) { 150 - writer.WriteHeader(304) 151 - return 304 152 - } 153 - 154 - id := req.URL.Path[len("/music/"):] 155 - // http.Redirect(writer, req, "https://mellodoot.com/music/"+title, 302) 156 - // return 157 - release, ok := music.GetRelease(id) 158 - if !ok { 159 - return handle_not_found(writer, req, root) 160 - } 161 - gateway_template := template.Must(root.ParseFiles("views/music-gateway.html")) 162 - err := gateway_template.Execute(writer, release) 163 - if err != nil { 164 - http.Error(writer, err.Error(), http.StatusInternalServerError) 165 - return 500 166 - } 167 - return 200 168 - } 169 - 170 - func handle_admin(writer http.ResponseWriter, req *http.Request, root *template.Template) int { 171 - if !was_modified(req, LAST_MODIFIED) { 172 - writer.WriteHeader(304) 173 - return 304 174 - } 175 - 176 - admin_template := template.Must(root.ParseFiles("views/admin.html")) 177 - err := admin_template.Execute(writer, nil) 178 - if err != nil { 179 - http.Error(writer, err.Error(), http.StatusInternalServerError) 180 - return 500 181 - } 182 - return 200 183 - } 184 - 185 - func static_handler(writer http.ResponseWriter, req *http.Request, root *template.Template) int { 186 - filename := "public/" + req.URL.Path[1:] 187 - 188 - // check the file's metadata 189 - info, err := os.Stat(filename) 190 - if err != nil { 191 - return handle_not_found(writer, req, root) 192 - } 193 - 194 - if !was_modified(req, info.ModTime()) { 195 - writer.WriteHeader(304) 196 - return 304 197 - } 198 - 199 - // set Last-Modified to file modification date 200 - writer.Header().Set("Last-Modified", info.ModTime().Format(http.TimeFormat)) 201 - 202 - // read the file 203 - body, err := os.ReadFile(filename) 204 - if err != nil { 205 - http.Error(writer, err.Error(), http.StatusInternalServerError) 206 - return 500 207 - } 208 - 209 - // setting MIME types 210 - filetype := filename[strings.LastIndex(filename, ".")+1:] 211 - if mime_type, ok := mime_types[filetype]; ok { 212 - writer.Header().Set("Content-Type", mime_type) 213 - } else { 214 - writer.Header().Set("Content-Type", "text/plain; charset=utf-8") 215 - } 216 - 217 - writer.Write([]byte(body)) 218 - return 200 219 - } 220 - 221 - func handle_not_found(writer http.ResponseWriter, req *http.Request, root *template.Template) int { 222 - type ErrorData struct { 223 - Target string 224 - } 225 - error_data := ErrorData{ Target: req.URL.Path } 226 - writer.WriteHeader(404); 227 - error_template := template.Must(root.ParseFiles("views/404.html")) 228 - err := error_template.Execute(writer, error_data) 229 - if err != nil { 230 - http.Error(writer, err.Error(), http.StatusInternalServerError) 231 - return 500 232 - } 233 - return 404 234 - } 235 - 236 - func was_modified(req *http.Request, last_modified time.Time) bool { 237 - if len(req.Header["If-Modified-Since"]) == 0 || len(req.Header["If-Modified-Since"][0]) == 0 { 238 - return true 239 - } 240 - request_time, err := time.Parse(http.TimeFormat, req.Header["If-Modified-Since"][0]) 241 - if err != nil { 242 - return true 243 - } 244 - if request_time.Before(last_modified) { 245 - return true 246 - } 247 - return false 248 - } 249 - 250 - func parse_markdown(md []byte) []byte { 251 - extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock 252 - p := parser.NewWithExtensions(extensions) 253 - doc := p.Parse(md) 254 - 255 - htmlFlags := html.CommonFlags 256 - opts := html.RendererOptions{Flags: htmlFlags} 257 - renderer := html.NewRenderer(opts) 25 + now := time.Now() 26 + difference := (now.Nanosecond() - start_time.Nanosecond()) / 1_000_000 27 + elapsed := "<1" 28 + if difference >= 1 { 29 + elapsed = strconv.Itoa(difference) 30 + } 258 31 259 - return markdown.Render(doc, renderer) 32 + fmt.Printf("[%s] %s %s - %d (%sms) (%s)\n", 33 + now.Format(time.UnixDate), 34 + req.Method, 35 + req.URL.Path, 36 + code, 37 + elapsed, 38 + req.Header["User-Agent"][0]) 260 39 } 261 40 262 - func push_to_db_this_is_a_testing_thing_and_will_be_superfluous_later() { 263 - db := InitDatabase() 264 - defer db.Close() 41 + // func handle_request(res http.ResponseWriter, req *http.Request) { 42 + // uri := req.URL.Path 43 + // start_time := time.Now() 44 + // 45 + // res.Header().Set("Server", "arimelody.me") 46 + // 47 + // code := func(res http.ResponseWriter, req *http.Request) int { 48 + // var root = template.Must(base_template.Clone()) 49 + // 50 + // if req.URL.Path == "/" { 51 + // return handle_index(res, req, root) 52 + // } 53 + // 54 + // if uri == "/music" || uri == "/music/" { 55 + // return handle_music(res, req, root) 56 + // } 57 + // 58 + // if strings.HasPrefix(uri, "/music/") { 59 + // return handle_music_gateway(res, req, root) 60 + // } 61 + // 62 + // if strings.HasPrefix(uri, "/admin") { 63 + // return admin.Handle(res, req, root) 64 + // } 65 + // 66 + // if strings.HasPrefix(uri, "/api") { 67 + // return api.Handle(res, req, root) 68 + // } 69 + // 70 + // return static_handler(res, req, root) 71 + // }(res, req) 72 + // 73 + // log_request(req, code, start_time) 74 + // } 75 + // 76 + // func handle_index(res http.ResponseWriter, req *http.Request, root *template.Template) int { 77 + // if !global.IsModified(req, global.LAST_MODIFIED) { 78 + // res.WriteHeader(304) 79 + // return 304 80 + // } 81 + // 82 + // index_template := template.Must(root.ParseFiles("views/index.html")) 83 + // err := index_template.Execute(res, nil) 84 + // if err != nil { 85 + // http.Error(res, err.Error(), http.StatusInternalServerError) 86 + // return 500 87 + // } 88 + // return 200 89 + // } 90 + // 91 + // func handle_music(res http.ResponseWriter, req *http.Request, root *template.Template) int { 92 + // if !global.IsModified(req, global.LAST_MODIFIED) { 93 + // res.WriteHeader(304) 94 + // return 304 95 + // } 96 + // 97 + // music_template := template.Must(root.ParseFiles("views/music.html")) 98 + // err := music_template.Execute(res, music.Releases) 99 + // if err != nil { 100 + // http.Error(res, err.Error(), http.StatusInternalServerError) 101 + // return 500 102 + // } 103 + // return 200 104 + // } 105 + // 106 + // func handle_music_gateway(res http.ResponseWriter, req *http.Request, root *template.Template) int { 107 + // if !global.IsModified(req, global.LAST_MODIFIED) { 108 + // res.WriteHeader(304) 109 + // return 304 110 + // } 111 + // 112 + // id := req.URL.Path[len("/music/"):] 113 + // release, err := music.GetRelease(id) 114 + // if err != nil { 115 + // return handle_not_found(res, req, root) 116 + // } 117 + // gateway_template := template.Must(root.ParseFiles("views/music-gateway.html")) 118 + // err = gateway_template.Execute(res, release) 119 + // if err != nil { 120 + // http.Error(res, err.Error(), http.StatusInternalServerError) 121 + // return 500 122 + // } 123 + // return 200 124 + // } 125 + // 126 + // func static_handler(res http.ResponseWriter, req *http.Request, root *template.Template) int { 127 + // filename := "public/" + req.URL.Path[1:] 128 + // 129 + // // check the file's metadata 130 + // info, err := os.Stat(filename) 131 + // if err != nil { 132 + // return handle_not_found(res, req, root) 133 + // } 134 + // 135 + // if !global.IsModified(req, info.ModTime()) { 136 + // res.WriteHeader(304) 137 + // return 304 138 + // } 139 + // 140 + // // set Last-Modified to file modification date 141 + // res.Header().Set("Last-Modified", info.ModTime().Format(http.TimeFormat)) 142 + // 143 + // // read the file 144 + // body, err := os.ReadFile(filename) 145 + // if err != nil { 146 + // http.Error(res, err.Error(), http.StatusInternalServerError) 147 + // return 500 148 + // } 149 + // 150 + // // setting MIME types 151 + // filetype := filename[strings.LastIndex(filename, ".")+1:] 152 + // if mime_type, ok := global.MimeTypes[filetype]; ok { 153 + // res.Header().Set("Content-Type", mime_type) 154 + // } else { 155 + // res.Header().Set("Content-Type", "text/plain; charset=utf-8") 156 + // } 157 + // 158 + // res.Write([]byte(body)) 159 + // return 200 160 + // } 161 + // 162 + // func handle_not_found(res http.ResponseWriter, req *http.Request, root *template.Template) int { 163 + // type ErrorData struct { 164 + // Target string 165 + // } 166 + // error_data := ErrorData{ Target: req.URL.Path } 167 + // res.WriteHeader(404); 168 + // error_template := template.Must(root.ParseFiles("views/404.html")) 169 + // err := error_template.Execute(res, error_data) 170 + // if err != nil { 171 + // http.Error(res, err.Error(), http.StatusInternalServerError) 172 + // return 500 173 + // } 174 + // return 404 175 + // } 176 + // 177 + // func parse_markdown(md []byte) []byte { 178 + // extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock 179 + // p := parser.NewWithExtensions(extensions) 180 + // doc := p.Parse(md) 181 + // 182 + // htmlFlags := html.CommonFlags 183 + // opts := html.RendererOptions{Flags: htmlFlags} 184 + // renderer := html.NewRenderer(opts) 185 + // 186 + // return markdown.Render(doc, renderer) 187 + // } 265 188 266 - for _, artist := range music.QueryAllArtists() { 267 - PushArtist(db, artist) 268 - } 189 + func main() { 190 + db := InitDatabase() 191 + defer db.Close() 192 + 193 + var err error 194 + music.Artists, err = PullAllArtists(db) 195 + if err != nil { 196 + fmt.Printf("Failed to pull artists from database: %v\n", err); 197 + panic(1) 198 + } 199 + music.Releases, err = PullAllReleases(db) 200 + if err != nil { 201 + fmt.Printf("Failed to pull releases from database: %v\n", err); 202 + panic(1) 203 + } 269 204 270 - for _, album := range music.QueryAllMusic() { 271 - PushRelease(db, album) 272 - } 273 - } 205 + mux := http.NewServeMux() 274 206 275 - func main() { 276 - push_to_db_this_is_a_testing_thing_and_will_be_superfluous_later() 207 + mux.Handle("/api/v1/admin", admin.Handler()) 208 + mux.Handle("/api/v1/admin/login", admin.LoginHandler()) 209 + mux.Handle("/api/v1/admin/callback", admin.OAuthCallbackHandler()) 210 + mux.Handle("/api/v1/admin/verify", admin.AuthorisedHandler(admin.VerifyHandler())) 211 + mux.Handle("/api/v1/admin/logout", admin.AuthorisedHandler(admin.LogoutHandler())) 277 212 278 - http.HandleFunc("/", handle_request) 279 - 280 - fmt.Printf("now serving at http://127.0.0.1:%d\n", PORT) 281 - log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", PORT), nil)) 213 + port := DEFAULT_PORT 214 + fmt.Printf("now serving at http://127.0.0.1:%d\n", port) 215 + log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), mux)) 282 216 }
public/img/buttons/elke.gif

This is a binary file and will not be displayed.

public/img/buttons/itzzen.png

This is a binary file and will not be displayed.

public/img/buttons/misc/blink.gif

This is a binary file and will not be displayed.

public/img/favicon-256.png

This is a binary file and will not be displayed.

public/img/favicon-36.png

This is a binary file and will not be displayed.

public/img/favicon.png

This is a binary file and will not be displayed.

+10 -1
public/img/mailicon.svg
··· 1 - <?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 267 267" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><use id="Background" xlink:href="#_Image1" x="0" y="0" width="648px" height="648px" transform="matrix(0.998523,0,0,0.998523,0,0)"/><defs><image id="_Image1" width="267px" height="267px" xlink:href="data:image/jpeg;base64,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"/></defs></svg> 1 + <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 + <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> 3 + <svg width="100%" height="100%" viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"> 4 + <g id="favicon" transform="matrix(0.237037,0,0,0.237037,0,0)"> 5 + <use xlink:href="#_Image1" x="0" y="0" width="1080px" height="1080px" transform="matrix(4.21875,0,0,4.21875,0,0)"/> 6 + </g> 7 + <defs> 8 + <image id="_Image1" width="256px" height="256px" xlink:href="data:image/jpeg;base64,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"/> 9 + </defs> 10 + </svg>
+32
public/img/prideflag.svg
··· 1 + <!-- 2 + pride flag - copyright (c) 2024 ari melody 3 + 4 + this code is provided AS-IS, WITHOUT ANY WARRANTY, to be 5 + freely redistributed and/or modified as you please, however 6 + retaining this license in any redistribution. 7 + 8 + please use this flag to link to an LGBTQI+-supporting page 9 + of your choosing! 10 + 11 + web: https://arimelody.me 12 + source: https://git.arimelody.me/ari/prideflag 13 + --> 14 + 15 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120"> 16 + <path id="red" d="M120,80 L100,100 L120,120 Z" style="fill:#d20605"/> 17 + <path id="orange" d="M120,80 V40 L80,80 L100,100 Z" style="fill:#ef9c00"/> 18 + <path id="yellow" d="M120,40 V0 L60,60 L80,80 Z" style="fill:#e5fe02"/> 19 + <path id="green" d="M120,0 H80 L40,40 L60,60 Z" style="fill:#09be01"/> 20 + <path id="blue" d="M80,0 H40 L20,20 L40,40 Z" style="fill:#081a9a"/> 21 + <path id="purple" d="M40,0 H0 L20,20 Z" style="fill:#76008a"/> 22 + 23 + <rect id="black" x="60" width="60" height="60" style="fill:#010101"/> 24 + <rect id="brown" x="70" width="50" height="50" style="fill:#603814"/> 25 + <rect id="lightblue" x="80" width="40" height="40" style="fill:#73d6ed"/> 26 + <rect id="pink" x="90" width="30" height="30" style="fill:#ffafc8"/> 27 + <rect id="white" x="100" width="20" height="20" style="fill:#fff"/> 28 + 29 + <rect id="intyellow" x="110" width="10" height="10" style="fill:#fed800"/> 30 + <circle id="intpurple" cx="120" cy="0" r="5" stroke="#7601ad" stroke-width="2" fill="none"/> 31 + </svg> 32 +
+56 -62
public/script/music-gateway.js
··· 1 1 import "./main.js"; 2 2 3 3 function apply_funny_bob_to_upcoming_tags() { 4 - const upcoming_tags = document.querySelectorAll("#type.upcoming"); 5 - for (var i = 0; i < upcoming_tags.length; i++) { 6 - const tag = upcoming_tags[i]; 7 - const chars = tag.innerText.split(""); 8 - const result = chars.map((c, i) => `<span style="animation-delay: -${i * 100}ms;">${c}</span>`); 9 - tag.innerHTML = result.join(""); 10 - } 4 + const upcoming_tags = document.querySelectorAll("#type.upcoming"); 5 + for (var i = 0; i < upcoming_tags.length; i++) { 6 + const tag = upcoming_tags[i]; 7 + const chars = tag.innerText.split(""); 8 + const result = chars.map((c, i) => `<span style="animation-delay: -${i * 100}ms;">${c}</span>`); 9 + tag.innerHTML = result.join(""); 10 + } 11 11 } 12 12 13 13 function update_extras_buttons() { 14 - const extras_pairs = Array.from(document.querySelectorAll("div#info > div").values()).map(container => { 15 - return { 16 - container, 17 - button: document.getElementById("extras").querySelector(`ul li a[href="#${container.id}"]`) 18 - }; 19 - }); 20 - const info_container = document.getElementById("info") 21 - info_container.addEventListener("scroll", update_extras_buttons); 22 - const info_rect = info_container.getBoundingClientRect(); 23 - const info_y = info_rect.y; 24 - const font_size = parseFloat(getComputedStyle(document.documentElement).fontSize); 25 - let current = extras_pairs[0]; 26 - extras_pairs.forEach(pair => { 27 - pair.button.classList.remove("active"); 28 - const scroll_diff = pair.container.getBoundingClientRect().y - 29 - info_rect.y - 30 - info_rect.height / 2 + 31 - 4 * font_size; 32 - if (scroll_diff <= 0) current = pair; 33 - }) 34 - current.button.classList.add("active"); 14 + const extras_pairs = Array.from(document.querySelectorAll("div#info > div").values()).map(container => { 15 + return { 16 + container, 17 + button: document.getElementById("extras").querySelector(`ul li a[href="#${container.id}"]`) 18 + }; 19 + }); 20 + const info_container = document.getElementById("info") 21 + info_container.addEventListener("scroll", update_extras_buttons); 22 + const info_rect = info_container.getBoundingClientRect(); 23 + const info_y = info_rect.y; 24 + const font_size = parseFloat(getComputedStyle(document.documentElement).fontSize); 25 + let current = extras_pairs[0]; 26 + extras_pairs.forEach(pair => { 27 + pair.button.classList.remove("active"); 28 + const scroll_diff = pair.container.getBoundingClientRect().y - 29 + info_rect.y - 30 + info_rect.height / 2 + 31 + 4 * font_size; 32 + if (scroll_diff <= 0) current = pair; 33 + }) 34 + current.button.classList.add("active"); 35 35 36 - document.querySelectorAll("div#extras ul li a[href]").forEach(link => { 37 - link.addEventListener("click", event => { 38 - event.preventDefault(); 39 - location.replace(link.href); 40 - }); 36 + document.querySelectorAll("div#extras ul li a[href]").forEach(link => { 37 + link.addEventListener("click", event => { 38 + event.preventDefault(); 39 + const tag = link.href.split('#').slice(-1)[0]; 40 + document.getElementById(tag).scrollIntoView(); 41 41 }); 42 + }); 42 43 } 43 44 44 45 function bind_go_back_btn() { 45 - const go_back_btn = document.getElementById("go-back") 46 - go_back_btn.innerText = "<"; 47 - go_back_btn.addEventListener("click", () => { 48 - window.history.back(); 49 - }); 46 + const go_back_btn = document.getElementById("go-back") 47 + go_back_btn.innerText = "<"; 48 + go_back_btn.addEventListener("click", () => { 49 + window.history.back(); 50 + }); 50 51 } 51 52 52 53 function bind_share_btn() { 53 - const share_btn = document.getElementById("share"); 54 - if (navigator.clipboard === undefined) { 55 - share_btn.onclick = event => { 56 - console.error("clipboard is not supported by this browser!"); 57 - }; 58 - return; 59 - } 54 + const share_btn = document.getElementById("share"); 55 + if (navigator.clipboard === undefined) { 60 56 share_btn.onclick = event => { 61 - event.preventDefault(); 62 - navigator.clipboard.writeText(window.location.href); 63 - share_btn.classList.remove('active'); 64 - void share_btn.offsetWidth; 65 - share_btn.classList.add('active'); 66 - } 57 + console.error("clipboard is not supported by this browser!"); 58 + }; 59 + return; 60 + } 61 + share_btn.onclick = event => { 62 + event.preventDefault(); 63 + navigator.clipboard.writeText(window.location.href); 64 + share_btn.classList.remove('active'); 65 + void share_btn.offsetWidth; 66 + share_btn.classList.add('active'); 67 + } 67 68 } 68 69 69 - function start() { 70 - bind_share_btn(); 71 - bind_go_back_btn(); 72 - apply_funny_bob_to_upcoming_tags(); 73 - update_extras_buttons(); 74 - } 75 - 76 - document.addEventListener("swap", () => { 77 - if (!window.location.pathname.startsWith("/music/")) return; 78 - start(); 79 - }); 70 + bind_share_btn(); 71 + bind_go_back_btn(); 72 + apply_funny_bob_to_upcoming_tags(); 73 + update_extras_buttons();
+380 -379
public/style/music-gateway.css
··· 1 1 @font-face { 2 - font-family: "Monaspace Argon"; 3 - src: url("/font/monaspace-argon/MonaspaceArgonVarVF[wght,wdth,slnt].woff2") format("woff2-variations"); 4 - font-weight: 125 950; 5 - font-stretch: 75% 125%; 6 - font-style: oblique 0deg 20deg; 2 + font-family: "Monaspace Argon"; 3 + src: url("/font/monaspace-argon/MonaspaceArgonVarVF[wght,wdth,slnt].woff2") format("woff2-variations"); 4 + font-weight: 125 950; 5 + font-stretch: 75% 125%; 6 + font-style: oblique 0deg 20deg; 7 7 } 8 8 9 9 html, 10 10 body { 11 - margin: 0; 12 - padding: 0; 13 - font-size: 18px; 14 - color: #fff; 15 - background-color: #111; 16 - font-family: "Monaspace Argon", monospace; 11 + margin: 0; 12 + padding: 0; 13 + font-size: 16px; 14 + line-height: 1.5em; 15 + color: #fff; 16 + background-color: #111; 17 + font-family: "Monaspace Argon", monospace; 17 18 } 18 19 19 20 #background { 20 - position: fixed; 21 - top: 0; 22 - left: 0; 23 - width: 100vw; 24 - height: 100vh; 25 - background-size: cover; 26 - background-position: center; 27 - filter: blur(25px) saturate(25%) brightness(0.5); 28 - -webkit-filter: blur(25px) saturate(25%) brightness(0.5);; 29 - animation: background-init .5s forwards,background-loop 30s ease-in-out infinite 21 + position: fixed; 22 + top: 0; 23 + left: 0; 24 + width: 100vw; 25 + height: 100vh; 26 + background-size: cover; 27 + background-position: center; 28 + filter: blur(25px) saturate(25%) brightness(0.5); 29 + -webkit-filter: blur(25px) saturate(25%) brightness(0.5);; 30 + animation: background-init .5s forwards,background-loop 30s ease-in-out infinite 30 31 } 31 32 32 33 #go-back { 33 - position: fixed; 34 - top: 4rem; 35 - left: 1rem; 36 - width: 2.5rem; 37 - height: 2.5rem; 38 - margin: 0; 39 - font-size: 1.5rem; 40 - line-height: 2.6rem; 41 - display: flex; 42 - justify-content: center; 43 - color: #fff; 44 - background-color: #111; 45 - border-radius: 4px; 46 - text-decoration: none; 47 - box-shadow: 0 0 8px #0004; 48 - z-index: 1; 34 + position: fixed; 35 + top: 4rem; 36 + left: 1rem; 37 + width: 2.5rem; 38 + height: 2.5rem; 39 + margin: 0; 40 + font-size: 1.5rem; 41 + line-height: 2.6rem; 42 + display: flex; 43 + justify-content: center; 44 + color: #fff; 45 + background-color: #111; 46 + border-radius: 4px; 47 + text-decoration: none; 48 + box-shadow: 0 0 8px #0004; 49 + z-index: 1; 49 50 } 50 51 51 52 main { 52 - position: absolute; 53 - top: 3rem; 54 - width: 100vw; 55 - min-height: calc(100vh - 9rem); 56 - margin: 0; 57 - display: flex; 58 - justify-content: center; 59 - align-items: center; 53 + position: absolute; 54 + top: 3rem; 55 + width: 100vw; 56 + min-height: calc(100vh - 9rem); 57 + margin: 0; 58 + display: flex; 59 + justify-content: center; 60 + align-items: center; 60 61 } 61 62 62 63 #music-container { 63 - width: min(960px, calc(100vw - 4rem)); 64 - height: 360px; 65 - display: flex; 66 - flex-direction: row; 67 - flex-wrap: nowrap; 68 - gap: 4rem; 69 - font-size: 16px; 70 - animation: card-init .5s forwards; 71 - padding: 4rem; 64 + width: min(960px, calc(100vw - 4rem)); 65 + height: 360px; 66 + display: flex; 67 + flex-direction: row; 68 + flex-wrap: nowrap; 69 + gap: 4rem; 70 + font-size: 16px; 71 + animation: card-init .5s forwards; 72 + padding: 4rem; 72 73 } 73 74 74 75 #art-container { 75 - display: flex; 76 - align-items: center; 77 - position: relative; 78 - width: 20rem; 79 - transition: transform .5s cubic-bezier(0,0,0,1); 76 + display: flex; 77 + align-items: center; 78 + position: relative; 79 + width: 20rem; 80 + transition: transform .5s cubic-bezier(0,0,0,1); 80 81 } 81 82 82 83 #art-container:hover { 83 - transform: scale(1.05) translateY(-5px); 84 + transform: scale(1.05) translateY(-5px); 84 85 } 85 86 86 87 #art-container:hover img#artwork { 87 - box-shadow: 0 16px 18px #0003; 88 + box-shadow: 0 16px 18px #0003; 88 89 } 89 90 90 91 /* TILT CONTROLS */ 91 92 92 93 #art-container > div { 93 - position: absolute; 94 - width: 33.3%; 95 - height: 33.3%; 96 - z-index: 2; 94 + position: absolute; 95 + width: 33.3%; 96 + height: 33.3%; 97 + z-index: 2; 97 98 } 98 99 99 100 #art-container > div.tilt-topleft { 100 - top: 0; 101 - left: 0; 101 + top: 0; 102 + left: 0; 102 103 } 103 104 104 105 #art-container > div.tilt-topleft:hover ~ #artwork { 105 - transform: rotate3d(-1, 1, 0, 20deg); 106 - filter: brightness(var(--shine)); 107 - -webkit-filter: brightness(var(--shine)); 106 + transform: rotate3d(-1, 1, 0, 20deg); 107 + filter: brightness(var(--shine)); 108 + -webkit-filter: brightness(var(--shine)); 108 109 } 109 110 110 111 #art-container > div.tilt-top { 111 - top: 0; 112 - left: 33.3%; 112 + top: 0; 113 + left: 33.3%; 113 114 } 114 115 115 116 #art-container > div.tilt-top:hover ~ #artwork { 116 - transform: rotate3d(-1, 0, 0, 20deg); 117 - filter: brightness(var(--shine)); 118 - -webkit-filter: brightness(var(--shine)); 117 + transform: rotate3d(-1, 0, 0, 20deg); 118 + filter: brightness(var(--shine)); 119 + -webkit-filter: brightness(var(--shine)); 119 120 } 120 121 121 122 #art-container > div.tilt-topright { 122 - top: 0; 123 - right: 0; 123 + top: 0; 124 + right: 0; 124 125 } 125 126 126 127 #art-container > div.tilt-topright:hover ~ #artwork { 127 - transform: rotate3d(-1, -1, 0, 20deg); 128 - filter: brightness(var(--shine)); 129 - -webkit-filter: brightness(var(--shine)); 128 + transform: rotate3d(-1, -1, 0, 20deg); 129 + filter: brightness(var(--shine)); 130 + -webkit-filter: brightness(var(--shine)); 130 131 } 131 132 132 133 #art-container > div.tilt-right { 133 - top: 33.3%; 134 - right: 0; 134 + top: 33.3%; 135 + right: 0; 135 136 } 136 137 137 138 #art-container > div.tilt-right:hover ~ #artwork { 138 - transform: rotate3d(0, -1, 0, 20deg); 139 + transform: rotate3d(0, -1, 0, 20deg); 139 140 } 140 141 141 142 #art-container > div.tilt-bottomright { 142 - bottom: 0; 143 - right: 0; 143 + bottom: 0; 144 + right: 0; 144 145 } 145 146 146 147 #art-container > div.tilt-bottomright:hover ~ #artwork { 147 - transform: rotate3d(1, -1, 0, 20deg); 148 - filter: brightness(var(--shadow)); 149 - -webkit-filter: brightness(var(--shadow)); 148 + transform: rotate3d(1, -1, 0, 20deg); 149 + filter: brightness(var(--shadow)); 150 + -webkit-filter: brightness(var(--shadow)); 150 151 } 151 152 152 153 #art-container > div.tilt-bottom { 153 - bottom: 0; 154 - left: 33.3%; 154 + bottom: 0; 155 + left: 33.3%; 155 156 } 156 157 157 158 #art-container > div.tilt-bottom:hover ~ #artwork { 158 - transform: rotate3d(1, 0, 0, 20deg); 159 - filter: brightness(var(--shadow)); 160 - -webkit-filter: brightness(var(--shadow)); 159 + transform: rotate3d(1, 0, 0, 20deg); 160 + filter: brightness(var(--shadow)); 161 + -webkit-filter: brightness(var(--shadow)); 161 162 } 162 163 163 164 #art-container > div.tilt-bottomleft { 164 - bottom: 0; 165 - left: 0; 165 + bottom: 0; 166 + left: 0; 166 167 } 167 168 168 169 #art-container > div.tilt-bottomleft:hover ~ #artwork { 169 - transform: rotate3d(1, 1, 0, 20deg); 170 - filter: brightness(var(--shadow)); 171 - -webkit-filter: brightness(var(--shadow)); 170 + transform: rotate3d(1, 1, 0, 20deg); 171 + filter: brightness(var(--shadow)); 172 + -webkit-filter: brightness(var(--shadow)); 172 173 } 173 174 174 175 #art-container > div.tilt-left { 175 - top: 33.3%; 176 - left: 0; 176 + top: 33.3%; 177 + left: 0; 177 178 } 178 179 179 180 #art-container > div.tilt-left:hover ~ #artwork { 180 - transform: rotate3d(0, 1, 0, 20deg); 181 + transform: rotate3d(0, 1, 0, 20deg); 181 182 } 182 183 183 184 /* TILT CONTROLS */ 184 185 185 186 #artwork { 186 - --shine: 1.05; 187 - --shadow: 0.8; 188 - width: 360px; 189 - height: 360px; 190 - margin: auto; 191 - display: flex; 192 - justify-content: center; 193 - line-height: 15rem; 194 - border-radius: 4px; 195 - color: #888; 196 - background-color: #ccc; 197 - box-shadow: 0 16px 16px #0004; 198 - transition: transform 1s cubic-bezier(0,0,0,1), filter .3s linear, box-shadow .3s linear; 187 + --shine: 1.05; 188 + --shadow: 0.8; 189 + width: 360px; 190 + height: 360px; 191 + margin: auto; 192 + display: flex; 193 + justify-content: center; 194 + line-height: 15rem; 195 + border-radius: 4px; 196 + color: #888; 197 + background-color: #ccc; 198 + box-shadow: 0 16px 16px #0004; 199 + transition: transform 1s cubic-bezier(0,0,0,1), filter .3s linear, box-shadow .3s linear; 199 200 } 200 201 201 202 div#vertical-line { 202 - width: 1px; 203 - background-color: #fff4; 203 + width: 1px; 204 + background-color: #fff4; 204 205 } 205 206 206 207 div#info { 207 - margin: -4rem; 208 - padding: 4rem; 209 - height: 360px; 210 - overflow: scroll; 211 - scroll-behavior: smooth; 212 - mask-image: linear-gradient(to bottom, transparent 0%, black 10%, black 90%, transparent 100%); 208 + margin: -4rem; 209 + padding: 4rem; 210 + height: 360px; 211 + overflow: scroll; 212 + scroll-behavior: smooth; 213 + mask-image: linear-gradient(to bottom, transparent 0%, black 10%, black 90%, transparent 100%); 213 214 214 - -ms-overflow-style: none; 215 - scrollbar-width: none; 216 - ::-webkit-scrollbar { 217 - display: none; 218 - } 215 + -ms-overflow-style: none; 216 + scrollbar-width: none; 217 + ::-webkit-scrollbar { 218 + display: none; 219 + } 219 220 } 220 221 221 222 div#info > div { 222 - min-width: 420px; 223 - min-height: 360px; 224 - padding: 4rem 1rem 4rem 4rem; 225 - margin: -4rem -3.5rem -4rem -4rem; 223 + min-width: 420px; 224 + min-height: 360px; 225 + padding: 4rem 1rem 4rem 4rem; 226 + margin: -4rem -3.5rem -4rem -4rem; 226 227 } 227 228 228 229 div#info p { 229 - max-width: 500px; 230 - white-space: pre-line; 230 + max-width: 500px; 231 + white-space: pre-line; 231 232 } 232 233 233 234 #title-container { 234 - display: flex; 235 - align-items: last baseline; 236 - flex-direction: row; 235 + display: flex; 236 + align-items: last baseline; 237 + flex-direction: row; 237 238 } 238 239 239 240 #title { 240 - margin: 0; 241 - line-height: 1em; 242 - font-size: 2.5em; 241 + margin: 0; 242 + line-height: 1em; 243 + font-size: 2.5em; 243 244 } 244 245 245 246 #year { 246 - margin-left: .9em; 247 - font-size: 1.2em; 248 - color: #eee8; 249 - font-weight: bold; 247 + margin-left: .9em; 248 + font-size: 1.2em; 249 + color: #eee8; 250 + font-weight: bold; 250 251 } 251 252 252 253 #artist { 253 - margin: .2em 0 1em 0; 254 - font-size: 1em; 255 - color: #eee; 254 + margin: .2em 0 1em 0; 255 + font-size: 1em; 256 + color: #eee; 256 257 } 257 258 258 259 #title, 259 260 #artist { 260 - text-shadow: 0 .05em 2px #0004 261 + text-shadow: 0 .05em 2px #0004 261 262 } 262 263 263 264 #type { 264 - display: inline-block; 265 - margin: 0; 266 - padding: .3em .8em; 267 - color: #fff; 268 - background-color: #111; 269 - text-transform: uppercase; 270 - border-radius: 4px; 265 + display: inline-block; 266 + margin: 0; 267 + padding: .3em .8em; 268 + color: #fff; 269 + background-color: #111; 270 + text-transform: uppercase; 271 + border-radius: 4px; 271 272 } 272 273 273 274 #type.single { 274 - background-color: #3b47f4; 275 + background-color: #3b47f4; 275 276 } 276 277 277 278 #type.ep { 278 - background-color: #f419bd 279 + background-color: #f419bd 279 280 } 280 281 281 282 #type.album { 282 - background-color: #34c627 283 + background-color: #34c627 283 284 } 284 285 285 286 #type.comp { 286 - background-color: #ee8d46 287 + background-color: #ee8d46 287 288 } 288 289 289 290 #type.upcoming { 290 - background-color: #ff3e3e 291 + background-color: #ff3e3e 291 292 } 292 293 293 294 #type.upcoming span { 294 - animation: bob 2s ease-in-out infinite; 295 - display: inline-block; 295 + animation: bob 2s ease-in-out infinite; 296 + display: inline-block; 296 297 } 297 298 298 299 ul#links { 299 - width: 100%; 300 - margin: 1rem 0; 301 - padding: 0; 302 - display: flex; 303 - flex-direction: row; 304 - flex-wrap: wrap; 305 - gap: .5rem; 306 - list-style: none; 300 + width: 100%; 301 + margin: 1rem 0; 302 + padding: 0; 303 + display: flex; 304 + flex-direction: row; 305 + flex-wrap: wrap; 306 + gap: .5rem; 307 + list-style: none; 307 308 } 308 309 309 310 ul#links li { 310 - flex-grow: 1; 311 + flex-grow: 1; 311 312 } 312 313 313 314 ul#links a { 314 - width: calc(100% - 1.6em); 315 - padding: .5em .8em; 316 - display: block; 317 - border-radius: 4px; 318 - font-size: 1em; 319 - color: #111; 320 - background-color: #fff; 321 - text-align: center; 322 - text-decoration: none; 323 - transition: filter .1s,-webkit-filter .1s 315 + width: calc(100% - 1.6em); 316 + padding: .5em .8em; 317 + display: block; 318 + border-radius: 4px; 319 + font-size: 1em; 320 + color: #111; 321 + background-color: #fff; 322 + text-align: center; 323 + text-decoration: none; 324 + transition: filter .1s,-webkit-filter .1s 324 325 } 325 326 326 327 ul#links a.buy { 327 - background-color: #ff94e9 328 + background-color: #ff94e9 328 329 } 329 330 330 331 ul#links a.presave { 331 - background-color: #ff94e9 332 + background-color: #ff94e9 332 333 } 333 334 334 335 ul#links a.spotify { 335 - background-color: #8cff83 336 + background-color: #8cff83 336 337 } 337 338 338 339 ul#links a.applemusic { 339 - background-color: #8cd9ff 340 + background-color: #8cd9ff 340 341 } 341 342 342 343 ul#links a.soundcloud { 343 - background-color: #fdaa6d 344 + background-color: #fdaa6d 344 345 } 345 346 346 347 ul#links a.youtube { 347 - background-color: #ff6e6e 348 + background-color: #ff6e6e 348 349 } 349 350 350 351 ul#links a:hover { 351 - filter: brightness(125%); 352 - -webkit-filter: brightness(125%) 352 + filter: brightness(125%); 353 + -webkit-filter: brightness(125%) 353 354 } 354 355 355 356 #description { 356 - font-size: 1.2em; 357 + font-size: 1.2em; 357 358 } 358 359 359 360 #share { 360 - margin: 0; 361 - padding: 0; 362 - display: inline-block; 363 - color: inherit; 364 - font-family: inherit; 365 - font-size: inherit; 366 - line-height: 1em; 367 - text-shadow: 0 .05em 2px #0004; 368 - cursor: pointer; 369 - opacity: .5; 370 - background: none; 371 - border: none; 372 - transition: opacity .1s linear; 361 + margin: 0; 362 + padding: 0; 363 + display: inline-block; 364 + color: inherit; 365 + font-family: inherit; 366 + font-size: inherit; 367 + line-height: 1em; 368 + text-shadow: 0 .05em 2px #0004; 369 + cursor: pointer; 370 + opacity: .5; 371 + background: none; 372 + border: none; 373 + transition: opacity .1s linear; 373 374 } 374 375 375 376 #share:hover { 376 - opacity: 1; 377 + opacity: 1; 377 378 } 378 379 379 380 #share.active { 380 - animation: share-click .5s forwards 381 + animation: share-click .5s forwards 381 382 } 382 383 383 384 #share.active::after { 384 - content: "✓"; 385 - position: relative; 386 - left: .2em; 387 - font-size: 1.2em; 388 - line-height: 1em; 389 - animation: share-after 2s cubic-bezier(.5,0,1,.5) forwards 385 + content: "✓"; 386 + position: relative; 387 + left: .2em; 388 + font-size: 1.2em; 389 + line-height: 1em; 390 + animation: share-after 2s cubic-bezier(.5,0,1,.5) forwards 390 391 } 391 392 392 393 div#extras ul { 393 - height: 100%; 394 - display: flex; 395 - padding: 0; 396 - flex-direction: column; 397 - margin: 0 -.5rem; 398 - position: relative; 394 + height: 100%; 395 + display: flex; 396 + padding: 0; 397 + flex-direction: column; 398 + margin: 0 -.5rem; 399 + position: relative; 399 400 } 400 401 401 402 div#extras ul li { 402 - flex-grow: 1; 403 - display: flex; 404 - width: 0; 403 + flex-grow: 1; 404 + display: flex; 405 + width: 0; 405 406 } 406 407 407 408 div#extras ul li a { 408 - padding: 0 .5rem; 409 - writing-mode: vertical-rl; 410 - text-align: center; 411 - color: #888; 412 - text-decoration: none; 413 - font-style: italic; 414 - transition: color .1s linear; 409 + padding: 0 .5rem; 410 + writing-mode: vertical-rl; 411 + text-align: center; 412 + color: #888; 413 + text-decoration: none; 414 + font-style: italic; 415 + transition: color .1s linear; 415 416 } 416 417 417 418 div#extras ul li a:hover, 418 419 div#extras ul li a.active { 419 - color: #eee; 420 + color: #eee; 420 421 } 421 422 422 423 #credits ul { 423 - list-style: "> "; 424 + list-style: "> "; 424 425 } 425 426 426 427 #credits ul li { 427 - margin-bottom: 1rem; 428 + margin-bottom: 1rem; 428 429 } 429 430 430 - #tracks h3 { 431 - margin-bottom: .5rem; 432 - padding: 1rem; 433 - background-color: #0008; 434 - cursor: pointer; 435 - transition: background-color .1s linear; 431 + #tracks details[open] { 432 + margin-bottom: 2em; 436 433 } 437 434 438 - #tracks h3:hover { 439 - background-color: #2228; 435 + #tracks summary { 436 + margin-bottom: 1em; 437 + padding: 1em; 438 + background-color: #0008; 439 + cursor: pointer; 440 + transition: background-color .1s linear; 440 441 } 441 442 442 - #tracks h3:active { 443 - background-color: #4448; 443 + #tracks summary:hover { 444 + background-color: #2228; 444 445 } 445 446 446 - #tracks h3.active { 447 - color: #000; 448 - background-color: var(--primary); 447 + #tracks summary:active { 448 + background-color: #4448; 449 449 } 450 450 451 - #tracks p { 452 - margin-bottom: 3em; 451 + #tracks summary.active { 452 + color: #000; 453 + background-color: var(--primary); 453 454 } 454 455 455 456 footer { 456 - position: fixed; 457 - left: 0; 458 - bottom: 0; 459 - width: 100vw; 460 - height: 6rem; 461 - display: flex; 462 - flex-direction: column; 463 - justify-content: center; 464 - align-items: center; 465 - color: #eee; 466 - background-color: #0008; 467 - backdrop-filter: blur(25px) 457 + position: fixed; 458 + left: 0; 459 + bottom: 0; 460 + width: 100vw; 461 + height: 6rem; 462 + display: flex; 463 + flex-direction: column; 464 + justify-content: center; 465 + align-items: center; 466 + color: #eee; 467 + background-color: #0008; 468 + backdrop-filter: blur(25px) 468 469 } 469 470 470 471 footer p { 471 - margin: 0 472 + margin: 0 472 473 } 473 474 474 475 footer a { 475 - color: inherit; 476 - text-decoration: none 476 + color: inherit; 477 + text-decoration: none 477 478 } 478 479 479 480 footer a:hover { 480 - text-decoration: underline 481 + text-decoration: underline 481 482 } 482 483 483 484 @media only screen and (max-width: 1105px) { 484 - main { 485 - min-height: calc(100vh - 4rem); 486 - } 485 + main { 486 + min-height: calc(100vh - 4rem); 487 + } 487 488 488 - #music-container { 489 - width: calc(100vw - 8rem); 490 - height: fit-content; 491 - padding: 2rem 0 6rem 0; 492 - gap: 1rem; 493 - font-size: 12px; 494 - flex-direction: column; 495 - text-align: center; 496 - } 489 + #music-container { 490 + width: calc(100vw - 8rem); 491 + height: fit-content; 492 + padding: 2rem 0 6rem 0; 493 + gap: 1rem; 494 + font-size: 12px; 495 + flex-direction: column; 496 + text-align: center; 497 + } 497 498 498 - #art-container { 499 - width: 100%; 500 - margin-bottom: 2rem; 501 - } 499 + #art-container { 500 + width: 100%; 501 + margin-bottom: 2rem; 502 + } 502 503 503 - #artwork { 504 - width: auto; 505 - max-width: 50vw; 506 - height: auto; 507 - max-height: 50vh; 508 - } 504 + #artwork { 505 + width: auto; 506 + max-width: 50vw; 507 + height: auto; 508 + max-height: 50vh; 509 + } 509 510 510 - #vertical-line { 511 - display: none; 512 - } 511 + #vertical-line { 512 + display: none; 513 + } 513 514 514 - div#info { 515 - width: 100%; 516 - gap: 2rem; 517 - height: auto; 518 - overflow-y: auto; 519 - } 515 + div#info { 516 + width: 100%; 517 + gap: 2rem; 518 + height: auto; 519 + overflow-y: auto; 520 + } 520 521 521 - div#info > div { 522 - min-width: auto; 523 - min-height: auto; 524 - padding: 0; 525 - margin: 0; 526 - overflow-y: unset; 527 - mask-image: none; 528 - } 522 + div#info > div { 523 + min-width: auto; 524 + min-height: auto; 525 + padding: 0; 526 + margin: 0; 527 + overflow-y: unset; 528 + mask-image: none; 529 + } 529 530 530 - div#info p { 531 - margin: 0 auto; 532 - } 531 + div#info p { 532 + margin: 0 auto; 533 + } 533 534 534 - div#extras { 535 - display: none; 536 - } 535 + div#extras { 536 + display: none; 537 + } 537 538 538 - #title-container { 539 - flex-direction: column; 540 - align-items: center; 541 - } 539 + #title-container { 540 + flex-direction: column; 541 + align-items: center; 542 + } 542 543 543 - #year { 544 - display: block; 545 - margin: 0; 546 - } 544 + #year { 545 + display: block; 546 + margin: 0; 547 + } 547 548 548 - #artist { 549 - margin: .2em auto 1em auto; 550 - } 549 + #artist { 550 + margin: .2em auto 1em auto; 551 + } 551 552 552 - #links { 553 - margin: 2rem 0; 554 - justify-content: center; 555 - } 553 + #links { 554 + margin: 2rem 0; 555 + justify-content: center; 556 + } 556 557 557 - #extras { 558 - justify-content: center; 559 - } 558 + #extras { 559 + justify-content: center; 560 + } 560 561 561 - #credits ul { 562 - padding: 0; 563 - list-style: none; 564 - } 562 + #credits ul { 563 + padding: 0; 564 + list-style: none; 565 + } 565 566 566 - #share.active: : after { 567 - transform: translate(calc(-50% - .6em),1.5em); 568 - } 567 + #share.active: : after { 568 + transform: translate(calc(-50% - .6em),1.5em); 569 + } 569 570 570 - footer { 571 - height: 4rem; 572 - font-size: .8rem; 573 - } 571 + footer { 572 + height: 4rem; 573 + font-size: .8rem; 574 + } 574 575 575 - #pride-flag { 576 - display: none; 577 - } 576 + #pride-flag { 577 + display: none; 578 + } 578 579 579 580 } 580 581 581 582 @keyframes background-init { 582 - from { 583 - opacity: 0 584 - } 585 - to { 586 - opacity: 1 587 - } 583 + from { 584 + opacity: 0 585 + } 586 + to { 587 + opacity: 1 588 + } 588 589 } 589 590 590 591 @keyframes background-loop { 591 - from { 592 - transform: scale(1) 593 - } 594 - 50% { 595 - transform: scale(1.05) 596 - } 597 - to { 598 - transform: scale(1) 599 - } 592 + from { 593 + transform: scale(1) 594 + } 595 + 50% { 596 + transform: scale(1.05) 597 + } 598 + to { 599 + transform: scale(1) 600 + } 600 601 } 601 602 602 603 @keyframes card-init { 603 - from { 604 - opacity: 0; 605 - transform: scale(.9) 606 - } 607 - to { 608 - opacity: 1; 609 - transform: scale(1) 610 - } 604 + from { 605 + opacity: 0; 606 + transform: scale(.9) 607 + } 608 + to { 609 + opacity: 1; 610 + transform: scale(1) 611 + } 611 612 } 612 613 613 614 @keyframes share-click { 614 - from { 615 - color: #5dfc01 616 - } 617 - to { 618 - color: #eee 619 - } 615 + from { 616 + color: #5dfc01 617 + } 618 + to { 619 + color: #eee 620 + } 620 621 } 621 622 622 623 @keyframes share-after { 623 - from { 624 - opacity: 1 625 - } 626 - to { 627 - opacity: 0 628 - } 624 + from { 625 + opacity: 1 626 + } 627 + to { 628 + opacity: 0 629 + } 629 630 } 630 631 631 632 @keyframes bob { 632 - from, 633 - to { 634 - transform: translateY(-10%); 635 - } 636 - 50% { 637 - transform: translateY(10%); 638 - } 633 + from, 634 + to { 635 + transform: translateY(-10%); 636 + } 637 + 50% { 638 + transform: translateY(10%); 639 + } 639 640 } 640 641
+52
schema.sql
··· 1 + CREATE TABLE IF NOT EXISTS artists ( 2 + id text NOT NULL, 3 + name text, 4 + website text 5 + ); 6 + ALTER TABLE artists ADD CONSTRAINT artists_pk PRIMARY KEY (id); 7 + 8 + CREATE TABLE IF NOT EXISTS musicreleases ( 9 + id character varying(64) NOT NULL, 10 + title text NOT NULL, 11 + type text, 12 + release_date DATE NOT NULL, 13 + artwork text, 14 + buyname text, 15 + buylink text 16 + ); 17 + ALTER TABLE musicreleases ADD CONSTRAINT musicreleases_pk PRIMARY KEY (id); 18 + 19 + CREATE TABLE IF NOT EXISTS musiclinks ( 20 + release character varying(64) NOT NULL, 21 + name text NOT NULL, 22 + url text, 23 + ); 24 + ALTER TABLE musiclinks ADD CONSTRAINT musiclinks_pk PRIMARY KEY (release, name); 25 + 26 + CREATE TABLE IF NOT EXISTS musiccredits ( 27 + release character varying(64) NOT NULL, 28 + artist text NOT NULL, 29 + role text, 30 + is_primary boolean, 31 + ); 32 + ALTER TABLE musiccredits ADD CONSTRAINT musiccredits_pk PRIMARY KEY (release, artist); 33 + 34 + CREATE TABLE IF NOT EXISTS musictracks ( 35 + release character varying(64) NOT NULL, 36 + number integer NOT NULL, 37 + title text NOT NULL, 38 + description text, 39 + lyrics text, 40 + preview_url text, 41 + ); 42 + ALTER TABLE musictracks ADD CONSTRAINT musictracks_pk PRIMARY KEY (release, number); 43 + 44 + -- foreign keys 45 + 46 + ALTER TABLE public.musiccredits ADD CONSTRAINT musiccredits_artist_fk FOREIGN KEY (artist) REFERENCES public.artists(id) ON DELETE CASCADE; 47 + 48 + ALTER TABLE public.musiccredits ADD CONSTRAINT musiccredits_release_fk FOREIGN KEY (release) REFERENCES public.musicreleases(id) ON DELETE CASCADE; 49 + 50 + ALTER TABLE public.musiclinks ADD CONSTRAINT musiclinks_release_fk FOREIGN KEY (release) REFERENCES public.musicreleases(id) ON UPDATE CASCADE ON DELETE CASCADE; 51 + 52 + ALTER TABLE public.musictracks ADD CONSTRAINT musictracks_release_fk FOREIGN KEY (release) REFERENCES public.musicreleases(id) ON DELETE CASCADE;
+3 -1
views/admin.html
··· 14 14 </h1> 15 15 16 16 <p> 17 - bappity boopity 17 + whapow! nothing here. 18 + <br> 19 + nice try, though. 18 20 </p> 19 21 </main> 20 22 {{end}}
+1 -1
views/base.html
··· 13 13 <!-- <script type="application/javascript" src="/script/lib/htmx.js"></script> --> 14 14 <!-- <script type="application/javascript" src="/script/lib/htmx.min.js"></script> --> 15 15 <!-- <script type="application/javascript" src="/script/lib/htmx-preload.js"></script> --> 16 - <script type="application/javascript" src="/script/swap.js"></script> 16 + <!-- <script type="application/javascript" src="/script/swap.js"></script> --> 17 17 </head> 18 18 19 19 <body>
+148 -151
views/index.html
··· 20 20 21 21 {{define "content"}} 22 22 <main> 23 - <script type="module" src="/script/main.js"></script> 23 + <h1> 24 + # hello, world! 25 + </h1> 24 26 25 - <h1> 26 - # hello, world! 27 - </h1> 27 + <p> 28 + <strong>i'm ari!</strong> 29 + <br> 30 + <small>she/her 🏳️‍⚧️🏳️‍🌈💫🦆🇮🇪</small> 31 + </p> 28 32 33 + <p> 34 + i'm a <a href="/music">musician</a>, <a href="https://github.com/arimelody?tab=repositories">developer</a>, 35 + <a href="https://twitch.tv/arispacegirl">streamer</a>, <a href="https://youtube.com/@arispacegirl">youtuber</a>, 36 + and probably a bunch of other things i forgot to mention! 37 + </p> 38 + <p> 39 + you're very welcome to take a look around my little space on the internet here, 40 + or explore any of the other parts i inhabit! 41 + </p> 42 + <p> 43 + if you're looking to support me financially, that's so cool of you!! 44 + if you like, you can buy some of my music over on 45 + <a href="https://arimelody.bandcamp.com" target="_blank">bandcamp</a> 46 + so you can at least get something for your money. 47 + thank you very much either way!! 💕 48 + </p> 49 + <p> 50 + for anything else, you can reach me for any and all communications through 51 + <a href="mailto:ari@arimelody.me">ari@arimelody.me</a>. if your message 52 + contains anything beyond a silly gag, i strongly recommend encrypting 53 + your message using my public pgp key, listed below! 54 + </p> 55 + <p> 56 + thank you for stopping by- i hope you have a lovely rest of your day! 💫 57 + </p> 29 58 30 - <p> 31 - <strong>i'm ari!</strong> 32 - <br> 33 - <small>she/her 🏳️‍⚧️🏳️‍🌈💫🦆🇮🇪</small> 34 - </p> 59 + <hr> 35 60 36 - <p> 37 - i like to create things on the internet! namely <a href="/music">music</a>, 38 - <a href="https://youtube.com/mellodoot" target="_blank">videos</a>, art <small><em>(link pending)</em></small>, and the odd 39 - <a href="https://github.com/mellodoot?tab=repositories" target="_blank">code project</a> from time to time! 40 - if it's a thing you can create on a computer, i've probably taken a swing at it. 41 - </p> 42 - <p> 43 - you're very welcome to take a look around my little slice of the internet here, 44 - or explore any of the other corners i inhabit! 45 - </p> 46 - <p> 47 - and if you're looking to support me financially, that's so cool of you!! 48 - if you like, you can buy one or more of my songs over on 49 - <a href="https://arimelody.bandcamp.com" target="_blank">bandcamp</a>, 50 - so you can at least get something for your money. 51 - thank you very much either way!! 💕 52 - </p> 53 - <p> 54 - for anything else, you can reach me for any and all communications through 55 - <a href="mailto:ari@arimelody.me">ari@arimelody.me</a>. if your message 56 - contains anything beyond a silly gag, i strongly recommend encrypting 57 - your message using my public pgp key, listed below! 58 - </p> 59 - <p> 60 - thank you for stopping by- i hope you have a lovely rest of your day! 💫 61 - </p> 61 + <h2> 62 + ## metadata 63 + </h2> 62 64 63 - <hr> 65 + <p> 66 + <strong>my colours 🌈</strong> 67 + </p> 68 + <ul> 69 + <li>primary: <span class="col-primary">#b7fd49</span></li> 70 + <li>secondary: <span class="col-secondary">#f8e05b</span></li> 71 + <li>tertiary: <span class="col-tertiary">#f788fe</span></li> 72 + </ul> 64 73 65 - <h2> 66 - ## metadata 67 - </h2> 74 + <p> 75 + <strong>my keys 🔑</strong> 76 + </p> 77 + <ul> 78 + <li>pgp: <a href="/keys/ari melody_0x92678188_public.asc" target="_blank">[link]</a></li> 79 + <li>ssh (ed25519): <a href="/keys/id_ari_ed25519.pub" target="_blank">[link]</a></li> 80 + </ul> 68 81 69 - <p> 70 - <strong>my colours 🌈</strong> 71 - </p> 72 - <ul> 73 - <li>primary: <span class="col-primary">#b7fd49</span></li> 74 - <li>secondary: <span class="col-secondary">#f8e05b</span></li> 75 - <li>tertiary: <span class="col-tertiary">#f788fe</span></li> 76 - </ul> 82 + <p> 83 + <strong>where to find me 🛰️</strong> 84 + </p> 85 + <ul class="links"> 86 + <li> 87 + <a href="https://youtube.com/@arispacegirl" target="_blank">youtube</a> 88 + </li> 89 + <li> 90 + <a href="https://twitch.tv/arispacegirl" target="_blank">twitch</a> 91 + </li> 92 + <li> 93 + <a href="https://sptfy.com/mellodoot" target="_blank">spotify</a> 94 + </li> 95 + <li> 96 + <a href="https://arimelody.bandcamp.com" target="_blank">bandcamp</a> 97 + </li> 98 + <li> 99 + <a href="https://github.com/arimelody" target="_blank">github</a> 100 + </li> 101 + </ul> 102 + 103 + <p> 104 + <strong>projects i've worked on 🛠️</strong> 105 + </p> 106 + <ul class="links"> 107 + <li> 108 + <a href="https://catdance.arimelody.me" target="_blank"> 109 + catdance 110 + </a> 111 + </li> 112 + <li> 113 + <a href="https://git.arimelody.me/ari/prideflag" target="_blank"> 114 + pride flag 115 + </a> 116 + </li> 117 + <li> 118 + <a href="https://github.com/arimelody/ipaddrgen" target="_blank"> 119 + ipaddrgen 120 + </a> 121 + </li> 122 + <li> 123 + <a href="https://impact.arimelody.me/" target="_blank"> 124 + impact meme 125 + </a> 126 + </li> 127 + <li> 128 + <a href="https://term.arimelody.me/" target="_blank"> 129 + OpenTerminal 130 + </a> 131 + </li> 132 + </ul> 77 133 78 - <p> 79 - <strong>my keys 🔑</strong> 80 - </p> 81 - <ul> 82 - <li>pgp: <a href="/keys/ari melody_0x92678188_public.asc" target="_blank">[link]</a></li> 83 - <li>ssh (ed25519): <a href="/keys/id_ari_ed25519.pub" target="_blank">[link]</a></li> 84 - </ul> 134 + <hr> 85 135 86 - <p> 87 - <strong>where to find me 🛰️</strong> 88 - </p> 89 - <ul class="links"> 90 - <li> 91 - <a href="https://youtube.com/@mellodoot" target="_blank">youtube</a> 92 - </li> 93 - <li> 94 - <a href="https://mellodoot.tumblr.com" target="_blank">tumblr</a> 95 - </li> 96 - <li> 97 - <a href="https://twitch.tv/mellodoot" target="_blank">twitch</a> 98 - </li> 99 - <li> 100 - <a href="https://sptfy.com/mellodoot" target="_blank">spotify</a> 101 - </li> 102 - <li> 103 - <a href="https://soundcloud.com/arimelody" target="_blank">soundcloud</a> 104 - </li> 105 - <li> 106 - <a href="https://github.com/mellodoot" target="_blank">github</a> 107 - </li> 108 - </ul> 136 + <h2> 137 + ## cool people 138 + </h2> 109 139 110 - <p> 111 - <strong>projects i've worked on 🛠️</strong> 112 - </p> 113 - <ul class="links"> 114 - <li> 115 - <a href="https://catdance.xyz" target="_blank"> 116 - catdance 117 - </a> 118 - </li> 119 - <li> 120 - <a href="https://github.com/mellodoot/sandblock" target="_blank"> 121 - sandblock 122 - </a> 123 - </li> 124 - <li> 125 - <a href="https://github.com/mellodoot/prideflag" target="_blank"> 126 - pride flag 127 - </a> 128 - </li> 129 - <li> 130 - <a href="https://github.com/mellodoot/ipaddrgen" target="_blank"> 131 - ipaddrgen 132 - </a> 133 - </li> 134 - <li> 135 - <a href="https://impact.arimelody.me/" target="_blank"> 136 - impact meme 137 - </a> 138 - </li> 139 - <li> 140 - <a href="https://term.arimelody.me/" target="_blank"> 141 - OpenTerminal 142 - </a> 143 - </li> 144 - </ul> 140 + <div id="web-buttons"> 141 + <a href="https://arimelody.me"> 142 + <img src="/img/buttons/ari melody.gif" alt="ari melody web button" width="88" height="31"> 143 + </a> 144 + <a href="https://supitszaire.com" target="_blank"> 145 + <img src="/img/buttons/zaire.gif" alt="zaire web button" width="88" height="31"> 146 + </a> 147 + <a href="https://mae.wtf" target="_blank"> 148 + <img src="/img/buttons/mae.png" alt="vimae web button" width="88" height="31"> 149 + </a> 150 + <a href="https://zvava.org" target="_blank"> 151 + <img src="/img/buttons/zvava.png" alt="zvava web button" width="88" height="31"> 152 + </a> 153 + <a href="https://elke.cafe" target="_blank"> 154 + <img src="/img/buttons/elke.gif" alt="elke web button" width="88" height="31"> 155 + </a> 156 + <a href="https://itzzen.net" target="_blank"> 157 + <img src="/img/buttons/itzzen.png" alt="itzzen web button" width="88" height="31"> 158 + </a> 145 159 146 160 <hr> 147 161 148 - <h2>## cool people</h2> 149 - 150 - <div id="web-buttons"> 151 - <a href="https://arimelody.me"> 152 - <img src="/img/buttons/ari melody.gif" alt="ari melody web button" loading="lazy"> 153 - </a> 154 - <a href="https://supitszaire.com" target="_blank"> 155 - <img src="/img/buttons/zaire.gif" alt="zaire web button" loading="lazy"> 156 - </a> 157 - <a href="https://mae.wtf" target="_blank"> 158 - <img src="/img/buttons/mae.png" alt="vimae web button" loading="lazy"> 159 - </a> 160 - <a href="https://zvava.org" target="_blank"> 161 - <img src="/img/buttons/zvava.png" alt="zvava web button" loading="lazy"> 162 - </a> 163 - 164 - <hr> 165 - 166 - <img src="/img/buttons/misc/debian.gif" alt="powered by debian" loading="lazy"> 167 - <img src="/img/buttons/misc/girls4notepad.gif" alt="girls 4 notepad" loading="lazy"> 168 - <img src="/img/buttons/misc/gaywebring.gif" alt="this website is GAY" loading="lazy"> 169 - <img src="/img/buttons/misc/graphicdesign.gif" alt="graphic design is my passion" loading="lazy"> 170 - <img src="/img/buttons/misc/gplv3.gif" alt="GPLv3 free software" loading="lazy"> 171 - <img src="/img/buttons/misc/hl.gif" alt="half-life" loading="lazy"> 172 - <img src="/img/buttons/misc/h-free-anim.gif" alt="dis site is hentai FREE" loading="lazy"> 173 - <img src="/img/buttons/misc/sprunk.gif" alt="sprunk" loading="lazy"> 174 - <img src="/img/buttons/misc/tohell.gif" alt="go straight to hell" loading="lazy"> 175 - <img src="/img/buttons/misc/virusalert.gif" alt="virus alert! click here" onclick="alert('meow :3')" loading="lazy"> 176 - <img src="/img/buttons/misc/wii.gif" alt="wii" loading="lazy"> 177 - <img src="/img/buttons/misc/www2.gif" alt="www" loading="lazy"> 178 - <img src="/img/buttons/misc/iemandatory.gif" alt="get mandatory internet explorer" loading="lazy"> 179 - <img src="/img/buttons/misc/learn_html.gif" alt="HTML - learn it today!" loading="lazy"> 180 - <a href="https://smokepowered.com" target="_blank"> 181 - <img src="/img/buttons/misc/smokepowered.gif" alt="high on SMOKE" loading="lazy"> 182 - </a> 183 - <a href="https://epicblazed.com" target="_blank"> 184 - <img src="/img/buttons/misc/epicblazed.png" alt="epic blazed" loading="lazy"> 185 - </a> 186 - </div> 162 + <img src="/img/buttons/misc/debian.gif" alt="powered by debian" width="88" height="31"> 163 + <img src="/img/buttons/misc/girls4notepad.gif" alt="girls 4 notepad" width="88" height="31"> 164 + <img src="/img/buttons/misc/gaywebring.gif" alt="this website is GAY" width="88" height="31"> 165 + <img src="/img/buttons/misc/graphicdesign.gif" alt="graphic design is my passion" width="88" height="31"> 166 + <img src="/img/buttons/misc/gplv3.gif" alt="GPLv3 free software" width="88" height="31"> 167 + <img src="/img/buttons/misc/hl.gif" alt="half-life" width="88" height="31"> 168 + <img src="/img/buttons/misc/h-free-anim.gif" alt="dis site is hentai FREE" width="88" height="31"> 169 + <img src="/img/buttons/misc/sprunk.gif" alt="sprunk" width="88" height="31"> 170 + <img src="/img/buttons/misc/tohell.gif" alt="go straight to hell" width="88" height="31"> 171 + <img src="/img/buttons/misc/virusalert.gif" alt="virus alert! click here" onclick="alert('meow :3')" width="88" height="31"> 172 + <img src="/img/buttons/misc/wii.gif" alt="wii" width="88" height="31"> 173 + <img src="/img/buttons/misc/www2.gif" alt="www" width="88" height="31"> 174 + <img src="/img/buttons/misc/iemandatory.gif" alt="get mandatory internet explorer" width="88" height="31"> 175 + <img src="/img/buttons/misc/learn_html.gif" alt="HTML - learn it today!" width="88" height="31"> 176 + <a href="https://smokepowered.com" target="_blank"> 177 + <img src="/img/buttons/misc/smokepowered.gif" alt="high on SMOKE" width="88" height="31"> 178 + </a> 179 + <a href="https://epicblazed.com" target="_blank"> 180 + <img src="/img/buttons/misc/epicblazed.png" alt="epic blazed" width="88" height="31"> 181 + </a> 182 + <img src="/img/buttons/misc/blink.gif" alt="closeup anime blink" width="88" height="31"> 183 + </div> 187 184 </main> 188 185 {{end}}
+127 -125
views/music-gateway.html
··· 1 1 {{define "head"}} 2 - <title>{{.PrintPrimaryArtists false}} - {{.Title}}</title> 2 + <title>{{.Title}} - {{.PrintPrimaryArtists false true}}</title> 3 3 <link rel="icon" href="{{.ResolveArtwork}}"> 4 4 5 - <meta name="description" content="Stream &quot;{{.Title}}&quot; by {{.PrintPrimaryArtists false}} on all platforms!"> 6 - <meta name="author" content="{{.PrintPrimaryArtists false}}"> 7 - <meta name="keywords" content="{{.PrintCommaPrimaryArtists false}}, music, {{.Title}}, {{.Id}}, {{.GetReleaseYear}}"> 5 + <meta name="description" content="Stream &quot;{{.Title}}&quot; by {{.PrintPrimaryArtists false true}} on all platforms!"> 6 + <meta name="author" content="{{.PrintPrimaryArtists false true}}"> 7 + <meta name="keywords" content="{{.PrintPrimaryArtists false false}}, music, {{.Title}}, {{.Id}}, {{.GetReleaseYear}}"> 8 8 9 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="{{.PrintPrimaryArtists false}} - {{.Title}}"> 14 - <meta property="og:description" content="Stream &quot;{{.Title}}&quot; by {{.PrintPrimaryArtists false}} on all platforms!"> 13 + <meta property="og.Title" content="{{.Title}} - {{.PrintPrimaryArtists false true}}"> 14 + <meta property="og:description" content="Stream &quot;{{.Title}}&quot; by {{.PrintPrimaryArtists false true}} on all platforms!"> 15 15 <meta property="og:image" content="https://arimelody.me{{.ResolveArtwork}}"> 16 16 17 17 <meta name="twitter:card" content="summary_large_image"> ··· 19 19 <meta name="twitter:creator" content="@funniduck"> 20 20 <meta property="twitter:domain" content="arimelody.me"> 21 21 <meta property="twitter:url" content="https://arimelody.me/music/{{.Id}}"> 22 - <meta name="twitter.Title" content="{{.PrintPrimaryArtists false}} - {{.Title}}"> 23 - <meta name="twitter:description" content="Stream &quot;{{.Title}}&quot; by mellodoot on all platforms!"> 22 + <meta name="twitter.Title" content="{{.PrintPrimaryArtists false true}} - {{.Title}}"> 23 + <meta name="twitter:description" content="Stream &quot;{{.Title}}&quot; by {{.PrintPrimaryArtists false true}} on all platforms!"> 24 24 <meta name="twitter:image" content="https://arimelody.me{{.ResolveArtwork}}"> 25 25 <meta name="twitter:image:alt" content="Cover art for &quot;{{.Title}}&quot;"> 26 26 ··· 30 30 31 31 {{define "content"}} 32 32 <main> 33 - <script type="module" src="/script/music-gateway.js"></script> 33 + <script type="module" src="/script/music-gateway.js"></script> 34 34 35 - <div id="background" style="background-image: url({{.ResolveArtwork}})"></div> 35 + <div id="background" style="background-image: url({{.ResolveArtwork}})"></div> 36 36 37 - <a href="/music" swap-url="/music" id="go-back" title="back to arimelody.me">&lt;</a> 38 - <br><br> 37 + <a href="/music" swap-url="/music" id="go-back" title="back to arimelody.me">&lt;</a> 38 + <br><br> 39 39 40 - <div id="music-container"> 41 - <div id="art-container"> 42 - <div class="tilt-topleft"></div> 43 - <div class="tilt-top"></div> 44 - <div class="tilt-topright"></div> 45 - <div class="tilt-right"></div> 46 - <div class="tilt-bottomright"></div> 47 - <div class="tilt-bottom"></div> 48 - <div class="tilt-bottomleft"></div> 49 - <div class="tilt-left"></div> 50 - <img id="artwork" src="{{.ResolveArtwork}}" alt="{{.Title}} artwork" width=240 height=240> 40 + <div id="music-container"> 41 + <div id="art-container"> 42 + <div class="tilt-topleft"></div> 43 + <div class="tilt-top"></div> 44 + <div class="tilt-topright"></div> 45 + <div class="tilt-right"></div> 46 + <div class="tilt-bottomright"></div> 47 + <div class="tilt-bottom"></div> 48 + <div class="tilt-bottomleft"></div> 49 + <div class="tilt-left"></div> 50 + <img id="artwork" src="{{.ResolveArtwork}}" alt="{{.Title}} artwork" width=240 height=240> 51 + </div> 52 + <div id="vertical-line"></div> 53 + <div id="info"> 54 + <div id="overview"> 55 + <div id="title-container"> 56 + <h1 id="title">{{.Title}}</h1> 57 + <span id="year" title="{{.PrintReleaseDate}}">{{.GetReleaseYear}}</span> 51 58 </div> 52 - <div id="vertical-line"></div> 53 - <div id="info"> 54 - <div id="overview"> 55 - <div id="title-container"> 56 - <h1 id="title">{{.Title}}</h1> 57 - <span id="year" title="{{.PrintReleaseDate}}">{{.GetReleaseYear}}</span> 58 - </div> 59 - <p id="artist">{{.PrintPrimaryArtists false}}</p> 60 - <p id="type" class="{{.ResolveType}}">{{.ResolveType}}</p> 59 + <p id="artist">{{.PrintPrimaryArtists false true}}</p> 60 + <p id="type" class="{{.ResolveType}}">{{.ResolveType}}</p> 61 61 62 - <ul id="links"> 63 - {{if .Buylink}} 64 - <li> 65 - <a href="{{.Buylink}}" class="buy">{{or .Buyname "buy"}}</a> 66 - </li> 67 - {{end}} 62 + <ul id="links"> 63 + {{if .Buylink}} 64 + <li> 65 + <a href="{{.Buylink}}" class="buy">{{or .Buyname "buy"}}</a> 66 + </li> 67 + {{end}} 68 68 69 - {{range .Links}} 70 - <li> 71 - <a class="{{.NormaliseName}}" href="{{.Url}}">{{.Name}}</a> 72 - </li> 73 - {{end}} 74 - </ul> 69 + {{range .Links}} 70 + <li> 71 + <a class="{{.NormaliseName}}" href="{{.Url}}">{{.Name}}</a> 72 + </li> 73 + {{end}} 74 + </ul> 75 75 76 - {{if .Description}} 77 - <p id="description"> 78 - {{.Description}} 79 - </p> 80 - {{end}} 76 + {{if .Description}} 77 + <p id="description"> 78 + {{.Description}} 79 + </p> 80 + {{end}} 81 81 82 - <button id="share">share</button> 83 - </div> 82 + <button id="share">share</button> 83 + </div> 84 84 85 - {{if .Credits}} 86 - <div id="credits"> 87 - <h2>credits:</h2> 88 - <ul> 89 - {{range .Credits}} 90 - {{$Artist := .ResolveArtist}} 91 - {{if $Artist.Website}} 92 - <li><strong><a href="{{$Artist.Website}}">{{$Artist.Name}}</a></strong>: {{.Role}}</li> 93 - {{else}} 94 - <li><strong>{{$Artist.Name}}</strong>: {{.Role}}</li> 95 - {{end}} 96 - {{end}} 97 - </ul> 98 - </div> 99 - {{end}} 85 + {{if .Credits}} 86 + <div id="credits"> 87 + <h2>credits:</h2> 88 + <ul> 89 + {{range .Credits}} 90 + {{$Artist := .ResolveArtist}} 91 + {{if $Artist.Website}} 92 + <li><strong><a href="{{$Artist.Website}}">{{$Artist.Name}}</a></strong>: {{.Role}}</li> 93 + {{else}} 94 + <li><strong>{{$Artist.Name}}</strong>: {{.Role}}</li> 95 + {{end}} 96 + {{end}} 97 + </ul> 98 + </div> 99 + {{end}} 100 100 101 - {{if .IsSingle}} 102 - {{$Track := index .Tracks 0}} 103 - {{if $Track.Lyrics}} 104 - <div id="lyrics"> 105 - <h2>lyrics:</h2> 106 - <p>{{$Track.Lyrics}}</p> 107 - </div> 108 - {{end}} 109 - {{else}} 110 - <div id="tracks"> 111 - <h2>tracks:</h2> 112 - {{range .Tracks}} 113 - <h3>{{.Title}}</h3> 114 - <p>{{.Lyrics}}</p> 115 - {{end}} 116 - </div> 117 - {{end}} 118 - </div> 101 + {{if .IsSingle}} 102 + {{$Track := index .Tracks 0}} 103 + {{if $Track.Lyrics}} 104 + <div id="lyrics"> 105 + <h2>lyrics:</h2> 106 + <p>{{$Track.Lyrics}}</p> 107 + </div> 108 + {{end}} 109 + {{else}} 110 + <div id="tracks"> 111 + <h2>tracks:</h2> 112 + {{range .Tracks}} 113 + <details> 114 + <summary class="album-track-title">{{.Title}}</summary> 115 + {{.Lyrics}} 116 + </details> 117 + {{end}} 118 + </div> 119 + {{end}} 120 + </div> 119 121 120 - {{if or .Credits not .IsSingle}} 121 - <div id="extras"> 122 - <ul> 123 - <li><a href="#overview">overview</a></li> 122 + {{if or .Credits not .IsSingle}} 123 + <div id="extras"> 124 + <ul> 125 + <li><a href="#overview">overview</a></li> 124 126 125 - {{if .Credits}} 126 - <li><a href="#credits">credits</a></li> 127 - {{end}} 127 + {{if .Credits}} 128 + <li><a href="#credits">credits</a></li> 129 + {{end}} 128 130 129 - {{if .IsSingle}} 130 - {{$Track := index .Tracks 0}} 131 - {{if $Track.Lyrics}} 132 - <li><a href="#lyrics">lyrics</a></li> 133 - {{end}} 134 - {{else}} 135 - <li><a href="#tracks">tracks</a></li> 136 - {{end}} 137 - </ul> 138 - </div> 131 + {{if .IsSingle}} 132 + {{$Track := index .Tracks 0}} 133 + {{if $Track.Lyrics}} 134 + <li><a href="#lyrics">lyrics</a></li> 139 135 {{end}} 140 - <!-- <div id="tracks"> --> 141 - <!-- <% var file = `/audio/preview/${data.id}.webm` %> --> 142 - <!-- <% if (data.tracks && typeof(data.tracks) == typeof([])) { %> --> 143 - <!-- <% for( let i = 0; i < data.tracks.length; i++ ) { %> --> 144 - <!-- <% --> 145 - <!-- songid = data.tracks[i].title.toLowerCase().replace(/[^a-z0-9]/g, ""); --> 146 - <!-- file = `/audio/preview/${data.id}-${songid}.webm`; --> 147 - <!-- %> --> 148 - <!----> 149 - <!-- <div class="track-preview" id="preview-<%= songid %>"> --> 150 - <!-- <i class="fa-solid fa-play play"></i> --> 151 - <!-- <p><%= data.tracks[i].title %></p> --> 152 - <!-- <audio src="<%= file %>"></audio> --> 153 - <!-- </div> --> 154 - <!----> 155 - <!-- <% } %> --> 156 - <!-- <% } else { %> --> 157 - <!-- <div class="track-preview" id="preview-<%= data.id %>"> --> 158 - <!-- <i class="fa-solid fa-play play"></i> --> 159 - <!-- <p>{{.Title}}</p> --> 160 - <!-- <audio src="<%= file %>"></audio> --> 161 - <!-- </div> --> 162 - <!-- <% } %> --> 163 - <!-- </div> --> 136 + {{else}} 137 + <li><a href="#tracks">tracks</a></li> 138 + {{end}} 139 + </ul> 164 140 </div> 141 + {{end}} 142 + <!-- <div id="tracks"> --> 143 + <!-- <% var file = `/audio/preview/${data.id}.webm` %> --> 144 + <!-- <% if (data.tracks && typeof(data.tracks) == typeof([])) { %> --> 145 + <!-- <% for( let i = 0; i < data.tracks.length; i++ ) { %> --> 146 + <!-- <% --> 147 + <!-- songid = data.tracks[i].title.toLowerCase().replace(/[^a-z0-9]/g, ""); --> 148 + <!-- file = `/audio/preview/${data.id}-${songid}.webm`; --> 149 + <!-- %> --> 150 + <!----> 151 + <!-- <div class="track-preview" id="preview-<%= songid %>"> --> 152 + <!-- <i class="fa-solid fa-play play"></i> --> 153 + <!-- <p><%= data.tracks[i].title %></p> --> 154 + <!-- <audio src="<%= file %>"></audio> --> 155 + <!-- </div> --> 156 + <!----> 157 + <!-- <% } %> --> 158 + <!-- <% } else { %> --> 159 + <!-- <div class="track-preview" id="preview-<%= data.id %>"> --> 160 + <!-- <i class="fa-solid fa-play play"></i> --> 161 + <!-- <p>{{.Title}}</p> --> 162 + <!-- <audio src="<%= file %>"></audio> --> 163 + <!-- </div> --> 164 + <!-- <% } %> --> 165 + <!-- </div> --> 166 + </div> 165 167 </main> 166 168 {{end}}
+39 -38
views/music.html
··· 32 32 </div> 33 33 <div class="music-details"> 34 34 <a href="/music/{{$Album.Id}}"><h1 class="music-title">{{$Album.Title}}</h1></a> 35 - <h2 class="music-artist">{{$Album.PrintPrimaryArtists false}}</h2> 35 + <h2 class="music-artist">{{$Album.PrintPrimaryArtists false true}}</h2> 36 36 <h3 class="music-type-{{.ResolveType}}">{{$Album.ResolveType}}</h3> 37 37 <ul class="music-links"> 38 38 {{range $Link := $Album.Links}} ··· 46 46 {{end}} 47 47 </div> 48 48 49 - <h2 id="usage" class="question" swap-url="/music#usage"> 50 - <a href="#usage"> 51 - &gt; "can i use your music in my content?" 52 - </a> 49 + <h2 id="usage" class="question"> 50 + <a href="#usage"> 51 + &gt; "can i use your music in my content?" 52 + </a> 53 53 </h2> 54 54 <div class="answer"> 55 - <p> 56 - <strong class="big">yes!</strong> well, in most cases... 57 - </p> 58 - <p> 59 - from <a href="/music/dream">Dream (2022)</a> onward, all of my <em>self-released</em> songs are 60 - licensed under <a href="https://creativecommons.org/licenses/by-sa/3.0/" target="_blank">Creative Commons Attribution-ShareAlike 3.0</a>. anyone may use these 61 - songs freely, so long as they provide credit back to me! 62 - </p> 63 - <p> 64 - a great example of some credit text would be as follows: 65 - </p> 66 - <blockquote> 67 - music used: mellodoot - Dream<br> 68 - buy it here: <a href="/music/dream">https://arimelody.me/music/dream</a><br> 69 - licensed under CC BY-SA 3.0. 70 - </blockquote> 71 - <p> 72 - for any songs prior to this, they were all either released by me (in which case, i honestly 73 - don't mind), or in collaboration with chill people who i don't see having an issue with it. 74 - do be sure to ask them about it, though! 75 - </p> 76 - <p> 77 - in the event the song you want to use is released under some other label, their usage rights 78 - will more than likely trump whatever i'd otherwise have in mind. i'll try to negotiate some 79 - nice terms, though! ;3 80 - </p> 81 - <p> 82 - i love the idea of other creators using my songs in their work, so if you do happen to use 83 - my stuff in a work you're particularly proud of, feel free to send it my way! 84 - </p> 85 - <p> 86 - &gt; <a href="mailto:ari@arimelody.me">ari@arimelody.me</a> 87 - </p> 55 + <p> 56 + <strong class="big">yes!</strong> well, in most cases... 57 + </p> 58 + <p> 59 + from <a href="/music/dream">Dream (2022)</a> onward, all of my <em>self-released</em> songs are 60 + licensed under <a href="https://creativecommons.org/licenses/by-sa/4.0/" target="_blank">Creative Commons Attribution-ShareAlike 4.0</a>. 61 + anyone may use and remix these songs freely, so long as they provide credit back to me and link back to this license! 62 + please note that all derivative works must inherit this license. 63 + </p> 64 + <p> 65 + a great example of some credit text would be as follows: 66 + </p> 67 + <blockquote> 68 + music used: mellodoot - Dream<br> 69 + <a href="/music/dream">https://arimelody.me/music/dream</a><br> 70 + licensed under <a href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA 4.0</a>. 71 + </blockquote> 72 + <p> 73 + for any songs prior to this, they were all either released by me (in which case, i honestly 74 + don't mind), or in collaboration with chill people who i don't see having an issue with it. 75 + do be sure to ask them about it, though! 76 + </p> 77 + <p> 78 + in the event the song you want to use is released under some other label, their usage rights 79 + will more than likely trump whatever i'd otherwise have in mind. i'll try to negotiate some 80 + nice terms, though! ;3 81 + </p> 82 + <p> 83 + i love the idea of other creators using my songs in their work, so if you do happen to use 84 + my stuff in a work you're particularly proud of, feel free to send it my way! 85 + </p> 86 + <p> 87 + &gt; <a href="mailto:ari@arimelody.me">ari@arimelody.me</a> 88 + </p> 88 89 </div> 89 90 90 91 <a href="#" id="backtotop">back to top</a>