-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
1,029 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.