added stuff, broke some other stuff, made admin auth!
Signed-off-by: ari melody <ari@arimelody.me>
1
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
|||
**/.DS_Store
|
||||
.idea/
|
||||
tmp/
|
||||
test/
|
||||
|
|
18
api/api.go
|
@ -1,20 +1,12 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"html/template"
|
||||
|
||||
"arimelody.me/arimelody.me/api/v1/admin"
|
||||
"net/http"
|
||||
"html/template"
|
||||
)
|
||||
|
||||
func Handle(writer http.ResponseWriter, req *http.Request, root *template.Template) int {
|
||||
code := 404;
|
||||
if req.URL.Path == "/api/v1/admin/login" {
|
||||
code = admin.HandleLogin(writer, req, root)
|
||||
}
|
||||
|
||||
if code == 404 {
|
||||
writer.Write([]byte("404 not found"))
|
||||
}
|
||||
return code;
|
||||
writer.WriteHeader(501)
|
||||
writer.Write([]byte("501 Not Implemented"))
|
||||
return 501;
|
||||
}
|
||||
|
|
|
@ -1,43 +1,228 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"arimelody.me/arimelody.me/discord"
|
||||
)
|
||||
|
||||
type (
|
||||
State struct {
|
||||
Token string
|
||||
}
|
||||
Session struct {
|
||||
UserID string
|
||||
Token string
|
||||
}
|
||||
|
||||
loginData struct {
|
||||
UserID string
|
||||
Password string
|
||||
}
|
||||
)
|
||||
|
||||
func CreateState() *State {
|
||||
return &State{
|
||||
Token: "you are the WINRAR!!",
|
||||
}
|
||||
const TOKEN_LENGTH = 64
|
||||
const TOKEN_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
// TODO: consider relying *entirely* on env vars instead of hard-coded fallbacks
|
||||
var ADMIN_ID_DISCORD = func() string {
|
||||
envvar := os.Getenv("DISCORD_ADMIN_ID")
|
||||
if envvar != "" {
|
||||
return envvar
|
||||
} else {
|
||||
return "356210742200107009"
|
||||
}
|
||||
}()
|
||||
|
||||
var sessions []*Session
|
||||
|
||||
func CreateSession(UserID string) Session {
|
||||
return Session{
|
||||
UserID: UserID,
|
||||
Token: string(generateToken()),
|
||||
}
|
||||
}
|
||||
|
||||
func HandleLogin(writer http.ResponseWriter, req *http.Request, root *template.Template) int {
|
||||
if req.Method != "POST" {
|
||||
return 404;
|
||||
func Handler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Println(r.URL.Path)
|
||||
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte("hello admin!"))
|
||||
})
|
||||
}
|
||||
|
||||
func AuthorisedHandler(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
auth := r.Header.Get("Authorization")
|
||||
if auth == "" || !strings.HasPrefix(auth, "Bearer ") {
|
||||
cookie, err := r.Cookie("token")
|
||||
if err != nil {
|
||||
w.WriteHeader(401)
|
||||
w.Write([]byte("Unauthorized"))
|
||||
return
|
||||
}
|
||||
auth = cookie.Value
|
||||
}
|
||||
auth = auth[7:]
|
||||
|
||||
var session *Session
|
||||
for _, s := range sessions {
|
||||
if s.Token == auth {
|
||||
session = s
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if session == nil {
|
||||
w.WriteHeader(401)
|
||||
w.Write([]byte("Unauthorized"))
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(req.Body)
|
||||
ctx := context.WithValue(r.Context(), "token", session.Token)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
func LoginHandler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
code := r.URL.Query().Get("code")
|
||||
|
||||
if code == "" {
|
||||
w.Header().Add("Location", discord.REDIRECT_URI)
|
||||
w.WriteHeader(307)
|
||||
return
|
||||
}
|
||||
|
||||
// let's get an oauth token!
|
||||
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/oauth2/token", discord.API_ENDPOINT),
|
||||
strings.NewReader(url.Values{
|
||||
"client_id": {discord.CLIENT_ID},
|
||||
"client_secret": {discord.CLIENT_SECRET},
|
||||
"grant_type": {"authorization_code"},
|
||||
"code": {code},
|
||||
"redirect_uri": {discord.MY_REDIRECT_URI},
|
||||
}.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to parse request body!\n");
|
||||
return 500;
|
||||
fmt.Printf("Failed to retrieve OAuth token: %s\n", err)
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte("Internal server error"))
|
||||
return
|
||||
}
|
||||
|
||||
if string(body) != "super epic mega gaming password" {
|
||||
return 400;
|
||||
oauth := discord.AccessTokenResponse{}
|
||||
|
||||
err = json.NewDecoder(res.Body).Decode(&oauth)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to parse OAuth response data from discord: %s\n", err)
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte("Internal server error"))
|
||||
return
|
||||
}
|
||||
res.Body.Close()
|
||||
|
||||
discord_access_token := oauth.AccessToken
|
||||
|
||||
// let's get authorisation information!
|
||||
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%s/oauth2/@me", discord.API_ENDPOINT), nil)
|
||||
req.Header.Add("Authorization", "Bearer " + discord_access_token)
|
||||
|
||||
res, err = http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to retrieve discord auth information: %s\n", err)
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte("Internal server error"))
|
||||
return
|
||||
}
|
||||
|
||||
state := CreateState();
|
||||
auth_info := discord.AuthInfoResponse{}
|
||||
|
||||
writer.WriteHeader(200);
|
||||
writer.Write([]byte(state.Token))
|
||||
err = json.NewDecoder(res.Body).Decode(&auth_info)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to parse auth information from discord: %s\n", err)
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte("Internal server error"))
|
||||
return
|
||||
}
|
||||
res.Body.Close()
|
||||
|
||||
return 200;
|
||||
discord_user_id := auth_info.User.Id
|
||||
|
||||
if discord_user_id != ADMIN_ID_DISCORD {
|
||||
// TODO: unauthorized user. revoke the token
|
||||
w.WriteHeader(401)
|
||||
w.Write([]byte("Unauthorized"))
|
||||
return
|
||||
}
|
||||
|
||||
// login success!
|
||||
session := CreateSession(auth_info.User.Username)
|
||||
sessions = append(sessions, &session)
|
||||
|
||||
cookie := http.Cookie{}
|
||||
cookie.Name = "token"
|
||||
cookie.Value = session.Token
|
||||
cookie.Expires = time.Now().Add(24 * time.Hour)
|
||||
// cookie.Secure = true
|
||||
cookie.HttpOnly = true
|
||||
cookie.Path = "/"
|
||||
http.SetCookie(w, &cookie)
|
||||
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(session.Token))
|
||||
})
|
||||
}
|
||||
|
||||
func LogoutHandler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.Context().Value("token").(string)
|
||||
|
||||
if token == "" {
|
||||
w.WriteHeader(401)
|
||||
return
|
||||
}
|
||||
|
||||
// remove this session from the list
|
||||
sessions = func (token string) []*Session {
|
||||
new_sessions := []*Session{}
|
||||
for _, session := range sessions {
|
||||
new_sessions = append(new_sessions, session)
|
||||
}
|
||||
return new_sessions
|
||||
}(token)
|
||||
|
||||
w.WriteHeader(200)
|
||||
})
|
||||
}
|
||||
|
||||
func OAuthCallbackHandler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
func VerifyHandler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// this is an authorised endpoint, so you *must* supply a valid token
|
||||
// before accessing this route.
|
||||
w.WriteHeader(200)
|
||||
})
|
||||
}
|
||||
|
||||
func generateToken() string {
|
||||
var token []byte
|
||||
|
||||
for i := 0; i < TOKEN_LENGTH; i++ {
|
||||
token = append(token, TOKEN_CHARS[rand.Intn(len(TOKEN_CHARS))])
|
||||
}
|
||||
|
||||
return string(token)
|
||||
}
|
||||
|
|
|
@ -1,33 +1,37 @@
|
|||
package music
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
var Releases []*MusicRelease;
|
||||
var Artists []*Artist;
|
||||
|
||||
func make_date_work(date string) time.Time {
|
||||
res, err := time.Parse("2-Jan-2006", date)
|
||||
if err != nil {
|
||||
fmt.Printf("somehow we failed to parse %s! falling back to epoch :]\n", date)
|
||||
return time.Unix(0, 0)
|
||||
res, err := time.Parse("2-Jan-2006", date)
|
||||
if err != nil {
|
||||
fmt.Printf("somehow we failed to parse %s! falling back to epoch :]\n", date)
|
||||
return time.Unix(0, 0)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func GetRelease(id string) (*MusicRelease, error) {
|
||||
for _, release := range Releases {
|
||||
if release.Id == id {
|
||||
return release, nil
|
||||
}
|
||||
return res
|
||||
}
|
||||
return nil, errors.New(fmt.Sprintf("Release %s not found", id))
|
||||
}
|
||||
|
||||
func GetRelease(id string) (MusicRelease, bool) {
|
||||
for _, album := range placeholders {
|
||||
if album.Id == id {
|
||||
return album, true
|
||||
}
|
||||
func GetArtist(id string) (*Artist, error) {
|
||||
for _, artist := range Artists {
|
||||
if artist.Id == id {
|
||||
return artist, nil
|
||||
}
|
||||
return MusicRelease{}, false
|
||||
}
|
||||
return nil, errors.New(fmt.Sprintf("Artist %s not found", id))
|
||||
}
|
||||
|
||||
func QueryAllMusic() ([]MusicRelease) {
|
||||
return placeholders
|
||||
}
|
||||
|
||||
func QueryAllArtists() ([]Artist) {
|
||||
return []Artist{ ari, mellodoot, zaire, mae, loudar, red }
|
||||
}
|
||||
|
||||
|
|
|
@ -1,161 +0,0 @@
|
|||
package music
|
||||
|
||||
var ari = Artist{
|
||||
Id: "arimelody",
|
||||
Name: "ari melody",
|
||||
Website: "https://arimelody.me",
|
||||
}
|
||||
var mellodoot = Artist{
|
||||
Id: "mellodoot",
|
||||
Name: "mellodoot",
|
||||
Website: "https://mellodoot.com",
|
||||
}
|
||||
var zaire = Artist{
|
||||
Id: "zaire",
|
||||
Name: "zaire",
|
||||
Website: "https://supitszaire.com",
|
||||
}
|
||||
var mae = Artist{
|
||||
Id: "maetaylor",
|
||||
Name: "mae taylor",
|
||||
Website: "https://mae.wtf",
|
||||
}
|
||||
var loudar = Artist{
|
||||
Id: "loudar",
|
||||
Name: "Loudar",
|
||||
Website: "https://alex.targoninc.com",
|
||||
}
|
||||
var red = Artist {
|
||||
Id: "smoljorb",
|
||||
Name: "smoljorb",
|
||||
}
|
||||
|
||||
var placeholders = []MusicRelease{
|
||||
{
|
||||
Id: "test",
|
||||
Title: "test album",
|
||||
Type: "album",
|
||||
ReleaseDate: make_date_work("18-Mar-2024"),
|
||||
Buyname: "go get it!!",
|
||||
Buylink: "https://arimelody.me/",
|
||||
Links: []MusicLink{
|
||||
{
|
||||
Name: "youtube",
|
||||
Url: "https://youtu.be/dQw4w9WgXcQ",
|
||||
},
|
||||
},
|
||||
Description:
|
||||
`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.`,
|
||||
Credits: []MusicCredit{
|
||||
{
|
||||
Artist: &ari,
|
||||
Role: "having the swag",
|
||||
},
|
||||
{
|
||||
Artist: &zaire,
|
||||
Role: "having the swag",
|
||||
},
|
||||
{
|
||||
Artist: &mae,
|
||||
Role: "having the swag",
|
||||
},
|
||||
{
|
||||
Artist: &loudar,
|
||||
Role: "having the swag",
|
||||
},
|
||||
},
|
||||
Tracks: []MusicTrack{
|
||||
{
|
||||
Number: 0,
|
||||
Title: "track 1",
|
||||
Description: "sample track description",
|
||||
Lyrics: "sample lyrics for track 1!",
|
||||
PreviewUrl: "https://mellodoot.com/audio/preview/dream.webm",
|
||||
},
|
||||
{
|
||||
Number: 1,
|
||||
Title: "track 2",
|
||||
Description: "sample track description",
|
||||
Lyrics: "sample lyrics for track 2!",
|
||||
PreviewUrl: "https://mellodoot.com/audio/preview/dream.webm",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "dream",
|
||||
Title: "Dream",
|
||||
Type: "single",
|
||||
ReleaseDate: make_date_work("11-Nov-2022"),
|
||||
Artwork: "https://mellodoot.com/img/music_artwork/mellodoot_-_Dream.webp",
|
||||
Buylink: "https://arimelody.bandcamp.com/track/dream",
|
||||
Links: []MusicLink{
|
||||
{
|
||||
Name: "spotify",
|
||||
Url: "https://open.spotify.com/album/5talRpqzjExP1w6j5LFIAh",
|
||||
},
|
||||
{
|
||||
Name: "apple music",
|
||||
Url: "https://music.apple.com/ie/album/dream-single/1650037132",
|
||||
},
|
||||
{
|
||||
Name: "soundcloud",
|
||||
Url: "https://soundcloud.com/arimelody/dream2022",
|
||||
},
|
||||
{
|
||||
Name: "youtube",
|
||||
Url: "https://www.youtube.com/watch?v=nfFgtMuYAx8",
|
||||
},
|
||||
},
|
||||
Description: "living the dream 🌌 ✨",
|
||||
Credits: []MusicCredit{
|
||||
{
|
||||
Artist: &mellodoot,
|
||||
Role: "vocals",
|
||||
},
|
||||
{
|
||||
Artist: &mellodoot,
|
||||
Role: "production",
|
||||
},
|
||||
{
|
||||
Artist: &mellodoot,
|
||||
Role: "artwork",
|
||||
},
|
||||
},
|
||||
Tracks: []MusicTrack{
|
||||
{
|
||||
Number: 0,
|
||||
Title: "Dream",
|
||||
Description: "no description here!",
|
||||
Lyrics:
|
||||
`the truth is what you make of it
|
||||
in the end, what you spend, is the end of it
|
||||
when you're lost in the life
|
||||
the life that you created on your own
|
||||
i'm becoming one
|
||||
with the soul that i see in the mirror
|
||||
blending one and whole
|
||||
this time, i'm real
|
||||
|
||||
i'm living the dream
|
||||
i'm living my best life
|
||||
running out of time
|
||||
i gotta make this right
|
||||
whenever you rise
|
||||
whenever you come down
|
||||
fall away from the light
|
||||
and then fall into our arms
|
||||
|
||||
the truth is what you make of it
|
||||
in the end, what you spend, is the end of it
|
||||
when you're lost in the life
|
||||
the life that you created on your own
|
||||
i'm becoming one
|
||||
with the soul that i see in the mirror
|
||||
blending one and whole
|
||||
this time, i'm real`,
|
||||
PreviewUrl: "https://mellodoot.com/audio/preview/dream.webm",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -1,136 +1,133 @@
|
|||
package music
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
Artist struct {
|
||||
Id string
|
||||
Name string
|
||||
Website string
|
||||
}
|
||||
Artist struct {
|
||||
Id string
|
||||
Name string
|
||||
Website string
|
||||
}
|
||||
|
||||
MusicRelease struct {
|
||||
Id string
|
||||
Title string
|
||||
Type string
|
||||
ReleaseDate time.Time
|
||||
Artwork string
|
||||
Buyname string
|
||||
Buylink string
|
||||
Links []MusicLink
|
||||
Description string
|
||||
Credits []MusicCredit
|
||||
Tracks []MusicTrack
|
||||
}
|
||||
MusicRelease struct {
|
||||
Id string
|
||||
Title string
|
||||
Type string
|
||||
ReleaseDate time.Time
|
||||
Artwork string
|
||||
Buyname string
|
||||
Buylink string
|
||||
Links []MusicLink
|
||||
Description string
|
||||
Credits []MusicCredit
|
||||
Tracks []MusicTrack
|
||||
}
|
||||
|
||||
MusicLink struct {
|
||||
Name string
|
||||
Url string
|
||||
}
|
||||
MusicLink struct {
|
||||
Name string
|
||||
Url string
|
||||
}
|
||||
|
||||
MusicCredit struct {
|
||||
Artist *Artist
|
||||
Role string
|
||||
Meta bool // for "meta" contributors (i.e. not credited for the musical work, but other related assets)
|
||||
}
|
||||
MusicCredit struct {
|
||||
Artist *Artist
|
||||
Role string
|
||||
Primary bool
|
||||
}
|
||||
|
||||
MusicTrack struct {
|
||||
Number int
|
||||
Title string
|
||||
Description string
|
||||
Lyrics string
|
||||
PreviewUrl string
|
||||
}
|
||||
MusicTrack struct {
|
||||
Number int
|
||||
Title string
|
||||
Description string
|
||||
Lyrics string
|
||||
PreviewUrl string
|
||||
}
|
||||
)
|
||||
|
||||
func (release MusicRelease) GetUniqueArtists(include_meta bool) []*Artist {
|
||||
res := []*Artist{}
|
||||
for _, credit := range release.Credits {
|
||||
if !include_meta && credit.Meta {
|
||||
continue
|
||||
}
|
||||
|
||||
exists := false
|
||||
for _, a := range res {
|
||||
if a == credit.Artist {
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if exists {
|
||||
continue
|
||||
}
|
||||
|
||||
res = append(res, credit.Artist)
|
||||
func (release MusicRelease) GetUniqueArtists(include_non_primary bool) []*Artist {
|
||||
res := []*Artist{}
|
||||
for _, credit := range release.Credits {
|
||||
if !include_non_primary && !credit.Primary {
|
||||
continue
|
||||
}
|
||||
|
||||
// now create the actual array to send
|
||||
return res
|
||||
exists := false
|
||||
for _, a := range res {
|
||||
if a == credit.Artist {
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if exists {
|
||||
continue
|
||||
}
|
||||
|
||||
res = append(res, credit.Artist)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func (release MusicRelease) GetUniqueArtistNames(include_meta bool) []string {
|
||||
artists := release.GetUniqueArtists(include_meta)
|
||||
names := []string{}
|
||||
for _, artist := range artists {
|
||||
names = append(names, artist.Name)
|
||||
}
|
||||
func (release MusicRelease) GetUniqueArtistNames(include_non_primary bool) []string {
|
||||
artists := release.GetUniqueArtists(include_non_primary)
|
||||
names := []string{}
|
||||
for _, artist := range artists {
|
||||
names = append(names, artist.Name)
|
||||
}
|
||||
|
||||
return names
|
||||
return names
|
||||
}
|
||||
|
||||
func (album MusicRelease) PrintPrimaryArtists(include_meta bool) string {
|
||||
names := album.GetUniqueArtistNames(include_meta)
|
||||
if len(names) == 1 {
|
||||
return names[0]
|
||||
}
|
||||
func (release MusicRelease) PrintPrimaryArtists(include_non_primary bool, ampersand bool) string {
|
||||
names := release.GetUniqueArtistNames(include_non_primary)
|
||||
if len(names) == 0 {
|
||||
return "Unknown Artist"
|
||||
} else if len(names) == 1 {
|
||||
return names[0]
|
||||
}
|
||||
if ampersand {
|
||||
res := strings.Join(names[:len(names)-1], ", ")
|
||||
res += " & " + names[len(names)-1]
|
||||
return res
|
||||
}
|
||||
|
||||
func (album MusicRelease) PrintCommaPrimaryArtists(include_meta bool) string {
|
||||
names := album.GetUniqueArtistNames(include_meta)
|
||||
if len(names) == 1 {
|
||||
return names[0]
|
||||
}
|
||||
} else {
|
||||
return strings.Join(names[:], ", ")
|
||||
}
|
||||
}
|
||||
|
||||
func (album MusicRelease) ResolveType() string {
|
||||
if album.Type != "" {
|
||||
return album.Type
|
||||
}
|
||||
return "unknown"
|
||||
func (release MusicRelease) ResolveType() string {
|
||||
if release.Type != "" {
|
||||
return release.Type
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func (album MusicRelease) ResolveArtwork() string {
|
||||
if album.Artwork != "" {
|
||||
return album.Artwork
|
||||
}
|
||||
return "/img/music-artwork/default.png"
|
||||
func (release MusicRelease) ResolveArtwork() string {
|
||||
if release.Artwork != "" {
|
||||
return release.Artwork
|
||||
}
|
||||
return "/img/music-artwork/default.png"
|
||||
}
|
||||
|
||||
func (album MusicRelease) PrintReleaseDate() string {
|
||||
return album.ReleaseDate.Format("02 January 2006")
|
||||
func (release MusicRelease) PrintReleaseDate() string {
|
||||
return release.ReleaseDate.Format("02 January 2006")
|
||||
}
|
||||
|
||||
func (album MusicRelease) GetReleaseYear() int {
|
||||
return album.ReleaseDate.Year()
|
||||
func (release MusicRelease) GetReleaseYear() int {
|
||||
return release.ReleaseDate.Year()
|
||||
}
|
||||
|
||||
func (link MusicLink) NormaliseName() string {
|
||||
re := regexp.MustCompile(`[^a-z0-9]`)
|
||||
return strings.ToLower(re.ReplaceAllString(link.Name, ""))
|
||||
re := regexp.MustCompile(`[^a-z0-9]`)
|
||||
return strings.ToLower(re.ReplaceAllString(link.Name, ""))
|
||||
}
|
||||
|
||||
func (release MusicRelease) IsSingle() bool {
|
||||
return len(release.Tracks) == 1;
|
||||
return len(release.Tracks) == 1;
|
||||
}
|
||||
|
||||
func (credit MusicCredit) ResolveArtist() Artist {
|
||||
return *credit.Artist
|
||||
return *credit.Artist
|
||||
}
|
||||
|
|
210
db.go
|
@ -1,72 +1,61 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"arimelody.me/arimelody.me/api/v1/music"
|
||||
"arimelody.me/arimelody.me/api/v1/music"
|
||||
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/jmoiron/sqlx"
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
var schema =
|
||||
`CREATE TABLE IF NOT EXISTS artists (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT,
|
||||
website TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS musicreleases (
|
||||
id VARCHAR(64) PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
type TEXT,
|
||||
release_date DATE NOT NULL,
|
||||
artwork TEXT,
|
||||
buyname TEXT,
|
||||
buylink TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS musiclinks (
|
||||
release VARCHAR(64) REFERENCES musicreleases(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
name TEXT,
|
||||
url TEXT,
|
||||
CONSTRAINT musiclinks_pk PRIMARY KEY (release, name)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS musiccredits (
|
||||
release VARCHAR(64) REFERENCES musicreleases(ID) ON DELETE CASCADE,
|
||||
artist TEXT REFERENCES artists(id) ON DELETE CASCADE,
|
||||
role TEXT,
|
||||
meta BOOLEAN,
|
||||
constraint musiccredits_pk PRIMARY KEY (release, artist)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS musictracks (
|
||||
release VARCHAR(64) REFERENCES musicreleases(ID) ON DELETE CASCADE,
|
||||
number INT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
lyrics TEXT,
|
||||
preview_url TEXT,
|
||||
CONSTRAINT musictracks_pk PRIMARY KEY (release, number)
|
||||
);`
|
||||
|
||||
func PushArtist(db *sqlx.DB, artist music.Artist) {
|
||||
fmt.Printf("syncing artist [%s] to database...", artist.Name)
|
||||
fmt.Printf("pushing artist [%s] to database...", artist.Name)
|
||||
|
||||
db.MustExec("INSERT INTO artists (id, name, website) VALUES ($1, $2, $3) ON CONFLICT (id) DO UPDATE SET name=$2, website=$3",
|
||||
db.MustExec("INSERT INTO artists (id, name, website) VALUES ($1, $2, $3) ON CONFLICT (id) DO UPDATE SET name=$2, website=$3",
|
||||
&artist.Id,
|
||||
&artist.Name,
|
||||
&artist.Website,
|
||||
)
|
||||
)
|
||||
|
||||
fmt.Printf("done!\n")
|
||||
fmt.Printf("done!\n")
|
||||
}
|
||||
|
||||
func PullAllArtists(db *sqlx.DB) ([]*music.Artist, error) {
|
||||
artists := []*music.Artist{}
|
||||
|
||||
rows, err := db.Query("SELECT id, name, website FROM artists")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var artist = music.Artist{}
|
||||
err = rows.Scan(&artist.Id, &artist.Name, &artist.Website)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
artists = append(artists, &artist)
|
||||
}
|
||||
|
||||
return artists, nil
|
||||
}
|
||||
|
||||
func PullArtist(db *sqlx.DB, artistID string) (music.Artist, error) {
|
||||
artist := music.Artist{}
|
||||
|
||||
err := db.Get(&artist, "SELECT id, name, website FROM artists WHERE id=$1", artistID)
|
||||
if err != nil {
|
||||
return music.Artist{}, err
|
||||
}
|
||||
|
||||
return artist, nil
|
||||
}
|
||||
|
||||
func PushRelease(db *sqlx.DB, release music.MusicRelease) {
|
||||
fmt.Printf("syncing release [%s] to database...", release.Id)
|
||||
fmt.Printf("pushing release [%s] to database...", release.Id)
|
||||
|
||||
tx := db.MustBegin()
|
||||
tx.MustExec("INSERT INTO musicreleases (id, title, type, release_date, artwork, buyname, buylink) VALUES ($1, $2, $3, $4, $5, $6, $7) "+
|
||||
|
@ -78,8 +67,8 @@ func PushRelease(db *sqlx.DB, release music.MusicRelease) {
|
|||
&release.Id, &link.Name, &link.Url)
|
||||
}
|
||||
for _, credit := range release.Credits {
|
||||
tx.MustExec("INSERT INTO musiccredits (release, artist, role, meta) VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING",
|
||||
&release.Id, &credit.Artist.Id, &credit.Role, &credit.Meta)
|
||||
tx.MustExec("INSERT INTO musiccredits (release, artist, role, is_primary) VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING",
|
||||
&release.Id, &credit.Artist.Id, &credit.Role, &credit.Primary)
|
||||
}
|
||||
for _, track := range release.Tracks {
|
||||
tx.MustExec("INSERT INTO musictracks (release, number, title, description, lyrics, preview_url) VALUES ($1, $2, $3, $4, $5, $6) "+
|
||||
|
@ -92,6 +81,114 @@ func PushRelease(db *sqlx.DB, release music.MusicRelease) {
|
|||
fmt.Printf("done!\n")
|
||||
}
|
||||
|
||||
func PullAllReleases(db *sqlx.DB) ([]*music.MusicRelease, error) {
|
||||
releases := []*music.MusicRelease{}
|
||||
|
||||
rows, err := db.Query("SELECT id, title, type, release_date, artwork, buyname, buylink FROM musicreleases")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var release = music.MusicRelease{}
|
||||
release.Credits = []music.MusicCredit{}
|
||||
release.Links = []music.MusicLink{}
|
||||
release.Tracks = []music.MusicTrack{}
|
||||
|
||||
err = rows.Scan(
|
||||
&release.Id,
|
||||
&release.Title,
|
||||
&release.Type,
|
||||
&release.ReleaseDate,
|
||||
&release.Artwork,
|
||||
&release.Buyname,
|
||||
&release.Buylink)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// pull musiccredits for artist data
|
||||
credit_rows, err := db.Query("SELECT artist, role, is_primary FROM musiccredits WHERE release=$1", release.Id)
|
||||
if err != nil {
|
||||
fmt.Printf("error pulling credits for %s: %v\n", release.Id, err)
|
||||
continue
|
||||
}
|
||||
for credit_rows.Next() {
|
||||
var artistID string
|
||||
var credit = music.MusicCredit{}
|
||||
err = credit_rows.Scan(
|
||||
&artistID,
|
||||
&credit.Role,
|
||||
&credit.Primary)
|
||||
if err != nil {
|
||||
fmt.Printf("error pulling credit for %s: %v\n", release.Id, err)
|
||||
continue
|
||||
}
|
||||
artist, err := music.GetArtist(artistID)
|
||||
if err != nil {
|
||||
fmt.Printf("error pulling credit for %s: %v\n", release.Id, err)
|
||||
continue
|
||||
}
|
||||
credit.Artist = artist
|
||||
release.Credits = append(release.Credits, credit)
|
||||
}
|
||||
|
||||
// pull musiclinks for link data
|
||||
link_rows, err := db.Query("SELECT name, url FROM musiclinks WHERE release=$1", release.Id);
|
||||
if err != nil {
|
||||
fmt.Printf("error pulling links for %s: %v\n", release.Id, err)
|
||||
continue
|
||||
}
|
||||
for link_rows.Next() {
|
||||
var link = music.MusicLink{}
|
||||
err = link_rows.Scan(
|
||||
&link.Name,
|
||||
&link.Url)
|
||||
if err != nil {
|
||||
fmt.Printf("error pulling link for %s: %v\n", release.Id, err)
|
||||
continue
|
||||
}
|
||||
release.Links = append(release.Links, link)
|
||||
}
|
||||
|
||||
// pull musictracks for track data
|
||||
track_rows, err := db.Query("SELECT number, title, description, lyrics, preview_url FROM musictracks WHERE release=$1", release.Id);
|
||||
if err != nil {
|
||||
fmt.Printf("error pulling tracks for %s: %v\n", release.Id, err)
|
||||
continue
|
||||
}
|
||||
for track_rows.Next() {
|
||||
var track = music.MusicTrack{}
|
||||
err = track_rows.Scan(
|
||||
&track.Number,
|
||||
&track.Title,
|
||||
&track.Description,
|
||||
&track.Lyrics,
|
||||
&track.PreviewUrl)
|
||||
if err != nil {
|
||||
fmt.Printf("error pulling track for %s: %v\n", release.Id, err)
|
||||
continue
|
||||
}
|
||||
release.Tracks = append(release.Tracks, track)
|
||||
}
|
||||
|
||||
releases = append(releases, &release)
|
||||
}
|
||||
|
||||
return releases, nil
|
||||
}
|
||||
|
||||
func PullRelease(db *sqlx.DB, releaseID string) (music.MusicRelease, error) {
|
||||
release := music.MusicRelease{}
|
||||
|
||||
err := db.Get(&release, "SELECT id, title, type, release_date, artwork, buyname, buylink FROM musicreleases WHERE id=$1", releaseID)
|
||||
if err != nil {
|
||||
return music.MusicRelease{}, err
|
||||
}
|
||||
|
||||
return release, nil
|
||||
}
|
||||
|
||||
func InitDatabase() *sqlx.DB {
|
||||
db, err := sqlx.Connect("postgres", "user=arimelody dbname=arimelody password=fuckingpassword sslmode=disable")
|
||||
if err != nil {
|
||||
|
@ -103,8 +200,5 @@ func InitDatabase() *sqlx.DB {
|
|||
db.SetMaxOpenConns(10)
|
||||
db.SetMaxIdleConns(10)
|
||||
|
||||
db.MustExec(schema)
|
||||
fmt.Printf("database schema synchronised.\n")
|
||||
|
||||
return db
|
||||
}
|
||||
|
|
44
discord/discord.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
package discord
|
||||
|
||||
const API_ENDPOINT = "https://discord.com/api/v10"
|
||||
const CLIENT_ID = "1268013769578119208"
|
||||
// TODO: good GOD change this later please i beg you. we've already broken
|
||||
// the rules by doing this at all
|
||||
const CLIENT_SECRET = "JUEZnixhN7BxmLIHmbECiKETMP85VT0E"
|
||||
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"
|
||||
// TODO: change before prod
|
||||
const MY_REDIRECT_URI = "http://127.0.0.1:8080/api/v1/admin/login"
|
||||
|
||||
type (
|
||||
AccessTokenResponse struct {
|
||||
TokenType string `json:"token_type"`
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
Scope string `json:"scope"`
|
||||
}
|
||||
|
||||
AuthInfoResponse struct {
|
||||
Application struct {
|
||||
Id string
|
||||
Name string
|
||||
Icon string
|
||||
Description string
|
||||
Hook bool
|
||||
BotPublic bool
|
||||
botRequireCodeGrant bool
|
||||
VerifyKey bool
|
||||
}
|
||||
Scopes []string
|
||||
Expires string
|
||||
User struct {
|
||||
Id string
|
||||
Username string
|
||||
Avatar string
|
||||
Discriminator string
|
||||
GlobalName string
|
||||
PublicFlags int
|
||||
}
|
||||
}
|
||||
)
|
||||
|
34
global/global.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
package global
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
var LAST_MODIFIED = time.Now()
|
||||
|
||||
var MimeTypes = map[string]string{
|
||||
"css": "text/css; charset=utf-8",
|
||||
"png": "image/png",
|
||||
"jpg": "image/jpg",
|
||||
"webp": "image/webp",
|
||||
"html": "text/html",
|
||||
"asc": "text/plain",
|
||||
"pub": "text/plain",
|
||||
"js": "application/javascript",
|
||||
}
|
||||
|
||||
func IsModified(req *http.Request, last_modified time.Time) bool {
|
||||
if len(req.Header["If-Modified-Since"]) == 0 || len(req.Header["If-Modified-Since"][0]) == 0 {
|
||||
return true
|
||||
}
|
||||
request_time, err := time.Parse(http.TimeFormat, req.Header["If-Modified-Since"][0])
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
if request_time.Before(last_modified) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
446
main.go
|
@ -5,278 +5,212 @@ import (
|
|||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"arimelody.me/arimelody.me/api"
|
||||
"arimelody.me/arimelody.me/api/v1/admin"
|
||||
"arimelody.me/arimelody.me/api/v1/music"
|
||||
|
||||
"github.com/gomarkdown/markdown"
|
||||
"github.com/gomarkdown/markdown/html"
|
||||
"github.com/gomarkdown/markdown/parser"
|
||||
)
|
||||
|
||||
const PORT int = 8080
|
||||
var LAST_MODIFIED = time.Now()
|
||||
|
||||
var mime_types = map[string]string{
|
||||
"css": "text/css; charset=utf-8",
|
||||
"png": "image/png",
|
||||
"jpg": "image/jpg",
|
||||
"webp": "image/webp",
|
||||
"html": "text/html",
|
||||
"asc": "text/plain",
|
||||
"pub": "text/plain",
|
||||
"js": "application/javascript",
|
||||
}
|
||||
const DEFAULT_PORT int = 8080
|
||||
|
||||
var base_template = template.Must(template.ParseFiles(
|
||||
"views/base.html",
|
||||
"views/header.html",
|
||||
"views/footer.html",
|
||||
"views/prideflag.html",
|
||||
"views/base.html",
|
||||
"views/header.html",
|
||||
"views/footer.html",
|
||||
"views/prideflag.html",
|
||||
))
|
||||
// var htmx_template = template.Must(template.New("root").Parse(`<head>{{block "head" .}}{{end}}</head>{{block "content" .}}{{end}}`))
|
||||
|
||||
func log_request(req *http.Request, code int, start_time time.Time) {
|
||||
now := time.Now()
|
||||
difference := (now.Nanosecond() - start_time.Nanosecond()) / 1_000_000
|
||||
elapsed := "<1"
|
||||
if difference >= 1 {
|
||||
elapsed = strconv.Itoa(difference)
|
||||
}
|
||||
now := time.Now()
|
||||
difference := (now.Nanosecond() - start_time.Nanosecond()) / 1_000_000
|
||||
elapsed := "<1"
|
||||
if difference >= 1 {
|
||||
elapsed = strconv.Itoa(difference)
|
||||
}
|
||||
|
||||
fmt.Printf("[%s] %s %s - %d (%sms) (%s)\n",
|
||||
now.Format(time.UnixDate),
|
||||
req.Method,
|
||||
req.URL.Path,
|
||||
code,
|
||||
elapsed,
|
||||
req.Header["User-Agent"][0],
|
||||
)
|
||||
fmt.Printf("[%s] %s %s - %d (%sms) (%s)\n",
|
||||
now.Format(time.UnixDate),
|
||||
req.Method,
|
||||
req.URL.Path,
|
||||
code,
|
||||
elapsed,
|
||||
req.Header["User-Agent"][0])
|
||||
}
|
||||
|
||||
func handle_request(writer http.ResponseWriter, req *http.Request) {
|
||||
uri := req.URL.Path
|
||||
start_time := time.Now()
|
||||
|
||||
// hx_request := len(req.Header["Hx-Request"]) > 0 && req.Header["Hx-Request"][0] == "true"
|
||||
//
|
||||
// // don't bother fulfilling requests to a page that's already loaded on the client!
|
||||
// if hx_request && len(req.Header["Referer"]) > 0 && len(req.Header["Hx-Current-Url"]) > 0 {
|
||||
// regex := regexp.MustCompile(`https?:\/\/[^\/]+`)
|
||||
// current_location := regex.ReplaceAllString(req.Header["Hx-Current-Url"][0], "")
|
||||
// if current_location == req.URL.Path {
|
||||
// writer.WriteHeader(204);
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
|
||||
writer.Header().Set("Server", "arimelody.me")
|
||||
writer.Header().Set("Cache-Control", "max-age=86400")
|
||||
writer.Header().Set("Last-Modified", LAST_MODIFIED.Format(http.TimeFormat))
|
||||
|
||||
code := func(writer http.ResponseWriter, req *http.Request) int {
|
||||
// var root *template.Template
|
||||
// if hx_request {
|
||||
// root = template.Must(htmx_template.Clone())
|
||||
// } else {
|
||||
// root = template.Must(base_template.Clone())
|
||||
// }
|
||||
|
||||
var root = template.Must(base_template.Clone())
|
||||
|
||||
if req.URL.Path == "/" {
|
||||
return handle_index(writer, req, root)
|
||||
}
|
||||
|
||||
if uri == "/music" || uri == "/music/" {
|
||||
return handle_music(writer, req, root)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(uri, "/music/") {
|
||||
return handle_music_gateway(writer, req, root)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(uri, "/admin") {
|
||||
return handle_admin(writer, req, root)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(uri, "/api") {
|
||||
return api.Handle(writer, req, root)
|
||||
}
|
||||
|
||||
return static_handler(writer, req, root)
|
||||
}(writer, req)
|
||||
|
||||
log_request(req, code, start_time)
|
||||
}
|
||||
|
||||
func handle_index(writer http.ResponseWriter, req *http.Request, root *template.Template) int {
|
||||
if !was_modified(req, LAST_MODIFIED) {
|
||||
writer.WriteHeader(304)
|
||||
return 304
|
||||
}
|
||||
|
||||
index_template := template.Must(root.ParseFiles("views/index.html"))
|
||||
err := index_template.Execute(writer, nil)
|
||||
if err != nil {
|
||||
http.Error(writer, err.Error(), http.StatusInternalServerError)
|
||||
return 500
|
||||
}
|
||||
return 200
|
||||
}
|
||||
|
||||
func handle_music(writer http.ResponseWriter, req *http.Request, root *template.Template) int {
|
||||
if !was_modified(req, LAST_MODIFIED) {
|
||||
writer.WriteHeader(304)
|
||||
return 304
|
||||
}
|
||||
|
||||
music_template := template.Must(root.ParseFiles("views/music.html"))
|
||||
music := music.QueryAllMusic()
|
||||
err := music_template.Execute(writer, music)
|
||||
if err != nil {
|
||||
http.Error(writer, err.Error(), http.StatusInternalServerError)
|
||||
return 500
|
||||
}
|
||||
return 200
|
||||
}
|
||||
|
||||
func handle_music_gateway(writer http.ResponseWriter, req *http.Request, root *template.Template) int {
|
||||
if !was_modified(req, LAST_MODIFIED) {
|
||||
writer.WriteHeader(304)
|
||||
return 304
|
||||
}
|
||||
|
||||
id := req.URL.Path[len("/music/"):]
|
||||
// http.Redirect(writer, req, "https://mellodoot.com/music/"+title, 302)
|
||||
// return
|
||||
release, ok := music.GetRelease(id)
|
||||
if !ok {
|
||||
return handle_not_found(writer, req, root)
|
||||
}
|
||||
gateway_template := template.Must(root.ParseFiles("views/music-gateway.html"))
|
||||
err := gateway_template.Execute(writer, release)
|
||||
if err != nil {
|
||||
http.Error(writer, err.Error(), http.StatusInternalServerError)
|
||||
return 500
|
||||
}
|
||||
return 200
|
||||
}
|
||||
|
||||
func handle_admin(writer http.ResponseWriter, req *http.Request, root *template.Template) int {
|
||||
if !was_modified(req, LAST_MODIFIED) {
|
||||
writer.WriteHeader(304)
|
||||
return 304
|
||||
}
|
||||
|
||||
admin_template := template.Must(root.ParseFiles("views/admin.html"))
|
||||
err := admin_template.Execute(writer, nil)
|
||||
if err != nil {
|
||||
http.Error(writer, err.Error(), http.StatusInternalServerError)
|
||||
return 500
|
||||
}
|
||||
return 200
|
||||
}
|
||||
|
||||
func static_handler(writer http.ResponseWriter, req *http.Request, root *template.Template) int {
|
||||
filename := "public/" + req.URL.Path[1:]
|
||||
|
||||
// check the file's metadata
|
||||
info, err := os.Stat(filename)
|
||||
if err != nil {
|
||||
return handle_not_found(writer, req, root)
|
||||
}
|
||||
|
||||
if !was_modified(req, info.ModTime()) {
|
||||
writer.WriteHeader(304)
|
||||
return 304
|
||||
}
|
||||
|
||||
// set Last-Modified to file modification date
|
||||
writer.Header().Set("Last-Modified", info.ModTime().Format(http.TimeFormat))
|
||||
|
||||
// read the file
|
||||
body, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
http.Error(writer, err.Error(), http.StatusInternalServerError)
|
||||
return 500
|
||||
}
|
||||
|
||||
// setting MIME types
|
||||
filetype := filename[strings.LastIndex(filename, ".")+1:]
|
||||
if mime_type, ok := mime_types[filetype]; ok {
|
||||
writer.Header().Set("Content-Type", mime_type)
|
||||
} else {
|
||||
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
}
|
||||
|
||||
writer.Write([]byte(body))
|
||||
return 200
|
||||
}
|
||||
|
||||
func handle_not_found(writer http.ResponseWriter, req *http.Request, root *template.Template) int {
|
||||
type ErrorData struct {
|
||||
Target string
|
||||
}
|
||||
error_data := ErrorData{ Target: req.URL.Path }
|
||||
writer.WriteHeader(404);
|
||||
error_template := template.Must(root.ParseFiles("views/404.html"))
|
||||
err := error_template.Execute(writer, error_data)
|
||||
if err != nil {
|
||||
http.Error(writer, err.Error(), http.StatusInternalServerError)
|
||||
return 500
|
||||
}
|
||||
return 404
|
||||
}
|
||||
|
||||
func was_modified(req *http.Request, last_modified time.Time) bool {
|
||||
if len(req.Header["If-Modified-Since"]) == 0 || len(req.Header["If-Modified-Since"][0]) == 0 {
|
||||
return true
|
||||
}
|
||||
request_time, err := time.Parse(http.TimeFormat, req.Header["If-Modified-Since"][0])
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
if request_time.Before(last_modified) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func parse_markdown(md []byte) []byte {
|
||||
extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock
|
||||
p := parser.NewWithExtensions(extensions)
|
||||
doc := p.Parse(md)
|
||||
|
||||
htmlFlags := html.CommonFlags
|
||||
opts := html.RendererOptions{Flags: htmlFlags}
|
||||
renderer := html.NewRenderer(opts)
|
||||
|
||||
return markdown.Render(doc, renderer)
|
||||
}
|
||||
|
||||
func push_to_db_this_is_a_testing_thing_and_will_be_superfluous_later() {
|
||||
db := InitDatabase()
|
||||
defer db.Close()
|
||||
|
||||
for _, artist := range music.QueryAllArtists() {
|
||||
PushArtist(db, artist)
|
||||
}
|
||||
|
||||
for _, album := range music.QueryAllMusic() {
|
||||
PushRelease(db, album)
|
||||
}
|
||||
}
|
||||
// func handle_request(res http.ResponseWriter, req *http.Request) {
|
||||
// uri := req.URL.Path
|
||||
// start_time := time.Now()
|
||||
//
|
||||
// res.Header().Set("Server", "arimelody.me")
|
||||
//
|
||||
// code := func(res http.ResponseWriter, req *http.Request) int {
|
||||
// var root = template.Must(base_template.Clone())
|
||||
//
|
||||
// if req.URL.Path == "/" {
|
||||
// return handle_index(res, req, root)
|
||||
// }
|
||||
//
|
||||
// if uri == "/music" || uri == "/music/" {
|
||||
// return handle_music(res, req, root)
|
||||
// }
|
||||
//
|
||||
// if strings.HasPrefix(uri, "/music/") {
|
||||
// return handle_music_gateway(res, req, root)
|
||||
// }
|
||||
//
|
||||
// if strings.HasPrefix(uri, "/admin") {
|
||||
// return admin.Handle(res, req, root)
|
||||
// }
|
||||
//
|
||||
// if strings.HasPrefix(uri, "/api") {
|
||||
// return api.Handle(res, req, root)
|
||||
// }
|
||||
//
|
||||
// return static_handler(res, req, root)
|
||||
// }(res, req)
|
||||
//
|
||||
// log_request(req, code, start_time)
|
||||
// }
|
||||
//
|
||||
// func handle_index(res http.ResponseWriter, req *http.Request, root *template.Template) int {
|
||||
// if !global.IsModified(req, global.LAST_MODIFIED) {
|
||||
// res.WriteHeader(304)
|
||||
// return 304
|
||||
// }
|
||||
//
|
||||
// index_template := template.Must(root.ParseFiles("views/index.html"))
|
||||
// err := index_template.Execute(res, nil)
|
||||
// if err != nil {
|
||||
// http.Error(res, err.Error(), http.StatusInternalServerError)
|
||||
// return 500
|
||||
// }
|
||||
// return 200
|
||||
// }
|
||||
//
|
||||
// func handle_music(res http.ResponseWriter, req *http.Request, root *template.Template) int {
|
||||
// if !global.IsModified(req, global.LAST_MODIFIED) {
|
||||
// res.WriteHeader(304)
|
||||
// return 304
|
||||
// }
|
||||
//
|
||||
// music_template := template.Must(root.ParseFiles("views/music.html"))
|
||||
// err := music_template.Execute(res, music.Releases)
|
||||
// if err != nil {
|
||||
// http.Error(res, err.Error(), http.StatusInternalServerError)
|
||||
// return 500
|
||||
// }
|
||||
// return 200
|
||||
// }
|
||||
//
|
||||
// func handle_music_gateway(res http.ResponseWriter, req *http.Request, root *template.Template) int {
|
||||
// if !global.IsModified(req, global.LAST_MODIFIED) {
|
||||
// res.WriteHeader(304)
|
||||
// return 304
|
||||
// }
|
||||
//
|
||||
// id := req.URL.Path[len("/music/"):]
|
||||
// release, err := music.GetRelease(id)
|
||||
// if err != nil {
|
||||
// return handle_not_found(res, req, root)
|
||||
// }
|
||||
// gateway_template := template.Must(root.ParseFiles("views/music-gateway.html"))
|
||||
// err = gateway_template.Execute(res, release)
|
||||
// if err != nil {
|
||||
// http.Error(res, err.Error(), http.StatusInternalServerError)
|
||||
// return 500
|
||||
// }
|
||||
// return 200
|
||||
// }
|
||||
//
|
||||
// func static_handler(res http.ResponseWriter, req *http.Request, root *template.Template) int {
|
||||
// filename := "public/" + req.URL.Path[1:]
|
||||
//
|
||||
// // check the file's metadata
|
||||
// info, err := os.Stat(filename)
|
||||
// if err != nil {
|
||||
// return handle_not_found(res, req, root)
|
||||
// }
|
||||
//
|
||||
// if !global.IsModified(req, info.ModTime()) {
|
||||
// res.WriteHeader(304)
|
||||
// return 304
|
||||
// }
|
||||
//
|
||||
// // set Last-Modified to file modification date
|
||||
// res.Header().Set("Last-Modified", info.ModTime().Format(http.TimeFormat))
|
||||
//
|
||||
// // read the file
|
||||
// body, err := os.ReadFile(filename)
|
||||
// if err != nil {
|
||||
// http.Error(res, err.Error(), http.StatusInternalServerError)
|
||||
// return 500
|
||||
// }
|
||||
//
|
||||
// // setting MIME types
|
||||
// filetype := filename[strings.LastIndex(filename, ".")+1:]
|
||||
// if mime_type, ok := global.MimeTypes[filetype]; ok {
|
||||
// res.Header().Set("Content-Type", mime_type)
|
||||
// } else {
|
||||
// res.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
// }
|
||||
//
|
||||
// res.Write([]byte(body))
|
||||
// return 200
|
||||
// }
|
||||
//
|
||||
// func handle_not_found(res http.ResponseWriter, req *http.Request, root *template.Template) int {
|
||||
// type ErrorData struct {
|
||||
// Target string
|
||||
// }
|
||||
// error_data := ErrorData{ Target: req.URL.Path }
|
||||
// res.WriteHeader(404);
|
||||
// error_template := template.Must(root.ParseFiles("views/404.html"))
|
||||
// err := error_template.Execute(res, error_data)
|
||||
// if err != nil {
|
||||
// http.Error(res, err.Error(), http.StatusInternalServerError)
|
||||
// return 500
|
||||
// }
|
||||
// return 404
|
||||
// }
|
||||
//
|
||||
// func parse_markdown(md []byte) []byte {
|
||||
// extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock
|
||||
// p := parser.NewWithExtensions(extensions)
|
||||
// doc := p.Parse(md)
|
||||
//
|
||||
// htmlFlags := html.CommonFlags
|
||||
// opts := html.RendererOptions{Flags: htmlFlags}
|
||||
// renderer := html.NewRenderer(opts)
|
||||
//
|
||||
// return markdown.Render(doc, renderer)
|
||||
// }
|
||||
|
||||
func main() {
|
||||
push_to_db_this_is_a_testing_thing_and_will_be_superfluous_later()
|
||||
db := InitDatabase()
|
||||
defer db.Close()
|
||||
|
||||
var err error
|
||||
music.Artists, err = PullAllArtists(db)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to pull artists from database: %v\n", err);
|
||||
panic(1)
|
||||
}
|
||||
music.Releases, err = PullAllReleases(db)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to pull releases from database: %v\n", err);
|
||||
panic(1)
|
||||
}
|
||||
|
||||
http.HandleFunc("/", handle_request)
|
||||
mux := http.NewServeMux()
|
||||
|
||||
fmt.Printf("now serving at http://127.0.0.1:%d\n", PORT)
|
||||
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", PORT), nil))
|
||||
mux.Handle("/api/v1/admin", admin.Handler())
|
||||
mux.Handle("/api/v1/admin/login", admin.LoginHandler())
|
||||
mux.Handle("/api/v1/admin/callback", admin.OAuthCallbackHandler())
|
||||
mux.Handle("/api/v1/admin/verify", admin.AuthorisedHandler(admin.VerifyHandler()))
|
||||
mux.Handle("/api/v1/admin/logout", admin.AuthorisedHandler(admin.LogoutHandler()))
|
||||
|
||||
port := DEFAULT_PORT
|
||||
fmt.Printf("now serving at http://127.0.0.1:%d\n", port)
|
||||
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), mux))
|
||||
}
|
||||
|
|
BIN
public/img/buttons/elke.gif
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
public/img/buttons/itzzen.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
public/img/buttons/misc/blink.gif
Normal file
After Width: | Height: | Size: 6.4 KiB |
BIN
public/img/favicon-256.png
Normal file
After Width: | Height: | Size: 95 KiB |
BIN
public/img/favicon-36.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 944 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 24 KiB |
32
public/img/prideflag.svg
Normal file
|
@ -0,0 +1,32 @@
|
|||
<!--
|
||||
pride flag - copyright (c) 2024 ari melody
|
||||
|
||||
this code is provided AS-IS, WITHOUT ANY WARRANTY, to be
|
||||
freely redistributed and/or modified as you please, however
|
||||
retaining this license in any redistribution.
|
||||
|
||||
please use this flag to link to an LGBTQI+-supporting page
|
||||
of your choosing!
|
||||
|
||||
web: https://arimelody.me
|
||||
source: https://git.arimelody.me/ari/prideflag
|
||||
-->
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
|
||||
<path id="red" d="M120,80 L100,100 L120,120 Z" style="fill:#d20605"/>
|
||||
<path id="orange" d="M120,80 V40 L80,80 L100,100 Z" style="fill:#ef9c00"/>
|
||||
<path id="yellow" d="M120,40 V0 L60,60 L80,80 Z" style="fill:#e5fe02"/>
|
||||
<path id="green" d="M120,0 H80 L40,40 L60,60 Z" style="fill:#09be01"/>
|
||||
<path id="blue" d="M80,0 H40 L20,20 L40,40 Z" style="fill:#081a9a"/>
|
||||
<path id="purple" d="M40,0 H0 L20,20 Z" style="fill:#76008a"/>
|
||||
|
||||
<rect id="black" x="60" width="60" height="60" style="fill:#010101"/>
|
||||
<rect id="brown" x="70" width="50" height="50" style="fill:#603814"/>
|
||||
<rect id="lightblue" x="80" width="40" height="40" style="fill:#73d6ed"/>
|
||||
<rect id="pink" x="90" width="30" height="30" style="fill:#ffafc8"/>
|
||||
<rect id="white" x="100" width="20" height="20" style="fill:#fff"/>
|
||||
|
||||
<rect id="intyellow" x="110" width="10" height="10" style="fill:#fed800"/>
|
||||
<circle id="intpurple" cx="120" cy="0" r="5" stroke="#7601ad" stroke-width="2" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -1,79 +1,73 @@
|
|||
import "./main.js";
|
||||
|
||||
function apply_funny_bob_to_upcoming_tags() {
|
||||
const upcoming_tags = document.querySelectorAll("#type.upcoming");
|
||||
for (var i = 0; i < upcoming_tags.length; i++) {
|
||||
const tag = upcoming_tags[i];
|
||||
const chars = tag.innerText.split("");
|
||||
const result = chars.map((c, i) => `<span style="animation-delay: -${i * 100}ms;">${c}</span>`);
|
||||
tag.innerHTML = result.join("");
|
||||
}
|
||||
const upcoming_tags = document.querySelectorAll("#type.upcoming");
|
||||
for (var i = 0; i < upcoming_tags.length; i++) {
|
||||
const tag = upcoming_tags[i];
|
||||
const chars = tag.innerText.split("");
|
||||
const result = chars.map((c, i) => `<span style="animation-delay: -${i * 100}ms;">${c}</span>`);
|
||||
tag.innerHTML = result.join("");
|
||||
}
|
||||
}
|
||||
|
||||
function update_extras_buttons() {
|
||||
const extras_pairs = Array.from(document.querySelectorAll("div#info > div").values()).map(container => {
|
||||
return {
|
||||
container,
|
||||
button: document.getElementById("extras").querySelector(`ul li a[href="#${container.id}"]`)
|
||||
};
|
||||
});
|
||||
const info_container = document.getElementById("info")
|
||||
info_container.addEventListener("scroll", update_extras_buttons);
|
||||
const info_rect = info_container.getBoundingClientRect();
|
||||
const info_y = info_rect.y;
|
||||
const font_size = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||
let current = extras_pairs[0];
|
||||
extras_pairs.forEach(pair => {
|
||||
pair.button.classList.remove("active");
|
||||
const scroll_diff = pair.container.getBoundingClientRect().y -
|
||||
info_rect.y -
|
||||
info_rect.height / 2 +
|
||||
4 * font_size;
|
||||
if (scroll_diff <= 0) current = pair;
|
||||
})
|
||||
current.button.classList.add("active");
|
||||
const extras_pairs = Array.from(document.querySelectorAll("div#info > div").values()).map(container => {
|
||||
return {
|
||||
container,
|
||||
button: document.getElementById("extras").querySelector(`ul li a[href="#${container.id}"]`)
|
||||
};
|
||||
});
|
||||
const info_container = document.getElementById("info")
|
||||
info_container.addEventListener("scroll", update_extras_buttons);
|
||||
const info_rect = info_container.getBoundingClientRect();
|
||||
const info_y = info_rect.y;
|
||||
const font_size = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||
let current = extras_pairs[0];
|
||||
extras_pairs.forEach(pair => {
|
||||
pair.button.classList.remove("active");
|
||||
const scroll_diff = pair.container.getBoundingClientRect().y -
|
||||
info_rect.y -
|
||||
info_rect.height / 2 +
|
||||
4 * font_size;
|
||||
if (scroll_diff <= 0) current = pair;
|
||||
})
|
||||
current.button.classList.add("active");
|
||||
|
||||
document.querySelectorAll("div#extras ul li a[href]").forEach(link => {
|
||||
link.addEventListener("click", event => {
|
||||
event.preventDefault();
|
||||
location.replace(link.href);
|
||||
});
|
||||
document.querySelectorAll("div#extras ul li a[href]").forEach(link => {
|
||||
link.addEventListener("click", event => {
|
||||
event.preventDefault();
|
||||
const tag = link.href.split('#').slice(-1)[0];
|
||||
document.getElementById(tag).scrollIntoView();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function bind_go_back_btn() {
|
||||
const go_back_btn = document.getElementById("go-back")
|
||||
go_back_btn.innerText = "<";
|
||||
go_back_btn.addEventListener("click", () => {
|
||||
window.history.back();
|
||||
});
|
||||
const go_back_btn = document.getElementById("go-back")
|
||||
go_back_btn.innerText = "<";
|
||||
go_back_btn.addEventListener("click", () => {
|
||||
window.history.back();
|
||||
});
|
||||
}
|
||||
|
||||
function bind_share_btn() {
|
||||
const share_btn = document.getElementById("share");
|
||||
if (navigator.clipboard === undefined) {
|
||||
share_btn.onclick = event => {
|
||||
console.error("clipboard is not supported by this browser!");
|
||||
};
|
||||
return;
|
||||
}
|
||||
const share_btn = document.getElementById("share");
|
||||
if (navigator.clipboard === undefined) {
|
||||
share_btn.onclick = event => {
|
||||
event.preventDefault();
|
||||
navigator.clipboard.writeText(window.location.href);
|
||||
share_btn.classList.remove('active');
|
||||
void share_btn.offsetWidth;
|
||||
share_btn.classList.add('active');
|
||||
}
|
||||
console.error("clipboard is not supported by this browser!");
|
||||
};
|
||||
return;
|
||||
}
|
||||
share_btn.onclick = event => {
|
||||
event.preventDefault();
|
||||
navigator.clipboard.writeText(window.location.href);
|
||||
share_btn.classList.remove('active');
|
||||
void share_btn.offsetWidth;
|
||||
share_btn.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
function start() {
|
||||
bind_share_btn();
|
||||
bind_go_back_btn();
|
||||
apply_funny_bob_to_upcoming_tags();
|
||||
update_extras_buttons();
|
||||
}
|
||||
|
||||
document.addEventListener("swap", () => {
|
||||
if (!window.location.pathname.startsWith("/music/")) return;
|
||||
start();
|
||||
});
|
||||
bind_share_btn();
|
||||
bind_go_back_btn();
|
||||
apply_funny_bob_to_upcoming_tags();
|
||||
update_extras_buttons();
|
||||
|
|
52
schema.sql
Normal file
|
@ -0,0 +1,52 @@
|
|||
CREATE TABLE IF NOT EXISTS artists (
|
||||
id text NOT NULL,
|
||||
name text,
|
||||
website text
|
||||
);
|
||||
ALTER TABLE artists ADD CONSTRAINT artists_pk PRIMARY KEY (id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS musicreleases (
|
||||
id character varying(64) NOT NULL,
|
||||
title text NOT NULL,
|
||||
type text,
|
||||
release_date DATE NOT NULL,
|
||||
artwork text,
|
||||
buyname text,
|
||||
buylink text
|
||||
);
|
||||
ALTER TABLE musicreleases ADD CONSTRAINT musicreleases_pk PRIMARY KEY (id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS musiclinks (
|
||||
release character varying(64) NOT NULL,
|
||||
name text NOT NULL,
|
||||
url text,
|
||||
);
|
||||
ALTER TABLE musiclinks ADD CONSTRAINT musiclinks_pk PRIMARY KEY (release, name);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS musiccredits (
|
||||
release character varying(64) NOT NULL,
|
||||
artist text NOT NULL,
|
||||
role text,
|
||||
is_primary boolean,
|
||||
);
|
||||
ALTER TABLE musiccredits ADD CONSTRAINT musiccredits_pk PRIMARY KEY (release, artist);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS musictracks (
|
||||
release character varying(64) NOT NULL,
|
||||
number integer NOT NULL,
|
||||
title text NOT NULL,
|
||||
description text,
|
||||
lyrics text,
|
||||
preview_url text,
|
||||
);
|
||||
ALTER TABLE musictracks ADD CONSTRAINT musictracks_pk PRIMARY KEY (release, number);
|
||||
|
||||
-- foreign keys
|
||||
|
||||
ALTER TABLE public.musiccredits ADD CONSTRAINT musiccredits_artist_fk FOREIGN KEY (artist) REFERENCES public.artists(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE public.musiccredits ADD CONSTRAINT musiccredits_release_fk FOREIGN KEY (release) REFERENCES public.musicreleases(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE public.musiclinks ADD CONSTRAINT musiclinks_release_fk FOREIGN KEY (release) REFERENCES public.musicreleases(id) ON UPDATE CASCADE ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE public.musictracks ADD CONSTRAINT musictracks_release_fk FOREIGN KEY (release) REFERENCES public.musicreleases(id) ON DELETE CASCADE;
|
|
@ -14,7 +14,9 @@
|
|||
</h1>
|
||||
|
||||
<p>
|
||||
bappity boopity
|
||||
whapow! nothing here.
|
||||
<br>
|
||||
nice try, though.
|
||||
</p>
|
||||
</main>
|
||||
{{end}}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<!-- <script type="application/javascript" src="/script/lib/htmx.js"></script> -->
|
||||
<!-- <script type="application/javascript" src="/script/lib/htmx.min.js"></script> -->
|
||||
<!-- <script type="application/javascript" src="/script/lib/htmx-preload.js"></script> -->
|
||||
<script type="application/javascript" src="/script/swap.js"></script>
|
||||
<!-- <script type="application/javascript" src="/script/swap.js"></script> -->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
311
views/index.html
|
@ -20,169 +20,166 @@
|
|||
|
||||
{{define "content"}}
|
||||
<main>
|
||||
<script type="module" src="/script/main.js"></script>
|
||||
<h1>
|
||||
# hello, world!
|
||||
</h1>
|
||||
|
||||
<h1>
|
||||
# hello, world!
|
||||
</h1>
|
||||
<p>
|
||||
<strong>i'm ari!</strong>
|
||||
<br>
|
||||
<small>she/her 🏳️⚧️🏳️🌈💫🦆🇮🇪</small>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
i'm a <a href="/music">musician</a>, <a href="https://github.com/arimelody?tab=repositories">developer</a>,
|
||||
<a href="https://twitch.tv/arispacegirl">streamer</a>, <a href="https://youtube.com/@arispacegirl">youtuber</a>,
|
||||
and probably a bunch of other things i forgot to mention!
|
||||
</p>
|
||||
<p>
|
||||
you're very welcome to take a look around my little space on the internet here,
|
||||
or explore any of the other parts i inhabit!
|
||||
</p>
|
||||
<p>
|
||||
if you're looking to support me financially, that's so cool of you!!
|
||||
if you like, you can buy some of my music over on
|
||||
<a href="https://arimelody.bandcamp.com" target="_blank">bandcamp</a>
|
||||
so you can at least get something for your money.
|
||||
thank you very much either way!! 💕
|
||||
</p>
|
||||
<p>
|
||||
for anything else, you can reach me for any and all communications through
|
||||
<a href="mailto:ari@arimelody.me">ari@arimelody.me</a>. if your message
|
||||
contains anything beyond a silly gag, i strongly recommend encrypting
|
||||
your message using my public pgp key, listed below!
|
||||
</p>
|
||||
<p>
|
||||
thank you for stopping by- i hope you have a lovely rest of your day! 💫
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>i'm ari!</strong>
|
||||
<br>
|
||||
<small>she/her 🏳️⚧️🏳️🌈💫🦆🇮🇪</small>
|
||||
</p>
|
||||
<hr>
|
||||
|
||||
<p>
|
||||
i like to create things on the internet! namely <a href="/music">music</a>,
|
||||
<a href="https://youtube.com/mellodoot" target="_blank">videos</a>, art <small><em>(link pending)</em></small>, and the odd
|
||||
<a href="https://github.com/mellodoot?tab=repositories" target="_blank">code project</a> from time to time!
|
||||
if it's a thing you can create on a computer, i've probably taken a swing at it.
|
||||
</p>
|
||||
<p>
|
||||
you're very welcome to take a look around my little slice of the internet here,
|
||||
or explore any of the other corners i inhabit!
|
||||
</p>
|
||||
<p>
|
||||
and if you're looking to support me financially, that's so cool of you!!
|
||||
if you like, you can buy one or more of my songs over on
|
||||
<a href="https://arimelody.bandcamp.com" target="_blank">bandcamp</a>,
|
||||
so you can at least get something for your money.
|
||||
thank you very much either way!! 💕
|
||||
</p>
|
||||
<p>
|
||||
for anything else, you can reach me for any and all communications through
|
||||
<a href="mailto:ari@arimelody.me">ari@arimelody.me</a>. if your message
|
||||
contains anything beyond a silly gag, i strongly recommend encrypting
|
||||
your message using my public pgp key, listed below!
|
||||
</p>
|
||||
<p>
|
||||
thank you for stopping by- i hope you have a lovely rest of your day! 💫
|
||||
</p>
|
||||
<h2>
|
||||
## metadata
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
<strong>my colours 🌈</strong>
|
||||
</p>
|
||||
<ul>
|
||||
<li>primary: <span class="col-primary">#b7fd49</span></li>
|
||||
<li>secondary: <span class="col-secondary">#f8e05b</span></li>
|
||||
<li>tertiary: <span class="col-tertiary">#f788fe</span></li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
<strong>my keys 🔑</strong>
|
||||
</p>
|
||||
<ul>
|
||||
<li>pgp: <a href="/keys/ari melody_0x92678188_public.asc" target="_blank">[link]</a></li>
|
||||
<li>ssh (ed25519): <a href="/keys/id_ari_ed25519.pub" target="_blank">[link]</a></li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
<strong>where to find me 🛰️</strong>
|
||||
</p>
|
||||
<ul class="links">
|
||||
<li>
|
||||
<a href="https://youtube.com/@arispacegirl" target="_blank">youtube</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://twitch.tv/arispacegirl" target="_blank">twitch</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://sptfy.com/mellodoot" target="_blank">spotify</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://arimelody.bandcamp.com" target="_blank">bandcamp</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/arimelody" target="_blank">github</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
<strong>projects i've worked on 🛠️</strong>
|
||||
</p>
|
||||
<ul class="links">
|
||||
<li>
|
||||
<a href="https://catdance.arimelody.me" target="_blank">
|
||||
catdance
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://git.arimelody.me/ari/prideflag" target="_blank">
|
||||
pride flag
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/arimelody/ipaddrgen" target="_blank">
|
||||
ipaddrgen
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://impact.arimelody.me/" target="_blank">
|
||||
impact meme
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://term.arimelody.me/" target="_blank">
|
||||
OpenTerminal
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2>
|
||||
## cool people
|
||||
</h2>
|
||||
|
||||
<div id="web-buttons">
|
||||
<a href="https://arimelody.me">
|
||||
<img src="/img/buttons/ari melody.gif" alt="ari melody web button" width="88" height="31">
|
||||
</a>
|
||||
<a href="https://supitszaire.com" target="_blank">
|
||||
<img src="/img/buttons/zaire.gif" alt="zaire web button" width="88" height="31">
|
||||
</a>
|
||||
<a href="https://mae.wtf" target="_blank">
|
||||
<img src="/img/buttons/mae.png" alt="vimae web button" width="88" height="31">
|
||||
</a>
|
||||
<a href="https://zvava.org" target="_blank">
|
||||
<img src="/img/buttons/zvava.png" alt="zvava web button" width="88" height="31">
|
||||
</a>
|
||||
<a href="https://elke.cafe" target="_blank">
|
||||
<img src="/img/buttons/elke.gif" alt="elke web button" width="88" height="31">
|
||||
</a>
|
||||
<a href="https://itzzen.net" target="_blank">
|
||||
<img src="/img/buttons/itzzen.png" alt="itzzen web button" width="88" height="31">
|
||||
</a>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2>
|
||||
## metadata
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
<strong>my colours 🌈</strong>
|
||||
</p>
|
||||
<ul>
|
||||
<li>primary: <span class="col-primary">#b7fd49</span></li>
|
||||
<li>secondary: <span class="col-secondary">#f8e05b</span></li>
|
||||
<li>tertiary: <span class="col-tertiary">#f788fe</span></li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
<strong>my keys 🔑</strong>
|
||||
</p>
|
||||
<ul>
|
||||
<li>pgp: <a href="/keys/ari melody_0x92678188_public.asc" target="_blank">[link]</a></li>
|
||||
<li>ssh (ed25519): <a href="/keys/id_ari_ed25519.pub" target="_blank">[link]</a></li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
<strong>where to find me 🛰️</strong>
|
||||
</p>
|
||||
<ul class="links">
|
||||
<li>
|
||||
<a href="https://youtube.com/@mellodoot" target="_blank">youtube</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://mellodoot.tumblr.com" target="_blank">tumblr</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://twitch.tv/mellodoot" target="_blank">twitch</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://sptfy.com/mellodoot" target="_blank">spotify</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://soundcloud.com/arimelody" target="_blank">soundcloud</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/mellodoot" target="_blank">github</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
<strong>projects i've worked on 🛠️</strong>
|
||||
</p>
|
||||
<ul class="links">
|
||||
<li>
|
||||
<a href="https://catdance.xyz" target="_blank">
|
||||
catdance
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/mellodoot/sandblock" target="_blank">
|
||||
sandblock
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/mellodoot/prideflag" target="_blank">
|
||||
pride flag
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/mellodoot/ipaddrgen" target="_blank">
|
||||
ipaddrgen
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://impact.arimelody.me/" target="_blank">
|
||||
impact meme
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://term.arimelody.me/" target="_blank">
|
||||
OpenTerminal
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2>## cool people</h2>
|
||||
|
||||
<div id="web-buttons">
|
||||
<a href="https://arimelody.me">
|
||||
<img src="/img/buttons/ari melody.gif" alt="ari melody web button" loading="lazy">
|
||||
</a>
|
||||
<a href="https://supitszaire.com" target="_blank">
|
||||
<img src="/img/buttons/zaire.gif" alt="zaire web button" loading="lazy">
|
||||
</a>
|
||||
<a href="https://mae.wtf" target="_blank">
|
||||
<img src="/img/buttons/mae.png" alt="vimae web button" loading="lazy">
|
||||
</a>
|
||||
<a href="https://zvava.org" target="_blank">
|
||||
<img src="/img/buttons/zvava.png" alt="zvava web button" loading="lazy">
|
||||
</a>
|
||||
|
||||
<hr>
|
||||
|
||||
<img src="/img/buttons/misc/debian.gif" alt="powered by debian" loading="lazy">
|
||||
<img src="/img/buttons/misc/girls4notepad.gif" alt="girls 4 notepad" loading="lazy">
|
||||
<img src="/img/buttons/misc/gaywebring.gif" alt="this website is GAY" loading="lazy">
|
||||
<img src="/img/buttons/misc/graphicdesign.gif" alt="graphic design is my passion" loading="lazy">
|
||||
<img src="/img/buttons/misc/gplv3.gif" alt="GPLv3 free software" loading="lazy">
|
||||
<img src="/img/buttons/misc/hl.gif" alt="half-life" loading="lazy">
|
||||
<img src="/img/buttons/misc/h-free-anim.gif" alt="dis site is hentai FREE" loading="lazy">
|
||||
<img src="/img/buttons/misc/sprunk.gif" alt="sprunk" loading="lazy">
|
||||
<img src="/img/buttons/misc/tohell.gif" alt="go straight to hell" loading="lazy">
|
||||
<img src="/img/buttons/misc/virusalert.gif" alt="virus alert! click here" onclick="alert('meow :3')" loading="lazy">
|
||||
<img src="/img/buttons/misc/wii.gif" alt="wii" loading="lazy">
|
||||
<img src="/img/buttons/misc/www2.gif" alt="www" loading="lazy">
|
||||
<img src="/img/buttons/misc/iemandatory.gif" alt="get mandatory internet explorer" loading="lazy">
|
||||
<img src="/img/buttons/misc/learn_html.gif" alt="HTML - learn it today!" loading="lazy">
|
||||
<a href="https://smokepowered.com" target="_blank">
|
||||
<img src="/img/buttons/misc/smokepowered.gif" alt="high on SMOKE" loading="lazy">
|
||||
</a>
|
||||
<a href="https://epicblazed.com" target="_blank">
|
||||
<img src="/img/buttons/misc/epicblazed.png" alt="epic blazed" loading="lazy">
|
||||
</a>
|
||||
</div>
|
||||
<img src="/img/buttons/misc/debian.gif" alt="powered by debian" width="88" height="31">
|
||||
<img src="/img/buttons/misc/girls4notepad.gif" alt="girls 4 notepad" width="88" height="31">
|
||||
<img src="/img/buttons/misc/gaywebring.gif" alt="this website is GAY" width="88" height="31">
|
||||
<img src="/img/buttons/misc/graphicdesign.gif" alt="graphic design is my passion" width="88" height="31">
|
||||
<img src="/img/buttons/misc/gplv3.gif" alt="GPLv3 free software" width="88" height="31">
|
||||
<img src="/img/buttons/misc/hl.gif" alt="half-life" width="88" height="31">
|
||||
<img src="/img/buttons/misc/h-free-anim.gif" alt="dis site is hentai FREE" width="88" height="31">
|
||||
<img src="/img/buttons/misc/sprunk.gif" alt="sprunk" width="88" height="31">
|
||||
<img src="/img/buttons/misc/tohell.gif" alt="go straight to hell" width="88" height="31">
|
||||
<img src="/img/buttons/misc/virusalert.gif" alt="virus alert! click here" onclick="alert('meow :3')" width="88" height="31">
|
||||
<img src="/img/buttons/misc/wii.gif" alt="wii" width="88" height="31">
|
||||
<img src="/img/buttons/misc/www2.gif" alt="www" width="88" height="31">
|
||||
<img src="/img/buttons/misc/iemandatory.gif" alt="get mandatory internet explorer" width="88" height="31">
|
||||
<img src="/img/buttons/misc/learn_html.gif" alt="HTML - learn it today!" width="88" height="31">
|
||||
<a href="https://smokepowered.com" target="_blank">
|
||||
<img src="/img/buttons/misc/smokepowered.gif" alt="high on SMOKE" width="88" height="31">
|
||||
</a>
|
||||
<a href="https://epicblazed.com" target="_blank">
|
||||
<img src="/img/buttons/misc/epicblazed.png" alt="epic blazed" width="88" height="31">
|
||||
</a>
|
||||
<img src="/img/buttons/misc/blink.gif" alt="closeup anime blink" width="88" height="31">
|
||||
</div>
|
||||
</main>
|
||||
{{end}}
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
{{define "head"}}
|
||||
<title>{{.PrintPrimaryArtists false}} - {{.Title}}</title>
|
||||
<title>{{.Title}} - {{.PrintPrimaryArtists false true}}</title>
|
||||
<link rel="icon" href="{{.ResolveArtwork}}">
|
||||
|
||||
<meta name="description" content="Stream "{{.Title}}" by {{.PrintPrimaryArtists false}} on all platforms!">
|
||||
<meta name="author" content="{{.PrintPrimaryArtists false}}">
|
||||
<meta name="keywords" content="{{.PrintCommaPrimaryArtists false}}, music, {{.Title}}, {{.Id}}, {{.GetReleaseYear}}">
|
||||
<meta name="description" content="Stream "{{.Title}}" by {{.PrintPrimaryArtists false true}} on all platforms!">
|
||||
<meta name="author" content="{{.PrintPrimaryArtists false true}}">
|
||||
<meta name="keywords" content="{{.PrintPrimaryArtists false false}}, music, {{.Title}}, {{.Id}}, {{.GetReleaseYear}}">
|
||||
|
||||
<meta property="og:url" content="https://arimelody.me/music/{{.Id}}">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:locale" content="en_IE">
|
||||
<meta property="og:site_name" content="ari melody music">
|
||||
<meta property="og.Title" content="{{.PrintPrimaryArtists false}} - {{.Title}}">
|
||||
<meta property="og:description" content="Stream "{{.Title}}" by {{.PrintPrimaryArtists false}} on all platforms!">
|
||||
<meta property="og.Title" content="{{.Title}} - {{.PrintPrimaryArtists false true}}">
|
||||
<meta property="og:description" content="Stream "{{.Title}}" by {{.PrintPrimaryArtists false true}} on all platforms!">
|
||||
<meta property="og:image" content="https://arimelody.me{{.ResolveArtwork}}">
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
|
@ -19,8 +19,8 @@
|
|||
<meta name="twitter:creator" content="@funniduck">
|
||||
<meta property="twitter:domain" content="arimelody.me">
|
||||
<meta property="twitter:url" content="https://arimelody.me/music/{{.Id}}">
|
||||
<meta name="twitter.Title" content="{{.PrintPrimaryArtists false}} - {{.Title}}">
|
||||
<meta name="twitter:description" content="Stream "{{.Title}}" by mellodoot on all platforms!">
|
||||
<meta name="twitter.Title" content="{{.PrintPrimaryArtists false true}} - {{.Title}}">
|
||||
<meta name="twitter:description" content="Stream "{{.Title}}" by {{.PrintPrimaryArtists false true}} on all platforms!">
|
||||
<meta name="twitter:image" content="https://arimelody.me{{.ResolveArtwork}}">
|
||||
<meta name="twitter:image:alt" content="Cover art for "{{.Title}}"">
|
||||
|
||||
|
@ -30,137 +30,139 @@
|
|||
|
||||
{{define "content"}}
|
||||
<main>
|
||||
<script type="module" src="/script/music-gateway.js"></script>
|
||||
<script type="module" src="/script/music-gateway.js"></script>
|
||||
|
||||
<div id="background" style="background-image: url({{.ResolveArtwork}})"></div>
|
||||
<div id="background" style="background-image: url({{.ResolveArtwork}})"></div>
|
||||
|
||||
<a href="/music" swap-url="/music" id="go-back" title="back to arimelody.me"><</a>
|
||||
<br><br>
|
||||
<a href="/music" swap-url="/music" id="go-back" title="back to arimelody.me"><</a>
|
||||
<br><br>
|
||||
|
||||
<div id="music-container">
|
||||
<div id="art-container">
|
||||
<div class="tilt-topleft"></div>
|
||||
<div class="tilt-top"></div>
|
||||
<div class="tilt-topright"></div>
|
||||
<div class="tilt-right"></div>
|
||||
<div class="tilt-bottomright"></div>
|
||||
<div class="tilt-bottom"></div>
|
||||
<div class="tilt-bottomleft"></div>
|
||||
<div class="tilt-left"></div>
|
||||
<img id="artwork" src="{{.ResolveArtwork}}" alt="{{.Title}} artwork" width=240 height=240>
|
||||
</div>
|
||||
<div id="vertical-line"></div>
|
||||
<div id="info">
|
||||
<div id="overview">
|
||||
<div id="title-container">
|
||||
<h1 id="title">{{.Title}}</h1>
|
||||
<span id="year" title="{{.PrintReleaseDate}}">{{.GetReleaseYear}}</span>
|
||||
</div>
|
||||
<p id="artist">{{.PrintPrimaryArtists false}}</p>
|
||||
<p id="type" class="{{.ResolveType}}">{{.ResolveType}}</p>
|
||||
|
||||
<ul id="links">
|
||||
{{if .Buylink}}
|
||||
<li>
|
||||
<a href="{{.Buylink}}" class="buy">{{or .Buyname "buy"}}</a>
|
||||
</li>
|
||||
{{end}}
|
||||
|
||||
{{range .Links}}
|
||||
<li>
|
||||
<a class="{{.NormaliseName}}" href="{{.Url}}">{{.Name}}</a>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
|
||||
{{if .Description}}
|
||||
<p id="description">
|
||||
{{.Description}}
|
||||
</p>
|
||||
{{end}}
|
||||
|
||||
<button id="share">share</button>
|
||||
</div>
|
||||
|
||||
{{if .Credits}}
|
||||
<div id="credits">
|
||||
<h2>credits:</h2>
|
||||
<ul>
|
||||
{{range .Credits}}
|
||||
{{$Artist := .ResolveArtist}}
|
||||
{{if $Artist.Website}}
|
||||
<li><strong><a href="{{$Artist.Website}}">{{$Artist.Name}}</a></strong>: {{.Role}}</li>
|
||||
{{else}}
|
||||
<li><strong>{{$Artist.Name}}</strong>: {{.Role}}</li>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .IsSingle}}
|
||||
{{$Track := index .Tracks 0}}
|
||||
{{if $Track.Lyrics}}
|
||||
<div id="lyrics">
|
||||
<h2>lyrics:</h2>
|
||||
<p>{{$Track.Lyrics}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<div id="tracks">
|
||||
<h2>tracks:</h2>
|
||||
{{range .Tracks}}
|
||||
<h3>{{.Title}}</h3>
|
||||
<p>{{.Lyrics}}</p>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{if or .Credits not .IsSingle}}
|
||||
<div id="extras">
|
||||
<ul>
|
||||
<li><a href="#overview">overview</a></li>
|
||||
|
||||
{{if .Credits}}
|
||||
<li><a href="#credits">credits</a></li>
|
||||
{{end}}
|
||||
|
||||
{{if .IsSingle}}
|
||||
{{$Track := index .Tracks 0}}
|
||||
{{if $Track.Lyrics}}
|
||||
<li><a href="#lyrics">lyrics</a></li>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<li><a href="#tracks">tracks</a></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
{{end}}
|
||||
<!-- <div id="tracks"> -->
|
||||
<!-- <% var file = `/audio/preview/${data.id}.webm` %> -->
|
||||
<!-- <% if (data.tracks && typeof(data.tracks) == typeof([])) { %> -->
|
||||
<!-- <% for( let i = 0; i < data.tracks.length; i++ ) { %> -->
|
||||
<!-- <% -->
|
||||
<!-- songid = data.tracks[i].title.toLowerCase().replace(/[^a-z0-9]/g, ""); -->
|
||||
<!-- file = `/audio/preview/${data.id}-${songid}.webm`; -->
|
||||
<!-- %> -->
|
||||
<!---->
|
||||
<!-- <div class="track-preview" id="preview-<%= songid %>"> -->
|
||||
<!-- <i class="fa-solid fa-play play"></i> -->
|
||||
<!-- <p><%= data.tracks[i].title %></p> -->
|
||||
<!-- <audio src="<%= file %>"></audio> -->
|
||||
<!-- </div> -->
|
||||
<!---->
|
||||
<!-- <% } %> -->
|
||||
<!-- <% } else { %> -->
|
||||
<!-- <div class="track-preview" id="preview-<%= data.id %>"> -->
|
||||
<!-- <i class="fa-solid fa-play play"></i> -->
|
||||
<!-- <p>{{.Title}}</p> -->
|
||||
<!-- <audio src="<%= file %>"></audio> -->
|
||||
<!-- </div> -->
|
||||
<!-- <% } %> -->
|
||||
<!-- </div> -->
|
||||
<div id="music-container">
|
||||
<div id="art-container">
|
||||
<div class="tilt-topleft"></div>
|
||||
<div class="tilt-top"></div>
|
||||
<div class="tilt-topright"></div>
|
||||
<div class="tilt-right"></div>
|
||||
<div class="tilt-bottomright"></div>
|
||||
<div class="tilt-bottom"></div>
|
||||
<div class="tilt-bottomleft"></div>
|
||||
<div class="tilt-left"></div>
|
||||
<img id="artwork" src="{{.ResolveArtwork}}" alt="{{.Title}} artwork" width=240 height=240>
|
||||
</div>
|
||||
<div id="vertical-line"></div>
|
||||
<div id="info">
|
||||
<div id="overview">
|
||||
<div id="title-container">
|
||||
<h1 id="title">{{.Title}}</h1>
|
||||
<span id="year" title="{{.PrintReleaseDate}}">{{.GetReleaseYear}}</span>
|
||||
</div>
|
||||
<p id="artist">{{.PrintPrimaryArtists false true}}</p>
|
||||
<p id="type" class="{{.ResolveType}}">{{.ResolveType}}</p>
|
||||
|
||||
<ul id="links">
|
||||
{{if .Buylink}}
|
||||
<li>
|
||||
<a href="{{.Buylink}}" class="buy">{{or .Buyname "buy"}}</a>
|
||||
</li>
|
||||
{{end}}
|
||||
|
||||
{{range .Links}}
|
||||
<li>
|
||||
<a class="{{.NormaliseName}}" href="{{.Url}}">{{.Name}}</a>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
|
||||
{{if .Description}}
|
||||
<p id="description">
|
||||
{{.Description}}
|
||||
</p>
|
||||
{{end}}
|
||||
|
||||
<button id="share">share</button>
|
||||
</div>
|
||||
|
||||
{{if .Credits}}
|
||||
<div id="credits">
|
||||
<h2>credits:</h2>
|
||||
<ul>
|
||||
{{range .Credits}}
|
||||
{{$Artist := .ResolveArtist}}
|
||||
{{if $Artist.Website}}
|
||||
<li><strong><a href="{{$Artist.Website}}">{{$Artist.Name}}</a></strong>: {{.Role}}</li>
|
||||
{{else}}
|
||||
<li><strong>{{$Artist.Name}}</strong>: {{.Role}}</li>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .IsSingle}}
|
||||
{{$Track := index .Tracks 0}}
|
||||
{{if $Track.Lyrics}}
|
||||
<div id="lyrics">
|
||||
<h2>lyrics:</h2>
|
||||
<p>{{$Track.Lyrics}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<div id="tracks">
|
||||
<h2>tracks:</h2>
|
||||
{{range .Tracks}}
|
||||
<details>
|
||||
<summary class="album-track-title">{{.Title}}</summary>
|
||||
{{.Lyrics}}
|
||||
</details>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{if or .Credits not .IsSingle}}
|
||||
<div id="extras">
|
||||
<ul>
|
||||
<li><a href="#overview">overview</a></li>
|
||||
|
||||
{{if .Credits}}
|
||||
<li><a href="#credits">credits</a></li>
|
||||
{{end}}
|
||||
|
||||
{{if .IsSingle}}
|
||||
{{$Track := index .Tracks 0}}
|
||||
{{if $Track.Lyrics}}
|
||||
<li><a href="#lyrics">lyrics</a></li>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<li><a href="#tracks">tracks</a></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
{{end}}
|
||||
<!-- <div id="tracks"> -->
|
||||
<!-- <% var file = `/audio/preview/${data.id}.webm` %> -->
|
||||
<!-- <% if (data.tracks && typeof(data.tracks) == typeof([])) { %> -->
|
||||
<!-- <% for( let i = 0; i < data.tracks.length; i++ ) { %> -->
|
||||
<!-- <% -->
|
||||
<!-- songid = data.tracks[i].title.toLowerCase().replace(/[^a-z0-9]/g, ""); -->
|
||||
<!-- file = `/audio/preview/${data.id}-${songid}.webm`; -->
|
||||
<!-- %> -->
|
||||
<!---->
|
||||
<!-- <div class="track-preview" id="preview-<%= songid %>"> -->
|
||||
<!-- <i class="fa-solid fa-play play"></i> -->
|
||||
<!-- <p><%= data.tracks[i].title %></p> -->
|
||||
<!-- <audio src="<%= file %>"></audio> -->
|
||||
<!-- </div> -->
|
||||
<!---->
|
||||
<!-- <% } %> -->
|
||||
<!-- <% } else { %> -->
|
||||
<!-- <div class="track-preview" id="preview-<%= data.id %>"> -->
|
||||
<!-- <i class="fa-solid fa-play play"></i> -->
|
||||
<!-- <p>{{.Title}}</p> -->
|
||||
<!-- <audio src="<%= file %>"></audio> -->
|
||||
<!-- </div> -->
|
||||
<!-- <% } %> -->
|
||||
<!-- </div> -->
|
||||
</div>
|
||||
</main>
|
||||
{{end}}
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
</div>
|
||||
<div class="music-details">
|
||||
<a href="/music/{{$Album.Id}}"><h1 class="music-title">{{$Album.Title}}</h1></a>
|
||||
<h2 class="music-artist">{{$Album.PrintPrimaryArtists false}}</h2>
|
||||
<h2 class="music-artist">{{$Album.PrintPrimaryArtists false true}}</h2>
|
||||
<h3 class="music-type-{{.ResolveType}}">{{$Album.ResolveType}}</h3>
|
||||
<ul class="music-links">
|
||||
{{range $Link := $Album.Links}}
|
||||
|
@ -46,45 +46,46 @@
|
|||
{{end}}
|
||||
</div>
|
||||
|
||||
<h2 id="usage" class="question" swap-url="/music#usage">
|
||||
<a href="#usage">
|
||||
> "can i use your music in my content?"
|
||||
</a>
|
||||
<h2 id="usage" class="question">
|
||||
<a href="#usage">
|
||||
> "can i use your music in my content?"
|
||||
</a>
|
||||
</h2>
|
||||
<div class="answer">
|
||||
<p>
|
||||
<strong class="big">yes!</strong> well, in most cases...
|
||||
</p>
|
||||
<p>
|
||||
from <a href="/music/dream">Dream (2022)</a> onward, all of my <em>self-released</em> songs are
|
||||
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
|
||||
songs freely, so long as they provide credit back to me!
|
||||
</p>
|
||||
<p>
|
||||
a great example of some credit text would be as follows:
|
||||
</p>
|
||||
<blockquote>
|
||||
music used: mellodoot - Dream<br>
|
||||
buy it here: <a href="/music/dream">https://arimelody.me/music/dream</a><br>
|
||||
licensed under CC BY-SA 3.0.
|
||||
</blockquote>
|
||||
<p>
|
||||
for any songs prior to this, they were all either released by me (in which case, i honestly
|
||||
don't mind), or in collaboration with chill people who i don't see having an issue with it.
|
||||
do be sure to ask them about it, though!
|
||||
</p>
|
||||
<p>
|
||||
in the event the song you want to use is released under some other label, their usage rights
|
||||
will more than likely trump whatever i'd otherwise have in mind. i'll try to negotiate some
|
||||
nice terms, though! ;3
|
||||
</p>
|
||||
<p>
|
||||
i love the idea of other creators using my songs in their work, so if you do happen to use
|
||||
my stuff in a work you're particularly proud of, feel free to send it my way!
|
||||
</p>
|
||||
<p>
|
||||
> <a href="mailto:ari@arimelody.me">ari@arimelody.me</a>
|
||||
</p>
|
||||
<p>
|
||||
<strong class="big">yes!</strong> well, in most cases...
|
||||
</p>
|
||||
<p>
|
||||
from <a href="/music/dream">Dream (2022)</a> onward, all of my <em>self-released</em> songs are
|
||||
licensed under <a href="https://creativecommons.org/licenses/by-sa/4.0/" target="_blank">Creative Commons Attribution-ShareAlike 4.0</a>.
|
||||
anyone may use and remix these songs freely, so long as they provide credit back to me and link back to this license!
|
||||
please note that all derivative works must inherit this license.
|
||||
</p>
|
||||
<p>
|
||||
a great example of some credit text would be as follows:
|
||||
</p>
|
||||
<blockquote>
|
||||
music used: mellodoot - Dream<br>
|
||||
<a href="/music/dream">https://arimelody.me/music/dream</a><br>
|
||||
licensed under <a href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA 4.0</a>.
|
||||
</blockquote>
|
||||
<p>
|
||||
for any songs prior to this, they were all either released by me (in which case, i honestly
|
||||
don't mind), or in collaboration with chill people who i don't see having an issue with it.
|
||||
do be sure to ask them about it, though!
|
||||
</p>
|
||||
<p>
|
||||
in the event the song you want to use is released under some other label, their usage rights
|
||||
will more than likely trump whatever i'd otherwise have in mind. i'll try to negotiate some
|
||||
nice terms, though! ;3
|
||||
</p>
|
||||
<p>
|
||||
i love the idea of other creators using my songs in their work, so if you do happen to use
|
||||
my stuff in a work you're particularly proud of, feel free to send it my way!
|
||||
</p>
|
||||
<p>
|
||||
> <a href="mailto:ari@arimelody.me">ari@arimelody.me</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<a href="#" id="backtotop">back to top</a>
|
||||
|
|