From 06fabf5d93090816209662b82c64eef535550d01 Mon Sep 17 00:00:00 2001 From: kokopi-dev Date: Sun, 21 Dec 2025 20:16:43 +0900 Subject: [PATCH] init --- .gitignore | 14 +++++ README.md | 19 ++++++ config/config.go | 94 ++++++++++++++++++++++++++++++ go.mod | 8 +++ go.sum | 4 ++ main.go | 138 ++++++++++++++++++++++++++++++++++++++++++++ scripts/build.sh | 3 + scripts/playlist.sh | 11 ++++ 8 files changed, 291 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 config/config.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100755 scripts/build.sh create mode 100755 scripts/playlist.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e22be8d --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +*.exe +*.dll +*.so +*.dylib +*.test +*.out + +/bin/ +/dist/ + +config.json +.env + +mpv-discord-rpc diff --git a/README.md b/README.md new file mode 100644 index 0000000..32c4ce1 --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# mpv Discord RPC +For updating discord music status when playing with mpv + +## Note +For Linux only + + +### Build +``` +cd scripts +./build.sh + +# this creates mpv-discord-rpc binary in the root project folder +``` + +Running the built binary `mpv-discord-rpc` the first time will create the config file in `~/.config/mpv-discord-rpc`, fill in the config with the correct data: + - `client_id`: Your own created discord application's `application id` + - `music_folders`: The folder(s) of music that you want discord to see + diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..3eb80a6 --- /dev/null +++ b/config/config.go @@ -0,0 +1,94 @@ +package config + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" +) + +type Config struct { + ClientID string `json:"client_id"` + MusicFolders []string `json:"music_folders"` +} + +func getConfigPath() string { + home, err := os.UserHomeDir() + if err != nil { + return "" + } + return filepath.Join(home, ".config", "mpv-discord-rpc", "config.json") +} + +func loadConfig() (*Config, error) { + configPath := getConfigPath() + + if _, err := os.Stat(configPath); os.IsNotExist(err) { + return nil, fmt.Errorf("config file not found at %s\nExample config was created, fill in the values\n", configPath) + } + + file, err := os.Open(configPath) + if err != nil { + return nil, fmt.Errorf("failed to open config file: %w", err) + } + defer file.Close() + + var config Config + decoder := json.NewDecoder(file) + if err := decoder.Decode(&config); err != nil { + return nil, fmt.Errorf("failed to parse config file: %w", err) + } + + if config.ClientID == "" { + return nil, fmt.Errorf("client_id is empty in config file") + } + + return &config, nil +} + +func createExampleConfig() error { + configPath := getConfigPath() + configDir := filepath.Dir(configPath) + + // Create directory if it doesn't exist + if err := os.MkdirAll(configDir, 0755); err != nil { + return fmt.Errorf("failed to create config directory: %w", err) + } + + // Create example config + exampleConfig := Config{ + ClientID: "your_discord_application_id_here", + MusicFolders: []string{ + "folder1", + "folder2", + }, + } + + file, err := os.Create(configPath) + if err != nil { + return fmt.Errorf("failed to create config file: %w", err) + } + defer file.Close() + + encoder := json.NewEncoder(file) + encoder.SetIndent("", " ") + if err := encoder.Encode(exampleConfig); err != nil { + return fmt.Errorf("failed to write config file: %w", err) + } + + return nil +} + +func GetConfig() (*Config, error) { + config, err := loadConfig() + if err != nil { + fmt.Printf("Error: %v\n", err) + fmt.Printf("\nConfig file location: %s\n", getConfigPath()) + createExampleConfig() + os.Exit(1) + } + fmt.Println("✓ Config loaded") + fmt.Printf("Client ID: %s\n", config.ClientID) + + return config, err +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..702ea4e --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module mpv-discord-rpc + +go 1.25.5 + +require ( + github.com/hugolgst/rich-go v0.0.0-20240715122152-74618cc1ace2 // indirect + gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3944035 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/hugolgst/rich-go v0.0.0-20240715122152-74618cc1ace2 h1:9qOViOQGFIP5ar+2NorfAIsfuADEKXtklySC0zNnYf4= +github.com/hugolgst/rich-go v0.0.0-20240715122152-74618cc1ace2/go.mod h1:nGaW7CGfNZnhtiFxMpc4OZdqIexGXjUlBnlmpZmjEKA= +gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU= +gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= diff --git a/main.go b/main.go new file mode 100644 index 0000000..1456470 --- /dev/null +++ b/main.go @@ -0,0 +1,138 @@ +package main + +import ( + "encoding/json" + "fmt" + "math/rand/v2" + "mpv-discord-rpc/config" + "net" + "os" + "path/filepath" + "strings" + "time" + + "github.com/hugolgst/rich-go/client" +) + +// const CLIENT_ID = "1452184186969657455" +const SOCKET_PATH = "/tmp/mpvsocket" + +type MPVResponse struct { + Data any `json:"data"` + Error string `json:"error"` +} + +func mpvCommand(command []any) (string, error) { + conn, err := net.DialTimeout("unix", SOCKET_PATH, 1*time.Second) + if err != nil { + return "", err + } + defer conn.Close() + + cmd := map[string]any{"command": command} + data, _ := json.Marshal(cmd) + data = append(data, '\n') + + conn.Write(data) + + buf := make([]byte, 4096) + n, err := conn.Read(buf) + if err != nil { + return "", err + } + + var resp MPVResponse + json.Unmarshal(buf[:n], &resp) + + if resp.Data != nil { + return fmt.Sprintf("%v", resp.Data), nil + } + return "", fmt.Errorf("no data") +} + +func cleanFilename(filename string) string { + exts := []string{".mp3", ".flac", ".m4a", ".opus", ".ogg", ".wav"} + for _, ext := range exts { + filename = strings.ReplaceAll(filename, ext, "") + } + return filename +} + +func randomStatePrefix() string { + prefixes := []string{"Listening", "Vibing", "Jamming"} + return prefixes[rand.IntN(len(prefixes))] +} + +func isInMusicFolder(fp string, musicFolders []string) bool { + for _, folder := range musicFolders { + if strings.HasPrefix(folder, "~") { + home, _ := os.UserHomeDir() + folder = strings.Replace(folder, "~", home, 1) + } + + if !filepath.IsAbs(folder) { + home, _ := os.UserHomeDir() + folder = filepath.Join(home, folder) + } + + folder = filepath.Clean(folder) + filePath := filepath.Clean(fp) + + if strings.HasPrefix(filePath, folder) { + return true + } + } + return false +} + +func main() { + var connected bool + var lastSong string + config, _:= config.GetConfig() + for { + if _, err := os.Stat(SOCKET_PATH); err == nil { + if !connected { + err := client.Login(config.ClientID) + if err == nil { + connected = true + fmt.Println("Connected to Discord") + } else { + fmt.Println("Failed to connect to Discord, reconnecting 5s...") + time.Sleep(5 * time.Second) + continue + } + } + + filename, err := mpvCommand([]any{"get_property", "filename"}) + fp, err := mpvCommand([]any{"get_property", "path"}) + if err == nil && fp != "" && filename != "" && isInMusicFolder(fp, config.MusicFolders) { + song := cleanFilename(filename) + if song != lastSong { + lastSong = song + err = client.SetActivity(client.Activity{ + State: randomStatePrefix() + " to music", + Details: song, + LargeImage: "music", + LargeText: "MPV", + }) + if err != nil { + fmt.Printf("✗ Failed to set activity: %v\n", err) + } else { + fmt.Printf("✓ Activity updated: %s\n", song) + } + } + } else { + fmt.Printf("✗ Connection broken, restart mpv to reconnect: %v\n", err) + } + + time.Sleep(5 * time.Second) + } else { + if connected { + client.Logout() + connected = false + fmt.Println("Disconnected from Discord") + } + time.Sleep(2 * time.Second) + } + } +} diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..edaa40a --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,3 @@ +#!/bin/bash +cd .. +go build -o mpv-discord-rpc main.go diff --git a/scripts/playlist.sh b/scripts/playlist.sh new file mode 100755 index 0000000..e61d695 --- /dev/null +++ b/scripts/playlist.sh @@ -0,0 +1,11 @@ +#!/bin/bash +if ! command -v mpv &> /dev/null; then + echo "mpv is not installed" + exit 1 +fi + +echo "space = play/pause" +echo "<,> = back,next" +echo "q = quit" +echo "left/right = skip seconds" +mpv --input-ipc-server=/tmp/mpvsocket --shuffle --loop-playlist=inf "$@"