Skip to content

Commit

Permalink
Initial project commit
Browse files Browse the repository at this point in the history
  • Loading branch information
emmaly committed Feb 20, 2025
1 parent 224c917 commit 6345f10
Show file tree
Hide file tree
Showing 11 changed files with 1,029 additions and 0 deletions.
88 changes: 88 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# SorceressEmmaly's MusicState - a music visualization overlay for streams

A sleek music visualization overlay for your streams that shows what's currently playing in TIDAL or Spotify.

## Features

- Shows current song and artist information from TIDAL or Spotify
- Smooth animations for song changes
- Customizable appearance including:
- Position on screen (9 different anchor points)
- Colors
- Size
- Animation speed
- Background opacity
- Mini mode for a more compact display
- Works as a browser source in OBS, Streamlabs, etc.
- No login or authentication required - works by detecting currently playing songs locally

## Setup

1. Download and run the application
2. It will start a local web server and display the URL to use (typically `http://localhost:52846`)
3. Add this URL as a browser source in your streaming software
4. Play music in either TIDAL or Spotify desktop applications
5. The overlay will automatically detect and display currently playing songs

## Customization

You can customize the appearance by adding URL parameters:

- `origin`: Position anchor (default: `bl`)
- `tl`: Top left
- `tc`: Top center
- `tr`: Top right
- `cl`: Center left
- `cc`: Center
- `cr`: Center right
- `bl`: Bottom left
- `bc`: Bottom center
- `br`: Bottom right
- `size`: Font size in pixels (default: `36`)
- `color`: Text color (default: `white`)
- `bg`: Background color/opacity (default: `rgba(0,0,0,0.5)`)
- `speed`: Animation speed in seconds (default: `0.6`)
- `mini`: Scale factor for mini mode (default: `0.6`)
- `dwell`: How long to show full size before mini mode in seconds (default: `3`)

Example URL with parameters:

```text
http://localhost:52846?origin=tr&size=42&color=yellow&bg=rgba(0,0,0,0.8)&speed=0.8&mini=0.5&dwell=5
```

## Requirements

- Windows OS _(for now)_
- TIDAL or Spotify desktop application
- Web browser or streaming software that supports browser sources

## Building from Source

1. Ensure you have Go installed
2. Clone the repository
3. Run `go build`

## Limitations

- Currently only supports Windows _(for now)_
- Works with TIDAL and Spotify desktop applications only (not web players)
- Requires the desktop applications to be running and playing music

## Contributing

Contributions are welcome! Please feel free to submit pull requests.

## License

This project is open source and available under the GNU General Public License v3 (GPL-3.0). This means:

- Anyone can view, use, modify, and distribute the code
- If you distribute modified versions, you must:
- Make your source code available
- License it under GPL-3.0
- Document your changes
- If this code is used in a larger software project, that project must also be GPL-3.0 compliant
- No additional restrictions can be placed on recipients of the code

This license is specifically chosen to ensure that improvements to the code remain available to the community.
15 changes: 15 additions & 0 deletions cmd/obstest/obstest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package main

import (
"github.com/emmaly/musicstate/obs/config"
)

func main() {
c, err := config.GetLocalConfig()
if err != nil {
panic(err)
}

println("Port:", c.Port)
println("Password:", c.Password)
}
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/emmaly/musicstate

go 1.23.1

require github.com/gorilla/websocket v1.5.3
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
222 changes: 222 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
package main

import (
"embed"
"fmt"
"log"
"net/http"
"regexp"
"strings"
"sync"
"time"

"github.com/emmaly/musicstate/winapi"
"github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true // Allow all origins
},
}

type Song struct {
Artist string `json:"artist"`
Song string `json:"song"`
AlbumArt string `json:"albumArt"`
}

type SongUpdate struct {
Type string `json:"type"` // "change" or "stop"
Artist string `json:"artist"` // Only used for "change"
Song string `json:"song"` // Only used for "change"
AlbumArt string `json:"albumArt"` // Only used for "change"
}

type Server struct {
song *Song
connections []*websocket.Conn
mu sync.Mutex
}

//go:embed static/*
var staticFiles embed.FS

func main() {
fileServer := http.FileServer(http.FS(staticFiles))
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Rewrite the path to include "static"
r.URL.Path = "/static" + r.URL.Path
fileServer.ServeHTTP(w, r)
})

// Handle WebSocket connections
http.HandleFunc("/ws", wsHandler())

