├── .github └── workflows │ ├── codecov.yml │ └── go.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── codecov.yml ├── dbus-bus-object_moq_test.go ├── dbus-call_moq_test.go ├── dbus-conn_moq_test.go ├── dbus-wrapper.go ├── doc.go ├── examples ├── cli.go └── doc.go ├── go.mod ├── go.sum ├── player.go ├── player_test.go ├── types.go └── types_test.go /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | name: codecov 2 | on: [ push ] 3 | 4 | jobs: 5 | codecov: 6 | name: codecov 7 | runs-on: ubuntu-latest 8 | steps: 9 | 10 | - name: Set up Go 1.21 11 | uses: actions/setup-go@v1 12 | with: 13 | go-version: 1.21 14 | id: go 15 | 16 | - name: Check out code into the Go module directory 17 | uses: actions/checkout@v1 18 | 19 | - name: Get dependencies 20 | run: | 21 | go get -v -t -d ./... 22 | if [ -f Gopkg.toml ]; then 23 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 24 | dep ensure 25 | fi 26 | 27 | - name: Generate coverage report 28 | run: | 29 | go test `go list ./... | grep -v examples` -coverprofile=coverage.txt -covermode=atomic 30 | 31 | - name: Upload coverage report 32 | uses: codecov/codecov-action@v3 33 | with: 34 | token: ${{ secrets.CODECOV_TOKEN }} 35 | file: ./coverage.txt 36 | flags: unittests 37 | name: codecov-umbrella -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: go 2 | on: [ push ] 3 | 4 | jobs: 5 | build: 6 | name: Build 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Set up Go 1.21 10 | uses: actions/setup-go@v1 11 | with: 12 | go-version: 1.21 13 | id: go 14 | 15 | - name: Check out code into the Go module directory 16 | uses: actions/checkout@v2 17 | 18 | - name: Get dependencies 19 | run: | 20 | go get -v -t -d ./... 21 | if [ -f Gopkg.toml ]; then 22 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 23 | dep ensure 24 | fi 25 | 26 | - name: Test 27 | run: go test -cover -count=1 ./... 28 | 29 | - name: Vet 30 | run: go vet ./... 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v0.2.2 4 | 5 | - add go mod retract directive to fix accidental released v1.1.0 fail 6 | 7 | ## v0.2.1 8 | 9 | - make goreportcard 100% happy 10 | 11 | ## v0.2.0 12 | 13 | - fix dbusConnWrapper close handling 14 | - update go deps 15 | 16 | ## v0.1.0 17 | 18 | - add mpris MediaPlayer2.Player signals (all) 19 | - add ability to close dbus connection 20 | 21 | ## v0.0.1 22 | 23 | - add mpris MediaPlayer2.Player methods (all) 24 | - add mpris MediaPlayer2.Player properties (all) 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Max 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-mpris 2 | 3 | [![Go](https://github.com/leberKleber/go-mpris/workflows/go/badge.svg)](https://github.com/leberKleber/go-mpris/actions?query=workflow%3Ago) 4 | [![GoDoc](https://godoc.org/github.com/leberKleber/go-mpris?status.png)](https://godoc.org/github.com/leberKleber/go-mpris) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/leberKleber/go-mpris)](https://goreportcard.com/report/github.com/leberKleber/go-mpris) 6 | [![codecov](https://codecov.io/gh/leberKleber/go-mpris/branch/main/graph/badge.svg)](https://codecov.io/gh/leberKleber/go-mpris) 7 | 8 | go-mpris is an implementation of the mpris dbus interface written in go (golang). 9 | Implemented and tested against version 2.2. See: https://specifications.freedesktop.org/mpris-spec/2.2. 10 | 11 | * [Example](#example) 12 | * [Features](#features) 13 | * [Player](#player) 14 | * [Methods](#methods) 15 | * [Properties](#properties) 16 | * [Signals](#signals) 17 | * [TrackList](#tracklist) 18 | * [Methods](#methods-1) 19 | * [Properties](#properties-1) 20 | * [Signals](#signals-1) 21 | * [Development](#development) 22 | * [Versioning](#versioning) 23 | * [Commits](#commits) 24 | * [Mocks](#mocks) 25 | * [Go Docs](#go-docs) 26 | 27 | ## Example 28 | 29 | Example cli has been implemented. 30 | 31 | ```shell 32 | git clone git@github.com:leberKleber/go-mpris.git 33 | 34 | go build examples/cli.go 35 | 36 | ./cli-client 37 | ``` 38 | 39 | ## Features 40 | 41 | ### Player 42 | 43 | https://specifications.freedesktop.org/mpris-spec/2.2/Player_Interface.html 44 | 45 | #### Methods 46 | 47 | | method | library path | implemented | 48 | |-------------|-------------------------------------------------------------------------|--------------------| 49 | | Next | `mpris.Player.Next()` | :heavy_check_mark: | 50 | | Previous | `mpris.Player.Previous()` | :heavy_check_mark: | 51 | | Pause | `mpris.Player.Pause()` | :heavy_check_mark: | 52 | | PlayPause | `mpris.Player.PlayPause()` | :heavy_check_mark: | 53 | | Stop | `mpris.Player.Stop()` | :heavy_check_mark: | 54 | | Seek | `mpris.Player.SeekTo( int64)`¹ | :heavy_check_mark: | 55 | | SetPosition | `mpris.Player.SetPosition( dbus.ObjectPath, int64)` | :heavy_check_mark: | 56 | | OpenUri | `mpris.Player.OpenUri( string)` | :heavy_check_mark: | 57 | 58 | ¹ Could not be named Seek, it's a reserved function name. 59 | 60 | #### Properties 61 | 62 | | property | library path | implemented | 63 | |----------------|-------------------------------------------------------------------------|--------------------| 64 | | PlaybackStatus | `mpris.Player.PlaybackStatus() (mpris.PlaybackStatus, error)` | :heavy_check_mark: | 65 | | LoopStatus | `mpris.Player.LoopStatus() (mpris.LoopStatus, error)` | :heavy_check_mark: | 66 | | LoopStatus | `mpris.Player.SetLoopStatus( mpris.LoopStatus) error` | :heavy_check_mark: | 67 | | Rate | `mpris.Player.Rate() (float64, error)` | :heavy_check_mark: | 68 | | Rate | `mpris.Player.SetRate( float64) error` | :heavy_check_mark: | 69 | | Shuffle | `mpris.Player.Shuffle() (bool, error)` | :heavy_check_mark: | 70 | | Shuffle | `mpris.Player.SetShuffle( bool) error` | :heavy_check_mark: | 71 | | Metadata | `mpris.Player.Metadata() (mpris.Metadata, error)` | :heavy_check_mark: | 72 | | Volume | `mpris.Player.Volume() (float64, error)` | :heavy_check_mark: | 73 | | Volume | `mpris.Player.SetVolume( float64) (error)` | :heavy_check_mark: | 74 | | Position | `mpris.Player.Position() (int64, error)` | :heavy_check_mark: | 75 | | Position | `mpris.Player.SetPosition( dbus.ObjectPath, int64)` | :heavy_check_mark: | 76 | | MinimumRate | `mpris.Player.MinimumRate() (float64, error)` | :heavy_check_mark: | 77 | | MaximumRate | `mpris.Player.MaximumRate() (float64, error)` | :heavy_check_mark: | 78 | | CanGoNext | `mpris.Player.CanGoNext() (bool, error)` | :heavy_check_mark: | 79 | | CanGoPrevious | `mpris.Player.CanGoPrevious() (bool, error)` | :heavy_check_mark: | 80 | | CanPlay | `mpris.Player.CanPlay() (bool, error)` | :heavy_check_mark: | 81 | | CanPause | `mpris.Player.CanPause() (bool, error)` | :heavy_check_mark: | 82 | | CanSeek | `mpris.Player.CanSeek() (bool, error)` | :heavy_check_mark: | 83 | | CanControl | `mpris.Player.CanControl() (bool, error)` | :heavy_check_mark: | 84 | 85 | #### Signals 86 | 87 | | signal | library path | implemented | 88 | |--------|-------------------------------------------------------------------|--------------------| 89 | | Seeked | `mpris.Player.Seeked( context.Context) (<-chan int, error) ` | :heavy_check_mark: | 90 | 91 | ### TrackList 92 | 93 | https://specifications.freedesktop.org/mpris-spec/2.2/Track_List_Interface.html 94 | 95 | #### Methods 96 | 97 | | method | library path | implemented | 98 | |-------------------|---------------------|--------------------------| 99 | | GetTracksMetadata | Not implemented yet | :heavy_multiplication_x: | 100 | | AddTrack | Not implemented yet | :heavy_multiplication_x: | 101 | | RemoveTrack | Not implemented yet | :heavy_multiplication_x: | 102 | | GoTo | Not implemented yet | :heavy_multiplication_x: | 103 | 104 | #### Properties 105 | 106 | | property | library path | implemented | 107 | |----------------|---------------------|--------------------------| 108 | | Tracks | Not implemented yet | :heavy_multiplication_x: | 109 | | CanEditTracks | Not implemented yet | :heavy_multiplication_x: | 110 | 111 | 112 | #### Signals 113 | 114 | | signal | library path | implemented | 115 | |----------------------|---------------------|--------------------------| 116 | | TrackListReplaced | Not implemented yet | :heavy_multiplication_x: | 117 | | TrackAdded | Not implemented yet | :heavy_multiplication_x: | 118 | | TrackRemoved | Not implemented yet | :heavy_multiplication_x: | 119 | | TrackMetadataChanged | Not implemented yet | :heavy_multiplication_x: | 120 | 121 | ## Development 122 | 123 | ### Versioning 124 | 125 | This library follows the semantic versioning concept. 126 | 127 | ### Commits 128 | 129 | Commits should follow the conventional commit rules. 130 | See: https://conventionalcommits.org. 131 | 132 | ### Mocks 133 | 134 | Mocks will be generated with `github.com/matryer/moq`. It can be installed with 135 | `go install github.com/matryer/moq@latest`. Generation can be triggered with `go generate ./...`. 136 | 137 | ### Go Docs 138 | 139 | Read the docs at https://pkg.go.dev/github.com/leberKleber/go-mpris -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: 80..90 # coverage lower than 50 is red, higher than 90 green, between color code 3 | ignore: 4 | - "examples/" 5 | 6 | status: 7 | project: # settings affecting project coverage 8 | enabled: yes 9 | target: auto # auto % coverage target 10 | threshold: 5% # allow for 5% reduction of coverage without failing 11 | 12 | # do not run coverage on patch nor changes 13 | patch: no 14 | changes: no -------------------------------------------------------------------------------- /dbus-bus-object_moq_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by moq; DO NOT EDIT. 2 | // github.com/matryer/moq 3 | 4 | package mpris 5 | 6 | import ( 7 | "github.com/godbus/dbus/v5" 8 | "sync" 9 | ) 10 | 11 | // Ensure, that dbusBusObjectMock does implement dbusBusObject. 12 | // If this is not the case, regenerate this file with moq. 13 | var _ dbusBusObject = &dbusBusObjectMock{} 14 | 15 | // dbusBusObjectMock is a mock implementation of dbusBusObject. 16 | // 17 | // func TestSomethingThatUsesdbusBusObject(t *testing.T) { 18 | // 19 | // // make and configure a mocked dbusBusObject 20 | // mockeddbusBusObject := &dbusBusObjectMock{ 21 | // CallFunc: func(method string, flags dbus.Flags, args ...interface{}) dbusCall { 22 | // panic("mock out the Call method") 23 | // }, 24 | // GetPropertyFunc: func(p string) (dbus.Variant, error) { 25 | // panic("mock out the GetProperty method") 26 | // }, 27 | // SetPropertyFunc: func(p string, v interface{}) error { 28 | // panic("mock out the SetProperty method") 29 | // }, 30 | // } 31 | // 32 | // // use mockeddbusBusObject in code that requires dbusBusObject 33 | // // and then make assertions. 34 | // 35 | // } 36 | type dbusBusObjectMock struct { 37 | // CallFunc mocks the Call method. 38 | CallFunc func(method string, flags dbus.Flags, args ...interface{}) dbusCall 39 | 40 | // GetPropertyFunc mocks the GetProperty method. 41 | GetPropertyFunc func(p string) (dbus.Variant, error) 42 | 43 | // SetPropertyFunc mocks the SetProperty method. 44 | SetPropertyFunc func(p string, v interface{}) error 45 | 46 | // calls tracks calls to the methods. 47 | calls struct { 48 | // Call holds details about calls to the Call method. 49 | Call []struct { 50 | // Method is the method argument value. 51 | Method string 52 | // Flags is the flags argument value. 53 | Flags dbus.Flags 54 | // Args is the args argument value. 55 | Args []interface{} 56 | } 57 | // GetProperty holds details about calls to the GetProperty method. 58 | GetProperty []struct { 59 | // P is the p argument value. 60 | P string 61 | } 62 | // SetProperty holds details about calls to the SetProperty method. 63 | SetProperty []struct { 64 | // P is the p argument value. 65 | P string 66 | // V is the v argument value. 67 | V interface{} 68 | } 69 | } 70 | lockCall sync.RWMutex 71 | lockGetProperty sync.RWMutex 72 | lockSetProperty sync.RWMutex 73 | } 74 | 75 | // Call calls CallFunc. 76 | func (mock *dbusBusObjectMock) Call(method string, flags dbus.Flags, args ...interface{}) dbusCall { 77 | if mock.CallFunc == nil { 78 | panic("dbusBusObjectMock.CallFunc: method is nil but dbusBusObject.Call was just called") 79 | } 80 | callInfo := struct { 81 | Method string 82 | Flags dbus.Flags 83 | Args []interface{} 84 | }{ 85 | Method: method, 86 | Flags: flags, 87 | Args: args, 88 | } 89 | mock.lockCall.Lock() 90 | mock.calls.Call = append(mock.calls.Call, callInfo) 91 | mock.lockCall.Unlock() 92 | return mock.CallFunc(method, flags, args...) 93 | } 94 | 95 | // CallCalls gets all the calls that were made to Call. 96 | // Check the length with: 97 | // 98 | // len(mockeddbusBusObject.CallCalls()) 99 | func (mock *dbusBusObjectMock) CallCalls() []struct { 100 | Method string 101 | Flags dbus.Flags 102 | Args []interface{} 103 | } { 104 | var calls []struct { 105 | Method string 106 | Flags dbus.Flags 107 | Args []interface{} 108 | } 109 | mock.lockCall.RLock() 110 | calls = mock.calls.Call 111 | mock.lockCall.RUnlock() 112 | return calls 113 | } 114 | 115 | // GetProperty calls GetPropertyFunc. 116 | func (mock *dbusBusObjectMock) GetProperty(p string) (dbus.Variant, error) { 117 | if mock.GetPropertyFunc == nil { 118 | panic("dbusBusObjectMock.GetPropertyFunc: method is nil but dbusBusObject.GetProperty was just called") 119 | } 120 | callInfo := struct { 121 | P string 122 | }{ 123 | P: p, 124 | } 125 | mock.lockGetProperty.Lock() 126 | mock.calls.GetProperty = append(mock.calls.GetProperty, callInfo) 127 | mock.lockGetProperty.Unlock() 128 | return mock.GetPropertyFunc(p) 129 | } 130 | 131 | // GetPropertyCalls gets all the calls that were made to GetProperty. 132 | // Check the length with: 133 | // 134 | // len(mockeddbusBusObject.GetPropertyCalls()) 135 | func (mock *dbusBusObjectMock) GetPropertyCalls() []struct { 136 | P string 137 | } { 138 | var calls []struct { 139 | P string 140 | } 141 | mock.lockGetProperty.RLock() 142 | calls = mock.calls.GetProperty 143 | mock.lockGetProperty.RUnlock() 144 | return calls 145 | } 146 | 147 | // SetProperty calls SetPropertyFunc. 148 | func (mock *dbusBusObjectMock) SetProperty(p string, v interface{}) error { 149 | if mock.SetPropertyFunc == nil { 150 | panic("dbusBusObjectMock.SetPropertyFunc: method is nil but dbusBusObject.SetProperty was just called") 151 | } 152 | callInfo := struct { 153 | P string 154 | V interface{} 155 | }{ 156 | P: p, 157 | V: v, 158 | } 159 | mock.lockSetProperty.Lock() 160 | mock.calls.SetProperty = append(mock.calls.SetProperty, callInfo) 161 | mock.lockSetProperty.Unlock() 162 | return mock.SetPropertyFunc(p, v) 163 | } 164 | 165 | // SetPropertyCalls gets all the calls that were made to SetProperty. 166 | // Check the length with: 167 | // 168 | // len(mockeddbusBusObject.SetPropertyCalls()) 169 | func (mock *dbusBusObjectMock) SetPropertyCalls() []struct { 170 | P string 171 | V interface{} 172 | } { 173 | var calls []struct { 174 | P string 175 | V interface{} 176 | } 177 | mock.lockSetProperty.RLock() 178 | calls = mock.calls.SetProperty 179 | mock.lockSetProperty.RUnlock() 180 | return calls 181 | } 182 | -------------------------------------------------------------------------------- /dbus-call_moq_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by moq; DO NOT EDIT. 2 | // github.com/matryer/moq 3 | 4 | package mpris 5 | 6 | import ( 7 | "sync" 8 | ) 9 | 10 | // Ensure, that dbusCallMock does implement dbusCall. 11 | // If this is not the case, regenerate this file with moq. 12 | var _ dbusCall = &dbusCallMock{} 13 | 14 | // dbusCallMock is a mock implementation of dbusCall. 15 | // 16 | // func TestSomethingThatUsesdbusCall(t *testing.T) { 17 | // 18 | // // make and configure a mocked dbusCall 19 | // mockeddbusCall := &dbusCallMock{ 20 | // StoreFunc: func(retvalues ...interface{}) error { 21 | // panic("mock out the Store method") 22 | // }, 23 | // } 24 | // 25 | // // use mockeddbusCall in code that requires dbusCall 26 | // // and then make assertions. 27 | // 28 | // } 29 | type dbusCallMock struct { 30 | // StoreFunc mocks the Store method. 31 | StoreFunc func(retvalues ...interface{}) error 32 | 33 | // calls tracks calls to the methods. 34 | calls struct { 35 | // Store holds details about calls to the Store method. 36 | Store []struct { 37 | // Retvalues is the retvalues argument value. 38 | Retvalues []interface{} 39 | } 40 | } 41 | lockStore sync.RWMutex 42 | } 43 | 44 | // Store calls StoreFunc. 45 | func (mock *dbusCallMock) Store(retvalues ...interface{}) error { 46 | if mock.StoreFunc == nil { 47 | panic("dbusCallMock.StoreFunc: method is nil but dbusCall.Store was just called") 48 | } 49 | callInfo := struct { 50 | Retvalues []interface{} 51 | }{ 52 | Retvalues: retvalues, 53 | } 54 | mock.lockStore.Lock() 55 | mock.calls.Store = append(mock.calls.Store, callInfo) 56 | mock.lockStore.Unlock() 57 | return mock.StoreFunc(retvalues...) 58 | } 59 | 60 | // StoreCalls gets all the calls that were made to Store. 61 | // Check the length with: 62 | // 63 | // len(mockeddbusCall.StoreCalls()) 64 | func (mock *dbusCallMock) StoreCalls() []struct { 65 | Retvalues []interface{} 66 | } { 67 | var calls []struct { 68 | Retvalues []interface{} 69 | } 70 | mock.lockStore.RLock() 71 | calls = mock.calls.Store 72 | mock.lockStore.RUnlock() 73 | return calls 74 | } 75 | -------------------------------------------------------------------------------- /dbus-conn_moq_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by moq; DO NOT EDIT. 2 | // github.com/matryer/moq 3 | 4 | package mpris 5 | 6 | import ( 7 | "github.com/godbus/dbus/v5" 8 | "sync" 9 | ) 10 | 11 | // Ensure, that dbusConnMock does implement dbusConn. 12 | // If this is not the case, regenerate this file with moq. 13 | var _ dbusConn = &dbusConnMock{} 14 | 15 | // dbusConnMock is a mock implementation of dbusConn. 16 | // 17 | // func TestSomethingThatUsesdbusConn(t *testing.T) { 18 | // 19 | // // make and configure a mocked dbusConn 20 | // mockeddbusConn := &dbusConnMock{ 21 | // AddMatchSignalFunc: func(matchOptions ...dbus.MatchOption) error { 22 | // panic("mock out the AddMatchSignal method") 23 | // }, 24 | // CloseFunc: func() error { 25 | // panic("mock out the Close method") 26 | // }, 27 | // ObjectFunc: func(s string, objectPath dbus.ObjectPath) dbusBusObject { 28 | // panic("mock out the Object method") 29 | // }, 30 | // SignalFunc: func(ch chan<- *dbus.Signal) { 31 | // panic("mock out the Signal method") 32 | // }, 33 | // } 34 | // 35 | // // use mockeddbusConn in code that requires dbusConn 36 | // // and then make assertions. 37 | // 38 | // } 39 | type dbusConnMock struct { 40 | // AddMatchSignalFunc mocks the AddMatchSignal method. 41 | AddMatchSignalFunc func(matchOptions ...dbus.MatchOption) error 42 | 43 | // CloseFunc mocks the Close method. 44 | CloseFunc func() error 45 | 46 | // ObjectFunc mocks the Object method. 47 | ObjectFunc func(s string, objectPath dbus.ObjectPath) dbusBusObject 48 | 49 | // SignalFunc mocks the Signal method. 50 | SignalFunc func(ch chan<- *dbus.Signal) 51 | 52 | // calls tracks calls to the methods. 53 | calls struct { 54 | // AddMatchSignal holds details about calls to the AddMatchSignal method. 55 | AddMatchSignal []struct { 56 | // MatchOptions is the matchOptions argument value. 57 | MatchOptions []dbus.MatchOption 58 | } 59 | // Close holds details about calls to the Close method. 60 | Close []struct { 61 | } 62 | // Object holds details about calls to the Object method. 63 | Object []struct { 64 | // S is the s argument value. 65 | S string 66 | // ObjectPath is the objectPath argument value. 67 | ObjectPath dbus.ObjectPath 68 | } 69 | // Signal holds details about calls to the Signal method. 70 | Signal []struct { 71 | // Ch is the ch argument value. 72 | Ch chan<- *dbus.Signal 73 | } 74 | } 75 | lockAddMatchSignal sync.RWMutex 76 | lockClose sync.RWMutex 77 | lockObject sync.RWMutex 78 | lockSignal sync.RWMutex 79 | } 80 | 81 | // AddMatchSignal calls AddMatchSignalFunc. 82 | func (mock *dbusConnMock) AddMatchSignal(matchOptions ...dbus.MatchOption) error { 83 | if mock.AddMatchSignalFunc == nil { 84 | panic("dbusConnMock.AddMatchSignalFunc: method is nil but dbusConn.AddMatchSignal was just called") 85 | } 86 | callInfo := struct { 87 | MatchOptions []dbus.MatchOption 88 | }{ 89 | MatchOptions: matchOptions, 90 | } 91 | mock.lockAddMatchSignal.Lock() 92 | mock.calls.AddMatchSignal = append(mock.calls.AddMatchSignal, callInfo) 93 | mock.lockAddMatchSignal.Unlock() 94 | return mock.AddMatchSignalFunc(matchOptions...) 95 | } 96 | 97 | // AddMatchSignalCalls gets all the calls that were made to AddMatchSignal. 98 | // Check the length with: 99 | // 100 | // len(mockeddbusConn.AddMatchSignalCalls()) 101 | func (mock *dbusConnMock) AddMatchSignalCalls() []struct { 102 | MatchOptions []dbus.MatchOption 103 | } { 104 | var calls []struct { 105 | MatchOptions []dbus.MatchOption 106 | } 107 | mock.lockAddMatchSignal.RLock() 108 | calls = mock.calls.AddMatchSignal 109 | mock.lockAddMatchSignal.RUnlock() 110 | return calls 111 | } 112 | 113 | // Close calls CloseFunc. 114 | func (mock *dbusConnMock) Close() error { 115 | if mock.CloseFunc == nil { 116 | panic("dbusConnMock.CloseFunc: method is nil but dbusConn.Close was just called") 117 | } 118 | callInfo := struct { 119 | }{} 120 | mock.lockClose.Lock() 121 | mock.calls.Close = append(mock.calls.Close, callInfo) 122 | mock.lockClose.Unlock() 123 | return mock.CloseFunc() 124 | } 125 | 126 | // CloseCalls gets all the calls that were made to Close. 127 | // Check the length with: 128 | // 129 | // len(mockeddbusConn.CloseCalls()) 130 | func (mock *dbusConnMock) CloseCalls() []struct { 131 | } { 132 | var calls []struct { 133 | } 134 | mock.lockClose.RLock() 135 | calls = mock.calls.Close 136 | mock.lockClose.RUnlock() 137 | return calls 138 | } 139 | 140 | // Object calls ObjectFunc. 141 | func (mock *dbusConnMock) Object(s string, objectPath dbus.ObjectPath) dbusBusObject { 142 | if mock.ObjectFunc == nil { 143 | panic("dbusConnMock.ObjectFunc: method is nil but dbusConn.Object was just called") 144 | } 145 | callInfo := struct { 146 | S string 147 | ObjectPath dbus.ObjectPath 148 | }{ 149 | S: s, 150 | ObjectPath: objectPath, 151 | } 152 | mock.lockObject.Lock() 153 | mock.calls.Object = append(mock.calls.Object, callInfo) 154 | mock.lockObject.Unlock() 155 | return mock.ObjectFunc(s, objectPath) 156 | } 157 | 158 | // ObjectCalls gets all the calls that were made to Object. 159 | // Check the length with: 160 | // 161 | // len(mockeddbusConn.ObjectCalls()) 162 | func (mock *dbusConnMock) ObjectCalls() []struct { 163 | S string 164 | ObjectPath dbus.ObjectPath 165 | } { 166 | var calls []struct { 167 | S string 168 | ObjectPath dbus.ObjectPath 169 | } 170 | mock.lockObject.RLock() 171 | calls = mock.calls.Object 172 | mock.lockObject.RUnlock() 173 | return calls 174 | } 175 | 176 | // Signal calls SignalFunc. 177 | func (mock *dbusConnMock) Signal(ch chan<- *dbus.Signal) { 178 | if mock.SignalFunc == nil { 179 | panic("dbusConnMock.SignalFunc: method is nil but dbusConn.Signal was just called") 180 | } 181 | callInfo := struct { 182 | Ch chan<- *dbus.Signal 183 | }{ 184 | Ch: ch, 185 | } 186 | mock.lockSignal.Lock() 187 | mock.calls.Signal = append(mock.calls.Signal, callInfo) 188 | mock.lockSignal.Unlock() 189 | mock.SignalFunc(ch) 190 | } 191 | 192 | // SignalCalls gets all the calls that were made to Signal. 193 | // Check the length with: 194 | // 195 | // len(mockeddbusConn.SignalCalls()) 196 | func (mock *dbusConnMock) SignalCalls() []struct { 197 | Ch chan<- *dbus.Signal 198 | } { 199 | var calls []struct { 200 | Ch chan<- *dbus.Signal 201 | } 202 | mock.lockSignal.RLock() 203 | calls = mock.calls.Signal 204 | mock.lockSignal.RUnlock() 205 | return calls 206 | } 207 | -------------------------------------------------------------------------------- /dbus-wrapper.go: -------------------------------------------------------------------------------- 1 | package mpris 2 | 3 | import "github.com/godbus/dbus/v5" 4 | 5 | type dbusConnWrapper struct { 6 | conn *dbus.Conn 7 | } 8 | 9 | func (w dbusConnWrapper) Object(dest string, path dbus.ObjectPath) dbusBusObject { 10 | return dbusBusObjectWrapper{ 11 | obj: w.conn.Object(dest, path), 12 | } 13 | } 14 | 15 | func (w dbusConnWrapper) AddMatchSignal(options ...dbus.MatchOption) error { 16 | return w.conn.AddMatchSignal(options...) 17 | } 18 | 19 | func (w dbusConnWrapper) Signal(ch chan<- *dbus.Signal) { 20 | w.conn.Signal(ch) 21 | } 22 | 23 | type dbusBusObjectWrapper struct { 24 | obj dbus.BusObject 25 | } 26 | 27 | func (w dbusBusObjectWrapper) Call(method string, flags dbus.Flags, args ...interface{}) dbusCall { 28 | return dbusCallWrapper{ 29 | call: w.obj.Call(method, flags, args), 30 | } 31 | } 32 | 33 | func (w dbusBusObjectWrapper) GetProperty(p string) (dbus.Variant, error) { 34 | return w.obj.GetProperty(p) 35 | } 36 | 37 | func (w dbusBusObjectWrapper) SetProperty(p string, v interface{}) error { 38 | return w.obj.SetProperty(p, v) 39 | } 40 | 41 | type dbusCallWrapper struct { 42 | call *dbus.Call 43 | } 44 | 45 | func (w dbusCallWrapper) Store(retvalues ...interface{}) error { 46 | return w.call.Store(retvalues) 47 | } 48 | 49 | func (w dbusConnWrapper) Close() error { 50 | return w.conn.Close() 51 | } 52 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package mpris contains all core stuff for this library. 2 | package mpris 3 | -------------------------------------------------------------------------------- /examples/cli.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/leberKleber/go-mpris" 11 | ) 12 | 13 | func main() { 14 | p, err := mpris.NewPlayer("org.mpris.MediaPlayer2.vlc") 15 | if err != nil { 16 | fmt.Printf("failed to create gompris.Player: %s\n", err) 17 | os.Exit(1) 18 | } 19 | 20 | reader := bufio.NewReader(os.Stdin) 21 | fmt.Println("mpris example client with mpv (https://mpv.io) and mpv-mpris (https://github.com/hoyon/mpv-mpris)") 22 | 23 | for { 24 | fmt.Print("> ") 25 | input, err := reader.ReadString('\n') 26 | if err != nil { 27 | fmt.Printf("failed to read input: %s\n", err) 28 | os.Exit(1) 29 | } 30 | 31 | handleInput(p, reader, strings.Trim(input, "\n ")) 32 | } 33 | } 34 | 35 | func handleSetInput(p mpris.Player, reader *bufio.Reader, input string) error { 36 | var err error 37 | switch input { 38 | case "set-position": 39 | err = handleSetPosition(p, reader) 40 | case "set loop-status": 41 | err = handleSetLoopStatus(p, reader) 42 | case "set rate": 43 | err = handleSetRate(p, reader) 44 | case "set shuffle": 45 | err = handleSetShuffle(p, reader) 46 | case "set volume": 47 | err = handleSetVolume(p, reader) 48 | case "seek": 49 | err = handleSeek(p, reader) 50 | case "open-uri": 51 | err = handleOpenURI(p, reader) 52 | } 53 | 54 | return err 55 | } 56 | 57 | func handleOpenURI(p mpris.Player, reader *bufio.Reader) error { 58 | fmt.Println("Open the given uri.") 59 | fmt.Print("-->") 60 | uri, err := reader.ReadString('\n') 61 | if err != nil { 62 | return fmt.Errorf("failed to read input: %w", err) 63 | } 64 | 65 | p.OpenURI(strings.Trim(uri, "\n ")) 66 | return nil 67 | } 68 | 69 | func handleSeek(p mpris.Player, reader *bufio.Reader) error { 70 | fmt.Println("The number of microseconds to seek forward") 71 | fmt.Print("-->") 72 | offsetAsString, err := reader.ReadString('\n') 73 | if err != nil { 74 | return fmt.Errorf("failed to read input: %w", err) 75 | } 76 | 77 | offset, err := strconv.ParseInt(strings.Trim(offsetAsString, "\n "), 10, 64) 78 | if err != nil { 79 | fmt.Println("input must be a number") 80 | return nil 81 | } 82 | 83 | p.SeekTo(offset) 84 | return nil 85 | } 86 | 87 | func handleSetVolume(p mpris.Player, reader *bufio.Reader) error { 88 | volumeAsString, err := reader.ReadString('\n') 89 | if err != nil { 90 | return fmt.Errorf("failed to read input: %w", err) 91 | } 92 | 93 | shuffle, err := strconv.ParseFloat(strings.Trim(volumeAsString, "\n "), 10) 94 | if err != nil { 95 | fmt.Println("input must be a float64") 96 | } 97 | 98 | err = p.SetVolume(shuffle) 99 | if err != nil { 100 | fmt.Printf("failed to set volume: %s\n", err) 101 | } 102 | return nil 103 | } 104 | 105 | func handleSetShuffle(p mpris.Player, reader *bufio.Reader) error { 106 | shuffleAsString, err := reader.ReadString('\n') 107 | if err != nil { 108 | return fmt.Errorf("failed to read input: %w", err) 109 | } 110 | 111 | shuffle, err := strconv.ParseBool(strings.Trim(shuffleAsString, "\n ")) 112 | if err != nil { 113 | fmt.Println("input must be a bool") 114 | } 115 | 116 | err = p.SetShuffle(shuffle) 117 | if err != nil { 118 | fmt.Printf("failed to set shuffle: %s\n", err) 119 | } 120 | return nil 121 | } 122 | 123 | func handleSetRate(p mpris.Player, reader *bufio.Reader) error { 124 | rateAsString, err := reader.ReadString('\n') 125 | if err != nil { 126 | return fmt.Errorf("failed to read input: %w", err) 127 | } 128 | 129 | rate, err := strconv.ParseFloat(strings.Trim(rateAsString, "\n "), 10) 130 | if err != nil { 131 | fmt.Println("input must be a float64") 132 | } 133 | 134 | err = p.SetRate(rate) 135 | if err != nil { 136 | fmt.Printf("failed to set rate: %s\n", err) 137 | } 138 | return nil 139 | } 140 | 141 | func handleSetLoopStatus(p mpris.Player, reader *bufio.Reader) error { 142 | status, err := reader.ReadString('\n') 143 | if err != nil { 144 | return fmt.Errorf("failed to read input: %w", err) 145 | } 146 | 147 | err = p.SetLoopStatus(mpris.LoopStatus(strings.Trim(status, "\n "))) 148 | if err != nil { 149 | fmt.Printf("failed to set loop status: %s\n", err) 150 | } 151 | return nil 152 | } 153 | 154 | func handleSetPosition(p mpris.Player, reader *bufio.Reader) error { 155 | fmt.Println("The track position in microseconds.") 156 | fmt.Print("-->") 157 | offsetAsString, err := reader.ReadString('\n') 158 | if err != nil { 159 | return fmt.Errorf("failed to read input: %w", err) 160 | } 161 | 162 | position, err := strconv.ParseInt(strings.Trim(offsetAsString, "\n "), 10, 64) 163 | if err != nil { 164 | fmt.Println("input must be a number") 165 | return nil 166 | } 167 | 168 | p.SetPosition("/not/used", position) 169 | return nil 170 | } 171 | 172 | func handleGetInput(p mpris.Player, input string) { 173 | switch input { 174 | case "playback-status": 175 | handleGetPlaybackStatus(p) 176 | case "loop-status": 177 | handleGetLoopStatus(p) 178 | case "rate": 179 | handleGetRate(p) 180 | case "shuffle": 181 | handleGetShuffle(p) 182 | case "metadata": 183 | handleGetMetadata(p) 184 | case "volume": 185 | handleGetVolume(p) 186 | case "position": 187 | handleGetPosition(p) 188 | case "minimum-rate": 189 | handleGetMinimumRate(p) 190 | case "maximum-rate": 191 | handleExtMaximumRate(p) 192 | } 193 | } 194 | 195 | func handleGetPlaybackStatus(p mpris.Player) { 196 | s, err := p.PlaybackStatus() 197 | if err != nil { 198 | fmt.Printf("failed to get playback status: %s\n", err) 199 | } 200 | fmt.Println(s) 201 | } 202 | 203 | func handleGetLoopStatus(p mpris.Player) { 204 | s, err := p.LoopStatus() 205 | if err != nil { 206 | fmt.Printf("failed to get loop status: %s\n", err) 207 | } 208 | fmt.Println(s) 209 | } 210 | 211 | func handleGetRate(p mpris.Player) { 212 | s, err := p.Rate() 213 | if err != nil { 214 | fmt.Printf("failed to get rate: %s\n", err) 215 | } 216 | fmt.Println(s) 217 | } 218 | 219 | func handleGetShuffle(p mpris.Player) { 220 | s, err := p.Shuffle() 221 | if err != nil { 222 | fmt.Printf("failed to get shuffle: %s\n", err) 223 | } 224 | fmt.Println(s) 225 | } 226 | 227 | func handleGetMetadata(p mpris.Player) { 228 | s, err := p.Metadata() 229 | if err != nil { 230 | fmt.Printf("failed to get metadata: %s\n", err) 231 | } 232 | fmt.Println(s) 233 | } 234 | 235 | func handleGetVolume(p mpris.Player) { 236 | s, err := p.Volume() 237 | if err != nil { 238 | fmt.Printf("failed to get volume: %s\n", err) 239 | } 240 | fmt.Println(s) 241 | } 242 | 243 | func handleGetPosition(p mpris.Player) { 244 | s, err := p.Position() 245 | if err != nil { 246 | fmt.Printf("failed to get position: %s\n", err) 247 | } 248 | fmt.Println(s) 249 | } 250 | 251 | func handleGetMinimumRate(p mpris.Player) { 252 | s, err := p.MinimumRate() 253 | if err != nil { 254 | fmt.Printf("failed to get minimum-rate: %s\n", err) 255 | } 256 | fmt.Println(s) 257 | } 258 | 259 | func handleExtMaximumRate(p mpris.Player) { 260 | s, err := p.MaximumRate() 261 | if err != nil { 262 | fmt.Printf("failed to get maximum-rate: %s\n", err) 263 | } 264 | fmt.Println(s) 265 | } 266 | 267 | func handleCan(p mpris.Player, input string) { 268 | switch input { 269 | case "can-go-next": 270 | s, err := p.CanGoNext() 271 | if err != nil { 272 | fmt.Printf("failed to get can-go-next: %s\n", err) 273 | } 274 | fmt.Println(s) 275 | case "can-go-previous": 276 | s, err := p.CanGoNext() 277 | if err != nil { 278 | fmt.Printf("failed to get can-go-previous: %s\n", err) 279 | } 280 | fmt.Println(s) 281 | case "can-play": 282 | s, err := p.CanPlay() 283 | if err != nil { 284 | fmt.Printf("failed to get can-play: %s\n", err) 285 | } 286 | fmt.Println(s) 287 | case "can-pause": 288 | s, err := p.CanPause() 289 | if err != nil { 290 | fmt.Printf("failed to get can-pause: %s\n", err) 291 | } 292 | fmt.Println(s) 293 | case "can-seek": 294 | s, err := p.CanSeek() 295 | if err != nil { 296 | fmt.Printf("failed to get can-seek: %s\n", err) 297 | } 298 | fmt.Println(s) 299 | case "can-control": 300 | s, err := p.CanControl() 301 | if err != nil { 302 | fmt.Printf("failed to get can-control: %s\n", err) 303 | } 304 | fmt.Println(s) 305 | } 306 | } 307 | 308 | func handleInput(p mpris.Player, reader *bufio.Reader, input string) { 309 | handleGetInput(p, input) 310 | handleCan(p, input) 311 | 312 | err := handleSetInput(p, reader, input) 313 | if err != nil { 314 | fmt.Printf("failed to handle set input: %s\n", err) 315 | } 316 | 317 | switch input { 318 | case "help": 319 | printHelp() 320 | case "quit": 321 | os.Exit(0) 322 | case "next": 323 | p.Next() 324 | case "previous": 325 | p.Next() 326 | case "pause": 327 | p.Pause() 328 | case "play-pause": 329 | p.PlayPause() 330 | case "stop": 331 | p.Stop() 332 | case "play": 333 | p.Play() 334 | } 335 | } 336 | 337 | func printHelp() { 338 | fmt.Println("Available commands") 339 | fmt.Println("- quit (exit this cli)") 340 | fmt.Println("- next") 341 | fmt.Println("- previous") 342 | fmt.Println("- pause") 343 | fmt.Println("- play-pause") 344 | fmt.Println("- stop") 345 | fmt.Println("- play") 346 | fmt.Println("- seek") 347 | fmt.Println("- set-position") 348 | fmt.Println("- open-uri") 349 | fmt.Println("- playback-status") 350 | fmt.Println("- loop-status") 351 | fmt.Println("- set-loop-status") 352 | fmt.Println("- rate") 353 | fmt.Println("- set-rate") 354 | fmt.Println("- metadata") 355 | fmt.Println("- volume") 356 | fmt.Println("- set-volume") 357 | fmt.Println("- position") 358 | fmt.Println("- minimum-rate") 359 | fmt.Println("- maximum-rate") 360 | fmt.Println("- can-go-next") 361 | fmt.Println("- can-go-previous") 362 | fmt.Println("- can-play") 363 | fmt.Println("- can-pause") 364 | fmt.Println("- can-seek") 365 | fmt.Println("- can-control") 366 | } 367 | -------------------------------------------------------------------------------- /examples/doc.go: -------------------------------------------------------------------------------- 1 | // Package main contains an example cli. 2 | package main 3 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/leberKleber/go-mpris 2 | 3 | go 1.21 4 | 5 | toolchain go1.21.5 6 | 7 | retract v1.1.0 // Published accidentally. 8 | 9 | require ( 10 | github.com/godbus/dbus/v5 v5.1.0 11 | github.com/stretchr/testify v1.8.4 12 | ) 13 | 14 | require ( 15 | github.com/davecgh/go-spew v1.1.1 // indirect 16 | github.com/pmezard/go-difflib v1.0.0 // indirect 17 | gopkg.in/yaml.v3 v3.0.1 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= 4 | github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 8 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 11 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 12 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 13 | -------------------------------------------------------------------------------- /player.go: -------------------------------------------------------------------------------- 1 | package mpris 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/godbus/dbus/v5" 8 | ) 9 | 10 | const ( 11 | playerObjectPath = "/org/mpris/MediaPlayer2" 12 | playerInterface = "org.mpris.MediaPlayer2.Player" 13 | playerNextMethod = playerInterface + ".Next" 14 | playerPreviousMethod = playerInterface + ".Previous" 15 | playerPauseMethod = playerInterface + ".Pause" 16 | playerPlayPauseMethod = playerInterface + ".PlayPause" 17 | playerStopMethod = playerInterface + ".Stop" 18 | playerPlayMethod = playerInterface + ".Play" 19 | playerSeekMethod = playerInterface + ".SeekTo" 20 | playerSetPositionMethod = playerInterface + ".SetPosition" 21 | playerOpenURIMethod = playerInterface + ".OpenUri" 22 | playerPlaybackStatusProperty = playerInterface + ".PlaybackStatus" 23 | playerLoopStatusProperty = playerInterface + ".LoopStatus" 24 | playerRateProperty = playerInterface + ".Rate" 25 | playerShuffleProperty = playerInterface + ".Shuffle" 26 | playerMetadataProperty = playerInterface + ".Metadata" 27 | playerVolumeProperty = playerInterface + ".Volume" 28 | playerPositionProperty = playerInterface + ".Position" 29 | playerMinimumRateProperty = playerInterface + ".MinimumRate" 30 | playerMaximumRateProperty = playerInterface + ".MaximumRate" 31 | playerCanGoNextProperty = playerInterface + ".CanGoNext" 32 | playerCanGoPreviousProperty = playerInterface + ".CanGoPrevious" 33 | playerCanPlayProperty = playerInterface + ".CanPlay" 34 | playerCanPauseProperty = playerInterface + ".CanPause" 35 | playerCanSeekProperty = playerInterface + ".CanSeek" 36 | playerCanControlProperty = playerInterface + ".CanControl" 37 | signalNameSeeked = "org.mpris.MediaPlayer2.Player.Seeked" 38 | ) 39 | 40 | var dbusSessionBus = dbus.SessionBus 41 | 42 | //go:generate moq -out dbus-conn_moq_test.go . dbusConn 43 | type dbusConn interface { 44 | Object(string, dbus.ObjectPath) dbusBusObject 45 | AddMatchSignal(...dbus.MatchOption) error 46 | Signal(ch chan<- *dbus.Signal) 47 | Close() error 48 | } 49 | 50 | //go:generate moq -out dbus-bus-object_moq_test.go . dbusBusObject 51 | type dbusBusObject interface { 52 | Call(method string, flags dbus.Flags, args ...interface{}) dbusCall 53 | GetProperty(p string) (v dbus.Variant, e error) 54 | SetProperty(p string, v interface{}) error 55 | } 56 | 57 | //go:generate moq -out dbus-call_moq_test.go . dbusCall 58 | type dbusCall interface { 59 | Store(retvalues ...interface{}) error 60 | } 61 | 62 | // Player is an implementation of dbus org.mpris.MediaPlayer2.Player. see: https://specifications.freedesktop.org/mpris-spec/2.2/Player_Interface.html 63 | // Use NewPlayer to create a new instance with a connected session-bus via dbus.SessionBus. 64 | // Use NewPlayerWithConnection when you want to use a self-configured dbus.Conn 65 | type Player struct { 66 | name string 67 | connection dbusConn 68 | } 69 | 70 | // NewPlayer returns a new Player which is already connected to session-bus via dbus.SessionBus. 71 | // Don't forget to Player.Close() the player after use. 72 | func NewPlayer(name string) (Player, error) { 73 | connection, err := dbusSessionBus() 74 | if err != nil { 75 | return Player{}, fmt.Errorf("failed to connect to session-bus: %w", err) 76 | } 77 | 78 | return Player{ 79 | name: name, 80 | connection: &dbusConnWrapper{ 81 | conn: connection, 82 | }, 83 | }, nil 84 | } 85 | 86 | // NewPlayerWithConnection returns a new Player with the given name and connection. 87 | // Deprecated: NewPlayerWithConnection will be removed in future (v1.X.X). 88 | // Plain Struct initialization should be used instead. 89 | // Private fields will be public. 90 | func NewPlayerWithConnection(name string, connection *dbus.Conn) Player { 91 | return Player{ 92 | name: name, 93 | connection: &dbusConnWrapper{ 94 | conn: connection, 95 | }, 96 | } 97 | } 98 | 99 | // Close closes the dbus connection. 100 | func (p Player) Close() error { 101 | err := p.connection.Close() 102 | if err != nil { 103 | return fmt.Errorf("failed to close dbus connection: %w", err) 104 | } 105 | 106 | return nil 107 | } 108 | 109 | // Next skips to the next track in the tracklist. 110 | // If there is no next track (and endless playback and track repeat are both off), stop playback. 111 | // If playback is paused or stopped, it remains that way. 112 | // If CanGoNext is false, attempting to call this method should have no effect. 113 | // see: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:Next 114 | func (p Player) Next() { 115 | p.connection.Object(p.name, playerObjectPath).Call(playerNextMethod, 0) 116 | } 117 | 118 | // Previous skips to the previous track in the tracklist. 119 | // If there is no previous track (and endless playback and track repeat are both off), stop playback. 120 | // If playback is paused or stopped, it remains that way. 121 | // If CanGoPrevious is false, attempting to call this method should have no effect. 122 | // see: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:Previous 123 | func (p Player) Previous() { 124 | p.connection.Object(p.name, playerObjectPath).Call(playerPreviousMethod, 0) 125 | } 126 | 127 | // Pause pauses playback. 128 | // If playback is already paused, this has no effect. 129 | // Calling Play after this should cause playback to start again from the same position. 130 | // If CanPause is false, attempting to call this method should have no effect. 131 | // see: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:Pause 132 | func (p Player) Pause() { 133 | p.connection.Object(p.name, playerObjectPath).Call(playerPauseMethod, 0) 134 | } 135 | 136 | // Play starts or resumes playback. 137 | // If already playing, this has no effect. 138 | // If paused, playback resumes from the current position. 139 | // If there is no track to play, this has no effect. 140 | // If CanPlay is false, attempting to call this method should have no effect. 141 | // see: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:Play 142 | func (p Player) Play() { 143 | p.connection.Object(p.name, playerObjectPath).Call(playerPlayMethod, 0) 144 | } 145 | 146 | // PlayPause pauses playback. 147 | // If playback is already paused, resumes playback. 148 | // If playback is stopped, starts playback. 149 | // If CanPause is false, attempting to call this method should have no effect and raise an error. 150 | // see: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:PlayPause 151 | func (p Player) PlayPause() { 152 | p.connection.Object(p.name, playerObjectPath).Call(playerPlayPauseMethod, 0) 153 | } 154 | 155 | // Stop stops playback. 156 | // If playback is already stopped, this has no effect. 157 | // Calling Play after this should cause playback to start again from the beginning of the track. 158 | // If CanControl is false, attempting to call this method should have no effect and raise an error. 159 | // see: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:Stop 160 | func (p Player) Stop() { 161 | p.connection.Object(p.name, playerObjectPath).Call(playerStopMethod, 0) 162 | } 163 | 164 | // SeekTo seeks forward in the current track by the specified number of microseconds. 165 | // Parameters: 166 | // - offset (The number of microseconds to seek forward.) 167 | // A negative value seeks back. If this would mean seeking back further than the start of the track, the position is set to 0. 168 | // If the value passed in would mean seeking beyond the end of the track, acts like a call to Next. 169 | // If the CanSeek property is false, this has no effect. 170 | // see: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:Seek 171 | func (p Player) SeekTo(offset int64) { 172 | p.connection.Object(p.name, playerObjectPath).Call(playerSeekMethod, 0, offset) 173 | } 174 | 175 | // OpenURI opens the Uri given as an argument 176 | // Parameters: 177 | // - uri (Uri of the track to load. Its uri scheme should be an element of the org.mpris.MediaPlayer2.SupportedUriSchemes property and the mime-type should match one of the elements of the org.mpris.MediaPlayer2.SupportedMimeTypes.) 178 | // If the playback is stopped, starts playing 179 | // If the uri scheme or the mime-type of the uri to open is not supported, this method does nothing and may raise an error. In particular, if the list of available uri schemes is empty, this method may not be implemented. 180 | // Clients should not assume that the Uri has been opened as soon as this method returns. They should wait until the mpris:trackid field in the Metadata property changes. 181 | // If the media player implements the TrackList interface, then the opened track should be made part of the tracklist, the org.mpris.MediaPlayer2.TrackList.TrackAdded or org.mpris.MediaPlayer2.TrackList.TrackListReplaced signal should be fired, as well as the org.freedesktop.DBus.Properties.PropertiesChanged signal on the tracklist interface. 182 | // see: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:OpenUri 183 | func (p Player) OpenURI(uri string) { 184 | p.connection.Object(p.name, playerObjectPath).Call(playerOpenURIMethod, 0, uri) 185 | } 186 | 187 | // PlaybackStatus returns the current playback status. 188 | // May be "Playing" as PlaybackStatusPlaying, "Paused" as PlaybackStatusPaused or "Stopped" as PlaybackStatusStopped. 189 | // https://specifications.freedesktop.org/mpris-spec/2.2/Player_Interface.html#Property:PlaybackStatus 190 | func (p Player) PlaybackStatus() (PlaybackStatus, error) { 191 | v, err := p.getProperty(playerPlaybackStatusProperty) 192 | if err != nil { 193 | return "", err 194 | } 195 | return PlaybackStatus(v.Value().(string)), nil 196 | } 197 | 198 | // LoopStatus returns the current loop / repeat status 199 | // May be: 200 | // "None" as LoopStatusNone if the playback will stop when there are no more tracks to play 201 | // "Track" as LoopStatusTrack if the current track will start again from the beginning once it has finished playing 202 | // "Playlist" as LoopStatusPlaylist if the playback loops through a list of tracks 203 | // If CanControl is false, attempting to set this property (SetLoopStatus) should have no effect and raise an error. 204 | // https://specifications.freedesktop.org/mpris-spec/2.2/Player_Interface.html#Property:LoopStatus 205 | func (p Player) LoopStatus() (LoopStatus, error) { 206 | v, err := p.getProperty(playerLoopStatusProperty) 207 | if err != nil { 208 | return "", err 209 | } 210 | return LoopStatus(v.Value().(string)), nil 211 | } 212 | 213 | // SetLoopStatus sets the current loop / repeat status 214 | // May be: 215 | // "None" as LoopStatusNone if the playback will stop when there are no more tracks to play 216 | // "Track" as LoopStatusTrack if the current track will start again from the beginning once it has finished playing 217 | // "Playlist" as LoopStatusPlaylist if the playback loops through a list of tracks 218 | // If CanControl is false, attempting to set this property (SetLoopStatus) should have no effect and raise an error. 219 | // see: https://specifications.freedesktop.org/mpris-spec/2.2/Player_Interface.html#Property:LoopStatus 220 | func (p Player) SetLoopStatus(status LoopStatus) error { 221 | return p.setProperty(playerLoopStatusProperty, string(status)) 222 | } 223 | 224 | // Rate return the current playback rate. 225 | // The value must fall in the range described by MinimumRate and MaximumRate, and must not be 0.0. If playback is paused, the PlaybackStatus property should be used to indicate this. A value of 0.0 should not be set by the client. If it is, the media player should act as though Pause was called. 226 | // If the media player has no ability to play at speeds other than the normal playback rate, this must still be implemented, and must return 1.0. The MinimumRate and MaximumRate properties must also be set to 1.0. 227 | // Not all values may be accepted by the media player. It is left to media player implementations to decide how to deal with values they cannot use; they may either ignore them or pick a "best fit" value. Clients are recommended to only use sensible fractions or multiples of 1 (eg: 0.5, 0.25, 1.5, 2.0, etc). 228 | // see: https://specifications.freedesktop.org/mpris-spec/2.2/Player_Interface.html#Property:Rate 229 | func (p Player) Rate() (float64, error) { 230 | v, err := p.getProperty(playerRateProperty) 231 | if err != nil { 232 | return 0, err 233 | } 234 | return v.Value().(float64), nil 235 | } 236 | 237 | // SetRate sets the current playback rate. 238 | // The value must fall in the range described by MinimumRate and MaximumRate, and must not be 0.0. If playback is paused, the PlaybackStatus property should be used to indicate this. A value of 0.0 should not be set by the client. If it is, the media player should act as though Pause was called. 239 | // If the media player has no ability to play at speeds other than the normal playback rate, this must still be implemented, and must return 1.0. The MinimumRate and MaximumRate properties must also be set to 1.0. 240 | // Not all values may be accepted by the media player. It is left to media player implementations to decide how to deal with values they cannot use; they may either ignore them or pick a "best fit" value. Clients are recommended to only use sensible fractions or multiples of 1 (eg: 0.5, 0.25, 1.5, 2.0, etc). 241 | // see: https://specifications.freedesktop.org/mpris-spec/2.2/Player_Interface.html#Property:Rate 242 | func (p Player) SetRate(rate float64) error { 243 | return p.setProperty(playerRateProperty, rate) 244 | } 245 | 246 | // Shuffle returns a value of false indicates that playback is progressing linearly through a playlist, while true means playback is progressing through a playlist in some other order. 247 | // If CanControl is false, attempting to set this property should have no effect and raise an error. 248 | // see: https://specifications.freedesktop.org/mpris-spec/2.2/Player_Interface.html#Property:Shuffle 249 | func (p Player) Shuffle() (bool, error) { 250 | v, err := p.getProperty(playerShuffleProperty) 251 | if err != nil { 252 | return false, err 253 | } 254 | return v.Value().(bool), nil 255 | } 256 | 257 | // SetShuffle set a value of false indicates that playback is progressing linearly through a playlist, while true means playback is progressing through a playlist in some other order. 258 | // If CanControl is false, attempting to set this property should have no effect and raise an error. 259 | // see: https://specifications.freedesktop.org/mpris-spec/2.2/Player_Interface.html#Property:Shuffle 260 | func (p Player) SetShuffle(shuffle bool) error { 261 | return p.setProperty(playerShuffleProperty, shuffle) 262 | } 263 | 264 | // Metadata of the current element. 265 | // If there is a current track, this must have a "mpris:trackid" entry (of D-Bus type "o") at the very least, which contains a D-Bus path that uniquely identifies this track. 266 | // See the type documentation for more details. 267 | // see: https://specifications.freedesktop.org/mpris-spec/2.2/Player_Interface.html#Property:Metadata 268 | func (p Player) Metadata() (Metadata, error) { 269 | v, err := p.getProperty(playerMetadataProperty) 270 | if err != nil { 271 | return nil, err 272 | } 273 | return v.Value().(map[string]dbus.Variant), nil 274 | } 275 | 276 | // Volume returns the volume level. 277 | // When setting, if a negative value is passed, the volume should be set to 0.0. 278 | // If CanControl is false, attempting to set this property should have no effect and raise an error. 279 | // see: https://specifications.freedesktop.org/mpris-spec/2.2/Player_Interface.html#Property:Volume 280 | func (p Player) Volume() (float64, error) { 281 | v, err := p.getProperty(playerVolumeProperty) 282 | if err != nil { 283 | return 0, err 284 | } 285 | return v.Value().(float64), nil 286 | } 287 | 288 | // SetVolume sets the volume level. 289 | // When setting, if a negative value is passed, the volume should be set to 0.0. 290 | // If CanControl is false, attempting to set this property should have no effect and raise an error. 291 | // see: https://specifications.freedesktop.org/mpris-spec/2.2/Player_Interface.html#Property:Volume 292 | func (p Player) SetVolume(volume float64) error { 293 | return p.setProperty(playerVolumeProperty, volume) 294 | } 295 | 296 | // Position returns current track position in microseconds, between 0 and the 'mpris:length' metadata entry (see Metadata). 297 | // Note: If the media player allows it, the current playback position can be changed either the SetPosition method or the Seek method on this interface. If this is not the case, the CanSeek property is false, and setting this property has no effect and can raise an error. 298 | // If the playback progresses in a way that is inconsistent with the Rate property, the Seeked signal is emitted. 299 | // see: https://specifications.freedesktop.org/mpris-spec/2.2/Player_Interface.html#Property:Position 300 | func (p Player) Position() (int64, error) { 301 | v, err := p.getProperty(playerPositionProperty) 302 | if err != nil { 303 | return 0, err 304 | } 305 | return v.Value().(int64), nil 306 | } 307 | 308 | // SetPosition Sets the current track position in microseconds. 309 | // Parameters: 310 | // - trackID (The currently playing track's identifier. If this does not match the id of the currently-playing track, the call is ignored as "stale".) 311 | // - position (Track position in microseconds. This must be between 0 and .) 312 | // If this does not match the id of the currently-playing track, the call is ignored as "stale". 313 | // If the Position argument is less than 0, do nothing. 314 | // If the Position argument is greater than the track length, do nothing. 315 | // If the CanSeek property is false, this has no effect. 316 | // see: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:SetPosition 317 | func (p Player) SetPosition(trackID dbus.ObjectPath, position int64) { 318 | p.connection.Object(p.name, playerObjectPath).Call(playerSetPositionMethod, 0, trackID, position) 319 | } 320 | 321 | // MinimumRate returns the minimum value which the Rate property can take. Clients should not attempt to set the Rate property below this value. 322 | // Note that even if this value is 0.0 or negative, clients should not attempt to set the Rate property to 0.0. 323 | // This value should always be 1.0 or less. 324 | // see: https://specifications.freedesktop.org/mpris-spec/2.2/Player_Interface.html#Property:MinimumRate 325 | func (p Player) MinimumRate() (float64, error) { 326 | v, err := p.getProperty(playerMinimumRateProperty) 327 | if err != nil { 328 | return 0, err 329 | } 330 | return v.Value().(float64), nil 331 | } 332 | 333 | // MaximumRate returns the maximum value which the Rate property can take. Clients should not attempt to set the Rate property above this value. 334 | // This value should always be 1.0 or greater. 335 | // see: https://specifications.freedesktop.org/mpris-spec/2.2/Player_Interface.html#Property:MaximumRate 336 | func (p Player) MaximumRate() (float64, error) { 337 | v, err := p.getProperty(playerMaximumRateProperty) 338 | if err != nil { 339 | return 0, err 340 | } 341 | return v.Value().(float64), nil 342 | } 343 | 344 | // CanGoNext returns true whether the client can call the Next method on this interface and expect the current track to change. 345 | // If it is unknown whether a call to Next will be successful (for example, when streaming tracks), this property should be set to true. 346 | // If CanControl is false, this property should also be false. 347 | // see: https://specifications.freedesktop.org/mpris-spec/2.2/Player_Interface.html#Property:CanGoNext 348 | func (p Player) CanGoNext() (bool, error) { 349 | v, err := p.getProperty(playerCanGoNextProperty) 350 | if err != nil { 351 | return false, err 352 | } 353 | return v.Value().(bool), nil 354 | } 355 | 356 | // CanGoPrevious returns true whether the client can call the Previous method on this interface and expect the current track to change. 357 | // If it is unknown whether a call to Previous will be successful (for example, when streaming tracks), this property should be set to true. 358 | // If CanControl is false, this property should also be false. 359 | // see: https://specifications.freedesktop.org/mpris-spec/2.2/Player_Interface.html#Property:CanGoPrevious 360 | func (p Player) CanGoPrevious() (bool, error) { 361 | v, err := p.getProperty(playerCanGoPreviousProperty) 362 | if err != nil { 363 | return false, err 364 | } 365 | return v.Value().(bool), nil 366 | } 367 | 368 | // CanPlay returns true whether playback can be started using Play or PlayPause. 369 | // Note that this is related to whether there is a "current track": the value should not depend on whether the track is currently paused or playing. In fact, if a track is currently playing (and CanControl is true), this should be true. 370 | // If CanControl is false, this property should also be false. 371 | // see: https://specifications.freedesktop.org/mpris-spec/2.2/Player_Interface.html#Property:CanPlay 372 | func (p Player) CanPlay() (bool, error) { 373 | v, err := p.getProperty(playerCanPlayProperty) 374 | if err != nil { 375 | return false, err 376 | } 377 | return v.Value().(bool), nil 378 | } 379 | 380 | // CanPause returns true whether playback can be paused using Pause or PlayPause. 381 | // Note that this is an intrinsic property of the current track: its value should not depend on whether the track is currently paused or playing. In fact, if playback is currently paused (and CanControl is true), this should be true. 382 | // If CanControl is false, this property should also be false. 383 | // see: https://specifications.freedesktop.org/mpris-spec/2.2/Player_Interface.html#Property:CanPause 384 | func (p Player) CanPause() (bool, error) { 385 | v, err := p.getProperty(playerCanPauseProperty) 386 | if err != nil { 387 | return false, err 388 | } 389 | return v.Value().(bool), nil 390 | } 391 | 392 | // CanSeek returns true whether the client can control the playback position using Seek and SetPosition. This may be different for different tracks. 393 | // If CanControl is false, this property should also be false. 394 | // see: https://specifications.freedesktop.org/mpris-spec/2.2/Player_Interface.html#Property:CanSeek 395 | func (p Player) CanSeek() (bool, error) { 396 | v, err := p.getProperty(playerCanSeekProperty) 397 | if err != nil { 398 | return false, err 399 | } 400 | return v.Value().(bool), nil 401 | } 402 | 403 | // CanControl is true whether the media player may be controlled over this interface. 404 | // This property is not expected to change, as it describes an intrinsic capability of the implementation. 405 | // If this is false, clients should assume that all properties on this interface are read-only (and will raise errors if writing to them is attempted), no methods are implemented and all other properties starting with "Can" are also false. 406 | // see: https://specifications.freedesktop.org/mpris-spec/2.2/Player_Interface.html#Property:CanControl 407 | func (p Player) CanControl() (bool, error) { 408 | v, err := p.getProperty(playerCanControlProperty) 409 | if err != nil { 410 | return false, err 411 | } 412 | return v.Value().(bool), nil 413 | } 414 | 415 | // Seeked indicates that the track position has changed in a way that is inconsistent with the current playing state. 416 | // When this signal is not received, clients should assume that: 417 | // - When playing, the position progresses according to the rate property. 418 | // - When paused, it remains constant. 419 | // This signal does not need to be emitted when playback starts or when the track changes, unless the track is starting 420 | // at an unexpected position. An expected position would be the last known one when going from Paused to Playing, and 0 421 | // when going from Stopped to Playing. 422 | // see: https://specifications.freedesktop.org/mpris-spec/2.2/Player_Interface.html#Signal:Seeked 423 | func (p Player) Seeked(ctx context.Context) (<-chan int, error) { 424 | err := p.connection.AddMatchSignal(dbus.WithMatchSender("org.mpris.MediaPlayer2.vlc")) 425 | if err != nil { 426 | return nil, fmt.Errorf("failed to add signal match option: %w", err) 427 | } 428 | 429 | positions := make(chan int) 430 | errs := make(chan error) 431 | go func() { 432 | defer func() { 433 | close(positions) 434 | close(errs) 435 | }() 436 | 437 | signals := make(chan *dbus.Signal) 438 | defer close(signals) 439 | p.connection.Signal(signals) 440 | 441 | collectPositions: 442 | for { 443 | select { 444 | case sig := <-signals: 445 | if sig.Name != signalNameSeeked || // irrelevant signal 446 | len(sig.Body) != 1 { // invalid event 447 | continue 448 | } 449 | micros, ok := sig.Body[0].(int64) 450 | if !ok { // broken signal 451 | continue 452 | } 453 | positions <- int(micros) 454 | case <-ctx.Done(): 455 | break collectPositions 456 | } 457 | } 458 | }() 459 | 460 | return positions, nil 461 | } 462 | 463 | func (p Player) getProperty(property string) (dbus.Variant, error) { 464 | v, err := p.connection.Object(p.name, playerObjectPath).GetProperty(property) 465 | if err != nil { 466 | return dbus.Variant{}, fmt.Errorf("failed to get property %q: %w", property, err) 467 | } 468 | 469 | return v, nil 470 | } 471 | 472 | func (p Player) setProperty(property string, value interface{}) error { 473 | err := p.connection.Object(p.name, playerObjectPath).SetProperty(property, dbus.MakeVariant(value)) 474 | if err != nil { 475 | return fmt.Errorf("failed to set property %q: %w", property, err) 476 | } 477 | 478 | return nil 479 | } 480 | -------------------------------------------------------------------------------- /player_test.go: -------------------------------------------------------------------------------- 1 | package mpris 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "testing" 8 | "time" 9 | 10 | "github.com/godbus/dbus/v5" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestPlayer_Methods(t *testing.T) { 16 | tests := []struct { 17 | name string 18 | givenName string 19 | action func(p *Player) 20 | expectedDest string 21 | expectedPath dbus.ObjectPath 22 | expectedMethod string 23 | expectedFlags dbus.Flags 24 | expectedArgs []interface{} 25 | }{ 26 | { 27 | name: "Next", 28 | givenName: "next", 29 | action: func(p *Player) { 30 | p.Next() 31 | }, 32 | expectedDest: "next", 33 | expectedPath: "/org/mpris/MediaPlayer2", 34 | expectedMethod: "org.mpris.MediaPlayer2.Player.Next", 35 | expectedFlags: 0, 36 | expectedArgs: nil, 37 | }, 38 | { 39 | name: "Previous", 40 | givenName: "previous", 41 | action: func(p *Player) { 42 | p.Previous() 43 | }, 44 | expectedDest: "previous", 45 | expectedPath: "/org/mpris/MediaPlayer2", 46 | expectedMethod: "org.mpris.MediaPlayer2.Player.Previous", 47 | expectedFlags: 0, 48 | expectedArgs: nil, 49 | }, 50 | { 51 | name: "Pause", 52 | givenName: "pause", 53 | action: func(p *Player) { 54 | p.Pause() 55 | }, 56 | expectedDest: "pause", 57 | expectedPath: "/org/mpris/MediaPlayer2", 58 | expectedMethod: "org.mpris.MediaPlayer2.Player.Pause", 59 | expectedFlags: 0, 60 | expectedArgs: nil, 61 | }, 62 | { 63 | name: "Play", 64 | givenName: "play", 65 | action: func(p *Player) { 66 | p.Play() 67 | }, 68 | expectedDest: "play", 69 | expectedPath: "/org/mpris/MediaPlayer2", 70 | expectedMethod: "org.mpris.MediaPlayer2.Player.Play", 71 | expectedFlags: 0, 72 | expectedArgs: nil, 73 | }, 74 | { 75 | name: "PlayPause", 76 | givenName: "play-pause", 77 | action: func(p *Player) { 78 | p.PlayPause() 79 | }, 80 | expectedDest: "play-pause", 81 | expectedPath: "/org/mpris/MediaPlayer2", 82 | expectedMethod: "org.mpris.MediaPlayer2.Player.PlayPause", 83 | expectedFlags: 0, 84 | expectedArgs: nil, 85 | }, 86 | { 87 | name: "SeekTo", 88 | givenName: "seek-to", 89 | action: func(p *Player) { 90 | p.SeekTo(12356789) 91 | }, 92 | expectedDest: "seek-to", 93 | expectedPath: "/org/mpris/MediaPlayer2", 94 | expectedMethod: "org.mpris.MediaPlayer2.Player.SeekTo", 95 | expectedFlags: 0, 96 | expectedArgs: []interface{}{int64(12356789)}, 97 | }, 98 | { 99 | name: "Stop", 100 | givenName: "stop", 101 | action: func(p *Player) { 102 | p.Stop() 103 | }, 104 | expectedDest: "stop", 105 | expectedPath: "/org/mpris/MediaPlayer2", 106 | expectedMethod: "org.mpris.MediaPlayer2.Player.Stop", 107 | expectedFlags: 0, 108 | expectedArgs: nil, 109 | }, 110 | { 111 | name: "SetPosition", 112 | givenName: "set-position", 113 | action: func(p *Player) { 114 | p.SetPosition("/my/path", 123456789) 115 | }, 116 | expectedDest: "set-position", 117 | expectedPath: "/org/mpris/MediaPlayer2", 118 | expectedMethod: "org.mpris.MediaPlayer2.Player.SetPosition", 119 | expectedFlags: 0, 120 | expectedArgs: []interface{}{dbus.ObjectPath("/my/path"), int64(123456789)}, 121 | }, 122 | { 123 | name: "OpenUri", 124 | givenName: "open-uri", 125 | action: func(p *Player) { 126 | p.OpenURI("file://my/uri") 127 | }, 128 | expectedDest: "open-uri", 129 | expectedPath: "/org/mpris/MediaPlayer2", 130 | expectedMethod: "org.mpris.MediaPlayer2.Player.OpenUri", 131 | expectedFlags: 0, 132 | expectedArgs: []interface{}{"file://my/uri"}, 133 | }, 134 | } 135 | 136 | for _, tt := range tests { 137 | t.Run(tt.name, func(t *testing.T) { 138 | var givenDest string 139 | var givenPath dbus.ObjectPath 140 | var givenMethod string 141 | var givenFlags dbus.Flags 142 | var givenArgs []interface{} 143 | 144 | tt.action(&Player{ 145 | name: tt.givenName, 146 | connection: &dbusConnMock{ 147 | ObjectFunc: func(dest string, path dbus.ObjectPath) dbusBusObject { 148 | givenDest = dest 149 | givenPath = path 150 | return &dbusBusObjectMock{ 151 | CallFunc: func(method string, flags dbus.Flags, args ...interface{}) dbusCall { 152 | givenMethod = method 153 | givenFlags = flags 154 | givenArgs = args 155 | return nil 156 | }, 157 | } 158 | }, 159 | }, 160 | }) 161 | 162 | assert.Equal(t, tt.expectedDest, givenDest, "given dest is not as expected") 163 | assert.Equal(t, tt.expectedPath, givenPath, "given path is not as expected") 164 | assert.Equal(t, tt.expectedMethod, givenMethod, "given method is not as expected") 165 | assert.Equal(t, tt.expectedFlags, givenFlags, "given flags is not as expected") 166 | assert.EqualValues(t, tt.expectedArgs, givenArgs, "given args is not as expected") 167 | }) 168 | } 169 | } 170 | 171 | func TestPlayer_GetProperties(t *testing.T) { 172 | tests := []struct { 173 | name string 174 | givenName string 175 | callVariant dbus.Variant 176 | callError error 177 | runAndValidate func(t *testing.T, p *Player) 178 | expectedDest string 179 | expectedPath dbus.ObjectPath 180 | expectedKey string 181 | }{ 182 | { 183 | name: "PlaybackStatus", 184 | callVariant: dbus.MakeVariant("Paused"), 185 | givenName: "playback-status", 186 | runAndValidate: func(t *testing.T, p *Player) { 187 | s, err := p.PlaybackStatus() 188 | assert.NoError(t, err) 189 | assert.Equal(t, PlaybackStatusPaused, s, "status is not as expected") 190 | }, 191 | expectedDest: "playback-status", 192 | expectedPath: "/org/mpris/MediaPlayer2", 193 | expectedKey: "org.mpris.MediaPlayer2.Player.PlaybackStatus", 194 | }, 195 | { 196 | name: "PlaybackStatus error", 197 | callError: errors.New("nope"), 198 | givenName: "playback-status", 199 | runAndValidate: func(t *testing.T, p *Player) { 200 | _, err := p.PlaybackStatus() 201 | assert.Equal(t, "failed to get property \"org.mpris.MediaPlayer2.Player.PlaybackStatus\": nope", fmt.Sprint(err)) 202 | }, 203 | expectedDest: "playback-status", 204 | expectedPath: "/org/mpris/MediaPlayer2", 205 | expectedKey: "org.mpris.MediaPlayer2.Player.PlaybackStatus", 206 | }, 207 | { 208 | name: "LoopStatus", 209 | callVariant: dbus.MakeVariant("Track"), 210 | givenName: "loop-status", 211 | runAndValidate: func(t *testing.T, p *Player) { 212 | s, err := p.LoopStatus() 213 | assert.NoError(t, err) 214 | assert.Equal(t, LoopStatusTrack, s, "status is not as expected") 215 | }, 216 | expectedDest: "loop-status", 217 | expectedPath: "/org/mpris/MediaPlayer2", 218 | expectedKey: "org.mpris.MediaPlayer2.Player.LoopStatus", 219 | }, 220 | { 221 | name: "LoopStatus error", 222 | callError: errors.New("nope"), 223 | givenName: "loop-status", 224 | runAndValidate: func(t *testing.T, p *Player) { 225 | _, err := p.LoopStatus() 226 | assert.Equal(t, "failed to get property \"org.mpris.MediaPlayer2.Player.LoopStatus\": nope", fmt.Sprint(err)) 227 | }, 228 | expectedDest: "loop-status", 229 | expectedPath: "/org/mpris/MediaPlayer2", 230 | expectedKey: "org.mpris.MediaPlayer2.Player.LoopStatus", 231 | }, 232 | { 233 | name: "Rate", 234 | callVariant: dbus.MakeVariant(float64(3)), 235 | givenName: "rate", 236 | runAndValidate: func(t *testing.T, p *Player) { 237 | s, err := p.Rate() 238 | assert.NoError(t, err) 239 | assert.Equal(t, float64(3), s, "rate is not as expected") 240 | }, 241 | expectedDest: "rate", 242 | expectedPath: "/org/mpris/MediaPlayer2", 243 | expectedKey: "org.mpris.MediaPlayer2.Player.Rate", 244 | }, 245 | { 246 | name: "Rate error", 247 | callError: errors.New("nope"), 248 | givenName: "rate", 249 | runAndValidate: func(t *testing.T, p *Player) { 250 | _, err := p.Rate() 251 | assert.Equal(t, "failed to get property \"org.mpris.MediaPlayer2.Player.Rate\": nope", fmt.Sprint(err)) 252 | }, 253 | expectedDest: "rate", 254 | expectedPath: "/org/mpris/MediaPlayer2", 255 | expectedKey: "org.mpris.MediaPlayer2.Player.Rate", 256 | }, 257 | { 258 | name: "Shuffle", 259 | callVariant: dbus.MakeVariant(false), 260 | givenName: "shuffle", 261 | runAndValidate: func(t *testing.T, p *Player) { 262 | s, err := p.Shuffle() 263 | assert.NoError(t, err) 264 | assert.Equal(t, false, s, "shuffle is not as expected") 265 | }, 266 | expectedDest: "shuffle", 267 | expectedPath: "/org/mpris/MediaPlayer2", 268 | expectedKey: "org.mpris.MediaPlayer2.Player.Shuffle", 269 | }, 270 | { 271 | name: "Shuffle error", 272 | callError: errors.New("nope"), 273 | givenName: "shuffle", 274 | runAndValidate: func(t *testing.T, p *Player) { 275 | _, err := p.Shuffle() 276 | assert.Equal(t, "failed to get property \"org.mpris.MediaPlayer2.Player.Shuffle\": nope", fmt.Sprint(err)) 277 | }, 278 | expectedDest: "shuffle", 279 | expectedPath: "/org/mpris/MediaPlayer2", 280 | expectedKey: "org.mpris.MediaPlayer2.Player.Shuffle", 281 | }, 282 | { 283 | name: "Metadata", 284 | callVariant: dbus.MakeVariant(map[string]dbus.Variant{ 285 | "myKey1": dbus.MakeVariant(true), 286 | "myKey2": dbus.MakeVariant("key2"), 287 | }), 288 | givenName: "metadata", 289 | runAndValidate: func(t *testing.T, p *Player) { 290 | s, err := p.Metadata() 291 | assert.NoError(t, err) 292 | assert.Equal(t, Metadata{ 293 | "myKey1": dbus.MakeVariant(true), 294 | "myKey2": dbus.MakeVariant("key2"), 295 | }, s, "metadata is not as expected") 296 | }, 297 | expectedDest: "metadata", 298 | expectedPath: "/org/mpris/MediaPlayer2", 299 | expectedKey: "org.mpris.MediaPlayer2.Player.Metadata", 300 | }, 301 | { 302 | name: "Metadata error", 303 | callError: errors.New("nope"), 304 | givenName: "metadata", 305 | runAndValidate: func(t *testing.T, p *Player) { 306 | _, err := p.Metadata() 307 | assert.Equal(t, "failed to get property \"org.mpris.MediaPlayer2.Player.Metadata\": nope", fmt.Sprint(err)) 308 | }, 309 | expectedDest: "metadata", 310 | expectedPath: "/org/mpris/MediaPlayer2", 311 | expectedKey: "org.mpris.MediaPlayer2.Player.Metadata", 312 | }, 313 | { 314 | name: "Volume", 315 | callVariant: dbus.MakeVariant(0.5), 316 | givenName: "volume", 317 | runAndValidate: func(t *testing.T, p *Player) { 318 | s, err := p.Volume() 319 | assert.NoError(t, err) 320 | assert.Equal(t, 0.5, s, "volume is not as expected") 321 | }, 322 | expectedDest: "volume", 323 | expectedPath: "/org/mpris/MediaPlayer2", 324 | expectedKey: "org.mpris.MediaPlayer2.Player.Volume", 325 | }, 326 | { 327 | name: "Volume error", 328 | callError: errors.New("nope"), 329 | givenName: "volume", 330 | runAndValidate: func(t *testing.T, p *Player) { 331 | _, err := p.Volume() 332 | assert.Equal(t, "failed to get property \"org.mpris.MediaPlayer2.Player.Volume\": nope", fmt.Sprint(err)) 333 | }, 334 | expectedDest: "volume", 335 | expectedPath: "/org/mpris/MediaPlayer2", 336 | expectedKey: "org.mpris.MediaPlayer2.Player.Volume", 337 | }, 338 | { 339 | name: "Position", 340 | callVariant: dbus.MakeVariant(int64(220342)), 341 | givenName: "position", 342 | runAndValidate: func(t *testing.T, p *Player) { 343 | s, err := p.Position() 344 | assert.NoError(t, err) 345 | assert.Equal(t, int64(220342), s, "position is not as expected") 346 | }, 347 | expectedDest: "position", 348 | expectedPath: "/org/mpris/MediaPlayer2", 349 | expectedKey: "org.mpris.MediaPlayer2.Player.Position", 350 | }, 351 | { 352 | name: "Position error", 353 | callError: errors.New("nope"), 354 | givenName: "position", 355 | runAndValidate: func(t *testing.T, p *Player) { 356 | _, err := p.Position() 357 | assert.Equal(t, "failed to get property \"org.mpris.MediaPlayer2.Player.Position\": nope", fmt.Sprint(err)) 358 | }, 359 | expectedDest: "position", 360 | expectedPath: "/org/mpris/MediaPlayer2", 361 | expectedKey: "org.mpris.MediaPlayer2.Player.Position", 362 | }, 363 | { 364 | name: "MinimumRate", 365 | callVariant: dbus.MakeVariant(0.000001), 366 | givenName: "minimum-rate", 367 | runAndValidate: func(t *testing.T, p *Player) { 368 | s, err := p.MinimumRate() 369 | assert.NoError(t, err) 370 | assert.Equal(t, 0.000001, s, "minimum-rate is not as expected") 371 | }, 372 | expectedDest: "minimum-rate", 373 | expectedPath: "/org/mpris/MediaPlayer2", 374 | expectedKey: "org.mpris.MediaPlayer2.Player.MinimumRate", 375 | }, 376 | { 377 | name: "MinimumRate error", 378 | callError: errors.New("nope"), 379 | givenName: "minimum-rate", 380 | runAndValidate: func(t *testing.T, p *Player) { 381 | _, err := p.MinimumRate() 382 | assert.Equal(t, "failed to get property \"org.mpris.MediaPlayer2.Player.MinimumRate\": nope", fmt.Sprint(err)) 383 | }, 384 | expectedDest: "minimum-rate", 385 | expectedPath: "/org/mpris/MediaPlayer2", 386 | expectedKey: "org.mpris.MediaPlayer2.Player.MinimumRate", 387 | }, 388 | { 389 | name: "MaximumRate", 390 | callVariant: dbus.MakeVariant(0.000001), 391 | givenName: "maximum-rate", 392 | runAndValidate: func(t *testing.T, p *Player) { 393 | s, err := p.MaximumRate() 394 | assert.NoError(t, err) 395 | assert.Equal(t, 0.000001, s, "maximum-rate is not as expected") 396 | }, 397 | expectedDest: "maximum-rate", 398 | expectedPath: "/org/mpris/MediaPlayer2", 399 | expectedKey: "org.mpris.MediaPlayer2.Player.MaximumRate", 400 | }, 401 | { 402 | name: "MaximumRate error", 403 | callError: errors.New("nope"), 404 | givenName: "maximum-rate", 405 | runAndValidate: func(t *testing.T, p *Player) { 406 | _, err := p.MaximumRate() 407 | assert.Equal(t, "failed to get property \"org.mpris.MediaPlayer2.Player.MaximumRate\": nope", fmt.Sprint(err)) 408 | }, 409 | expectedDest: "maximum-rate", 410 | expectedPath: "/org/mpris/MediaPlayer2", 411 | expectedKey: "org.mpris.MediaPlayer2.Player.MaximumRate", 412 | }, 413 | { 414 | name: "CanGoNext", 415 | callVariant: dbus.MakeVariant(true), 416 | givenName: "can-go-next", 417 | runAndValidate: func(t *testing.T, p *Player) { 418 | s, err := p.CanGoNext() 419 | assert.NoError(t, err) 420 | assert.Equal(t, true, s, "can-go-next is not as expected") 421 | }, 422 | expectedDest: "can-go-next", 423 | expectedPath: "/org/mpris/MediaPlayer2", 424 | expectedKey: "org.mpris.MediaPlayer2.Player.CanGoNext", 425 | }, 426 | { 427 | name: "CanGoNext error", 428 | callError: errors.New("nope"), 429 | givenName: "can-go-next", 430 | runAndValidate: func(t *testing.T, p *Player) { 431 | _, err := p.CanGoNext() 432 | assert.Equal(t, "failed to get property \"org.mpris.MediaPlayer2.Player.CanGoNext\": nope", fmt.Sprint(err)) 433 | }, 434 | expectedDest: "can-go-next", 435 | expectedPath: "/org/mpris/MediaPlayer2", 436 | expectedKey: "org.mpris.MediaPlayer2.Player.CanGoNext", 437 | }, 438 | { 439 | name: "CanGoPrevious", 440 | callVariant: dbus.MakeVariant(true), 441 | givenName: "can-go-previous", 442 | runAndValidate: func(t *testing.T, p *Player) { 443 | s, err := p.CanGoPrevious() 444 | assert.NoError(t, err) 445 | assert.Equal(t, true, s, "can-go-previous is not as expected") 446 | }, 447 | expectedDest: "can-go-previous", 448 | expectedPath: "/org/mpris/MediaPlayer2", 449 | expectedKey: "org.mpris.MediaPlayer2.Player.CanGoPrevious", 450 | }, 451 | { 452 | name: "CanGoPrevious error", 453 | callError: errors.New("nope"), 454 | givenName: "can-go-previous", 455 | runAndValidate: func(t *testing.T, p *Player) { 456 | _, err := p.CanGoPrevious() 457 | assert.Equal(t, "failed to get property \"org.mpris.MediaPlayer2.Player.CanGoPrevious\": nope", fmt.Sprint(err)) 458 | }, 459 | expectedDest: "can-go-previous", 460 | expectedPath: "/org/mpris/MediaPlayer2", 461 | expectedKey: "org.mpris.MediaPlayer2.Player.CanGoPrevious", 462 | }, 463 | { 464 | name: "CanPlay", 465 | callVariant: dbus.MakeVariant(true), 466 | givenName: "can-play", 467 | runAndValidate: func(t *testing.T, p *Player) { 468 | s, err := p.CanPlay() 469 | assert.NoError(t, err) 470 | assert.Equal(t, true, s, "can-play is not as expected") 471 | }, 472 | expectedDest: "can-play", 473 | expectedPath: "/org/mpris/MediaPlayer2", 474 | expectedKey: "org.mpris.MediaPlayer2.Player.CanPlay", 475 | }, 476 | { 477 | name: "CanPlay error", 478 | callError: errors.New("nope"), 479 | givenName: "can-play", 480 | runAndValidate: func(t *testing.T, p *Player) { 481 | _, err := p.CanPlay() 482 | assert.Equal(t, "failed to get property \"org.mpris.MediaPlayer2.Player.CanPlay\": nope", fmt.Sprint(err)) 483 | }, 484 | expectedDest: "can-play", 485 | expectedPath: "/org/mpris/MediaPlayer2", 486 | expectedKey: "org.mpris.MediaPlayer2.Player.CanPlay", 487 | }, 488 | { 489 | name: "CanPause", 490 | callVariant: dbus.MakeVariant(true), 491 | givenName: "can-pause", 492 | runAndValidate: func(t *testing.T, p *Player) { 493 | s, err := p.CanPause() 494 | assert.NoError(t, err) 495 | assert.Equal(t, true, s, "can-pause is not as expected") 496 | }, 497 | expectedDest: "can-pause", 498 | expectedPath: "/org/mpris/MediaPlayer2", 499 | expectedKey: "org.mpris.MediaPlayer2.Player.CanPause", 500 | }, 501 | { 502 | name: "CanPause error", 503 | callError: errors.New("nope"), 504 | givenName: "can-pause", 505 | runAndValidate: func(t *testing.T, p *Player) { 506 | _, err := p.CanPause() 507 | assert.Equal(t, "failed to get property \"org.mpris.MediaPlayer2.Player.CanPause\": nope", fmt.Sprint(err)) 508 | }, 509 | expectedDest: "can-pause", 510 | expectedPath: "/org/mpris/MediaPlayer2", 511 | expectedKey: "org.mpris.MediaPlayer2.Player.CanPause", 512 | }, 513 | { 514 | name: "CanSeek", 515 | callVariant: dbus.MakeVariant(true), 516 | givenName: "can-seek", 517 | runAndValidate: func(t *testing.T, p *Player) { 518 | s, err := p.CanSeek() 519 | assert.NoError(t, err) 520 | assert.Equal(t, true, s, "can-seek is not as expected") 521 | }, 522 | expectedDest: "can-seek", 523 | expectedPath: "/org/mpris/MediaPlayer2", 524 | expectedKey: "org.mpris.MediaPlayer2.Player.CanSeek", 525 | }, 526 | { 527 | name: "CanSeek error", 528 | callError: errors.New("nope"), 529 | givenName: "can-seek", 530 | runAndValidate: func(t *testing.T, p *Player) { 531 | _, err := p.CanSeek() 532 | assert.Equal(t, "failed to get property \"org.mpris.MediaPlayer2.Player.CanSeek\": nope", fmt.Sprint(err)) 533 | }, 534 | expectedDest: "can-seek", 535 | expectedPath: "/org/mpris/MediaPlayer2", 536 | expectedKey: "org.mpris.MediaPlayer2.Player.CanSeek", 537 | }, 538 | { 539 | name: "CanControl", 540 | callVariant: dbus.MakeVariant(true), 541 | givenName: "can-control", 542 | runAndValidate: func(t *testing.T, p *Player) { 543 | s, err := p.CanControl() 544 | assert.NoError(t, err) 545 | assert.Equal(t, true, s, "can-control is not as expected") 546 | }, 547 | expectedDest: "can-control", 548 | expectedPath: "/org/mpris/MediaPlayer2", 549 | expectedKey: "org.mpris.MediaPlayer2.Player.CanControl", 550 | }, 551 | { 552 | name: "CanControl error", 553 | callError: errors.New("nope"), 554 | givenName: "can-control", 555 | runAndValidate: func(t *testing.T, p *Player) { 556 | _, err := p.CanControl() 557 | assert.Equal(t, "failed to get property \"org.mpris.MediaPlayer2.Player.CanControl\": nope", fmt.Sprint(err)) 558 | }, 559 | expectedDest: "can-control", 560 | expectedPath: "/org/mpris/MediaPlayer2", 561 | expectedKey: "org.mpris.MediaPlayer2.Player.CanControl", 562 | }, 563 | } 564 | 565 | for _, tt := range tests { 566 | t.Run(tt.name, func(t *testing.T) { 567 | var calledDest string 568 | var calledPath dbus.ObjectPath 569 | var calledKey string 570 | 571 | m := &dbusConnMock{ 572 | ObjectFunc: func(dest string, path dbus.ObjectPath) dbusBusObject { 573 | calledDest = dest 574 | calledPath = path 575 | return &dbusBusObjectMock{ 576 | GetPropertyFunc: func(key string) (dbus.Variant, error) { 577 | calledKey = key 578 | return tt.callVariant, tt.callError 579 | }, 580 | } 581 | }, 582 | } 583 | 584 | tt.runAndValidate(t, &Player{name: tt.givenName, connection: m}) 585 | 586 | assert.Equal(t, tt.expectedDest, calledDest, "called dest is not as expected") 587 | assert.Equal(t, tt.expectedPath, calledPath, "called path is not as expected") 588 | assert.Equal(t, tt.expectedKey, calledKey, "called key is not as expected") 589 | }) 590 | } 591 | } 592 | 593 | func TestPlayer_SetProperties(t *testing.T) { 594 | tests := []struct { 595 | name string 596 | givenName string 597 | callError error 598 | runAndValidate func(t *testing.T, p *Player) 599 | expectedDest string 600 | expectedPath dbus.ObjectPath 601 | expectedProperty string 602 | expectedValue interface{} 603 | }{ 604 | { 605 | name: "LoopStatus", 606 | givenName: "loop-status", 607 | runAndValidate: func(t *testing.T, p *Player) { 608 | err := p.SetLoopStatus(LoopStatusTrack) 609 | assert.NoError(t, err) 610 | }, 611 | expectedDest: "loop-status", 612 | expectedPath: "/org/mpris/MediaPlayer2", 613 | expectedProperty: "org.mpris.MediaPlayer2.Player.LoopStatus", 614 | expectedValue: dbus.MakeVariant("Track"), 615 | }, 616 | { 617 | name: "LoopStatus error", 618 | callError: errors.New("nope"), 619 | givenName: "loop-status", 620 | runAndValidate: func(t *testing.T, p *Player) { 621 | err := p.SetLoopStatus(LoopStatusPlaylist) 622 | assert.Equal(t, "failed to set property \"org.mpris.MediaPlayer2.Player.LoopStatus\": nope", fmt.Sprint(err)) 623 | }, 624 | expectedDest: "loop-status", 625 | expectedPath: "/org/mpris/MediaPlayer2", 626 | expectedProperty: "org.mpris.MediaPlayer2.Player.LoopStatus", 627 | expectedValue: dbus.MakeVariant("Playlist"), 628 | }, 629 | { 630 | name: "Rate", 631 | givenName: "rate", 632 | runAndValidate: func(t *testing.T, p *Player) { 633 | err := p.SetRate(0.5) 634 | assert.NoError(t, err) 635 | }, 636 | expectedDest: "rate", 637 | expectedPath: "/org/mpris/MediaPlayer2", 638 | expectedProperty: "org.mpris.MediaPlayer2.Player.Rate", 639 | expectedValue: dbus.MakeVariant(0.5), 640 | }, 641 | { 642 | name: "Rate error", 643 | callError: errors.New("nope"), 644 | givenName: "rate", 645 | runAndValidate: func(t *testing.T, p *Player) { 646 | err := p.SetRate(2) 647 | assert.Equal(t, "failed to set property \"org.mpris.MediaPlayer2.Player.Rate\": nope", fmt.Sprint(err)) 648 | }, 649 | expectedDest: "rate", 650 | expectedPath: "/org/mpris/MediaPlayer2", 651 | expectedProperty: "org.mpris.MediaPlayer2.Player.Rate", 652 | expectedValue: dbus.MakeVariant(float64(2)), 653 | }, 654 | { 655 | name: "Shuffle", 656 | givenName: "shuffle", 657 | runAndValidate: func(t *testing.T, p *Player) { 658 | err := p.SetShuffle(true) 659 | assert.NoError(t, err) 660 | }, 661 | expectedDest: "shuffle", 662 | expectedPath: "/org/mpris/MediaPlayer2", 663 | expectedProperty: "org.mpris.MediaPlayer2.Player.Shuffle", 664 | expectedValue: dbus.MakeVariant(true), 665 | }, 666 | { 667 | name: "Shuffle error", 668 | callError: errors.New("nope"), 669 | givenName: "shuffle", 670 | runAndValidate: func(t *testing.T, p *Player) { 671 | err := p.SetShuffle(false) 672 | assert.Equal(t, "failed to set property \"org.mpris.MediaPlayer2.Player.Shuffle\": nope", fmt.Sprint(err)) 673 | }, 674 | expectedDest: "shuffle", 675 | expectedPath: "/org/mpris/MediaPlayer2", 676 | expectedProperty: "org.mpris.MediaPlayer2.Player.Shuffle", 677 | expectedValue: dbus.MakeVariant(false), 678 | }, 679 | { 680 | name: "Volume", 681 | givenName: "volume", 682 | runAndValidate: func(t *testing.T, p *Player) { 683 | err := p.SetVolume(1) 684 | assert.NoError(t, err) 685 | }, 686 | expectedDest: "volume", 687 | expectedPath: "/org/mpris/MediaPlayer2", 688 | expectedProperty: "org.mpris.MediaPlayer2.Player.Volume", 689 | expectedValue: dbus.MakeVariant(float64(1)), 690 | }, 691 | { 692 | name: "Volume error", 693 | callError: errors.New("nope"), 694 | givenName: "volume", 695 | runAndValidate: func(t *testing.T, p *Player) { 696 | err := p.SetVolume(0.5) 697 | assert.Equal(t, "failed to set property \"org.mpris.MediaPlayer2.Player.Volume\": nope", fmt.Sprint(err)) 698 | }, 699 | expectedDest: "volume", 700 | expectedPath: "/org/mpris/MediaPlayer2", 701 | expectedProperty: "org.mpris.MediaPlayer2.Player.Volume", 702 | expectedValue: dbus.MakeVariant(0.5), 703 | }, 704 | } 705 | 706 | for _, tt := range tests { 707 | t.Run(tt.name, func(t *testing.T) { 708 | var calledDest string 709 | var calledPath dbus.ObjectPath 710 | var calledProperty string 711 | var calledValue interface{} 712 | 713 | m := &dbusConnMock{ 714 | ObjectFunc: func(dest string, path dbus.ObjectPath) dbusBusObject { 715 | calledDest = dest 716 | calledPath = path 717 | return &dbusBusObjectMock{ 718 | SetPropertyFunc: func(p string, v interface{}) error { 719 | calledProperty = p 720 | calledValue = v 721 | 722 | return tt.callError 723 | }, 724 | } 725 | }, 726 | } 727 | 728 | tt.runAndValidate(t, &Player{name: tt.givenName, connection: m}) 729 | 730 | assert.Equal(t, tt.expectedDest, calledDest, "called dest is not as expected") 731 | assert.Equal(t, tt.expectedPath, calledPath, "called path is not as expected") 732 | assert.Equal(t, tt.expectedProperty, calledProperty, "called property is not as expected") 733 | assert.Equal(t, tt.expectedValue, calledValue, "called value is not as expected") 734 | }) 735 | } 736 | } 737 | 738 | func TestPlayer_Close(t *testing.T) { 739 | tests := []struct { 740 | name string 741 | closeErr error 742 | expectedErr string 743 | }{ 744 | { 745 | name: "Happycase", 746 | }, { 747 | name: "Close error", 748 | closeErr: errors.New("unexpected error"), 749 | expectedErr: "failed to close dbus connection: unexpected error", 750 | }, 751 | } 752 | for _, tt := range tests { 753 | t.Run(tt.name, func(t *testing.T) { 754 | mock := dbusConnMock{ 755 | CloseFunc: func() error { 756 | return tt.closeErr 757 | }, 758 | } 759 | 760 | err := Player{ 761 | connection: &mock, 762 | }.Close() 763 | require.Equal(t, msgOrEmpty(err), tt.expectedErr) 764 | assert.Equal(t, 1, len(mock.CloseCalls())) 765 | }) 766 | } 767 | } 768 | 769 | func TestPlayer_Seeked(t *testing.T) { 770 | tests := []struct { 771 | name string 772 | 773 | givenSignals []*dbus.Signal 774 | 775 | addMatchSignalErr error 776 | expectedAddMatchSignalCallCount int 777 | expectedSignalCallCount int 778 | 779 | expectedErr string 780 | expectedPositions []int 781 | }{ 782 | { 783 | name: "happycase", 784 | expectedAddMatchSignalCallCount: 1, 785 | expectedSignalCallCount: 1, 786 | givenSignals: []*dbus.Signal{ 787 | { 788 | Name: "org.mpris.MediaPlayer2.Player.Seeked", 789 | Body: []interface{}{int64(1111)}, 790 | }, { 791 | Name: "unknown name", 792 | Body: []interface{}{int64(22222)}, 793 | }, { // multiple body infos 794 | Name: "org.mpris.MediaPlayer2.Player.Seeked", 795 | Body: []interface{}{int64(333111), int64(333222)}, 796 | }, { // invalid body type 797 | Name: "org.mpris.MediaPlayer2.Player.Seeked", 798 | Body: []interface{}{"4444444"}, 799 | }, { 800 | Name: "org.mpris.MediaPlayer2.Player.Seeked", 801 | Body: []interface{}{int64(55555555)}, 802 | }, 803 | }, 804 | expectedPositions: []int{ 805 | 1111, 806 | 55555555, 807 | }, 808 | }, { 809 | name: "add match signal error", 810 | addMatchSignalErr: errors.New("unexpected error"), 811 | expectedAddMatchSignalCallCount: 1, 812 | expectedSignalCallCount: 0, 813 | expectedErr: "failed to add signal match option: unexpected error", 814 | givenSignals: []*dbus.Signal{ 815 | { 816 | Name: "org.mpris.MediaPlayer2.Player.Seeked", 817 | Body: []interface{}{int64(1111)}, 818 | }, 819 | }, 820 | }, 821 | } 822 | 823 | for _, tt := range tests { 824 | t.Run(tt.name, func(t *testing.T) { 825 | testCtx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 826 | defer cancel() 827 | 828 | mock := &dbusConnMock{ 829 | AddMatchSignalFunc: func(_ ...dbus.MatchOption) error { 830 | return tt.addMatchSignalErr 831 | }, 832 | SignalFunc: func(ch chan<- *dbus.Signal) { 833 | go func() { 834 | for _, sig := range tt.givenSignals { 835 | ch <- sig 836 | } 837 | }() 838 | }, 839 | } 840 | 841 | poss, err := Player{ 842 | connection: mock, 843 | }.Seeked(testCtx) 844 | require.Equal(t, tt.expectedErr, msgOrEmpty(err)) 845 | 846 | var collectedPoss []int 847 | if poss != nil { 848 | for { 849 | p, ok := <-poss 850 | if !ok { 851 | break 852 | } 853 | collectedPoss = append(collectedPoss, p) 854 | } 855 | } 856 | assert.EqualValues(t, tt.expectedPositions, collectedPoss) 857 | assert.Equal(t, tt.expectedAddMatchSignalCallCount, len(mock.AddMatchSignalCalls())) 858 | assert.Equal(t, tt.expectedSignalCallCount, len(mock.SignalCalls())) 859 | }) 860 | } 861 | } 862 | 863 | func TestNewPlayer(t *testing.T) { 864 | oldDbusSessionBus := dbusSessionBus 865 | defer func() { 866 | dbusSessionBus = oldDbusSessionBus 867 | }() 868 | 869 | dbusConn := &dbus.Conn{} 870 | dbusSessionBus = func() (conn *dbus.Conn, err error) { 871 | return dbusConn, nil 872 | } 873 | 874 | p, err := NewPlayer("test") 875 | 876 | assert.NoError(t, err, "unexpected error") 877 | assert.Equal(t, &dbusConnWrapper{dbusConn}, p.connection) 878 | } 879 | 880 | func TestNewPlayer_Error(t *testing.T) { 881 | oldDbusSessionBus := dbusSessionBus 882 | defer func() { 883 | dbusSessionBus = oldDbusSessionBus 884 | }() 885 | 886 | dbusSessionBusErr := errors.New("nope") 887 | dbusSessionBus = func() (conn *dbus.Conn, err error) { 888 | return nil, dbusSessionBusErr 889 | } 890 | 891 | expectedError := errors.New("failed to connect to session-bus: nope") 892 | _, err := NewPlayer("test") 893 | if fmt.Sprint(err) == fmt.Sprint() { 894 | t.Fatalf("unexpected error. Given: %q, Expected: %q", err, expectedError) 895 | } 896 | } 897 | 898 | func TestNewPlayerWithConnection(t *testing.T) { 899 | dbusConn := &dbus.Conn{} 900 | 901 | p := NewPlayerWithConnection("test", dbusConn) 902 | assert.Equal(t, &dbusConnWrapper{dbusConn}, p.connection) 903 | } 904 | 905 | func msgOrEmpty(err error) string { 906 | if err == nil { 907 | return "" 908 | } 909 | 910 | return err.Error() 911 | 912 | } 913 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package mpris 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/godbus/dbus/v5" 9 | ) 10 | 11 | const timeFormat = "2006-01-02T15:04-07:00" 12 | 13 | // ErrTypeNotParsable indicates, that the given type is not parable. 14 | var ErrTypeNotParsable = errors.New("the given type is not as expected") 15 | 16 | // PlaybackStatus represents the playback status. 17 | type PlaybackStatus string 18 | 19 | const ( 20 | // PlaybackStatusPlaying represents the playback status "Playing". 21 | PlaybackStatusPlaying PlaybackStatus = "Playing" 22 | // PlaybackStatusStopped represents the playback status "Paused". 23 | PlaybackStatusStopped PlaybackStatus = "Stopped" 24 | // PlaybackStatusPaused represents the playback status "Stopped". 25 | PlaybackStatusPaused PlaybackStatus = "Paused" 26 | ) 27 | 28 | // LoopStatus represents the loop status. 29 | type LoopStatus string 30 | 31 | const ( 32 | // LoopStatusNone represents the loop status "None". 33 | LoopStatusNone LoopStatus = "None" 34 | // LoopStatusTrack represents the loop status "Track". 35 | LoopStatusTrack LoopStatus = "Track" 36 | // LoopStatusPlaylist represents the loop status "Playlist". 37 | LoopStatusPlaylist LoopStatus = "Playlist" 38 | ) 39 | 40 | // Metadata represents the mpris-metadata 41 | // see: https://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata/ 42 | type Metadata map[string]dbus.Variant 43 | 44 | // MPRISTrackID returns a unique identity for this track within the context of an MPRIS object (eg: tracklist). 45 | func (md Metadata) MPRISTrackID() (dbus.ObjectPath, error) { 46 | vt := md["mpris:trackid"].Value() 47 | if vt == nil { 48 | return "", nil 49 | } 50 | 51 | v, ok := vt.(dbus.ObjectPath) 52 | if !ok { 53 | return "", fmt.Errorf("%T could not be parsed to dbus.ObjectPath: %w", vt, ErrTypeNotParsable) 54 | } 55 | 56 | return v, nil 57 | } 58 | 59 | // MPRISLength returns the duration of the track in microseconds. 60 | func (md Metadata) MPRISLength() (int64, error) { 61 | vl := md["mpris:length"].Value() 62 | if vl == nil { 63 | return 0, nil 64 | } 65 | 66 | v, ok := vl.(int64) 67 | if !ok { 68 | return 0, fmt.Errorf("%T could not be parsed to int64: %w", vl, ErrTypeNotParsable) 69 | } 70 | 71 | return v, nil 72 | } 73 | 74 | // MPRISArtURL returns the location of an image representing the track or album. Clients should not assume this will 75 | // continue to exist when the media player stops giving out the URL. 76 | func (md Metadata) MPRISArtURL() (string, error) { 77 | va := md["mpris:artUrl"].Value() 78 | if va == nil { 79 | return "", nil 80 | } 81 | 82 | v, ok := va.(string) 83 | if !ok { 84 | return "", fmt.Errorf("%T could not be parsed to string: %w", va, ErrTypeNotParsable) 85 | } 86 | 87 | return v, nil 88 | } 89 | 90 | // XESAMAlbum returns the album name. 91 | func (md Metadata) XESAMAlbum() (string, error) { 92 | va := md["xesam:album"].Value() 93 | if va == nil { 94 | return "", nil 95 | } 96 | 97 | v, ok := va.(string) 98 | if !ok { 99 | return "", fmt.Errorf("%T could not be parsed to string: %w", va, ErrTypeNotParsable) 100 | } 101 | 102 | return v, nil 103 | } 104 | 105 | // XESAMAlbumArtist returns the album artist(s) 106 | func (md Metadata) XESAMAlbumArtist() ([]string, error) { 107 | va := md["xesam:albumArtist"].Value() 108 | if va == nil { 109 | return nil, nil 110 | } 111 | 112 | v, ok := va.([]string) 113 | if !ok { 114 | return nil, fmt.Errorf("%T could not be parsed to []string: %w", va, ErrTypeNotParsable) 115 | } 116 | 117 | return v, nil 118 | } 119 | 120 | // XESAMArtist returns the track artist(s). 121 | func (md Metadata) XESAMArtist() ([]string, error) { 122 | va := md["xesam:artist"].Value() 123 | if va == nil { 124 | return nil, nil 125 | } 126 | 127 | v, ok := va.([]string) 128 | if !ok { 129 | return nil, fmt.Errorf("%T could not be parsed to []string: %w", va, ErrTypeNotParsable) 130 | } 131 | 132 | return v, nil 133 | } 134 | 135 | // XESAMAsText returns the track lyrics. 136 | func (md Metadata) XESAMAsText() (string, error) { 137 | vt := md["xesam:asText"].Value() 138 | if vt == nil { 139 | return "", nil 140 | } 141 | 142 | v, ok := vt.(string) 143 | if !ok { 144 | return "", fmt.Errorf("%T could not be parsed to string: %w", vt, ErrTypeNotParsable) 145 | } 146 | 147 | return v, nil 148 | } 149 | 150 | // XESAMAudioBPM returns the speed of the music, in beats per minute. 151 | func (md Metadata) XESAMAudioBPM() (int, error) { 152 | va := md["xesam:audioBPM"].Value() 153 | if va == nil { 154 | return 0, nil 155 | } 156 | 157 | v, ok := va.(int) 158 | if !ok { 159 | return 0, fmt.Errorf("%T could not be parsed to int: %w", va, ErrTypeNotParsable) 160 | } 161 | 162 | return v, nil 163 | } 164 | 165 | // XESAMAutoRating returns an automatically-generated rating, based on things such as how often it has been played. 166 | // This should be in the range 0.0 to 1.0. 167 | func (md Metadata) XESAMAutoRating() (float64, error) { 168 | va := md["xesam:autoRating"].Value() 169 | if va == nil { 170 | return 0, nil 171 | } 172 | 173 | v, ok := va.(float64) 174 | if !ok { 175 | return 0, fmt.Errorf("%T could not be parsed to float64: %w", va, ErrTypeNotParsable) 176 | } 177 | return v, nil 178 | } 179 | 180 | // XESAMComment returns a (list of) freeform comment(s). 181 | func (md Metadata) XESAMComment() ([]string, error) { 182 | vc := md["xesam:comment"].Value() 183 | if vc == nil { 184 | return nil, nil 185 | } 186 | 187 | v, ok := vc.([]string) 188 | if !ok { 189 | return nil, fmt.Errorf("%T could not be parsed to []string: %w", vc, ErrTypeNotParsable) 190 | } 191 | return v, nil 192 | } 193 | 194 | // XESAMComposer returns the composer(s) of the track. 195 | func (md Metadata) XESAMComposer() ([]string, error) { 196 | vc := md["xesam:composer"].Value() 197 | if vc == nil { 198 | return nil, nil 199 | } 200 | 201 | v, ok := vc.([]string) 202 | if !ok { 203 | return nil, fmt.Errorf("%T could not be parsed to []string: %w", vc, ErrTypeNotParsable) 204 | } 205 | return v, nil 206 | } 207 | 208 | // XESAMContentCreated returns when the track was created. Usually only the year component will be useful. 209 | func (md Metadata) XESAMContentCreated() (time.Time, error) { 210 | vc := md["xesam:contentCreated"].Value() 211 | if vc == nil { 212 | return time.Time{}, nil 213 | } 214 | 215 | vs, ok := vc.(string) 216 | if !ok { 217 | return time.Time{}, fmt.Errorf("%T could not be parsed to string: %w", vc, ErrTypeNotParsable) 218 | } 219 | 220 | t, err := time.Parse(timeFormat, vs) 221 | if err != nil { 222 | return time.Time{}, fmt.Errorf("cound not parse time: %s: %w", err, ErrTypeNotParsable) 223 | } 224 | 225 | return t, nil 226 | } 227 | 228 | // XESAMDiscNumber returns the disc number on the album that this track is from. 229 | func (md Metadata) XESAMDiscNumber() (int, error) { 230 | vn := md["xesam:discNumber"].Value() 231 | if vn == nil { 232 | return 0, nil 233 | } 234 | 235 | v, ok := vn.(int) 236 | if !ok { 237 | return 0, fmt.Errorf("%T could not be parsed to int: %w", vn, ErrTypeNotParsable) 238 | } 239 | return v, nil 240 | } 241 | 242 | // XESAMFirstUsed returns when the track was first played. 243 | func (md Metadata) XESAMFirstUsed() (time.Time, error) { 244 | vu := md["xesam:firstUsed"].Value() 245 | if vu == nil { 246 | return time.Time{}, nil 247 | } 248 | 249 | vs, ok := vu.(string) 250 | if !ok { 251 | return time.Time{}, fmt.Errorf("%T could not be parsed to string: %w", vu, ErrTypeNotParsable) 252 | } 253 | 254 | t, err := time.Parse(timeFormat, vs) 255 | if err != nil { 256 | return time.Time{}, fmt.Errorf("cound not parse time: %s: %w", err, ErrTypeNotParsable) 257 | } 258 | 259 | return t, nil 260 | } 261 | 262 | // XESAMGenre returns the genre(s) of the track. 263 | func (md Metadata) XESAMGenre() ([]string, error) { 264 | vg := md["xesam:genre"].Value() 265 | if vg == nil { 266 | return nil, nil 267 | } 268 | 269 | v, ok := vg.([]string) 270 | if !ok { 271 | return nil, fmt.Errorf("%T could not be parsed to []string: %w", vg, ErrTypeNotParsable) 272 | } 273 | return v, nil 274 | } 275 | 276 | // XESAMLastUsed returns when the track was last played. 277 | func (md Metadata) XESAMLastUsed() (time.Time, error) { 278 | vu := md["xesam:lastUsed"].Value() 279 | if vu == nil { 280 | return time.Time{}, nil 281 | } 282 | 283 | vs, ok := vu.(string) 284 | if !ok { 285 | return time.Time{}, fmt.Errorf("%T could not be parsed to string: %w", vu, ErrTypeNotParsable) 286 | } 287 | 288 | t, err := time.Parse(timeFormat, vs) 289 | if err != nil { 290 | return time.Time{}, fmt.Errorf("cound not parse time: %s: %w", err, ErrTypeNotParsable) 291 | } 292 | 293 | return t, nil 294 | } 295 | 296 | // XESAMLyricist returns the lyricist(s) of the track. 297 | func (md Metadata) XESAMLyricist() ([]string, error) { 298 | vl := md["xesam:lyricist"].Value() 299 | if vl == nil { 300 | return nil, nil 301 | } 302 | 303 | v, ok := vl.([]string) 304 | if !ok { 305 | return nil, fmt.Errorf("%T could not be parsed to []string: %w", vl, ErrTypeNotParsable) 306 | } 307 | return v, nil 308 | } 309 | 310 | // XESAMTitle returns the track title. 311 | func (md Metadata) XESAMTitle() (string, error) { 312 | vt := md["xesam:title"].Value() 313 | if vt == nil { 314 | return "", nil 315 | } 316 | 317 | v, ok := vt.(string) 318 | if !ok { 319 | return "", fmt.Errorf("%T could not be parsed to string: %w", vt, ErrTypeNotParsable) 320 | } 321 | 322 | return v, nil 323 | } 324 | 325 | // XESAMTrackNumber returns the track number on the album disc. 326 | func (md Metadata) XESAMTrackNumber() (int, error) { 327 | vn := md["xesam:trackNumber"].Value() 328 | if vn == nil { 329 | return 0, nil 330 | } 331 | 332 | v, ok := vn.(int) 333 | if !ok { 334 | return 0, fmt.Errorf("%T could not be parsed to int: %w", vn, ErrTypeNotParsable) 335 | } 336 | return v, nil 337 | } 338 | 339 | // XESAMURL returns the location of the media file. 340 | func (md Metadata) XESAMURL() (string, error) { 341 | vu := md["xesam:url"].Value() 342 | if vu == nil { 343 | return "", nil 344 | } 345 | 346 | v, ok := vu.(string) 347 | if !ok { 348 | return "", fmt.Errorf("%T could not be parsed to string: %w", vu, ErrTypeNotParsable) 349 | } 350 | 351 | return v, nil 352 | } 353 | 354 | // XESAMUseCount returns hte number of times the track has been played. 355 | func (md Metadata) XESAMUseCount() (int, error) { 356 | vc := md["xesam:useCount"].Value() 357 | if vc == nil { 358 | return 0, nil 359 | } 360 | 361 | v, ok := vc.(int) 362 | if !ok { 363 | return 0, fmt.Errorf("%T could not be parsed to int: %w", vc, ErrTypeNotParsable) 364 | } 365 | return v, nil 366 | } 367 | 368 | // XESAMUserRating returns a user-specified rating. This should be in the range 0.0 to 1.0. 369 | func (md Metadata) XESAMUserRating() (float64, error) { 370 | vr := md["xesam:userRating"].Value() 371 | if vr == nil { 372 | return 0, nil 373 | } 374 | 375 | v, ok := vr.(float64) 376 | if !ok { 377 | return 0, fmt.Errorf("%T could not be parsed to float64: %w", vr, ErrTypeNotParsable) 378 | } 379 | return v, nil 380 | } 381 | 382 | // Find returns a generic representation of the requested value when present 383 | func (md Metadata) Find(key string) (dbus.Variant, bool) { 384 | variant, found := md[key] 385 | return variant, found 386 | } 387 | -------------------------------------------------------------------------------- /types_test.go: -------------------------------------------------------------------------------- 1 | package mpris 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/godbus/dbus/v5" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestMetadata_MPRISTrackID(t *testing.T) { 13 | var expectedTrackID dbus.ObjectPath 14 | var expectedErrorText string 15 | var trackID dbus.ObjectPath 16 | var err error 17 | 18 | //happycase 19 | expectedTrackID = "/my/path" 20 | expectedErrorText = "" 21 | trackID, err = Metadata{ 22 | "mpris:trackid": dbus.MakeVariant(dbus.ObjectPath("/my/path")), 23 | }.MPRISTrackID() 24 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 25 | assert.Equal(t, expectedTrackID, trackID, "unexpected trackID") 26 | 27 | //not present 28 | expectedTrackID = "" 29 | expectedErrorText = "" 30 | trackID, err = Metadata{}.MPRISTrackID() 31 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 32 | assert.Equal(t, expectedTrackID, trackID, "unexpected trackID") 33 | 34 | //unexpected type 35 | expectedTrackID = "" 36 | expectedErrorText = "int could not be parsed to dbus.ObjectPath: the given type is not as expected" 37 | trackID, err = Metadata{ 38 | "mpris:trackid": dbus.MakeVariant(123456789), 39 | }.MPRISTrackID() 40 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 41 | assert.Equal(t, expectedTrackID, trackID, "unexpected trackID") 42 | } 43 | 44 | func TestMetadata_MPRISLength(t *testing.T) { 45 | var expectedLength int64 46 | var expectedErrorText string 47 | var length int64 48 | var err error 49 | 50 | //happycase 51 | expectedLength = 42 52 | expectedErrorText = "" 53 | length, err = Metadata{ 54 | "mpris:length": dbus.MakeVariant(int64(42)), 55 | }.MPRISLength() 56 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 57 | assert.Equal(t, expectedLength, length, "unexpected length") 58 | 59 | //not present 60 | expectedLength = 0 61 | expectedErrorText = "" 62 | length, err = Metadata{}.MPRISLength() 63 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 64 | assert.Equal(t, expectedLength, length, "unexpected length") 65 | 66 | //unexpected type 67 | expectedLength = 0 68 | expectedErrorText = "string could not be parsed to int64: the given type is not as expected" 69 | length, err = Metadata{ 70 | "mpris:length": dbus.MakeVariant("nope"), 71 | }.MPRISLength() 72 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 73 | assert.Equal(t, expectedLength, length, "unexpected length") 74 | } 75 | 76 | func TestMetadata_MPRISArtURL(t *testing.T) { 77 | var expectedArtURL string 78 | var expectedErrorText string 79 | var artURL string 80 | var err error 81 | 82 | //happycase 83 | expectedArtURL = "/my/url" 84 | expectedErrorText = "" 85 | artURL, err = Metadata{ 86 | "mpris:artUrl": dbus.MakeVariant("/my/url"), 87 | }.MPRISArtURL() 88 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 89 | assert.Equal(t, expectedArtURL, artURL, "unexpected artURL") 90 | 91 | //not present 92 | expectedArtURL = "" 93 | expectedErrorText = "" 94 | artURL, err = Metadata{}.MPRISArtURL() 95 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 96 | assert.Equal(t, expectedArtURL, artURL, "unexpected artURL") 97 | 98 | //unexpected type 99 | expectedArtURL = "" 100 | expectedErrorText = "int could not be parsed to string: the given type is not as expected" 101 | artURL, err = Metadata{ 102 | "mpris:artUrl": dbus.MakeVariant(42), 103 | }.MPRISArtURL() 104 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 105 | assert.Equal(t, expectedArtURL, artURL, "unexpected artURL") 106 | } 107 | 108 | func TestMetadata_XESAMAlbum(t *testing.T) { 109 | var expectedAlbum string 110 | var expectedErrorText string 111 | var album string 112 | var err error 113 | 114 | //happycase 115 | expectedAlbum = "/my/url" 116 | expectedErrorText = "" 117 | album, err = Metadata{ 118 | "xesam:album": dbus.MakeVariant("/my/url"), 119 | }.XESAMAlbum() 120 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 121 | assert.Equal(t, expectedAlbum, album, "unexpected album") 122 | 123 | //not present 124 | expectedAlbum = "" 125 | expectedErrorText = "" 126 | album, err = Metadata{}.XESAMAlbum() 127 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 128 | assert.Equal(t, expectedAlbum, album, "unexpected album") 129 | 130 | //unexpected type 131 | expectedAlbum = "" 132 | expectedErrorText = "int could not be parsed to string: the given type is not as expected" 133 | album, err = Metadata{ 134 | "xesam:album": dbus.MakeVariant(42), 135 | }.XESAMAlbum() 136 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 137 | assert.Equal(t, expectedAlbum, album, "unexpected album") 138 | } 139 | 140 | func TestMetadata_XESAMAlbumArtist(t *testing.T) { 141 | var expectedAlbumArtist []string 142 | var expectedErrorText string 143 | var albumArtist []string 144 | var err error 145 | 146 | //happycase 147 | expectedAlbumArtist = []string{"artist1", "artist2"} 148 | expectedErrorText = "" 149 | albumArtist, err = Metadata{ 150 | "xesam:albumArtist": dbus.MakeVariant([]string{"artist1", "artist2"}), 151 | }.XESAMAlbumArtist() 152 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 153 | assert.Equal(t, expectedAlbumArtist, albumArtist, "unexpected albumArtist") 154 | 155 | //not present 156 | expectedAlbumArtist = nil 157 | expectedErrorText = "" 158 | albumArtist, err = Metadata{}.XESAMAlbumArtist() 159 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 160 | assert.Equal(t, expectedAlbumArtist, albumArtist, "unexpected albumArtist") 161 | 162 | //unexpected type 163 | expectedAlbumArtist = nil 164 | expectedErrorText = "int could not be parsed to []string: the given type is not as expected" 165 | albumArtist, err = Metadata{ 166 | "xesam:albumArtist": dbus.MakeVariant(42), 167 | }.XESAMAlbumArtist() 168 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 169 | assert.Equal(t, expectedAlbumArtist, albumArtist, "unexpected albumArtist") 170 | } 171 | 172 | func TestMetadata_XESAMArtist(t *testing.T) { 173 | var expectedArtist []string 174 | var expectedErrorText string 175 | var artist []string 176 | var err error 177 | 178 | //happycase 179 | expectedArtist = []string{"artist1", "artist2"} 180 | expectedErrorText = "" 181 | artist, err = Metadata{ 182 | "xesam:artist": dbus.MakeVariant([]string{"artist1", "artist2"}), 183 | }.XESAMArtist() 184 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 185 | assert.Equal(t, expectedArtist, artist, "unexpected artist") 186 | 187 | //not present 188 | expectedArtist = nil 189 | expectedErrorText = "" 190 | artist, err = Metadata{}.XESAMArtist() 191 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 192 | assert.Equal(t, expectedArtist, artist, "unexpected artist") 193 | 194 | //unexpected type 195 | expectedArtist = nil 196 | expectedErrorText = "int could not be parsed to []string: the given type is not as expected" 197 | artist, err = Metadata{ 198 | "xesam:artist": dbus.MakeVariant(42), 199 | }.XESAMArtist() 200 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 201 | assert.Equal(t, expectedArtist, artist, "unexpected artist") 202 | } 203 | 204 | func TestMetadata_XESAMAsText(t *testing.T) { 205 | var expectedAsText string 206 | var expectedErrorText string 207 | var asText string 208 | var err error 209 | 210 | //happycase 211 | expectedAsText = "asText" 212 | expectedErrorText = "" 213 | asText, err = Metadata{ 214 | "xesam:asText": dbus.MakeVariant("asText"), 215 | }.XESAMAsText() 216 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 217 | assert.Equal(t, expectedAsText, asText, "unexpected asText") 218 | 219 | //not present 220 | expectedAsText = "" 221 | expectedErrorText = "" 222 | asText, err = Metadata{}.XESAMAsText() 223 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 224 | assert.Equal(t, expectedAsText, asText, "unexpected asText") 225 | 226 | //unexpected type 227 | expectedAsText = "" 228 | expectedErrorText = "int could not be parsed to string: the given type is not as expected" 229 | asText, err = Metadata{ 230 | "xesam:asText": dbus.MakeVariant(42), 231 | }.XESAMAsText() 232 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 233 | assert.Equal(t, expectedAsText, asText, "unexpected asText") 234 | } 235 | 236 | func TestMetadata_XESAMAudioBPM(t *testing.T) { 237 | var expectedAudioBPM int 238 | var expectedErrorText string 239 | var audioBPM int 240 | var err error 241 | 242 | //happycase 243 | expectedAudioBPM = 4711 244 | expectedErrorText = "" 245 | audioBPM, err = Metadata{ 246 | "xesam:audioBPM": dbus.MakeVariant(4711), 247 | }.XESAMAudioBPM() 248 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 249 | assert.Equal(t, expectedAudioBPM, audioBPM, "unexpected audioBPM") 250 | 251 | //not present 252 | expectedAudioBPM = 0 253 | expectedErrorText = "" 254 | audioBPM, err = Metadata{}.XESAMAudioBPM() 255 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 256 | assert.Equal(t, expectedAudioBPM, audioBPM, "unexpected audioBPM") 257 | 258 | //unexpected type 259 | expectedAudioBPM = 0 260 | expectedErrorText = "string could not be parsed to int: the given type is not as expected" 261 | audioBPM, err = Metadata{ 262 | "xesam:audioBPM": dbus.MakeVariant("string"), 263 | }.XESAMAudioBPM() 264 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 265 | assert.Equal(t, expectedAudioBPM, audioBPM, "unexpected audioBPM") 266 | } 267 | 268 | func TestMetadata_XESAMAutoRating(t *testing.T) { 269 | var expectedAutoRating float64 270 | var expectedErrorText string 271 | var autoRating float64 272 | var err error 273 | 274 | //happycase 275 | expectedAutoRating = 4711 276 | expectedErrorText = "" 277 | autoRating, err = Metadata{ 278 | "xesam:autoRating": dbus.MakeVariant(float64(4711)), 279 | }.XESAMAutoRating() 280 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 281 | assert.Equal(t, expectedAutoRating, autoRating, "unexpected autoRating") 282 | 283 | //not present 284 | expectedAutoRating = 0 285 | expectedErrorText = "" 286 | autoRating, err = Metadata{}.XESAMAutoRating() 287 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 288 | assert.Equal(t, expectedAutoRating, autoRating, "unexpected autoRating") 289 | 290 | //unexpected type 291 | expectedAutoRating = 0 292 | expectedErrorText = "string could not be parsed to float64: the given type is not as expected" 293 | autoRating, err = Metadata{ 294 | "xesam:autoRating": dbus.MakeVariant("string"), 295 | }.XESAMAutoRating() 296 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 297 | assert.Equal(t, expectedAutoRating, autoRating, "unexpected autoRating") 298 | } 299 | 300 | func TestMetadata_XESAMComment(t *testing.T) { 301 | var expectedComment []string 302 | var expectedErrorText string 303 | var comment []string 304 | var err error 305 | 306 | //happycase 307 | expectedComment = []string{"comment1", "comment2"} 308 | expectedErrorText = "" 309 | comment, err = Metadata{ 310 | "xesam:comment": dbus.MakeVariant([]string{"comment1", "comment2"}), 311 | }.XESAMComment() 312 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 313 | assert.Equal(t, expectedComment, comment, "unexpected comment") 314 | 315 | //not present 316 | expectedComment = nil 317 | expectedErrorText = "" 318 | comment, err = Metadata{}.XESAMComment() 319 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 320 | assert.Equal(t, expectedComment, comment, "unexpected comment") 321 | 322 | //unexpected type 323 | expectedComment = nil 324 | expectedErrorText = "string could not be parsed to []string: the given type is not as expected" 325 | comment, err = Metadata{ 326 | "xesam:comment": dbus.MakeVariant("string"), 327 | }.XESAMComment() 328 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 329 | assert.Equal(t, expectedComment, comment, "unexpected comment") 330 | } 331 | 332 | func TestMetadata_XESAMComposer(t *testing.T) { 333 | var expectedComment []string 334 | var expectedErrorText string 335 | var comment []string 336 | var err error 337 | 338 | //happycase 339 | expectedComment = []string{"comment1", "comment2"} 340 | expectedErrorText = "" 341 | comment, err = Metadata{ 342 | "xesam:composer": dbus.MakeVariant([]string{"comment1", "comment2"}), 343 | }.XESAMComposer() 344 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 345 | assert.Equal(t, expectedComment, comment, "unexpected comment") 346 | 347 | //not present 348 | expectedComment = nil 349 | expectedErrorText = "" 350 | comment, err = Metadata{}.XESAMComposer() 351 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 352 | assert.Equal(t, expectedComment, comment, "unexpected comment") 353 | 354 | //unexpected type 355 | expectedComment = nil 356 | expectedErrorText = "string could not be parsed to []string: the given type is not as expected" 357 | comment, err = Metadata{ 358 | "xesam:composer": dbus.MakeVariant("string"), 359 | }.XESAMComposer() 360 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 361 | assert.Equal(t, expectedComment, comment, "unexpected comment") 362 | } 363 | 364 | func TestMetadata_XESAMContentCreated(t *testing.T) { 365 | var expectedContentCreated time.Time 366 | var expectedErrorText string 367 | var contentCreated time.Time 368 | var err error 369 | 370 | //happycase 371 | expectedContentCreated = time.Date(2007, 4, 29, 13, 56, 0, 0, time.UTC) 372 | expectedErrorText = "" 373 | contentCreated, err = Metadata{ 374 | "xesam:contentCreated": dbus.MakeVariant("2007-04-29T13:56+00:00"), 375 | }.XESAMContentCreated() 376 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 377 | assert.True(t, expectedContentCreated.Equal(contentCreated), "unexpected contentCreated", expectedContentCreated, contentCreated) 378 | 379 | //not present 380 | expectedContentCreated = time.Time{} 381 | expectedErrorText = "" 382 | contentCreated, err = Metadata{}.XESAMContentCreated() 383 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 384 | assert.True(t, expectedContentCreated.Equal(contentCreated), "unexpected contentCreated", expectedContentCreated, contentCreated) 385 | 386 | //unexpected type 387 | expectedContentCreated = time.Time{} 388 | expectedErrorText = "int could not be parsed to string: the given type is not as expected" 389 | contentCreated, err = Metadata{ 390 | "xesam:contentCreated": dbus.MakeVariant(42), 391 | }.XESAMContentCreated() 392 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 393 | assert.True(t, expectedContentCreated.Equal(contentCreated), "unexpected contentCreated", expectedContentCreated, contentCreated) 394 | 395 | //unexpected date format 396 | expectedContentCreated = time.Time{} 397 | expectedErrorText = `cound not parse time: parsing time "not a date-time" as "2006-01-02T15:04-07:00": cannot parse "not a date-time" as "2006": the given type is not as expected` 398 | contentCreated, err = Metadata{ 399 | "xesam:contentCreated": dbus.MakeVariant("not a date-time"), 400 | }.XESAMContentCreated() 401 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 402 | assert.True(t, expectedContentCreated.Equal(contentCreated), "unexpected contentCreated", expectedContentCreated, contentCreated) 403 | } 404 | 405 | func TestMetadata_XESAMDiscNumber(t *testing.T) { 406 | var expectedDiscNumber int 407 | var expectedErrorText string 408 | var discNumber int 409 | var err error 410 | 411 | //happycase 412 | expectedDiscNumber = 42 413 | expectedErrorText = "" 414 | discNumber, err = Metadata{ 415 | "xesam:discNumber": dbus.MakeVariant(42), 416 | }.XESAMDiscNumber() 417 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 418 | assert.Equal(t, expectedDiscNumber, discNumber, "unexpected discNumber") 419 | 420 | //not present 421 | expectedDiscNumber = 0 422 | expectedErrorText = "" 423 | discNumber, err = Metadata{}.XESAMDiscNumber() 424 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 425 | assert.Equal(t, expectedDiscNumber, discNumber, "unexpected discNumber") 426 | 427 | //unexpected type 428 | expectedDiscNumber = 0 429 | expectedErrorText = "string could not be parsed to int: the given type is not as expected" 430 | discNumber, err = Metadata{ 431 | "xesam:discNumber": dbus.MakeVariant("string"), 432 | }.XESAMDiscNumber() 433 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 434 | assert.Equal(t, expectedDiscNumber, discNumber, "unexpected discNumber") 435 | } 436 | 437 | func TestMetadata_XESAMFirstUsed(t *testing.T) { 438 | var expectedFirstUsed time.Time 439 | var expectedErrorText string 440 | var firstUsed time.Time 441 | var err error 442 | 443 | //happycase 444 | expectedFirstUsed = time.Date(2007, 4, 29, 13, 56, 0, 0, time.FixedZone("", 0)) 445 | expectedErrorText = "" 446 | firstUsed, err = Metadata{ 447 | "xesam:firstUsed": dbus.MakeVariant("2007-04-29T13:56+00:00"), 448 | }.XESAMFirstUsed() 449 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 450 | assert.True(t, expectedFirstUsed.Equal(firstUsed), "unexpected firstUsed", expectedFirstUsed, firstUsed) 451 | 452 | //not present 453 | expectedFirstUsed = time.Time{} 454 | expectedErrorText = "" 455 | firstUsed, err = Metadata{}.XESAMFirstUsed() 456 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 457 | assert.True(t, expectedFirstUsed.Equal(firstUsed), "unexpected firstUsed", expectedFirstUsed, firstUsed) 458 | 459 | //unexpected type 460 | expectedFirstUsed = time.Time{} 461 | expectedErrorText = "int could not be parsed to string: the given type is not as expected" 462 | firstUsed, err = Metadata{ 463 | "xesam:firstUsed": dbus.MakeVariant(42), 464 | }.XESAMFirstUsed() 465 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 466 | assert.True(t, expectedFirstUsed.Equal(firstUsed), "unexpected firstUsed", expectedFirstUsed, firstUsed) 467 | 468 | //unexpected date format 469 | expectedFirstUsed = time.Time{} 470 | expectedErrorText = `cound not parse time: parsing time "not a date-time" as "2006-01-02T15:04-07:00": cannot parse "not a date-time" as "2006": the given type is not as expected` 471 | firstUsed, err = Metadata{ 472 | "xesam:firstUsed": dbus.MakeVariant("not a date-time"), 473 | }.XESAMFirstUsed() 474 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 475 | assert.True(t, expectedFirstUsed.Equal(firstUsed), "unexpected firstUsed", expectedFirstUsed, firstUsed) 476 | } 477 | 478 | func TestMetadata_XESAMGenre(t *testing.T) { 479 | var expectedGenre []string 480 | var expectedErrorText string 481 | var genre []string 482 | var err error 483 | 484 | //happycase 485 | expectedGenre = []string{"genre1", "genre2"} 486 | expectedErrorText = "" 487 | genre, err = Metadata{ 488 | "xesam:genre": dbus.MakeVariant([]string{"genre1", "genre2"}), 489 | }.XESAMGenre() 490 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 491 | assert.Equal(t, expectedGenre, genre, "unexpected genre") 492 | 493 | //not present 494 | expectedGenre = nil 495 | expectedErrorText = "" 496 | genre, err = Metadata{}.XESAMGenre() 497 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 498 | assert.Equal(t, expectedGenre, genre, "unexpected genre") 499 | 500 | //unexpected type 501 | expectedGenre = nil 502 | expectedErrorText = "int could not be parsed to []string: the given type is not as expected" 503 | genre, err = Metadata{ 504 | "xesam:genre": dbus.MakeVariant(42), 505 | }.XESAMGenre() 506 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 507 | assert.Equal(t, expectedGenre, genre, "unexpected genre") 508 | } 509 | 510 | func TestMetadata_XESAMLastUsed(t *testing.T) { 511 | var expectedLastUsed time.Time 512 | var expectedErrorText string 513 | var lastUsed time.Time 514 | var err error 515 | 516 | //happycase 517 | expectedLastUsed = time.Date(2007, 4, 29, 13, 56, 0, 0, time.FixedZone("", 0)) 518 | expectedErrorText = "" 519 | lastUsed, err = Metadata{ 520 | "xesam:lastUsed": dbus.MakeVariant("2007-04-29T13:56+00:00"), 521 | }.XESAMLastUsed() 522 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 523 | assert.True(t, expectedLastUsed.Equal(lastUsed), "unexpected lastUsed", expectedLastUsed, lastUsed) 524 | 525 | //not present 526 | expectedLastUsed = time.Time{} 527 | expectedErrorText = "" 528 | lastUsed, err = Metadata{}.XESAMLastUsed() 529 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 530 | assert.True(t, expectedLastUsed.Equal(lastUsed), "unexpected lastUsed", expectedLastUsed, lastUsed) 531 | 532 | //unexpected type 533 | expectedLastUsed = time.Time{} 534 | expectedErrorText = "int could not be parsed to string: the given type is not as expected" 535 | lastUsed, err = Metadata{ 536 | "xesam:lastUsed": dbus.MakeVariant(42), 537 | }.XESAMLastUsed() 538 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 539 | assert.True(t, expectedLastUsed.Equal(lastUsed), "unexpected lastUsed", expectedLastUsed, lastUsed) 540 | 541 | //unexpected date format 542 | expectedLastUsed = time.Time{} 543 | expectedErrorText = `cound not parse time: parsing time "not a date-time" as "2006-01-02T15:04-07:00": cannot parse "not a date-time" as "2006": the given type is not as expected` 544 | lastUsed, err = Metadata{ 545 | "xesam:lastUsed": dbus.MakeVariant("not a date-time"), 546 | }.XESAMLastUsed() 547 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 548 | assert.True(t, expectedLastUsed.Equal(lastUsed), "unexpected lastUsed", expectedLastUsed, lastUsed) 549 | } 550 | 551 | func TestMetadata_XESAMLyricist(t *testing.T) { 552 | var expectedLyricist []string 553 | var expectedErrorText string 554 | var lyricist []string 555 | var err error 556 | 557 | //happycase 558 | expectedLyricist = []string{"genre1", "genre2"} 559 | expectedErrorText = "" 560 | lyricist, err = Metadata{ 561 | "xesam:lyricist": dbus.MakeVariant([]string{"genre1", "genre2"}), 562 | }.XESAMLyricist() 563 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 564 | assert.Equal(t, expectedLyricist, lyricist, "unexpected lyricist") 565 | 566 | //not present 567 | expectedLyricist = nil 568 | expectedErrorText = "" 569 | lyricist, err = Metadata{}.XESAMLyricist() 570 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 571 | assert.Equal(t, expectedLyricist, lyricist, "unexpected lyricist") 572 | 573 | //unexpected type 574 | expectedLyricist = nil 575 | expectedErrorText = "int could not be parsed to []string: the given type is not as expected" 576 | lyricist, err = Metadata{ 577 | "xesam:lyricist": dbus.MakeVariant(42), 578 | }.XESAMLyricist() 579 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 580 | assert.Equal(t, expectedLyricist, lyricist, "unexpected lyricist") 581 | } 582 | 583 | func TestMetadata_XESAMTitle(t *testing.T) { 584 | var expectedTitle string 585 | var expectedErrorText string 586 | var title string 587 | var err error 588 | 589 | //happycase 590 | expectedTitle = "title" 591 | expectedErrorText = "" 592 | title, err = Metadata{ 593 | "xesam:title": dbus.MakeVariant("title"), 594 | }.XESAMTitle() 595 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 596 | assert.Equal(t, expectedTitle, title, "unexpected title") 597 | 598 | //not present 599 | expectedTitle = "" 600 | expectedErrorText = "" 601 | title, err = Metadata{}.XESAMTitle() 602 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 603 | assert.Equal(t, expectedTitle, title, "unexpected title") 604 | 605 | //unexpected type 606 | expectedTitle = "" 607 | expectedErrorText = "int could not be parsed to string: the given type is not as expected" 608 | title, err = Metadata{ 609 | "xesam:title": dbus.MakeVariant(42), 610 | }.XESAMTitle() 611 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 612 | assert.Equal(t, expectedTitle, title, "unexpected title") 613 | } 614 | 615 | func TestMetadata_XESAMTrackNumber(t *testing.T) { 616 | var expectedTrackNumber int 617 | var expectedErrorText string 618 | var trackNumber int 619 | var err error 620 | 621 | //happycase 622 | expectedTrackNumber = 42 623 | expectedErrorText = "" 624 | trackNumber, err = Metadata{ 625 | "xesam:trackNumber": dbus.MakeVariant(42), 626 | }.XESAMTrackNumber() 627 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 628 | assert.Equal(t, expectedTrackNumber, trackNumber, "unexpected trackNumber") 629 | 630 | //not present 631 | expectedTrackNumber = 0 632 | expectedErrorText = "" 633 | trackNumber, err = Metadata{}.XESAMTrackNumber() 634 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 635 | assert.Equal(t, expectedTrackNumber, trackNumber, "unexpected trackNumber") 636 | 637 | //unexpected type 638 | expectedTrackNumber = 0 639 | expectedErrorText = "string could not be parsed to int: the given type is not as expected" 640 | trackNumber, err = Metadata{ 641 | "xesam:trackNumber": dbus.MakeVariant("string"), 642 | }.XESAMTrackNumber() 643 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 644 | assert.Equal(t, expectedTrackNumber, trackNumber, "unexpected trackNumber") 645 | } 646 | 647 | func TestMetadata_XESAMURL(t *testing.T) { 648 | var expectedURL string 649 | var expectedErrorText string 650 | var url string 651 | var err error 652 | 653 | //happycase 654 | expectedURL = "url" 655 | expectedErrorText = "" 656 | url, err = Metadata{ 657 | "xesam:url": dbus.MakeVariant("url"), 658 | }.XESAMURL() 659 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 660 | assert.Equal(t, expectedURL, url, "unexpected url") 661 | 662 | //not present 663 | expectedURL = "" 664 | expectedErrorText = "" 665 | url, err = Metadata{}.XESAMURL() 666 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 667 | assert.Equal(t, expectedURL, url, "unexpected url") 668 | 669 | //unexpected type 670 | expectedURL = "" 671 | expectedErrorText = "int could not be parsed to string: the given type is not as expected" 672 | url, err = Metadata{ 673 | "xesam:url": dbus.MakeVariant(42), 674 | }.XESAMURL() 675 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 676 | assert.Equal(t, expectedURL, url, "unexpected url") 677 | } 678 | 679 | func TestMetadata_XESAMUseCount(t *testing.T) { 680 | var expectedUseCount int 681 | var expectedErrorText string 682 | var useCount int 683 | var err error 684 | 685 | //happycase 686 | expectedUseCount = 42 687 | expectedErrorText = "" 688 | useCount, err = Metadata{ 689 | "xesam:useCount": dbus.MakeVariant(42), 690 | }.XESAMUseCount() 691 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 692 | assert.Equal(t, expectedUseCount, useCount, "unexpected useCount") 693 | 694 | //not present 695 | expectedUseCount = 0 696 | expectedErrorText = "" 697 | useCount, err = Metadata{}.XESAMUseCount() 698 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 699 | assert.Equal(t, expectedUseCount, useCount, "unexpected useCount") 700 | 701 | //unexpected type 702 | expectedUseCount = 0 703 | expectedErrorText = "string could not be parsed to int: the given type is not as expected" 704 | useCount, err = Metadata{ 705 | "xesam:useCount": dbus.MakeVariant("string"), 706 | }.XESAMUseCount() 707 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 708 | assert.Equal(t, expectedUseCount, useCount, "unexpected useCount") 709 | } 710 | 711 | func TestMetadata_XESAMUserRating(t *testing.T) { 712 | var expectedUserRating float64 713 | var expectedErrorText string 714 | var userRating float64 715 | var err error 716 | 717 | //happycase 718 | expectedUserRating = 4711 719 | expectedErrorText = "" 720 | userRating, err = Metadata{ 721 | "xesam:userRating": dbus.MakeVariant(float64(4711)), 722 | }.XESAMUserRating() 723 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 724 | assert.Equal(t, expectedUserRating, userRating, "unexpected userRating") 725 | 726 | //not present 727 | expectedUserRating = 0 728 | expectedErrorText = "" 729 | userRating, err = Metadata{}.XESAMUserRating() 730 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 731 | assert.Equal(t, expectedUserRating, userRating, "unexpected userRating") 732 | 733 | //unexpected type 734 | expectedUserRating = 0 735 | expectedErrorText = "string could not be parsed to float64: the given type is not as expected" 736 | userRating, err = Metadata{ 737 | "xesam:userRating": dbus.MakeVariant("string"), 738 | }.XESAMUserRating() 739 | assert.Equal(t, expectedErrorText, fmt.Sprint(err), "unexpected error text") 740 | assert.Equal(t, expectedUserRating, userRating, "unexpected userRating") 741 | } 742 | 743 | func TestMetadata_Find(t *testing.T) { 744 | expectedValue := dbus.MakeVariant("123456") 745 | 746 | value, found := Metadata{ 747 | "myKey": dbus.MakeVariant("123456"), 748 | }.Find("myKey") 749 | assert.True(t, found) 750 | assert.Equal(t, expectedValue, value) 751 | 752 | } 753 | --------------------------------------------------------------------------------