fully-functioning music status!
- accessible via /music - only detects strawberry for now
This commit is contained in:
parent
f838edeadb
commit
8a1fabf91a
53 changed files with 923 additions and 51 deletions
58
.air.toml
Normal file
58
.air.toml
Normal file
|
|
@ -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
|
||||
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
*.woff2 filter=lfs diff=lfs merge=lfs -text
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1 +1,2 @@
|
|||
tmp/
|
||||
learning-title.txt
|
||||
|
|
|
|||
3
go.mod
3
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
|
||||
|
|
|
|||
2
go.sum
2
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=
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
}
|
||||
}
|
||||
|
|
|
|||
7
main.go
7
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))
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
package mpris
|
||||
292
music/music.go
Normal file
292
music/music.go
Normal file
|
|
@ -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
|
||||
}
|
||||
31
music/pages/music.html
Normal file
31
music/pages/music.html
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Nothing is playing...</title>
|
||||
<link rel="stylesheet" href="/music/public/fonts/inter/inter.css">
|
||||
<link rel="stylesheet" href="/music/public/music.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="music" class="paused">
|
||||
<div class="flex flex-col">
|
||||
<!-- <p class="nowplaying">Now Playing</p> -->
|
||||
|
||||
<div class="flex flex-row">
|
||||
<img src="/music/artwork"
|
||||
alt="song artwork"
|
||||
class="artwork"
|
||||
width="96" height="96"/>
|
||||
|
||||
<div class="flex flex-col justify-center title-artist">
|
||||
<p class="title text-ellipses">Untitled Track</p>
|
||||
<p class="artist text-ellipses">Unknown Artist</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/music/public/music.js"></script>
|
||||
<div id="cover"></div>
|
||||
</body>
|
||||
</html>
|
||||
BIN
music/public/default-cover-art.png
Executable file
BIN
music/public/default-cover-art.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 139 KiB |
BIN
music/public/fonts/inter/Inter-Black.woff2
(Stored with Git LFS)
Normal file
BIN
music/public/fonts/inter/Inter-Black.woff2
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
music/public/fonts/inter/Inter-BlackItalic.woff2
(Stored with Git LFS)
Normal file
BIN
music/public/fonts/inter/Inter-BlackItalic.woff2
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
music/public/fonts/inter/Inter-Bold.woff2
(Stored with Git LFS)
Normal file
BIN
music/public/fonts/inter/Inter-Bold.woff2
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
music/public/fonts/inter/Inter-BoldItalic.woff2
(Stored with Git LFS)
Normal file
BIN
music/public/fonts/inter/Inter-BoldItalic.woff2
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
music/public/fonts/inter/Inter-ExtraBold.woff2
(Stored with Git LFS)
Normal file
BIN
music/public/fonts/inter/Inter-ExtraBold.woff2
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
music/public/fonts/inter/Inter-ExtraBoldItalic.woff2
(Stored with Git LFS)
Normal file
BIN
music/public/fonts/inter/Inter-ExtraBoldItalic.woff2
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
music/public/fonts/inter/Inter-ExtraLight.woff2
(Stored with Git LFS)
Normal file
BIN
music/public/fonts/inter/Inter-ExtraLight.woff2
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
music/public/fonts/inter/Inter-ExtraLightItalic.woff2
(Stored with Git LFS)
Normal file
BIN
music/public/fonts/inter/Inter-ExtraLightItalic.woff2
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
music/public/fonts/inter/Inter-Italic.woff2
(Stored with Git LFS)
Normal file
BIN
music/public/fonts/inter/Inter-Italic.woff2
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
music/public/fonts/inter/Inter-Light.woff2
(Stored with Git LFS)
Normal file
BIN
music/public/fonts/inter/Inter-Light.woff2
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
music/public/fonts/inter/Inter-LightItalic.woff2
(Stored with Git LFS)
Normal file
BIN
music/public/fonts/inter/Inter-LightItalic.woff2
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
music/public/fonts/inter/Inter-Medium.woff2
(Stored with Git LFS)
Normal file
BIN
music/public/fonts/inter/Inter-Medium.woff2
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
music/public/fonts/inter/Inter-MediumItalic.woff2
(Stored with Git LFS)
Normal file
BIN
music/public/fonts/inter/Inter-MediumItalic.woff2
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
music/public/fonts/inter/Inter-Regular.woff2
(Stored with Git LFS)
Normal file
BIN
music/public/fonts/inter/Inter-Regular.woff2
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
music/public/fonts/inter/Inter-SemiBold.woff2
(Stored with Git LFS)
Normal file
BIN
music/public/fonts/inter/Inter-SemiBold.woff2
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
music/public/fonts/inter/Inter-SemiBoldItalic.woff2
(Stored with Git LFS)
Normal file
BIN
music/public/fonts/inter/Inter-SemiBoldItalic.woff2
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
music/public/fonts/inter/Inter-Thin.woff2
(Stored with Git LFS)
Normal file
BIN
music/public/fonts/inter/Inter-Thin.woff2
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
music/public/fonts/inter/Inter-ThinItalic.woff2
(Stored with Git LFS)
Normal file
BIN
music/public/fonts/inter/Inter-ThinItalic.woff2
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
music/public/fonts/inter/InterDisplay-Black.woff2
(Stored with Git LFS)
Normal file
BIN
music/public/fonts/inter/InterDisplay-Black.woff2
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
music/public/fonts/inter/InterDisplay-BlackItalic.woff2
(Stored with Git LFS)
Normal file
BIN
music/public/fonts/inter/InterDisplay-BlackItalic.woff2
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
music/public/fonts/inter/InterDisplay-Bold.woff2
(Stored with Git LFS)
Normal file
BIN
music/public/fonts/inter/InterDisplay-Bold.woff2
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
music/public/fonts/inter/InterDisplay-BoldItalic.woff2
(Stored with Git LFS)
Normal file
BIN
music/public/fonts/inter/InterDisplay-BoldItalic.woff2
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
music/public/fonts/inter/InterDisplay-ExtraBold.woff2
(Stored with Git LFS)
Normal file
BIN
music/public/fonts/inter/InterDisplay-ExtraBold.woff2
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
music/public/fonts/inter/InterDisplay-ExtraBoldItalic.woff2
(Stored with Git LFS)
Normal file
BIN
music/public/fonts/inter/InterDisplay-ExtraBoldItalic.woff2
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
music/public/fonts/inter/InterDisplay-ExtraLight.woff2
(Stored with Git LFS)
Normal file
BIN
music/public/fonts/inter/InterDisplay-ExtraLight.woff2
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
music/public/fonts/inter/InterDisplay-ExtraLightItalic.woff2
(Stored with Git LFS)
Normal file
BIN
music/public/fonts/inter/InterDisplay-ExtraLightItalic.woff2
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
music/public/fonts/inter/InterDisplay-Italic.woff2
(Stored with Git LFS)
Normal file
BIN
music/public/fonts/inter/InterDisplay-Italic.woff2
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
music/public/fonts/inter/InterDisplay-Light.woff2
(Stored with Git LFS)
Normal file
BIN
music/public/fonts/inter/InterDisplay-Light.woff2
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
music/public/fonts/inter/InterDisplay-LightItalic.woff2
(Stored with Git LFS)
Normal file
BIN
music/public/fonts/inter/InterDisplay-LightItalic.woff2
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
music/public/fonts/inter/InterDisplay-Medium.woff2
(Stored with Git LFS)
Normal file
BIN
music/public/fonts/inter/InterDisplay-Medium.woff2
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
music/public/fonts/inter/InterDisplay-MediumItalic.woff2
(Stored with Git LFS)
Normal file
BIN
music/public/fonts/inter/InterDisplay-MediumItalic.woff2
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
music/public/fonts/inter/InterDisplay-Regular.woff2
(Stored with Git LFS)
Normal file
BIN
music/public/fonts/inter/InterDisplay-Regular.woff2
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
music/public/fonts/inter/InterDisplay-SemiBold.woff2
(Stored with Git LFS)
Normal file
BIN
music/public/fonts/inter/InterDisplay-SemiBold.woff2
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
music/public/fonts/inter/InterDisplay-SemiBoldItalic.woff2
(Stored with Git LFS)
Normal file
BIN
music/public/fonts/inter/InterDisplay-SemiBoldItalic.woff2
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
music/public/fonts/inter/InterDisplay-Thin.woff2
(Stored with Git LFS)
Normal file
BIN
music/public/fonts/inter/InterDisplay-Thin.woff2
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
music/public/fonts/inter/InterDisplay-ThinItalic.woff2
(Stored with Git LFS)
Normal file
BIN
music/public/fonts/inter/InterDisplay-ThinItalic.woff2
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
music/public/fonts/inter/InterVariable-Italic.woff2
(Stored with Git LFS)
Normal file
BIN
music/public/fonts/inter/InterVariable-Italic.woff2
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
music/public/fonts/inter/InterVariable.woff2
(Stored with Git LFS)
Normal file
BIN
music/public/fonts/inter/InterVariable.woff2
(Stored with Git LFS)
Normal file
Binary file not shown.
92
music/public/fonts/inter/LICENSE.txt
Normal file
92
music/public/fonts/inter/LICENSE.txt
Normal file
|
|
@ -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.
|
||||
148
music/public/fonts/inter/inter.css
Normal file
148
music/public/fonts/inter/inter.css
Normal file
|
|
@ -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 */
|
||||
}
|
||||
}
|
||||
120
music/public/music.css
Normal file
120
music/public/music.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
48
music/public/music.js
Normal file
48
music/public/music.js
Normal file
|
|
@ -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();
|
||||
Loading…
Add table
Add a link
Reference in a new issue