hostAddr := "localhost:52846"
fmt.Printf("SorceressEmmaly's MusicState\nCopyright (C) 2025 emmaly\nSee https://github.com/emmaly/musicstate for documentation, source code, and to file issues.\nThis program is licensed GPLv3; it comes with ABSOLUTELY NO WARRANTY.\nThis is free software, and you are welcome to redistribute it under certain conditions.\nReview license details at https://github.com/emmaly/musicstate/LICENSE\n\n\nUse http://%s as your browser source overlay URL.\n\n\n", hostAddr)
err := http.ListenAndServe(hostAddr, nil)
if err != nil {
log.Fatal("error starting service: ", err)
}
}

func wsHandler() http.HandlerFunc {
server := Server{}

// Start watching for music changes
go server.watchMusic()

return func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("Upgrade error:", err)
return
}
defer conn.Close()

server.addConnection(conn)
defer server.removeConnection(conn)

if server.song != nil {
// Send the current song to the new connection
err = conn.WriteJSON(server.song.AsSongUpdate())
if err != nil {
log.Println("Write error:", err)
return
}
}

// Keep connection alive and handle any incoming messages
for {
_, _, err := conn.ReadMessage()
if err != nil {
log.Println("Read error:", err)
break
}
}
}
}

func (s *Song) AsSongUpdate() SongUpdate {
if s != nil {
return SongUpdate{
Type: "change",
Artist: s.Artist,
Song: s.Song,
AlbumArt: s.AlbumArt,
}
}

return SongUpdate{Type: "stop"}
}

func (server *Server) reportMusic(song *Song) {
server.mu.Lock()
defer server.mu.Unlock()

if (server.song == nil && song == nil) ||
(server.song != nil &&
song != nil &&
server.song.Song == song.Song &&
server.song.Artist == song.Artist &&
server.song.AlbumArt == song.AlbumArt) {
return // nothing to do
}

server.song = song

if server.connections == nil {
return
}

update := song.AsSongUpdate()

// Send to all connected clients
for _, conn := range server.connections {
err := conn.WriteJSON(update)
if err != nil {
log.Println("Write error:", err)
}
}
}

func (server *Server) addConnection(conn *websocket.Conn) {
server.mu.Lock()
defer server.mu.Unlock()

if server.connections == nil {
server.connections = make([]*websocket.Conn, 0)
}

server.connections = append(server.connections, conn)
}

func (server *Server) removeConnection(conn *websocket.Conn) {
server.mu.Lock()
defer server.mu.Unlock()

if server.connections == nil {
return
}

for i, c := range server.connections {
if c == conn {
server.connections = append(server.connections[:i], server.connections[i+1:]...)
break
}
}
}

func (server *Server) watchMusic() {
const (
tidal = "TIDAL.exe"
spotify = "Spotify.exe"
)

for {
windows, err := winapi.FindWindowsByProcess(
[]string{tidal, spotify},
winapi.WinVisible(true),
winapi.WinClass("Chrome_WidgetWin_1"),
winapi.WinTitlePattern(*regexp.MustCompile("-")),
)
if err != nil {
log.Fatal(err)
}

var song *Song

for _, w := range windows {
delimiter := " - "
songParts := strings.Split(w.Title, delimiter)
if len(songParts) >= 2 {
if w.ProcessName == tidal {
song = &Song{
Artist: songParts[len(songParts)-1],
Song: strings.Join(songParts[:len(songParts)-1], delimiter),
}
} else if w.ProcessName == spotify {
song = &Song{
Artist: songParts[0],
Song: strings.Join(songParts[1:], delimiter),
}
}
}
if song != nil {
break
}
}

if song != nil && song.Song != "" && song.AlbumArt == "" {
// set the placeholder album art
song.AlbumArt = "http://localhost:52846/images/album.jpg"
}

server.reportMusic(song)

time.Sleep(1 * time.Second)
}
}
27 changes: 27 additions & 0 deletions musicstate/musicstate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package musicstate

// NowPlaying represents the current track information from any supported player
type NowPlaying struct {
Artist string
Title string
Player string // e.g. "TIDAL", "Spotify"
WindowID uintptr // Platform-specific window identifier
}

// Player defines the interface for music player adapters
type Player interface {
// Name returns the friendly name of the player (e.g. "TIDAL", "Spotify")
Name() string

// ProcessName returns the executable name to search for (e.g. "TIDAL.exe")
ProcessName() string

// ParseWindowTitle attempts to extract track info from the window title
ParseWindowTitle(title string) (*NowPlaying, error)
}

// GetCurrentTrack returns NowPlaying info from the first found supported player
func GetCurrentTrack(players ...Player) (*NowPlaying, error) {
// Implementation will use winapi package
return nil, nil
}
Loading

0 comments on commit 6345f10

Please sign in to comment.