Skip to content

Commit 8cbe5d8

Browse files
authored
Merge pull request #18 from b0bbywan/scheduler
add mpd scheduler
2 parents 42f05c5 + 8042e79 commit 8cbe5d8

File tree

6 files changed

+158
-12
lines changed

6 files changed

+158
-12
lines changed

README.md

+34-8
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ PulseServer: ""
146146
MountConfig: "mpd"
147147
MPDCueSubfolder: ".disc-cuer"
148148
MPDUSBSubfolder: ".udisks"
149+
Schedule: {}
149150

150151
```
151152
@@ -159,13 +160,7 @@ The type of connection to use. Supported values:
159160
160161
- **Address**:
161162
- For Type: `"unix"`, this is the path to the MPD socket file (e.g., `/var/run/mpd/socket` ) *(recommended)*.
162-
- For Type: `"tcp"`, this is the <hostname>:<port> of the MPD server (e.g., `127.0.0.1:6600`) *(default)*. *(Even though remote MPD server are supported, they won't work without additional setup not covered in this documentation)*
163-
164-
165-
#### Notifications Options
166-
- **AudioBackend**: `"pulse"` *(default)*, `"alsa"` or `"none` (disable notifications).
167-
- **PulseServer**: Check [Pulseaudio Server String doc](https://www.freedesktop.org/wiki/Software/PulseAudio/Documentation/User/ServerStrings/)
168-
- **SoundsLocation**: `"/usr/local/share/mpd-discplayer"` *(default)*. No default sounds are provided at the moment. Notifications expect `in.mp3`, `out.mp3` and `error.mp3` to be present in the specified folder or notifications will be disabled.
163+
- For Type: `"tcp"`, this is the <hostname>:<port> of the MPD server (e.g., `127.0.0.1:6600`) *(default)*.
169164

170165
#### Mouting Options
171166
For USB stick support, the content of the stick must be made available in MPD database. MPD-Discplayer supports the native mpd mouting feature, or symlinks for MPD servers that do not support this feature.
@@ -175,6 +170,36 @@ For USB stick support, the content of the stick must be made available in MPD da
175170
- **MPDLibraryFolder**: path to MPD music_directory *(self discovered when using MPD unix socket)*
176171
- **MPDUSBSubfolder**: path inside `MPDLibraryFolder` to store symlinks to usb original mountpoints. Only with `MountConfig: "symlink"`, not used with `MountConfig: "mpd"`
177172

173+
#### Schedule Option
174+
The Schedule option allows you to automate playback of MPD-compatible URIs based on a cron schedule. It currently supports the following:
175+
- **Webradio** URIs (e.g., HTTP streams).
176+
- **Audio CDs** (using cdda:// protocol).
177+
- **USB devices** (using mount points or symlinks).
178+
- **MPD Database** (use `mpc listall` to list MPD Database)
179+
180+
Incompatible cron will be discarded and logged on startup. A notification will be trigerred before starting the scheduled playback and if an error happens while loading the uri.
181+
182+
Example configuration:
183+
184+
```yaml
185+
Schedule:
186+
# Play a reggae radio stream on weekdays at 6:30 AM
187+
"30 6 * * 1-5": "//hd.lagrosseradio.info/lagrosseradio-reggae-192.mp3"
188+
# Play an audio CD on Saturdays at 9:00 AM
189+
"0 9 * * 6": "cdda://"
190+
# Play from a USB device (symlink mount) on Sundays at 9:00 PM
191+
"0 21 * * 7": ".udisks/{usb_label}"
192+
# Play from an MPD-mounted USB device on Sundays at 9:00 PM
193+
"0 21 * * 7": "{usb_label}"
194+
195+
```
196+
Note: Ensure that the `usb_label` matches the label of the USB device. For audio CDs, only `cdda://` protocol is supported for now.
197+
198+
#### Notifications Options
199+
- **AudioBackend**: `"pulse"` *(default)*, `"alsa"` or `"none` (disable notifications).
200+
- **PulseServer**: Check [Pulseaudio Server String doc](https://www.freedesktop.org/wiki/Software/PulseAudio/Documentation/User/ServerStrings/)
201+
- **SoundsLocation**: `"/usr/local/share/mpd-discplayer"` *(default)*. No default sounds are provided at the moment. Notifications expect `in.mp3`, `out.mp3` and `error.mp3` to be present in the specified folder or notifications will be disabled.
202+
178203
### Environment Variables
179204

180205
If a configuration file is not provided, you can use environment variables to set the same options. Below is the list of supported variables and their defaults (if applicable):
@@ -192,7 +217,8 @@ If a configuration file is not provided, you can use environment variables to se
192217
| `MPD_DISCPLAYER_AUDIOBACKEND` | `AudioBackend` | `pulse` |
193218
| `MPD_DISCPLAYER_PULSESERVER` | `PulseServer` | *(Default to `""`, e.g. local pulseaudio unix socket)* | `MPD_DISCPLAYER_MOUNTCONFIG` | `MountConfig` | `mpd`
194219
| `MPD_DISCPLAYER_MPDCUESUBFOLDER` | `MPDCueSubfolder` | `.disc-cuer` |
195-
| `MPD_DISCPLAYER_MPDUSBSUBFOLDER` | `MPDUSBSubfolder` | `.udisks`
220+
| `MPD_DISCPLAYER_MPDUSBSUBFOLDER` | `MPDUSBSubfolder` | `.udisks` |
221+
| *(Unsupported)* | `Schedule` | *{} (empty, disables scheduling)* |
196222

197223
#### Priority of Configuration
198224
The configuration is loaded in the following order of priority:

cmd/player.go

+12-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import (
2222

2323
const (
2424
AppName = "mpd-discplayer"
25-
AppVersion = "0.5"
25+
AppVersion = "0.6"
2626
defaultMpdFolder = "/var/lib/mpd/music"
2727
)
2828

@@ -35,6 +35,7 @@ type Player struct {
3535
Notifier *notifications.Notifier
3636
Mounter *mounts.MountManager
3737
handlers []*hwcontrol.EventHandler
38+
scheduler *scheduler
3839
}
3940

4041
func NewPlayer() (*Player, error) {
@@ -49,6 +50,7 @@ func NewPlayer() (*Player, error) {
4950
viper.SetDefault("AudioBackend", "pulse")
5051
viper.SetDefault("PulseServer", "")
5152
viper.SetDefault("MountConfig", "mpd")
53+
viper.SetDefault("Schedule", make(map[string]string))
5254

5355
// Load from configuration file, environment variables, and CLI flags
5456
viper.SetConfigName("config") // name of config file (without extension)
@@ -112,6 +114,9 @@ func NewPlayer() (*Player, error) {
112114
)
113115
notifier := notifications.NewNotifier(notificationConfig)
114116

117+
schedules := newSchedulerUris(mpdClient, notifier, viper.GetStringMapString("Schedule"))
118+
scheduler := newScheduler(schedules)
119+
115120
return &Player{
116121
ctx: ctx,
117122
cancel: cancel,
@@ -120,10 +125,13 @@ func NewPlayer() (*Player, error) {
120125
Client: mpdClient,
121126
Notifier: notifier,
122127
Mounter: mounter,
128+
scheduler: scheduler,
123129
}, nil
124130
}
125131

126132
func (p *Player) Start() {
133+
p.StartScheduler()
134+
127135
// Create event handlers (subscribers) passing the context
128136
p.newDiscHandlers()
129137
p.newUSBHandlers()
@@ -179,6 +187,9 @@ func (p *Player) Close() {
179187
if p.Notifier != nil {
180188
p.Notifier.Close()
181189
}
190+
if p.scheduler != nil {
191+
p.scheduler.Close()
192+
}
182193
p.wg.Wait()
183194
}
184195

cmd/scheduler.go

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package cmd
2+
3+
import (
4+
"log"
5+
6+
"github.com/robfig/cron/v3"
7+
8+
"github.com/b0bbywan/go-mpd-discplayer/mpdplayer"
9+
"github.com/b0bbywan/go-mpd-discplayer/notifications"
10+
)
11+
12+
type scheduler struct {
13+
c *cron.Cron
14+
schedule []*ScheduleUri
15+
}
16+
17+
type ScheduleUri struct {
18+
schedule string
19+
uri string
20+
callback func()
21+
jobId cron.EntryID
22+
}
23+
24+
func newScheduler(schedulers []*ScheduleUri) *scheduler {
25+
if len(schedulers) == 0 {
26+
return nil
27+
}
28+
29+
c := cron.New()
30+
for _, v := range schedulers {
31+
jobId, err := c.AddFunc(v.schedule, v.callback)
32+
if err != nil {
33+
log.Printf("Failed to add %s cron, check syntax: %v", v.schedule, err)
34+
}
35+
v.jobId = jobId
36+
log.Printf("Added schedule: cron='%s' uri='%s'", v.schedule, v.uri)
37+
}
38+
s := &scheduler{
39+
c: c,
40+
schedule: schedulers,
41+
}
42+
return s
43+
}
44+
45+
func (s *scheduler) Close() {
46+
if s != nil {
47+
s.c.Stop()
48+
s.c = nil
49+
s.schedule = nil
50+
}
51+
}
52+
53+
func (p *Player) StopScheduler() {
54+
if p.scheduler != nil {
55+
p.scheduler.c.Stop()
56+
}
57+
}
58+
59+
func (p *Player) StartScheduler() {
60+
if p.scheduler != nil {
61+
p.scheduler.c.Start()
62+
}
63+
}
64+
65+
func newSchedulerUris(
66+
mpdClient *mpdplayer.ReconnectingMPDClient,
67+
notifier *notifications.Notifier,
68+
schedules map[string]string,
69+
) []*ScheduleUri {
70+
var schedulers []*ScheduleUri
71+
for k, v := range schedules {
72+
schedulers = append(schedulers, newSchedulerUri(mpdClient, notifier, k, v))
73+
}
74+
return schedulers
75+
}
76+
77+
func newSchedulerUri(
78+
mpdClient *mpdplayer.ReconnectingMPDClient,
79+
notifier *notifications.Notifier,
80+
schedule, uri string,
81+
) *ScheduleUri {
82+
callback := func() {
83+
if notifier != nil {
84+
notifier.PlayEvent(notifications.EventAdd)
85+
}
86+
if err := mpdClient.StartPlayback(uri); err != nil {
87+
if notifier != nil {
88+
notifier.PlayError()
89+
}
90+
log.Printf("Failed to play %s: %v", uri, err)
91+
}
92+
}
93+
return &ScheduleUri{
94+
schedule: schedule,
95+
uri: uri,
96+
callback: callback,
97+
}
98+
}

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ require (
99
github.com/hajimehoshi/oto/v2 v2.4.2
1010
github.com/jfreymuth/pulse v0.1.1
1111
github.com/jochenvg/go-udev v0.0.0-20240801134859-b65ed646224b
12+
github.com/robfig/cron/v3 v3.0.0
1213
github.com/spf13/viper v1.19.0
1314
golang.org/x/sys v0.27.0
1415
)

go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6
3434
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
3535
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
3636
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
37+
github.com/robfig/cron/v3 v3.0.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E=
38+
github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
3739
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
3840
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
3941
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=

mpdplayer/operations.go

+11-3
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ func (rc *ReconnectingMPDClient) StartUSBPlayback(device string) error {
2424
return rc.startPlayback(addUSBToQueue, device)
2525
}
2626

27+
func (rc *ReconnectingMPDClient) StartPlayback(uri string) error {
28+
return rc.startPlayback(addUri, uri)
29+
}
30+
2731
// StartDiscPlayback now accepts a custom playback function
2832
func (rc *ReconnectingMPDClient) startPlayback(playbackFunc PlaybackAction, device string) error {
2933
return rc.execute(func(client *mpd.Client) error {
@@ -158,7 +162,7 @@ func loadCue(client *mpd.Client, cuerConfig *config.Config, device string) error
158162
// addTracks adds individual CDDA tracks to the MPD playlist based on the specified track count.
159163
func addTracks(client *mpd.Client, trackCount int) error {
160164
for track := 1; track <= trackCount; track++ {
161-
if err := client.Add(fmt.Sprintf("%s/%d", CDDAPathPrefix, track)); err != nil {
165+
if err := addUri(client, fmt.Sprintf("%s/%d", CDDAPathPrefix, track)); err != nil {
162166
return fmt.Errorf("failed to add track %d: %w", track, err)
163167
}
164168
}
@@ -193,8 +197,12 @@ func addUSBToQueue(client *mpd.Client, label string) error {
193197
return fmt.Errorf("Database update failed: %w", err)
194198
}
195199
log.Printf("Adding %s files to queue...", label)
196-
if err := client.Add(label); err != nil {
197-
return fmt.Errorf("failed to add files from %s: %w", label, err)
200+
return addUri(client, label)
201+
}
202+
203+
func addUri(client *mpd.Client, uri string) error {
204+
if err := client.Add(uri); err != nil {
205+
return fmt.Errorf("failed to add uri %s: %w", uri, err)
198206
}
199207
return nil
200208
}

0 commit comments

Comments
 (0)