diff --git a/.air.toml b/.air.toml
new file mode 100644
index 0000000..23141a3
--- /dev/null
+++ b/.air.toml
@@ -0,0 +1,58 @@
+#:schema https://json.schemastore.org/any.json
+
+env_files = []
+root = "."
+testdata_dir = "testdata"
+tmp_dir = "tmp"
+
+[build]
+ args_bin = []
+ bin = "./tmp/main"
+ cmd = "go build -o ./tmp/main ."
+ delay = 1000
+ entrypoint = ["./tmp/main"]
+ exclude_dir = ["assets", "tmp", "vendor", "testdata"]
+ exclude_file = []
+ exclude_regex = ["_test.go"]
+ exclude_unchanged = false
+ follow_symlink = false
+ full_bin = ""
+ ignore_dangerous_root_dir = false
+ include_dir = []
+ include_ext = ["go", "tpl", "tmpl", "html", "css", "js"]
+ include_file = []
+ kill_delay = "0s"
+ log = "build-errors.log"
+ poll = false
+ poll_interval = 0
+ post_cmd = []
+ pre_cmd = []
+ rerun = false
+ rerun_delay = 500
+ send_interrupt = false
+ stop_on_error = false
+
+[color]
+ app = ""
+ build = "yellow"
+ main = "magenta"
+ runner = "green"
+ watcher = "cyan"
+
+[log]
+ main_only = false
+ silent = false
+ time = false
+
+[misc]
+ clean_on_exit = false
+
+[proxy]
+ app_port = 0
+ app_start_timeout = 0
+ enabled = false
+ proxy_port = 0
+
+[screen]
+ clear_on_rebuild = false
+ keep_scroll = true
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..75449c8
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+*.woff2 filter=lfs diff=lfs merge=lfs -text
diff --git a/.gitignore b/.gitignore
index ec14f66..710c0af 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
+tmp/
learning-title.txt
diff --git a/go.mod b/go.mod
index 7dd510e..e29cd74 100644
--- a/go.mod
+++ b/go.mod
@@ -1,4 +1,4 @@
-module codeberg.org/arimelody/ari-stream-utils
+module codeberg.org/arimelody/ari-stream-tools
go 1.26.4
@@ -16,6 +16,7 @@ require (
github.com/go-playground/validator/v10 v10.30.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
+ github.com/godbus/dbus/v5 v5.2.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
diff --git a/go.sum b/go.sum
index 6a9c680..882bc10 100644
--- a/go.sum
+++ b/go.sum
@@ -24,6 +24,8 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
+github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
+github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
diff --git a/learning/learning.go b/learning/learning.go
index 139ed45..d93201b 100644
--- a/learning/learning.go
+++ b/learning/learning.go
@@ -3,7 +3,6 @@ package learning
import (
"context"
"embed"
- "fmt"
"io"
"log"
"net/http"
@@ -12,7 +11,7 @@ import (
"sync"
"time"
- "codeberg.org/arimelody/ari-is-learning/broadcast"
+ "codeberg.org/arimelody/ari-stream-tools/broadcast"
"github.com/gin-gonic/gin"
)
@@ -29,7 +28,6 @@ type (
Service struct {
cfg ServiceConfig
- ctx context.Context
titleFileMutex sync.Mutex
titleLastModTime time.Time
titleText string
@@ -39,21 +37,25 @@ type (
)
func New(ctx context.Context, cfg ServiceConfig) *Service {
+ titleText := []byte{}
titleUpdated := make(chan string)
if _, err := os.Stat(cfg.TitleFilePath); os.IsNotExist(err) {
log.Printf("Learning title file [%s] does not exist, creating one...", cfg.TitleFilePath)
os.WriteFile(cfg.TitleFilePath, []byte("Untitled"), 0644)
+ titleText = []byte("Untitled")
} else if err != nil {
log.Fatalf("Failed to stat title file: %v", err)
+ } else {
+ titleText, err = os.ReadFile(cfg.TitleFilePath)
+ if err != nil { log.Fatalf("Failed to read title file: %v", err) }
}
srv := &Service{
cfg: cfg,
- ctx: ctx,
titleFileMutex: sync.Mutex{},
titleLastModTime: time.Unix(0, 0),
- titleText: "",
+ titleText: string(titleText),
titleUpdated: titleUpdated,
titleUpdatedBroadcast: broadcast.NewBroadcastChannel(ctx, titleUpdated),
}
@@ -110,6 +112,8 @@ func (srv *Service) BindRoutes(group *gin.RouterGroup) {
return
}
+ srv.titleUpdated<-string(data)
+
ctx.String(http.StatusOK, http.StatusText(http.StatusOK))
})
@@ -123,45 +127,3 @@ func (srv *Service) BindRoutes(group *gin.RouterGroup) {
http.ServeFileFS(ctx.Writer, ctx.Request, pagesFS, "pages/bye.html")
})
}
-
-func (srv *Service) Run(ctx context.Context) {
- failed := make(chan error)
-
- go func() {
- for {
- srv.titleFileMutex.Lock()
- stat, err := os.Stat(srv.cfg.TitleFilePath)
- if err != nil {
- failed<-fmt.Errorf("Failed to stat title file: %v", err)
- return
- }
-
- if srv.titleLastModTime.Before(stat.ModTime()) {
- data, err := os.ReadFile(srv.cfg.TitleFilePath)
- if err != nil {
- failed<-fmt.Errorf("Failed to read title file: %v", err)
- return
- }
-
- log.Printf(
- "title updated: \"%s\" -> \"%s\"",
- srv.titleText,
- strings.TrimSpace(string(data)),
- )
-
- srv.titleText = strings.TrimSpace(string(data))
- if !srv.titleLastModTime.IsZero() { srv.titleUpdated<-srv.titleText }
- srv.titleLastModTime = stat.ModTime()
- }
- srv.titleFileMutex.Unlock()
-
- time.Sleep(100 * time.Millisecond)
- }
- }()
-
- select {
- case err := <-failed:
- log.Fatalf("Learning service error: %v", err)
- case <-ctx.Done():
- }
-}
diff --git a/main.go b/main.go
index c0ce017..33a9a51 100644
--- a/main.go
+++ b/main.go
@@ -10,7 +10,8 @@ import (
"strconv"
"syscall"
- "codeberg.org/arimelody/ari-stream-utils/learning"
+ "codeberg.org/arimelody/ari-stream-tools/learning"
+ "codeberg.org/arimelody/ari-stream-tools/music"
"github.com/gin-gonic/gin"
)
@@ -41,11 +42,13 @@ func main() {
learningService := learning.New(ctx, learning.ServiceConfig{
TitleFilePath: "learning-title.txt",
})
+ musicService := music.New(ctx)
srv := gin.Default()
learningService.BindRoutes(srv.Group("/learning"))
+ musicService.BindRoutes(srv.Group("/music"))
- go learningService.Run(ctx)
+ go musicService.Run(ctx)
go func() {
log.Printf("Now serving at http://%s:%d\n", host, port)
failed <- srv.Run(fmt.Sprintf("%s:%d", host, port))
diff --git a/mpris/mpris.go b/mpris/mpris.go
deleted file mode 100644
index 31d5fb9..0000000
--- a/mpris/mpris.go
+++ /dev/null
@@ -1 +0,0 @@
-package mpris
diff --git a/music/music.go b/music/music.go
new file mode 100644
index 0000000..3f9c3a5
--- /dev/null
+++ b/music/music.go
@@ -0,0 +1,292 @@
+package music
+
+import (
+ "context"
+ "embed"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "strings"
+ "time"
+
+ "codeberg.org/arimelody/ari-stream-tools/broadcast"
+ "github.com/gin-gonic/gin"
+ "github.com/godbus/dbus/v5"
+)
+
+const (
+ MPRIS_ART_URL = "mpris:artUrl"
+ MPRIS_LENGTH = "mpris:length"
+ MPRIS_TRACK_ID = "mpris:trackid"
+ XESAM_TITLE = "xesam:title"
+ XESAM_ALBUM = "xesam:album"
+ XESAM_ARTISTS = "xesam:artist"
+
+ DBUS_STRAWBERRY = "org.mpris.MediaPlayer2.strawberry"
+)
+
+//go:embed public
+var publicFS embed.FS
+//go:embed pages
+var pagesFS embed.FS
+
+type (
+ trackMetadata struct {
+ ArtworkURL string
+ Length int64
+ Title string
+ Album string
+ Artists []string
+ }
+
+ Service struct {
+ track *trackMetadata
+ nextTrack chan *trackMetadata
+ nextTrackBroadcast broadcast.BroadcastChannel[*trackMetadata]
+ playing bool
+ nextPlaying chan bool
+ playingBroadcast broadcast.BroadcastChannel[bool]
+ }
+)
+
+func New(ctx context.Context) *Service {
+ nextTrack := make(chan *trackMetadata)
+ nextPlaying := make(chan bool)
+
+ srv := &Service{
+ track: nil,
+ nextTrack: nextTrack,
+ nextTrackBroadcast: broadcast.NewBroadcastChannel(ctx, nextTrack),
+ playing: false,
+ nextPlaying: nextPlaying,
+ playingBroadcast: broadcast.NewBroadcastChannel(ctx, nextPlaying),
+ }
+
+ return srv
+}
+
+func (srv *Service) BindRoutes(group *gin.RouterGroup) {
+ group.GET("/public/*path", func(ctx *gin.Context) {
+ path := strings.TrimPrefix(ctx.Request.URL.Path, "/music/")
+ http.ServeFileFS(ctx.Writer, ctx.Request, publicFS, path)
+ })
+
+ group.GET("/sse", func(ctx *gin.Context) {
+ ctx.Header("connection", "keep-alive")
+
+ nextTrack := srv.nextTrackBroadcast.Subscribe()
+ nextPlaying := srv.playingBroadcast.Subscribe()
+
+ if srv.track == nil { <-nextTrack }
+
+ ctx.SSEvent("music", gin.H{
+ "title": srv.track.Title,
+ "artist": artistString(srv.track),
+ })
+ ctx.SSEvent("music-state", gin.H{ "playing": srv.playing })
+
+ ticker := time.NewTicker(10 * time.Millisecond)
+ ctx.Stream(func(w io.Writer) bool {
+ select {
+ case track := <-nextTrack:
+ ctx.SSEvent("music", gin.H{
+ "title": track.Title,
+ "artist": artistString(track),
+ })
+ case playing := <-nextPlaying:
+ ctx.SSEvent("music-state", gin.H{ "playing": playing })
+ case <-ticker.C:
+ }
+ return true
+ })
+
+ srv.nextTrackBroadcast.Cancel(nextTrack)
+ srv.playingBroadcast.Cancel(nextPlaying)
+ })
+
+ group.GET("/artwork", func(ctx *gin.Context) {
+ ctx.Header("cache-control", "no-cache")
+
+ if srv.track == nil || srv.track.ArtworkURL == "" {
+ http.ServeFileFS(ctx.Writer, ctx.Request, publicFS, "public/default-cover-art.png")
+ return
+ }
+
+ artworkPath := strings.TrimPrefix(srv.track.ArtworkURL, "file://")
+ http.ServeFile(ctx.Writer, ctx.Request, artworkPath)
+ })
+
+ group.GET("/", func(ctx *gin.Context) {
+ http.ServeFileFS(ctx.Writer, ctx.Request, pagesFS, "pages/music.html")
+ })
+}
+
+func (srv *Service) Run(ctx context.Context) {
+ var err error
+
+ conn, err := dbus.ConnectSessionBus()
+ if err != nil {
+ log.Fatalf("Failed to connect to session bus: %v", err)
+ }
+ defer conn.Close()
+
+ var data map[string]dbus.Variant
+ obj := conn.Object(DBUS_STRAWBERRY, "/org/mpris/MediaPlayer2")
+ if err := obj.Call(
+ "org.freedesktop.DBus.Properties.GetAll",
+ 0,
+ "org.mpris.MediaPlayer2.Player",
+ ).Store(&data); err != nil {
+ log.Fatalf("Failed to query media player data: %v", err)
+ }
+
+ metadata, ok := data["Metadata"].Value().(map[string]dbus.Variant)
+ if !ok { log.Fatalf("Could not cast metadata to map[string]dbus.Variant") }
+
+ playbackStatus, ok := data["PlaybackStatus"].Value().(string)
+ if !ok { log.Fatalf("Could not cast metadata to string") }
+
+ // on first run, mpris:artUrl returns a temporary strawberry player artwork
+ // located in /tmp. this is gone by the time we attempt to read, so we need
+ // a way to pull the real artwork filepath at runtime.
+ // TODO: figure out how to pull real (not /tmp) track artwork
+ if srv.track, err = parseTrackMetadata(metadata); err == nil {
+ log.Printf("Now playing: %s - %s", artistString(srv.track), srv.track.Title)
+ } else {
+ log.Printf("Failed to parse track metadata: %v", err)
+ }
+ srv.playing = playbackStatus == "Playing"
+
+ var dbusChan chan *dbus.Signal
+ dbusChan, err = setupPlayerListener(conn)
+ if err != nil {
+ log.Fatalf("Failed to connect to session bus: %v", err)
+ }
+
+ failed := make(chan error)
+ go func() {
+ for {
+ for signal := range dbusChan {
+ data, ok := signal.Body[1].(map[string]dbus.Variant)
+ if !ok { continue }
+
+ if _, ok := data["PlaybackStatus"]; ok {
+ playbackStatus, ok := data["PlaybackStatus"].Value().(string)
+ if ok {
+ if playbackStatus == "Playing" {
+ srv.playing = true
+ log.Print("Music playing")
+ } else {
+ srv.playing = false
+ log.Print("Music paused")
+ }
+ srv.nextPlaying<-srv.playing
+ }
+
+ } else if _, ok := data["Metadata"]; ok {
+ metadata, ok := data["Metadata"].Value().(map[string]dbus.Variant)
+ if !ok { continue }
+
+ nextTrack, err := parseTrackMetadata(metadata)
+ if err != nil {
+ log.Printf("Failed to parse track metadata: %v", err)
+ continue
+ }
+ /*
+ if nextTrack.Title == srv.track.Title &&
+ slices.Compare(nextTrack.Artists, srv.track.Artists) == 0 {
+ continue
+ }
+ */
+ srv.track = nextTrack
+ srv.nextTrack<-nextTrack
+
+ log.Printf("Now playing: %s - %s", artistString(nextTrack), nextTrack.Title)
+
+ }
+ }
+
+ log.Printf("Session bus connection lost. Reconnecting...")
+ conn, err = dbus.ConnectSessionBus()
+ dbusChan, err = setupPlayerListener(conn)
+ if err != nil {
+ log.Fatalf("Failed to connect to session bus: %v", err)
+ }
+
+ time.Sleep(1 * time.Second)
+ }
+ }()
+
+ select {
+ case err := <-failed:
+ log.Fatalf("MPRIS/D-Bus error: %v", err)
+ case <-ctx.Done():
+ }
+}
+
+func parseTrackMetadata(metadata map[string]dbus.Variant) (*trackMetadata, error) {
+ if _, exists := metadata[MPRIS_LENGTH]; !exists {
+ return nil, fmt.Errorf("Metadata does not contain %s", MPRIS_LENGTH)
+ }
+ length, ok := metadata[MPRIS_LENGTH].Value().(int64)
+ if !ok { return nil, fmt.Errorf("Failed to cast %s to int64", MPRIS_LENGTH) }
+
+ var artUrl string
+ if _, exists := metadata[MPRIS_ART_URL]; exists {
+ artUrl, ok = metadata[MPRIS_ART_URL].Value().(string)
+ if !ok { return nil, fmt.Errorf("Failed to cast %s to string", MPRIS_ART_URL) }
+ }
+
+ if _, exists := metadata[XESAM_TITLE]; !exists {
+ return nil, fmt.Errorf("Metadata does not contain %s", XESAM_TITLE)
+ }
+ title, ok := metadata[XESAM_TITLE].Value().(string)
+ if !ok { return nil, fmt.Errorf("Failed to cast %s to string", XESAM_TITLE) }
+
+ var album string
+ if _, exists := metadata[XESAM_ALBUM]; exists {
+ album, ok = metadata[XESAM_ALBUM].Value().(string)
+ if !ok { return nil, fmt.Errorf("Failed to cast %s to string", XESAM_ALBUM) }
+ }
+
+ var artists []string
+ if _, exists := metadata[XESAM_ARTISTS]; exists {
+ artists, ok = metadata[XESAM_ARTISTS].Value().([]string)
+ if !ok { return nil, fmt.Errorf("Failed to cast %s to []string", XESAM_ARTISTS) }
+ }
+
+ trackMetadata := &trackMetadata{
+ ArtworkURL: artUrl,
+ Length: length,
+ Title: title,
+ Album: album,
+ Artists: artists,
+ }
+
+ return trackMetadata, nil
+}
+
+func artistString(track *trackMetadata) string {
+ artists := ""
+ if len(track.Artists) == 0 { return "Unknown Artist" }
+ if len(track.Artists) == 1 { return track.Artists[0] }
+ artists += strings.Join(track.Artists[:len(track.Artists)-2], ", ")
+ artists += " & " + track.Artists[len(track.Artists) - 1]
+ return artists
+}
+
+func setupPlayerListener(conn *dbus.Conn) (chan *dbus.Signal, error) {
+ if err := conn.AddMatchSignal(
+ dbus.WithMatchObjectPath("/org/mpris/MediaPlayer2"),
+ dbus.WithMatchInterface("org.freedesktop.DBus.Properties"),
+ dbus.WithMatchSender(DBUS_STRAWBERRY),
+ ); err != nil {
+ return nil, fmt.Errorf("Failed to configure D-Bus signal listener: %v", err)
+ }
+
+ c := make(chan *dbus.Signal)
+ conn.Signal(c)
+
+ return c, nil
+}
diff --git a/music/pages/music.html b/music/pages/music.html
new file mode 100644
index 0000000..0f854af
--- /dev/null
+++ b/music/pages/music.html
@@ -0,0 +1,31 @@
+
+
+
+
+
+ Nothing is playing...
+
+
+
+
+
+
+
+
+
+

+
+
+
Untitled Track
+
Unknown Artist
+
+
+
+
+
+
+
+
diff --git a/music/public/default-cover-art.png b/music/public/default-cover-art.png
new file mode 100755
index 0000000..8e19b8c
Binary files /dev/null and b/music/public/default-cover-art.png differ
diff --git a/music/public/fonts/inter/Inter-Black.woff2 b/music/public/fonts/inter/Inter-Black.woff2
new file mode 100644
index 0000000..89b8df9
--- /dev/null
+++ b/music/public/fonts/inter/Inter-Black.woff2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:12ed0eed6749099b46c7b2e8198dc30c2d7e0f2a4e5fb1d12f0b6ae2c4f33cc4
+size 111668
diff --git a/music/public/fonts/inter/Inter-BlackItalic.woff2 b/music/public/fonts/inter/Inter-BlackItalic.woff2
new file mode 100644
index 0000000..71f56ab
--- /dev/null
+++ b/music/public/fonts/inter/Inter-BlackItalic.woff2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:308e101141e9dee3b6b121614da03969eb8179886975a9eeb5348021b04368fe
+size 118420
diff --git a/music/public/fonts/inter/Inter-Bold.woff2 b/music/public/fonts/inter/Inter-Bold.woff2
new file mode 100644
index 0000000..07604e9
--- /dev/null
+++ b/music/public/fonts/inter/Inter-Bold.woff2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fa888127b6da015b65569f0351f3b5c391ad928904951f1c20e9f8462a8d95ea
+size 114840
diff --git a/music/public/fonts/inter/Inter-BoldItalic.woff2 b/music/public/fonts/inter/Inter-BoldItalic.woff2
new file mode 100644
index 0000000..ac216a9
--- /dev/null
+++ b/music/public/fonts/inter/Inter-BoldItalic.woff2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4a10532d76c90c4597c0ad4bfc9284600e597d3012dd1ec16c1d44e0ad0058ab
+size 121500
diff --git a/music/public/fonts/inter/Inter-ExtraBold.woff2 b/music/public/fonts/inter/Inter-ExtraBold.woff2
new file mode 100644
index 0000000..1f88168
--- /dev/null
+++ b/music/public/fonts/inter/Inter-ExtraBold.woff2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6f75025856f8db1b2186e9cb89be9de9894932c8b7b20f4df5e65916ff714e34
+size 114856
diff --git a/music/public/fonts/inter/Inter-ExtraBoldItalic.woff2 b/music/public/fonts/inter/Inter-ExtraBoldItalic.woff2
new file mode 100644
index 0000000..25e52ce
--- /dev/null
+++ b/music/public/fonts/inter/Inter-ExtraBoldItalic.woff2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:53bb20ba292e2ac28a0777bf0f56f31b98be432a3cc04359e2eb608c758349c7
+size 121516
diff --git a/music/public/fonts/inter/Inter-ExtraLight.woff2 b/music/public/fonts/inter/Inter-ExtraLight.woff2
new file mode 100644
index 0000000..1ab4d09
--- /dev/null
+++ b/music/public/fonts/inter/Inter-ExtraLight.woff2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ba4fc81dbb25871f1bcabc664b1e37703fca0a05f7248a923e7db497c6d211cc
+size 112728
diff --git a/music/public/fonts/inter/Inter-ExtraLightItalic.woff2 b/music/public/fonts/inter/Inter-ExtraLightItalic.woff2
new file mode 100644
index 0000000..e88cd22
--- /dev/null
+++ b/music/public/fonts/inter/Inter-ExtraLightItalic.woff2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:38bad3a6a9ad1a7fcd8082e6423ba5e0baea16598f2cf304e527844f697e32b2
+size 119320
diff --git a/music/public/fonts/inter/Inter-Italic.woff2 b/music/public/fonts/inter/Inter-Italic.woff2
new file mode 100644
index 0000000..6b36b21
--- /dev/null
+++ b/music/public/fonts/inter/Inter-Italic.woff2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2d078cb3bc8f934740d53b39dd23b0678f2f97477e49ec785dd9d8acd8b96bfc
+size 117700
diff --git a/music/public/fonts/inter/Inter-Light.woff2 b/music/public/fonts/inter/Inter-Light.woff2
new file mode 100644
index 0000000..1feb0f1
--- /dev/null
+++ b/music/public/fonts/inter/Inter-Light.woff2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e111a1e2ad914ccda9179b95e83fb10234dd52a1932e0b93c480476227983fd9
+size 112592
diff --git a/music/public/fonts/inter/Inter-LightItalic.woff2 b/music/public/fonts/inter/Inter-LightItalic.woff2
new file mode 100644
index 0000000..5d65227
--- /dev/null
+++ b/music/public/fonts/inter/Inter-LightItalic.woff2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:050265b27d11860a26c66c054210852661abb5d229c4e0bb3525757a830f3790
+size 119608
diff --git a/music/public/fonts/inter/Inter-Medium.woff2 b/music/public/fonts/inter/Inter-Medium.woff2
new file mode 100644
index 0000000..f6354b1
--- /dev/null
+++ b/music/public/fonts/inter/Inter-Medium.woff2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0ff3e94614e1493eb556314fd247ae6c4a85a7783b4cc86be539940cf83f2a48
+size 114348
diff --git a/music/public/fonts/inter/Inter-MediumItalic.woff2 b/music/public/fonts/inter/Inter-MediumItalic.woff2
new file mode 100644
index 0000000..f578443
--- /dev/null
+++ b/music/public/fonts/inter/Inter-MediumItalic.woff2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1b0df503de488a92082a8c5d72ddc4c0229183ceb645ae1c25c2d0d63778517c
+size 120784
diff --git a/music/public/fonts/inter/Inter-Regular.woff2 b/music/public/fonts/inter/Inter-Regular.woff2
new file mode 100644
index 0000000..cd2b9fe
--- /dev/null
+++ b/music/public/fonts/inter/Inter-Regular.woff2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e06f6b1bc553aaea4e4668023ed0ab0a147129c3107f511bc7d03d361b0ae085
+size 111268
diff --git a/music/public/fonts/inter/Inter-SemiBold.woff2 b/music/public/fonts/inter/Inter-SemiBold.woff2
new file mode 100644
index 0000000..6d48aeb
--- /dev/null
+++ b/music/public/fonts/inter/Inter-SemiBold.woff2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5cb7103e4e605989afebc03d989c79201e54b21b5183db33981f70db9178a301
+size 114812
diff --git a/music/public/fonts/inter/Inter-SemiBoldItalic.woff2 b/music/public/fonts/inter/Inter-SemiBoldItalic.woff2
new file mode 100644
index 0000000..89d1089
--- /dev/null
+++ b/music/public/fonts/inter/Inter-SemiBoldItalic.woff2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5e2a2bc3b5b9af0644f005693b10499dab18065d6aedbf46c8b74ccede27daeb
+size 121416
diff --git a/music/public/fonts/inter/Inter-Thin.woff2 b/music/public/fonts/inter/Inter-Thin.woff2
new file mode 100644
index 0000000..8607f76
--- /dev/null
+++ b/music/public/fonts/inter/Inter-Thin.woff2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:70ca998635d9fc627dede8108f04d0989e6e03346183f0ad0917723e790f6973
+size 109548
diff --git a/music/public/fonts/inter/Inter-ThinItalic.woff2 b/music/public/fonts/inter/Inter-ThinItalic.woff2
new file mode 100644
index 0000000..5637118
--- /dev/null
+++ b/music/public/fonts/inter/Inter-ThinItalic.woff2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:930d666b74e0dc0c26c2821f5643b701f8a41beaf3bb95ce146298104f29169c
+size 116880
diff --git a/music/public/fonts/inter/InterDisplay-Black.woff2 b/music/public/fonts/inter/InterDisplay-Black.woff2
new file mode 100644
index 0000000..d7dbac2
--- /dev/null
+++ b/music/public/fonts/inter/InterDisplay-Black.woff2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:05650cb83c70fda2a465eade0c0a1a6c25d37ec49df3cd74b7e564683f9729f1
+size 110308
diff --git a/music/public/fonts/inter/InterDisplay-BlackItalic.woff2 b/music/public/fonts/inter/InterDisplay-BlackItalic.woff2
new file mode 100644
index 0000000..98950fe
--- /dev/null
+++ b/music/public/fonts/inter/InterDisplay-BlackItalic.woff2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9746bf2bd43fe158144b83ede747088e80396daad55b9e6ea7232e0e22ee6fce
+size 116820
diff --git a/music/public/fonts/inter/InterDisplay-Bold.woff2 b/music/public/fonts/inter/InterDisplay-Bold.woff2
new file mode 100644
index 0000000..c6992e4
--- /dev/null
+++ b/music/public/fonts/inter/InterDisplay-Bold.woff2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:23bc37619593377e128f24660fedb2869d18277b4026cb46e5637be7643faf91
+size 113556
diff --git a/music/public/fonts/inter/InterDisplay-BoldItalic.woff2 b/music/public/fonts/inter/InterDisplay-BoldItalic.woff2
new file mode 100644
index 0000000..30c6bfb
--- /dev/null
+++ b/music/public/fonts/inter/InterDisplay-BoldItalic.woff2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5a0fe1f01b8777fab4a5f73c757743fa52579c7723b0291777625cb53c0a792a
+size 120432
diff --git a/music/public/fonts/inter/InterDisplay-ExtraBold.woff2 b/music/public/fonts/inter/InterDisplay-ExtraBold.woff2
new file mode 100644
index 0000000..a426fdb
--- /dev/null
+++ b/music/public/fonts/inter/InterDisplay-ExtraBold.woff2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:97eeee5a6f4f22fef1f607cf8836104d65968f03e59bc068a7586bf18364a8a7
+size 113636
diff --git a/music/public/fonts/inter/InterDisplay-ExtraBoldItalic.woff2 b/music/public/fonts/inter/InterDisplay-ExtraBoldItalic.woff2
new file mode 100644
index 0000000..7358e69
--- /dev/null
+++ b/music/public/fonts/inter/InterDisplay-ExtraBoldItalic.woff2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:159b72ba47887032915420aa1445ae6768197e7ea732230182998f14d1cdb932
+size 120712
diff --git a/music/public/fonts/inter/InterDisplay-ExtraLight.woff2 b/music/public/fonts/inter/InterDisplay-ExtraLight.woff2
new file mode 100644
index 0000000..f93b602
--- /dev/null
+++ b/music/public/fonts/inter/InterDisplay-ExtraLight.woff2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9031b6d2a1a559753d50b311ecb463ddf7acaaae1d992a7e9b009620d8ce6def
+size 113108
diff --git a/music/public/fonts/inter/InterDisplay-ExtraLightItalic.woff2 b/music/public/fonts/inter/InterDisplay-ExtraLightItalic.woff2
new file mode 100644
index 0000000..0c749bb
--- /dev/null
+++ b/music/public/fonts/inter/InterDisplay-ExtraLightItalic.woff2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e8d7078160ef92b82c2123857c1b3aefaa4a795a44b56df5fe3edade60d81576
+size 120124
diff --git a/music/public/fonts/inter/InterDisplay-Italic.woff2 b/music/public/fonts/inter/InterDisplay-Italic.woff2
new file mode 100644
index 0000000..4d6a0e4
--- /dev/null
+++ b/music/public/fonts/inter/InterDisplay-Italic.woff2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:dfd398d7372b61278e1505bfbdfc5ab0f6511ef887a47a2c90c13475eae585da
+size 116988
diff --git a/music/public/fonts/inter/InterDisplay-Light.woff2 b/music/public/fonts/inter/InterDisplay-Light.woff2
new file mode 100644
index 0000000..c1cdf65
--- /dev/null
+++ b/music/public/fonts/inter/InterDisplay-Light.woff2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:01dd2d491ed863177e61f6e9458813baa85453ad97b32c5c1216927d4eebfe65
+size 112976
diff --git a/music/public/fonts/inter/InterDisplay-LightItalic.woff2 b/music/public/fonts/inter/InterDisplay-LightItalic.woff2
new file mode 100644
index 0000000..84336f7
--- /dev/null
+++ b/music/public/fonts/inter/InterDisplay-LightItalic.woff2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:59b5be56306fd353d067aa6995c2c92bc3feb3ace73684d94e3c6e73fe2beaac
+size 119692
diff --git a/music/public/fonts/inter/InterDisplay-Medium.woff2 b/music/public/fonts/inter/InterDisplay-Medium.woff2
new file mode 100644
index 0000000..cccb58b
--- /dev/null
+++ b/music/public/fonts/inter/InterDisplay-Medium.woff2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f1227907684853882ad00d7f97ce9f64bc17b89a2a291a7d4ec84fccfa442934
+size 113476
diff --git a/music/public/fonts/inter/InterDisplay-MediumItalic.woff2 b/music/public/fonts/inter/InterDisplay-MediumItalic.woff2
new file mode 100644
index 0000000..7d96b60
--- /dev/null
+++ b/music/public/fonts/inter/InterDisplay-MediumItalic.woff2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6bf254c1a65c806c019f52818add66747da83595a314112917911bcf5fa2f5c5
+size 120560
diff --git a/music/public/fonts/inter/InterDisplay-Regular.woff2 b/music/public/fonts/inter/InterDisplay-Regular.woff2
new file mode 100644
index 0000000..8112f1d
--- /dev/null
+++ b/music/public/fonts/inter/InterDisplay-Regular.woff2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3a9463a58c3e7ba1e3cd65b5dbff91a35c508ff78a104cd1121feff83efeb787
+size 108948
diff --git a/music/public/fonts/inter/InterDisplay-SemiBold.woff2 b/music/public/fonts/inter/InterDisplay-SemiBold.woff2
new file mode 100644
index 0000000..5b42dac
--- /dev/null
+++ b/music/public/fonts/inter/InterDisplay-SemiBold.woff2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d9f63a82b826fb0117c92715c3a52a2d2247bc321bc39341420bf52d91e8277a
+size 113772
diff --git a/music/public/fonts/inter/InterDisplay-SemiBoldItalic.woff2 b/music/public/fonts/inter/InterDisplay-SemiBoldItalic.woff2
new file mode 100644
index 0000000..257b0fa
--- /dev/null
+++ b/music/public/fonts/inter/InterDisplay-SemiBoldItalic.woff2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:dd9d78314bdd5062302a73c7e2954008bab8158304fa7ac4f0ea8977b841dccb
+size 120468
diff --git a/music/public/fonts/inter/InterDisplay-Thin.woff2 b/music/public/fonts/inter/InterDisplay-Thin.woff2
new file mode 100644
index 0000000..18ede47
--- /dev/null
+++ b/music/public/fonts/inter/InterDisplay-Thin.woff2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:21cfe42e7f1031b410e247ed199f43fc362415af81b3a8e442a97f63b4dbe327
+size 108820
diff --git a/music/public/fonts/inter/InterDisplay-ThinItalic.woff2 b/music/public/fonts/inter/InterDisplay-ThinItalic.woff2
new file mode 100644
index 0000000..8b58c68
--- /dev/null
+++ b/music/public/fonts/inter/InterDisplay-ThinItalic.woff2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:eff21589eacf1f6a596191038d7b47b4dd2455a096b175bb3572ecb6d026a611
+size 116040
diff --git a/music/public/fonts/inter/InterVariable-Italic.woff2 b/music/public/fonts/inter/InterVariable-Italic.woff2
new file mode 100644
index 0000000..318e720
--- /dev/null
+++ b/music/public/fonts/inter/InterVariable-Italic.woff2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e564f652916db6c139570fefb9524a77c4d48f30c92928de9db19b6b5c7a262a
+size 387976
diff --git a/music/public/fonts/inter/InterVariable.woff2 b/music/public/fonts/inter/InterVariable.woff2
new file mode 100644
index 0000000..e511e5e
--- /dev/null
+++ b/music/public/fonts/inter/InterVariable.woff2
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:693b77d4f32ee9b8bfc995589b5fad5e99adf2832738661f5402f9978429a8e3
+size 352240
diff --git a/music/public/fonts/inter/LICENSE.txt b/music/public/fonts/inter/LICENSE.txt
new file mode 100644
index 0000000..9b2ca37
--- /dev/null
+++ b/music/public/fonts/inter/LICENSE.txt
@@ -0,0 +1,92 @@
+Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION AND CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/music/public/fonts/inter/inter.css b/music/public/fonts/inter/inter.css
new file mode 100644
index 0000000..6144065
--- /dev/null
+++ b/music/public/fonts/inter/inter.css
@@ -0,0 +1,148 @@
+/* Variable fonts usage:
+:root { font-family: "Inter", sans-serif; }
+@supports (font-variation-settings: normal) {
+ :root { font-family: "InterVariable", sans-serif; font-optical-sizing: auto; }
+} */
+@font-face {
+ font-family: InterVariable;
+ font-style: normal;
+ font-weight: 100 900;
+ font-display: swap;
+ src: url("InterVariable.woff2") format("woff2");
+}
+@font-face {
+ font-family: InterVariable;
+ font-style: italic;
+ font-weight: 100 900;
+ font-display: swap;
+ src: url("InterVariable-Italic.woff2") format("woff2");
+}
+
+/* static fonts */
+@font-face { font-family: "Inter"; font-style: normal; font-weight: 100; font-display: swap; src: url("Inter-Thin.woff2") format("woff2"); }
+@font-face { font-family: "Inter"; font-style: italic; font-weight: 100; font-display: swap; src: url("Inter-ThinItalic.woff2") format("woff2"); }
+@font-face { font-family: "Inter"; font-style: normal; font-weight: 200; font-display: swap; src: url("Inter-ExtraLight.woff2") format("woff2"); }
+@font-face { font-family: "Inter"; font-style: italic; font-weight: 200; font-display: swap; src: url("Inter-ExtraLightItalic.woff2") format("woff2"); }
+@font-face { font-family: "Inter"; font-style: normal; font-weight: 300; font-display: swap; src: url("Inter-Light.woff2") format("woff2"); }
+@font-face { font-family: "Inter"; font-style: italic; font-weight: 300; font-display: swap; src: url("Inter-LightItalic.woff2") format("woff2"); }
+@font-face { font-family: "Inter"; font-style: normal; font-weight: 400; font-display: swap; src: url("Inter-Regular.woff2") format("woff2"); }
+@font-face { font-family: "Inter"; font-style: italic; font-weight: 400; font-display: swap; src: url("Inter-Italic.woff2") format("woff2"); }
+@font-face { font-family: "Inter"; font-style: normal; font-weight: 500; font-display: swap; src: url("Inter-Medium.woff2") format("woff2"); }
+@font-face { font-family: "Inter"; font-style: italic; font-weight: 500; font-display: swap; src: url("Inter-MediumItalic.woff2") format("woff2"); }
+@font-face { font-family: "Inter"; font-style: normal; font-weight: 600; font-display: swap; src: url("Inter-SemiBold.woff2") format("woff2"); }
+@font-face { font-family: "Inter"; font-style: italic; font-weight: 600; font-display: swap; src: url("Inter-SemiBoldItalic.woff2") format("woff2"); }
+@font-face { font-family: "Inter"; font-style: normal; font-weight: 700; font-display: swap; src: url("Inter-Bold.woff2") format("woff2"); }
+@font-face { font-family: "Inter"; font-style: italic; font-weight: 700; font-display: swap; src: url("Inter-BoldItalic.woff2") format("woff2"); }
+@font-face { font-family: "Inter"; font-style: normal; font-weight: 800; font-display: swap; src: url("Inter-ExtraBold.woff2") format("woff2"); }
+@font-face { font-family: "Inter"; font-style: italic; font-weight: 800; font-display: swap; src: url("Inter-ExtraBoldItalic.woff2") format("woff2"); }
+@font-face { font-family: "Inter"; font-style: normal; font-weight: 900; font-display: swap; src: url("Inter-Black.woff2") format("woff2"); }
+@font-face { font-family: "Inter"; font-style: italic; font-weight: 900; font-display: swap; src: url("Inter-BlackItalic.woff2") format("woff2"); }
+@font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 100; font-display: swap; src: url("InterDisplay-Thin.woff2") format("woff2"); }
+@font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 100; font-display: swap; src: url("InterDisplay-ThinItalic.woff2") format("woff2"); }
+@font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 200; font-display: swap; src: url("InterDisplay-ExtraLight.woff2") format("woff2"); }
+@font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 200; font-display: swap; src: url("InterDisplay-ExtraLightItalic.woff2") format("woff2"); }
+@font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 300; font-display: swap; src: url("InterDisplay-Light.woff2") format("woff2"); }
+@font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 300; font-display: swap; src: url("InterDisplay-LightItalic.woff2") format("woff2"); }
+@font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 400; font-display: swap; src: url("InterDisplay-Regular.woff2") format("woff2"); }
+@font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 400; font-display: swap; src: url("InterDisplay-Italic.woff2") format("woff2"); }
+@font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 500; font-display: swap; src: url("InterDisplay-Medium.woff2") format("woff2"); }
+@font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 500; font-display: swap; src: url("InterDisplay-MediumItalic.woff2") format("woff2"); }
+@font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 600; font-display: swap; src: url("InterDisplay-SemiBold.woff2") format("woff2"); }
+@font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 600; font-display: swap; src: url("InterDisplay-SemiBoldItalic.woff2") format("woff2"); }
+@font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 700; font-display: swap; src: url("InterDisplay-Bold.woff2") format("woff2"); }
+@font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 700; font-display: swap; src: url("InterDisplay-BoldItalic.woff2") format("woff2"); }
+@font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 800; font-display: swap; src: url("InterDisplay-ExtraBold.woff2") format("woff2"); }
+@font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 800; font-display: swap; src: url("InterDisplay-ExtraBoldItalic.woff2") format("woff2"); }
+@font-face { font-family: "InterDisplay"; font-style: normal; font-weight: 900; font-display: swap; src: url("InterDisplay-Black.woff2") format("woff2"); }
+@font-face { font-family: "InterDisplay"; font-style: italic; font-weight: 900; font-display: swap; src: url("InterDisplay-BlackItalic.woff2") format("woff2"); }
+
+@font-feature-values InterVariable {
+ @character-variant {
+ cv01: 1; cv02: 2; cv03: 3; cv04: 4; cv05: 5; cv06: 6; cv07: 7; cv08: 8;
+ cv09: 9; cv10: 10; cv11: 11; cv12: 12; cv13: 13;
+ alt-1: 1; /* Alternate one */
+ alt-3: 9; /* Flat-top three */
+ open-4: 2; /* Open four */
+ open-6: 3; /* Open six */
+ open-9: 4; /* Open nine */
+ lc-l-with-tail: 5; /* Lower-case L with tail */
+ simplified-u: 6; /* Simplified u */
+ alt-double-s: 7; /* Alternate German double s */
+ uc-i-with-serif: 8; /* Upper-case i with serif */
+ uc-g-with-spur: 10; /* Capital G with spur */
+ single-story-a: 11; /* Single-story a */
+ compact-lc-f: 12; /* Compact f */
+ compact-lc-t: 13; /* Compact t */
+ }
+ @styleset {
+ ss01: 1; ss02: 2; ss03: 3; ss04: 4; ss05: 5; ss06: 6; ss07: 7; ss08: 8;
+ open-digits: 1; /* Open digits */
+ disambiguation: 2; /* Disambiguation (with zero) */
+ disambiguation-except-zero: 4; /* Disambiguation (no zero) */
+ round-quotes-and-commas: 3; /* Round quotes & commas */
+ square-punctuation: 7; /* Square punctuation */
+ square-quotes: 8; /* Square quotes */
+ circled-characters: 5; /* Circled characters */
+ squared-characters: 6; /* Squared characters */
+ }
+}
+@font-feature-values Inter {
+ @character-variant {
+ cv01: 1; cv02: 2; cv03: 3; cv04: 4; cv05: 5; cv06: 6; cv07: 7; cv08: 8;
+ cv09: 9; cv10: 10; cv11: 11; cv12: 12; cv13: 13;
+ alt-1: 1; /* Alternate one */
+ alt-3: 9; /* Flat-top three */
+ open-4: 2; /* Open four */
+ open-6: 3; /* Open six */
+ open-9: 4; /* Open nine */
+ lc-l-with-tail: 5; /* Lower-case L with tail */
+ simplified-u: 6; /* Simplified u */
+ alt-double-s: 7; /* Alternate German double s */
+ uc-i-with-serif: 8; /* Upper-case i with serif */
+ uc-g-with-spur: 10; /* Capital G with spur */
+ single-story-a: 11; /* Single-story a */
+ compact-lc-f: 12; /* Compact f */
+ compact-lc-t: 13; /* Compact t */
+ }
+ @styleset {
+ ss01: 1; ss02: 2; ss03: 3; ss04: 4; ss05: 5; ss06: 6; ss07: 7; ss08: 8;
+ open-digits: 1; /* Open digits */
+ disambiguation: 2; /* Disambiguation (with zero) */
+ disambiguation-except-zero: 4; /* Disambiguation (no zero) */
+ round-quotes-and-commas: 3; /* Round quotes & commas */
+ square-punctuation: 7; /* Square punctuation */
+ square-quotes: 8; /* Square quotes */
+ circled-characters: 5; /* Circled characters */
+ squared-characters: 6; /* Squared characters */
+ }
+}
+@font-feature-values InterDisplay {
+ @character-variant {
+ cv01: 1; cv02: 2; cv03: 3; cv04: 4; cv05: 5; cv06: 6; cv07: 7; cv08: 8;
+ cv09: 9; cv10: 10; cv11: 11; cv12: 12; cv13: 13;
+ alt-1: 1; /* Alternate one */
+ alt-3: 9; /* Flat-top three */
+ open-4: 2; /* Open four */
+ open-6: 3; /* Open six */
+ open-9: 4; /* Open nine */
+ lc-l-with-tail: 5; /* Lower-case L with tail */
+ simplified-u: 6; /* Simplified u */
+ alt-double-s: 7; /* Alternate German double s */
+ uc-i-with-serif: 8; /* Upper-case i with serif */
+ uc-g-with-spur: 10; /* Capital G with spur */
+ single-story-a: 11; /* Single-story a */
+ compact-lc-f: 12; /* Compact f */
+ compact-lc-t: 13; /* Compact t */
+ }
+ @styleset {
+ ss01: 1; ss02: 2; ss03: 3; ss04: 4; ss05: 5; ss06: 6; ss07: 7; ss08: 8;
+ open-digits: 1; /* Open digits */
+ disambiguation: 2; /* Disambiguation (with zero) */
+ disambiguation-except-zero: 4; /* Disambiguation (no zero) */
+ round-quotes-and-commas: 3; /* Round quotes & commas */
+ square-punctuation: 7; /* Square punctuation */
+ square-quotes: 8; /* Square quotes */
+ circled-characters: 5; /* Circled characters */
+ squared-characters: 6; /* Squared characters */
+ }
+}
diff --git a/music/public/music.css b/music/public/music.css
new file mode 100644
index 0000000..f86e398
--- /dev/null
+++ b/music/public/music.css
@@ -0,0 +1,120 @@
+:root {
+ --sp-1: 1px;
+ --sp-2: 2px;
+ --sp-3: 4px;
+ --sp-4: 8px;
+ --sp-5: 16px;
+
+ --col-fg: #ffffff;
+
+ --smooth: cubic-bezier(.1, 1, .5, 1);
+}
+
+body {
+ width: 100%;
+ height: 100vh;
+ margin: 0;
+ font-family: Inter, sans-serif;
+ font-feature-settings: 'liga' 1, 'calt' 1;
+
+ color: var(--col-fg);
+ background-color: transparent;
+
+ text-shadow:
+ var(--sp-1) var(--sp-1) var(--sp-4) #000,
+ var(--sp-1) var(--sp-2) #0008;
+}
+@supports (font-variation-settings: normal) {
+ body { font-family: InterVariable, sans-serif; }
+}
+
+.flex {
+ display: flex;
+}
+
+.flex-col {
+ flex-direction: column;
+}
+
+.flex-row {
+ flex-direction: row;
+}
+
+.justify-center {
+ justify-content: center;
+}
+
+.gap-1 { gap: var(--sp-1); }
+.gap-2 { gap: var(--sp-2); }
+.gap-3 { gap: var(--sp-3); }
+.gap-4 { gap: var(--sp-4); }
+.gap-5 { gap: var(--sp-5); }
+
+.text-ellipses {
+ text-overflow: ellipsis;
+ overflow-x: clip;
+ text-wrap: nowrap;
+}
+
+p {
+ margin: 0;
+}
+
+#music {
+ position: fixed;
+ left: 0;
+ bottom: 0;
+ width: min-content;
+ padding: 2em;
+}
+
+.nowplaying {
+ margin-bottom: var(--sp-3);
+ text-transform: uppercase;
+ opacity: .5;
+ font-weight: bold;
+}
+
+.artwork {
+ width: 80px;
+ height: 80px;
+ border-radius: var(--sp-3);
+ background: #0008;
+ box-shadow:
+ 0 0 var(--sp-5) #000c,
+ var(--sp-1) var(--sp-1) var(--sp-2) #0004;
+ transition: transform .5s var(--smooth), opacity .5s, filter .5s;
+}
+
+.title-artist {
+ max-width: calc(100vw - 2em - 80px);
+}
+
+.title {
+ padding: 0 var(--sp-5);
+ font-size: 2em;
+ font-weight: 800;
+ transition: transform .5s var(--smooth), opacity .1s;
+}
+
+.artist {
+ padding: 0 var(--sp-5);
+ margin-top: calc(0px - var(--sp-3));
+ font-size: 1.5em;
+ font-weight: 600;
+ transition: transform .5s var(--smooth), opacity .1s;
+ transition-delay: .05s;
+}
+
+#music.paused, #music.switching {
+ .artwork {
+ transform: scale(0.8);
+ opacity: .5;
+ filter: grayscale(1);
+ }
+
+ .title, .artist {
+ transform: translateX(-16px);
+ opacity: 0;
+ }
+}
diff --git a/music/public/music.js b/music/public/music.js
new file mode 100644
index 0000000..115452b
--- /dev/null
+++ b/music/public/music.js
@@ -0,0 +1,48 @@
+const musicEl = document.getElementById("music");
+const artworkEl = document.querySelector(".artwork");
+const titleEl = document.querySelector(".title");
+const artistEl = document.querySelector(".artist");
+
+async function sync() {
+ const eventSource = new EventSource("/music/sse");
+ eventSource.addEventListener("open", () => {
+ console.log("Connected.");
+
+ eventSource.addEventListener("music", event => {
+ const nextTrack = JSON.parse(event.data)
+ console.log(`Now playing: ${nextTrack.artist} - ${nextTrack.title}`)
+
+ musicEl.classList.add("switching");
+
+ setTimeout(() => {
+ musicEl.classList.remove("switching");
+ titleEl.innerText = nextTrack.title;
+ artistEl.innerText = nextTrack.artist;
+ artworkEl.src = "/music/artwork?r=" + Math.round(Math.random() * 1_000_000);
+ }, 500);
+ });
+
+ eventSource.addEventListener("music-state", event => {
+ const data = JSON.parse(event.data)
+ console.log(data.playing ? "Music playing." : "Music paused.");
+ if (data.playing)
+ musicEl.classList.remove("paused");
+ else
+ musicEl.classList.add("paused");
+ });
+
+ const errListener = eventSource.addEventListener("error", () => {
+ eventSource.removeEventListener("error", errListener);
+ eventSource.close();
+ console.error("Connection lost, or an error has occurred.");
+ console.log("Attempting to reconnect...");
+
+ // TODO: stop reconnecting after 10 failed attempts
+
+ setTimeout(() => {
+ sync();
+ }, 1000);
+ });
+ });
+}
+sync();