MORE REFACTORING!! + some improvements

Signed-off-by: ari melody <ari@arimelody.me>
This commit is contained in:
ari melody 2024-08-02 00:53:19 +01:00
parent 151b2d8fd9
commit cba791deba
17 changed files with 376 additions and 223 deletions

47
admin/admin.go Normal file
View file

@ -0,0 +1,47 @@
package admin
import (
"fmt"
"math/rand"
"os"
"time"
)
type (
Session struct {
UserID string
Token string
Expires int64
}
)
const TOKEN_LENGTH = 64
const TOKEN_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
var ADMIN_ID_DISCORD = func() string {
envvar := os.Getenv("DISCORD_ADMIN_ID")
if envvar == "" {
fmt.Printf("DISCORD_ADMIN_ID was not provided. Admin login will be unavailable.\n")
}
return envvar
}()
var sessions []*Session
func createSession(UserID string) Session {
return Session{
UserID: UserID,
Token: string(generateToken()),
Expires: time.Now().Add(24 * time.Hour).Unix(),
}
}
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)
}

View file

@ -3,45 +3,17 @@ package admin
import (
"context"
"fmt"
"math/rand"
"html/template"
"net/http"
"os"
// "strings"
"path/filepath"
"strings"
"time"
"arimelody.me/arimelody.me/discord"
"arimelody.me/arimelody.me/global"
)
type (
Session struct {
UserID string
Token string
Expires int64
}
)
const TOKEN_LENGTH = 64
const TOKEN_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
var ADMIN_ID_DISCORD = func() string {
envvar := os.Getenv("DISCORD_ADMIN_ID")
if envvar == "" {
fmt.Printf("DISCORD_ADMIN_ID was not provided. Admin login will be unavailable.\n")
}
return envvar
}()
var sessions []*Session
func CreateSession(UserID string) Session {
return Session{
UserID: UserID,
Token: string(generateToken()),
Expires: time.Now().Add(24 * time.Hour).Unix(),
}
}
func Handler() http.Handler {
mux := http.NewServeMux()
@ -53,18 +25,13 @@ func Handler() http.Handler {
mux.Handle("/login", global.HTTPLog(LoginHandler()))
mux.Handle("/verify", global.HTTPLog(MustAuthorise(VerifyHandler())))
mux.Handle("/logout", global.HTTPLog(MustAuthorise(LogoutHandler())))
mux.Handle("/static", global.HTTPLog(MustAuthorise(staticHandler())))
return mux
}
func MustAuthorise(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// TEMPORARY
ctx := context.WithValue(r.Context(), "role", "admin")
next.ServeHTTP(w, r.WithContext(ctx))
return
/*
auth := r.Header.Get("Authorization")
if strings.HasPrefix(auth, "Bearer ") {
auth = auth[7:]
@ -104,7 +71,6 @@ func MustAuthorise(next http.Handler) http.Handler {
ctx := context.WithValue(r.Context(), "role", "admin")
next.ServeHTTP(w, r.WithContext(ctx))
*/
})
}
@ -143,7 +109,7 @@ func LoginHandler() http.Handler {
}
// login success!
session := CreateSession(discord_user.Username)
session := createSession(discord_user.Username)
sessions = append(sessions, &session)
cookie := http.Cookie{}
@ -197,12 +163,60 @@ func VerifyHandler() http.Handler {
})
}
func generateToken() string {
var token []byte
func ServeTemplate(page string, data any) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
lp_layout := filepath.Join("views", "layout.html")
lp_header := filepath.Join("views", "header.html")
lp_footer := filepath.Join("views", "footer.html")
lp_prideflag := filepath.Join("views", "prideflag.html")
fp := filepath.Join("views", filepath.Clean(page))
for i := 0; i < TOKEN_LENGTH; i++ {
token = append(token, TOKEN_CHARS[rand.Intn(len(TOKEN_CHARS))])
}
info, err := os.Stat(fp)
if err != nil {
if os.IsNotExist(err) {
http.NotFound(w, r)
return
}
}
return string(token)
if info.IsDir() {
http.NotFound(w, r)
return
}
template, err := template.ParseFiles(lp_layout, lp_header, lp_footer, lp_prideflag, fp)
if err != nil {
fmt.Printf("Error parsing template files: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
err = template.ExecuteTemplate(w, "layout.html", data)
if err != nil {
fmt.Printf("Error executing template: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
})
}
func staticHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
info, err := os.Stat(filepath.Join("admin", "static", filepath.Clean(r.URL.Path)))
// does the file exist?
if err != nil {
if os.IsNotExist(err) {
http.NotFound(w, r)
return
}
}
// is thjs a directory? (forbidden)
if info.IsDir() {
http.NotFound(w, r)
return
}
http.FileServer(http.Dir(filepath.Join("admin", "static"))).ServeHTTP(w, r)
})
}

11
colour/colour.go Normal file
View file

@ -0,0 +1,11 @@
package colour
var Reset = "\033[0m"
var Red = "\033[31m"
var Green = "\033[32m"
var Yellow = "\033[33m"
var Blue = "\033[34m"
var Purple = "\033[35m"
var Cyan = "\033[36m"
var Gray = "\033[37m"
var White = "\033[97m"

View file

@ -1,24 +1,45 @@
package discord
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"os"
"strings"
)
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"
var CLIENT_ID = func() string {
envvar := os.Getenv("DISCORD_CLIENT_ID")
if envvar == "" {
fmt.Printf("DISCORD_CLIENT_ID was not provided. Admin login will be unavailable.\n")
}
return envvar
}()
var CLIENT_SECRET = func() string {
envvar := os.Getenv("DISCORD_CLIENT_SECRET")
if envvar == "" {
fmt.Printf("DISCORD_CLIENT_SECRET was not provided. Admin login will be unavailable.\n")
}
return envvar
}()
var REDIRECT_URI = func() string {
envvar := os.Getenv("DISCORD_REDIRECT_URI")
if envvar == "" {
fmt.Printf("DISCORD_REDIRECT_URI was not provided. Admin login will be unavailable.\n")
}
return envvar
}()
var OAUTH_CALLBACK_URI = func() string {
envvar := os.Getenv("OAUTH_CALLBACK_URI")
if envvar == "" {
fmt.Printf("OAUTH_CALLBACK_URI was not provided. Admin login will be unavailable.\n")
}
return envvar
}()
type (
AccessTokenResponse struct {
@ -63,7 +84,7 @@ func GetOAuthTokenFromCode(code string) (string, error) {
"client_secret": {CLIENT_SECRET},
"grant_type": {"authorization_code"},
"code": {code},
"redirect_uri": {MY_REDIRECT_URI},
"redirect_uri": {OAUTH_CALLBACK_URI},
}.Encode()))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")

View file

@ -1,26 +1,16 @@
package global
import (
"fmt"
"net/http"
"os"
"path/filepath"
"html/template"
"strconv"
"time"
)
"fmt"
"net/http"
"os"
"path/filepath"
"html/template"
"strconv"
"time"
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",
"txt": "text/plain",
"js": "application/javascript",
}
"arimelody.me/arimelody.me/colour"
)
func DefaultHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@ -54,11 +44,20 @@ func HTTPLog(next http.Handler) http.Handler {
elapsed = strconv.Itoa(difference)
}
fmt.Printf("[%s] %s %s - %d (%sms) (%s)\n",
codeColour := colour.Reset
if lrw.Code - 600 <= 0 { codeColour = colour.Red }
if lrw.Code - 500 <= 0 { codeColour = colour.Yellow }
if lrw.Code - 400 <= 0 { codeColour = colour.White }
if lrw.Code - 300 <= 0 { codeColour = colour.Green }
fmt.Printf("[%s] %s %s - %s%d%s (%sms) (%s)\n",
after.Format(time.UnixDate),
r.Method,
r.URL.Path,
codeColour,
lrw.Code,
colour.Reset,
elapsed,
r.Header["User-Agent"][0])
})

View file

