added stuff, broke some other stuff, made admin auth!
Signed-off-by: ari melody <ari@arimelody.me>
This commit is contained in:
parent
0d1e694b59
commit
5631c4bd87
26 changed files with 1615 additions and 1401 deletions
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue