pax_global_header00006660000000000000000000000064146257711200014517gustar00rootroot0000000000000052 comment=1430556db33b1fcd718cf403185f997bd9b77ae0 golang-github-delthas-go-libnp-0.0~git20221222.0e45ece/000077500000000000000000000000001462577112000221045ustar00rootroot00000000000000golang-github-delthas-go-libnp-0.0~git20221222.0e45ece/.gitmodules000066400000000000000000000001161462577112000242570ustar00rootroot00000000000000[submodule "libnp"] path = libnp url = https://github.com/delthas/libnp.git golang-github-delthas-go-libnp-0.0~git20221222.0e45ece/LICENSE000066400000000000000000000021071462577112000231110ustar00rootroot00000000000000MIT License Copyright (c) 2022 delthas Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. golang-github-delthas-go-libnp-0.0~git20221222.0e45ece/README.md000066400000000000000000000030151462577112000233620ustar00rootroot00000000000000# go-libnp [![GoDoc](https://godocs.io/github.com/delthas/go-libnp?status.svg)](https://godocs.io/github.com/delthas/go-libnp) [![stability-experimental](https://img.shields.io/badge/stability-experimental-orange.svg)](https://github.com/emersion/stability-badges#experimental) A tiny cross-platform library for extracting information about the music/image/video that is **N**ow **P**laying on the system. Supported OS: - Windows (using CGo: GlobalSystemMediaTransportControlsSessionManager) - Linux (using plain Go: DBus/MPRIS) This is essentially Go bindings for [libnp](https://github.com/delthas/libnp), but with the Linux implementation rewritten in plain Go. ## Usage The API is well-documented in its [![GoDoc](https://godocs.io/github.com/delthas/go-libnp?status.svg)](https://godocs.io/github.com/delthas/go-libnp) ``` info, err := libnp.GetInfo(context.Background()) fmt.Println(info.Album) ``` ## Building On Windows, building requires a MinGW toolchain, and the Windows SDK. Make sure that GCC is in your path, and set `CPATH` to include the Windows SDK include folder: ```cmd set "CPATH=C:\Program Files (x86)\Windows Kits\10\Include\10.0.22621\winrt" ``` Replace the version above with your SDK version. Otherwise, if you just want to build the library with the stub backend (always returning an empty song), just compile with `CGO_ENABLED=0`. ## Status Used in small-scale production environments. The API could be slightly changed in backwards-incompatible ways for now. - [ ] Add darwin - [ ] Add web ## License MIT golang-github-delthas-go-libnp-0.0~git20221222.0e45ece/cmd/000077500000000000000000000000001462577112000226475ustar00rootroot00000000000000golang-github-delthas-go-libnp-0.0~git20221222.0e45ece/cmd/np/000077500000000000000000000000001462577112000232645ustar00rootroot00000000000000golang-github-delthas-go-libnp-0.0~git20221222.0e45ece/cmd/np/main.go000066400000000000000000000034011462577112000245350ustar00rootroot00000000000000package main import ( "context" "fmt" "github.com/delthas/go-libnp" "log" "time" ) func main() { info, err := libnp.GetInfo(context.Background()) if err != nil { log.Fatal(err) } for _, v := range info.AlbumArtists { fmt.Printf("Album Artist: %s\n", v) } for _, v := range info.Artists { fmt.Printf("Artist: %s\n", v) } for _, v := range info.Composers { fmt.Printf("Composer: %s\n", v) } for _, v := range info.Genres { fmt.Printf("Genre: %s\n", v) } for _, v := range info.Lyricists { fmt.Printf("Lyricist: %s\n", v) } if !info.Created.IsZero() { fmt.Printf("Created: %s\n", info.Created.Format(time.RFC3339)) } if !info.FirstPlayed.IsZero() { fmt.Printf("First Played: %s\n", info.FirstPlayed.Format(time.RFC3339)) } if !info.LastPlayed.IsZero() { fmt.Printf("Last Played: %s\n", info.LastPlayed.Format(time.RFC3339)) } if info.Album != "" { fmt.Printf("Album Title: %s\n", info.Album) } if info.AlbumTrackCount != 0 { fmt.Printf("Album Track Count: %d\n", info.AlbumTrackCount) } if info.ArtURL != "" { fmt.Printf("Art URL: %s\n", info.ArtURL) } if info.BPM != 0 { fmt.Printf("BPM: %d\n", info.BPM) } if info.DiscNumber != 0 { fmt.Printf("Disc Number: %d\n", info.DiscNumber) } if info.Length != 0 { fmt.Printf("Length: %d\n", info.Length/time.Second) } if info.PlayCount != 0 { fmt.Printf("Play Count: %d\n", info.PlayCount) } if info.Subtitle != "" { fmt.Printf("Subtitle: %s\n", info.Subtitle) } if info.Title != "" { fmt.Printf("Title: %s\n", info.Title) } if info.TrackNumber != 0 { fmt.Printf("Track Number: %d\n", info.TrackNumber) } if info.URL != "" { fmt.Printf("URL: %s\n", info.URL) } if info.PlaybackType != libnp.PlaybackTypeUnknown { fmt.Printf("Type: %v\n", info.PlaybackType) } } golang-github-delthas-go-libnp-0.0~git20221222.0e45ece/go.mod000066400000000000000000000001261462577112000232110ustar00rootroot00000000000000module github.com/delthas/go-libnp go 1.18 require github.com/godbus/dbus/v5 v5.1.0 golang-github-delthas-go-libnp-0.0~git20221222.0e45ece/go.sum000066400000000000000000000002511462577112000232350ustar00rootroot00000000000000github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= golang-github-delthas-go-libnp-0.0~git20221222.0e45ece/libnp.go000066400000000000000000000034431462577112000235430ustar00rootroot00000000000000// Package libnp offers information about the media that is currently being played. // The main entrypoint is GetInfo, which returns an Info. package libnp import ( "context" "time" ) // PlaybackType represents the kind of media being played. // PlaybackTypeUnknown is used on some platforms. type PlaybackType int const ( PlaybackTypeUnknown PlaybackType = iota PlaybackTypeMusic PlaybackTypeVideo PlaybackTypeImage ) func (p PlaybackType) String() string { switch p { case PlaybackTypeMusic: return "Music" case PlaybackTypeVideo: return "Video" case PlaybackTypeImage: return "Image" default: return "Unknown" } } // Info stores information about the media being played. type Info struct { AlbumArtists []string Artists []string Composers []string // linux only Genres []string // linux only Lyricists []string // linux only Created time.Time // linux only FirstPlayed time.Time // linux only LastPlayed time.Time // linux only Album string AlbumTrackCount int // windows only ArtURL string // linux only BPM int // linux only DiscNumber int // linux only Length time.Duration // linux only PlayCount int // linux only Subtitle string // windows only Title string TrackNumber int URL string // linux only PlaybackType PlaybackType // windows only } // GetInfo returns Info about the media currently being played. // // An error is returned if the library has issues getting valid information // about the song being played. // // A nil Info with a nil error is returned if there is no media being played. func GetInfo(ctx context.Context) (*Info, error) { return getInfo(ctx) } golang-github-delthas-go-libnp-0.0~git20221222.0e45ece/libnp_linux.go000066400000000000000000000060041462577112000247560ustar00rootroot00000000000000package libnp import ( "context" "fmt" "github.com/godbus/dbus/v5" "strings" "time" ) func getInfo(ctx context.Context) (*Info, error) { bus, err := dbus.ConnectSessionBus(dbus.WithContext(ctx)) if err != nil { return nil, fmt.Errorf("opening session bus: %v", err) } defer bus.Close() var names []string if err := bus.BusObject().CallWithContext(ctx, "org.freedesktop.DBus.ListNames", 0).Store(&names); err != nil { return nil, fmt.Errorf("listing dbus names: %v", err) } var r *Info for _, name := range names { if !strings.HasPrefix(name, "org.mpris.MediaPlayer2.") { continue } var playing bool o := bus.Object(name, "/org/mpris/MediaPlayer2") var status string if err := o.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.mpris.MediaPlayer2.Player", "PlaybackStatus").Store(&status); err != nil { continue } switch status { case "Playing": playing = true case "Paused": playing = false default: continue } var metadata map[string]dbus.Variant if err := o.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.mpris.MediaPlayer2.Player", "Metadata").Store(&metadata); err != nil { continue } var info Info if e, ok := metadata["mpris:length"].Value().(int64); ok { info.Length = time.Duration(e) * time.Microsecond } if e, ok := metadata["mpris:artUrl"].Value().(string); ok { info.ArtURL = e } if e, ok := metadata["xesam:album"].Value().(string); ok { info.Album = e } if e, ok := metadata["xesam:albumArtist"].Value().([]string); ok && len(e) > 0 { info.AlbumArtists = e } if e, ok := metadata["xesam:artist"].Value().([]string); ok && len(e) > 0 { info.Artists = e } if e, ok := metadata["xesam:audioBPM"].Value().(int); ok { info.BPM = e } if e, ok := metadata["xesam:composer"].Value().([]string); ok && len(e) > 0 { info.Composers = e } if e, ok := metadata["xesam:contentCreated"].Value().(string); ok { if t, err := time.Parse(time.RFC3339, e); err == nil { info.Created = t } } if e, ok := metadata["xesam:discNumber"].Value().(int); ok { info.DiscNumber = e } if e, ok := metadata["xesam:firstUsed"].Value().(string); ok { if t, err := time.Parse(time.RFC3339, e); err == nil { info.FirstPlayed = t } } if e, ok := metadata["xesam:genre"].Value().([]string); ok && len(e) > 0 { info.Genres = e } if e, ok := metadata["xesam:lastUsed"].Value().(string); ok { if t, err := time.Parse(time.RFC3339, e); err == nil { info.LastPlayed = t } } if e, ok := metadata["xesam:lyricist"].Value().([]string); ok && len(e) > 0 { info.Lyricists = e } if e, ok := metadata["xesam:title"].Value().(string); ok { info.Title = e } if e, ok := metadata["xesam:trackNumber"].Value().(int); ok { info.TrackNumber = e } if e, ok := metadata["xesam:url"].Value().(string); ok { info.URL = e } if e, ok := metadata["xesam:useCount"].Value().(int); ok { info.PlayCount = e } r = &info if playing { return r, nil } } return r, nil } golang-github-delthas-go-libnp-0.0~git20221222.0e45ece/libnp_stub.go000066400000000000000000000002171462577112000245740ustar00rootroot00000000000000//go:build !linux && !(windows && cgo) package libnp import "context" func getInfo(ctx context.Context) (*Info, error) { return nil, nil } golang-github-delthas-go-libnp-0.0~git20221222.0e45ece/libnp_windows.go000066400000000000000000000040641462577112000253150ustar00rootroot00000000000000//go:build cgo package libnp // #cgo CFLAGS: -Ilibnp/include // #cgo LDFLAGS: -lruntimeobject // #include // #include // #include import "C" import ( "context" "runtime" "time" "unsafe" ) // dummy object to represent the global np state to destroy var global []interface{} func init() { C.np_init() global = []interface{}{} runtime.SetFinalizer(&global, func(_ interface{}) { C.np_destroy() }) } func parseArray(p **C.char, size C.size_t) []string { if p == nil || int(size) == 0 { return nil } buf := (*[1 << 30]*C.char)(unsafe.Pointer(p)) // see: https://groups.google.com/g/golang-nuts/c/sV_f0VkjZTA b := make([]string, int(size)) for i := range b { b[i] = C.GoString(buf[i]) } return b } func parseTime(p *C.char) time.Time { if p == nil { return time.Time{} } t, err := time.Parse(time.RFC3339, C.GoString(p)) if err != nil { return time.Time{} } return t } func getInfo(ctx context.Context) (*Info, error) { ci := C.np_info_get() if ci == nil { return nil, nil } info := &Info{ AlbumArtists: parseArray(ci.album_artists, ci.album_artists_size), Artists: parseArray(ci.artists, ci.artists_size), Composers: parseArray(ci.composers, ci.composers_size), Genres: parseArray(ci.genres, ci.genres_size), Lyricists: parseArray(ci.lyricists, ci.lyricists_size), Created: parseTime(ci.created), FirstPlayed: parseTime(ci.first_played), LastPlayed: parseTime(ci.last_played), Album: C.GoString(ci.album), AlbumTrackCount: int(ci.album_track_count), ArtURL: C.GoString(ci.art_url), BPM: int(ci.bpm), DiscNumber: int(ci.disc_number), Length: time.Duration(ci.length) * time.Microsecond, PlayCount: int(ci.play_count), Subtitle: C.GoString(ci.subtitle), Title: C.GoString(ci.title), TrackNumber: int(ci.track_number), URL: C.GoString(ci.url), PlaybackType: PlaybackType(int(ci.playback_type)), } C.np_info_destroy(ci) return info, nil }