@ -7,8 +7,8 @@ import (
"os"
"path/filepath"
"arimelody.me/arimelody.me/api/v1/admin"
"arimelody.me/arimelody.me/api/v1/music"
"arimelody.me/arimelody.me/admin"
"arimelody.me/arimelody.me/music"
"arimelody.me/arimelody.me/db"
"arimelody.me/arimelody.me/global"
)
@ -44,7 +44,7 @@ func main() {
func createServeMux() *http.ServeMux {
mux := http.NewServeMux()
mux.Handle("/api/v1/admin/", global.HTTPLog(http.StripPrefix("/api/v1/admin", admin.Handler())))
mux.Handle("/admin/", global.HTTPLog(http.StripPrefix("/admin", admin.Handler())))
mux.Handle("/api/v1/music/artist/", global.HTTPLog(http.StripPrefix("/api/v1/music/artist", music.ServeArtist())))
mux.Handle("/api/v1/music/", global.HTTPLog(http.StripPrefix("/api/v1/music", music.ServeRelease())))

View file

@ -6,11 +6,19 @@ import (
"github.com/jmoiron/sqlx"
)
type Credit struct {
Artist *Artist `json:"artist"`
Role string `json:"role"`
Primary bool `json:"primary"`
}
type (
Credit struct {
Artist *Artist `json:"artist"`
Role string `json:"role"`
Primary bool `json:"primary"`
}
PostCreditBody struct {
Artist string `json:"artist"`
Role string `json:"role"`
Primary bool `json:"primary"`
}
)
// GETTERS

View file

@ -7,19 +7,10 @@ import (
"path/filepath"
"strings"
"arimelody.me/arimelody.me/api/v1/admin"
"arimelody.me/arimelody.me/admin"
"arimelody.me/arimelody.me/global"
)
// 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)
// }
// return res
// }
// HTTP HANDLER METHODS
func ServeCatalog() http.Handler {
@ -33,7 +24,7 @@ func ServeCatalog() http.Handler {
releases = append(releases, release)
}
global.ServeTemplate("music.html", releases).ServeHTTP(w, r)
global.ServeTemplate("music.html", Releases).ServeHTTP(w, r)
})
}
@ -96,7 +87,6 @@ func ServeArtwork() http.Handler {
}
fp := filepath.Join("data", "music-artwork", releaseID + ".png")
fmt.Println(fp)
info, err := os.Stat(fp)
if err != nil {
if os.IsNotExist(err) {

View file

@ -8,7 +8,7 @@ import (
"strings"
"time"
"arimelody.me/arimelody.me/api/v1/admin"
"arimelody.me/arimelody.me/admin"
"github.com/jmoiron/sqlx"
)
@ -21,19 +21,35 @@ const (
Compilation ReleaseType = "Compilation"
)
type Release struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
ReleaseType ReleaseType `json:"type"`
ReleaseDate time.Time `json:"releaseDate"`
Artwork string `json:"artwork"`
Buyname string `json:"buyname"`
Buylink string `json:"buylink"`
Links []Link `json:"links"`
Credits []Credit `json:"credits"`
Tracks []Track `json:"tracks"`
}
type (
Release struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
ReleaseType ReleaseType `json:"type"`
ReleaseDate time.Time `json:"releaseDate"`
Artwork string `json:"artwork"`
Buyname string `json:"buyname"`
Buylink string `json:"buylink"`
Links []Link `json:"links"`
Credits []Credit `json:"credits"`
Tracks []Track `json:"tracks"`
}
PostReleaseBody struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
ReleaseType ReleaseType `json:"type"`
ReleaseDate time.Time `json:"releaseDate"`
Artwork string `json:"artwork"`
Buyname string `json:"buyname"`
Buylink string `json:"buylink"`
Links []Link `json:"links"`
Credits []PostCreditBody `json:"credits"`
Tracks []Track `json:"tracks"`
}
)
var Releases []Release;
@ -337,7 +353,7 @@ func (release Release) DeleteFromDB(db *sqlx.DB) error {
func PullAllReleases(db *sqlx.DB) ([]Release, error) {
releases := []Release{}
rows, err := db.Query("SELECT id, title, description, type, release_date, artwork, buyname, buylink FROM musicreleases")
rows, err := db.Query("SELECT id, title, description, type, release_date, artwork, buyname, buylink FROM musicreleases ORDER BY release_date DESC")
if err != nil {
return nil, err
}
@ -423,19 +439,50 @@ func PostRelease() http.Handler {
return
}
var release Release
err := json.NewDecoder(r.Body).Decode(&release)
var data PostReleaseBody
err := json.NewDecoder(r.Body).Decode(&data)
if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
if GetRelease(release.ID) != nil {
http.Error(w, fmt.Sprintf("Release %s already exists", release.ID), http.StatusBadRequest)
if GetRelease(data.ID) != nil {
http.Error(w, fmt.Sprintf("Release %s already exists", data.ID), http.StatusBadRequest)
return
}
Releases = append(Releases, release)
var credits = []Credit{}
for _, credit := range data.Credits {
var artist = GetArtist(credit.Artist)
if artist == nil {
http.Error(w, fmt.Sprintf("Artist %s does not exist", credit.Artist), http.StatusBadRequest)
return
}
credits = append(credits, Credit{
Artist: artist,
Role: credit.Role,
Primary: credit.Primary,
})
}
var release = Release{
ID: data.ID,
Title: data.Title,
Description: data.Description,
ReleaseType: data.ReleaseType,
ReleaseDate: data.ReleaseDate,
Artwork: data.Artwork,
Buyname: data.Buyname,
Buylink: data.Buylink,
Links: data.Links,
Credits: credits,
Tracks: data.Tracks,
}
Releases = append([]Release{release}, Releases...)
jsonBytes, err := json.Marshal(release)
w.Header().Add("Content-Type", "application/json")

View file

@ -2,11 +2,11 @@ const header_links = document.getElementById("header-links");
const hamburger = document.getElementById("header-links-toggle");
document.addEventListener("click", event => {
if (!header_links.contains(event.target) && !hamburger.contains(event.target) && !header_links.href) {
header_links.classList.remove("open");
}
if (!header_links.contains(event.target) && !hamburger.contains(event.target) && !header_links.href) {
header_links.classList.remove("open");
}
});
hamburger.addEventListener("click", event => {
header_links.classList.toggle("open");
hamburger.addEventListener("click", () => {
header_links.classList.toggle("open");
});

View file

@ -1,7 +1,12 @@
import "./main.js";
document.addEventListener("swap", () => {
document.querySelectorAll("h1.music-title").forEach(element => {
element.href = "";
});
document.querySelectorAll("div.music").forEach(container => {
const link = container.querySelector(".music-title a").href
container.addEventListener("click", event => {
if (event.target.href) return;
event.preventDefault();
location = link;
});
});

View file

@ -1,17 +1,18 @@
--
-- Artists (should be applicable to all art)
--
CREATE TABLE IF NOT EXISTS artists (
id text NOT NULL,
name text,
website text
CREATE TABLE artist (
id uuid DEFAULT gen_random_uuid(),
name text NOT NULL,
website text,
avatar text
);
ALTER TABLE artists ADD CONSTRAINT artists_pk PRIMARY KEY (id);
ALTER TABLE artist ADD CONSTRAINT artist_pk PRIMARY KEY (id);
--
-- Music releases
--
CREATE TABLE IF NOT EXISTS musicreleases (
CREATE TABLE musicrelease (
id character varying(64) NOT NULL,
title text NOT NULL,
description text,
@ -21,50 +22,56 @@ CREATE TABLE IF NOT EXISTS musicreleases (
buyname text,
buylink text
);
ALTER TABLE musicreleases ADD CONSTRAINT musicreleases_pk PRIMARY KEY (id);
ALTER TABLE musicrelease ADD CONSTRAINT musicrelease_pk PRIMARY KEY (id);
--
-- Music links (external platform links under a release)
--
CREATE TABLE IF NOT EXISTS musiclinks (
CREATE TABLE musiclink (
release character varying(64) NOT NULL,
name text NOT NULL,
url text
url text NOT NULL
);
ALTER TABLE musiclinks ADD CONSTRAINT musiclinks_pk PRIMARY KEY (release, name);
ALTER TABLE musiclink ADD CONSTRAINT musiclink_pk PRIMARY KEY (release, name);
--
-- Music credits (artist credits under a release)
--
CREATE TABLE IF NOT EXISTS musiccredits (
CREATE TABLE musiccredit (
release character varying(64) NOT NULL,
artist text NOT NULL,
role text,
is_primary boolean
artist uuid NOT NULL,
role text NOT NULL,
is_primary boolean DEFAULT false
);
ALTER TABLE musiccredits ADD CONSTRAINT musiccredits_pk PRIMARY KEY (release, artist);
ALTER TABLE musiccredit ADD CONSTRAINT musiccredit_pk PRIMARY KEY (release, artist);
--
-- Music tracks (tracks under a release)
--
CREATE TABLE IF NOT EXISTS musictracks (
release character varying(64) NOT NULL,
number integer NOT NULL,
CREATE TABLE musictrack (
id uuid DEFAULT gen_random_uuid(),
title text NOT NULL,
description text,
lyrics text,
preview_url text
);
ALTER TABLE musictracks ADD CONSTRAINT musictracks_pk PRIMARY KEY (release, number);
ALTER TABLE musictrack ADD CONSTRAINT musictrack_pk PRIMARY KEY (id);
--
-- Music release/track pairs
--
CREATE TABLE musicreleasetrack (
release character varying(64) NOT NULL,
track uuid NOT NULL,
number integer NOT NULL
);
ALTER TABLE musicreleasetrack ADD CONSTRAINT musictrack_pk PRIMARY KEY (release, track);
--
-- Foreign keys
--
ALTER TABLE musiccredits ADD CONSTRAINT IF NOT EXISTS musiccredits_artist_fk FOREIGN KEY (artist) REFERENCES artists(id) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE musiccredits ADD CONSTRAINT IF NOT EXISTS musiccredits_release_fk FOREIGN KEY (release) REFERENCES musicreleases(id) ON DELETE CASCADE;
ALTER TABLE musiclinks ADD CONSTRAINT IF NOT EXISTS musiclinks_release_fk FOREIGN KEY (release) REFERENCES musicreleases(id) ON UPDATE CASCADE ON DELETE CASCADE;
ALTER TABLE musictracks ADD CONSTRAINT IF NOT EXISTS musictracks_release_fk FOREIGN KEY (release) REFERENCES musicreleases(id) ON DELETE CASCADE;
ALTER TABLE musiccredit ADD CONSTRAINT musiccredit_artist_fk FOREIGN KEY (artist) REFERENCES artist(id) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE musiccredit ADD CONSTRAINT musiccredit_release_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON DELETE CASCADE;
ALTER TABLE musiclink ADD CONSTRAINT musiclink_release_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON UPDATE CASCADE ON DELETE CASCADE;
ALTER TABLE musicreleasetrack ADD CONSTRAINT music_pair_trackref_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON DELETE CASCADE;
ALTER TABLE musicreleasetrack ADD CONSTRAINT music_pair_releaseref_fk FOREIGN KEY (track) REFERENCES musictrack(id) ON DELETE CASCADE;

View file

@ -111,7 +111,7 @@
<h2>tracks:</h2>
{{range $i, $track := .GetTracks}}
<details>
<summary class="album-track-title">{{$i}}. {{$track.GetTitle}}</summary>
<summary class="album-track-title">{{$track.GetNumber}}. {{$track.GetTitle}}</summary>
{{$track.GetLyrics}}
</details>
{{end}}

View file

@ -18,76 +18,80 @@
{{define "content"}}
<main>
<script type="module" src="/script/music.js"></script>
<script type="module" src="/script/music.js"></script>
<h1>
# my music
</h1>
<h1>
# my music
</h1>
<div id="music-container">
{{range $Release := .}}
<div class="music" id="{{$Release.GetID}}" swap-url="/music/{{$Release.GetID}}">
<div class="music-artwork">
<img src="{{$Release.GetArtwork}}" alt="{{$Release.GetTitle}} artwork" width="128" loading="lazy">
</div>
<div class="music-details">
<a href="/music/{{$Release.GetID}}"><h1 class="music-title">{{$Release.GetTitle}}</h1></a>
<h2 class="music-artist">{{$Release.PrintArtists true true}}</h2>
<h3 class="music-type-{{$Release.GetType}}">{{$Release.GetType}}</h3>
<ul class="music-links">
{{range $Link := $Release.GetLinks}}
<li>
<a href="{{$Link.GetURL}}" class="link-button">{{$Link.GetName}}</a>
</li>
{{end}}
</ul>
</div>
</div>
{{end}}
<div id="music-container">
{{range $Release := .}}
<div class="music" id="{{$Release.GetID}}" swap-url="/music/{{$Release.GetID}}">
<div class="music-artwork">
<img src="{{$Release.GetArtwork}}" alt="{{$Release.GetTitle}} artwork" width="128" loading="lazy">
</div>
<div class="music-details">
<h1 class="music-title">
<a href="/music/{{$Release.GetID}}">
{{$Release.GetTitle}}
</a>
</h1>
<h2 class="music-artist">{{$Release.PrintArtists true true}}</h2>
<h3 class="music-type-{{$Release.GetType}}">{{$Release.GetType}}</h3>
<ul class="music-links">
{{range $Link := $Release.GetLinks}}
<li>
<a href="{{$Link.GetURL}}" class="link-button">{{$Link.GetName}}</a>
</li>
{{end}}
</ul>
</div>
</div>
{{end}}
</div>
<h2 id="usage" class="question">
<a href="#usage">
&gt; "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/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>
&gt; <a href="mailto:ari@arimelody.me">ari@arimelody.me</a>
</p>
</div>
<h2 id="usage" class="question">
<a href="#usage">
&gt; "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/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>
&gt; <a href="mailto:ari@arimelody.me">ari@arimelody.me</a>
</p>
</div>
<a href="#" id="backtotop">back to top</a>
<a href="#" id="backtotop">back to top</a>
</main>
{{end}}