all messages for Guix-related lists mirrored at yhetil.org
 help / color / mirror / code / Atom feed
* [bug#59866] [PATCH 0/2] services: mpd: Refactor MPD service
@ 2022-12-06 23:22 mirai
  2022-12-06 23:25 ` [bug#59866] [PATCH 1/2] services: mpd: use 'define-configuration' mirai
                   ` (11 more replies)
  0 siblings, 12 replies; 29+ messages in thread
From: mirai @ 2022-12-06 23:22 UTC (permalink / raw)
  To: 59866; +Cc: mirai

Modernizes and expands 'mpd-service-type'.
Performs "pretty-formatting" to the generated configuration file
at the cost of a few extra procedures.
It also deprecates some of the fields (abbreviated forms).

Bruno Victal (2):
  services: mpd: use 'define-configuration'.
  services: mpd: Refactor MPD service.

 doc/guix.texi          | 172 +++++++++++--
 gnu/services/audio.scm | 549 ++++++++++++++++++++++++++++++-----------
 2 files changed, 556 insertions(+), 165 deletions(-)


base-commit: b94724e8b2102be0fe9d19e9dfe44d6f7101bd4b
-- 
2.38.1





^ permalink raw reply	[flat|nested] 29+ messages in thread

* [bug#59866] [PATCH 1/2] services: mpd: use 'define-configuration'.
  2022-12-06 23:22 [bug#59866] [PATCH 0/2] services: mpd: Refactor MPD service mirai
@ 2022-12-06 23:25 ` mirai
  2022-12-06 23:25 ` [bug#59866] [PATCH 2/2] services: mpd: Refactor MPD service mirai
                   ` (10 subsequent siblings)
  11 siblings, 0 replies; 29+ messages in thread
From: mirai @ 2022-12-06 23:25 UTC (permalink / raw)
  To: 59866; +Cc: Bruno Victal

From: Bruno Victal <mirai@makinata.eu>

---
 gnu/services/audio.scm | 217 ++++++++++++++++++++++++-----------------
 1 file changed, 129 insertions(+), 88 deletions(-)

diff --git a/gnu/services/audio.scm b/gnu/services/audio.scm
index c60053f33c..2351db8a4a 100644
--- a/gnu/services/audio.scm
+++ b/gnu/services/audio.scm
@@ -21,6 +21,7 @@
 (define-module (gnu services audio)
   #:use-module (guix gexp)
   #:use-module (gnu services)
+  #:use-module (gnu services configuration)
   #:use-module (gnu services shepherd)
   #:use-module (gnu system shadow)
   #:use-module (gnu packages admin)
@@ -28,6 +29,8 @@ (define-module (gnu services audio)
   #:use-module (guix records)
   #:use-module (ice-9 match)
   #:use-module (ice-9 format)
+  #:use-module (srfi srfi-1)
+  #:use-module (srfi srfi-26)
   #:export (mpd-output
             mpd-output?
             mpd-configuration
@@ -40,93 +43,131 @@ (define-module (gnu services audio)
 ;;;
 ;;; Code:
 
-(define-record-type* <mpd-output>
-  mpd-output make-mpd-output
-  mpd-output?
-  (type          mpd-output-type
-                 (default "pulse"))
-  (name          mpd-output-name
-                 (default "MPD"))
-  (enabled?      mpd-output-enabled?
-                 (default #t))
-  (tags?         mpd-output-tags?
-                 (default #t))
-  (always-on?    mpd-output-always-on?
-                 (default #f))
-  (mixer-type    mpd-output-mixer-type
-                 ;; valid: hardware, software, null, none
-                 (default #f))
-  (extra-options mpd-output-extra-options
-                 (default '())))
-
-(define-record-type* <mpd-configuration>
-  mpd-configuration make-mpd-configuration
-  mpd-configuration?
-  (user         mpd-configuration-user
-                (default "mpd"))
-  (music-dir    mpd-configuration-music-dir
-                (default "~/Music"))
-  (playlist-dir mpd-configuration-playlist-dir
-                (default "~/.mpd/playlists"))
-  (db-file      mpd-configuration-db-file
-                (default "~/.mpd/tag_cache"))
-  (state-file   mpd-configuration-state-file
-                (default "~/.mpd/state"))
-  (sticker-file mpd-configuration-sticker-file
-                (default "~/.mpd/sticker.sql"))
-  (port         mpd-configuration-port
-                (default "6600"))
-  (address      mpd-configuration-address
-                (default "any"))
-  (outputs      mpd-configuration-outputs
-                (default (list (mpd-output)))))
-
-(define (mpd-output->string output)
-  "Convert the OUTPUT of type <mpd-output> to a configuration file snippet."
-  (let ((extra (string-join
-                (map (match-lambda
-                       ((key . value)
-                        (format #f "  ~a \"~a\""
-                                (string-map
-                                 (lambda (c) (if (char=? c #\-) #\_ c))
-                                 (symbol->string key))
-                                value)))
-                     (mpd-output-extra-options output))
-                "\n")))
-    (format #f "\
-audio_output {
-  type \"~a\"
-  name \"~a\"
-~:[  enabled \"no\"~%~;~]\
-~:[  tags \"no\"~%~;~]\
-~:[~;  always_on \"yes\"~%~]\
-~@[  mixer_type \"~a\"~%~]\
-~a~%}~%"
-            (mpd-output-type output)
-            (mpd-output-name output)
-            (mpd-output-enabled? output)
-            (mpd-output-tags? output)
-            (mpd-output-always-on? output)
-            (mpd-output-mixer-type output)
-            extra)))
-
-(define (mpd-config->file config)
-  (apply
-   mixed-text-file "mpd.conf"
-   "pid_file \"" (mpd-file-name config "pid") "\"\n"
-   (append (map mpd-output->string
-                (mpd-configuration-outputs config))
-           (map (match-lambda
-                  ((config-name config-val)
-                   (string-append config-name " \"" (config-val config) "\"\n")))
-                `(("user" ,mpd-configuration-user)
-                  ("music_directory" ,mpd-configuration-music-dir)
-                  ("playlist_directory" ,mpd-configuration-playlist-dir)
-                  ("db_file" ,mpd-configuration-db-file)
-                  ("state_file" ,mpd-configuration-state-file)
-                  ("sticker_file" ,mpd-configuration-sticker-file)
-                  ("port" ,mpd-configuration-port)
-                  ("bind_to_address" ,mpd-configuration-address))))))
+(define (uglify-field-name field-name)
+  (let ((str (symbol->string field-name)))
+    (string-join (string-split (if (string-suffix? "?" str)
+                                   (string-drop-right str 1)
+                                   str)
+                               #\-)
+                 "_")))
+
+(define (free-form-args? val)
+  (match val
+    (() #t)
+    ((((? symbol?) . (? string?)) . val) (free-form-args? val))
+    (_ #f)))
+
+(define* (mpd-serialize-field field-name value #:optional (indent-level 0))
+  #~(begin
+      (use-modules ((ice-9 format)))
+      (format #f "~v/~a \"~a\"~%" #$indent-level #$(if (string? field-name)
+                                                       field-name
+                                                       (uglify-field-name field-name)) #$value)))
+
+(define* (mpd-serialize-free-form-args field-name value #:optional (indent-level 0))
+  (generic-serialize-alist string-append (cut mpd-serialize-field <> <> indent-level) value))
+
+(define mpd-serialize-number mpd-serialize-field)
+
+(define mpd-serialize-string mpd-serialize-field)
+
+(define* (mpd-serialize-boolean field-name value #:optional (indent-level 0))
+  (mpd-serialize-field field-name (if value "yes" "no") indent-level))
+
+(define (mpd-serialize-list-of-mpd-output field-name value)
+  #~(string-append "\naudio_output {\n"
+                   #$@(map (cut serialize-configuration <>
+                                mpd-output-fields)
+                           value)
+                   "}\n"))
+
+(define (mpd-serialize-configuration configuration)
+  (mixed-text-file
+   "mpd.conf"
+   (serialize-configuration configuration mpd-configuration-fields)))
+
+(define mpd-subsystem-serialize-field (cut mpd-serialize-field <> <> 1))
+(define mpd-subsystem-serialize-string (cut mpd-serialize-string <> <> 1))
+(define mpd-subsystem-serialize-number (cut mpd-serialize-number <> <> 1))
+(define mpd-subsystem-serialize-boolean (cut mpd-serialize-boolean <> <> 1))
+(define mpd-subsystem-serialize-free-form-args (cut mpd-serialize-free-form-args <> <> 1))
+
+(define-configuration mpd-output
+  (name
+   (string "MPD")
+   "The name of the audio output.")
+  (type
+   (string "pulse")
+   "The type of audio output.")
+  (enabled?
+   (boolean #t)
+   "Specifies whether this audio output is enabled when MPD is started. By
+default, all audio outputs are enabled. This is just the default
+setting when there is no state file; with a state file, the previous
+state is restored.")
+  (tags?
+   (boolean #t)
+   "If set to @code{#f}, then MPD will not send tags to this output. This
+is only useful for output plugins that can receive tags, for example the
+@code{httpd} output plugin.")
+  (always-on?
+   (boolean #f)
+   "If set to @code{#t}, then MPD attempts to keep this audio output always
+open. This may be useful for streaming servers, when you don’t want to
+disconnect all listeners even when playback is accidentally stopped.")
+  (mixer-type
+   (string "none")
+   "This field accepts a symbol that specifies which mixer should be used
+for this audio output: the @code{hardware} mixer, the @code{software}
+mixer, the @code{null} mixer (allows setting the volume, but with no
+effect; this can be used as a trick to implement an external mixer
+External Mixer) or no mixer (@code{none}).")
+  (extra-options
+   (free-form-args '())
+   "An association list of option symbols to string values to be appended to
+the audio output configuration.")
+  (prefix mpd-subsystem-))
+
+(define list-of-mpd-output?
+  (list-of mpd-output?))
+
+(define-configuration mpd-configuration
+  (user
+   (string "mpd")
+   "The user to run mpd as.")
+  (music-dir
+   (string "~/Music")
+   "The directory to scan for music files."
+   (lambda (_ x)
+     (mpd-serialize-field "music_directory" x)))
+  (playlist-dir
+   (string "~/.mpd/playlists")
+   "The directory to store playlists."
+   (lambda (_ x)
+     (mpd-serialize-field "playlist_directory" x)))
+  (db-file
+   (string "~/.mpd/tag_cache")
+   "The location of the music database.")
+  (state-file
+   (string "~/.mpd/state")
+   "The location of the file that stores current MPD's state.")
+  (sticker-file
+   (string "~/.mpd/sticker.sql")
+   "The location of the sticker database.")
+  (port
+   (string "6600")
+   "The port to run mpd on.")
+  (address
+   (string "any")
+   "The address that mpd will bind to.
+To use a Unix domain socket, an absolute path can be specified here."
+   (lambda (_ x)
+     (mpd-serialize-field "bind_to_address" x)))
+  (outputs
+   (list-of-mpd-output (list (mpd-output)))
+   "The audio outputs that MPD can use.
+By default this is a single output using pulseaudio.")
+  (prefix mpd-))
 
 (define (mpd-file-name config file)
   "Return a path in /var/run/mpd/ that is writable
@@ -143,7 +184,7 @@ (define (mpd-shepherd-service config)
    (start #~(make-forkexec-constructor
              (list #$(file-append mpd "/bin/mpd")
                    "--no-daemon"
-                   #$(mpd-config->file config))
+                   #$(mpd-serialize-configuration config))
              #:environment-variables
              ;; Required to detect PulseAudio when run under a user account.
              (list (string-append
-- 
2.38.1





^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [bug#59866] [PATCH 2/2] services: mpd: Refactor MPD service.
  2022-12-06 23:22 [bug#59866] [PATCH 0/2] services: mpd: Refactor MPD service mirai
  2022-12-06 23:25 ` [bug#59866] [PATCH 1/2] services: mpd: use 'define-configuration' mirai
@ 2022-12-06 23:25 ` mirai
  2022-12-07  8:59 ` [bug#59866] [PATCH 0/2] " ( via Guix-patches via
                   ` (9 subsequent siblings)
  11 siblings, 0 replies; 29+ messages in thread
From: mirai @ 2022-12-06 23:25 UTC (permalink / raw)
  To: 59866; +Cc: Bruno Victal

From: Bruno Victal <mirai@makinata.eu>

Introduces 'mpd-plugin' and 'mpd-partition' records.
Expands 'mpd-output' and 'mpd-configuration' records.
Deprecates redundant abbreviated fields in 'mpd-configuration' and
avoids serializing unused fields that may introduce undesired behavior.
Implement log-rotation via rottlog.
Implements Shepherd actions: 'reload' and 'configuration'.
---
 doc/guix.texi          | 172 ++++++++++++++---
 gnu/services/audio.scm | 422 +++++++++++++++++++++++++++++++----------
 2 files changed, 472 insertions(+), 122 deletions(-)

diff --git a/doc/guix.texi b/doc/guix.texi
index a79b777826..0408cab1cb 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -109,6 +109,7 @@ Copyright @copyright{} 2022 Reily Siegel@*
 Copyright @copyright{} 2022 Simon Streit@*
 Copyright @copyright{} 2022 (@*
 Copyright @copyright{} 2022 John Kehayias@*
+Copyright @copyright{} 2022 Bruno Victal@*
 
 Permission is granted to copy, distribute and/or modify this document
 under the terms of the GNU Free Documentation License, Version 1.3 or
@@ -32823,79 +32824,184 @@ The service type for @command{mpd}
 Data type representing the configuration of @command{mpd}.
 
 @table @asis
-@item @code{user} (default: @code{"mpd"})
+@item @code{package} (default: @code{mpd}) (type: file-like)
+The MPD package.
+
+@item @code{user} (default: @code{"mpd"}) (type: string)
 The user to run mpd as.
 
-@item @code{music-dir} (default: @code{"~/Music"})
+@item @code{group} (type: maybe-string)
+The group to run mpd as.
+
+@item @code{shepherd-requirement} (default: @code{()}) (type: list-of-symbol)
+This is a list of symbols naming Shepherd services that this service
+will depend on.
+
+@item @code{log-file} (default: @code{"/var/log/mpd/log"}) (type: maybe-string)
+The location of the log file.  Set to @code{syslog} to use the local
+syslog daemon or @code{%unset-value} to omit this directive from the
+configuration file.
+
+@item @code{log-level} (type: maybe-string)
+Supress any messages below this threshold.  Available values:
+@code{notice}, @code{info}, @code{verbose}, @code{warning} and
+@code{error}.
+
+@item @code{music-directory} (type: maybe-string)
 The directory to scan for music files.
 
-@item @code{playlist-dir} (default: @code{"~/.mpd/playlists"})
+@item @code{playlist-directory} (type: maybe-string)
 The directory to store playlists.
 
-@item @code{db-file} (default: @code{"~/.mpd/tag_cache"})
+@item @code{db-file} (type: maybe-string)
 The location of the music database.
 
-@item @code{state-file} (default: @code{"~/.mpd/state"})
+@item @code{state-file} (type: maybe-string)
 The location of the file that stores current MPD's state.
 
-@item @code{sticker-file} (default: @code{"~/.mpd/sticker.sql"})
+@item @code{sticker-file} (type: maybe-string)
 The location of the sticker database.
 
-@item @code{port} (default: @code{"6600"})
+@item @code{port} (default: @code{6600}) (type: maybe-integer)
 The port to run mpd on.
 
-@item @code{address} (default: @code{"any"})
-The address that mpd will bind to.  To use a Unix domain socket,
-an absolute path can be specified here.
+@item @code{addresses} (type: maybe-list-of-string)
+The addresses that mpd will bind to.  To use a Unix domain socket, an
+absolute path can be specified here.
+
+@item @code{database} (type: maybe-mpd-plugin)
+MPD database plugin configuration.
+
+@item @code{partitions} (default: @code{()}) (type: list-of-mpd-partition)
+List of MPD "partitions".
+
+@item @code{neighbors} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD neighbor plugin configurations.
 
-@item @code{outputs} (default: @code{"(list (mpd-output))"})
-The audio outputs that MPD can use.  By default this is a single output using pulseaudio.
+@item @code{inputs} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD input plugin configurations.
+
+@item @code{archive-plugins} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD archive plugin configurations.
+
+@item @code{input-cache-size} (type: maybe-string)
+MPD input cache size.
+
+@item @code{decoders} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD decoder plugin configurations.
+
+@item @code{resampler} (type: maybe-mpd-plugin)
+MPD resampler plugin configuration.
+
+@item @code{filters} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD filter plugin configurations.
+
+@item @code{outputs} (type: list-of-mpd-output)
+The audio outputs that MPD can use.  By default this is a single output
+using pulseaudio.
+
+@item @code{playlist-plugins} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD playlist plugin configurations.
+
+@item @code{extra-options} (default: @code{()}) (type: alist)
+An association list of option symbols/strings to string values to be
+appended to the configuration.
+
+@end table
+@end deftp
+
+@deftp {Data Type} mpd-plugin
+Data type representing a @command{mpd} plugin.
+
+@table @asis
+@item @code{plugin} (type: maybe-string)
+Plugin name.
+
+@item @code{name} (type: maybe-string)
+Name.
+
+@item @code{enabled?} (type: maybe-boolean)
+Whether the plugin is enabled/disabled.
+
+@item @code{extra-options} (default: @code{()}) (type: alist)
+An association list of option symbols/strings to string values to be
+appended to the plugin configuration.  See
+@uref{https://mpd.readthedocs.io/en/latest/plugins.html,MPD plugin
+reference} for available options.
+
+@end table
+@end deftp
+
+@deftp {Data Type} mpd-partition
+Data type representing a @command{mpd} partition.
+
+@table @asis
+@item @code{name} (type: string)
+Partition name.
+
+@item @code{extra-options} (default: @code{()}) (type: alist)
+An association list of option symbols/strings to string values to be
+appended to the partition configuration.  See
+@uref{https://mpd.readthedocs.io/en/latest/user.html#configuring-partitions,Configuring
+Partitions} for available options.
 
 @end table
 @end deftp
 
 @deftp {Data Type} mpd-output
-Data type representing an @command{mpd} audio output.
+Data type representing a @command{mpd} audio output.
 
 @table @asis
-@item @code{name} (default: @code{"MPD"})
+@item @code{name} (default: @code{"MPD"}) (type: string)
 The name of the audio output.
 
-@item @code{type} (default: @code{"pulse"})
+@item @code{type} (default: @code{"pulse"}) (type: string)
 The type of audio output.
 
-@item @code{enabled?} (default: @code{#t})
+@item @code{enabled?} (default: @code{#t}) (type: boolean)
 Specifies whether this audio output is enabled when MPD is started.  By
 default, all audio outputs are enabled.  This is just the default
 setting when there is no state file; with a state file, the previous
 state is restored.
 
-@item @code{tags?} (default: @code{#t})
+@item @code{format} (type: maybe-string)
+Force a specific audio format on output.  See
+@uref{https://mpd.readthedocs.io/en/latest/user.html#audio-output-format,Global
+Audio Format} for a more detailed description.
+
+@item @code{tags?} (default: @code{#t}) (type: boolean)
 If set to @code{#f}, then MPD will not send tags to this output.  This
 is only useful for output plugins that can receive tags, for example the
 @code{httpd} output plugin.
 
-@item @code{always-on?} (default: @code{#f})
+@item @code{always-on?} (default: @code{#f}) (type: boolean)
 If set to @code{#t}, then MPD attempts to keep this audio output always
-open.  This may be useful for streaming servers, when you don’t want to
+open.  This may be useful for streaming servers, when you don?t want to
 disconnect all listeners even when playback is accidentally stopped.
 
-@item @code{mixer-type}
-This field accepts a symbol that specifies which mixer should be used
+@item @code{mixer-type} (default: @code{"none"}) (type: string)
+This field accepts a string that specifies which mixer should be used
 for this audio output: the @code{hardware} mixer, the @code{software}
 mixer, the @code{null} mixer (allows setting the volume, but with no
 effect; this can be used as a trick to implement an external mixer
 External Mixer) or no mixer (@code{none}).
 
-@item @code{extra-options} (default: @code{'()})
-An association list of option symbols to string values to be appended to
-the audio output configuration.
+@item @code{replay-gain-handler} (type: maybe-string)
+This field accepts a string that specifies how
+@uref{https://mpd.readthedocs.io/en/latest/user.html#replay-gain,Replay
+Gain} is to be applied.  @code{software} uses an internal software
+volume control, @code{mixer} uses the configured (hardware) mixer
+control and @code{none} disables replay gain on this audio output.
+
+@item @code{extra-options} (default: @code{()}) (type: alist)
+An association list of option symbols/strings to string values to be
+appended to the audio output configuration.
 
 @end table
 @end deftp
 
-The following example shows a configuration of @code{mpd} that provides
-an HTTP audio streaming output.
+The following example shows a configuration of @command{mpd} that
+configures some of its plugins and provides a HTTP audio streaming output.
 
 @lisp
 (service mpd-service-type
@@ -32907,7 +33013,19 @@ an HTTP audio streaming output.
                      (mixer-type 'null)
                      (extra-options
                       `((encoder . "vorbis")
-                        (port    . "8080"))))))))
+                        (port    . "8080"))))))
+           (decoders
+             (list (mpd-plugin
+                     (plugin "mikmod")
+                     (enabled? #f))
+                   (mpd-plugin
+                     (plugin "openmpt")
+                     (enabled? #t)
+                     (extra-options `((repeat-count . -1)
+                                      (interpolation-filter . 1))))))
+           (resampler (mpd-plugin
+                        (plugin "libsamplerate")
+                        (extra-options `((type . 0)))))))
 @end lisp
 
 
diff --git a/gnu/services/audio.scm b/gnu/services/audio.scm
index 2351db8a4a..b0587e0d3c 100644
--- a/gnu/services/audio.scm
+++ b/gnu/services/audio.scm
@@ -2,6 +2,7 @@
 ;;; Copyright © 2017 Peter Mikkelsen <petermikkelsen10@gmail.com>
 ;;; Copyright © 2019 Ricardo Wurmus <rekado@elephly.net>
 ;;; Copyright © 2020 Ludovic Courtès <ludo@gnu.org>
+;;; Copyright © 2022 Bruno Victal <mirai@makinata.eu>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -20,15 +21,18 @@
 
 (define-module (gnu services audio)
   #:use-module (guix gexp)
+  #:use-module (guix deprecation)
+  #:use-module (guix diagnostics)
+  #:use-module (guix i18n)
   #:use-module (gnu services)
   #:use-module (gnu services configuration)
   #:use-module (gnu services shepherd)
+  #:use-module (gnu services admin)
   #:use-module (gnu system shadow)
   #:use-module (gnu packages admin)
   #:use-module (gnu packages mpd)
   #:use-module (guix records)
   #:use-module (ice-9 match)
-  #:use-module (ice-9 format)
   #:use-module (srfi srfi-1)
   #:use-module (srfi srfi-26)
   #:export (mpd-output
@@ -51,180 +55,406 @@ (define (uglify-field-name field-name)
                                #\-)
                  "_")))
 
-(define (free-form-args? val)
-  (match val
-    (() #t)
-    ((((? symbol?) . (? string?)) . val) (free-form-args? val))
-    (_ #f)))
+(define list-of-string?
+  (list-of string?))
 
-(define* (mpd-serialize-field field-name value #:optional (indent-level 0))
-  #~(begin
-      (use-modules ((ice-9 format)))
-      (format #f "~v/~a \"~a\"~%" #$indent-level #$(if (string? field-name)
-                                                       field-name
-                                                       (uglify-field-name field-name)) #$value)))
+(define list-of-symbol?
+  (list-of symbol?))
 
-(define* (mpd-serialize-free-form-args field-name value #:optional (indent-level 0))
-  (generic-serialize-alist string-append (cut mpd-serialize-field <> <> indent-level) value))
+(define* (mpd-serialize-field field-name value #:optional (indent-level 0))
+  (let ((field (if (string? field-name) field-name
+                   (uglify-field-name field-name)))
+        (value (if (boolean? value) (if value "yes" "no") value)))
+    #~(begin
+        (use-modules (ice-9 format))
+        (format #f "~v/~a \"~a\"~%" #$indent-level #$field #$value))))
 
 (define mpd-serialize-number mpd-serialize-field)
 
 (define mpd-serialize-string mpd-serialize-field)
 
-(define* (mpd-serialize-boolean field-name value #:optional (indent-level 0))
-  (mpd-serialize-field field-name (if value "yes" "no") indent-level))
+(define mpd-serialize-boolean mpd-serialize-field)
 
-(define (mpd-serialize-list-of-mpd-output field-name value)
-  #~(string-append "\naudio_output {\n"
-                   #$@(map (cut serialize-configuration <>
-                                mpd-output-fields)
-                           value)
-                   "}\n"))
+(define* (mpd-serialize-alist field-name value #:optional (indent-level 0))
+  #~(string-append #$@(generic-serialize-alist list (cut mpd-serialize-field <> <> indent-level) value)))
 
-(define (mpd-serialize-configuration configuration)
-  (mixed-text-file
-   "mpd.conf"
-   (serialize-configuration configuration mpd-configuration-fields)))
+(define-maybe string (prefix mpd-))
+(define-maybe list-of-string (prefix mpd-))
 
 (define mpd-subsystem-serialize-field (cut mpd-serialize-field <> <> 1))
 (define mpd-subsystem-serialize-string (cut mpd-serialize-string <> <> 1))
 (define mpd-subsystem-serialize-number (cut mpd-serialize-number <> <> 1))
 (define mpd-subsystem-serialize-boolean (cut mpd-serialize-boolean <> <> 1))
-(define mpd-subsystem-serialize-free-form-args (cut mpd-serialize-free-form-args <> <> 1))
+(define mpd-subsystem-serialize-alist (cut mpd-serialize-alist <> <> 1))
+
+(define-maybe string (prefix mpd-subsystem-))
+(define-maybe boolean (prefix mpd-subsystem-))
+
+;;; TODO: Procedures for deprecated fields, to be removed.
+
+(define mpd-deprecated-fields '((music-dir . music-directory)
+                                (playlist-dir . playlist-directory)
+                                (address . addresses)))
+
+(define (port? value) (or (string? value) (integer? value)))
+
+(define (mpd-serialize-deprecated-field field-name value)
+  (if (maybe-value-set? value)
+      (begin (warn-about-deprecation field-name #f
+                                     #:replacement (assoc-ref mpd-deprecated-fields field-name))
+             (match field-name
+               ('playlist-dir (mpd-serialize-string "playlist_directory" value))
+               ('music-dir (mpd-serialize-string "music_directory" value))
+               ('address (mpd-serialize-string "bind_to_address" value))))
+      ""))
+
+(define (mpd-serialize-port field-name value)
+  (when (string? value)
+    (warning (G_ "string value for '~a' is deprecated, use integer instead~%") field-name))
+  (mpd-serialize-field field-name value))
+
+(define-maybe port (prefix mpd-))
+
+;;;
+
+;; Generic MPD plugin record, lists only the most prevalent fields.
+(define-configuration mpd-plugin
+  (plugin
+   maybe-string
+   "Plugin name.")
+
+  (name
+   maybe-string
+   "Name.")
+
+  (enabled?
+   maybe-boolean
+   "Whether the plugin is enabled/disabled.")
+
+  (extra-options
+   (alist '())
+   "An association list of option symbols/strings to string values to be appended to
+the plugin configuration. See
+@uref{https://mpd.readthedocs.io/en/latest/plugins.html,MPD plugin reference}
+for available options.")
+
+  (prefix mpd-subsystem-))
+
+;; Translate <mpd-configuration> field name into block name for mpd.conf"
+(define mpd-subsystem-name
+  (match-lambda
+    ('archive-plugins "archive_plugin")
+    ('playlist-plugins "playlist_plugin")
+    ('inputs "input")
+    ('decoders "decoder")
+    ('filters "filter")
+    (x (symbol->string x))))
+
+(define (mpd-serialize-mpd-plugin field-name value)
+  #~(string-append "\n" #$(mpd-subsystem-name field-name) " {\n"
+                   #$(serialize-configuration value mpd-plugin-fields)
+                   "}\n"))
+
+(define (mpd-serialize-list-of-mpd-plugin field-name value)
+  #~(string-append #$@(map (cut mpd-serialize-mpd-plugin field-name <>) value)))
+
+(define list-of-mpd-plugin? (list-of mpd-plugin?))
+
+(define-maybe mpd-plugin (prefix mpd-))
+
+(define-configuration mpd-partition
+  (name
+   string
+   "Partition name.")
+
+  (extra-options
+   (alist '())
+   "An association list of option symbols/strings to string values to be appended to
+the partition configuration. See
+@uref{https://mpd.readthedocs.io/en/latest/user.html#configuring-partitions,Configuring Partitions}
+for available options.")
+
+  (prefix mpd-subsystem-))
+
+(define (mpd-serialize-mpd-partition field-name value)
+  #~(string-append "\npartition {\n"
+                   #$(serialize-configuration value mpd-partition-fields)
+                   "}\n"))
+
+(define (mpd-serialize-list-of-mpd-partition field-name value)
+  #~(string-append #$@(map (cut mpd-serialize-mpd-partition #f <>) value)))
+
+(define list-of-mpd-partition?
+  (list-of mpd-partition?))
 
 (define-configuration mpd-output
   (name
    (string "MPD")
    "The name of the audio output.")
+
   (type
    (string "pulse")
    "The type of audio output.")
+
   (enabled?
    (boolean #t)
    "Specifies whether this audio output is enabled when MPD is started. By
 default, all audio outputs are enabled. This is just the default
 setting when there is no state file; with a state file, the previous
 state is restored.")
+
+  (format
+   maybe-string
+   "Force a specific audio format on output. See
+@uref{https://mpd.readthedocs.io/en/latest/user.html#audio-output-format,Global Audio Format}
+for a more detailed description.")
+   
   (tags?
    (boolean #t)
    "If set to @code{#f}, then MPD will not send tags to this output. This
 is only useful for output plugins that can receive tags, for example the
 @code{httpd} output plugin.")
+
   (always-on?
    (boolean #f)
    "If set to @code{#t}, then MPD attempts to keep this audio output always
 open. This may be useful for streaming servers, when you don’t want to
 disconnect all listeners even when playback is accidentally stopped.")
+
   (mixer-type
    (string "none")
-   "This field accepts a symbol that specifies which mixer should be used
+   "This field accepts a string that specifies which mixer should be used
 for this audio output: the @code{hardware} mixer, the @code{software}
 mixer, the @code{null} mixer (allows setting the volume, but with no
 effect; this can be used as a trick to implement an external mixer
 External Mixer) or no mixer (@code{none}).")
+
+  (replay-gain-handler
+   maybe-string
+   "This field accepts a string that specifies how
+@uref{https://mpd.readthedocs.io/en/latest/user.html#replay-gain,Replay Gain} is
+to be applied. @code{software} uses an internal software volume control,
+@code{mixer} uses the configured (hardware) mixer control and @code{none}
+disables replay gain on this audio output.")
+
   (extra-options
-   (free-form-args '())
-   "An association list of option symbols to string values to be appended to
+   (alist '())
+   "An association list of option symbols/strings to string values to be appended to
 the audio output configuration.")
+
   (prefix mpd-subsystem-))
 
+(define (mpd-serialize-mpd-output field-name value)
+  #~(string-append "\naudio_output {\n"
+                   #$(serialize-configuration value mpd-output-fields)
+                   "}\n"))
+
+(define (mpd-serialize-list-of-mpd-output field-name value)
+  #~(string-append #$@(map (cut mpd-serialize-mpd-output #f <>) value)))
+
 (define list-of-mpd-output?
   (list-of mpd-output?))
 
 (define-configuration mpd-configuration
+  (package
+   (file-like mpd)
+   "The MPD package."
+   empty-serializer)
+
   (user
    (string "mpd")
    "The user to run mpd as.")
-  (music-dir
-   (string "~/Music")
+
+  (group
+   maybe-string
+   "The group to run mpd as.")
+
+  (shepherd-requirement
+   (list-of-symbol '())
+   "This is a list of symbols naming Shepherd services that this service
+will depend on."
+   empty-serializer)
+
+  (log-file
+   (maybe-string "/var/log/mpd/log")
+   "The location of the log file. Set to @code{syslog} to use the local syslog daemon or
+@code{%unset-value} to omit this directive from the configuration file.")
+
+  (log-level
+   maybe-string
+   "Supress any messages below this threshold. Available values: @code{notice},
+@code{info}, @code{verbose}, @code{warning} and @code{error}.")
+
+  (music-directory
+   maybe-string
+   "The directory to scan for music files.")
+
+  (music-dir ; TODO: deprecated, remove later
+   maybe-string
    "The directory to scan for music files."
-   (lambda (_ x)
-     (mpd-serialize-field "music_directory" x)))
-  (playlist-dir
-   (string "~/.mpd/playlists")
+   mpd-serialize-deprecated-field)
+
+  (playlist-directory
+   maybe-string
+   "The directory to store playlists.")
+
+  (playlist-dir ; TODO: deprecated, remove later
+   maybe-string
    "The directory to store playlists."
-   (lambda (_ x)
-     (mpd-serialize-field "playlist_directory" x)))
+   mpd-serialize-deprecated-field)
+
   (db-file
-   (string "~/.mpd/tag_cache")
+   maybe-string
    "The location of the music database.")
+
   (state-file
-   (string "~/.mpd/state")
+   maybe-string
    "The location of the file that stores current MPD's state.")
+
   (sticker-file
-   (string "~/.mpd/sticker.sql")
+   maybe-string
    "The location of the sticker database.")
+
   (port
-   (string "6600")
+   (maybe-port 6600) ; TODO: switch to integer
    "The port to run mpd on.")
-  (address
-   (string "any")
+
+  (addresses
+   maybe-list-of-string
+   "The addresses that mpd will bind to.
+To use a Unix domain socket, an absolute path can be specified here."
+   (lambda (_ x)
+     (if (maybe-value-set? x)
+         #~(string-append #$@(map
+                              (cut mpd-serialize-field "bind_to_address" <>)
+                              x)) "")))
+
+  (address ; TODO: deprecated, remove later
+   maybe-string
    "The address that mpd will bind to.
 To use a Unix domain socket, an absolute path can be specified here."
+   mpd-serialize-deprecated-field)
+
+  (database
+   maybe-mpd-plugin
+   "MPD database plugin configuration.")
+
+  (partitions
+   (list-of-mpd-partition '())
+   "List of MPD \"partitions\".")
+  
+  (neighbors
+   (list-of-mpd-plugin '())
+   "List of MPD neighbor plugin configurations.")
+
+  (inputs
+   (list-of-mpd-plugin '())
+   "List of MPD input plugin configurations.")
+
+  (archive-plugins
+   (list-of-mpd-plugin '())
+   "List of MPD archive plugin configurations.")
+
+  (input-cache-size
+   maybe-string
+   "MPD input cache size."
    (lambda (_ x)
-     (mpd-serialize-field "bind_to_address" x)))
+     (if (maybe-value-set? x)
+         #~(string-append "\ninput_cache {\n"
+                          #$(mpd-subsystem-serialize-string "size" x)
+                          "}\n") "")))
+
+  (decoders
+   (list-of-mpd-plugin '())
+   "List of MPD decoder plugin configurations.")
+
+  (resampler
+   maybe-mpd-plugin
+   "MPD resampler plugin configuration.")
+
+  (filters
+   (list-of-mpd-plugin '())
+   "List of MPD filter plugin configurations.")
+
   (outputs
    (list-of-mpd-output (list (mpd-output)))
    "The audio outputs that MPD can use.
 By default this is a single output using pulseaudio.")
+
+  (playlist-plugins
+   (list-of-mpd-plugin '())
+   "List of MPD playlist plugin configurations.")
+
+  (extra-options
+   (alist '())
+   "An association list of option symbols/strings to string values to be appended to
+the configuration.")
+
   (prefix mpd-))
 
-(define (mpd-file-name config file)
-  "Return a path in /var/run/mpd/ that is writable
-   by @code{user} from @code{config}."
-  (string-append "/var/run/mpd/"
-                 (mpd-configuration-user config)
-                 "/" file))
+(define (mpd-serialize-configuration configuration)
+  (mixed-text-file
+   "mpd.conf"
+   (serialize-configuration configuration mpd-configuration-fields)))
+
+(define (mpd-log-rotation config)
+  (match-record config <mpd-configuration> (log-file)
+    (log-rotation
+     (files (list log-file))
+     (post-rotate #~(begin
+                      (use-modules (gnu services herd))
+                      (with-shepherd-action 'mpd ('reload) #f))))))
 
 (define (mpd-shepherd-service config)
-  (shepherd-service
-   (documentation "Run the MPD (Music Player Daemon)")
-   (requirement '(user-processes))
-   (provision '(mpd))
-   (start #~(make-forkexec-constructor
-             (list #$(file-append mpd "/bin/mpd")
-                   "--no-daemon"
-                   #$(mpd-serialize-configuration config))
-             #:environment-variables
-             ;; Required to detect PulseAudio when run under a user account.
-             (list (string-append
-                    "XDG_RUNTIME_DIR=/run/user/"
-                    (number->string
-                     (passwd:uid
-                      (getpwnam #$(mpd-configuration-user config))))))
-             #:log-file #$(mpd-file-name config "log")))
-   (stop  #~(make-kill-destructor))))
+  (match-record config <mpd-configuration> (user package shepherd-requirement)
+    (let* ((config-file (mpd-serialize-configuration config)))
+      (shepherd-service
+       (documentation "Run the MPD (Music Player Daemon)")
+       (requirement `(user-processes loopback ,@shepherd-requirement))
+       (provision '(mpd))
+       (start #~(make-forkexec-constructor
+                 (list #$(file-append package "/bin/mpd")
+                       "--no-daemon"
+                       #$config-file)
+                 #:environment-variables
+                 ;; Required to detect PulseAudio when run under a user account.
+                 (list (string-append "XDG_RUNTIME_DIR=/run/user/"
+                                      (number->string (passwd:uid (getpwnam #$user)))))))
+       (stop  #~(make-kill-destructor))
+       (actions
+        (list (shepherd-configuration-action config-file)
+              (shepherd-action
+               (name 'reload)
+               (documentation "Reopen log files and flush caches.")
+               (procedure #~(lambda (pid)
+                              (if pid
+                                  (begin (kill pid SIGHUP)
+                                         (format #t "Issued SIGHUP to Service MPD (PID ~a)." pid))
+                                  (format #t "Service MPD is not running.")))))))))))
 
 (define (mpd-service-activation config)
-  (with-imported-modules '((guix build utils))
+  (match-record config <mpd-configuration> (user log-file)
     #~(begin
         (use-modules (guix build utils))
-        (define %user
-          (getpw #$(mpd-configuration-user config)))
-
-        (let ((directory #$(mpd-file-name config ".mpd")))
-          (mkdir-p directory)
-          (chown directory (passwd:uid %user) (passwd:gid %user))
-
-          ;; Make /var/run/mpd/USER user-owned as well.
-          (chown (dirname directory)
-                 (passwd:uid %user) (passwd:gid %user))))))
-
-
-(define %mpd-accounts
-  ;; Default account and group for MPD.
-  (list (user-group (name "mpd") (system? #t))
-        (user-account
-         (name "mpd")
-         (group "mpd")
-         (system? #t)
-         (comment "Music Player Daemon (MPD) user")
 
-         ;; Note: /var/run/mpd hosts one sub-directory per user, of which
-         ;; /var/run/mpd/mpd corresponds to the "mpd" user.
-         (home-directory "/var/run/mpd/mpd")
+        (let* ((user (getpw #$user))
+               (deprecated-directory (string-append "/var/run/mpd/" #$user "/.mpd"))
+               (new-directory (string-append (passwd:dir user) "/.config/mpd")))
+          ;; TODO: remove me, migrates from the old location at /var/run/mpd to the new one at /var/lib/mpd.
+          (when (and (file-exists? deprecated-directory) (not (file-exists? new-directory)))
+            (rename-file deprecated-directory new-directory)
+            (chown new-directory (passwd:uid user) (passwd:gid user)))
+          (mkdir-p (dirname #$log-file))))))
 
-         (shell (file-append shadow "/sbin/nologin")))))
+(define (mpd-accounts config)
+  (match-record config <mpd-configuration> (user)
+    (list (user-account
+           (name user)
+           (group "nogroup")
+           (system? #t)
+           (comment "Music Player Daemon (MPD) user")
+           (home-directory "/var/lib/mpd")  ; MPD can use $HOME (or $XDG_CONFIG_HOME) to place its data
+           (shell (file-append shadow "/sbin/nologin"))))))
 
 (define mpd-service-type
   (service-type
@@ -234,7 +464,9 @@ (define mpd-service-type
     (list (service-extension shepherd-root-service-type
                              (compose list mpd-shepherd-service))
           (service-extension account-service-type
-                             (const %mpd-accounts))
+                             mpd-accounts)
           (service-extension activation-service-type
-                             mpd-service-activation)))
+                             mpd-service-activation)
+          (service-extension rottlog-service-type
+                             (compose list mpd-log-rotation))))
    (default-value (mpd-configuration))))
-- 
2.38.1





^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [bug#59866] [PATCH 0/2] services: mpd: Refactor MPD service
  2022-12-06 23:22 [bug#59866] [PATCH 0/2] services: mpd: Refactor MPD service mirai
  2022-12-06 23:25 ` [bug#59866] [PATCH 1/2] services: mpd: use 'define-configuration' mirai
  2022-12-06 23:25 ` [bug#59866] [PATCH 2/2] services: mpd: Refactor MPD service mirai
@ 2022-12-07  8:59 ` ( via Guix-patches via
  2022-12-07 13:42   ` mirai
  2022-12-07 18:27 ` Liliana Marie Prikler
                   ` (8 subsequent siblings)
  11 siblings, 1 reply; 29+ messages in thread
From: ( via Guix-patches via @ 2022-12-07  8:59 UTC (permalink / raw)
  To: mirai, 59866

[-- Attachment #1: Type: text/plain, Size: 725 bytes --]

On Tue Dec 6, 2022 at 11:22 PM GMT,  wrote:
> Modernizes and expands 'mpd-service-type'.
> Performs "pretty-formatting" to the generated configuration file
> at the cost of a few extra procedures.
> It also deprecates some of the fields (abbreviated forms).
>
> Bruno Victal (2):
>   services: mpd: use 'define-configuration'.
>   services: mpd: Refactor MPD service.
>
>  doc/guix.texi          | 172 +++++++++++--
>  gnu/services/audio.scm | 549 ++++++++++++++++++++++++++++++-----------
>  2 files changed, 556 insertions(+), 165 deletions(-)
>
>
> base-commit: b94724e8b2102be0fe9d19e9dfe44d6f7101bd4b
> -- 
> 2.38.1

Could you also add support for ``herd configuration'' (added in #59197)?
    -- (

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 659 bytes --]

^ permalink raw reply	[flat|nested] 29+ messages in thread

* [bug#59866] [PATCH 0/2] services: mpd: Refactor MPD service
  2022-12-07  8:59 ` [bug#59866] [PATCH 0/2] " ( via Guix-patches via
@ 2022-12-07 13:42   ` mirai
  2022-12-07 13:43     ` ( via Guix-patches via
  0 siblings, 1 reply; 29+ messages in thread
From: mirai @ 2022-12-07 13:42 UTC (permalink / raw)
  To: (, 59866

It already does! The second commit introduces the bulk of the changes :)

On 2022-12-07 08:59, ( wrote:
> On Tue Dec 6, 2022 at 11:22 PM GMT,  wrote:
>> Modernizes and expands 'mpd-service-type'.
>> Performs "pretty-formatting" to the generated configuration file
>> at the cost of a few extra procedures.
>> It also deprecates some of the fields (abbreviated forms).
>>
>> Bruno Victal (2):
>>   services: mpd: use 'define-configuration'.
>>   services: mpd: Refactor MPD service.
>>
>>  doc/guix.texi          | 172 +++++++++++--
>>  gnu/services/audio.scm | 549 ++++++++++++++++++++++++++++++-----------
>>  2 files changed, 556 insertions(+), 165 deletions(-)
>>
>>
>> base-commit: b94724e8b2102be0fe9d19e9dfe44d6f7101bd4b
>> -- 
>> 2.38.1
> 
> Could you also add support for ``herd configuration'' (added in #59197)?
>     -- (




^ permalink raw reply	[flat|nested] 29+ messages in thread

* [bug#59866] [PATCH 0/2] services: mpd: Refactor MPD service
  2022-12-07 13:42   ` mirai
@ 2022-12-07 13:43     ` ( via Guix-patches via
  0 siblings, 0 replies; 29+ messages in thread
From: ( via Guix-patches via @ 2022-12-07 13:43 UTC (permalink / raw)
  To: mirai, 59866

[-- Attachment #1: Type: text/plain, Size: 174 bytes --]

Heya,

On Wed Dec 7, 2022 at 1:42 PM GMT, mirai wrote:
> It already does! The second commit introduces the bulk of the changes :)

Oops :) Reviewing now...

    -- (

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 659 bytes --]

^ permalink raw reply	[flat|nested] 29+ messages in thread

* [bug#59866] [PATCH 0/2] services: mpd: Refactor MPD service
  2022-12-06 23:22 [bug#59866] [PATCH 0/2] services: mpd: Refactor MPD service mirai
                   ` (2 preceding siblings ...)
  2022-12-07  8:59 ` [bug#59866] [PATCH 0/2] " ( via Guix-patches via
@ 2022-12-07 18:27 ` Liliana Marie Prikler
  2022-12-08 13:11   ` mirai
  2022-12-08  0:59 ` [bug#59866] [PATCH v2] " mirai
                   ` (7 subsequent siblings)
  11 siblings, 1 reply; 29+ messages in thread
From: Liliana Marie Prikler @ 2022-12-07 18:27 UTC (permalink / raw)
  To: mirai, 59866

Am Dienstag, dem 06.12.2022 um 23:22 +0000 schrieb mirai@makinata.eu:
> Modernizes and expands 'mpd-service-type'.
> Performs "pretty-formatting" to the generated configuration file
> at the cost of a few extra procedures.
> It also deprecates some of the fields (abbreviated forms).
> 
> Bruno Victal (2):
>   services: mpd: use 'define-configuration'.
>   services: mpd: Refactor MPD service.
Note that there is [1], which attempts to make it so that shepherd
endpoints can be specified in lieu of MPD endpoints.  You don't need to
implement this logic – you are free to do so if you want to – but could
you make it so that there is an explicit endpoint abstraction that
would allow for extension later on?

Cheers

[1] https://issues.guix.gnu.org/54986




^ permalink raw reply	[flat|nested] 29+ messages in thread

* [bug#59866] [PATCH v2] services: mpd: Refactor MPD service.
  2022-12-06 23:22 [bug#59866] [PATCH 0/2] services: mpd: Refactor MPD service mirai
                   ` (3 preceding siblings ...)
  2022-12-07 18:27 ` Liliana Marie Prikler
@ 2022-12-08  0:59 ` mirai
  2022-12-16 14:15 ` [bug#59866] [PATCH v3] " mirai
                   ` (6 subsequent siblings)
  11 siblings, 0 replies; 29+ messages in thread
From: mirai @ 2022-12-08  0:59 UTC (permalink / raw)
  To: 59866; +Cc: Bruno Victal

From: Bruno Victal <mirai@makinata.eu>

Introduces 'mpd-plugin' and 'mpd-partition' records.
Expands 'mpd-output' and 'mpd-configuration' records.
Deprecates redundant abbreviated fields in 'mpd-configuration' and
avoids serializing unused fields that may introduce undesired behavior.
Implement log-rotation via rottlog.
Implements Shepherd actions: 'reload' and 'configuration'.
---

Slight oversight, mpd-plugin and mpd-partition record types weren't
being exported.

 doc/guix.texi          | 172 ++++++++++++++---
 gnu/services/audio.scm | 426 ++++++++++++++++++++++++++++++++---------
 2 files changed, 476 insertions(+), 122 deletions(-)

diff --git a/doc/guix.texi b/doc/guix.texi
index 50487a5172..5a009df240 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -109,6 +109,7 @@ Copyright @copyright{} 2022 Reily Siegel@*
 Copyright @copyright{} 2022 Simon Streit@*
 Copyright @copyright{} 2022 (@*
 Copyright @copyright{} 2022 John Kehayias@*
+Copyright @copyright{} 2022 Bruno Victal@*
 
 Permission is granted to copy, distribute and/or modify this document
 under the terms of the GNU Free Documentation License, Version 1.3 or
@@ -32823,79 +32824,184 @@ The service type for @command{mpd}
 Data type representing the configuration of @command{mpd}.
 
 @table @asis
-@item @code{user} (default: @code{"mpd"})
+@item @code{package} (default: @code{mpd}) (type: file-like)
+The MPD package.
+
+@item @code{user} (default: @code{"mpd"}) (type: string)
 The user to run mpd as.
 
-@item @code{music-dir} (default: @code{"~/Music"})
+@item @code{group} (type: maybe-string)
+The group to run mpd as.
+
+@item @code{shepherd-requirement} (default: @code{()}) (type: list-of-symbol)
+This is a list of symbols naming Shepherd services that this service
+will depend on.
+
+@item @code{log-file} (default: @code{"/var/log/mpd/log"}) (type: maybe-string)
+The location of the log file.  Set to @code{syslog} to use the local
+syslog daemon or @code{%unset-value} to omit this directive from the
+configuration file.
+
+@item @code{log-level} (type: maybe-string)
+Supress any messages below this threshold.  Available values:
+@code{notice}, @code{info}, @code{verbose}, @code{warning} and
+@code{error}.
+
+@item @code{music-directory} (type: maybe-string)
 The directory to scan for music files.
 
-@item @code{playlist-dir} (default: @code{"~/.mpd/playlists"})
+@item @code{playlist-directory} (type: maybe-string)
 The directory to store playlists.
 
-@item @code{db-file} (default: @code{"~/.mpd/tag_cache"})
+@item @code{db-file} (type: maybe-string)
 The location of the music database.
 
-@item @code{state-file} (default: @code{"~/.mpd/state"})
+@item @code{state-file} (type: maybe-string)
 The location of the file that stores current MPD's state.
 
-@item @code{sticker-file} (default: @code{"~/.mpd/sticker.sql"})
+@item @code{sticker-file} (type: maybe-string)
 The location of the sticker database.
 
-@item @code{port} (default: @code{"6600"})
+@item @code{port} (default: @code{6600}) (type: maybe-integer)
 The port to run mpd on.
 
-@item @code{address} (default: @code{"any"})
-The address that mpd will bind to.  To use a Unix domain socket,
-an absolute path can be specified here.
+@item @code{addresses} (type: maybe-list-of-string)
+The addresses that mpd will bind to.  To use a Unix domain socket, an
+absolute path can be specified here.
+
+@item @code{database} (type: maybe-mpd-plugin)
+MPD database plugin configuration.
+
+@item @code{partitions} (default: @code{()}) (type: list-of-mpd-partition)
+List of MPD "partitions".
+
+@item @code{neighbors} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD neighbor plugin configurations.
 
-@item @code{outputs} (default: @code{"(list (mpd-output))"})
-The audio outputs that MPD can use.  By default this is a single output using pulseaudio.
+@item @code{inputs} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD input plugin configurations.
+
+@item @code{archive-plugins} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD archive plugin configurations.
+
+@item @code{input-cache-size} (type: maybe-string)
+MPD input cache size.
+
+@item @code{decoders} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD decoder plugin configurations.
+
+@item @code{resampler} (type: maybe-mpd-plugin)
+MPD resampler plugin configuration.
+
+@item @code{filters} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD filter plugin configurations.
+
+@item @code{outputs} (type: list-of-mpd-output)
+The audio outputs that MPD can use.  By default this is a single output
+using pulseaudio.
+
+@item @code{playlist-plugins} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD playlist plugin configurations.
+
+@item @code{extra-options} (default: @code{()}) (type: alist)
+An association list of option symbols/strings to string values to be
+appended to the configuration.
+
+@end table
+@end deftp
+
+@deftp {Data Type} mpd-plugin
+Data type representing a @command{mpd} plugin.
+
+@table @asis
+@item @code{plugin} (type: maybe-string)
+Plugin name.
+
+@item @code{name} (type: maybe-string)
+Name.
+
+@item @code{enabled?} (type: maybe-boolean)
+Whether the plugin is enabled/disabled.
+
+@item @code{extra-options} (default: @code{()}) (type: alist)
+An association list of option symbols/strings to string values to be
+appended to the plugin configuration.  See
+@uref{https://mpd.readthedocs.io/en/latest/plugins.html,MPD plugin
+reference} for available options.
+
+@end table
+@end deftp
+
+@deftp {Data Type} mpd-partition
+Data type representing a @command{mpd} partition.
+
+@table @asis
+@item @code{name} (type: string)
+Partition name.
+
+@item @code{extra-options} (default: @code{()}) (type: alist)
+An association list of option symbols/strings to string values to be
+appended to the partition configuration.  See
+@uref{https://mpd.readthedocs.io/en/latest/user.html#configuring-partitions,Configuring
+Partitions} for available options.
 
 @end table
 @end deftp
 
 @deftp {Data Type} mpd-output
-Data type representing an @command{mpd} audio output.
+Data type representing a @command{mpd} audio output.
 
 @table @asis
-@item @code{name} (default: @code{"MPD"})
+@item @code{name} (default: @code{"MPD"}) (type: string)
 The name of the audio output.
 
-@item @code{type} (default: @code{"pulse"})
+@item @code{type} (default: @code{"pulse"}) (type: string)
 The type of audio output.
 
-@item @code{enabled?} (default: @code{#t})
+@item @code{enabled?} (default: @code{#t}) (type: boolean)
 Specifies whether this audio output is enabled when MPD is started.  By
 default, all audio outputs are enabled.  This is just the default
 setting when there is no state file; with a state file, the previous
 state is restored.
 
-@item @code{tags?} (default: @code{#t})
+@item @code{format} (type: maybe-string)
+Force a specific audio format on output.  See
+@uref{https://mpd.readthedocs.io/en/latest/user.html#audio-output-format,Global
+Audio Format} for a more detailed description.
+
+@item @code{tags?} (default: @code{#t}) (type: boolean)
 If set to @code{#f}, then MPD will not send tags to this output.  This
 is only useful for output plugins that can receive tags, for example the
 @code{httpd} output plugin.
 
-@item @code{always-on?} (default: @code{#f})
+@item @code{always-on?} (default: @code{#f}) (type: boolean)
 If set to @code{#t}, then MPD attempts to keep this audio output always
-open.  This may be useful for streaming servers, when you don’t want to
+open.  This may be useful for streaming servers, when you don?t want to
 disconnect all listeners even when playback is accidentally stopped.
 
-@item @code{mixer-type}
-This field accepts a symbol that specifies which mixer should be used
+@item @code{mixer-type} (default: @code{"none"}) (type: string)
+This field accepts a string that specifies which mixer should be used
 for this audio output: the @code{hardware} mixer, the @code{software}
 mixer, the @code{null} mixer (allows setting the volume, but with no
 effect; this can be used as a trick to implement an external mixer
 External Mixer) or no mixer (@code{none}).
 
-@item @code{extra-options} (default: @code{'()})
-An association list of option symbols to string values to be appended to
-the audio output configuration.
+@item @code{replay-gain-handler} (type: maybe-string)
+This field accepts a string that specifies how
+@uref{https://mpd.readthedocs.io/en/latest/user.html#replay-gain,Replay
+Gain} is to be applied.  @code{software} uses an internal software
+volume control, @code{mixer} uses the configured (hardware) mixer
+control and @code{none} disables replay gain on this audio output.
+
+@item @code{extra-options} (default: @code{()}) (type: alist)
+An association list of option symbols/strings to string values to be
+appended to the audio output configuration.
 
 @end table
 @end deftp
 
-The following example shows a configuration of @code{mpd} that provides
-an HTTP audio streaming output.
+The following example shows a configuration of @command{mpd} that
+configures some of its plugins and provides a HTTP audio streaming output.
 
 @lisp
 (service mpd-service-type
@@ -32907,7 +33013,19 @@ an HTTP audio streaming output.
                      (mixer-type 'null)
                      (extra-options
                       `((encoder . "vorbis")
-                        (port    . "8080"))))))))
+                        (port    . "8080"))))))
+           (decoders
+             (list (mpd-plugin
+                     (plugin "mikmod")
+                     (enabled? #f))
+                   (mpd-plugin
+                     (plugin "openmpt")
+                     (enabled? #t)
+                     (extra-options `((repeat-count . -1)
+                                      (interpolation-filter . 1))))))
+           (resampler (mpd-plugin
+                        (plugin "libsamplerate")
+                        (extra-options `((type . 0)))))))
 @end lisp
 
 
diff --git a/gnu/services/audio.scm b/gnu/services/audio.scm
index 2351db8a4a..07baf8554c 100644
--- a/gnu/services/audio.scm
+++ b/gnu/services/audio.scm
@@ -2,6 +2,7 @@
 ;;; Copyright © 2017 Peter Mikkelsen <petermikkelsen10@gmail.com>
 ;;; Copyright © 2019 Ricardo Wurmus <rekado@elephly.net>
 ;;; Copyright © 2020 Ludovic Courtès <ludo@gnu.org>
+;;; Copyright © 2022 Bruno Victal <mirai@makinata.eu>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -20,19 +21,26 @@
 
 (define-module (gnu services audio)
   #:use-module (guix gexp)
+  #:use-module (guix deprecation)
+  #:use-module (guix diagnostics)
+  #:use-module (guix i18n)
   #:use-module (gnu services)
   #:use-module (gnu services configuration)
   #:use-module (gnu services shepherd)
+  #:use-module (gnu services admin)
   #:use-module (gnu system shadow)
   #:use-module (gnu packages admin)
   #:use-module (gnu packages mpd)
   #:use-module (guix records)
   #:use-module (ice-9 match)
-  #:use-module (ice-9 format)
   #:use-module (srfi srfi-1)
   #:use-module (srfi srfi-26)
   #:export (mpd-output
             mpd-output?
+            mpd-plugin
+            mpd-plugin?
+            mpd-partition
+            mpd-partition?
             mpd-configuration
             mpd-configuration?
             mpd-service-type))
@@ -51,180 +59,406 @@ (define (uglify-field-name field-name)
                                #\-)
                  "_")))
 
-(define (free-form-args? val)
-  (match val
-    (() #t)
-    ((((? symbol?) . (? string?)) . val) (free-form-args? val))
-    (_ #f)))
+(define list-of-string?
+  (list-of string?))
 
-(define* (mpd-serialize-field field-name value #:optional (indent-level 0))
-  #~(begin
-      (use-modules ((ice-9 format)))
-      (format #f "~v/~a \"~a\"~%" #$indent-level #$(if (string? field-name)
-                                                       field-name
-                                                       (uglify-field-name field-name)) #$value)))
+(define list-of-symbol?
+  (list-of symbol?))
 
-(define* (mpd-serialize-free-form-args field-name value #:optional (indent-level 0))
-  (generic-serialize-alist string-append (cut mpd-serialize-field <> <> indent-level) value))
+(define* (mpd-serialize-field field-name value #:optional (indent-level 0))
+  (let ((field (if (string? field-name) field-name
+                   (uglify-field-name field-name)))
+        (value (if (boolean? value) (if value "yes" "no") value)))
+    #~(begin
+        (use-modules (ice-9 format))
+        (format #f "~v/~a \"~a\"~%" #$indent-level #$field #$value))))
 
 (define mpd-serialize-number mpd-serialize-field)
 
 (define mpd-serialize-string mpd-serialize-field)
 
-(define* (mpd-serialize-boolean field-name value #:optional (indent-level 0))
-  (mpd-serialize-field field-name (if value "yes" "no") indent-level))
+(define mpd-serialize-boolean mpd-serialize-field)
 
-(define (mpd-serialize-list-of-mpd-output field-name value)
-  #~(string-append "\naudio_output {\n"
-                   #$@(map (cut serialize-configuration <>
-                                mpd-output-fields)
-                           value)
-                   "}\n"))
+(define* (mpd-serialize-alist field-name value #:optional (indent-level 0))
+  #~(string-append #$@(generic-serialize-alist list (cut mpd-serialize-field <> <> indent-level) value)))
 
-(define (mpd-serialize-configuration configuration)
-  (mixed-text-file
-   "mpd.conf"
-   (serialize-configuration configuration mpd-configuration-fields)))
+(define-maybe string (prefix mpd-))
+(define-maybe list-of-string (prefix mpd-))
 
 (define mpd-subsystem-serialize-field (cut mpd-serialize-field <> <> 1))
 (define mpd-subsystem-serialize-string (cut mpd-serialize-string <> <> 1))
 (define mpd-subsystem-serialize-number (cut mpd-serialize-number <> <> 1))
 (define mpd-subsystem-serialize-boolean (cut mpd-serialize-boolean <> <> 1))
-(define mpd-subsystem-serialize-free-form-args (cut mpd-serialize-free-form-args <> <> 1))
+(define mpd-subsystem-serialize-alist (cut mpd-serialize-alist <> <> 1))
+
+(define-maybe string (prefix mpd-subsystem-))
+(define-maybe boolean (prefix mpd-subsystem-))
+
+;;; TODO: Procedures for deprecated fields, to be removed.
+
+(define mpd-deprecated-fields '((music-dir . music-directory)
+                                (playlist-dir . playlist-directory)
+                                (address . addresses)))
+
+(define (port? value) (or (string? value) (integer? value)))
+
+(define (mpd-serialize-deprecated-field field-name value)
+  (if (maybe-value-set? value)
+      (begin (warn-about-deprecation field-name #f
+                                     #:replacement (assoc-ref mpd-deprecated-fields field-name))
+             (match field-name
+               ('playlist-dir (mpd-serialize-string "playlist_directory" value))
+               ('music-dir (mpd-serialize-string "music_directory" value))
+               ('address (mpd-serialize-string "bind_to_address" value))))
+      ""))
+
+(define (mpd-serialize-port field-name value)
+  (when (string? value)
+    (warning (G_ "string value for '~a' is deprecated, use integer instead~%") field-name))
+  (mpd-serialize-field field-name value))
+
+(define-maybe port (prefix mpd-))
+
+;;;
+
+;; Generic MPD plugin record, lists only the most prevalent fields.
+(define-configuration mpd-plugin
+  (plugin
+   maybe-string
+   "Plugin name.")
+
+  (name
+   maybe-string
+   "Name.")
+
+  (enabled?
+   maybe-boolean
+   "Whether the plugin is enabled/disabled.")
+
+  (extra-options
+   (alist '())
+   "An association list of option symbols/strings to string values to be appended to
+the plugin configuration. See
+@uref{https://mpd.readthedocs.io/en/latest/plugins.html,MPD plugin reference}
+for available options.")
+
+  (prefix mpd-subsystem-))
+
+;; Translate <mpd-configuration> field name into block name for mpd.conf"
+(define mpd-subsystem-name
+  (match-lambda
+    ('archive-plugins "archive_plugin")
+    ('playlist-plugins "playlist_plugin")
+    ('inputs "input")
+    ('decoders "decoder")
+    ('filters "filter")
+    (x (symbol->string x))))
+
+(define (mpd-serialize-mpd-plugin field-name value)
+  #~(string-append "\n" #$(mpd-subsystem-name field-name) " {\n"
+                   #$(serialize-configuration value mpd-plugin-fields)
+                   "}\n"))
+
+(define (mpd-serialize-list-of-mpd-plugin field-name value)
+  #~(string-append #$@(map (cut mpd-serialize-mpd-plugin field-name <>) value)))
+
+(define list-of-mpd-plugin? (list-of mpd-plugin?))
+
+(define-maybe mpd-plugin (prefix mpd-))
+
+(define-configuration mpd-partition
+  (name
+   string
+   "Partition name.")
+
+  (extra-options
+   (alist '())
+   "An association list of option symbols/strings to string values to be appended to
+the partition configuration. See
+@uref{https://mpd.readthedocs.io/en/latest/user.html#configuring-partitions,Configuring Partitions}
+for available options.")
+
+  (prefix mpd-subsystem-))
+
+(define (mpd-serialize-mpd-partition field-name value)
+  #~(string-append "\npartition {\n"
+                   #$(serialize-configuration value mpd-partition-fields)
+                   "}\n"))
+
+(define (mpd-serialize-list-of-mpd-partition field-name value)
+  #~(string-append #$@(map (cut mpd-serialize-mpd-partition #f <>) value)))
+
+(define list-of-mpd-partition?
+  (list-of mpd-partition?))
 
 (define-configuration mpd-output
   (name
    (string "MPD")
    "The name of the audio output.")
+
   (type
    (string "pulse")
    "The type of audio output.")
+
   (enabled?
    (boolean #t)
    "Specifies whether this audio output is enabled when MPD is started. By
 default, all audio outputs are enabled. This is just the default
 setting when there is no state file; with a state file, the previous
 state is restored.")
+
+  (format
+   maybe-string
+   "Force a specific audio format on output. See
+@uref{https://mpd.readthedocs.io/en/latest/user.html#audio-output-format,Global Audio Format}
+for a more detailed description.")
+   
   (tags?
    (boolean #t)
    "If set to @code{#f}, then MPD will not send tags to this output. This
 is only useful for output plugins that can receive tags, for example the
 @code{httpd} output plugin.")
+
   (always-on?
    (boolean #f)
    "If set to @code{#t}, then MPD attempts to keep this audio output always
 open. This may be useful for streaming servers, when you don’t want to
 disconnect all listeners even when playback is accidentally stopped.")
+
   (mixer-type
    (string "none")
-   "This field accepts a symbol that specifies which mixer should be used
+   "This field accepts a string that specifies which mixer should be used
 for this audio output: the @code{hardware} mixer, the @code{software}
 mixer, the @code{null} mixer (allows setting the volume, but with no
 effect; this can be used as a trick to implement an external mixer
 External Mixer) or no mixer (@code{none}).")
+
+  (replay-gain-handler
+   maybe-string
+   "This field accepts a string that specifies how
+@uref{https://mpd.readthedocs.io/en/latest/user.html#replay-gain,Replay Gain} is
+to be applied. @code{software} uses an internal software volume control,
+@code{mixer} uses the configured (hardware) mixer control and @code{none}
+disables replay gain on this audio output.")
+
   (extra-options
-   (free-form-args '())
-   "An association list of option symbols to string values to be appended to
+   (alist '())
+   "An association list of option symbols/strings to string values to be appended to
 the audio output configuration.")
+
   (prefix mpd-subsystem-))
 
+(define (mpd-serialize-mpd-output field-name value)
+  #~(string-append "\naudio_output {\n"
+                   #$(serialize-configuration value mpd-output-fields)
+                   "}\n"))
+
+(define (mpd-serialize-list-of-mpd-output field-name value)
+  #~(string-append #$@(map (cut mpd-serialize-mpd-output #f <>) value)))
+
 (define list-of-mpd-output?
   (list-of mpd-output?))
 
 (define-configuration mpd-configuration
+  (package
+   (file-like mpd)
+   "The MPD package."
+   empty-serializer)
+
   (user
    (string "mpd")
    "The user to run mpd as.")
-  (music-dir
-   (string "~/Music")
+
+  (group
+   maybe-string
+   "The group to run mpd as.")
+
+  (shepherd-requirement
+   (list-of-symbol '())
+   "This is a list of symbols naming Shepherd services that this service
+will depend on."
+   empty-serializer)
+
+  (log-file
+   (maybe-string "/var/log/mpd/log")
+   "The location of the log file. Set to @code{syslog} to use the local syslog daemon or
+@code{%unset-value} to omit this directive from the configuration file.")
+
+  (log-level
+   maybe-string
+   "Supress any messages below this threshold. Available values: @code{notice},
+@code{info}, @code{verbose}, @code{warning} and @code{error}.")
+
+  (music-directory
+   maybe-string
+   "The directory to scan for music files.")
+
+  (music-dir ; TODO: deprecated, remove later
+   maybe-string
    "The directory to scan for music files."
-   (lambda (_ x)
-     (mpd-serialize-field "music_directory" x)))
-  (playlist-dir
-   (string "~/.mpd/playlists")
+   mpd-serialize-deprecated-field)
+
+  (playlist-directory
+   maybe-string
+   "The directory to store playlists.")
+
+  (playlist-dir ; TODO: deprecated, remove later
+   maybe-string
    "The directory to store playlists."
-   (lambda (_ x)
-     (mpd-serialize-field "playlist_directory" x)))
+   mpd-serialize-deprecated-field)
+
   (db-file
-   (string "~/.mpd/tag_cache")
+   maybe-string
    "The location of the music database.")
+
   (state-file
-   (string "~/.mpd/state")
+   maybe-string
    "The location of the file that stores current MPD's state.")
+
   (sticker-file
-   (string "~/.mpd/sticker.sql")
+   maybe-string
    "The location of the sticker database.")
+
   (port
-   (string "6600")
+   (maybe-port 6600) ; TODO: switch to integer
    "The port to run mpd on.")
-  (address
-   (string "any")
+
+  (addresses
+   maybe-list-of-string
+   "The addresses that mpd will bind to.
+To use a Unix domain socket, an absolute path can be specified here."
+   (lambda (_ x)
+     (if (maybe-value-set? x)
+         #~(string-append #$@(map
+                              (cut mpd-serialize-field "bind_to_address" <>)
+                              x)) "")))
+
+  (address ; TODO: deprecated, remove later
+   maybe-string
    "The address that mpd will bind to.
 To use a Unix domain socket, an absolute path can be specified here."
+   mpd-serialize-deprecated-field)
+
+  (database
+   maybe-mpd-plugin
+   "MPD database plugin configuration.")
+
+  (partitions
+   (list-of-mpd-partition '())
+   "List of MPD \"partitions\".")
+  
+  (neighbors
+   (list-of-mpd-plugin '())
+   "List of MPD neighbor plugin configurations.")
+
+  (inputs
+   (list-of-mpd-plugin '())
+   "List of MPD input plugin configurations.")
+
+  (archive-plugins
+   (list-of-mpd-plugin '())
+   "List of MPD archive plugin configurations.")
+
+  (input-cache-size
+   maybe-string
+   "MPD input cache size."
    (lambda (_ x)
-     (mpd-serialize-field "bind_to_address" x)))
+     (if (maybe-value-set? x)
+         #~(string-append "\ninput_cache {\n"
+                          #$(mpd-subsystem-serialize-string "size" x)
+                          "}\n") "")))
+
+  (decoders
+   (list-of-mpd-plugin '())
+   "List of MPD decoder plugin configurations.")
+
+  (resampler
+   maybe-mpd-plugin
+   "MPD resampler plugin configuration.")
+
+  (filters
+   (list-of-mpd-plugin '())
+   "List of MPD filter plugin configurations.")
+
   (outputs
    (list-of-mpd-output (list (mpd-output)))
    "The audio outputs that MPD can use.
 By default this is a single output using pulseaudio.")
+
+  (playlist-plugins
+   (list-of-mpd-plugin '())
+   "List of MPD playlist plugin configurations.")
+
+  (extra-options
+   (alist '())
+   "An association list of option symbols/strings to string values to be appended to
+the configuration.")
+
   (prefix mpd-))
 
-(define (mpd-file-name config file)
-  "Return a path in /var/run/mpd/ that is writable
-   by @code{user} from @code{config}."
-  (string-append "/var/run/mpd/"
-                 (mpd-configuration-user config)
-                 "/" file))
+(define (mpd-serialize-configuration configuration)
+  (mixed-text-file
+   "mpd.conf"
+   (serialize-configuration configuration mpd-configuration-fields)))
+
+(define (mpd-log-rotation config)
+  (match-record config <mpd-configuration> (log-file)
+    (log-rotation
+     (files (list log-file))
+     (post-rotate #~(begin
+                      (use-modules (gnu services herd))
+                      (with-shepherd-action 'mpd ('reload) #f))))))
 
 (define (mpd-shepherd-service config)
-  (shepherd-service
-   (documentation "Run the MPD (Music Player Daemon)")
-   (requirement '(user-processes))
-   (provision '(mpd))
-   (start #~(make-forkexec-constructor
-             (list #$(file-append mpd "/bin/mpd")
-                   "--no-daemon"
-                   #$(mpd-serialize-configuration config))
-             #:environment-variables
-             ;; Required to detect PulseAudio when run under a user account.
-             (list (string-append
-                    "XDG_RUNTIME_DIR=/run/user/"
-                    (number->string
-                     (passwd:uid
-                      (getpwnam #$(mpd-configuration-user config))))))
-             #:log-file #$(mpd-file-name config "log")))
-   (stop  #~(make-kill-destructor))))
+  (match-record config <mpd-configuration> (user package shepherd-requirement)
+    (let* ((config-file (mpd-serialize-configuration config)))
+      (shepherd-service
+       (documentation "Run the MPD (Music Player Daemon)")
+       (requirement `(user-processes loopback ,@shepherd-requirement))
+       (provision '(mpd))
+       (start #~(make-forkexec-constructor
+                 (list #$(file-append package "/bin/mpd")
+                       "--no-daemon"
+                       #$config-file)
+                 #:environment-variables
+                 ;; Required to detect PulseAudio when run under a user account.
+                 (list (string-append "XDG_RUNTIME_DIR=/run/user/"
+                                      (number->string (passwd:uid (getpwnam #$user)))))))
+       (stop  #~(make-kill-destructor))
+       (actions
+        (list (shepherd-configuration-action config-file)
+              (shepherd-action
+               (name 'reload)
+               (documentation "Reopen log files and flush caches.")
+               (procedure #~(lambda (pid)
+                              (if pid
+                                  (begin (kill pid SIGHUP)
+                                         (format #t "Issued SIGHUP to Service MPD (PID ~a)." pid))
+                                  (format #t "Service MPD is not running.")))))))))))
 
 (define (mpd-service-activation config)
-  (with-imported-modules '((guix build utils))
+  (match-record config <mpd-configuration> (user log-file)
     #~(begin
         (use-modules (guix build utils))
-        (define %user
-          (getpw #$(mpd-configuration-user config)))
-
-        (let ((directory #$(mpd-file-name config ".mpd")))
-          (mkdir-p directory)
-          (chown directory (passwd:uid %user) (passwd:gid %user))
-
-          ;; Make /var/run/mpd/USER user-owned as well.
-          (chown (dirname directory)
-                 (passwd:uid %user) (passwd:gid %user))))))
-
-
-(define %mpd-accounts
-  ;; Default account and group for MPD.
-  (list (user-group (name "mpd") (system? #t))
-        (user-account
-         (name "mpd")
-         (group "mpd")
-         (system? #t)
-         (comment "Music Player Daemon (MPD) user")
 
-         ;; Note: /var/run/mpd hosts one sub-directory per user, of which
-         ;; /var/run/mpd/mpd corresponds to the "mpd" user.
-         (home-directory "/var/run/mpd/mpd")
+        (let* ((user (getpw #$user))
+               (deprecated-directory (string-append "/var/run/mpd/" #$user "/.mpd"))
+               (new-directory (string-append (passwd:dir user) "/.config/mpd")))
+          ;; TODO: remove me, migrates from the old location at /var/run/mpd to the new one at /var/lib/mpd.
+          (when (and (file-exists? deprecated-directory) (not (file-exists? new-directory)))
+            (rename-file deprecated-directory new-directory)
+            (chown new-directory (passwd:uid user) (passwd:gid user)))
+          (mkdir-p (dirname #$log-file))))))
 
-         (shell (file-append shadow "/sbin/nologin")))))
+(define (mpd-accounts config)
+  (match-record config <mpd-configuration> (user)
+    (list (user-account
+           (name user)
+           (group "nogroup")
+           (system? #t)
+           (comment "Music Player Daemon (MPD) user")
+           (home-directory "/var/lib/mpd")  ; MPD can use $HOME (or $XDG_CONFIG_HOME) to place its data
+           (shell (file-append shadow "/sbin/nologin"))))))
 
 (define mpd-service-type
   (service-type
@@ -234,7 +468,9 @@ (define mpd-service-type
     (list (service-extension shepherd-root-service-type
                              (compose list mpd-shepherd-service))
           (service-extension account-service-type
-                             (const %mpd-accounts))
+                             mpd-accounts)
           (service-extension activation-service-type
-                             mpd-service-activation)))
+                             mpd-service-activation)
+          (service-extension rottlog-service-type
+                             (compose list mpd-log-rotation))))
    (default-value (mpd-configuration))))

base-commit: 81191e3410cc00c6438f532599dd0b96d521982f
prerequisite-patch-id: 3b756bb4d930897ce79b1e51cc25478cb73fd9d8
-- 
2.38.1





^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [bug#59866] [PATCH 0/2] services: mpd: Refactor MPD service
  2022-12-07 18:27 ` Liliana Marie Prikler
@ 2022-12-08 13:11   ` mirai
  2022-12-08 13:35     ` [bug#54986] " Liliana Marie Prikler
  0 siblings, 1 reply; 29+ messages in thread
From: mirai @ 2022-12-08 13:11 UTC (permalink / raw)
  To: Liliana Marie Prikler, 59866; +Cc: 54986

On 2022-12-07 18:27, Liliana Marie Prikler wrote:
> Note that there is [1], which attempts to make it so that shepherd
> endpoints can be specified in lieu of MPD endpoints.  You don't need to
> implement this logic – you are free to do so if you want to – but could
> you make it so that there is an explicit endpoint abstraction that
> would allow for extension later on?
> 
> Cheers
> 
> [1] https://issues.guix.gnu.org/54986

Hi,

After reading issue #54986, regarding mpd escaping shepherd management,
my guess is that there could be two issues at play here:

* mpd.conf was serialized with pid_file [1]. The safest is
to actually not serialize any unused directives and let MPD decide
based on what's actually present. pid_file should only be set if
MPD is to be launched as a "daemon process". [2]

* But the service definition for MPD is launched with '--no-daemon' option
as seen in [3], which could be triggering a bug in MPD. It's probably worth
testing the combinations of having pid_file set and invoking --no-daemon.


> but could
> you make it so that there is an explicit endpoint abstraction that
> would allow for extension later on?

If I'm understanding correctly what [1] is about, I think this can be
implemented through the 'extend' interface. See how
certbot adds a nginx-server-configuration to a nginx-service-type
through the 'extend' interface. For MPD, this would amount to
appending the 'adresses' field with "adequately formatted" values.


[1]: https://git.savannah.gnu.org/cgit/guix.git/tree/gnu/services/audio.scm?id=1b6172d5f6ca22f0fa02cd1335b1b90e9501b731#n116
[2]: https://mpd.readthedocs.io/en/latest/user.html#starting-and-stopping-mpd
[3]: https://git.savannah.gnu.org/cgit/guix.git/tree/gnu/services/audio.scm?id=1b6172d5f6ca22f0fa02cd1335b1b90e9501b731#n145




^ permalink raw reply	[flat|nested] 29+ messages in thread

* [bug#54986] [PATCH 0/2] services: mpd: Refactor MPD service
  2022-12-08 13:11   ` mirai
@ 2022-12-08 13:35     ` Liliana Marie Prikler
  2022-12-09 13:44       ` [bug#59866] " mirai
  0 siblings, 1 reply; 29+ messages in thread
From: Liliana Marie Prikler @ 2022-12-08 13:35 UTC (permalink / raw)
  To: mirai, 59866; +Cc: 54986

Am Donnerstag, dem 08.12.2022 um 13:11 +0000 schrieb mirai:
> On 2022-12-07 18:27, Liliana Marie Prikler wrote:
> > Note that there is [1], which attempts to make it so that shepherd
> > endpoints can be specified in lieu of MPD endpoints.  You don't
> > need to implement this logic – you are free to do so if you want to
> > – but could you make it so that there is an explicit endpoint
> > abstraction that would allow for extension later on?
> > 
> > Cheers
> > 
> > [1] https://issues.guix.gnu.org/54986
> 
> Hi,
> 
> After reading issue #54986, regarding mpd escaping shepherd
> management, my guess is that there could be two issues at play here:
> 
> * mpd.conf was serialized with pid_file [1]. The safest is
> to actually not serialize any unused directives and let MPD decide
> based on what's actually present. pid_file should only be set if
> MPD is to be launched as a "daemon process". [2]
> 
> * But the service definition for MPD is launched with '--no-daemon'
> option as seen in [3], which could be triggering a bug in MPD. It's
> probably worth testing the combinations of having pid_file set and
> invoking --no-daemon.
Nice catch, but completely unrelated to the issue.  In neither case do
we ever spawn MPD as a regular daemon, yet only in the systemd case
does shepherd have a problem with it.  That statement was concerning
shepherd's (mis)management of sockets, not MPD.

> > but could you make it so that there is an explicit endpoint
> > abstraction that would allow for extension later on?
> 
> If I'm understanding correctly what [1] is about, I think this can be
> implemented through the 'extend' interface. See how certbot adds a
> nginx-server-configuration to a nginx-service-type through the
> 'extend' interface. For MPD, this would amount to appending the
> 'adresses' field with "adequately formatted" values.
This doesn't work for #54986, which makes it so that in-file addresses
are ignored in favour of handing over the sockets directly through
shepherd.  Looking at [4], it appears the meaning of "port" is closer
to that of a default port, as addresses can have ports in them.  But I
would still prefer addresses to be "endpoints", which if they happen to
be a list of strings are taken as MPD addresses and if they happen to
be shepherd endpoints are passed on to the shepherd service.

WDYT?

[4] https://mpd.readthedocs.io/en/stable/user.html#client-connections




^ permalink raw reply	[flat|nested] 29+ messages in thread

* [bug#59866] [PATCH 0/2] services: mpd: Refactor MPD service
  2022-12-08 13:35     ` [bug#54986] " Liliana Marie Prikler
@ 2022-12-09 13:44       ` mirai
  2022-12-09 19:22         ` Liliana Marie Prikler
  0 siblings, 1 reply; 29+ messages in thread
From: mirai @ 2022-12-09 13:44 UTC (permalink / raw)
  To: Liliana Marie Prikler, 59866; +Cc: 54986

On 2022-12-08 13:35, Liliana Marie Prikler wrote:
> This doesn't work for #54986, which makes it so that in-file addresses
> are ignored in favour of handing over the sockets directly through
> shepherd.  Looking at [4], it appears the meaning of "port" is closer
> to that of a default port, as addresses can have ports in them.  But I
> would still prefer addresses to be "endpoints", which if they happen to
> be a list of strings are taken as MPD addresses and if they happen to
> be shepherd endpoints are passed on to the shepherd service.

Are you proposing for the 'addresses' field to be a
"maybe-list-of-string-or-shepherd-endpoint"? (more of a xor as they can't
be used simultaneously)
Example:

--8<---------------cut here---------------start------------->8---
;; should fire a error message during guix system reconfigure
(mpd-configuration
  (addresses `("[::]:6645"  
               ,(shepherd-endpoint
                  (address "/var/run/mpd-shepherd-socket")))))
--8<---------------cut here---------------end--------------->8---

I don't think it breaks backward compatibility to introduce this
after #59866 is merged.
The type of field 'addresses' could be changed transparently to something like:

--8<---------------cut here---------------start------------->8---
(define list-of-addresses (list-of (lambda (x) (or (string? x) (shepherd-endpoint? x)))))
--8<---------------cut here---------------end--------------->8---




^ permalink raw reply	[flat|nested] 29+ messages in thread

* [bug#59866] [PATCH 0/2] services: mpd: Refactor MPD service
  2022-12-09 13:44       ` [bug#59866] " mirai
@ 2022-12-09 19:22         ` Liliana Marie Prikler
  2022-12-11 12:05           ` mirai
  0 siblings, 1 reply; 29+ messages in thread
From: Liliana Marie Prikler @ 2022-12-09 19:22 UTC (permalink / raw)
  To: mirai, 59866; +Cc: 54986

Am Freitag, dem 09.12.2022 um 13:44 +0000 schrieb mirai:
> On 2022-12-08 13:35, Liliana Marie Prikler wrote:
> > This doesn't work for #54986, which makes it so that in-file
> > addresses are ignored in favour of handing over the sockets
> > directly through shepherd.  Looking at [4], it appears the meaning
> > of "port" is closer to that of a default port, as addresses can
> > have ports in them. 
> > But I would still prefer addresses to be "endpoints", which if they
> > happen to be a list of strings are taken as MPD addresses and if
> > they happen to be shepherd endpoints are passed on to the shepherd
> > service.
> 
> Are you proposing for the 'addresses' field to be a
> "maybe-list-of-string-or-shepherd-endpoint"? (more of a xor as they
> can't be used simultaneously)
> Example:
> 
> --8<---------------cut here---------------start------------->8---
> ;; should fire a error message during guix system reconfigure
> (mpd-configuration
>   (addresses `("[::]:6645"  
>                ,(shepherd-endpoint
>                   (address "/var/run/mpd-shepherd-socket")))))
> --8<---------------cut here---------------end--------------->8---
> 
> I don't think it breaks backward compatibility to introduce this
> after #59866 is merged.
> The type of field 'addresses' could be changed transparently to
> something like:
> 
> --8<---------------cut here---------------start------------->8---
> (define list-of-addresses (list-of (lambda (x) (or (string? x)
> (shepherd-endpoint? x)))))
> --8<---------------cut here---------------end--------------->8---
Something like that, but I don't think the vocabulary matches 1:1.  In
my opinion, an address is an endpoint – not a shepherd endpoint, but an
endpoint still – while a shepherd endpoint is not an address.  Thus, I
propose changing the vocabulary now to not break backwards
compatibility later.  IIUC, the change from the previous records to
define-configuration is already an API change, so it'd be good to have
both in the same series.

Cheers




^ permalink raw reply	[flat|nested] 29+ messages in thread

* [bug#59866] [PATCH 0/2] services: mpd: Refactor MPD service
  2022-12-09 19:22         ` Liliana Marie Prikler
@ 2022-12-11 12:05           ` mirai
  0 siblings, 0 replies; 29+ messages in thread
From: mirai @ 2022-12-11 12:05 UTC (permalink / raw)
  To: Liliana Marie Prikler, 59866; +Cc: 54986

On 2022-12-09 19:22, Liliana Marie Prikler wrote:
> Something like that, but I don't think the vocabulary matches 1:1.  In
> my opinion, an address is an endpoint – not a shepherd endpoint, but an
> endpoint still – while a shepherd endpoint is not an address.  Thus, I
> propose changing the vocabulary now to not break backwards
> compatibility later.
You prefer the 'addresses' field to be renamed to 'endpoints', is that correct?

IIUC, the change from the previous records to
> define-configuration is already an API change, so it'd be good to have
> both in the same series.
The first patch in the series changes from define-record-type* to define-configuration
but it does not add anything, I don't think there are actually any (exported)
API changes visible to the user.

Only the second patch starts to introduce visible API changes.

Cheers





^ permalink raw reply	[flat|nested] 29+ messages in thread

* [bug#59866] [PATCH v3] services: mpd: Refactor MPD service.
  2022-12-06 23:22 [bug#59866] [PATCH 0/2] services: mpd: Refactor MPD service mirai
                   ` (4 preceding siblings ...)
  2022-12-08  0:59 ` [bug#59866] [PATCH v2] " mirai
@ 2022-12-16 14:15 ` mirai
  2022-12-21 14:15 ` [bug#59866] [PATCH v4 1/2] services: mpd: use 'define-configuration' mirai
                   ` (5 subsequent siblings)
  11 siblings, 0 replies; 29+ messages in thread
From: mirai @ 2022-12-16 14:15 UTC (permalink / raw)
  To: 59866; +Cc: Bruno Victal

From: Bruno Victal <mirai@makinata.eu>

Introduces 'mpd-plugin' and 'mpd-partition' records.
Expands 'mpd-output' and 'mpd-configuration' records.
Deprecates redundant abbreviated fields in 'mpd-configuration' and
avoids serializing unused fields that may introduce undesired behavior.
Implement log-rotation via rottlog.
Implements Shepherd actions: 'reopen' and 'configuration'.
---

Changes from v2 to v3:
* Removed "pretty-formatting" code to reduce overall code "weight".
* Renamed shepherd action 'reload' to 'reopen' as reloading would suggest
that it re-reads the config file.
* Renamed 'addresses' field to 'endpoints'.
* Allow mpd-plugin records to be used in 'outputs' field.
Technically an audio output is a mpd-plugin but since there
are numerous common audio output specific options it's worthwhile
having a mpd-output record-type for it.
* mpd-plugin serialization was simplified and the "subsystem block name"
was integrated directly into mpd-configuration record-type to better
track which field corresponds to which subsystem rather than keep an
external dictionary that maps the fields to the subsystem names.

 doc/guix.texi          | 172 +++++++++++++---
 gnu/services/audio.scm | 434 +++++++++++++++++++++++++++++++----------
 2 files changed, 476 insertions(+), 130 deletions(-)

diff --git a/doc/guix.texi b/doc/guix.texi
index 5cb5ae1dfe..3f186aefa9 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -109,6 +109,7 @@ Copyright @copyright{} 2022 Reily Siegel@*
 Copyright @copyright{} 2022 Simon Streit@*
 Copyright @copyright{} 2022 (@*
 Copyright @copyright{} 2022 John Kehayias@*
+Copyright @copyright{} 2022 Bruno Victal@*
 
 Permission is granted to copy, distribute and/or modify this document
 under the terms of the GNU Free Documentation License, Version 1.3 or
@@ -33007,79 +33008,184 @@ The service type for @command{mpd}
 Data type representing the configuration of @command{mpd}.
 
 @table @asis
-@item @code{user} (default: @code{"mpd"})
+@item @code{package} (default: @code{mpd}) (type: file-like)
+The MPD package.
+
+@item @code{user} (default: @code{"mpd"}) (type: string)
 The user to run mpd as.
 
-@item @code{music-dir} (default: @code{"~/Music"})
+@item @code{group} (type: maybe-string)
+The group to run mpd as.
+
+@item @code{shepherd-requirement} (default: @code{()}) (type: list-of-symbol)
+This is a list of symbols naming Shepherd services that this service
+will depend on.
+
+@item @code{log-file} (default: @code{"/var/log/mpd/log"}) (type: maybe-string)
+The location of the log file.  Set to @code{syslog} to use the local
+syslog daemon or @code{%unset-value} to omit this directive from the
+configuration file.
+
+@item @code{log-level} (type: maybe-string)
+Supress any messages below this threshold.  Available values:
+@code{notice}, @code{info}, @code{verbose}, @code{warning} and
+@code{error}.
+
+@item @code{music-directory} (type: maybe-string)
 The directory to scan for music files.
 
-@item @code{playlist-dir} (default: @code{"~/.mpd/playlists"})
+@item @code{playlist-directory} (type: maybe-string)
 The directory to store playlists.
 
-@item @code{db-file} (default: @code{"~/.mpd/tag_cache"})
+@item @code{db-file} (type: maybe-string)
 The location of the music database.
 
-@item @code{state-file} (default: @code{"~/.mpd/state"})
+@item @code{state-file} (type: maybe-string)
 The location of the file that stores current MPD's state.
 
-@item @code{sticker-file} (default: @code{"~/.mpd/sticker.sql"})
+@item @code{sticker-file} (type: maybe-string)
 The location of the sticker database.
 
-@item @code{port} (default: @code{"6600"})
+@item @code{port} (default: @code{6600}) (type: maybe-integer)
 The port to run mpd on.
 
-@item @code{address} (default: @code{"any"})
-The address that mpd will bind to.  To use a Unix domain socket,
-an absolute path can be specified here.
+@item @code{endpoints} (type: maybe-list-of-string)
+The addresses that mpd will bind to.  To use a Unix domain socket, an
+absolute path can be specified here.
+
+@item @code{database} (type: maybe-mpd-plugin)
+MPD database plugin configuration.
+
+@item @code{partitions} (default: @code{()}) (type: list-of-mpd-partition)
+List of MPD "partitions".
+
+@item @code{neighbors} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD neighbor plugin configurations.
 
-@item @code{outputs} (default: @code{"(list (mpd-output))"})
-The audio outputs that MPD can use.  By default this is a single output using pulseaudio.
+@item @code{inputs} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD input plugin configurations.
+
+@item @code{archive-plugins} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD archive plugin configurations.
+
+@item @code{input-cache-size} (type: maybe-string)
+MPD input cache size.
+
+@item @code{decoders} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD decoder plugin configurations.
+
+@item @code{resampler} (type: maybe-mpd-plugin)
+MPD resampler plugin configuration.
+
+@item @code{filters} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD filter plugin configurations.
+
+@item @code{outputs} (type: list-of-mpd-plugin-or-output)
+The audio outputs that MPD can use.  By default this is a single output
+using pulseaudio.
+
+@item @code{playlist-plugins} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD playlist plugin configurations.
+
+@item @code{extra-options} (default: @code{()}) (type: alist)
+An association list of option symbols/strings to string values to be
+appended to the configuration.
+
+@end table
+@end deftp
+
+@deftp {Data Type} mpd-plugin
+Data type representing a @command{mpd} plugin.
+
+@table @asis
+@item @code{plugin} (type: maybe-string)
+Plugin name.
+
+@item @code{name} (type: maybe-string)
+Name.
+
+@item @code{enabled?} (type: maybe-boolean)
+Whether the plugin is enabled/disabled.
+
+@item @code{extra-options} (default: @code{()}) (type: alist)
+An association list of option symbols/strings to string values to be
+appended to the plugin configuration.  See
+@uref{https://mpd.readthedocs.io/en/latest/plugins.html,MPD plugin
+reference} for available options.
+
+@end table
+@end deftp
+
+@deftp {Data Type} mpd-partition
+Data type representing a @command{mpd} partition.
+
+@table @asis
+@item @code{name} (type: string)
+Partition name.
+
+@item @code{extra-options} (default: @code{()}) (type: alist)
+An association list of option symbols/strings to string values to be
+appended to the partition configuration.  See
+@uref{https://mpd.readthedocs.io/en/latest/user.html#configuring-partitions,Configuring
+Partitions} for available options.
 
 @end table
 @end deftp
 
 @deftp {Data Type} mpd-output
-Data type representing an @command{mpd} audio output.
+Data type representing a @command{mpd} audio output.
 
 @table @asis
-@item @code{name} (default: @code{"MPD"})
+@item @code{name} (default: @code{"MPD"}) (type: string)
 The name of the audio output.
 
-@item @code{type} (default: @code{"pulse"})
+@item @code{type} (default: @code{"pulse"}) (type: string)
 The type of audio output.
 
-@item @code{enabled?} (default: @code{#t})
+@item @code{enabled?} (default: @code{#t}) (type: boolean)
 Specifies whether this audio output is enabled when MPD is started.  By
 default, all audio outputs are enabled.  This is just the default
 setting when there is no state file; with a state file, the previous
 state is restored.
 
-@item @code{tags?} (default: @code{#t})
+@item @code{format} (type: maybe-string)
+Force a specific audio format on output.  See
+@uref{https://mpd.readthedocs.io/en/latest/user.html#audio-output-format,Global
+Audio Format} for a more detailed description.
+
+@item @code{tags?} (default: @code{#t}) (type: boolean)
 If set to @code{#f}, then MPD will not send tags to this output.  This
 is only useful for output plugins that can receive tags, for example the
 @code{httpd} output plugin.
 
-@item @code{always-on?} (default: @code{#f})
+@item @code{always-on?} (default: @code{#f}) (type: boolean)
 If set to @code{#t}, then MPD attempts to keep this audio output always
-open.  This may be useful for streaming servers, when you don’t want to
+open.  This may be useful for streaming servers, when you don?t want to
 disconnect all listeners even when playback is accidentally stopped.
 
-@item @code{mixer-type}
-This field accepts a symbol that specifies which mixer should be used
+@item @code{mixer-type} (default: @code{"none"}) (type: string)
+This field accepts a string that specifies which mixer should be used
 for this audio output: the @code{hardware} mixer, the @code{software}
 mixer, the @code{null} mixer (allows setting the volume, but with no
 effect; this can be used as a trick to implement an external mixer
 External Mixer) or no mixer (@code{none}).
 
-@item @code{extra-options} (default: @code{'()})
-An association list of option symbols to string values to be appended to
-the audio output configuration.
+@item @code{replay-gain-handler} (type: maybe-string)
+This field accepts a string that specifies how
+@uref{https://mpd.readthedocs.io/en/latest/user.html#replay-gain,Replay
+Gain} is to be applied.  @code{software} uses an internal software
+volume control, @code{mixer} uses the configured (hardware) mixer
+control and @code{none} disables replay gain on this audio output.
+
+@item @code{extra-options} (default: @code{()}) (type: alist)
+An association list of option symbols/strings to string values to be
+appended to the audio output configuration.
 
 @end table
 @end deftp
 
-The following example shows a configuration of @code{mpd} that provides
-an HTTP audio streaming output.
+The following example shows a configuration of @command{mpd} that
+configures some of its plugins and provides a HTTP audio streaming output.
 
 @lisp
 (service mpd-service-type
@@ -33091,7 +33197,19 @@ an HTTP audio streaming output.
                      (mixer-type 'null)
                      (extra-options
                       `((encoder . "vorbis")
-                        (port    . "8080"))))))))
+                        (port    . "8080"))))))
+           (decoders
+             (list (mpd-plugin
+                     (plugin "mikmod")
+                     (enabled? #f))
+                   (mpd-plugin
+                     (plugin "openmpt")
+                     (enabled? #t)
+                     (extra-options `((repeat-count . -1)
+                                      (interpolation-filter . 1))))))
+           (resampler (mpd-plugin
+                        (plugin "libsamplerate")
+                        (extra-options `((type . 0)))))))
 @end lisp
 
 
diff --git a/gnu/services/audio.scm b/gnu/services/audio.scm
index 2351db8a4a..aeb081380a 100644
--- a/gnu/services/audio.scm
+++ b/gnu/services/audio.scm
@@ -2,6 +2,7 @@
 ;;; Copyright © 2017 Peter Mikkelsen <petermikkelsen10@gmail.com>
 ;;; Copyright © 2019 Ricardo Wurmus <rekado@elephly.net>
 ;;; Copyright © 2020 Ludovic Courtès <ludo@gnu.org>
+;;; Copyright © 2022 Bruno Victal <mirai@makinata.eu>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -20,19 +21,27 @@
 
 (define-module (gnu services audio)
   #:use-module (guix gexp)
+  #:use-module (guix deprecation)
+  #:use-module (guix diagnostics)
+  #:use-module (guix i18n)
   #:use-module (gnu services)
   #:use-module (gnu services configuration)
   #:use-module (gnu services shepherd)
+  #:use-module (gnu services admin)
   #:use-module (gnu system shadow)
   #:use-module (gnu packages admin)
   #:use-module (gnu packages mpd)
   #:use-module (guix records)
   #:use-module (ice-9 match)
-  #:use-module (ice-9 format)
   #:use-module (srfi srfi-1)
+  #:use-module (srfi srfi-8)
   #:use-module (srfi srfi-26)
   #:export (mpd-output
             mpd-output?
+            mpd-plugin
+            mpd-plugin?
+            mpd-partition
+            mpd-partition?
             mpd-configuration
             mpd-configuration?
             mpd-service-type))
@@ -51,180 +60,397 @@ (define (uglify-field-name field-name)
                                #\-)
                  "_")))
 
-(define (free-form-args? val)
-  (match val
-    (() #t)
-    ((((? symbol?) . (? string?)) . val) (free-form-args? val))
-    (_ #f)))
+(define list-of-string?
+  (list-of string?))
 
-(define* (mpd-serialize-field field-name value #:optional (indent-level 0))
-  #~(begin
-      (use-modules ((ice-9 format)))
-      (format #f "~v/~a \"~a\"~%" #$indent-level #$(if (string? field-name)
-                                                       field-name
-                                                       (uglify-field-name field-name)) #$value)))
+(define list-of-symbol?
+  (list-of symbol?))
 
-(define* (mpd-serialize-free-form-args field-name value #:optional (indent-level 0))
-  (generic-serialize-alist string-append (cut mpd-serialize-field <> <> indent-level) value))
+(define (mpd-serialize-field field-name value)
+  (let ((field (if (string? field-name) field-name
+                   (uglify-field-name field-name)))
+        (value (if (boolean? value) (if value "yes" "no") value)))
+    #~(format #f "~a \"~a\"~%" #$field #$value)))
 
 (define mpd-serialize-number mpd-serialize-field)
 
 (define mpd-serialize-string mpd-serialize-field)
 
-(define* (mpd-serialize-boolean field-name value #:optional (indent-level 0))
-  (mpd-serialize-field field-name (if value "yes" "no") indent-level))
+(define mpd-serialize-boolean mpd-serialize-field)
 
-(define (mpd-serialize-list-of-mpd-output field-name value)
-  #~(string-append "\naudio_output {\n"
-                   #$@(map (cut serialize-configuration <>
-                                mpd-output-fields)
-                           value)
-                   "}\n"))
+(define* (mpd-serialize-alist field-name value)
+  #~(string-append #$@(generic-serialize-alist list mpd-serialize-field value)))
 
-(define (mpd-serialize-configuration configuration)
-  (mixed-text-file
-   "mpd.conf"
-   (serialize-configuration configuration mpd-configuration-fields)))
+(define-maybe string (prefix mpd-))
+(define-maybe list-of-string (prefix mpd-))
+(define-maybe boolean (prefix mpd-))
+
+;;; TODO: Procedures for deprecated fields, to be removed.
+
+(define mpd-deprecated-fields '((music-dir . music-directory)
+                                (playlist-dir . playlist-directory)
+                                (address . addresses)))
+
+(define (port? value) (or (string? value) (integer? value)))
+
+(define (mpd-serialize-deprecated-field field-name value)
+  (if (maybe-value-set? value)
+      (begin (warn-about-deprecation field-name #f
+                                     #:replacement (assoc-ref mpd-deprecated-fields field-name))
+             (match field-name
+               ('playlist-dir (mpd-serialize-string "playlist_directory" value))
+               ('music-dir (mpd-serialize-string "music_directory" value))
+               ('address (mpd-serialize-string "bind_to_address" value))))
+      ""))
+
+(define (mpd-serialize-port field-name value)
+  (when (string? value)
+    (warning (G_ "string value for '~a' is deprecated, use integer instead~%") field-name))
+  (mpd-serialize-field field-name value))
+
+(define-maybe port (prefix mpd-))
+
+;;;
+
+;; Generic MPD plugin record, lists only the most prevalent fields.
+(define-configuration mpd-plugin
+  (plugin
+   maybe-string
+   "Plugin name.")
+
+  (name
+   maybe-string
+   "Name.")
+
+  (enabled?
+   maybe-boolean
+   "Whether the plugin is enabled/disabled.")
+
+  (extra-options
+   (alist '())
+   "An association list of option symbols/strings to string values to be appended to
+the plugin configuration. See
+@uref{https://mpd.readthedocs.io/en/latest/plugins.html,MPD plugin reference}
+for available options.")
+
+  (prefix mpd-))
+
+(define (mpd-serialize-mpd-plugin field-name value)
+  #~(format #f "~a {~%~a}~%"
+            '#$field-name #$(serialize-configuration value mpd-plugin-fields)))
+
+(define (mpd-serialize-list-of-mpd-plugin field-name value)
+  #~(string-append #$@(map (cut mpd-serialize-mpd-plugin field-name <>) value)))
 
-(define mpd-subsystem-serialize-field (cut mpd-serialize-field <> <> 1))
-(define mpd-subsystem-serialize-string (cut mpd-serialize-string <> <> 1))
-(define mpd-subsystem-serialize-number (cut mpd-serialize-number <> <> 1))
-(define mpd-subsystem-serialize-boolean (cut mpd-serialize-boolean <> <> 1))
-(define mpd-subsystem-serialize-free-form-args (cut mpd-serialize-free-form-args <> <> 1))
+(define list-of-mpd-plugin? (list-of mpd-plugin?))
+
+(define-maybe mpd-plugin (prefix mpd-))
+
+(define-configuration mpd-partition
+  (name
+   string
+   "Partition name.")
+
+  (extra-options
+   (alist '())
+   "An association list of option symbols/strings to string values to be appended to
+the partition configuration. See
+@uref{https://mpd.readthedocs.io/en/latest/user.html#configuring-partitions,Configuring Partitions}
+for available options.")
+
+  (prefix mpd-))
+
+(define (mpd-serialize-mpd-partition field-name value)
+  #~(format #f "partition {~%~a}~%"
+            #$(serialize-configuration value mpd-partition-fields)))
+
+(define (mpd-serialize-list-of-mpd-partition field-name value)
+  #~(string-append #$@(map (cut mpd-serialize-mpd-partition #f <>) value)))
+
+(define list-of-mpd-partition?
+  (list-of mpd-partition?))
 
 (define-configuration mpd-output
   (name
    (string "MPD")
    "The name of the audio output.")
+
   (type
    (string "pulse")
    "The type of audio output.")
+
   (enabled?
    (boolean #t)
    "Specifies whether this audio output is enabled when MPD is started. By
 default, all audio outputs are enabled. This is just the default
 setting when there is no state file; with a state file, the previous
 state is restored.")
+
+  (format
+   maybe-string
+   "Force a specific audio format on output. See
+@uref{https://mpd.readthedocs.io/en/latest/user.html#audio-output-format,Global Audio Format}
+for a more detailed description.")
+   
   (tags?
    (boolean #t)
    "If set to @code{#f}, then MPD will not send tags to this output. This
 is only useful for output plugins that can receive tags, for example the
 @code{httpd} output plugin.")
+
   (always-on?
    (boolean #f)
    "If set to @code{#t}, then MPD attempts to keep this audio output always
 open. This may be useful for streaming servers, when you don’t want to
 disconnect all listeners even when playback is accidentally stopped.")
+
   (mixer-type
    (string "none")
-   "This field accepts a symbol that specifies which mixer should be used
+   "This field accepts a string that specifies which mixer should be used
 for this audio output: the @code{hardware} mixer, the @code{software}
 mixer, the @code{null} mixer (allows setting the volume, but with no
 effect; this can be used as a trick to implement an external mixer
 External Mixer) or no mixer (@code{none}).")
+
+  (replay-gain-handler
+   maybe-string
+   "This field accepts a string that specifies how
+@uref{https://mpd.readthedocs.io/en/latest/user.html#replay-gain,Replay Gain} is
+to be applied. @code{software} uses an internal software volume control,
+@code{mixer} uses the configured (hardware) mixer control and @code{none}
+disables replay gain on this audio output.")
+
   (extra-options
-   (free-form-args '())
-   "An association list of option symbols to string values to be appended to
+   (alist '())
+   "An association list of option symbols/strings to string values to be appended to
 the audio output configuration.")
-  (prefix mpd-subsystem-))
 
-(define list-of-mpd-output?
-  (list-of mpd-output?))
+  (prefix mpd-))
+
+(define (mpd-serialize-mpd-output field-name value)
+  #~(format #f "audio_output {~%~a}~%"
+            #$(serialize-configuration value mpd-output-fields)))
+
+(define (mpd-serialize-list-of-mpd-plugin-or-output field-name value)
+  (receive (plugins outputs)
+      (partition mpd-plugin? value)
+    #~(string-append #$@(map (cut mpd-serialize-mpd-plugin "audio_output" <>) plugins)
+                     #$@(map (cut mpd-serialize-mpd-output #f <>) outputs))))
+
+(define list-of-mpd-plugin-or-output?
+  (list-of (lambda (x)
+             (or (mpd-output? x) (mpd-plugin? x)))))
 
 (define-configuration mpd-configuration
+  (package
+   (file-like mpd)
+   "The MPD package."
+   empty-serializer)
+
   (user
    (string "mpd")
    "The user to run mpd as.")
-  (music-dir
-   (string "~/Music")
+
+  (group
+   maybe-string
+   "The group to run mpd as.")
+
+  (shepherd-requirement
+   (list-of-symbol '())
+   "This is a list of symbols naming Shepherd services that this service
+will depend on."
+   empty-serializer)
+
+  (log-file
+   (maybe-string "/var/log/mpd/log")
+   "The location of the log file. Set to @code{syslog} to use the local syslog daemon or
+@code{%unset-value} to omit this directive from the configuration file.")
+
+  (log-level
+   maybe-string
+   "Supress any messages below this threshold. Available values: @code{notice},
+@code{info}, @code{verbose}, @code{warning} and @code{error}.")
+
+  (music-directory
+   maybe-string
+   "The directory to scan for music files.")
+
+  (music-dir ; TODO: deprecated, remove later
+   maybe-string
    "The directory to scan for music files."
-   (lambda (_ x)
-     (mpd-serialize-field "music_directory" x)))
-  (playlist-dir
-   (string "~/.mpd/playlists")
+   mpd-serialize-deprecated-field)
+
+  (playlist-directory
+   maybe-string
+   "The directory to store playlists.")
+
+  (playlist-dir ; TODO: deprecated, remove later
+   maybe-string
    "The directory to store playlists."
-   (lambda (_ x)
-     (mpd-serialize-field "playlist_directory" x)))
+   mpd-serialize-deprecated-field)
+
   (db-file
-   (string "~/.mpd/tag_cache")
+   maybe-string
    "The location of the music database.")
+
   (state-file
-   (string "~/.mpd/state")
+   maybe-string
    "The location of the file that stores current MPD's state.")
+
   (sticker-file
-   (string "~/.mpd/sticker.sql")
+   maybe-string
    "The location of the sticker database.")
+
   (port
-   (string "6600")
+   (maybe-port 6600) ; TODO: switch to integer
    "The port to run mpd on.")
-  (address
-   (string "any")
+
+  (endpoints
+   maybe-list-of-string
+   "The addresses that mpd will bind to.
+To use a Unix domain socket, an absolute path can be specified here."
+   (lambda (_ x)
+     (if (maybe-value-set? x)
+         #~(string-append #$@(map
+                              (cut mpd-serialize-field "bind_to_address" <>)
+                              x)) "")))
+
+  (address ; TODO: deprecated, remove later
+   maybe-string
    "The address that mpd will bind to.
 To use a Unix domain socket, an absolute path can be specified here."
+   mpd-serialize-deprecated-field)
+
+  (database
+   maybe-mpd-plugin
+   "MPD database plugin configuration.")
+
+  (partitions
+   (list-of-mpd-partition '())
+   "List of MPD \"partitions\".")
+  
+  (neighbors
+   (list-of-mpd-plugin '())
+   "List of MPD neighbor plugin configurations.")
+
+  (inputs
+   (list-of-mpd-plugin '())
+   "List of MPD input plugin configurations."
+   (lambda (_ x)
+     (mpd-serialize-list-of-mpd-plugin "input" x)))
+
+  (archive-plugins
+   (list-of-mpd-plugin '())
+   "List of MPD archive plugin configurations."
    (lambda (_ x)
-     (mpd-serialize-field "bind_to_address" x)))
+     (mpd-serialize-list-of-mpd-plugin "archive_plugin" x)))
+
+  (input-cache-size
+   maybe-string
+   "MPD input cache size."
+   (lambda (_ x)
+     (if (maybe-value-set? x)
+         #~(string-append "\ninput_cache {\n"
+                          #$(mpd-serialize-string "size" x)
+                          "}\n") "")))
+
+  (decoders
+   (list-of-mpd-plugin '())
+   "List of MPD decoder plugin configurations."
+   (lambda (_ x)
+     (mpd-serialize-list-of-mpd-plugin "decoder" x)))
+
+  (resampler
+   maybe-mpd-plugin
+   "MPD resampler plugin configuration.")
+
+  (filters
+   (list-of-mpd-plugin '())
+   "List of MPD filter plugin configurations."
+   (lambda (_ x)
+     (mpd-serialize-list-of-mpd-plugin "filter" x)))
+
   (outputs
-   (list-of-mpd-output (list (mpd-output)))
+   (list-of-mpd-plugin-or-output (list (mpd-output)))
    "The audio outputs that MPD can use.
 By default this is a single output using pulseaudio.")
+
+  (playlist-plugins
+   (list-of-mpd-plugin '())
+   "List of MPD playlist plugin configurations."
+   (lambda (_ x)
+     (mpd-serialize-list-of-mpd-plugin "playlist_plugin" x)))
+
+  (extra-options
+   (alist '())
+   "An association list of option symbols/strings to string values to be appended to
+the configuration.")
+
   (prefix mpd-))
 
-(define (mpd-file-name config file)
-  "Return a path in /var/run/mpd/ that is writable
-   by @code{user} from @code{config}."
-  (string-append "/var/run/mpd/"
-                 (mpd-configuration-user config)
-                 "/" file))
+(define (mpd-serialize-configuration configuration)
+  (mixed-text-file
+   "mpd.conf"
+   (serialize-configuration configuration mpd-configuration-fields)))
+
+(define (mpd-log-rotation config)
+  (match-record config <mpd-configuration> (log-file)
+    (log-rotation
+     (files (list log-file))
+     (post-rotate #~(begin
+                      (use-modules (gnu services herd))
+                      (with-shepherd-action 'mpd ('reopen) #f))))))
 
 (define (mpd-shepherd-service config)
-  (shepherd-service
-   (documentation "Run the MPD (Music Player Daemon)")
-   (requirement '(user-processes))
-   (provision '(mpd))
-   (start #~(make-forkexec-constructor
-             (list #$(file-append mpd "/bin/mpd")
-                   "--no-daemon"
-                   #$(mpd-serialize-configuration config))
-             #:environment-variables
-             ;; Required to detect PulseAudio when run under a user account.
-             (list (string-append
-                    "XDG_RUNTIME_DIR=/run/user/"
-                    (number->string
-                     (passwd:uid
-                      (getpwnam #$(mpd-configuration-user config))))))
-             #:log-file #$(mpd-file-name config "log")))
-   (stop  #~(make-kill-destructor))))
+  (match-record config <mpd-configuration> (user package shepherd-requirement)
+    (let* ((config-file (mpd-serialize-configuration config)))
+      (shepherd-service
+       (documentation "Run the MPD (Music Player Daemon)")
+       (requirement `(user-processes loopback ,@shepherd-requirement))
+       (provision '(mpd))
+       (start #~(make-forkexec-constructor
+                 (list #$(file-append package "/bin/mpd")
+                       "--no-daemon"
+                       #$config-file)
+                 #:environment-variables
+                 ;; Required to detect PulseAudio when run under a user account.
+                 (list (string-append "XDG_RUNTIME_DIR=/run/user/"
+                                      (number->string (passwd:uid (getpwnam #$user)))))))
+       (stop  #~(make-kill-destructor))
+       (actions
+        (list (shepherd-configuration-action config-file)
+              (shepherd-action
+               (name 'reopen)
+               (documentation "Re-open log files and flush caches.")
+               (procedure #~(lambda (pid)
+                              (if pid
+                                  (begin (kill pid SIGHUP)
+                                         (format #t "Issued SIGHUP to Service MPD (PID ~a)." pid))
+                                  (format #t "Service MPD is not running.")))))))))))
 
 (define (mpd-service-activation config)
-  (with-imported-modules '((guix build utils))
+  (match-record config <mpd-configuration> (user log-file)
     #~(begin
         (use-modules (guix build utils))
-        (define %user
-          (getpw #$(mpd-configuration-user config)))
-
-        (let ((directory #$(mpd-file-name config ".mpd")))
-          (mkdir-p directory)
-          (chown directory (passwd:uid %user) (passwd:gid %user))
-
-          ;; Make /var/run/mpd/USER user-owned as well.
-          (chown (dirname directory)
-                 (passwd:uid %user) (passwd:gid %user))))))
-
-
-(define %mpd-accounts
-  ;; Default account and group for MPD.
-  (list (user-group (name "mpd") (system? #t))
-        (user-account
-         (name "mpd")
-         (group "mpd")
-         (system? #t)
-         (comment "Music Player Daemon (MPD) user")
 
-         ;; Note: /var/run/mpd hosts one sub-directory per user, of which
-         ;; /var/run/mpd/mpd corresponds to the "mpd" user.
-         (home-directory "/var/run/mpd/mpd")
+        (let* ((user (getpw #$user))
+               (deprecated-directory (string-append "/var/run/mpd/" #$user "/.mpd"))
+               (new-directory (string-append (passwd:dir user) "/.config/mpd")))
+          ;; TODO: remove me, migrates from the old location at /var/run/mpd to the new one at /var/lib/mpd.
+          (when (and (file-exists? deprecated-directory) (not (file-exists? new-directory)))
+            (rename-file deprecated-directory new-directory)
+            (chown new-directory (passwd:uid user) (passwd:gid user)))
+          (mkdir-p (dirname #$log-file))))))
 
-         (shell (file-append shadow "/sbin/nologin")))))
+(define (mpd-accounts config)
+  (match-record config <mpd-configuration> (user)
+    (list (user-account
+           (name user)
+           (group "nogroup")
+           (system? #t)
+           (comment "Music Player Daemon (MPD) user")
+           (home-directory "/var/lib/mpd")  ; MPD can use $HOME (or $XDG_CONFIG_HOME) to place its data
+           (shell (file-append shadow "/sbin/nologin"))))))
 
 (define mpd-service-type
   (service-type
@@ -234,7 +460,9 @@ (define mpd-service-type
     (list (service-extension shepherd-root-service-type
                              (compose list mpd-shepherd-service))
           (service-extension account-service-type
-                             (const %mpd-accounts))
+                             mpd-accounts)
           (service-extension activation-service-type
-                             mpd-service-activation)))
+                             mpd-service-activation)
+          (service-extension rottlog-service-type
+                             (compose list mpd-log-rotation))))
    (default-value (mpd-configuration))))

base-commit: 7ce9b7e7062eee406e269bc40a20247973732be2
prerequisite-patch-id: 3b756bb4d930897ce79b1e51cc25478cb73fd9d8
-- 
2.38.1





^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [bug#59866] [PATCH v4 1/2] services: mpd: use 'define-configuration'.
  2022-12-06 23:22 [bug#59866] [PATCH 0/2] services: mpd: Refactor MPD service mirai
                   ` (5 preceding siblings ...)
  2022-12-16 14:15 ` [bug#59866] [PATCH v3] " mirai
@ 2022-12-21 14:15 ` mirai
  2022-12-21 14:15   ` [bug#59866] [PATCH v4 2/2] services: mpd: Refactor MPD service mirai
  2022-12-24 13:51 ` [bug#59866] [PATCH v5 1/2] services: mpd: rewrite using 'define-configuration' mirai
                   ` (4 subsequent siblings)
  11 siblings, 1 reply; 29+ messages in thread
From: mirai @ 2022-12-21 14:15 UTC (permalink / raw)
  To: 59866; +Cc: Bruno Victal

From: Bruno Victal <mirai@makinata.eu>

---
 gnu/services/audio.scm | 217 ++++++++++++++++++++++++-----------------
 1 file changed, 129 insertions(+), 88 deletions(-)

diff --git a/gnu/services/audio.scm b/gnu/services/audio.scm
index c60053f33c..2351db8a4a 100644
--- a/gnu/services/audio.scm
+++ b/gnu/services/audio.scm
@@ -21,6 +21,7 @@
 (define-module (gnu services audio)
   #:use-module (guix gexp)
   #:use-module (gnu services)
+  #:use-module (gnu services configuration)
   #:use-module (gnu services shepherd)
   #:use-module (gnu system shadow)
   #:use-module (gnu packages admin)
@@ -28,6 +29,8 @@ (define-module (gnu services audio)
   #:use-module (guix records)
   #:use-module (ice-9 match)
   #:use-module (ice-9 format)
+  #:use-module (srfi srfi-1)
+  #:use-module (srfi srfi-26)
   #:export (mpd-output
             mpd-output?
             mpd-configuration
@@ -40,93 +43,131 @@ (define-module (gnu services audio)
 ;;;
 ;;; Code:
 
-(define-record-type* <mpd-output>
-  mpd-output make-mpd-output
-  mpd-output?
-  (type          mpd-output-type
-                 (default "pulse"))
-  (name          mpd-output-name
-                 (default "MPD"))
-  (enabled?      mpd-output-enabled?
-                 (default #t))
-  (tags?         mpd-output-tags?
-                 (default #t))
-  (always-on?    mpd-output-always-on?
-                 (default #f))
-  (mixer-type    mpd-output-mixer-type
-                 ;; valid: hardware, software, null, none
-                 (default #f))
-  (extra-options mpd-output-extra-options
-                 (default '())))
-
-(define-record-type* <mpd-configuration>
-  mpd-configuration make-mpd-configuration
-  mpd-configuration?
-  (user         mpd-configuration-user
-                (default "mpd"))
-  (music-dir    mpd-configuration-music-dir
-                (default "~/Music"))
-  (playlist-dir mpd-configuration-playlist-dir
-                (default "~/.mpd/playlists"))
-  (db-file      mpd-configuration-db-file
-                (default "~/.mpd/tag_cache"))
-  (state-file   mpd-configuration-state-file
-                (default "~/.mpd/state"))
-  (sticker-file mpd-configuration-sticker-file
-                (default "~/.mpd/sticker.sql"))
-  (port         mpd-configuration-port
-                (default "6600"))
-  (address      mpd-configuration-address
-                (default "any"))
-  (outputs      mpd-configuration-outputs
-                (default (list (mpd-output)))))
-
-(define (mpd-output->string output)
-  "Convert the OUTPUT of type <mpd-output> to a configuration file snippet."
-  (let ((extra (string-join
-                (map (match-lambda
-                       ((key . value)
-                        (format #f "  ~a \"~a\""
-                                (string-map
-                                 (lambda (c) (if (char=? c #\-) #\_ c))
-                                 (symbol->string key))
-                                value)))
-                     (mpd-output-extra-options output))
-                "\n")))
-    (format #f "\
-audio_output {
-  type \"~a\"
-  name \"~a\"
-~:[  enabled \"no\"~%~;~]\
-~:[  tags \"no\"~%~;~]\
-~:[~;  always_on \"yes\"~%~]\
-~@[  mixer_type \"~a\"~%~]\
-~a~%}~%"
-            (mpd-output-type output)
-            (mpd-output-name output)
-            (mpd-output-enabled? output)
-            (mpd-output-tags? output)
-            (mpd-output-always-on? output)
-            (mpd-output-mixer-type output)
-            extra)))
-
-(define (mpd-config->file config)
-  (apply
-   mixed-text-file "mpd.conf"
-   "pid_file \"" (mpd-file-name config "pid") "\"\n"
-   (append (map mpd-output->string
-                (mpd-configuration-outputs config))
-           (map (match-lambda
-                  ((config-name config-val)
-                   (string-append config-name " \"" (config-val config) "\"\n")))
-                `(("user" ,mpd-configuration-user)
-                  ("music_directory" ,mpd-configuration-music-dir)
-                  ("playlist_directory" ,mpd-configuration-playlist-dir)
-                  ("db_file" ,mpd-configuration-db-file)
-                  ("state_file" ,mpd-configuration-state-file)
-                  ("sticker_file" ,mpd-configuration-sticker-file)
-                  ("port" ,mpd-configuration-port)
-                  ("bind_to_address" ,mpd-configuration-address))))))
+(define (uglify-field-name field-name)
+  (let ((str (symbol->string field-name)))
+    (string-join (string-split (if (string-suffix? "?" str)
+                                   (string-drop-right str 1)
+                                   str)
+                               #\-)
+                 "_")))
+
+(define (free-form-args? val)
+  (match val
+    (() #t)
+    ((((? symbol?) . (? string?)) . val) (free-form-args? val))
+    (_ #f)))
+
+(define* (mpd-serialize-field field-name value #:optional (indent-level 0))
+  #~(begin
+      (use-modules ((ice-9 format)))
+      (format #f "~v/~a \"~a\"~%" #$indent-level #$(if (string? field-name)
+                                                       field-name
+                                                       (uglify-field-name field-name)) #$value)))
+
+(define* (mpd-serialize-free-form-args field-name value #:optional (indent-level 0))
+  (generic-serialize-alist string-append (cut mpd-serialize-field <> <> indent-level) value))
+
+(define mpd-serialize-number mpd-serialize-field)
+
+(define mpd-serialize-string mpd-serialize-field)
+
+(define* (mpd-serialize-boolean field-name value #:optional (indent-level 0))
+  (mpd-serialize-field field-name (if value "yes" "no") indent-level))
+
+(define (mpd-serialize-list-of-mpd-output field-name value)
+  #~(string-append "\naudio_output {\n"
+                   #$@(map (cut serialize-configuration <>
+                                mpd-output-fields)
+                           value)
+                   "}\n"))
+
+(define (mpd-serialize-configuration configuration)
+  (mixed-text-file
+   "mpd.conf"
+   (serialize-configuration configuration mpd-configuration-fields)))
+
+(define mpd-subsystem-serialize-field (cut mpd-serialize-field <> <> 1))
+(define mpd-subsystem-serialize-string (cut mpd-serialize-string <> <> 1))
+(define mpd-subsystem-serialize-number (cut mpd-serialize-number <> <> 1))
+(define mpd-subsystem-serialize-boolean (cut mpd-serialize-boolean <> <> 1))
+(define mpd-subsystem-serialize-free-form-args (cut mpd-serialize-free-form-args <> <> 1))
+
+(define-configuration mpd-output
+  (name
+   (string "MPD")
+   "The name of the audio output.")
+  (type
+   (string "pulse")
+   "The type of audio output.")
+  (enabled?
+   (boolean #t)
+   "Specifies whether this audio output is enabled when MPD is started. By
+default, all audio outputs are enabled. This is just the default
+setting when there is no state file; with a state file, the previous
+state is restored.")
+  (tags?
+   (boolean #t)
+   "If set to @code{#f}, then MPD will not send tags to this output. This
+is only useful for output plugins that can receive tags, for example the
+@code{httpd} output plugin.")
+  (always-on?
+   (boolean #f)
+   "If set to @code{#t}, then MPD attempts to keep this audio output always
+open. This may be useful for streaming servers, when you don’t want to
+disconnect all listeners even when playback is accidentally stopped.")
+  (mixer-type
+   (string "none")
+   "This field accepts a symbol that specifies which mixer should be used
+for this audio output: the @code{hardware} mixer, the @code{software}
+mixer, the @code{null} mixer (allows setting the volume, but with no
+effect; this can be used as a trick to implement an external mixer
+External Mixer) or no mixer (@code{none}).")
+  (extra-options
+   (free-form-args '())
+   "An association list of option symbols to string values to be appended to
+the audio output configuration.")
+  (prefix mpd-subsystem-))
+
+(define list-of-mpd-output?
+  (list-of mpd-output?))
+
+(define-configuration mpd-configuration
+  (user
+   (string "mpd")
+   "The user to run mpd as.")
+  (music-dir
+   (string "~/Music")
+   "The directory to scan for music files."
+   (lambda (_ x)
+     (mpd-serialize-field "music_directory" x)))
+  (playlist-dir
+   (string "~/.mpd/playlists")
+   "The directory to store playlists."
+   (lambda (_ x)
+     (mpd-serialize-field "playlist_directory" x)))
+  (db-file
+   (string "~/.mpd/tag_cache")
+   "The location of the music database.")
+  (state-file
+   (string "~/.mpd/state")
+   "The location of the file that stores current MPD's state.")
+  (sticker-file
+   (string "~/.mpd/sticker.sql")
+   "The location of the sticker database.")
+  (port
+   (string "6600")
+   "The port to run mpd on.")
+  (address
+   (string "any")
+   "The address that mpd will bind to.
+To use a Unix domain socket, an absolute path can be specified here."
+   (lambda (_ x)
+     (mpd-serialize-field "bind_to_address" x)))
+  (outputs
+   (list-of-mpd-output (list (mpd-output)))
+   "The audio outputs that MPD can use.
+By default this is a single output using pulseaudio.")
+  (prefix mpd-))
 
 (define (mpd-file-name config file)
   "Return a path in /var/run/mpd/ that is writable
@@ -143,7 +184,7 @@ (define (mpd-shepherd-service config)
    (start #~(make-forkexec-constructor
              (list #$(file-append mpd "/bin/mpd")
                    "--no-daemon"
-                   #$(mpd-config->file config))
+                   #$(mpd-serialize-configuration config))
              #:environment-variables
              ;; Required to detect PulseAudio when run under a user account.
              (list (string-append

base-commit: 7833acab0da02335941974608510c02e2d1d8069
-- 
2.38.1





^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [bug#59866] [PATCH v4 2/2] services: mpd: Refactor MPD service.
  2022-12-21 14:15 ` [bug#59866] [PATCH v4 1/2] services: mpd: use 'define-configuration' mirai
@ 2022-12-21 14:15   ` mirai
  0 siblings, 0 replies; 29+ messages in thread
From: mirai @ 2022-12-21 14:15 UTC (permalink / raw)
  To: 59866; +Cc: Bruno Victal

From: Bruno Victal <mirai@makinata.eu>

Introduces 'mpd-plugin' and 'mpd-partition' records.
Expands 'mpd-output' and 'mpd-configuration' records.
Deprecates redundant abbreviated fields in 'mpd-configuration' and
avoids serializing unused fields that may introduce undesired behavior.
Implement log-rotation via rottlog.
Implements Shepherd actions: 'reopen' and 'configuration'.
---
 doc/guix.texi          | 172 +++++++++++++---
 gnu/services/audio.scm | 434 +++++++++++++++++++++++++++++++----------
 2 files changed, 476 insertions(+), 130 deletions(-)

diff --git a/doc/guix.texi b/doc/guix.texi
index fd03da8c97..3daec7dfaa 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -109,6 +109,7 @@ Copyright @copyright{} 2022 Reily Siegel@*
 Copyright @copyright{} 2022 Simon Streit@*
 Copyright @copyright{} 2022 (@*
 Copyright @copyright{} 2022 John Kehayias@*
+Copyright @copyright{} 2022 Bruno Victal@*
 
 Permission is granted to copy, distribute and/or modify this document
 under the terms of the GNU Free Documentation License, Version 1.3 or
@@ -33012,79 +33013,184 @@ The service type for @command{mpd}
 Data type representing the configuration of @command{mpd}.
 
 @table @asis
-@item @code{user} (default: @code{"mpd"})
+@item @code{package} (default: @code{mpd}) (type: file-like)
+The MPD package.
+
+@item @code{user} (default: @code{"mpd"}) (type: string)
 The user to run mpd as.
 
-@item @code{music-dir} (default: @code{"~/Music"})
+@item @code{group} (type: maybe-string)
+The group to run mpd as.
+
+@item @code{shepherd-requirement} (default: @code{()}) (type: list-of-symbol)
+This is a list of symbols naming Shepherd services that this service
+will depend on.
+
+@item @code{log-file} (default: @code{"/var/log/mpd/log"}) (type: maybe-string)
+The location of the log file.  Set to @code{syslog} to use the local
+syslog daemon or @code{%unset-value} to omit this directive from the
+configuration file.
+
+@item @code{log-level} (type: maybe-string)
+Supress any messages below this threshold.  Available values:
+@code{notice}, @code{info}, @code{verbose}, @code{warning} and
+@code{error}.
+
+@item @code{music-directory} (type: maybe-string)
 The directory to scan for music files.
 
-@item @code{playlist-dir} (default: @code{"~/.mpd/playlists"})
+@item @code{playlist-directory} (type: maybe-string)
 The directory to store playlists.
 
-@item @code{db-file} (default: @code{"~/.mpd/tag_cache"})
+@item @code{db-file} (type: maybe-string)
 The location of the music database.
 
-@item @code{state-file} (default: @code{"~/.mpd/state"})
+@item @code{state-file} (type: maybe-string)
 The location of the file that stores current MPD's state.
 
-@item @code{sticker-file} (default: @code{"~/.mpd/sticker.sql"})
+@item @code{sticker-file} (type: maybe-string)
 The location of the sticker database.
 
-@item @code{port} (default: @code{"6600"})
+@item @code{port} (default: @code{6600}) (type: maybe-integer)
 The port to run mpd on.
 
-@item @code{address} (default: @code{"any"})
-The address that mpd will bind to.  To use a Unix domain socket,
-an absolute path can be specified here.
+@item @code{endpoints} (type: maybe-list-of-string)
+The addresses that mpd will bind to.  To use a Unix domain socket, an
+absolute path can be specified here.
+
+@item @code{database} (type: maybe-mpd-plugin)
+MPD database plugin configuration.
+
+@item @code{partitions} (default: @code{()}) (type: list-of-mpd-partition)
+List of MPD "partitions".
+
+@item @code{neighbors} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD neighbor plugin configurations.
 
-@item @code{outputs} (default: @code{"(list (mpd-output))"})
-The audio outputs that MPD can use.  By default this is a single output using pulseaudio.
+@item @code{inputs} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD input plugin configurations.
+
+@item @code{archive-plugins} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD archive plugin configurations.
+
+@item @code{input-cache-size} (type: maybe-string)
+MPD input cache size.
+
+@item @code{decoders} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD decoder plugin configurations.
+
+@item @code{resampler} (type: maybe-mpd-plugin)
+MPD resampler plugin configuration.
+
+@item @code{filters} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD filter plugin configurations.
+
+@item @code{outputs} (type: list-of-mpd-plugin-or-output)
+The audio outputs that MPD can use.  By default this is a single output
+using pulseaudio.
+
+@item @code{playlist-plugins} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD playlist plugin configurations.
+
+@item @code{extra-options} (default: @code{()}) (type: alist)
+An association list of option symbols/strings to string values to be
+appended to the configuration.
+
+@end table
+@end deftp
+
+@deftp {Data Type} mpd-plugin
+Data type representing a @command{mpd} plugin.
+
+@table @asis
+@item @code{plugin} (type: maybe-string)
+Plugin name.
+
+@item @code{name} (type: maybe-string)
+Name.
+
+@item @code{enabled?} (type: maybe-boolean)
+Whether the plugin is enabled/disabled.
+
+@item @code{extra-options} (default: @code{()}) (type: alist)
+An association list of option symbols/strings to string values to be
+appended to the plugin configuration.  See
+@uref{https://mpd.readthedocs.io/en/latest/plugins.html,MPD plugin
+reference} for available options.
+
+@end table
+@end deftp
+
+@deftp {Data Type} mpd-partition
+Data type representing a @command{mpd} partition.
+
+@table @asis
+@item @code{name} (type: string)
+Partition name.
+
+@item @code{extra-options} (default: @code{()}) (type: alist)
+An association list of option symbols/strings to string values to be
+appended to the partition configuration.  See
+@uref{https://mpd.readthedocs.io/en/latest/user.html#configuring-partitions,Configuring
+Partitions} for available options.
 
 @end table
 @end deftp
 
 @deftp {Data Type} mpd-output
-Data type representing an @command{mpd} audio output.
+Data type representing a @command{mpd} audio output.
 
 @table @asis
-@item @code{name} (default: @code{"MPD"})
+@item @code{name} (default: @code{"MPD"}) (type: string)
 The name of the audio output.
 
-@item @code{type} (default: @code{"pulse"})
+@item @code{type} (default: @code{"pulse"}) (type: string)
 The type of audio output.
 
-@item @code{enabled?} (default: @code{#t})
+@item @code{enabled?} (default: @code{#t}) (type: boolean)
 Specifies whether this audio output is enabled when MPD is started.  By
 default, all audio outputs are enabled.  This is just the default
 setting when there is no state file; with a state file, the previous
 state is restored.
 
-@item @code{tags?} (default: @code{#t})
+@item @code{format} (type: maybe-string)
+Force a specific audio format on output.  See
+@uref{https://mpd.readthedocs.io/en/latest/user.html#audio-output-format,Global
+Audio Format} for a more detailed description.
+
+@item @code{tags?} (default: @code{#t}) (type: boolean)
 If set to @code{#f}, then MPD will not send tags to this output.  This
 is only useful for output plugins that can receive tags, for example the
 @code{httpd} output plugin.
 
-@item @code{always-on?} (default: @code{#f})
+@item @code{always-on?} (default: @code{#f}) (type: boolean)
 If set to @code{#t}, then MPD attempts to keep this audio output always
-open.  This may be useful for streaming servers, when you don’t want to
+open.  This may be useful for streaming servers, when you don?t want to
 disconnect all listeners even when playback is accidentally stopped.
 
-@item @code{mixer-type}
-This field accepts a symbol that specifies which mixer should be used
+@item @code{mixer-type} (default: @code{"none"}) (type: string)
+This field accepts a string that specifies which mixer should be used
 for this audio output: the @code{hardware} mixer, the @code{software}
 mixer, the @code{null} mixer (allows setting the volume, but with no
 effect; this can be used as a trick to implement an external mixer
 External Mixer) or no mixer (@code{none}).
 
-@item @code{extra-options} (default: @code{'()})
-An association list of option symbols to string values to be appended to
-the audio output configuration.
+@item @code{replay-gain-handler} (type: maybe-string)
+This field accepts a string that specifies how
+@uref{https://mpd.readthedocs.io/en/latest/user.html#replay-gain,Replay
+Gain} is to be applied.  @code{software} uses an internal software
+volume control, @code{mixer} uses the configured (hardware) mixer
+control and @code{none} disables replay gain on this audio output.
+
+@item @code{extra-options} (default: @code{()}) (type: alist)
+An association list of option symbols/strings to string values to be
+appended to the audio output configuration.
 
 @end table
 @end deftp
 
-The following example shows a configuration of @code{mpd} that provides
-an HTTP audio streaming output.
+The following example shows a configuration of @command{mpd} that
+configures some of its plugins and provides a HTTP audio streaming output.
 
 @lisp
 (service mpd-service-type
@@ -33096,7 +33202,19 @@ an HTTP audio streaming output.
                      (mixer-type 'null)
                      (extra-options
                       `((encoder . "vorbis")
-                        (port    . "8080"))))))))
+                        (port    . "8080"))))))
+           (decoders
+             (list (mpd-plugin
+                     (plugin "mikmod")
+                     (enabled? #f))
+                   (mpd-plugin
+                     (plugin "openmpt")
+                     (enabled? #t)
+                     (extra-options `((repeat-count . -1)
+                                      (interpolation-filter . 1))))))
+           (resampler (mpd-plugin
+                        (plugin "libsamplerate")
+                        (extra-options `((type . 0)))))))
 @end lisp
 
 
diff --git a/gnu/services/audio.scm b/gnu/services/audio.scm
index 2351db8a4a..2b0447fd09 100644
--- a/gnu/services/audio.scm
+++ b/gnu/services/audio.scm
@@ -2,6 +2,7 @@
 ;;; Copyright © 2017 Peter Mikkelsen <petermikkelsen10@gmail.com>
 ;;; Copyright © 2019 Ricardo Wurmus <rekado@elephly.net>
 ;;; Copyright © 2020 Ludovic Courtès <ludo@gnu.org>
+;;; Copyright © 2022 Bruno Victal <mirai@makinata.eu>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -20,19 +21,27 @@
 
 (define-module (gnu services audio)
   #:use-module (guix gexp)
+  #:use-module (guix deprecation)
+  #:use-module (guix diagnostics)
+  #:use-module (guix i18n)
   #:use-module (gnu services)
   #:use-module (gnu services configuration)
   #:use-module (gnu services shepherd)
+  #:use-module (gnu services admin)
   #:use-module (gnu system shadow)
   #:use-module (gnu packages admin)
   #:use-module (gnu packages mpd)
   #:use-module (guix records)
   #:use-module (ice-9 match)
-  #:use-module (ice-9 format)
   #:use-module (srfi srfi-1)
+  #:use-module (srfi srfi-8)
   #:use-module (srfi srfi-26)
   #:export (mpd-output
             mpd-output?
+            mpd-plugin
+            mpd-plugin?
+            mpd-partition
+            mpd-partition?
             mpd-configuration
             mpd-configuration?
             mpd-service-type))
@@ -51,180 +60,397 @@ (define (uglify-field-name field-name)
                                #\-)
                  "_")))
 
-(define (free-form-args? val)
-  (match val
-    (() #t)
-    ((((? symbol?) . (? string?)) . val) (free-form-args? val))
-    (_ #f)))
+(define list-of-string?
+  (list-of string?))
 
-(define* (mpd-serialize-field field-name value #:optional (indent-level 0))
-  #~(begin
-      (use-modules ((ice-9 format)))
-      (format #f "~v/~a \"~a\"~%" #$indent-level #$(if (string? field-name)
-                                                       field-name
-                                                       (uglify-field-name field-name)) #$value)))
+(define list-of-symbol?
+  (list-of symbol?))
 
-(define* (mpd-serialize-free-form-args field-name value #:optional (indent-level 0))
-  (generic-serialize-alist string-append (cut mpd-serialize-field <> <> indent-level) value))
+(define (mpd-serialize-field field-name value)
+  (let ((field (if (string? field-name) field-name
+                   (uglify-field-name field-name)))
+        (value (if (boolean? value) (if value "yes" "no") value)))
+    #~(format #f "~a \"~a\"~%" #$field #$value)))
 
 (define mpd-serialize-number mpd-serialize-field)
 
 (define mpd-serialize-string mpd-serialize-field)
 
-(define* (mpd-serialize-boolean field-name value #:optional (indent-level 0))
-  (mpd-serialize-field field-name (if value "yes" "no") indent-level))
+(define mpd-serialize-boolean mpd-serialize-field)
 
-(define (mpd-serialize-list-of-mpd-output field-name value)
-  #~(string-append "\naudio_output {\n"
-                   #$@(map (cut serialize-configuration <>
-                                mpd-output-fields)
-                           value)
-                   "}\n"))
+(define (mpd-serialize-alist field-name value)
+  #~(string-append #$@(generic-serialize-alist list mpd-serialize-field value)))
 
-(define (mpd-serialize-configuration configuration)
-  (mixed-text-file
-   "mpd.conf"
-   (serialize-configuration configuration mpd-configuration-fields)))
+(define-maybe string (prefix mpd-))
+(define-maybe list-of-string (prefix mpd-))
+(define-maybe boolean (prefix mpd-))
+
+;;; TODO: Procedures for deprecated fields, to be removed.
+
+(define mpd-deprecated-fields '((music-dir . music-directory)
+                                (playlist-dir . playlist-directory)
+                                (address . addresses)))
+
+(define (port? value) (or (string? value) (integer? value)))
+
+(define (mpd-serialize-deprecated-field field-name value)
+  (if (maybe-value-set? value)
+      (begin (warn-about-deprecation field-name #f
+                                     #:replacement (assoc-ref mpd-deprecated-fields field-name))
+             (match field-name
+               ('playlist-dir (mpd-serialize-string "playlist_directory" value))
+               ('music-dir (mpd-serialize-string "music_directory" value))
+               ('address (mpd-serialize-string "bind_to_address" value))))
+      ""))
+
+(define (mpd-serialize-port field-name value)
+  (when (string? value)
+    (warning (G_ "string value for '~a' is deprecated, use integer instead~%") field-name))
+  (mpd-serialize-field field-name value))
+
+(define-maybe port (prefix mpd-))
+
+;;;
+
+;; Generic MPD plugin record, lists only the most prevalent fields.
+(define-configuration mpd-plugin
+  (plugin
+   maybe-string
+   "Plugin name.")
+
+  (name
+   maybe-string
+   "Name.")
+
+  (enabled?
+   maybe-boolean
+   "Whether the plugin is enabled/disabled.")
+
+  (extra-options
+   (alist '())
+   "An association list of option symbols/strings to string values to be appended to
+the plugin configuration. See
+@uref{https://mpd.readthedocs.io/en/latest/plugins.html,MPD plugin reference}
+for available options.")
+
+  (prefix mpd-))
+
+(define (mpd-serialize-mpd-plugin field-name value)
+  #~(format #f "~a {~%~a}~%"
+            '#$field-name #$(serialize-configuration value mpd-plugin-fields)))
+
+(define (mpd-serialize-list-of-mpd-plugin field-name value)
+  #~(string-append #$@(map (cut mpd-serialize-mpd-plugin field-name <>) value)))
 
-(define mpd-subsystem-serialize-field (cut mpd-serialize-field <> <> 1))
-(define mpd-subsystem-serialize-string (cut mpd-serialize-string <> <> 1))
-(define mpd-subsystem-serialize-number (cut mpd-serialize-number <> <> 1))
-(define mpd-subsystem-serialize-boolean (cut mpd-serialize-boolean <> <> 1))
-(define mpd-subsystem-serialize-free-form-args (cut mpd-serialize-free-form-args <> <> 1))
+(define list-of-mpd-plugin? (list-of mpd-plugin?))
+
+(define-maybe mpd-plugin (prefix mpd-))
+
+(define-configuration mpd-partition
+  (name
+   string
+   "Partition name.")
+
+  (extra-options
+   (alist '())
+   "An association list of option symbols/strings to string values to be appended to
+the partition configuration. See
+@uref{https://mpd.readthedocs.io/en/latest/user.html#configuring-partitions,Configuring Partitions}
+for available options.")
+
+  (prefix mpd-))
+
+(define (mpd-serialize-mpd-partition field-name value)
+  #~(format #f "partition {~%~a}~%"
+            #$(serialize-configuration value mpd-partition-fields)))
+
+(define (mpd-serialize-list-of-mpd-partition field-name value)
+  #~(string-append #$@(map (cut mpd-serialize-mpd-partition #f <>) value)))
+
+(define list-of-mpd-partition?
+  (list-of mpd-partition?))
 
 (define-configuration mpd-output
   (name
    (string "MPD")
    "The name of the audio output.")
+
   (type
    (string "pulse")
    "The type of audio output.")
+
   (enabled?
    (boolean #t)
    "Specifies whether this audio output is enabled when MPD is started. By
 default, all audio outputs are enabled. This is just the default
 setting when there is no state file; with a state file, the previous
 state is restored.")
+
+  (format
+   maybe-string
+   "Force a specific audio format on output. See
+@uref{https://mpd.readthedocs.io/en/latest/user.html#audio-output-format,Global Audio Format}
+for a more detailed description.")
+   
   (tags?
    (boolean #t)
    "If set to @code{#f}, then MPD will not send tags to this output. This
 is only useful for output plugins that can receive tags, for example the
 @code{httpd} output plugin.")
+
   (always-on?
    (boolean #f)
    "If set to @code{#t}, then MPD attempts to keep this audio output always
 open. This may be useful for streaming servers, when you don’t want to
 disconnect all listeners even when playback is accidentally stopped.")
+
   (mixer-type
    (string "none")
-   "This field accepts a symbol that specifies which mixer should be used
+   "This field accepts a string that specifies which mixer should be used
 for this audio output: the @code{hardware} mixer, the @code{software}
 mixer, the @code{null} mixer (allows setting the volume, but with no
 effect; this can be used as a trick to implement an external mixer
 External Mixer) or no mixer (@code{none}).")
+
+  (replay-gain-handler
+   maybe-string
+   "This field accepts a string that specifies how
+@uref{https://mpd.readthedocs.io/en/latest/user.html#replay-gain,Replay Gain} is
+to be applied. @code{software} uses an internal software volume control,
+@code{mixer} uses the configured (hardware) mixer control and @code{none}
+disables replay gain on this audio output.")
+
   (extra-options
-   (free-form-args '())
-   "An association list of option symbols to string values to be appended to
+   (alist '())
+   "An association list of option symbols/strings to string values to be appended to
 the audio output configuration.")
-  (prefix mpd-subsystem-))
 
-(define list-of-mpd-output?
-  (list-of mpd-output?))
+  (prefix mpd-))
+
+(define (mpd-serialize-mpd-output field-name value)
+  #~(format #f "audio_output {~%~a}~%"
+            #$(serialize-configuration value mpd-output-fields)))
+
+(define (mpd-serialize-list-of-mpd-plugin-or-output field-name value)
+  (receive (plugins outputs)
+      (partition mpd-plugin? value)
+    #~(string-append #$@(map (cut mpd-serialize-mpd-plugin "audio_output" <>) plugins)
+                     #$@(map (cut mpd-serialize-mpd-output #f <>) outputs))))
+
+(define list-of-mpd-plugin-or-output?
+  (list-of (lambda (x)
+             (or (mpd-output? x) (mpd-plugin? x)))))
 
 (define-configuration mpd-configuration
+  (package
+   (file-like mpd)
+   "The MPD package."
+   empty-serializer)
+
   (user
    (string "mpd")
    "The user to run mpd as.")
-  (music-dir
-   (string "~/Music")
+
+  (group
+   maybe-string
+   "The group to run mpd as.")
+
+  (shepherd-requirement
+   (list-of-symbol '())
+   "This is a list of symbols naming Shepherd services that this service
+will depend on."
+   empty-serializer)
+
+  (log-file
+   (maybe-string "/var/log/mpd/log")
+   "The location of the log file. Set to @code{syslog} to use the local syslog daemon or
+@code{%unset-value} to omit this directive from the configuration file.")
+
+  (log-level
+   maybe-string
+   "Supress any messages below this threshold. Available values: @code{notice},
+@code{info}, @code{verbose}, @code{warning} and @code{error}.")
+
+  (music-directory
+   maybe-string
+   "The directory to scan for music files.")
+
+  (music-dir ; TODO: deprecated, remove later
+   maybe-string
    "The directory to scan for music files."
-   (lambda (_ x)
-     (mpd-serialize-field "music_directory" x)))
-  (playlist-dir
-   (string "~/.mpd/playlists")
+   mpd-serialize-deprecated-field)
+
+  (playlist-directory
+   maybe-string
+   "The directory to store playlists.")
+
+  (playlist-dir ; TODO: deprecated, remove later
+   maybe-string
    "The directory to store playlists."
-   (lambda (_ x)
-     (mpd-serialize-field "playlist_directory" x)))
+   mpd-serialize-deprecated-field)
+
   (db-file
-   (string "~/.mpd/tag_cache")
+   maybe-string
    "The location of the music database.")
+
   (state-file
-   (string "~/.mpd/state")
+   maybe-string
    "The location of the file that stores current MPD's state.")
+
   (sticker-file
-   (string "~/.mpd/sticker.sql")
+   maybe-string
    "The location of the sticker database.")
+
   (port
-   (string "6600")
+   (maybe-port 6600) ; TODO: switch to integer
    "The port to run mpd on.")
-  (address
-   (string "any")
+
+  (endpoints
+   maybe-list-of-string
+   "The addresses that mpd will bind to.
+To use a Unix domain socket, an absolute path can be specified here."
+   (lambda (_ x)
+     (if (maybe-value-set? x)
+         #~(string-append #$@(map
+                              (cut mpd-serialize-field "bind_to_address" <>)
+                              x)) "")))
+
+  (address ; TODO: deprecated, remove later
+   maybe-string
    "The address that mpd will bind to.
 To use a Unix domain socket, an absolute path can be specified here."
+   mpd-serialize-deprecated-field)
+
+  (database
+   maybe-mpd-plugin
+   "MPD database plugin configuration.")
+
+  (partitions
+   (list-of-mpd-partition '())
+   "List of MPD \"partitions\".")
+  
+  (neighbors
+   (list-of-mpd-plugin '())
+   "List of MPD neighbor plugin configurations.")
+
+  (inputs
+   (list-of-mpd-plugin '())
+   "List of MPD input plugin configurations."
+   (lambda (_ x)
+     (mpd-serialize-list-of-mpd-plugin "input" x)))
+
+  (archive-plugins
+   (list-of-mpd-plugin '())
+   "List of MPD archive plugin configurations."
    (lambda (_ x)
-     (mpd-serialize-field "bind_to_address" x)))
+     (mpd-serialize-list-of-mpd-plugin "archive_plugin" x)))
+
+  (input-cache-size
+   maybe-string
+   "MPD input cache size."
+   (lambda (_ x)
+     (if (maybe-value-set? x)
+         #~(string-append "\ninput_cache {\n"
+                          #$(mpd-serialize-string "size" x)
+                          "}\n") "")))
+
+  (decoders
+   (list-of-mpd-plugin '())
+   "List of MPD decoder plugin configurations."
+   (lambda (_ x)
+     (mpd-serialize-list-of-mpd-plugin "decoder" x)))
+
+  (resampler
+   maybe-mpd-plugin
+   "MPD resampler plugin configuration.")
+
+  (filters
+   (list-of-mpd-plugin '())
+   "List of MPD filter plugin configurations."
+   (lambda (_ x)
+     (mpd-serialize-list-of-mpd-plugin "filter" x)))
+
   (outputs
-   (list-of-mpd-output (list (mpd-output)))
+   (list-of-mpd-plugin-or-output (list (mpd-output)))
    "The audio outputs that MPD can use.
 By default this is a single output using pulseaudio.")
+
+  (playlist-plugins
+   (list-of-mpd-plugin '())
+   "List of MPD playlist plugin configurations."
+   (lambda (_ x)
+     (mpd-serialize-list-of-mpd-plugin "playlist_plugin" x)))
+
+  (extra-options
+   (alist '())
+   "An association list of option symbols/strings to string values to be appended to
+the configuration.")
+
   (prefix mpd-))
 
-(define (mpd-file-name config file)
-  "Return a path in /var/run/mpd/ that is writable
-   by @code{user} from @code{config}."
-  (string-append "/var/run/mpd/"
-                 (mpd-configuration-user config)
-                 "/" file))
+(define (mpd-serialize-configuration configuration)
+  (mixed-text-file
+   "mpd.conf"
+   (serialize-configuration configuration mpd-configuration-fields)))
+
+(define (mpd-log-rotation config)
+  (match-record config <mpd-configuration> (log-file)
+    (log-rotation
+     (files (list log-file))
+     (post-rotate #~(begin
+                      (use-modules (gnu services herd))
+                      (with-shepherd-action 'mpd ('reopen) #f))))))
 
 (define (mpd-shepherd-service config)
-  (shepherd-service
-   (documentation "Run the MPD (Music Player Daemon)")
-   (requirement '(user-processes))
-   (provision '(mpd))
-   (start #~(make-forkexec-constructor
-             (list #$(file-append mpd "/bin/mpd")
-                   "--no-daemon"
-                   #$(mpd-serialize-configuration config))
-             #:environment-variables
-             ;; Required to detect PulseAudio when run under a user account.
-             (list (string-append
-                    "XDG_RUNTIME_DIR=/run/user/"
-                    (number->string
-                     (passwd:uid
-                      (getpwnam #$(mpd-configuration-user config))))))
-             #:log-file #$(mpd-file-name config "log")))
-   (stop  #~(make-kill-destructor))))
+  (match-record config <mpd-configuration> (user package shepherd-requirement)
+    (let* ((config-file (mpd-serialize-configuration config)))
+      (shepherd-service
+       (documentation "Run the MPD (Music Player Daemon)")
+       (requirement `(user-processes loopback ,@shepherd-requirement))
+       (provision '(mpd))
+       (start #~(make-forkexec-constructor
+                 (list #$(file-append package "/bin/mpd")
+                       "--no-daemon"
+                       #$config-file)
+                 #:environment-variables
+                 ;; Required to detect PulseAudio when run under a user account.
+                 (list (string-append "XDG_RUNTIME_DIR=/run/user/"
+                                      (number->string (passwd:uid (getpwnam #$user)))))))
+       (stop  #~(make-kill-destructor))
+       (actions
+        (list (shepherd-configuration-action config-file)
+              (shepherd-action
+               (name 'reopen)
+               (documentation "Re-open log files and flush caches.")
+               (procedure #~(lambda (pid)
+                              (if pid
+                                  (begin (kill pid SIGHUP)
+                                         (format #t "Issued SIGHUP to Service MPD (PID ~a)." pid))
+                                  (format #t "Service MPD is not running.")))))))))))
 
 (define (mpd-service-activation config)
-  (with-imported-modules '((guix build utils))
+  (match-record config <mpd-configuration> (user log-file)
     #~(begin
         (use-modules (guix build utils))
-        (define %user
-          (getpw #$(mpd-configuration-user config)))
-
-        (let ((directory #$(mpd-file-name config ".mpd")))
-          (mkdir-p directory)
-          (chown directory (passwd:uid %user) (passwd:gid %user))
-
-          ;; Make /var/run/mpd/USER user-owned as well.
-          (chown (dirname directory)
-                 (passwd:uid %user) (passwd:gid %user))))))
-
-
-(define %mpd-accounts
-  ;; Default account and group for MPD.
-  (list (user-group (name "mpd") (system? #t))
-        (user-account
-         (name "mpd")
-         (group "mpd")
-         (system? #t)
-         (comment "Music Player Daemon (MPD) user")
 
-         ;; Note: /var/run/mpd hosts one sub-directory per user, of which
-         ;; /var/run/mpd/mpd corresponds to the "mpd" user.
-         (home-directory "/var/run/mpd/mpd")
+        (let* ((user (getpw #$user))
+               (deprecated-directory (string-append "/var/run/mpd/" #$user "/.mpd"))
+               (new-directory (string-append (passwd:dir user) "/.config/mpd")))
+          ;; TODO: remove me, migrates from the old location at /var/run/mpd to the new one at /var/lib/mpd.
+          (when (and (file-exists? deprecated-directory) (not (file-exists? new-directory)))
+            (rename-file deprecated-directory new-directory)
+            (chown new-directory (passwd:uid user) (passwd:gid user)))
+          (mkdir-p (dirname #$log-file))))))
 
-         (shell (file-append shadow "/sbin/nologin")))))
+(define (mpd-accounts config)
+  (match-record config <mpd-configuration> (user)
+    (list (user-account
+           (name user)
+           (group "nogroup")
+           (system? #t)
+           (comment "Music Player Daemon (MPD) user")
+           (home-directory "/var/lib/mpd")  ; MPD can use $HOME (or $XDG_CONFIG_HOME) to place its data
+           (shell (file-append shadow "/sbin/nologin"))))))
 
 (define mpd-service-type
   (service-type
@@ -234,7 +460,9 @@ (define mpd-service-type
     (list (service-extension shepherd-root-service-type
                              (compose list mpd-shepherd-service))
           (service-extension account-service-type
-                             (const %mpd-accounts))
+                             mpd-accounts)
           (service-extension activation-service-type
-                             mpd-service-activation)))
+                             mpd-service-activation)
+          (service-extension rottlog-service-type
+                             (compose list mpd-log-rotation))))
    (default-value (mpd-configuration))))
-- 
2.38.1





^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [bug#59866] [PATCH v5 1/2] services: mpd: rewrite using 'define-configuration'.
  2022-12-06 23:22 [bug#59866] [PATCH 0/2] services: mpd: Refactor MPD service mirai
                   ` (6 preceding siblings ...)
  2022-12-21 14:15 ` [bug#59866] [PATCH v4 1/2] services: mpd: use 'define-configuration' mirai
@ 2022-12-24 13:51 ` mirai
  2022-12-24 13:51   ` [bug#59866] [PATCH v5 2/2] services: mpd: Refactor MPD service mirai
  2022-12-24 14:11 ` [bug#59866] [PATCH v5.1] " mirai
                   ` (3 subsequent siblings)
  11 siblings, 1 reply; 29+ messages in thread
From: mirai @ 2022-12-24 13:51 UTC (permalink / raw)
  To: 59866; +Cc: Bruno Victal

From: Bruno Victal <mirai@makinata.eu>

* gnu/services/audio.scm
(mpd-configuration, mpd-output): Rewrite using define-configuration.
(uglify-field-name): New procedure.
(free-form-args?, list-of-mpd-output?): New predicate.
---

 Changes since v4:
 * Copyright line moved to patch 1/2.
 * Retitled commit message and added ChangeLog formatted entries.

 gnu/services/audio.scm | 218 ++++++++++++++++++++++++-----------------
 1 file changed, 130 insertions(+), 88 deletions(-)

diff --git a/gnu/services/audio.scm b/gnu/services/audio.scm
index c60053f33c..1a1026f342 100644
--- a/gnu/services/audio.scm
+++ b/gnu/services/audio.scm
@@ -2,6 +2,7 @@
 ;;; Copyright © 2017 Peter Mikkelsen <petermikkelsen10@gmail.com>
 ;;; Copyright © 2019 Ricardo Wurmus <rekado@elephly.net>
 ;;; Copyright © 2020 Ludovic Courtès <ludo@gnu.org>
+;;; Copyright © 2022 Bruno Victal <mirai@makinata.eu>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -21,6 +22,7 @@
 (define-module (gnu services audio)
   #:use-module (guix gexp)
   #:use-module (gnu services)
+  #:use-module (gnu services configuration)
   #:use-module (gnu services shepherd)
   #:use-module (gnu system shadow)
   #:use-module (gnu packages admin)
@@ -28,6 +30,8 @@ (define-module (gnu services audio)
   #:use-module (guix records)
   #:use-module (ice-9 match)
   #:use-module (ice-9 format)
+  #:use-module (srfi srfi-1)
+  #:use-module (srfi srfi-26)
   #:export (mpd-output
             mpd-output?
             mpd-configuration
@@ -40,93 +44,131 @@ (define-module (gnu services audio)
 ;;;
 ;;; Code:
 
-(define-record-type* <mpd-output>
-  mpd-output make-mpd-output
-  mpd-output?
-  (type          mpd-output-type
-                 (default "pulse"))
-  (name          mpd-output-name
-                 (default "MPD"))
-  (enabled?      mpd-output-enabled?
-                 (default #t))
-  (tags?         mpd-output-tags?
-                 (default #t))
-  (always-on?    mpd-output-always-on?
-                 (default #f))
-  (mixer-type    mpd-output-mixer-type
-                 ;; valid: hardware, software, null, none
-                 (default #f))
-  (extra-options mpd-output-extra-options
-                 (default '())))
-
-(define-record-type* <mpd-configuration>
-  mpd-configuration make-mpd-configuration
-  mpd-configuration?
-  (user         mpd-configuration-user
-                (default "mpd"))
-  (music-dir    mpd-configuration-music-dir
-                (default "~/Music"))
-  (playlist-dir mpd-configuration-playlist-dir
-                (default "~/.mpd/playlists"))
-  (db-file      mpd-configuration-db-file
-                (default "~/.mpd/tag_cache"))
-  (state-file   mpd-configuration-state-file
-                (default "~/.mpd/state"))
-  (sticker-file mpd-configuration-sticker-file
-                (default "~/.mpd/sticker.sql"))
-  (port         mpd-configuration-port
-                (default "6600"))
-  (address      mpd-configuration-address
-                (default "any"))
-  (outputs      mpd-configuration-outputs
-                (default (list (mpd-output)))))
-
-(define (mpd-output->string output)
-  "Convert the OUTPUT of type <mpd-output> to a configuration file snippet."
-  (let ((extra (string-join
-                (map (match-lambda
-                       ((key . value)
-                        (format #f "  ~a \"~a\""
-                                (string-map
-                                 (lambda (c) (if (char=? c #\-) #\_ c))
-                                 (symbol->string key))
-                                value)))
-                     (mpd-output-extra-options output))
-                "\n")))
-    (format #f "\
-audio_output {
-  type \"~a\"
-  name \"~a\"
-~:[  enabled \"no\"~%~;~]\
-~:[  tags \"no\"~%~;~]\
-~:[~;  always_on \"yes\"~%~]\
-~@[  mixer_type \"~a\"~%~]\
-~a~%}~%"
-            (mpd-output-type output)
-            (mpd-output-name output)
-            (mpd-output-enabled? output)
-            (mpd-output-tags? output)
-            (mpd-output-always-on? output)
-            (mpd-output-mixer-type output)
-            extra)))
-
-(define (mpd-config->file config)
-  (apply
-   mixed-text-file "mpd.conf"
-   "pid_file \"" (mpd-file-name config "pid") "\"\n"
-   (append (map mpd-output->string
-                (mpd-configuration-outputs config))
-           (map (match-lambda
-                  ((config-name config-val)
-                   (string-append config-name " \"" (config-val config) "\"\n")))
-                `(("user" ,mpd-configuration-user)
-                  ("music_directory" ,mpd-configuration-music-dir)
-                  ("playlist_directory" ,mpd-configuration-playlist-dir)
-                  ("db_file" ,mpd-configuration-db-file)
-                  ("state_file" ,mpd-configuration-state-file)
-                  ("sticker_file" ,mpd-configuration-sticker-file)
-                  ("port" ,mpd-configuration-port)
-                  ("bind_to_address" ,mpd-configuration-address))))))
+(define (uglify-field-name field-name)
+  (let ((str (symbol->string field-name)))
+    (string-join (string-split (if (string-suffix? "?" str)
+                                   (string-drop-right str 1)
+                                   str)
+                               #\-)
+                 "_")))
+
+(define (free-form-args? val)
+  (match val
+    (() #t)
+    ((((? symbol?) . (? string?)) . val) (free-form-args? val))
+    (_ #f)))
+
+(define* (mpd-serialize-field field-name value #:optional (indent-level 0))
+  #~(begin
+      (use-modules ((ice-9 format)))
+      (format #f "~v/~a \"~a\"~%" #$indent-level #$(if (string? field-name)
+                                                       field-name
+                                                       (uglify-field-name field-name)) #$value)))
+
+(define* (mpd-serialize-free-form-args field-name value #:optional (indent-level 0))
+  (generic-serialize-alist string-append (cut mpd-serialize-field <> <> indent-level) value))
+
+(define mpd-serialize-number mpd-serialize-field)
+
+(define mpd-serialize-string mpd-serialize-field)
+
+(define* (mpd-serialize-boolean field-name value #:optional (indent-level 0))
+  (mpd-serialize-field field-name (if value "yes" "no") indent-level))
+
+(define (mpd-serialize-list-of-mpd-output field-name value)
+  #~(string-append "\naudio_output {\n"
+                   #$@(map (cut serialize-configuration <>
+                                mpd-output-fields)
+                           value)
+                   "}\n"))
+
+(define (mpd-serialize-configuration configuration)
+  (mixed-text-file
+   "mpd.conf"
+   (serialize-configuration configuration mpd-configuration-fields)))
+
+(define mpd-subsystem-serialize-field (cut mpd-serialize-field <> <> 1))
+(define mpd-subsystem-serialize-string (cut mpd-serialize-string <> <> 1))
+(define mpd-subsystem-serialize-number (cut mpd-serialize-number <> <> 1))
+(define mpd-subsystem-serialize-boolean (cut mpd-serialize-boolean <> <> 1))
+(define mpd-subsystem-serialize-free-form-args (cut mpd-serialize-free-form-args <> <> 1))
+
+(define-configuration mpd-output
+  (name
+   (string "MPD")
+   "The name of the audio output.")
+  (type
+   (string "pulse")
+   "The type of audio output.")
+  (enabled?
+   (boolean #t)
+   "Specifies whether this audio output is enabled when MPD is started. By
+default, all audio outputs are enabled. This is just the default
+setting when there is no state file; with a state file, the previous
+state is restored.")
+  (tags?
+   (boolean #t)
+   "If set to @code{#f}, then MPD will not send tags to this output. This
+is only useful for output plugins that can receive tags, for example the
+@code{httpd} output plugin.")
+  (always-on?
+   (boolean #f)
+   "If set to @code{#t}, then MPD attempts to keep this audio output always
+open. This may be useful for streaming servers, when you don’t want to
+disconnect all listeners even when playback is accidentally stopped.")
+  (mixer-type
+   (string "none")
+   "This field accepts a symbol that specifies which mixer should be used
+for this audio output: the @code{hardware} mixer, the @code{software}
+mixer, the @code{null} mixer (allows setting the volume, but with no
+effect; this can be used as a trick to implement an external mixer
+External Mixer) or no mixer (@code{none}).")
+  (extra-options
+   (free-form-args '())
+   "An association list of option symbols to string values to be appended to
+the audio output configuration.")
+  (prefix mpd-subsystem-))
+
+(define list-of-mpd-output?
+  (list-of mpd-output?))
+
+(define-configuration mpd-configuration
+  (user
+   (string "mpd")
+   "The user to run mpd as.")
+  (music-dir
+   (string "~/Music")
+   "The directory to scan for music files."
+   (lambda (_ x)
+     (mpd-serialize-field "music_directory" x)))
+  (playlist-dir
+   (string "~/.mpd/playlists")
+   "The directory to store playlists."
+   (lambda (_ x)
+     (mpd-serialize-field "playlist_directory" x)))
+  (db-file
+   (string "~/.mpd/tag_cache")
+   "The location of the music database.")
+  (state-file
+   (string "~/.mpd/state")
+   "The location of the file that stores current MPD's state.")
+  (sticker-file
+   (string "~/.mpd/sticker.sql")
+   "The location of the sticker database.")
+  (port
+   (string "6600")
+   "The port to run mpd on.")
+  (address
+   (string "any")
+   "The address that mpd will bind to.
+To use a Unix domain socket, an absolute path can be specified here."
+   (lambda (_ x)
+     (mpd-serialize-field "bind_to_address" x)))
+  (outputs
+   (list-of-mpd-output (list (mpd-output)))
+   "The audio outputs that MPD can use.
+By default this is a single output using pulseaudio.")
+  (prefix mpd-))
 
 (define (mpd-file-name config file)
   "Return a path in /var/run/mpd/ that is writable
@@ -143,7 +185,7 @@ (define (mpd-shepherd-service config)
    (start #~(make-forkexec-constructor
              (list #$(file-append mpd "/bin/mpd")
                    "--no-daemon"
-                   #$(mpd-config->file config))
+                   #$(mpd-serialize-configuration config))
              #:environment-variables
              ;; Required to detect PulseAudio when run under a user account.
              (list (string-append

base-commit: aae8371f72805cc35e31817e4120468eee4a4a80
-- 
2.38.1





^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [bug#59866] [PATCH v5 2/2] services: mpd: Refactor MPD service.
  2022-12-24 13:51 ` [bug#59866] [PATCH v5 1/2] services: mpd: rewrite using 'define-configuration' mirai
@ 2022-12-24 13:51   ` mirai
  0 siblings, 0 replies; 29+ messages in thread
From: mirai @ 2022-12-24 13:51 UTC (permalink / raw)
  To: 59866; +Cc: Bruno Victal

From: Bruno Victal <mirai@makinata.eu>

Introduces 'mpd-plugin' and 'mpd-partition' records.
Expands 'mpd-output' and 'mpd-configuration' records.
Deprecates redundant abbreviated fields in 'mpd-configuration' and avoids
serializing unused fields that may introduce undesired behavior.
Replace free-form-args serialization by making 'mpd-serialize-field' handle
multiple types and using it with 'generic-serialize-alist'.
Reduce code weight by removing cosmetic indented serialization procedures.
Offload logging from shepherd to MPD.
Implement log-rotation via rottlog.
Implement Shepherd actions: 'reopen' and 'configuration'.

* gnu/services/audio.scm
(mpd-plugin, mpd-partition): New record.

(mpd-plugin?, mpd-partition?, list-of-string?, list-of-symbol?)
(list-of-mpd-plugin?, list-of-mpd-partition?)
(list-of-mpd-plugin-or-output?, port?): New predicate.

(mpd-serialize-field): Handle multiple types.

(mpd-configuration)
[package, group, shepherd-requirement, log-file, log-level, music-directory]
[playlist-directory, endpoints, database, partitions, neighbors, inputs]
[archive-plugins, input-cache-size, decoders, resampler, filters]
[playlist-plugins, extra-options]: New field.
[music-dir, playlist-dir, address]: Deprecate shorthand field.
[db-file, state-file, sticker-file, port, outputs]: Change admissible type.

(mpd-log-rotation): New procedure.

(mpd-shepherd-service)
[actions]: New shepherd actions: 'reopen' and 'configuration'.
[requirement]: Splice with 'shepherd-requirement' field.
[start]: Use 'package' field. Remove #:log-file parameter.

(mpd-service-activation): Create logging directory. Handle migration
from old-style configuration.

(mpd-accounts): Do not hardcode username, change primary group to
"nogroup".

(mpd-service-type): Extend rottlog-service-type for log-rotation.
Update activation-service-type extension to reflect mpd-accounts
procedure change.

* doc/guix.texi (Audio Services)[Music Player Daemon]: Update doc.
---

 Changes since v4:
 * Retitled commit message and added ChangeLog formatted entries.
 * Split long lines into shorter ones.
 * Fix leftover mentions of "addresses".
 * Document that endpoint addresses may contain ports in them.

 doc/guix.texi          | 177 +++++++++++++---
 gnu/services/audio.scm | 463 +++++++++++++++++++++++++++++++----------
 2 files changed, 506 insertions(+), 134 deletions(-)

diff --git a/doc/guix.texi b/doc/guix.texi
index e25692fd27..5663d1913a 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -109,6 +109,7 @@ Copyright @copyright{} 2022 Reily Siegel@*
 Copyright @copyright{} 2022 Simon Streit@*
 Copyright @copyright{} 2022 (@*
 Copyright @copyright{} 2022 John Kehayias@*
+Copyright @copyright{} 2022 Bruno Victal@*
 
 Permission is granted to copy, distribute and/or modify this document
 under the terms of the GNU Free Documentation License, Version 1.3 or
@@ -33014,79 +33015,187 @@ The service type for @command{mpd}
 Data type representing the configuration of @command{mpd}.
 
 @table @asis
-@item @code{user} (default: @code{"mpd"})
+@item @code{package} (default: @code{mpd}) (type: file-like)
+The MPD package.
+
+@item @code{user} (default: @code{"mpd"}) (type: string)
 The user to run mpd as.
 
-@item @code{music-dir} (default: @code{"~/Music"})
+@item @code{group} (type: maybe-string)
+The group to run mpd as.
+
+@item @code{shepherd-requirement} (default: @code{()}) (type: list-of-symbol)
+This is a list of symbols naming Shepherd services that this service
+will depend on.
+
+@item @code{log-file} (default: @code{"/var/log/mpd/log"}) (type: maybe-string)
+The location of the log file.  Set to @code{syslog} to use the local
+syslog daemon or @code{%unset-value} to omit this directive from the
+configuration file.
+
+@item @code{log-level} (type: maybe-string)
+Supress any messages below this threshold.  Available values:
+@code{notice}, @code{info}, @code{verbose}, @code{warning} and
+@code{error}.
+
+@item @code{music-directory} (type: maybe-string)
 The directory to scan for music files.
 
-@item @code{playlist-dir} (default: @code{"~/.mpd/playlists"})
+@item @code{playlist-directory} (type: maybe-string)
 The directory to store playlists.
 
-@item @code{db-file} (default: @code{"~/.mpd/tag_cache"})
+@item @code{db-file} (type: maybe-string)
 The location of the music database.
 
-@item @code{state-file} (default: @code{"~/.mpd/state"})
+@item @code{state-file} (type: maybe-string)
 The location of the file that stores current MPD's state.
 
-@item @code{sticker-file} (default: @code{"~/.mpd/sticker.sql"})
+@item @code{sticker-file} (type: maybe-string)
 The location of the sticker database.
 
-@item @code{port} (default: @code{"6600"})
-The port to run mpd on.
+@item @code{default-port} (default: @code{6600}) (type: maybe-integer)
+The default port to run mpd on.
+
+@item @code{endpoints} (type: maybe-list-of-string)
+The addresses that mpd will bind to.  A different port may be
+specified, e.g. @code{localhost:6602}.  IPv6 addresses must be
+enclosed in square brackets if a different port is used.
+To use a Unix domain socket, an absolute path or a path starting with @code{~}
+can be specified here.
+
+@item @code{database} (type: maybe-mpd-plugin)
+MPD database plugin configuration.
+
+@item @code{partitions} (default: @code{()}) (type: list-of-mpd-partition)
+List of MPD "partitions".
 
-@item @code{address} (default: @code{"any"})
-The address that mpd will bind to.  To use a Unix domain socket,
-an absolute path can be specified here.
+@item @code{neighbors} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD neighbor plugin configurations.
 
-@item @code{outputs} (default: @code{"(list (mpd-output))"})
-The audio outputs that MPD can use.  By default this is a single output using pulseaudio.
+@item @code{inputs} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD input plugin configurations.
+
+@item @code{archive-plugins} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD archive plugin configurations.
+
+@item @code{input-cache-size} (type: maybe-string)
+MPD input cache size.
+
+@item @code{decoders} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD decoder plugin configurations.
+
+@item @code{resampler} (type: maybe-mpd-plugin)
+MPD resampler plugin configuration.
+
+@item @code{filters} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD filter plugin configurations.
+
+@item @code{outputs} (type: list-of-mpd-plugin-or-output)
+The audio outputs that MPD can use.  By default this is a single output
+using pulseaudio.
+
+@item @code{playlist-plugins} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD playlist plugin configurations.
+
+@item @code{extra-options} (default: @code{()}) (type: alist)
+An association list of option symbols/strings to string values to be
+appended to the configuration.
+
+@end table
+@end deftp
+
+@deftp {Data Type} mpd-plugin
+Data type representing a @command{mpd} plugin.
+
+@table @asis
+@item @code{plugin} (type: maybe-string)
+Plugin name.
+
+@item @code{name} (type: maybe-string)
+Name.
+
+@item @code{enabled?} (type: maybe-boolean)
+Whether the plugin is enabled/disabled.
+
+@item @code{extra-options} (default: @code{()}) (type: alist)
+An association list of option symbols/strings to string values to be
+appended to the plugin configuration.  See
+@uref{https://mpd.readthedocs.io/en/latest/plugins.html,MPD plugin
+reference} for available options.
+
+@end table
+@end deftp
+
+@deftp {Data Type} mpd-partition
+Data type representing a @command{mpd} partition.
+
+@table @asis
+@item @code{name} (type: string)
+Partition name.
+
+@item @code{extra-options} (default: @code{()}) (type: alist)
+An association list of option symbols/strings to string values to be
+appended to the partition configuration.  See
+@uref{https://mpd.readthedocs.io/en/latest/user.html#configuring-partitions,Configuring
+Partitions} for available options.
 
 @end table
 @end deftp
 
 @deftp {Data Type} mpd-output
-Data type representing an @command{mpd} audio output.
+Data type representing a @command{mpd} audio output.
 
 @table @asis
-@item @code{name} (default: @code{"MPD"})
+@item @code{name} (default: @code{"MPD"}) (type: string)
 The name of the audio output.
 
-@item @code{type} (default: @code{"pulse"})
+@item @code{type} (default: @code{"pulse"}) (type: string)
 The type of audio output.
 
-@item @code{enabled?} (default: @code{#t})
+@item @code{enabled?} (default: @code{#t}) (type: boolean)
 Specifies whether this audio output is enabled when MPD is started.  By
 default, all audio outputs are enabled.  This is just the default
 setting when there is no state file; with a state file, the previous
 state is restored.
 
-@item @code{tags?} (default: @code{#t})
+@item @code{format} (type: maybe-string)
+Force a specific audio format on output.  See
+@uref{https://mpd.readthedocs.io/en/latest/user.html#audio-output-format,Global
+Audio Format} for a more detailed description.
+
+@item @code{tags?} (default: @code{#t}) (type: boolean)
 If set to @code{#f}, then MPD will not send tags to this output.  This
 is only useful for output plugins that can receive tags, for example the
 @code{httpd} output plugin.
 
-@item @code{always-on?} (default: @code{#f})
+@item @code{always-on?} (default: @code{#f}) (type: boolean)
 If set to @code{#t}, then MPD attempts to keep this audio output always
-open.  This may be useful for streaming servers, when you don’t want to
+open.  This may be useful for streaming servers, when you don?t want to
 disconnect all listeners even when playback is accidentally stopped.
 
-@item @code{mixer-type}
-This field accepts a symbol that specifies which mixer should be used
+@item @code{mixer-type} (default: @code{"none"}) (type: string)
+This field accepts a string that specifies which mixer should be used
 for this audio output: the @code{hardware} mixer, the @code{software}
 mixer, the @code{null} mixer (allows setting the volume, but with no
 effect; this can be used as a trick to implement an external mixer
 External Mixer) or no mixer (@code{none}).
 
-@item @code{extra-options} (default: @code{'()})
-An association list of option symbols to string values to be appended to
-the audio output configuration.
+@item @code{replay-gain-handler} (type: maybe-string)
+This field accepts a string that specifies how
+@uref{https://mpd.readthedocs.io/en/latest/user.html#replay-gain,Replay
+Gain} is to be applied.  @code{software} uses an internal software
+volume control, @code{mixer} uses the configured (hardware) mixer
+control and @code{none} disables replay gain on this audio output.
+
+@item @code{extra-options} (default: @code{()}) (type: alist)
+An association list of option symbols/strings to string values to be
+appended to the audio output configuration.
 
 @end table
 @end deftp
 
-The following example shows a configuration of @code{mpd} that provides
-an HTTP audio streaming output.
+The following example shows a configuration of @command{mpd} that
+configures some of its plugins and provides a HTTP audio streaming output.
 
 @lisp
 (service mpd-service-type
@@ -33098,7 +33207,19 @@ an HTTP audio streaming output.
                      (mixer-type 'null)
                      (extra-options
                       `((encoder . "vorbis")
-                        (port    . "8080"))))))))
+                        (port    . "8080"))))))
+           (decoders
+             (list (mpd-plugin
+                     (plugin "mikmod")
+                     (enabled? #f))
+                   (mpd-plugin
+                     (plugin "openmpt")
+                     (enabled? #t)
+                     (extra-options `((repeat-count . -1)
+                                      (interpolation-filter . 1))))))
+           (resampler (mpd-plugin
+                        (plugin "libsamplerate")
+                        (extra-options `((type . 0)))))))
 @end lisp
 
 
diff --git a/gnu/services/audio.scm b/gnu/services/audio.scm
index 1a1026f342..54b00157b0 100644
--- a/gnu/services/audio.scm
+++ b/gnu/services/audio.scm
@@ -21,19 +21,27 @@
 
 (define-module (gnu services audio)
   #:use-module (guix gexp)
+  #:use-module (guix deprecation)
+  #:use-module (guix diagnostics)
+  #:use-module (guix i18n)
   #:use-module (gnu services)
   #:use-module (gnu services configuration)
   #:use-module (gnu services shepherd)
+  #:use-module (gnu services admin)
   #:use-module (gnu system shadow)
   #:use-module (gnu packages admin)
   #:use-module (gnu packages mpd)
   #:use-module (guix records)
   #:use-module (ice-9 match)
-  #:use-module (ice-9 format)
   #:use-module (srfi srfi-1)
+  #:use-module (srfi srfi-8)
   #:use-module (srfi srfi-26)
   #:export (mpd-output
             mpd-output?
+            mpd-plugin
+            mpd-plugin?
+            mpd-partition
+            mpd-partition?
             mpd-configuration
             mpd-configuration?
             mpd-service-type))
@@ -52,180 +60,421 @@ (define (uglify-field-name field-name)
                                #\-)
                  "_")))
 
-(define (free-form-args? val)
-  (match val
-    (() #t)
-    ((((? symbol?) . (? string?)) . val) (free-form-args? val))
-    (_ #f)))
+(define list-of-string?
+  (list-of string?))
 
-(define* (mpd-serialize-field field-name value #:optional (indent-level 0))
-  #~(begin
-      (use-modules ((ice-9 format)))
-      (format #f "~v/~a \"~a\"~%" #$indent-level #$(if (string? field-name)
-                                                       field-name
-                                                       (uglify-field-name field-name)) #$value)))
+(define list-of-symbol?
+  (list-of symbol?))
 
-(define* (mpd-serialize-free-form-args field-name value #:optional (indent-level 0))
-  (generic-serialize-alist string-append (cut mpd-serialize-field <> <> indent-level) value))
+(define (mpd-serialize-field field-name value)
+  (let ((field (if (string? field-name) field-name
+                   (uglify-field-name field-name)))
+        (value (if (boolean? value) (if value "yes" "no") value)))
+    #~(format #f "~a \"~a\"~%" #$field #$value)))
 
 (define mpd-serialize-number mpd-serialize-field)
 
 (define mpd-serialize-string mpd-serialize-field)
 
-(define* (mpd-serialize-boolean field-name value #:optional (indent-level 0))
-  (mpd-serialize-field field-name (if value "yes" "no") indent-level))
+(define mpd-serialize-boolean mpd-serialize-field)
 
-(define (mpd-serialize-list-of-mpd-output field-name value)
-  #~(string-append "\naudio_output {\n"
-                   #$@(map (cut serialize-configuration <>
-                                mpd-output-fields)
-                           value)
-                   "}\n"))
+(define (mpd-serialize-alist field-name value)
+  #~(string-append #$@(generic-serialize-alist list
+                                               mpd-serialize-field
+                                               value)))
 
-(define (mpd-serialize-configuration configuration)
-  (mixed-text-file
-   "mpd.conf"
-   (serialize-configuration configuration mpd-configuration-fields)))
+(define-maybe string (prefix mpd-))
+(define-maybe list-of-string (prefix mpd-))
+(define-maybe boolean (prefix mpd-))
+
+;;; TODO: Procedures for deprecated fields, to be removed.
+
+(define mpd-deprecated-fields '((music-dir . music-directory)
+                                (playlist-dir . playlist-directory)
+                                (address . endpoints)))
+
+(define (port? value) (or (string? value) (integer? value)))
+
+(define (mpd-serialize-deprecated-field field-name value)
+  (if (maybe-value-set? value)
+      (begin
+        (warn-about-deprecation
+         field-name #f
+         #:replacement (assoc-ref mpd-deprecated-fields field-name))
+        (match field-name
+          ('playlist-dir (mpd-serialize-string "playlist_directory" value))
+          ('music-dir (mpd-serialize-string "music_directory" value))
+          ('address (mpd-serialize-string "bind_to_address" value))))
+      ""))
+
+(define (mpd-serialize-port field-name value)
+  (when (string? value)
+    (warning
+     (G_ "string value for '~a' is deprecated, use integer instead~%")
+     field-name))
+  (mpd-serialize-field field-name value))
+
+(define-maybe port (prefix mpd-))
+
+;;;
+
+;; Generic MPD plugin record, lists only the most prevalent fields.
+(define-configuration mpd-plugin
+  (plugin
+   maybe-string
+   "Plugin name.")
+
+  (name
+   maybe-string
+   "Name.")
+
+  (enabled?
+   maybe-boolean
+   "Whether the plugin is enabled/disabled.")
+
+  (extra-options
+   (alist '())
+   "An association list of option symbols/strings to string values
+to be appended to the plugin configuration. See
+@uref{https://mpd.readthedocs.io/en/latest/plugins.html,MPD plugin reference}
+for available options.")
+
+  (prefix mpd-))
+
+(define (mpd-serialize-mpd-plugin field-name value)
+  #~(format #f "~a {~%~a}~%"
+            '#$field-name
+            #$(serialize-configuration value mpd-plugin-fields)))
+
+(define (mpd-serialize-list-of-mpd-plugin field-name value)
+  #~(string-append #$@(map (cut mpd-serialize-mpd-plugin field-name <>)
+                           value)))
 
-(define mpd-subsystem-serialize-field (cut mpd-serialize-field <> <> 1))
-(define mpd-subsystem-serialize-string (cut mpd-serialize-string <> <> 1))
-(define mpd-subsystem-serialize-number (cut mpd-serialize-number <> <> 1))
-(define mpd-subsystem-serialize-boolean (cut mpd-serialize-boolean <> <> 1))
-(define mpd-subsystem-serialize-free-form-args (cut mpd-serialize-free-form-args <> <> 1))
+(define list-of-mpd-plugin? (list-of mpd-plugin?))
+
+(define-maybe mpd-plugin (prefix mpd-))
+
+(define-configuration mpd-partition
+  (name
+   string
+   "Partition name.")
+
+  (extra-options
+   (alist '())
+   "An association list of option symbols/strings to string values
+to be appended to the partition configuration. See
+@uref{https://mpd.readthedocs.io/en/latest/user.html#configuring-partitions,Configuring Partitions}
+for available options.")
+
+  (prefix mpd-))
+
+(define (mpd-serialize-mpd-partition field-name value)
+  #~(format #f "partition {~%~a}~%"
+            #$(serialize-configuration value mpd-partition-fields)))
+
+(define (mpd-serialize-list-of-mpd-partition field-name value)
+  #~(string-append #$@(map (cut mpd-serialize-mpd-partition #f <>) value)))
+
+(define list-of-mpd-partition?
+  (list-of mpd-partition?))
 
 (define-configuration mpd-output
   (name
    (string "MPD")
    "The name of the audio output.")
+
   (type
    (string "pulse")
    "The type of audio output.")
+
   (enabled?
    (boolean #t)
    "Specifies whether this audio output is enabled when MPD is started. By
 default, all audio outputs are enabled. This is just the default
 setting when there is no state file; with a state file, the previous
 state is restored.")
+
+  (format
+   maybe-string
+   "Force a specific audio format on output. See
+@uref{https://mpd.readthedocs.io/en/latest/user.html#audio-output-format,Global Audio Format}
+for a more detailed description.")
+   
   (tags?
    (boolean #t)
    "If set to @code{#f}, then MPD will not send tags to this output. This
 is only useful for output plugins that can receive tags, for example the
 @code{httpd} output plugin.")
+
   (always-on?
    (boolean #f)
    "If set to @code{#t}, then MPD attempts to keep this audio output always
 open. This may be useful for streaming servers, when you don’t want to
 disconnect all listeners even when playback is accidentally stopped.")
+
   (mixer-type
    (string "none")
-   "This field accepts a symbol that specifies which mixer should be used
+   "This field accepts a string that specifies which mixer should be used
 for this audio output: the @code{hardware} mixer, the @code{software}
 mixer, the @code{null} mixer (allows setting the volume, but with no
 effect; this can be used as a trick to implement an external mixer
 External Mixer) or no mixer (@code{none}).")
+
+  (replay-gain-handler
+   maybe-string
+   "This field accepts a string that specifies how
+@uref{https://mpd.readthedocs.io/en/latest/user.html#replay-gain,Replay Gain}
+is to be applied. @code{software} uses an internal software volume control,
+@code{mixer} uses the configured (hardware) mixer control and @code{none}
+disables replay gain on this audio output.")
+
   (extra-options
-   (free-form-args '())
-   "An association list of option symbols to string values to be appended to
-the audio output configuration.")
-  (prefix mpd-subsystem-))
+   (alist '())
+   "An association list of option symbols/strings to string values
+to be appended to the audio output configuration.")
 
-(define list-of-mpd-output?
-  (list-of mpd-output?))
+  (prefix mpd-))
+
+(define (mpd-serialize-mpd-output field-name value)
+  #~(format #f "audio_output {~%~a}~%"
+            #$(serialize-configuration value mpd-output-fields)))
+
+(define (mpd-serialize-list-of-mpd-plugin-or-output field-name value)
+  (receive (plugins outputs)
+      (partition mpd-plugin? value)
+    #~(string-append #$@(map (cut mpd-serialize-mpd-plugin "audio_output" <>)
+                             plugins)
+                     #$@(map (cut mpd-serialize-mpd-output #f <>) outputs))))
+
+(define list-of-mpd-plugin-or-output?
+  (list-of (lambda (x)
+             (or (mpd-output? x) (mpd-plugin? x)))))
 
 (define-configuration mpd-configuration
+  (package
+   (file-like mpd)
+   "The MPD package."
+   empty-serializer)
+
   (user
    (string "mpd")
    "The user to run mpd as.")
-  (music-dir
-   (string "~/Music")
+
+  (group
+   maybe-string
+   "The group to run mpd as.")
+
+  (shepherd-requirement
+   (list-of-symbol '())
+   "This is a list of symbols naming Shepherd services that this service
+will depend on."
+   empty-serializer)
+
+  (log-file
+   (maybe-string "/var/log/mpd/log")
+   "The location of the log file. Set to @code{syslog} to use the
+local syslog daemon or @code{%unset-value} to omit this directive
+from the configuration file.")
+
+  (log-level
+   maybe-string
+   "Supress any messages below this threshold.
+Available values: @code{notice}, @code{info}, @code{verbose},
+@code{warning} and @code{error}.")
+
+  (music-directory
+   maybe-string
+   "The directory to scan for music files.")
+
+  (music-dir ; TODO: deprecated, remove later
+   maybe-string
    "The directory to scan for music files."
-   (lambda (_ x)
-     (mpd-serialize-field "music_directory" x)))
-  (playlist-dir
-   (string "~/.mpd/playlists")
+   mpd-serialize-deprecated-field)
+
+  (playlist-directory
+   maybe-string
+   "The directory to store playlists.")
+
+  (playlist-dir ; TODO: deprecated, remove later
+   maybe-string
    "The directory to store playlists."
-   (lambda (_ x)
-     (mpd-serialize-field "playlist_directory" x)))
+   mpd-serialize-deprecated-field)
+
   (db-file
-   (string "~/.mpd/tag_cache")
+   maybe-string
    "The location of the music database.")
+
   (state-file
-   (string "~/.mpd/state")
+   maybe-string
    "The location of the file that stores current MPD's state.")
+
   (sticker-file
-   (string "~/.mpd/sticker.sql")
+   maybe-string
    "The location of the sticker database.")
-  (port
-   (string "6600")
-   "The port to run mpd on.")
-  (address
-   (string "any")
+
+  (default-port
+   (maybe-port 6600) ; TODO: switch to integer
+   "The default port to run mpd on.")
+
+  (endpoints
+   maybe-list-of-string
+   "The addresses that mpd will bind to. A different port may be
+specified, e.g. @code{localhost:6602}. IPv6 addresses must be
+enclosed in square brackets if a different port is used.
+To use a Unix domain socket, an absolute path or a path starting with @code{~}
+can be specified here."
+   (lambda (_ x)
+     (if (maybe-value-set? x)
+         #~(string-append #$@(map
+                              (cut mpd-serialize-field "bind_to_address" <>)
+                              x)) "")))
+
+  (address ; TODO: deprecated, remove later
+   maybe-string
    "The address that mpd will bind to.
 To use a Unix domain socket, an absolute path can be specified here."
+   mpd-serialize-deprecated-field)
+
+  (database
+   maybe-mpd-plugin
+   "MPD database plugin configuration.")
+
+  (partitions
+   (list-of-mpd-partition '())
+   "List of MPD \"partitions\".")
+  
+  (neighbors
+   (list-of-mpd-plugin '())
+   "List of MPD neighbor plugin configurations.")
+
+  (inputs
+   (list-of-mpd-plugin '())
+   "List of MPD input plugin configurations."
+   (lambda (_ x)
+     (mpd-serialize-list-of-mpd-plugin "input" x)))
+
+  (archive-plugins
+   (list-of-mpd-plugin '())
+   "List of MPD archive plugin configurations."
    (lambda (_ x)
-     (mpd-serialize-field "bind_to_address" x)))
+     (mpd-serialize-list-of-mpd-plugin "archive_plugin" x)))
+
+  (input-cache-size
+   maybe-string
+   "MPD input cache size."
+   (lambda (_ x)
+     (if (maybe-value-set? x)
+         #~(string-append "\ninput_cache {\n"
+                          #$(mpd-serialize-string "size" x)
+                          "}\n") "")))
+
+  (decoders
+   (list-of-mpd-plugin '())
+   "List of MPD decoder plugin configurations."
+   (lambda (_ x)
+     (mpd-serialize-list-of-mpd-plugin "decoder" x)))
+
+  (resampler
+   maybe-mpd-plugin
+   "MPD resampler plugin configuration.")
+
+  (filters
+   (list-of-mpd-plugin '())
+   "List of MPD filter plugin configurations."
+   (lambda (_ x)
+     (mpd-serialize-list-of-mpd-plugin "filter" x)))
+
   (outputs
-   (list-of-mpd-output (list (mpd-output)))
+   (list-of-mpd-plugin-or-output (list (mpd-output)))
    "The audio outputs that MPD can use.
 By default this is a single output using pulseaudio.")
+
+  (playlist-plugins
+   (list-of-mpd-plugin '())
+   "List of MPD playlist plugin configurations."
+   (lambda (_ x)
+     (mpd-serialize-list-of-mpd-plugin "playlist_plugin" x)))
+
+  (extra-options
+   (alist '())
+   "An association list of option symbols/strings to string values to be
+appended to the configuration.")
+
   (prefix mpd-))
 
-(define (mpd-file-name config file)
-  "Return a path in /var/run/mpd/ that is writable
-   by @code{user} from @code{config}."
-  (string-append "/var/run/mpd/"
-                 (mpd-configuration-user config)
-                 "/" file))
+(define (mpd-serialize-configuration configuration)
+  (mixed-text-file
+   "mpd.conf"
+   (serialize-configuration configuration mpd-configuration-fields)))
+
+(define (mpd-log-rotation config)
+  (match-record config <mpd-configuration> (log-file)
+    (log-rotation
+     (files (list log-file))
+     (post-rotate #~(begin
+                      (use-modules (gnu services herd))
+                      (with-shepherd-action 'mpd ('reopen) #f))))))
 
 (define (mpd-shepherd-service config)
-  (shepherd-service
-   (documentation "Run the MPD (Music Player Daemon)")
-   (requirement '(user-processes))
-   (provision '(mpd))
-   (start #~(make-forkexec-constructor
-             (list #$(file-append mpd "/bin/mpd")
-                   "--no-daemon"
-                   #$(mpd-serialize-configuration config))
-             #:environment-variables
-             ;; Required to detect PulseAudio when run under a user account.
-             (list (string-append
-                    "XDG_RUNTIME_DIR=/run/user/"
-                    (number->string
-                     (passwd:uid
-                      (getpwnam #$(mpd-configuration-user config))))))
-             #:log-file #$(mpd-file-name config "log")))
-   (stop  #~(make-kill-destructor))))
+  (match-record config <mpd-configuration> (user package shepherd-requirement)
+    (let* ((config-file (mpd-serialize-configuration config)))
+      (shepherd-service
+       (documentation "Run the MPD (Music Player Daemon)")
+       (requirement `(user-processes loopback ,@shepherd-requirement))
+       (provision '(mpd))
+       (start #~(make-forkexec-constructor
+                 (list #$(file-append package "/bin/mpd")
+                       "--no-daemon"
+                       #$config-file)
+                 #:environment-variables
+                 ;; Required to detect PulseAudio when run under a user account.
+                 (list (string-append
+                        "XDG_RUNTIME_DIR=/run/user/"
+                        (number->string (passwd:uid (getpwnam #$user)))))))
+       (stop  #~(make-kill-destructor))
+       (actions
+        (list (shepherd-configuration-action config-file)
+              (shepherd-action
+               (name 'reopen)
+               (documentation "Re-open log files and flush caches.")
+               (procedure
+                #~(lambda (pid)
+                    (if pid
+                        (begin
+                          (kill pid SIGHUP)
+                          (format #t
+                                  "Issued SIGHUP to Service MPD (PID ~a)."
+                                  pid))
+                        (format #t "Service MPD is not running.")))))))))))
 
 (define (mpd-service-activation config)
-  (with-imported-modules '((guix build utils))
+  (match-record config <mpd-configuration> (user log-file)
     #~(begin
         (use-modules (guix build utils))
-        (define %user
-          (getpw #$(mpd-configuration-user config)))
-
-        (let ((directory #$(mpd-file-name config ".mpd")))
-          (mkdir-p directory)
-          (chown directory (passwd:uid %user) (passwd:gid %user))
-
-          ;; Make /var/run/mpd/USER user-owned as well.
-          (chown (dirname directory)
-                 (passwd:uid %user) (passwd:gid %user))))))
-
-
-(define %mpd-accounts
-  ;; Default account and group for MPD.
-  (list (user-group (name "mpd") (system? #t))
-        (user-account
-         (name "mpd")
-         (group "mpd")
-         (system? #t)
-         (comment "Music Player Daemon (MPD) user")
 
-         ;; Note: /var/run/mpd hosts one sub-directory per user, of which
-         ;; /var/run/mpd/mpd corresponds to the "mpd" user.
-         (home-directory "/var/run/mpd/mpd")
+        ;; TODO: remove me, migrates from the old location at /var/run/mpd
+        ;;       to the new one at /var/lib/mpd.
+        (let* ((user (getpw #$user))
+               (deprecated-directory (string-append "/var/run/mpd/"
+                                                    #$user "/.mpd"))
+               (new-directory (string-append (passwd:dir user)
+                                             "/.config/mpd")))
+          (when (and (file-exists? deprecated-directory)
+                     (not (file-exists? new-directory)))
+            (rename-file deprecated-directory new-directory)
+            (chown new-directory (passwd:uid user) (passwd:gid user))))
+        (mkdir-p (dirname #$log-file)))))
 
-         (shell (file-append shadow "/sbin/nologin")))))
+(define (mpd-accounts config)
+  (match-record config <mpd-configuration> (user)
+    (list (user-account
+           (name user)
+           (group "nogroup")
+           (system? #t)
+           (comment "Music Player Daemon (MPD) user")
+           ;; MPD can use $HOME (or $XDG_CONFIG_HOME) to place its data
+           (home-directory "/var/lib/mpd")
+           (shell (file-append shadow "/sbin/nologin"))))))
 
 (define mpd-service-type
   (service-type
@@ -235,7 +484,9 @@ (define mpd-service-type
     (list (service-extension shepherd-root-service-type
                              (compose list mpd-shepherd-service))
           (service-extension account-service-type
-                             (const %mpd-accounts))
+                             mpd-accounts)
           (service-extension activation-service-type
-                             mpd-service-activation)))
+                             mpd-service-activation)
+          (service-extension rottlog-service-type
+                             (compose list mpd-log-rotation))))
    (default-value (mpd-configuration))))
-- 
2.38.1





^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [bug#59866] [PATCH v5.1] services: mpd: Refactor MPD service.
  2022-12-06 23:22 [bug#59866] [PATCH 0/2] services: mpd: Refactor MPD service mirai
                   ` (7 preceding siblings ...)
  2022-12-24 13:51 ` [bug#59866] [PATCH v5 1/2] services: mpd: rewrite using 'define-configuration' mirai
@ 2022-12-24 14:11 ` mirai
  2022-12-24 17:20   ` Liliana Marie Prikler
  2022-12-24 18:53 ` [bug#59866] [PATCH v5.2] " mirai
                   ` (2 subsequent siblings)
  11 siblings, 1 reply; 29+ messages in thread
From: mirai @ 2022-12-24 14:11 UTC (permalink / raw)
  To: 59866; +Cc: Bruno Victal

From: Bruno Victal <mirai@makinata.eu>

Introduces 'mpd-plugin' and 'mpd-partition' records.
Expands 'mpd-output' and 'mpd-configuration' records.
Deprecates redundant abbreviated fields in 'mpd-configuration' and avoids
serializing unused fields that may introduce undesired behavior.
Replace free-form-args serialization by making 'mpd-serialize-field' handle
multiple types and using it with 'generic-serialize-alist'.
Reduce code weight by removing cosmetic indented serialization procedures.
Offload logging from shepherd to MPD.
Implement log-rotation via rottlog.
Implement Shepherd actions: 'reopen' and 'configuration'.

* gnu/services/audio.scm
(mpd-plugin, mpd-partition): New record.

(mpd-plugin?, mpd-partition?, list-of-string?, list-of-symbol?)
(list-of-mpd-plugin?, list-of-mpd-partition?)
(list-of-mpd-plugin-or-output?, port?): New predicate.

(mpd-serialize-field): Handle multiple types.

(mpd-configuration)
[package, group, shepherd-requirement, log-file, log-level, music-directory]
[playlist-directory, endpoints, database, partitions, neighbors, inputs]
[archive-plugins, input-cache-size, decoders, resampler, filters]
[playlist-plugins, extra-options]: New field.
[music-dir, playlist-dir, address]: Deprecate shorthand field.
[db-file, state-file, sticker-file, port, outputs]: Change admissible type.

(mpd-log-rotation): New procedure.

(mpd-shepherd-service)
[actions]: New shepherd actions: 'reopen' and 'configuration'.
[requirement]: Splice with 'shepherd-requirement' field.
[start]: Use 'package' field. Remove #:log-file parameter.

(mpd-service-activation): Create logging directory. Handle migration
from old-style configuration.

(mpd-accounts): Do not hardcode username, change primary group to
"nogroup".

(mpd-service-type): Extend rottlog-service-type for log-rotation.
Update activation-service-type extension to reflect mpd-accounts
procedure change.

* doc/guix.texi (Audio Services)[Music Player Daemon]: Update doc.
---

 Changes since v5:
 * Serialize default_port field-name as "port".

 doc/guix.texi          | 177 +++++++++++++---
 gnu/services/audio.scm | 463 +++++++++++++++++++++++++++++++----------
 2 files changed, 506 insertions(+), 134 deletions(-)

diff --git a/doc/guix.texi b/doc/guix.texi
index e25692fd27..5663d1913a 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -109,6 +109,7 @@ Copyright @copyright{} 2022 Reily Siegel@*
 Copyright @copyright{} 2022 Simon Streit@*
 Copyright @copyright{} 2022 (@*
 Copyright @copyright{} 2022 John Kehayias@*
+Copyright @copyright{} 2022 Bruno Victal@*
 
 Permission is granted to copy, distribute and/or modify this document
 under the terms of the GNU Free Documentation License, Version 1.3 or
@@ -33014,79 +33015,187 @@ The service type for @command{mpd}
 Data type representing the configuration of @command{mpd}.
 
 @table @asis
-@item @code{user} (default: @code{"mpd"})
+@item @code{package} (default: @code{mpd}) (type: file-like)
+The MPD package.
+
+@item @code{user} (default: @code{"mpd"}) (type: string)
 The user to run mpd as.
 
-@item @code{music-dir} (default: @code{"~/Music"})
+@item @code{group} (type: maybe-string)
+The group to run mpd as.
+
+@item @code{shepherd-requirement} (default: @code{()}) (type: list-of-symbol)
+This is a list of symbols naming Shepherd services that this service
+will depend on.
+
+@item @code{log-file} (default: @code{"/var/log/mpd/log"}) (type: maybe-string)
+The location of the log file.  Set to @code{syslog} to use the local
+syslog daemon or @code{%unset-value} to omit this directive from the
+configuration file.
+
+@item @code{log-level} (type: maybe-string)
+Supress any messages below this threshold.  Available values:
+@code{notice}, @code{info}, @code{verbose}, @code{warning} and
+@code{error}.
+
+@item @code{music-directory} (type: maybe-string)
 The directory to scan for music files.
 
-@item @code{playlist-dir} (default: @code{"~/.mpd/playlists"})
+@item @code{playlist-directory} (type: maybe-string)
 The directory to store playlists.
 
-@item @code{db-file} (default: @code{"~/.mpd/tag_cache"})
+@item @code{db-file} (type: maybe-string)
 The location of the music database.
 
-@item @code{state-file} (default: @code{"~/.mpd/state"})
+@item @code{state-file} (type: maybe-string)
 The location of the file that stores current MPD's state.
 
-@item @code{sticker-file} (default: @code{"~/.mpd/sticker.sql"})
+@item @code{sticker-file} (type: maybe-string)
 The location of the sticker database.
 
-@item @code{port} (default: @code{"6600"})
-The port to run mpd on.
+@item @code{default-port} (default: @code{6600}) (type: maybe-integer)
+The default port to run mpd on.
+
+@item @code{endpoints} (type: maybe-list-of-string)
+The addresses that mpd will bind to.  A different port may be
+specified, e.g. @code{localhost:6602}.  IPv6 addresses must be
+enclosed in square brackets if a different port is used.
+To use a Unix domain socket, an absolute path or a path starting with @code{~}
+can be specified here.
+
+@item @code{database} (type: maybe-mpd-plugin)
+MPD database plugin configuration.
+
+@item @code{partitions} (default: @code{()}) (type: list-of-mpd-partition)
+List of MPD "partitions".
 
-@item @code{address} (default: @code{"any"})
-The address that mpd will bind to.  To use a Unix domain socket,
-an absolute path can be specified here.
+@item @code{neighbors} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD neighbor plugin configurations.
 
-@item @code{outputs} (default: @code{"(list (mpd-output))"})
-The audio outputs that MPD can use.  By default this is a single output using pulseaudio.
+@item @code{inputs} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD input plugin configurations.
+
+@item @code{archive-plugins} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD archive plugin configurations.
+
+@item @code{input-cache-size} (type: maybe-string)
+MPD input cache size.
+
+@item @code{decoders} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD decoder plugin configurations.
+
+@item @code{resampler} (type: maybe-mpd-plugin)
+MPD resampler plugin configuration.
+
+@item @code{filters} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD filter plugin configurations.
+
+@item @code{outputs} (type: list-of-mpd-plugin-or-output)
+The audio outputs that MPD can use.  By default this is a single output
+using pulseaudio.
+
+@item @code{playlist-plugins} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD playlist plugin configurations.
+
+@item @code{extra-options} (default: @code{()}) (type: alist)
+An association list of option symbols/strings to string values to be
+appended to the configuration.
+
+@end table
+@end deftp
+
+@deftp {Data Type} mpd-plugin
+Data type representing a @command{mpd} plugin.
+
+@table @asis
+@item @code{plugin} (type: maybe-string)
+Plugin name.
+
+@item @code{name} (type: maybe-string)
+Name.
+
+@item @code{enabled?} (type: maybe-boolean)
+Whether the plugin is enabled/disabled.
+
+@item @code{extra-options} (default: @code{()}) (type: alist)
+An association list of option symbols/strings to string values to be
+appended to the plugin configuration.  See
+@uref{https://mpd.readthedocs.io/en/latest/plugins.html,MPD plugin
+reference} for available options.
+
+@end table
+@end deftp
+
+@deftp {Data Type} mpd-partition
+Data type representing a @command{mpd} partition.
+
+@table @asis
+@item @code{name} (type: string)
+Partition name.
+
+@item @code{extra-options} (default: @code{()}) (type: alist)
+An association list of option symbols/strings to string values to be
+appended to the partition configuration.  See
+@uref{https://mpd.readthedocs.io/en/latest/user.html#configuring-partitions,Configuring
+Partitions} for available options.
 
 @end table
 @end deftp
 
 @deftp {Data Type} mpd-output
-Data type representing an @command{mpd} audio output.
+Data type representing a @command{mpd} audio output.
 
 @table @asis
-@item @code{name} (default: @code{"MPD"})
+@item @code{name} (default: @code{"MPD"}) (type: string)
 The name of the audio output.
 
-@item @code{type} (default: @code{"pulse"})
+@item @code{type} (default: @code{"pulse"}) (type: string)
 The type of audio output.
 
-@item @code{enabled?} (default: @code{#t})
+@item @code{enabled?} (default: @code{#t}) (type: boolean)
 Specifies whether this audio output is enabled when MPD is started.  By
 default, all audio outputs are enabled.  This is just the default
 setting when there is no state file; with a state file, the previous
 state is restored.
 
-@item @code{tags?} (default: @code{#t})
+@item @code{format} (type: maybe-string)
+Force a specific audio format on output.  See
+@uref{https://mpd.readthedocs.io/en/latest/user.html#audio-output-format,Global
+Audio Format} for a more detailed description.
+
+@item @code{tags?} (default: @code{#t}) (type: boolean)
 If set to @code{#f}, then MPD will not send tags to this output.  This
 is only useful for output plugins that can receive tags, for example the
 @code{httpd} output plugin.
 
-@item @code{always-on?} (default: @code{#f})
+@item @code{always-on?} (default: @code{#f}) (type: boolean)
 If set to @code{#t}, then MPD attempts to keep this audio output always
-open.  This may be useful for streaming servers, when you don’t want to
+open.  This may be useful for streaming servers, when you don?t want to
 disconnect all listeners even when playback is accidentally stopped.
 
-@item @code{mixer-type}
-This field accepts a symbol that specifies which mixer should be used
+@item @code{mixer-type} (default: @code{"none"}) (type: string)
+This field accepts a string that specifies which mixer should be used
 for this audio output: the @code{hardware} mixer, the @code{software}
 mixer, the @code{null} mixer (allows setting the volume, but with no
 effect; this can be used as a trick to implement an external mixer
 External Mixer) or no mixer (@code{none}).
 
-@item @code{extra-options} (default: @code{'()})
-An association list of option symbols to string values to be appended to
-the audio output configuration.
+@item @code{replay-gain-handler} (type: maybe-string)
+This field accepts a string that specifies how
+@uref{https://mpd.readthedocs.io/en/latest/user.html#replay-gain,Replay
+Gain} is to be applied.  @code{software} uses an internal software
+volume control, @code{mixer} uses the configured (hardware) mixer
+control and @code{none} disables replay gain on this audio output.
+
+@item @code{extra-options} (default: @code{()}) (type: alist)
+An association list of option symbols/strings to string values to be
+appended to the audio output configuration.
 
 @end table
 @end deftp
 
-The following example shows a configuration of @code{mpd} that provides
-an HTTP audio streaming output.
+The following example shows a configuration of @command{mpd} that
+configures some of its plugins and provides a HTTP audio streaming output.
 
 @lisp
 (service mpd-service-type
@@ -33098,7 +33207,19 @@ an HTTP audio streaming output.
                      (mixer-type 'null)
                      (extra-options
                       `((encoder . "vorbis")
-                        (port    . "8080"))))))))
+                        (port    . "8080"))))))
+           (decoders
+             (list (mpd-plugin
+                     (plugin "mikmod")
+                     (enabled? #f))
+                   (mpd-plugin
+                     (plugin "openmpt")
+                     (enabled? #t)
+                     (extra-options `((repeat-count . -1)
+                                      (interpolation-filter . 1))))))
+           (resampler (mpd-plugin
+                        (plugin "libsamplerate")
+                        (extra-options `((type . 0)))))))
 @end lisp
 
 
diff --git a/gnu/services/audio.scm b/gnu/services/audio.scm
index 1a1026f342..e456205e99 100644
--- a/gnu/services/audio.scm
+++ b/gnu/services/audio.scm
@@ -21,19 +21,27 @@
 
 (define-module (gnu services audio)
   #:use-module (guix gexp)
+  #:use-module (guix deprecation)
+  #:use-module (guix diagnostics)
+  #:use-module (guix i18n)
   #:use-module (gnu services)
   #:use-module (gnu services configuration)
   #:use-module (gnu services shepherd)
+  #:use-module (gnu services admin)
   #:use-module (gnu system shadow)
   #:use-module (gnu packages admin)
   #:use-module (gnu packages mpd)
   #:use-module (guix records)
   #:use-module (ice-9 match)
-  #:use-module (ice-9 format)
   #:use-module (srfi srfi-1)
+  #:use-module (srfi srfi-8)
   #:use-module (srfi srfi-26)
   #:export (mpd-output
             mpd-output?
+            mpd-plugin
+            mpd-plugin?
+            mpd-partition
+            mpd-partition?
             mpd-configuration
             mpd-configuration?
             mpd-service-type))
@@ -52,180 +60,421 @@ (define (uglify-field-name field-name)
                                #\-)
                  "_")))
 
-(define (free-form-args? val)
-  (match val
-    (() #t)
-    ((((? symbol?) . (? string?)) . val) (free-form-args? val))
-    (_ #f)))
+(define list-of-string?
+  (list-of string?))
 
-(define* (mpd-serialize-field field-name value #:optional (indent-level 0))
-  #~(begin
-      (use-modules ((ice-9 format)))
-      (format #f "~v/~a \"~a\"~%" #$indent-level #$(if (string? field-name)
-                                                       field-name
-                                                       (uglify-field-name field-name)) #$value)))
+(define list-of-symbol?
+  (list-of symbol?))
 
-(define* (mpd-serialize-free-form-args field-name value #:optional (indent-level 0))
-  (generic-serialize-alist string-append (cut mpd-serialize-field <> <> indent-level) value))
+(define (mpd-serialize-field field-name value)
+  (let ((field (if (string? field-name) field-name
+                   (uglify-field-name field-name)))
+        (value (if (boolean? value) (if value "yes" "no") value)))
+    #~(format #f "~a \"~a\"~%" #$field #$value)))
 
 (define mpd-serialize-number mpd-serialize-field)
 
 (define mpd-serialize-string mpd-serialize-field)
 
-(define* (mpd-serialize-boolean field-name value #:optional (indent-level 0))
-  (mpd-serialize-field field-name (if value "yes" "no") indent-level))
+(define mpd-serialize-boolean mpd-serialize-field)
 
-(define (mpd-serialize-list-of-mpd-output field-name value)
-  #~(string-append "\naudio_output {\n"
-                   #$@(map (cut serialize-configuration <>
-                                mpd-output-fields)
-                           value)
-                   "}\n"))
+(define (mpd-serialize-alist field-name value)
+  #~(string-append #$@(generic-serialize-alist list
+                                               mpd-serialize-field
+                                               value)))
 
-(define (mpd-serialize-configuration configuration)
-  (mixed-text-file
-   "mpd.conf"
-   (serialize-configuration configuration mpd-configuration-fields)))
+(define-maybe string (prefix mpd-))
+(define-maybe list-of-string (prefix mpd-))
+(define-maybe boolean (prefix mpd-))
+
+;;; TODO: Procedures for deprecated fields, to be removed.
+
+(define mpd-deprecated-fields '((music-dir . music-directory)
+                                (playlist-dir . playlist-directory)
+                                (address . endpoints)))
+
+(define (port? value) (or (string? value) (integer? value)))
+
+(define (mpd-serialize-deprecated-field field-name value)
+  (if (maybe-value-set? value)
+      (begin
+        (warn-about-deprecation
+         field-name #f
+         #:replacement (assoc-ref mpd-deprecated-fields field-name))
+        (match field-name
+          ('playlist-dir (mpd-serialize-string "playlist_directory" value))
+          ('music-dir (mpd-serialize-string "music_directory" value))
+          ('address (mpd-serialize-string "bind_to_address" value))))
+      ""))
+
+(define (mpd-serialize-port field-name value)
+  (when (string? value)
+    (warning
+     (G_ "string value for '~a' is deprecated, use integer instead~%")
+     field-name))
+  (mpd-serialize-field "port" value))
+
+(define-maybe port (prefix mpd-))
+
+;;;
+
+;; Generic MPD plugin record, lists only the most prevalent fields.
+(define-configuration mpd-plugin
+  (plugin
+   maybe-string
+   "Plugin name.")
+
+  (name
+   maybe-string
+   "Name.")
+
+  (enabled?
+   maybe-boolean
+   "Whether the plugin is enabled/disabled.")
+
+  (extra-options
+   (alist '())
+   "An association list of option symbols/strings to string values
+to be appended to the plugin configuration. See
+@uref{https://mpd.readthedocs.io/en/latest/plugins.html,MPD plugin reference}
+for available options.")
+
+  (prefix mpd-))
+
+(define (mpd-serialize-mpd-plugin field-name value)
+  #~(format #f "~a {~%~a}~%"
+            '#$field-name
+            #$(serialize-configuration value mpd-plugin-fields)))
+
+(define (mpd-serialize-list-of-mpd-plugin field-name value)
+  #~(string-append #$@(map (cut mpd-serialize-mpd-plugin field-name <>)
+                           value)))
 
-(define mpd-subsystem-serialize-field (cut mpd-serialize-field <> <> 1))
-(define mpd-subsystem-serialize-string (cut mpd-serialize-string <> <> 1))
-(define mpd-subsystem-serialize-number (cut mpd-serialize-number <> <> 1))
-(define mpd-subsystem-serialize-boolean (cut mpd-serialize-boolean <> <> 1))
-(define mpd-subsystem-serialize-free-form-args (cut mpd-serialize-free-form-args <> <> 1))
+(define list-of-mpd-plugin? (list-of mpd-plugin?))
+
+(define-maybe mpd-plugin (prefix mpd-))
+
+(define-configuration mpd-partition
+  (name
+   string
+   "Partition name.")
+
+  (extra-options
+   (alist '())
+   "An association list of option symbols/strings to string values
+to be appended to the partition configuration. See
+@uref{https://mpd.readthedocs.io/en/latest/user.html#configuring-partitions,Configuring Partitions}
+for available options.")
+
+  (prefix mpd-))
+
+(define (mpd-serialize-mpd-partition field-name value)
+  #~(format #f "partition {~%~a}~%"
+            #$(serialize-configuration value mpd-partition-fields)))
+
+(define (mpd-serialize-list-of-mpd-partition field-name value)
+  #~(string-append #$@(map (cut mpd-serialize-mpd-partition #f <>) value)))
+
+(define list-of-mpd-partition?
+  (list-of mpd-partition?))
 
 (define-configuration mpd-output
   (name
    (string "MPD")
    "The name of the audio output.")
+
   (type
    (string "pulse")
    "The type of audio output.")
+
   (enabled?
    (boolean #t)
    "Specifies whether this audio output is enabled when MPD is started. By
 default, all audio outputs are enabled. This is just the default
 setting when there is no state file; with a state file, the previous
 state is restored.")
+
+  (format
+   maybe-string
+   "Force a specific audio format on output. See
+@uref{https://mpd.readthedocs.io/en/latest/user.html#audio-output-format,Global Audio Format}
+for a more detailed description.")
+   
   (tags?
    (boolean #t)
    "If set to @code{#f}, then MPD will not send tags to this output. This
 is only useful for output plugins that can receive tags, for example the
 @code{httpd} output plugin.")
+
   (always-on?
    (boolean #f)
    "If set to @code{#t}, then MPD attempts to keep this audio output always
 open. This may be useful for streaming servers, when you don’t want to
 disconnect all listeners even when playback is accidentally stopped.")
+
   (mixer-type
    (string "none")
-   "This field accepts a symbol that specifies which mixer should be used
+   "This field accepts a string that specifies which mixer should be used
 for this audio output: the @code{hardware} mixer, the @code{software}
 mixer, the @code{null} mixer (allows setting the volume, but with no
 effect; this can be used as a trick to implement an external mixer
 External Mixer) or no mixer (@code{none}).")
+
+  (replay-gain-handler
+   maybe-string
+   "This field accepts a string that specifies how
+@uref{https://mpd.readthedocs.io/en/latest/user.html#replay-gain,Replay Gain}
+is to be applied. @code{software} uses an internal software volume control,
+@code{mixer} uses the configured (hardware) mixer control and @code{none}
+disables replay gain on this audio output.")
+
   (extra-options
-   (free-form-args '())
-   "An association list of option symbols to string values to be appended to
-the audio output configuration.")
-  (prefix mpd-subsystem-))
+   (alist '())
+   "An association list of option symbols/strings to string values
+to be appended to the audio output configuration.")
 
-(define list-of-mpd-output?
-  (list-of mpd-output?))
+  (prefix mpd-))
+
+(define (mpd-serialize-mpd-output field-name value)
+  #~(format #f "audio_output {~%~a}~%"
+            #$(serialize-configuration value mpd-output-fields)))
+
+(define (mpd-serialize-list-of-mpd-plugin-or-output field-name value)
+  (receive (plugins outputs)
+      (partition mpd-plugin? value)
+    #~(string-append #$@(map (cut mpd-serialize-mpd-plugin "audio_output" <>)
+                             plugins)
+                     #$@(map (cut mpd-serialize-mpd-output #f <>) outputs))))
+
+(define list-of-mpd-plugin-or-output?
+  (list-of (lambda (x)
+             (or (mpd-output? x) (mpd-plugin? x)))))
 
 (define-configuration mpd-configuration
+  (package
+   (file-like mpd)
+   "The MPD package."
+   empty-serializer)
+
   (user
    (string "mpd")
    "The user to run mpd as.")
-  (music-dir
-   (string "~/Music")
+
+  (group
+   maybe-string
+   "The group to run mpd as.")
+
+  (shepherd-requirement
+   (list-of-symbol '())
+   "This is a list of symbols naming Shepherd services that this service
+will depend on."
+   empty-serializer)
+
+  (log-file
+   (maybe-string "/var/log/mpd/log")
+   "The location of the log file. Set to @code{syslog} to use the
+local syslog daemon or @code{%unset-value} to omit this directive
+from the configuration file.")
+
+  (log-level
+   maybe-string
+   "Supress any messages below this threshold.
+Available values: @code{notice}, @code{info}, @code{verbose},
+@code{warning} and @code{error}.")
+
+  (music-directory
+   maybe-string
+   "The directory to scan for music files.")
+
+  (music-dir ; TODO: deprecated, remove later
+   maybe-string
    "The directory to scan for music files."
-   (lambda (_ x)
-     (mpd-serialize-field "music_directory" x)))
-  (playlist-dir
-   (string "~/.mpd/playlists")
+   mpd-serialize-deprecated-field)
+
+  (playlist-directory
+   maybe-string
+   "The directory to store playlists.")
+
+  (playlist-dir ; TODO: deprecated, remove later
+   maybe-string
    "The directory to store playlists."
-   (lambda (_ x)
-     (mpd-serialize-field "playlist_directory" x)))
+   mpd-serialize-deprecated-field)
+
   (db-file
-   (string "~/.mpd/tag_cache")
+   maybe-string
    "The location of the music database.")
+
   (state-file
-   (string "~/.mpd/state")
+   maybe-string
    "The location of the file that stores current MPD's state.")
+
   (sticker-file
-   (string "~/.mpd/sticker.sql")
+   maybe-string
    "The location of the sticker database.")
-  (port
-   (string "6600")
-   "The port to run mpd on.")
-  (address
-   (string "any")
+
+  (default-port
+   (maybe-port 6600) ; TODO: switch to integer
+   "The default port to run mpd on.")
+
+  (endpoints
+   maybe-list-of-string
+   "The addresses that mpd will bind to. A different port may be
+specified, e.g. @code{localhost:6602}. IPv6 addresses must be
+enclosed in square brackets if a different port is used.
+To use a Unix domain socket, an absolute path or a path starting with @code{~}
+can be specified here."
+   (lambda (_ x)
+     (if (maybe-value-set? x)
+         #~(string-append #$@(map
+                              (cut mpd-serialize-field "bind_to_address" <>)
+                              x)) "")))
+
+  (address ; TODO: deprecated, remove later
+   maybe-string
    "The address that mpd will bind to.
 To use a Unix domain socket, an absolute path can be specified here."
+   mpd-serialize-deprecated-field)
+
+  (database
+   maybe-mpd-plugin
+   "MPD database plugin configuration.")
+
+  (partitions
+   (list-of-mpd-partition '())
+   "List of MPD \"partitions\".")
+  
+  (neighbors
+   (list-of-mpd-plugin '())
+   "List of MPD neighbor plugin configurations.")
+
+  (inputs
+   (list-of-mpd-plugin '())
+   "List of MPD input plugin configurations."
+   (lambda (_ x)
+     (mpd-serialize-list-of-mpd-plugin "input" x)))
+
+  (archive-plugins
+   (list-of-mpd-plugin '())
+   "List of MPD archive plugin configurations."
    (lambda (_ x)
-     (mpd-serialize-field "bind_to_address" x)))
+     (mpd-serialize-list-of-mpd-plugin "archive_plugin" x)))
+
+  (input-cache-size
+   maybe-string
+   "MPD input cache size."
+   (lambda (_ x)
+     (if (maybe-value-set? x)
+         #~(string-append "\ninput_cache {\n"
+                          #$(mpd-serialize-string "size" x)
+                          "}\n") "")))
+
+  (decoders
+   (list-of-mpd-plugin '())
+   "List of MPD decoder plugin configurations."
+   (lambda (_ x)
+     (mpd-serialize-list-of-mpd-plugin "decoder" x)))
+
+  (resampler
+   maybe-mpd-plugin
+   "MPD resampler plugin configuration.")
+
+  (filters
+   (list-of-mpd-plugin '())
+   "List of MPD filter plugin configurations."
+   (lambda (_ x)
+     (mpd-serialize-list-of-mpd-plugin "filter" x)))
+
   (outputs
-   (list-of-mpd-output (list (mpd-output)))
+   (list-of-mpd-plugin-or-output (list (mpd-output)))
    "The audio outputs that MPD can use.
 By default this is a single output using pulseaudio.")
+
+  (playlist-plugins
+   (list-of-mpd-plugin '())
+   "List of MPD playlist plugin configurations."
+   (lambda (_ x)
+     (mpd-serialize-list-of-mpd-plugin "playlist_plugin" x)))
+
+  (extra-options
+   (alist '())
+   "An association list of option symbols/strings to string values to be
+appended to the configuration.")
+
   (prefix mpd-))
 
-(define (mpd-file-name config file)
-  "Return a path in /var/run/mpd/ that is writable
-   by @code{user} from @code{config}."
-  (string-append "/var/run/mpd/"
-                 (mpd-configuration-user config)
-                 "/" file))
+(define (mpd-serialize-configuration configuration)
+  (mixed-text-file
+   "mpd.conf"
+   (serialize-configuration configuration mpd-configuration-fields)))
+
+(define (mpd-log-rotation config)
+  (match-record config <mpd-configuration> (log-file)
+    (log-rotation
+     (files (list log-file))
+     (post-rotate #~(begin
+                      (use-modules (gnu services herd))
+                      (with-shepherd-action 'mpd ('reopen) #f))))))
 
 (define (mpd-shepherd-service config)
-  (shepherd-service
-   (documentation "Run the MPD (Music Player Daemon)")
-   (requirement '(user-processes))
-   (provision '(mpd))
-   (start #~(make-forkexec-constructor
-             (list #$(file-append mpd "/bin/mpd")
-                   "--no-daemon"
-                   #$(mpd-serialize-configuration config))
-             #:environment-variables
-             ;; Required to detect PulseAudio when run under a user account.
-             (list (string-append
-                    "XDG_RUNTIME_DIR=/run/user/"
-                    (number->string
-                     (passwd:uid
-                      (getpwnam #$(mpd-configuration-user config))))))
-             #:log-file #$(mpd-file-name config "log")))
-   (stop  #~(make-kill-destructor))))
+  (match-record config <mpd-configuration> (user package shepherd-requirement)
+    (let* ((config-file (mpd-serialize-configuration config)))
+      (shepherd-service
+       (documentation "Run the MPD (Music Player Daemon)")
+       (requirement `(user-processes loopback ,@shepherd-requirement))
+       (provision '(mpd))
+       (start #~(make-forkexec-constructor
+                 (list #$(file-append package "/bin/mpd")
+                       "--no-daemon"
+                       #$config-file)
+                 #:environment-variables
+                 ;; Required to detect PulseAudio when run under a user account.
+                 (list (string-append
+                        "XDG_RUNTIME_DIR=/run/user/"
+                        (number->string (passwd:uid (getpwnam #$user)))))))
+       (stop  #~(make-kill-destructor))
+       (actions
+        (list (shepherd-configuration-action config-file)
+              (shepherd-action
+               (name 'reopen)
+               (documentation "Re-open log files and flush caches.")
+               (procedure
+                #~(lambda (pid)
+                    (if pid
+                        (begin
+                          (kill pid SIGHUP)
+                          (format #t
+                                  "Issued SIGHUP to Service MPD (PID ~a)."
+                                  pid))
+                        (format #t "Service MPD is not running.")))))))))))
 
 (define (mpd-service-activation config)
-  (with-imported-modules '((guix build utils))
+  (match-record config <mpd-configuration> (user log-file)
     #~(begin
         (use-modules (guix build utils))
-        (define %user
-          (getpw #$(mpd-configuration-user config)))
-
-        (let ((directory #$(mpd-file-name config ".mpd")))
-          (mkdir-p directory)
-          (chown directory (passwd:uid %user) (passwd:gid %user))
-
-          ;; Make /var/run/mpd/USER user-owned as well.
-          (chown (dirname directory)
-                 (passwd:uid %user) (passwd:gid %user))))))
-
-
-(define %mpd-accounts
-  ;; Default account and group for MPD.
-  (list (user-group (name "mpd") (system? #t))
-        (user-account
-         (name "mpd")
-         (group "mpd")
-         (system? #t)
-         (comment "Music Player Daemon (MPD) user")
 
-         ;; Note: /var/run/mpd hosts one sub-directory per user, of which
-         ;; /var/run/mpd/mpd corresponds to the "mpd" user.
-         (home-directory "/var/run/mpd/mpd")
+        ;; TODO: remove me, migrates from the old location at /var/run/mpd
+        ;;       to the new one at /var/lib/mpd.
+        (let* ((user (getpw #$user))
+               (deprecated-directory (string-append "/var/run/mpd/"
+                                                    #$user "/.mpd"))
+               (new-directory (string-append (passwd:dir user)
+                                             "/.config/mpd")))
+          (when (and (file-exists? deprecated-directory)
+                     (not (file-exists? new-directory)))
+            (rename-file deprecated-directory new-directory)
+            (chown new-directory (passwd:uid user) (passwd:gid user))))
+        (mkdir-p (dirname #$log-file)))))
 
-         (shell (file-append shadow "/sbin/nologin")))))
+(define (mpd-accounts config)
+  (match-record config <mpd-configuration> (user)
+    (list (user-account
+           (name user)
+           (group "nogroup")
+           (system? #t)
+           (comment "Music Player Daemon (MPD) user")
+           ;; MPD can use $HOME (or $XDG_CONFIG_HOME) to place its data
+           (home-directory "/var/lib/mpd")
+           (shell (file-append shadow "/sbin/nologin"))))))
 
 (define mpd-service-type
   (service-type
@@ -235,7 +484,9 @@ (define mpd-service-type
     (list (service-extension shepherd-root-service-type
                              (compose list mpd-shepherd-service))
           (service-extension account-service-type
-                             (const %mpd-accounts))
+                             mpd-accounts)
           (service-extension activation-service-type
-                             mpd-service-activation)))
+                             mpd-service-activation)
+          (service-extension rottlog-service-type
+                             (compose list mpd-log-rotation))))
    (default-value (mpd-configuration))))

base-commit: aae8371f72805cc35e31817e4120468eee4a4a80
prerequisite-patch-id: e47455c06e8e73edcc3f36ccd7b6b289cdfa1e16
-- 
2.38.1





^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [bug#59866] [PATCH v5.1] services: mpd: Refactor MPD service.
  2022-12-24 14:11 ` [bug#59866] [PATCH v5.1] " mirai
@ 2022-12-24 17:20   ` Liliana Marie Prikler
  2022-12-24 19:17     ` mirai
  0 siblings, 1 reply; 29+ messages in thread
From: Liliana Marie Prikler @ 2022-12-24 17:20 UTC (permalink / raw)
  To: mirai, 59866

Am Samstag, dem 24.12.2022 um 14:11 +0000 schrieb mirai@makinata.eu:
> From: Bruno Victal <mirai@makinata.eu>
> 
> Introduces 'mpd-plugin' and 'mpd-partition' records.
> Expands 'mpd-output' and 'mpd-configuration' records.
> Deprecates redundant abbreviated fields in 'mpd-configuration' and
> avoids
> serializing unused fields that may introduce undesired behavior.
> Replace free-form-args serialization by making 'mpd-serialize-field'
> handle
> multiple types and using it with 'generic-serialize-alist'.
> Reduce code weight by removing cosmetic indented serialization
> procedures.
> Offload logging from shepherd to MPD.
> Implement log-rotation via rottlog.
> Implement Shepherd actions: 'reopen' and 'configuration'.
The things you wrote here read a like a less structured ChangeLog.  The
section between header and ChangeLog should instead be used to give
some wider context if needed, imo.

> * gnu/services/audio.scm
> (mpd-plugin, mpd-partition): New record.
> 
> (mpd-plugin?, mpd-partition?, list-of-string?, list-of-symbol?)
> (list-of-mpd-plugin?, list-of-mpd-partition?)
> (list-of-mpd-plugin-or-output?, port?): New predicate.
> 
> (mpd-serialize-field): Handle multiple types.
> 
> (mpd-configuration)
> [package, group, shepherd-requirement, log-file, log-level, music-
> directory]
> [playlist-directory, endpoints, database, partitions, neighbors,
> inputs]
> [archive-plugins, input-cache-size, decoders, resampler, filters]
> [playlist-plugins, extra-options]: New field.
> [music-dir, playlist-dir, address]: Deprecate shorthand field.
> [db-file, state-file, sticker-file, port, outputs]: Change admissible
> type.
> 
> (mpd-log-rotation): New procedure.
> 
> (mpd-shepherd-service)
> [actions]: New shepherd actions: 'reopen' and 'configuration'.
> [requirement]: Splice with 'shepherd-requirement' field.
> [start]: Use 'package' field. Remove #:log-file parameter.
> 
> (mpd-service-activation): Create logging directory. Handle migration
> from old-style configuration.
> 
> (mpd-accounts): Do not hardcode username, change primary group to
> "nogroup".
While I welcome the extensibility here, I think the default should
still be mpd:mpd.  I think you should make it so that you can pass a
user-account and user-group to the mpd service so that they can be
reused (with a sanitizer that creates a user/group from string).

> (mpd-service-type): Extend rottlog-service-type for log-rotation.
> Update activation-service-type extension to reflect mpd-accounts
> procedure change.
> 
> * doc/guix.texi (Audio Services)[Music Player Daemon]: Update doc.
> ---
> 
>  Changes since v5:
>  * Serialize default_port field-name as "port".
> 
>  doc/guix.texi          | 177 +++++++++++++---
>  gnu/services/audio.scm | 463 +++++++++++++++++++++++++++++++--------
> --
>  2 files changed, 506 insertions(+), 134 deletions(-)
> 
> diff --git a/doc/guix.texi b/doc/guix.texi
> index e25692fd27..5663d1913a 100644
> --- a/doc/guix.texi
> +++ b/doc/guix.texi
> @@ -109,6 +109,7 @@ Copyright @copyright{} 2022 Reily Siegel@*
>  Copyright @copyright{} 2022 Simon Streit@*
>  Copyright @copyright{} 2022 (@*
>  Copyright @copyright{} 2022 John Kehayias@*
> +Copyright @copyright{} 2022 Bruno Victal@*
>  
>  Permission is granted to copy, distribute and/or modify this
> document
>  under the terms of the GNU Free Documentation License, Version 1.3
> or
> @@ -33014,79 +33015,187 @@ The service type for @command{mpd}
>  Data type representing the configuration of @command{mpd}.
>  
>  @table @asis
> -@item @code{user} (default: @code{"mpd"})
> +@item @code{package} (default: @code{mpd}) (type: file-like)
> +The MPD package.
> +
> +@item @code{user} (default: @code{"mpd"}) (type: string)
>  The user to run mpd as.
>  
> -@item @code{music-dir} (default: @code{"~/Music"})
> +@item @code{group} (type: maybe-string)
> +The group to run mpd as.
> +
> +@item @code{shepherd-requirement} (default: @code{()}) (type: list-
> of-symbol)
> +This is a list of symbols naming Shepherd services that this service
> +will depend on.
> +
> +@item @code{log-file} (default: @code{"/var/log/mpd/log"}) (type:
> maybe-string)
> +The location of the log file.  Set to @code{syslog} to use the local
> +syslog daemon or @code{%unset-value} to omit this directive from the
> +configuration file.
> +
> +@item @code{log-level} (type: maybe-string)
> +Supress any messages below this threshold.  Available values:
> +@code{notice}, @code{info}, @code{verbose}, @code{warning} and
> +@code{error}.
> +
> +@item @code{music-directory} (type: maybe-string)
>  The directory to scan for music files.
>  
> -@item @code{playlist-dir} (default: @code{"~/.mpd/playlists"})
> +@item @code{playlist-directory} (type: maybe-string)
>  The directory to store playlists.
>  
> -@item @code{db-file} (default: @code{"~/.mpd/tag_cache"})
> +@item @code{db-file} (type: maybe-string)
>  The location of the music database.
>  
> -@item @code{state-file} (default: @code{"~/.mpd/state"})
> +@item @code{state-file} (type: maybe-string)
>  The location of the file that stores current MPD's state.
>  
> -@item @code{sticker-file} (default: @code{"~/.mpd/sticker.sql"})
> +@item @code{sticker-file} (type: maybe-string)
>  The location of the sticker database.
>  
> -@item @code{port} (default: @code{"6600"})
> -The port to run mpd on.
> +@item @code{default-port} (default: @code{6600}) (type: maybe-
> integer)
> +The default port to run mpd on.
> +
> +@item @code{endpoints} (type: maybe-list-of-string)
> +The addresses that mpd will bind to.  A different port may be
> +specified, e.g. @code{localhost:6602}.  IPv6 addresses must be
> +enclosed in square brackets if a different port is used.
> +To use a Unix domain socket, an absolute path or a path starting
> with @code{~}
> +can be specified here.
LGTM, but "A different port" should probably be "A port different from
@var{default-port}.

> +@item @code{database} (type: maybe-mpd-plugin)
> +MPD database plugin configuration.
> +
> +@item @code{partitions} (default: @code{()}) (type: list-of-mpd-
> partition)
> +List of MPD "partitions".
>  
> -@item @code{address} (default: @code{"any"})
> -The address that mpd will bind to.  To use a Unix domain socket,
> -an absolute path can be specified here.
> +@item @code{neighbors} (default: @code{()}) (type: list-of-mpd-
> plugin)
> +List of MPD neighbor plugin configurations.
>  
> -@item @code{outputs} (default: @code{"(list (mpd-output))"})
> -The audio outputs that MPD can use.  By default this is a single
> output using pulseaudio.
> +@item @code{inputs} (default: @code{()}) (type: list-of-mpd-plugin)
> +List of MPD input plugin configurations.
> +
> +@item @code{archive-plugins} (default: @code{()}) (type: list-of-
> mpd-plugin)
> +List of MPD archive plugin configurations.
> +
> +@item @code{input-cache-size} (type: maybe-string)
> +MPD input cache size.
> +
> +@item @code{decoders} (default: @code{()}) (type: list-of-mpd-
> plugin)
> +List of MPD decoder plugin configurations.
> +
> +@item @code{resampler} (type: maybe-mpd-plugin)
> +MPD resampler plugin configuration.
> +
> +@item @code{filters} (default: @code{()}) (type: list-of-mpd-plugin)
> +List of MPD filter plugin configurations.
> +
> +@item @code{outputs} (type: list-of-mpd-plugin-or-output)
> +The audio outputs that MPD can use.  By default this is a single
> output
> +using pulseaudio.
> +
> +@item @code{playlist-plugins} (default: @code{()}) (type: list-of-
> mpd-plugin)
> +List of MPD playlist plugin configurations.
> +
> +@item @code{extra-options} (default: @code{()}) (type: alist)
> +An association list of option symbols/strings to string values to be
> +appended to the configuration.
> +
> +@end table
> +@end deftp
> +
> +@deftp {Data Type} mpd-plugin
> +Data type representing a @command{mpd} plugin.
> +
> +@table @asis
> +@item @code{plugin} (type: maybe-string)
> +Plugin name.
> +
> +@item @code{name} (type: maybe-string)
> +Name.
> +
> +@item @code{enabled?} (type: maybe-boolean)
> +Whether the plugin is enabled/disabled.
> +
> +@item @code{extra-options} (default: @code{()}) (type: alist)
> +An association list of option symbols/strings to string values to be
> +appended to the plugin configuration.  See
> +@uref{https://mpd.readthedocs.io/en/latest/plugins.html,MPD plugin
> +reference} for available options.
> +
> +@end table
> +@end deftp
> +
> +@deftp {Data Type} mpd-partition
> +Data type representing a @command{mpd} partition.
> +
> +@table @asis
> +@item @code{name} (type: string)
> +Partition name.
> +
> +@item @code{extra-options} (default: @code{()}) (type: alist)
> +An association list of option symbols/strings to string values to be
> +appended to the partition configuration.  See
> +@uref{
> https://mpd.readthedocs.io/en/latest/user.html#configuring-partitions
> ,Configuring
> +Partitions} for available options.
>  
>  @end table
>  @end deftp
>  
>  @deftp {Data Type} mpd-output
> -Data type representing an @command{mpd} audio output.
> +Data type representing a @command{mpd} audio output.
>  
>  @table @asis
> -@item @code{name} (default: @code{"MPD"})
> +@item @code{name} (default: @code{"MPD"}) (type: string)
>  The name of the audio output.
>  
> -@item @code{type} (default: @code{"pulse"})
> +@item @code{type} (default: @code{"pulse"}) (type: string)
>  The type of audio output.
>  
> -@item @code{enabled?} (default: @code{#t})
> +@item @code{enabled?} (default: @code{#t}) (type: boolean)
>  Specifies whether this audio output is enabled when MPD is started. 
> By
>  default, all audio outputs are enabled.  This is just the default
>  setting when there is no state file; with a state file, the previous
>  state is restored.
>  
> -@item @code{tags?} (default: @code{#t})
> +@item @code{format} (type: maybe-string)
> +Force a specific audio format on output.  See
> +@uref{
> https://mpd.readthedocs.io/en/latest/user.html#audio-output-format,Gl
> obal
> +Audio Format} for a more detailed description.
> +
> +@item @code{tags?} (default: @code{#t}) (type: boolean)
>  If set to @code{#f}, then MPD will not send tags to this output. 
> This
>  is only useful for output plugins that can receive tags, for example
> the
>  @code{httpd} output plugin.
>  
> -@item @code{always-on?} (default: @code{#f})
> +@item @code{always-on?} (default: @code{#f}) (type: boolean)
>  If set to @code{#t}, then MPD attempts to keep this audio output
> always
> -open.  This may be useful for streaming servers, when you don’t want
> to
> +open.  This may be useful for streaming servers, when you don?t want
> to
>  disconnect all listeners even when playback is accidentally stopped.
>  
> -@item @code{mixer-type}
> -This field accepts a symbol that specifies which mixer should be
> used
> +@item @code{mixer-type} (default: @code{"none"}) (type: string)
> +This field accepts a string that specifies which mixer should be
> used
>  for this audio output: the @code{hardware} mixer, the
> @code{software}
>  mixer, the @code{null} mixer (allows setting the volume, but with no
>  effect; this can be used as a trick to implement an external mixer
>  External Mixer) or no mixer (@code{none}).
>  
> -@item @code{extra-options} (default: @code{'()})
> -An association list of option symbols to string values to be
> appended to
> -the audio output configuration.
> +@item @code{replay-gain-handler} (type: maybe-string)
> +This field accepts a string that specifies how
> +@uref{
> https://mpd.readthedocs.io/en/latest/user.html#replay-gain,Replay
> +Gain} is to be applied.  @code{software} uses an internal software
> +volume control, @code{mixer} uses the configured (hardware) mixer
> +control and @code{none} disables replay gain on this audio output.
> +
> +@item @code{extra-options} (default: @code{()}) (type: alist)
> +An association list of option symbols/strings to string values to be
> +appended to the audio output configuration.
>  
>  @end table
>  @end deftp
>  
> -The following example shows a configuration of @code{mpd} that
> provides
> -an HTTP audio streaming output.
> +The following example shows a configuration of @command{mpd} that
> +configures some of its plugins and provides a HTTP audio streaming
> output.
>  
>  @lisp
>  (service mpd-service-type
> @@ -33098,7 +33207,19 @@ an HTTP audio streaming output.
>                       (mixer-type 'null)
>                       (extra-options
>                        `((encoder . "vorbis")
> -                        (port    . "8080"))))))))
> +                        (port    . "8080"))))))
> +           (decoders
> +             (list (mpd-plugin
> +                     (plugin "mikmod")
> +                     (enabled? #f))
> +                   (mpd-plugin
> +                     (plugin "openmpt")
> +                     (enabled? #t)
> +                     (extra-options `((repeat-count . -1)
> +                                      (interpolation-filter .
> 1))))))
> +           (resampler (mpd-plugin
> +                        (plugin "libsamplerate")
> +                        (extra-options `((type . 0)))))))
>  @end lisp
>  
>  
> diff --git a/gnu/services/audio.scm b/gnu/services/audio.scm
> index 1a1026f342..e456205e99 100644
> --- a/gnu/services/audio.scm
> +++ b/gnu/services/audio.scm
> @@ -21,19 +21,27 @@
>  
>  (define-module (gnu services audio)
>    #:use-module (guix gexp)
> +  #:use-module (guix deprecation)
> +  #:use-module (guix diagnostics)
> +  #:use-module (guix i18n)
>    #:use-module (gnu services)
>    #:use-module (gnu services configuration)
>    #:use-module (gnu services shepherd)
> +  #:use-module (gnu services admin)
>    #:use-module (gnu system shadow)
>    #:use-module (gnu packages admin)
>    #:use-module (gnu packages mpd)
>    #:use-module (guix records)
>    #:use-module (ice-9 match)
> -  #:use-module (ice-9 format)
>    #:use-module (srfi srfi-1)
> +  #:use-module (srfi srfi-8)
>    #:use-module (srfi srfi-26)
>    #:export (mpd-output
>              mpd-output?
> +            mpd-plugin
> +            mpd-plugin?
> +            mpd-partition
> +            mpd-partition?
>              mpd-configuration
>              mpd-configuration?
>              mpd-service-type))
> @@ -52,180 +60,421 @@ (define (uglify-field-name field-name)
>                                 #\-)
>                   "_")))
>  
> -(define (free-form-args? val)
> -  (match val
> -    (() #t)
> -    ((((? symbol?) . (? string?)) . val) (free-form-args? val))
> -    (_ #f)))
> +(define list-of-string?
> +  (list-of string?))
>  
> -(define* (mpd-serialize-field field-name value #:optional (indent-
> level 0))
> -  #~(begin
> -      (use-modules ((ice-9 format)))
> -      (format #f "~v/~a \"~a\"~%" #$indent-level #$(if (string?
> field-name)
> -                                                       field-name
> -                                                       (uglify-
> field-name field-name)) #$value)))
> +(define list-of-symbol?
> +  (list-of symbol?))
>  
> -(define* (mpd-serialize-free-form-args field-name value #:optional
> (indent-level 0))
> -  (generic-serialize-alist string-append (cut mpd-serialize-field <>
> <> indent-level) value))
> +(define (mpd-serialize-field field-name value)
> +  (let ((field (if (string? field-name) field-name
> +                   (uglify-field-name field-name)))
> +        (value (if (boolean? value) (if value "yes" "no") value)))
> +    #~(format #f "~a \"~a\"~%" #$field #$value)))
>  
>  (define mpd-serialize-number mpd-serialize-field)
>  
>  (define mpd-serialize-string mpd-serialize-field)
>  
> -(define* (mpd-serialize-boolean field-name value #:optional (indent-
> level 0))
> -  (mpd-serialize-field field-name (if value "yes" "no") indent-
> level))
> +(define mpd-serialize-boolean mpd-serialize-field)
>  
> -(define (mpd-serialize-list-of-mpd-output field-name value)
> -  #~(string-append "\naudio_output {\n"
> -                   #$@(map (cut serialize-configuration <>
> -                                mpd-output-fields)
> -                           value)
> -                   "}\n"))
> +(define (mpd-serialize-alist field-name value)
> +  #~(string-append #$@(generic-serialize-alist list
> +                                               mpd-serialize-field
> +                                               value)))
>  
> -(define (mpd-serialize-configuration configuration)
> -  (mixed-text-file
> -   "mpd.conf"
> -   (serialize-configuration configuration mpd-configuration-
> fields)))
> +(define-maybe string (prefix mpd-))
> +(define-maybe list-of-string (prefix mpd-))
> +(define-maybe boolean (prefix mpd-))
> +
> +;;; TODO: Procedures for deprecated fields, to be removed.
> +
> +(define mpd-deprecated-fields '((music-dir . music-directory)
> +                                (playlist-dir . playlist-directory)
> +                                (address . endpoints)))
> +
> +(define (port? value) (or (string? value) (integer? value)))
> +
> +(define (mpd-serialize-deprecated-field field-name value)
> +  (if (maybe-value-set? value)
> +      (begin
> +        (warn-about-deprecation
> +         field-name #f
> +         #:replacement (assoc-ref mpd-deprecated-fields field-name))
> +        (match field-name
> +          ('playlist-dir (mpd-serialize-string "playlist_directory"
> value))
> +          ('music-dir (mpd-serialize-string "music_directory"
> value))
> +          ('address (mpd-serialize-string "bind_to_address"
> value))))
> +      ""))
> +
> +(define (mpd-serialize-port field-name value)
> +  (when (string? value)
> +    (warning
> +     (G_ "string value for '~a' is deprecated, use integer
> instead~%")
> +     field-name))
> +  (mpd-serialize-field "port" value))
> +
> +(define-maybe port (prefix mpd-))
> +
> +;;;
> +
> +;; Generic MPD plugin record, lists only the most prevalent fields.
> +(define-configuration mpd-plugin
> +  (plugin
> +   maybe-string
> +   "Plugin name.")
> +
> +  (name
> +   maybe-string
> +   "Name.")
> +
> +  (enabled?
> +   maybe-boolean
> +   "Whether the plugin is enabled/disabled.")
> +
> +  (extra-options
> +   (alist '())
> +   "An association list of option symbols/strings to string values
> +to be appended to the plugin configuration. See
> +@uref{https://mpd.readthedocs.io/en/latest/plugins.html,MPD plugin
> reference}
> +for available options.")
> +
> +  (prefix mpd-))
> +
> +(define (mpd-serialize-mpd-plugin field-name value)
> +  #~(format #f "~a {~%~a}~%"
> +            '#$field-name
> +            #$(serialize-configuration value mpd-plugin-fields)))
> +
> +(define (mpd-serialize-list-of-mpd-plugin field-name value)
> +  #~(string-append #$@(map (cut mpd-serialize-mpd-plugin field-name
> <>)
> +                           value)))
>  
> -(define mpd-subsystem-serialize-field (cut mpd-serialize-field <> <>
> 1))
> -(define mpd-subsystem-serialize-string (cut mpd-serialize-string <>
> <> 1))
> -(define mpd-subsystem-serialize-number (cut mpd-serialize-number <>
> <> 1))
> -(define mpd-subsystem-serialize-boolean (cut mpd-serialize-boolean
> <> <> 1))
> -(define mpd-subsystem-serialize-free-form-args (cut mpd-serialize-
> free-form-args <> <> 1))
> +(define list-of-mpd-plugin? (list-of mpd-plugin?))
> +
> +(define-maybe mpd-plugin (prefix mpd-))
> +
> +(define-configuration mpd-partition
> +  (name
> +   string
> +   "Partition name.")
> +
> +  (extra-options
> +   (alist '())
> +   "An association list of option symbols/strings to string values
> +to be appended to the partition configuration. See
> +@uref{
> https://mpd.readthedocs.io/en/latest/user.html#configuring-partitions
> ,Configuring Partitions}
> +for available options.")
> +
> +  (prefix mpd-))
> +
> +(define (mpd-serialize-mpd-partition field-name value)
> +  #~(format #f "partition {~%~a}~%"
> +            #$(serialize-configuration value mpd-partition-fields)))
> +
> +(define (mpd-serialize-list-of-mpd-partition field-name value)
> +  #~(string-append #$@(map (cut mpd-serialize-mpd-partition #f <>)
> value)))
> +
> +(define list-of-mpd-partition?
> +  (list-of mpd-partition?))
>  
>  (define-configuration mpd-output
>    (name
>     (string "MPD")
>     "The name of the audio output.")
> +
>    (type
>     (string "pulse")
>     "The type of audio output.")
> +
>    (enabled?
>     (boolean #t)
>     "Specifies whether this audio output is enabled when MPD is
> started. By
>  default, all audio outputs are enabled. This is just the default
>  setting when there is no state file; with a state file, the previous
>  state is restored.")
> +
> +  (format
> +   maybe-string
> +   "Force a specific audio format on output. See
> +@uref{
> https://mpd.readthedocs.io/en/latest/user.html#audio-output-format,Gl
> obal Audio Format}
> +for a more detailed description.")
> +   
>    (tags?
>     (boolean #t)
>     "If set to @code{#f}, then MPD will not send tags to this output.
> This
>  is only useful for output plugins that can receive tags, for example
> the
>  @code{httpd} output plugin.")
> +
>    (always-on?
>     (boolean #f)
>     "If set to @code{#t}, then MPD attempts to keep this audio output
> always
>  open. This may be useful for streaming servers, when you don’t want
> to
>  disconnect all listeners even when playback is accidentally
> stopped.")
> +
>    (mixer-type
>     (string "none")
> -   "This field accepts a symbol that specifies which mixer should be
> used
> +   "This field accepts a string that specifies which mixer should be
> used
>  for this audio output: the @code{hardware} mixer, the
> @code{software}
>  mixer, the @code{null} mixer (allows setting the volume, but with no
>  effect; this can be used as a trick to implement an external mixer
>  External Mixer) or no mixer (@code{none}).")
> +
> +  (replay-gain-handler
> +   maybe-string
> +   "This field accepts a string that specifies how
> +@uref{
> https://mpd.readthedocs.io/en/latest/user.html#replay-gain,Replay Gai
> n}
> +is to be applied. @code{software} uses an internal software volume
> control,
> +@code{mixer} uses the configured (hardware) mixer control and
> @code{none}
> +disables replay gain on this audio output.")
> +
>    (extra-options
> -   (free-form-args '())
> -   "An association list of option symbols to string values to be
> appended to
> -the audio output configuration.")
> -  (prefix mpd-subsystem-))
> +   (alist '())
> +   "An association list of option symbols/strings to string values
> +to be appended to the audio output configuration.")
>  
> -(define list-of-mpd-output?
> -  (list-of mpd-output?))
> +  (prefix mpd-))
> +
> +(define (mpd-serialize-mpd-output field-name value)
> +  #~(format #f "audio_output {~%~a}~%"
> +            #$(serialize-configuration value mpd-output-fields)))
> +
> +(define (mpd-serialize-list-of-mpd-plugin-or-output field-name
> value)
> +  (receive (plugins outputs)
> +      (partition mpd-plugin? value)
> +    #~(string-append #$@(map (cut mpd-serialize-mpd-plugin
> "audio_output" <>)
> +                             plugins)
> +                     #$@(map (cut mpd-serialize-mpd-output #f <>)
> outputs))))
> +
> +(define list-of-mpd-plugin-or-output?
> +  (list-of (lambda (x)
> +             (or (mpd-output? x) (mpd-plugin? x)))))
>  
>  (define-configuration mpd-configuration
> +  (package
> +   (file-like mpd)
> +   "The MPD package."
> +   empty-serializer)
> +
>    (user
>     (string "mpd")
>     "The user to run mpd as.")
> -  (music-dir
> -   (string "~/Music")
> +
> +  (group
> +   maybe-string
> +   "The group to run mpd as.")
> +
> +  (shepherd-requirement
> +   (list-of-symbol '())
> +   "This is a list of symbols naming Shepherd services that this
> service
> +will depend on."
> +   empty-serializer)
> +
> +  (log-file
> +   (maybe-string "/var/log/mpd/log")
> +   "The location of the log file. Set to @code{syslog} to use the
> +local syslog daemon or @code{%unset-value} to omit this directive
> +from the configuration file.")
> +
> +  (log-level
> +   maybe-string
> +   "Supress any messages below this threshold.
> +Available values: @code{notice}, @code{info}, @code{verbose},
> +@code{warning} and @code{error}.")
> +
> +  (music-directory
> +   maybe-string
> +   "The directory to scan for music files.")
> +
> +  (music-dir ; TODO: deprecated, remove later
> +   maybe-string
>     "The directory to scan for music files."
> -   (lambda (_ x)
> -     (mpd-serialize-field "music_directory" x)))
> -  (playlist-dir
> -   (string "~/.mpd/playlists")
> +   mpd-serialize-deprecated-field)
> +
> +  (playlist-directory
> +   maybe-string
> +   "The directory to store playlists.")
> +
> +  (playlist-dir ; TODO: deprecated, remove later
> +   maybe-string
>     "The directory to store playlists."
> -   (lambda (_ x)
> -     (mpd-serialize-field "playlist_directory" x)))
> +   mpd-serialize-deprecated-field)
> +
>    (db-file
> -   (string "~/.mpd/tag_cache")
> +   maybe-string
>     "The location of the music database.")
> +
>    (state-file
> -   (string "~/.mpd/state")
> +   maybe-string
>     "The location of the file that stores current MPD's state.")
> +
>    (sticker-file
> -   (string "~/.mpd/sticker.sql")
> +   maybe-string
>     "The location of the sticker database.")
> -  (port
> -   (string "6600")
> -   "The port to run mpd on.")
> -  (address
> -   (string "any")
> +
> +  (default-port
> +   (maybe-port 6600) ; TODO: switch to integer
> +   "The default port to run mpd on.")
> +
> +  (endpoints
> +   maybe-list-of-string
> +   "The addresses that mpd will bind to. A different port may be
> +specified, e.g. @code{localhost:6602}. IPv6 addresses must be
> +enclosed in square brackets if a different port is used.
> +To use a Unix domain socket, an absolute path or a path starting
> with @code{~}
> +can be specified here."
> +   (lambda (_ x)
> +     (if (maybe-value-set? x)
> +         #~(string-append #$@(map
> +                              (cut mpd-serialize-field
> "bind_to_address" <>)
> +                              x)) "")))
> +
> +  (address ; TODO: deprecated, remove later
> +   maybe-string
>     "The address that mpd will bind to.
>  To use a Unix domain socket, an absolute path can be specified
> here."
> +   mpd-serialize-deprecated-field)
> +
> +  (database
> +   maybe-mpd-plugin
> +   "MPD database plugin configuration.")
> +
> +  (partitions
> +   (list-of-mpd-partition '())
> +   "List of MPD \"partitions\".")
> +  
> +  (neighbors
> +   (list-of-mpd-plugin '())
> +   "List of MPD neighbor plugin configurations.")
> +
> +  (inputs
> +   (list-of-mpd-plugin '())
> +   "List of MPD input plugin configurations."
> +   (lambda (_ x)
> +     (mpd-serialize-list-of-mpd-plugin "input" x)))
> +
> +  (archive-plugins
> +   (list-of-mpd-plugin '())
> +   "List of MPD archive plugin configurations."
>     (lambda (_ x)
> -     (mpd-serialize-field "bind_to_address" x)))
> +     (mpd-serialize-list-of-mpd-plugin "archive_plugin" x)))
> +
> +  (input-cache-size
> +   maybe-string
> +   "MPD input cache size."
> +   (lambda (_ x)
> +     (if (maybe-value-set? x)
> +         #~(string-append "\ninput_cache {\n"
> +                          #$(mpd-serialize-string "size" x)
> +                          "}\n") "")))
> +
> +  (decoders
> +   (list-of-mpd-plugin '())
> +   "List of MPD decoder plugin configurations."
> +   (lambda (_ x)
> +     (mpd-serialize-list-of-mpd-plugin "decoder" x)))
> +
> +  (resampler
> +   maybe-mpd-plugin
> +   "MPD resampler plugin configuration.")
> +
> +  (filters
> +   (list-of-mpd-plugin '())
> +   "List of MPD filter plugin configurations."
> +   (lambda (_ x)
> +     (mpd-serialize-list-of-mpd-plugin "filter" x)))
> +
>    (outputs
> -   (list-of-mpd-output (list (mpd-output)))
> +   (list-of-mpd-plugin-or-output (list (mpd-output)))
>     "The audio outputs that MPD can use.
>  By default this is a single output using pulseaudio.")
> +
> +  (playlist-plugins
> +   (list-of-mpd-plugin '())
> +   "List of MPD playlist plugin configurations."
> +   (lambda (_ x)
> +     (mpd-serialize-list-of-mpd-plugin "playlist_plugin" x)))
> +
> +  (extra-options
> +   (alist '())
> +   "An association list of option symbols/strings to string values
> to be
> +appended to the configuration.")
> +
>    (prefix mpd-))
>  
> -(define (mpd-file-name config file)
> -  "Return a path in /var/run/mpd/ that is writable
> -   by @code{user} from @code{config}."
> -  (string-append "/var/run/mpd/"
> -                 (mpd-configuration-user config)
> -                 "/" file))
> +(define (mpd-serialize-configuration configuration)
> +  (mixed-text-file
> +   "mpd.conf"
> +   (serialize-configuration configuration mpd-configuration-
> fields)))
> +
> +(define (mpd-log-rotation config)
> +  (match-record config <mpd-configuration> (log-file)
> +    (log-rotation
> +     (files (list log-file))
> +     (post-rotate #~(begin
> +                      (use-modules (gnu services herd))
> +                      (with-shepherd-action 'mpd ('reopen) #f))))))
>  
>  (define (mpd-shepherd-service config)
> -  (shepherd-service
> -   (documentation "Run the MPD (Music Player Daemon)")
> -   (requirement '(user-processes))
> -   (provision '(mpd))
> -   (start #~(make-forkexec-constructor
> -             (list #$(file-append mpd "/bin/mpd")
> -                   "--no-daemon"
> -                   #$(mpd-serialize-configuration config))
> -             #:environment-variables
> -             ;; Required to detect PulseAudio when run under a user
> account.
> -             (list (string-append
> -                    "XDG_RUNTIME_DIR=/run/user/"
> -                    (number->string
> -                     (passwd:uid
> -                      (getpwnam #$(mpd-configuration-user
> config))))))
> -             #:log-file #$(mpd-file-name config "log")))
> -   (stop  #~(make-kill-destructor))))
> +  (match-record config <mpd-configuration> (user package shepherd-
> requirement)
> +    (let* ((config-file (mpd-serialize-configuration config)))
> +      (shepherd-service
> +       (documentation "Run the MPD (Music Player Daemon)")
> +       (requirement `(user-processes loopback ,@shepherd-
> requirement))
> +       (provision '(mpd))
> +       (start #~(make-forkexec-constructor
> +                 (list #$(file-append package "/bin/mpd")
> +                       "--no-daemon"
> +                       #$config-file)
> +                 #:environment-variables
> +                 ;; Required to detect PulseAudio when run under a
> user account.
> +                 (list (string-append
> +                        "XDG_RUNTIME_DIR=/run/user/"
> +                        (number->string (passwd:uid (getpwnam
> #$user)))))))
> +       (stop  #~(make-kill-destructor))
> +       (actions
> +        (list (shepherd-configuration-action config-file)
> +              (shepherd-action
> +               (name 'reopen)
> +               (documentation "Re-open log files and flush caches.")
> +               (procedure
> +                #~(lambda (pid)
> +                    (if pid
> +                        (begin
> +                          (kill pid SIGHUP)
> +                          (format #t
> +                                  "Issued SIGHUP to Service MPD (PID
> ~a)."
> +                                  pid))
> +                        (format #t "Service MPD is not
> running.")))))))))))
>  
>  (define (mpd-service-activation config)
> -  (with-imported-modules '((guix build utils))
> +  (match-record config <mpd-configuration> (user log-file)
>      #~(begin
>          (use-modules (guix build utils))
> -        (define %user
> -          (getpw #$(mpd-configuration-user config)))
> -
> -        (let ((directory #$(mpd-file-name config ".mpd")))
> -          (mkdir-p directory)
> -          (chown directory (passwd:uid %user) (passwd:gid %user))
> -
> -          ;; Make /var/run/mpd/USER user-owned as well.
> -          (chown (dirname directory)
> -                 (passwd:uid %user) (passwd:gid %user))))))
> -
> -
> -(define %mpd-accounts
> -  ;; Default account and group for MPD.
> -  (list (user-group (name "mpd") (system? #t))
> -        (user-account
> -         (name "mpd")
> -         (group "mpd")
> -         (system? #t)
> -         (comment "Music Player Daemon (MPD) user")
>  
> -         ;; Note: /var/run/mpd hosts one sub-directory per user, of
> which
> -         ;; /var/run/mpd/mpd corresponds to the "mpd" user.
> -         (home-directory "/var/run/mpd/mpd")
> +        ;; TODO: remove me, migrates from the old location at
> /var/run/mpd
> +        ;;       to the new one at /var/lib/mpd.
> +        (let* ((user (getpw #$user))
> +               (deprecated-directory (string-append "/var/run/mpd/"
> +                                                    #$user "/.mpd"))
> +               (new-directory (string-append (passwd:dir user)
> +                                             "/.config/mpd")))
> +          (when (and (file-exists? deprecated-directory)
> +                     (not (file-exists? new-directory)))
> +            (rename-file deprecated-directory new-directory)
> +            (chown new-directory (passwd:uid user) (passwd:gid
> user))))
> +        (mkdir-p (dirname #$log-file)))))
I'm not sure whether we should migrate the logs here.  I do think the
logs should be stored in /var/log rather than /var/run, but other than
that I'm not even sure there's much value in changing the /var/run/mpd
structure.  What's your use case for doing so?


Cheers




^ permalink raw reply	[flat|nested] 29+ messages in thread

* [bug#59866] [PATCH v5.2] services: mpd: Refactor MPD service.
  2022-12-06 23:22 [bug#59866] [PATCH 0/2] services: mpd: Refactor MPD service mirai
                   ` (8 preceding siblings ...)
  2022-12-24 14:11 ` [bug#59866] [PATCH v5.1] " mirai
@ 2022-12-24 18:53 ` mirai
  2023-01-03 14:43 ` [bug#59866] Pulseaudio woes mirai
  2023-02-02 20:07 ` [bug#59866] [PATCH v6 1/3] services: mpd: Rewrite using 'define-configuration' Bruno Victal
  11 siblings, 0 replies; 29+ messages in thread
From: mirai @ 2022-12-24 18:53 UTC (permalink / raw)
  To: 59866; +Cc: Bruno Victal

From: Bruno Victal <mirai@makinata.eu>

Refactor mpd-service-type to support additional mpd.conf
directives.

* gnu/services/audio.scm
(mpd-plugin, mpd-partition): New record.

(mpd-plugin?, mpd-partition?, list-of-string?, list-of-symbol?)
(list-of-mpd-plugin?, list-of-mpd-partition?)
(list-of-mpd-plugin-or-output?, port?): New predicate.

(mpd-serialize-field): Handle multiple types.

(mpd-configuration)
[package, group, shepherd-requirement, log-file, log-level, music-directory]
[playlist-directory, endpoints, database, partitions, neighbors, inputs]
[archive-plugins, input-cache-size, decoders, resampler, filters]
[playlist-plugins, extra-options]: New field.
[music-dir, playlist-dir, address]: Deprecate shorthand field.
[db-file, state-file, sticker-file, port, outputs]: Change admissible type.

(mpd-log-rotation): New procedure.

(mpd-shepherd-service)
[actions]: New shepherd actions: 'reopen' and 'configuration'.
[requirement]: Splice with 'shepherd-requirement' field.
[start]: Use 'package' field. Remove #:log-file parameter.

(mpd-service-activation): Create logging directory. Handle migration
from old-style configuration.

(mpd-accounts): Do not hardcode user and group names.

(mpd-service-type): Extend rottlog-service-type for log-rotation.
Update activation-service-type extension to reflect mpd-accounts
procedure change.

* doc/guix.texi (Audio Services)[Music Player Daemon]: Update doc.
---

 Changes since v5.1:
 * Rewrite commit message.
 * Create group and user, by default mpd:mpd.
 * Reword endpoints doc.

 doc/guix.texi          | 177 +++++++++++++---
 gnu/services/audio.scm | 467 +++++++++++++++++++++++++++++++----------
 2 files changed, 510 insertions(+), 134 deletions(-)

diff --git a/doc/guix.texi b/doc/guix.texi
index e25692fd27..870efdbe2d 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -109,6 +109,7 @@ Copyright @copyright{} 2022 Reily Siegel@*
 Copyright @copyright{} 2022 Simon Streit@*
 Copyright @copyright{} 2022 (@*
 Copyright @copyright{} 2022 John Kehayias@*
+Copyright @copyright{} 2022 Bruno Victal@*
 
 Permission is granted to copy, distribute and/or modify this document
 under the terms of the GNU Free Documentation License, Version 1.3 or
@@ -33014,79 +33015,187 @@ The service type for @command{mpd}
 Data type representing the configuration of @command{mpd}.
 
 @table @asis
-@item @code{user} (default: @code{"mpd"})
+@item @code{package} (default: @code{mpd}) (type: file-like)
+The MPD package.
+
+@item @code{user} (default: @code{"mpd"}) (type: string)
 The user to run mpd as.
 
-@item @code{music-dir} (default: @code{"~/Music"})
+@item @code{group} (default: @code{"mpd"}) (type: string)
+The group to run mpd as.
+
+@item @code{shepherd-requirement} (default: @code{()}) (type: list-of-symbol)
+This is a list of symbols naming Shepherd services that this service
+will depend on.
+
+@item @code{log-file} (default: @code{"/var/log/mpd/log"}) (type: maybe-string)
+The location of the log file.  Set to @code{syslog} to use the local
+syslog daemon or @code{%unset-value} to omit this directive from the
+configuration file.
+
+@item @code{log-level} (type: maybe-string)
+Supress any messages below this threshold.  Available values:
+@code{notice}, @code{info}, @code{verbose}, @code{warning} and
+@code{error}.
+
+@item @code{music-directory} (type: maybe-string)
 The directory to scan for music files.
 
-@item @code{playlist-dir} (default: @code{"~/.mpd/playlists"})
+@item @code{playlist-directory} (type: maybe-string)
 The directory to store playlists.
 
-@item @code{db-file} (default: @code{"~/.mpd/tag_cache"})
+@item @code{db-file} (type: maybe-string)
 The location of the music database.
 
-@item @code{state-file} (default: @code{"~/.mpd/state"})
+@item @code{state-file} (type: maybe-string)
 The location of the file that stores current MPD's state.
 
-@item @code{sticker-file} (default: @code{"~/.mpd/sticker.sql"})
+@item @code{sticker-file} (type: maybe-string)
 The location of the sticker database.
 
-@item @code{port} (default: @code{"6600"})
-The port to run mpd on.
+@item @code{default-port} (default: @code{6600}) (type: maybe-integer)
+The default port to run mpd on.
+
+@item @code{endpoints} (type: maybe-list-of-string)
+The addresses that mpd will bind to.  A port different from @var{default-port}
+may be specified, e.g. @code{localhost:6602} and IPv6 addresses must be
+enclosed in square brackets when a different port is used.
+To use a Unix domain socket, an absolute path or a path starting with @code{~}
+can be specified here.
+
+@item @code{database} (type: maybe-mpd-plugin)
+MPD database plugin configuration.
+
+@item @code{partitions} (default: @code{()}) (type: list-of-mpd-partition)
+List of MPD "partitions".
 
-@item @code{address} (default: @code{"any"})
-The address that mpd will bind to.  To use a Unix domain socket,
-an absolute path can be specified here.
+@item @code{neighbors} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD neighbor plugin configurations.
 
-@item @code{outputs} (default: @code{"(list (mpd-output))"})
-The audio outputs that MPD can use.  By default this is a single output using pulseaudio.
+@item @code{inputs} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD input plugin configurations.
+
+@item @code{archive-plugins} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD archive plugin configurations.
+
+@item @code{input-cache-size} (type: maybe-string)
+MPD input cache size.
+
+@item @code{decoders} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD decoder plugin configurations.
+
+@item @code{resampler} (type: maybe-mpd-plugin)
+MPD resampler plugin configuration.
+
+@item @code{filters} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD filter plugin configurations.
+
+@item @code{outputs} (type: list-of-mpd-plugin-or-output)
+The audio outputs that MPD can use.  By default this is a single output
+using pulseaudio.
+
+@item @code{playlist-plugins} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD playlist plugin configurations.
+
+@item @code{extra-options} (default: @code{()}) (type: alist)
+An association list of option symbols/strings to string values to be
+appended to the configuration.
+
+@end table
+@end deftp
+
+@deftp {Data Type} mpd-plugin
+Data type representing a @command{mpd} plugin.
+
+@table @asis
+@item @code{plugin} (type: maybe-string)
+Plugin name.
+
+@item @code{name} (type: maybe-string)
+Name.
+
+@item @code{enabled?} (type: maybe-boolean)
+Whether the plugin is enabled/disabled.
+
+@item @code{extra-options} (default: @code{()}) (type: alist)
+An association list of option symbols/strings to string values to be
+appended to the plugin configuration.  See
+@uref{https://mpd.readthedocs.io/en/latest/plugins.html,MPD plugin
+reference} for available options.
+
+@end table
+@end deftp
+
+@deftp {Data Type} mpd-partition
+Data type representing a @command{mpd} partition.
+
+@table @asis
+@item @code{name} (type: string)
+Partition name.
+
+@item @code{extra-options} (default: @code{()}) (type: alist)
+An association list of option symbols/strings to string values to be
+appended to the partition configuration.  See
+@uref{https://mpd.readthedocs.io/en/latest/user.html#configuring-partitions,Configuring
+Partitions} for available options.
 
 @end table
 @end deftp
 
 @deftp {Data Type} mpd-output
-Data type representing an @command{mpd} audio output.
+Data type representing a @command{mpd} audio output.
 
 @table @asis
-@item @code{name} (default: @code{"MPD"})
+@item @code{name} (default: @code{"MPD"}) (type: string)
 The name of the audio output.
 
-@item @code{type} (default: @code{"pulse"})
+@item @code{type} (default: @code{"pulse"}) (type: string)
 The type of audio output.
 
-@item @code{enabled?} (default: @code{#t})
+@item @code{enabled?} (default: @code{#t}) (type: boolean)
 Specifies whether this audio output is enabled when MPD is started.  By
 default, all audio outputs are enabled.  This is just the default
 setting when there is no state file; with a state file, the previous
 state is restored.
 
-@item @code{tags?} (default: @code{#t})
+@item @code{format} (type: maybe-string)
+Force a specific audio format on output.  See
+@uref{https://mpd.readthedocs.io/en/latest/user.html#audio-output-format,Global
+Audio Format} for a more detailed description.
+
+@item @code{tags?} (default: @code{#t}) (type: boolean)
 If set to @code{#f}, then MPD will not send tags to this output.  This
 is only useful for output plugins that can receive tags, for example the
 @code{httpd} output plugin.
 
-@item @code{always-on?} (default: @code{#f})
+@item @code{always-on?} (default: @code{#f}) (type: boolean)
 If set to @code{#t}, then MPD attempts to keep this audio output always
-open.  This may be useful for streaming servers, when you don’t want to
+open.  This may be useful for streaming servers, when you don?t want to
 disconnect all listeners even when playback is accidentally stopped.
 
-@item @code{mixer-type}
-This field accepts a symbol that specifies which mixer should be used
+@item @code{mixer-type} (default: @code{"none"}) (type: string)
+This field accepts a string that specifies which mixer should be used
 for this audio output: the @code{hardware} mixer, the @code{software}
 mixer, the @code{null} mixer (allows setting the volume, but with no
 effect; this can be used as a trick to implement an external mixer
 External Mixer) or no mixer (@code{none}).
 
-@item @code{extra-options} (default: @code{'()})
-An association list of option symbols to string values to be appended to
-the audio output configuration.
+@item @code{replay-gain-handler} (type: maybe-string)
+This field accepts a string that specifies how
+@uref{https://mpd.readthedocs.io/en/latest/user.html#replay-gain,Replay
+Gain} is to be applied.  @code{software} uses an internal software
+volume control, @code{mixer} uses the configured (hardware) mixer
+control and @code{none} disables replay gain on this audio output.
+
+@item @code{extra-options} (default: @code{()}) (type: alist)
+An association list of option symbols/strings to string values to be
+appended to the audio output configuration.
 
 @end table
 @end deftp
 
-The following example shows a configuration of @code{mpd} that provides
-an HTTP audio streaming output.
+The following example shows a configuration of @command{mpd} that
+configures some of its plugins and provides a HTTP audio streaming output.
 
 @lisp
 (service mpd-service-type
@@ -33098,7 +33207,19 @@ an HTTP audio streaming output.
                      (mixer-type 'null)
                      (extra-options
                       `((encoder . "vorbis")
-                        (port    . "8080"))))))))
+                        (port    . "8080"))))))
+           (decoders
+             (list (mpd-plugin
+                     (plugin "mikmod")
+                     (enabled? #f))
+                   (mpd-plugin
+                     (plugin "openmpt")
+                     (enabled? #t)
+                     (extra-options `((repeat-count . -1)
+                                      (interpolation-filter . 1))))))
+           (resampler (mpd-plugin
+                        (plugin "libsamplerate")
+                        (extra-options `((type . 0)))))))
 @end lisp
 
 
diff --git a/gnu/services/audio.scm b/gnu/services/audio.scm
index 1a1026f342..6caacf8c68 100644
--- a/gnu/services/audio.scm
+++ b/gnu/services/audio.scm
@@ -21,19 +21,27 @@
 
 (define-module (gnu services audio)
   #:use-module (guix gexp)
+  #:use-module (guix deprecation)
+  #:use-module (guix diagnostics)
+  #:use-module (guix i18n)
   #:use-module (gnu services)
   #:use-module (gnu services configuration)
   #:use-module (gnu services shepherd)
+  #:use-module (gnu services admin)
   #:use-module (gnu system shadow)
   #:use-module (gnu packages admin)
   #:use-module (gnu packages mpd)
   #:use-module (guix records)
   #:use-module (ice-9 match)
-  #:use-module (ice-9 format)
   #:use-module (srfi srfi-1)
+  #:use-module (srfi srfi-8)
   #:use-module (srfi srfi-26)
   #:export (mpd-output
             mpd-output?
+            mpd-plugin
+            mpd-plugin?
+            mpd-partition
+            mpd-partition?
             mpd-configuration
             mpd-configuration?
             mpd-service-type))
@@ -52,180 +60,425 @@ (define (uglify-field-name field-name)
                                #\-)
                  "_")))
 
-(define (free-form-args? val)
-  (match val
-    (() #t)
-    ((((? symbol?) . (? string?)) . val) (free-form-args? val))
-    (_ #f)))
+(define list-of-string?
+  (list-of string?))
 
-(define* (mpd-serialize-field field-name value #:optional (indent-level 0))
-  #~(begin
-      (use-modules ((ice-9 format)))
-      (format #f "~v/~a \"~a\"~%" #$indent-level #$(if (string? field-name)
-                                                       field-name
-                                                       (uglify-field-name field-name)) #$value)))
+(define list-of-symbol?
+  (list-of symbol?))
 
-(define* (mpd-serialize-free-form-args field-name value #:optional (indent-level 0))
-  (generic-serialize-alist string-append (cut mpd-serialize-field <> <> indent-level) value))
+(define (mpd-serialize-field field-name value)
+  (let ((field (if (string? field-name) field-name
+                   (uglify-field-name field-name)))
+        (value (if (boolean? value) (if value "yes" "no") value)))
+    #~(format #f "~a \"~a\"~%" #$field #$value)))
 
 (define mpd-serialize-number mpd-serialize-field)
 
 (define mpd-serialize-string mpd-serialize-field)
 
-(define* (mpd-serialize-boolean field-name value #:optional (indent-level 0))
-  (mpd-serialize-field field-name (if value "yes" "no") indent-level))
+(define mpd-serialize-boolean mpd-serialize-field)
 
-(define (mpd-serialize-list-of-mpd-output field-name value)
-  #~(string-append "\naudio_output {\n"
-                   #$@(map (cut serialize-configuration <>
-                                mpd-output-fields)
-                           value)
-                   "}\n"))
+(define (mpd-serialize-alist field-name value)
+  #~(string-append #$@(generic-serialize-alist list
+                                               mpd-serialize-field
+                                               value)))
 
-(define (mpd-serialize-configuration configuration)
-  (mixed-text-file
-   "mpd.conf"
-   (serialize-configuration configuration mpd-configuration-fields)))
+(define-maybe string (prefix mpd-))
+(define-maybe list-of-string (prefix mpd-))
+(define-maybe boolean (prefix mpd-))
+
+;;; TODO: Procedures for deprecated fields, to be removed.
+
+(define mpd-deprecated-fields '((music-dir . music-directory)
+                                (playlist-dir . playlist-directory)
+                                (address . endpoints)))
+
+(define (port? value) (or (string? value) (integer? value)))
+
+(define (mpd-serialize-deprecated-field field-name value)
+  (if (maybe-value-set? value)
+      (begin
+        (warn-about-deprecation
+         field-name #f
+         #:replacement (assoc-ref mpd-deprecated-fields field-name))
+        (match field-name
+          ('playlist-dir (mpd-serialize-string "playlist_directory" value))
+          ('music-dir (mpd-serialize-string "music_directory" value))
+          ('address (mpd-serialize-string "bind_to_address" value))))
+      ""))
+
+(define (mpd-serialize-port field-name value)
+  (when (string? value)
+    (warning
+     (G_ "string value for '~a' is deprecated, use integer instead~%")
+     field-name))
+  (mpd-serialize-field "port" value))
+
+(define-maybe port (prefix mpd-))
+
+;;;
+
+;; Generic MPD plugin record, lists only the most prevalent fields.
+(define-configuration mpd-plugin
+  (plugin
+   maybe-string
+   "Plugin name.")
+
+  (name
+   maybe-string
+   "Name.")
+
+  (enabled?
+   maybe-boolean
+   "Whether the plugin is enabled/disabled.")
+
+  (extra-options
+   (alist '())
+   "An association list of option symbols/strings to string values
+to be appended to the plugin configuration. See
+@uref{https://mpd.readthedocs.io/en/latest/plugins.html,MPD plugin reference}
+for available options.")
+
+  (prefix mpd-))
+
+(define (mpd-serialize-mpd-plugin field-name value)
+  #~(format #f "~a {~%~a}~%"
+            '#$field-name
+            #$(serialize-configuration value mpd-plugin-fields)))
+
+(define (mpd-serialize-list-of-mpd-plugin field-name value)
+  #~(string-append #$@(map (cut mpd-serialize-mpd-plugin field-name <>)
+                           value)))
 
-(define mpd-subsystem-serialize-field (cut mpd-serialize-field <> <> 1))
-(define mpd-subsystem-serialize-string (cut mpd-serialize-string <> <> 1))
-(define mpd-subsystem-serialize-number (cut mpd-serialize-number <> <> 1))
-(define mpd-subsystem-serialize-boolean (cut mpd-serialize-boolean <> <> 1))
-(define mpd-subsystem-serialize-free-form-args (cut mpd-serialize-free-form-args <> <> 1))
+(define list-of-mpd-plugin? (list-of mpd-plugin?))
+
+(define-maybe mpd-plugin (prefix mpd-))
+
+(define-configuration mpd-partition
+  (name
+   string
+   "Partition name.")
+
+  (extra-options
+   (alist '())
+   "An association list of option symbols/strings to string values
+to be appended to the partition configuration. See
+@uref{https://mpd.readthedocs.io/en/latest/user.html#configuring-partitions,Configuring Partitions}
+for available options.")
+
+  (prefix mpd-))
+
+(define (mpd-serialize-mpd-partition field-name value)
+  #~(format #f "partition {~%~a}~%"
+            #$(serialize-configuration value mpd-partition-fields)))
+
+(define (mpd-serialize-list-of-mpd-partition field-name value)
+  #~(string-append #$@(map (cut mpd-serialize-mpd-partition #f <>) value)))
+
+(define list-of-mpd-partition?
+  (list-of mpd-partition?))
 
 (define-configuration mpd-output
   (name
    (string "MPD")
    "The name of the audio output.")
+
   (type
    (string "pulse")
    "The type of audio output.")
+
   (enabled?
    (boolean #t)
    "Specifies whether this audio output is enabled when MPD is started. By
 default, all audio outputs are enabled. This is just the default
 setting when there is no state file; with a state file, the previous
 state is restored.")
+
+  (format
+   maybe-string
+   "Force a specific audio format on output. See
+@uref{https://mpd.readthedocs.io/en/latest/user.html#audio-output-format,Global Audio Format}
+for a more detailed description.")
+   
   (tags?
    (boolean #t)
    "If set to @code{#f}, then MPD will not send tags to this output. This
 is only useful for output plugins that can receive tags, for example the
 @code{httpd} output plugin.")
+
   (always-on?
    (boolean #f)
    "If set to @code{#t}, then MPD attempts to keep this audio output always
 open. This may be useful for streaming servers, when you don’t want to
 disconnect all listeners even when playback is accidentally stopped.")
+
   (mixer-type
    (string "none")
-   "This field accepts a symbol that specifies which mixer should be used
+   "This field accepts a string that specifies which mixer should be used
 for this audio output: the @code{hardware} mixer, the @code{software}
 mixer, the @code{null} mixer (allows setting the volume, but with no
 effect; this can be used as a trick to implement an external mixer
 External Mixer) or no mixer (@code{none}).")
+
+  (replay-gain-handler
+   maybe-string
+   "This field accepts a string that specifies how
+@uref{https://mpd.readthedocs.io/en/latest/user.html#replay-gain,Replay Gain}
+is to be applied. @code{software} uses an internal software volume control,
+@code{mixer} uses the configured (hardware) mixer control and @code{none}
+disables replay gain on this audio output.")
+
   (extra-options
-   (free-form-args '())
-   "An association list of option symbols to string values to be appended to
-the audio output configuration.")
-  (prefix mpd-subsystem-))
+   (alist '())
+   "An association list of option symbols/strings to string values
+to be appended to the audio output configuration.")
 
-(define list-of-mpd-output?
-  (list-of mpd-output?))
+  (prefix mpd-))
+
+(define (mpd-serialize-mpd-output field-name value)
+  #~(format #f "audio_output {~%~a}~%"
+            #$(serialize-configuration value mpd-output-fields)))
+
+(define (mpd-serialize-list-of-mpd-plugin-or-output field-name value)
+  (receive (plugins outputs)
+      (partition mpd-plugin? value)
+    #~(string-append #$@(map (cut mpd-serialize-mpd-plugin "audio_output" <>)
+                             plugins)
+                     #$@(map (cut mpd-serialize-mpd-output #f <>) outputs))))
+
+(define list-of-mpd-plugin-or-output?
+  (list-of (lambda (x)
+             (or (mpd-output? x) (mpd-plugin? x)))))
 
 (define-configuration mpd-configuration
+  (package
+   (file-like mpd)
+   "The MPD package."
+   empty-serializer)
+
   (user
    (string "mpd")
    "The user to run mpd as.")
-  (music-dir
-   (string "~/Music")
+
+  (group
+   (string "mpd")
+   "The group to run mpd as.")
+
+  (shepherd-requirement
+   (list-of-symbol '())
+   "This is a list of symbols naming Shepherd services that this service
+will depend on."
+   empty-serializer)
+
+  (log-file
+   (maybe-string "/var/log/mpd/log")
+   "The location of the log file. Set to @code{syslog} to use the
+local syslog daemon or @code{%unset-value} to omit this directive
+from the configuration file.")
+
+  (log-level
+   maybe-string
+   "Supress any messages below this threshold.
+Available values: @code{notice}, @code{info}, @code{verbose},
+@code{warning} and @code{error}.")
+
+  (music-directory
+   maybe-string
+   "The directory to scan for music files.")
+
+  (music-dir ; TODO: deprecated, remove later
+   maybe-string
    "The directory to scan for music files."
-   (lambda (_ x)
-     (mpd-serialize-field "music_directory" x)))
-  (playlist-dir
-   (string "~/.mpd/playlists")
+   mpd-serialize-deprecated-field)
+
+  (playlist-directory
+   maybe-string
+   "The directory to store playlists.")
+
+  (playlist-dir ; TODO: deprecated, remove later
+   maybe-string
    "The directory to store playlists."
-   (lambda (_ x)
-     (mpd-serialize-field "playlist_directory" x)))
+   mpd-serialize-deprecated-field)
+
   (db-file
-   (string "~/.mpd/tag_cache")
+   maybe-string
    "The location of the music database.")
+
   (state-file
-   (string "~/.mpd/state")
+   maybe-string
    "The location of the file that stores current MPD's state.")
+
   (sticker-file
-   (string "~/.mpd/sticker.sql")
+   maybe-string
    "The location of the sticker database.")
-  (port
-   (string "6600")
-   "The port to run mpd on.")
-  (address
-   (string "any")
+
+  (default-port
+   (maybe-port 6600) ; TODO: switch to integer
+   "The default port to run mpd on.")
+
+  (endpoints
+   maybe-list-of-string
+   "The addresses that mpd will bind to. A port different from
+@var{default-port} may be specified, e.g. @code{localhost:6602} and
+IPv6 addresses must be enclosed in square brackets when a different
+port is used.
+To use a Unix domain socket, an absolute path or a path starting with @code{~}
+can be specified here."
+   (lambda (_ x)
+     (if (maybe-value-set? x)
+         #~(string-append #$@(map
+                              (cut mpd-serialize-field "bind_to_address" <>)
+                              x)) "")))
+
+  (address ; TODO: deprecated, remove later
+   maybe-string
    "The address that mpd will bind to.
 To use a Unix domain socket, an absolute path can be specified here."
+   mpd-serialize-deprecated-field)
+
+  (database
+   maybe-mpd-plugin
+   "MPD database plugin configuration.")
+
+  (partitions
+   (list-of-mpd-partition '())
+   "List of MPD \"partitions\".")
+  
+  (neighbors
+   (list-of-mpd-plugin '())
+   "List of MPD neighbor plugin configurations.")
+
+  (inputs
+   (list-of-mpd-plugin '())
+   "List of MPD input plugin configurations."
+   (lambda (_ x)
+     (mpd-serialize-list-of-mpd-plugin "input" x)))
+
+  (archive-plugins
+   (list-of-mpd-plugin '())
+   "List of MPD archive plugin configurations."
    (lambda (_ x)
-     (mpd-serialize-field "bind_to_address" x)))
+     (mpd-serialize-list-of-mpd-plugin "archive_plugin" x)))
+
+  (input-cache-size
+   maybe-string
+   "MPD input cache size."
+   (lambda (_ x)
+     (if (maybe-value-set? x)
+         #~(string-append "\ninput_cache {\n"
+                          #$(mpd-serialize-string "size" x)
+                          "}\n") "")))
+
+  (decoders
+   (list-of-mpd-plugin '())
+   "List of MPD decoder plugin configurations."
+   (lambda (_ x)
+     (mpd-serialize-list-of-mpd-plugin "decoder" x)))
+
+  (resampler
+   maybe-mpd-plugin
+   "MPD resampler plugin configuration.")
+
+  (filters
+   (list-of-mpd-plugin '())
+   "List of MPD filter plugin configurations."
+   (lambda (_ x)
+     (mpd-serialize-list-of-mpd-plugin "filter" x)))
+
   (outputs
-   (list-of-mpd-output (list (mpd-output)))
+   (list-of-mpd-plugin-or-output (list (mpd-output)))
    "The audio outputs that MPD can use.
 By default this is a single output using pulseaudio.")
+
+  (playlist-plugins
+   (list-of-mpd-plugin '())
+   "List of MPD playlist plugin configurations."
+   (lambda (_ x)
+     (mpd-serialize-list-of-mpd-plugin "playlist_plugin" x)))
+
+  (extra-options
+   (alist '())
+   "An association list of option symbols/strings to string values to be
+appended to the configuration.")
+
   (prefix mpd-))
 
-(define (mpd-file-name config file)
-  "Return a path in /var/run/mpd/ that is writable
-   by @code{user} from @code{config}."
-  (string-append "/var/run/mpd/"
-                 (mpd-configuration-user config)
-                 "/" file))
+(define (mpd-serialize-configuration configuration)
+  (mixed-text-file
+   "mpd.conf"
+   (serialize-configuration configuration mpd-configuration-fields)))
+
+(define (mpd-log-rotation config)
+  (match-record config <mpd-configuration> (log-file)
+    (log-rotation
+     (files (list log-file))
+     (post-rotate #~(begin
+                      (use-modules (gnu services herd))
+                      (with-shepherd-action 'mpd ('reopen) #f))))))
 
 (define (mpd-shepherd-service config)
-  (shepherd-service
-   (documentation "Run the MPD (Music Player Daemon)")
-   (requirement '(user-processes))
-   (provision '(mpd))
-   (start #~(make-forkexec-constructor
-             (list #$(file-append mpd "/bin/mpd")
-                   "--no-daemon"
-                   #$(mpd-serialize-configuration config))
-             #:environment-variables
-             ;; Required to detect PulseAudio when run under a user account.
-             (list (string-append
-                    "XDG_RUNTIME_DIR=/run/user/"
-                    (number->string
-                     (passwd:uid
-                      (getpwnam #$(mpd-configuration-user config))))))
-             #:log-file #$(mpd-file-name config "log")))
-   (stop  #~(make-kill-destructor))))
+  (match-record config <mpd-configuration> (user package shepherd-requirement)
+    (let* ((config-file (mpd-serialize-configuration config)))
+      (shepherd-service
+       (documentation "Run the MPD (Music Player Daemon)")
+       (requirement `(user-processes loopback ,@shepherd-requirement))
+       (provision '(mpd))
+       (start #~(make-forkexec-constructor
+                 (list #$(file-append package "/bin/mpd")
+                       "--no-daemon"
+                       #$config-file)
+                 #:environment-variables
+                 ;; Required to detect PulseAudio when run under a user account.
+                 (list (string-append
+                        "XDG_RUNTIME_DIR=/run/user/"
+                        (number->string (passwd:uid (getpwnam #$user)))))))
+       (stop  #~(make-kill-destructor))
+       (actions
+        (list (shepherd-configuration-action config-file)
+              (shepherd-action
+               (name 'reopen)
+               (documentation "Re-open log files and flush caches.")
+               (procedure
+                #~(lambda (pid)
+                    (if pid
+                        (begin
+                          (kill pid SIGHUP)
+                          (format #t
+                                  "Issued SIGHUP to Service MPD (PID ~a)."
+                                  pid))
+                        (format #t "Service MPD is not running.")))))))))))
 
 (define (mpd-service-activation config)
-  (with-imported-modules '((guix build utils))
+  (match-record config <mpd-configuration> (user log-file)
     #~(begin
         (use-modules (guix build utils))
-        (define %user
-          (getpw #$(mpd-configuration-user config)))
-
-        (let ((directory #$(mpd-file-name config ".mpd")))
-          (mkdir-p directory)
-          (chown directory (passwd:uid %user) (passwd:gid %user))
-
-          ;; Make /var/run/mpd/USER user-owned as well.
-          (chown (dirname directory)
-                 (passwd:uid %user) (passwd:gid %user))))))
-
-
-(define %mpd-accounts
-  ;; Default account and group for MPD.
-  (list (user-group (name "mpd") (system? #t))
-        (user-account
-         (name "mpd")
-         (group "mpd")
-         (system? #t)
-         (comment "Music Player Daemon (MPD) user")
 
-         ;; Note: /var/run/mpd hosts one sub-directory per user, of which
-         ;; /var/run/mpd/mpd corresponds to the "mpd" user.
-         (home-directory "/var/run/mpd/mpd")
+        ;; TODO: remove me, migrates from the old location at /var/run/mpd
+        ;;       to the new one at /var/lib/mpd.
+        (let* ((user (getpw #$user))
+               (deprecated-directory (string-append "/var/run/mpd/"
+                                                    #$user "/.mpd"))
+               (new-directory (string-append (passwd:dir user)
+                                             "/.config/mpd")))
+          (when (and (file-exists? deprecated-directory)
+                     (not (file-exists? new-directory)))
+            (rename-file deprecated-directory new-directory)
+            (chown new-directory (passwd:uid user) (passwd:gid user))))
+        (mkdir-p (dirname #$log-file)))))
 
-         (shell (file-append shadow "/sbin/nologin")))))
+(define (mpd-accounts config)
+  (match-record config <mpd-configuration> (user group)
+    (list (user-group
+           (name group)
+           (system? #t))
+          (user-account
+           (name user)
+           (group group)
+           (system? #t)
+           (comment "Music Player Daemon (MPD) user")
+           ;; MPD can use $HOME (or $XDG_CONFIG_HOME) to place its data
+           (home-directory "/var/lib/mpd")
+           (shell (file-append shadow "/sbin/nologin"))))))
 
 (define mpd-service-type
   (service-type
@@ -235,7 +488,9 @@ (define mpd-service-type
     (list (service-extension shepherd-root-service-type
                              (compose list mpd-shepherd-service))
           (service-extension account-service-type
-                             (const %mpd-accounts))
+                             mpd-accounts)
           (service-extension activation-service-type
-                             mpd-service-activation)))
+                             mpd-service-activation)
+          (service-extension rottlog-service-type
+                             (compose list mpd-log-rotation))))
    (default-value (mpd-configuration))))

base-commit: aae8371f72805cc35e31817e4120468eee4a4a80
prerequisite-patch-id: e47455c06e8e73edcc3f36ccd7b6b289cdfa1e16
-- 
2.38.1





^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [bug#59866] [PATCH v5.1] services: mpd: Refactor MPD service.
  2022-12-24 17:20   ` Liliana Marie Prikler
@ 2022-12-24 19:17     ` mirai
  0 siblings, 0 replies; 29+ messages in thread
From: mirai @ 2022-12-24 19:17 UTC (permalink / raw)
  To: Liliana Marie Prikler, 59866

On 2022-12-24 17:20, Liliana Marie Prikler wrote:
>> -(define %mpd-accounts
>> -  ;; Default account and group for MPD.
>> -  (list (user-group (name "mpd") (system? #t))
>> -        (user-account
>> -         (name "mpd")
>> -         (group "mpd")
>> -         (system? #t)
>> -         (comment "Music Player Daemon (MPD) user")
>>  
>> -         ;; Note: /var/run/mpd hosts one sub-directory per user, of
>> which
>> -         ;; /var/run/mpd/mpd corresponds to the "mpd" user.
>> -         (home-directory "/var/run/mpd/mpd")
>> +        ;; TODO: remove me, migrates from the old location at
>> /var/run/mpd
>> +        ;;       to the new one at /var/lib/mpd.
>> +        (let* ((user (getpw #$user))
>> +               (deprecated-directory (string-append "/var/run/mpd/"
>> +                                                    #$user "/.mpd"))
>> +               (new-directory (string-append (passwd:dir user)
>> +                                             "/.config/mpd")))
>> +          (when (and (file-exists? deprecated-directory)
>> +                     (not (file-exists? new-directory)))
>> +            (rename-file deprecated-directory new-directory)
>> +            (chown new-directory (passwd:uid user) (passwd:gid
>> user))))
>> +        (mkdir-p (dirname #$log-file)))))
> I'm not sure whether we should migrate the logs here.  I do think the
> logs should be stored in /var/log rather than /var/run, but other than
> that I'm not even sure there's much value in changing the /var/run/mpd
> structure.  What's your use case for doing so?

I agree with you that they belong to /var/log, though I prefer leaving this to
MPDs default behavior when the directives are not explicitly set.
IIRC MPD will store them under $XDG_CONFIG_HOME/log if the "log_file" directive
is absent.

They were previously stored under /var/run because it was set by default to store
under ~/.mpd and the user account had its $HOME set as "/var/run/mpd/mpd".
(Looking closer to the old behavior, this also meant that the user field was actually
ignored as the user-group/user-account was hardcoded).
Since /var/run/mpd(/mpd) is the $HOME directory in the old style configuration, it
also contains other files of interest, as most of the fields default values are
directives relative to $HOME.

Personally I've no use to this migration part and I'd prefer an outright breaking
change to the new style, most of the MPD generated files can be regenerated without much hardship
and this is introducing complexity and soon™ to be obsolete code.

Cheers




^ permalink raw reply	[flat|nested] 29+ messages in thread

* [bug#59866] Pulseaudio woes
  2022-12-06 23:22 [bug#59866] [PATCH 0/2] services: mpd: Refactor MPD service mirai
                   ` (9 preceding siblings ...)
  2022-12-24 18:53 ` [bug#59866] [PATCH v5.2] " mirai
@ 2023-01-03 14:43 ` mirai
  2023-01-03 19:38   ` Liliana Marie Prikler
  2023-02-02 20:07 ` [bug#59866] [PATCH v6 1/3] services: mpd: Rewrite using 'define-configuration' Bruno Victal
  11 siblings, 1 reply; 29+ messages in thread
From: mirai @ 2023-01-03 14:43 UTC (permalink / raw)
  To: 59866; +Cc: Liliana Marie Prikler

I've found out that this service (even before this patch-set)
is biased towards being used as a home service rather than a system service.

The reason is that it sets the XDG_RUNTIME_DIR environment variable through shepherd
to:

--8<---------------cut here---------------start------------->8---
(list (string-append
        "XDG_RUNTIME_DIR=/run/user/"
        (number->string (passwd:uid (getpwnam #$user)))))
--8<---------------cut here---------------end--------------->8---

This directory does not exist if this is launched as a system-wide service.
I presume that this went unnoticed because most of the times you'd want to
use this service system-wide you also configure this either as a MPD
'satellite instance' or the audio-outputs are always network-streaming ones.

This falls apart if you configure a system-wide mpd-service with a pulseaudio
output as it will try to access XDG_RUNTIME_DIR which is not created for the
system? #t 'mpd' user.

Now, pulseaudio is usually launched as a 'per-user' daemon although it can
be used in a system-pulse configuration though this is strongly discouraged.
Under most conditions (pulseaudio config mostly unchanged),
a system-wide mpd-service-type is also able to launch its own pulse instance
but due to the inherent assumptions present in the original service we have that:
  - The XDG_RUNTIME_DIR env var is set when it should only be set for 'system? #f' users.
  - PULSE_CLIENTCONFIG and PULSE_CONFIG are not set by shepherd.
	These usually correspond to:
	  PULSE_CONFIG =/etc/pulse/client.conf
	  PULSE_CONFIG=/etc/pulse/daemon.conf
        But if you use mpd-service-type as a home service, you could also have
      these set to a user-specific pulse config, which will be overridden by shepherd
      if these are set in mpd-service-type.
        But again, if you don't set these env vars and use it as a systemwide service
      with a pulseaudio output and you workaround the XDG_RUNTIME_DIR by unsetting it,
      your pulse daemon won't read its config and there will be no output.

This is not just a theoretical issue, consider this snippet:

--8<---------------cut here---------------start------------->8---
;; Set a systemwide mpd service that streams with pulseaudio RTP output.

(service pulseaudio-service-type (pulseaudio-configuration
                                    (extra-script-files
                                       (list
                                         (plain-file
                                           "rtp.pa"
                                           (string-join (list "load-module module-null-sink sink_name=rtp"
                                                              "load-module module-rtp-send source=rtp.monitor"
                                                              "set-default-sink rtp") "\n" 'suffix))))))
(service mpd-service-type
                   (mpd-configuration
                    (outputs (list (mpd-output (name "Pulseaudio over RTP")
                                     (type "pulse")
                                     (extra-options '((sink . "rtp")))))))
--8<---------------cut here---------------end--------------->8---

The problem here is how to conditionally select which environment variables should be set based
on whether this is being launched as a system-wide or home-instance service. I'm all ears for
suggestions.


Regarding the current patch-set, I'm thinking that some of the non-public procedures could
be defined with 'define-deprecated' instead which should (?) be cleaner than the current
comments that are demarcating deprecated code.

Regards,
Bruno





^ permalink raw reply	[flat|nested] 29+ messages in thread

* [bug#59866] Pulseaudio woes
  2023-01-03 14:43 ` [bug#59866] Pulseaudio woes mirai
@ 2023-01-03 19:38   ` Liliana Marie Prikler
  0 siblings, 0 replies; 29+ messages in thread
From: Liliana Marie Prikler @ 2023-01-03 19:38 UTC (permalink / raw)
  To: mirai, 59866

Am Dienstag, dem 03.01.2023 um 14:43 +0000 schrieb mirai:
> I've found out that this service (even before this patch-set)
> is biased towards being used as a home service rather than a system
> service.
> 
> The reason is that it sets the XDG_RUNTIME_DIR environment variable
> through shepherd
> to:
> 
> --8<---------------cut here---------------start------------->8---
> (list (string-append
>         "XDG_RUNTIME_DIR=/run/user/"
>         (number->string (passwd:uid (getpwnam #$user)))))
> --8<---------------cut here---------------end--------------->8---
> 
> This directory does not exist if this is launched as a system-wide
> service.
> I presume that this went unnoticed because most of the times you'd
> want to use this service system-wide you also configure this either
> as a MPD 'satellite instance' or the audio-outputs are always
> network-streaming ones.
> 
> This falls apart if you configure a system-wide mpd-service with a
> pulseaudio output as it will try to access XDG_RUNTIME_DIR which is
> not created for the system? #t 'mpd' user.
As far as I'm aware, you can instantiate the MPD service system-wide
while also pointing it to a non-system user.  Admittedly, this is a
somewhat degenerate use case, but that's how things were handled before
home services were a thing, and there might still be valid reasons to
support it.

> Now, pulseaudio is usually launched as a 'per-user' daemon although
> it can be used in a system-pulse configuration though this is
> strongly discouraged.
If it is, then XDG_RUNTIME_DIR doesn't matter.

> Under most conditions (pulseaudio config mostly unchanged),
> a system-wide mpd-service-type is also able to launch its own pulse
> instance but due to the inherent assumptions present in the original
> service we have that:
>   - The XDG_RUNTIME_DIR env var is set when it should only be set for
> 'system? #f' users.
>   - PULSE_CLIENTCONFIG and PULSE_CONFIG are not set by shepherd.
>         These usually correspond to:
>           PULSE_CONFIG =/etc/pulse/client.conf
>           PULSE_CONFIG=/etc/pulse/daemon.conf
>         But if you use mpd-service-type as a home service, you could
> also have
>       these set to a user-specific pulse config, which will be
> overridden by shepherd
>       if these are set in mpd-service-type.
>         But again, if you don't set these env vars and use it as a
> systemwide service
>       with a pulseaudio output and you workaround the XDG_RUNTIME_DIR
> by unsetting it,
>       your pulse daemon won't read its config and there will be no
> output.
何?

> This is not just a theoretical issue, consider this snippet:
> 
> --8<---------------cut here---------------start------------->8---
> ;; Set a systemwide mpd service that streams with pulseaudio RTP
> output.
> 
> (service pulseaudio-service-type (pulseaudio-configuration
>                                     (extra-script-files
>                                        (list
>                                          (plain-file
>                                            "rtp.pa"
>                                            (string-join (list "load-
> module module-null-sink sink_name=rtp"
>                                                               "load-
> module module-rtp-send source=rtp.monitor"
>                                                               "set-
> default-sink rtp") "\n" 'suffix))))))
> (service mpd-service-type
>                    (mpd-configuration
>                     (outputs (list (mpd-output (name "Pulseaudio over
> RTP")
>                                      (type "pulse")
>                                      (extra-options '((sink .
> "rtp")))))))
> --8<---------------cut here---------------end--------------->8---
> 
> The problem here is how to conditionally select which environment
> variables should be set based on whether this is being launched as a
> system-wide or home-instance service. I'm all ears for
> suggestions.
I don't think you need to think about the home service too hard here. 
If you can't get the two use cases to work together, you can define a
home-mpd-service-type which takes a regular mpd-configuration or a
particularly crafted home-mpd-configuration, whichever makes more
sense.  Not saying that this would be a good design idea, just that the
option exists.

I do think trying to figure out what you're going to do with your
environment variables is a more worthwhile exercise at the moment.

> Regarding the current patch-set, I'm thinking that some of the non-
> public procedures could be defined with 'define-deprecated' instead
> which should (?) be cleaner than the current
> comments that are demarcating deprecated code.
If they aren't public, what is the point of define-deprecated?  You
will only trip over your own uses.

Cheers

^ permalink raw reply	[flat|nested] 29+ messages in thread

* [bug#59866] [PATCH v6 1/3] services: mpd: Rewrite using 'define-configuration'.
  2022-12-06 23:22 [bug#59866] [PATCH 0/2] services: mpd: Refactor MPD service mirai
                   ` (10 preceding siblings ...)
  2023-01-03 14:43 ` [bug#59866] Pulseaudio woes mirai
@ 2023-02-02 20:07 ` Bruno Victal
  2023-02-02 20:07   ` [bug#59866] [PATCH v6 2/3] services: mpd: Refactor MPD service Bruno Victal
  2023-02-02 20:07   ` [bug#59866] [PATCH v6 3/3] services: mpd: Do not hardcode environment variables Bruno Victal
  11 siblings, 2 replies; 29+ messages in thread
From: Bruno Victal @ 2023-02-02 20:07 UTC (permalink / raw)
  To: 59866; +Cc: Bruno Victal, liliana.prikler

* gnu/services/audio.scm
(mpd-configuration, mpd-output): Rewrite using define-configuration.
(uglify-field-name, mpd-serialize-field, mpd-serialize-alist)
(mpd-serialize-number, mpd-serialize-boolean, mpd-serialize-list-of-mpd-output)
(mpd-serialize-configuration): New procedure.
(list-of-mpd-output?): New predicate.
(mpd-config->file, mpd-output->string): Remove procedure.
---

Notable changes since v5:
  * Completely eliminate indented procedures from patchset.

 gnu/services/audio.scm | 222 ++++++++++++++++++++++++-----------------
 1 file changed, 133 insertions(+), 89 deletions(-)

diff --git a/gnu/services/audio.scm b/gnu/services/audio.scm
index c60053f33c..b7cb0ebe38 100644
--- a/gnu/services/audio.scm
+++ b/gnu/services/audio.scm
@@ -2,6 +2,7 @@
 ;;; Copyright © 2017 Peter Mikkelsen <petermikkelsen10@gmail.com>
 ;;; Copyright © 2019 Ricardo Wurmus <rekado@elephly.net>
 ;;; Copyright © 2020 Ludovic Courtès <ludo@gnu.org>
+;;; Copyright © 2022 Bruno Victal <mirai@makinata.eu>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -21,13 +22,15 @@
 (define-module (gnu services audio)
   #:use-module (guix gexp)
   #:use-module (gnu services)
+  #:use-module (gnu services configuration)
   #:use-module (gnu services shepherd)
   #:use-module (gnu system shadow)
   #:use-module (gnu packages admin)
   #:use-module (gnu packages mpd)
   #:use-module (guix records)
   #:use-module (ice-9 match)
-  #:use-module (ice-9 format)
+  #:use-module (srfi srfi-1)
+  #:use-module (srfi srfi-26)
   #:export (mpd-output
             mpd-output?
             mpd-configuration
@@ -40,93 +43,134 @@ (define-module (gnu services audio)
 ;;;
 ;;; Code:
 
-(define-record-type* <mpd-output>
-  mpd-output make-mpd-output
-  mpd-output?
-  (type          mpd-output-type
-                 (default "pulse"))
-  (name          mpd-output-name
-                 (default "MPD"))
-  (enabled?      mpd-output-enabled?
-                 (default #t))
-  (tags?         mpd-output-tags?
-                 (default #t))
-  (always-on?    mpd-output-always-on?
-                 (default #f))
-  (mixer-type    mpd-output-mixer-type
-                 ;; valid: hardware, software, null, none
-                 (default #f))
-  (extra-options mpd-output-extra-options
-                 (default '())))
-
-(define-record-type* <mpd-configuration>
-  mpd-configuration make-mpd-configuration
-  mpd-configuration?
-  (user         mpd-configuration-user
-                (default "mpd"))
-  (music-dir    mpd-configuration-music-dir
-                (default "~/Music"))
-  (playlist-dir mpd-configuration-playlist-dir
-                (default "~/.mpd/playlists"))
-  (db-file      mpd-configuration-db-file
-                (default "~/.mpd/tag_cache"))
-  (state-file   mpd-configuration-state-file
-                (default "~/.mpd/state"))
-  (sticker-file mpd-configuration-sticker-file
-                (default "~/.mpd/sticker.sql"))
-  (port         mpd-configuration-port
-                (default "6600"))
-  (address      mpd-configuration-address
-                (default "any"))
-  (outputs      mpd-configuration-outputs
-                (default (list (mpd-output)))))
-
-(define (mpd-output->string output)
-  "Convert the OUTPUT of type <mpd-output> to a configuration file snippet."
-  (let ((extra (string-join
-                (map (match-lambda
-                       ((key . value)
-                        (format #f "  ~a \"~a\""
-                                (string-map
-                                 (lambda (c) (if (char=? c #\-) #\_ c))
-                                 (symbol->string key))
-                                value)))
-                     (mpd-output-extra-options output))
-                "\n")))
-    (format #f "\
-audio_output {
-  type \"~a\"
-  name \"~a\"
-~:[  enabled \"no\"~%~;~]\
-~:[  tags \"no\"~%~;~]\
-~:[~;  always_on \"yes\"~%~]\
-~@[  mixer_type \"~a\"~%~]\
-~a~%}~%"
-            (mpd-output-type output)
-            (mpd-output-name output)
-            (mpd-output-enabled? output)
-            (mpd-output-tags? output)
-            (mpd-output-always-on? output)
-            (mpd-output-mixer-type output)
-            extra)))
-
-(define (mpd-config->file config)
-  (apply
-   mixed-text-file "mpd.conf"
-   "pid_file \"" (mpd-file-name config "pid") "\"\n"
-   (append (map mpd-output->string
-                (mpd-configuration-outputs config))
-           (map (match-lambda
-                  ((config-name config-val)
-                   (string-append config-name " \"" (config-val config) "\"\n")))
-                `(("user" ,mpd-configuration-user)
-                  ("music_directory" ,mpd-configuration-music-dir)
-                  ("playlist_directory" ,mpd-configuration-playlist-dir)
-                  ("db_file" ,mpd-configuration-db-file)
-                  ("state_file" ,mpd-configuration-state-file)
-                  ("sticker_file" ,mpd-configuration-sticker-file)
-                  ("port" ,mpd-configuration-port)
-                  ("bind_to_address" ,mpd-configuration-address))))))
+(define (uglify-field-name field-name)
+  (let ((str (symbol->string field-name)))
+    (string-join (string-split (if (string-suffix? "?" str)
+                                   (string-drop-right str 1)
+                                   str)
+                               #\-) "_")))
+
+(define (mpd-serialize-field field-name value)
+  #~(format #f "~a ~s~%" #$(if (string? field-name)
+                               field-name
+                               (uglify-field-name field-name))
+            #$(if (string? value)
+                  value
+                  (object->string value))))
+
+(define (mpd-serialize-alist field-name value)
+  #~(string-append #$@(generic-serialize-alist list mpd-serialize-field
+                                               value)))
+
+(define mpd-serialize-string mpd-serialize-field)
+
+(define (mpd-serialize-boolean field-name value)
+  (mpd-serialize-field field-name (if value "yes" "no")))
+
+(define (mpd-serialize-list-of-mpd-output field-name value)
+  #~(string-append "\naudio_output {\n"
+                   #$@(map (cut serialize-configuration <>
+                                mpd-output-fields)
+                           value)
+                   "}\n"))
+
+(define (mpd-serialize-configuration configuration)
+  (mixed-text-file
+   "mpd.conf"
+   (serialize-configuration configuration mpd-configuration-fields)))
+
+(define-configuration mpd-output
+  (name
+   (string "MPD")
+   "The name of the audio output.")
+
+  (type
+   (string "pulse")
+   "The type of audio output.")
+
+  (enabled?
+   (boolean #t)
+   "Specifies whether this audio output is enabled when MPD is started. By
+default, all audio outputs are enabled. This is just the default
+setting when there is no state file; with a state file, the previous
+state is restored.")
+
+  (tags?
+   (boolean #t)
+   "If set to @code{#f}, then MPD will not send tags to this output. This
+is only useful for output plugins that can receive tags, for example the
+@code{httpd} output plugin.")
+
+  (always-on?
+   (boolean #f)
+   "If set to @code{#t}, then MPD attempts to keep this audio output always
+open. This may be useful for streaming servers, when you don’t want to
+disconnect all listeners even when playback is accidentally stopped.")
+
+  (mixer-type
+   (string "none")
+   "This field accepts a symbol that specifies which mixer should be used
+for this audio output: the @code{hardware} mixer, the @code{software}
+mixer, the @code{null} mixer (allows setting the volume, but with no
+effect; this can be used as a trick to implement an external mixer
+External Mixer) or no mixer (@code{none}).")
+
+  (extra-options
+   (alist '())
+   "An association list of option symbols to string values to be appended to
+the audio output configuration.")
+
+  (prefix mpd-))
+
+(define list-of-mpd-output?
+  (list-of mpd-output?))
+
+(define-configuration mpd-configuration
+  (user
+   (string "mpd")
+   "The user to run mpd as.")
+
+  (music-dir
+   (string "~/Music")
+   "The directory to scan for music files."
+   (lambda (_ x)
+     (mpd-serialize-field "music_directory" x)))
+
+  (playlist-dir
+   (string "~/.mpd/playlists")
+   "The directory to store playlists."
+   (lambda (_ x)
+     (mpd-serialize-field "playlist_directory" x)))
+
+  (db-file
+   (string "~/.mpd/tag_cache")
+   "The location of the music database.")
+
+  (state-file
+   (string "~/.mpd/state")
+   "The location of the file that stores current MPD's state.")
+
+  (sticker-file
+   (string "~/.mpd/sticker.sql")
+   "The location of the sticker database.")
+
+  (port
+   (string "6600")
+   "The port to run mpd on.")
+
+  (address
+   (string "any")
+   "The address that mpd will bind to.
+To use a Unix domain socket, an absolute path can be specified here."
+   (lambda (_ x)
+     (mpd-serialize-field "bind_to_address" x)))
+
+  (outputs
+   (list-of-mpd-output (list (mpd-output)))
+   "The audio outputs that MPD can use.
+By default this is a single output using pulseaudio.")
+
+  (prefix mpd-))
 
 (define (mpd-file-name config file)
   "Return a path in /var/run/mpd/ that is writable
@@ -143,7 +187,7 @@ (define (mpd-shepherd-service config)
    (start #~(make-forkexec-constructor
              (list #$(file-append mpd "/bin/mpd")
                    "--no-daemon"
-                   #$(mpd-config->file config))
+                   #$(mpd-serialize-configuration config))
              #:environment-variables
              ;; Required to detect PulseAudio when run under a user account.
              (list (string-append
-- 
2.38.1





^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [bug#59866] [PATCH v6 2/3] services: mpd: Refactor MPD service.
  2023-02-02 20:07 ` [bug#59866] [PATCH v6 1/3] services: mpd: Rewrite using 'define-configuration' Bruno Victal
@ 2023-02-02 20:07   ` Bruno Victal
  2023-02-02 21:08     ` Liliana Marie Prikler
  2023-02-02 20:07   ` [bug#59866] [PATCH v6 3/3] services: mpd: Do not hardcode environment variables Bruno Victal
  1 sibling, 1 reply; 29+ messages in thread
From: Bruno Victal @ 2023-02-02 20:07 UTC (permalink / raw)
  To: 59866; +Cc: Bruno Victal, liliana.prikler

Refactor mpd-service-type to support additional mpd.conf directives
and move activation-service-extension into service constructor.

* gnu/services/audio.scm
(mpd-plugin, mpd-partition): New record.

(mpd-serialize-boolean, mpd-serialize-field): Integrate serializers
into a single procedure for alist interop.

(mpd-plugin?, mpd-partition?, list-of-string?, list-of-symbol?)
(list-of-mpd-plugin?, list-of-mpd-partition?)
(list-of-mpd-plugin-or-output?, port?): New predicate.

(mpd-file-name, mpd-service-activation): Remove procedure.

(mpd-configuration)
[package, group, shepherd-requirement, log-file, log-level, music-directory]
[playlist-directory, endpoints, database, partitions, neighbors, inputs]
[archive-plugins, input-cache-size, decoders, resampler, filters]
[playlist-plugins, extra-options]: New field.
[music-dir, playlist-dir, address]: Deprecate shorthand field.
[db-file, state-file, sticker-file, port, outputs]: Change admissible type.

(mpd-shepherd-service)
[actions]: New shepherd actions: 'reopen' and 'configuration'.
[requirement]: Splice with 'shepherd-requirement' field.
[start]: Use 'package' field. Remove #:log-file parameter.
Move activation-service extension into constructor.

(mpd-accounts): Honor user and group names from configuration.
(mpd-log-rotation): New procedure.
(mpd-service-type)[extensions]: Add rottlog-service-type extension.
Remove activation-service-type extension.

(mpd-output-name, mpd-output-type, mpd-output-enabled?, mpd-output-format)
(mpd-output-tags?, mpd-output-always-on?, mpd-output-mixer-type)
(mpd-output-replay-gain-handler, mpd-output-extra-options)
(mpd-plugin-plugin, mpd-plugin-name, mpd-plugin-enabled?)
(mpd-plugin-extra-options)
(mpd-partition-name, mpd-partition-extra-options)
(mpd-configuration-package, mpd-configuration-user)
(mpd-configuration-group, mpd-configuration-shepherd-requirement)
(mpd-configuration-log-file, mpd-configuration-log-level)
(mpd-configuration-music-directory, mpd-configuration-music-dir)
(mpd-configuration-playlist-directory, mpd-configuration-playlist-dir)
(mpd-configuration-db-file, mpd-configuration-state-file)
(mpd-configuration-sticker-file, mpd-configuration-default-port)
(mpd-configuration-endpoints, mpd-configuration-address)
(mpd-configuration-database, mpd-configuration-partitions)
(mpd-configuration-neighbors, mpd-configuration-inputs)
(mpd-configuration-archive-plugins, mpd-configuration-input-cache-size)
(mpd-configuration-decoders, mpd-configuration-resampler)
(mpd-configuration-filters, mpd-configuration-outputs)
(mpd-configuration-playlist-plugins, mpd-configuration-extra-options): Export accessors.

* doc/guix.texi (Audio Services)[Music Player Daemon]: Update doc.
---


Notable changes since v5:
  * Export accessors.
  * Integrate activation-service-type extension into service constructor.
  * Honor existing directories and permissions, only create when absent.

 doc/guix.texi          | 177 ++++++++++++---
 gnu/services/audio.scm | 492 +++++++++++++++++++++++++++++++++--------
 2 files changed, 543 insertions(+), 126 deletions(-)

diff --git a/doc/guix.texi b/doc/guix.texi
index 64873db00b..8e220e0631 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -109,6 +109,7 @@
 Copyright @copyright{} 2022 Simon Streit@*
 Copyright @copyright{} 2022 (@*
 Copyright @copyright{} 2022 John Kehayias@*
+Copyright @copyright{} 2022 Bruno Victal@*
 Copyright @copyright{} 2022 Ivan Vilata-i-Balaguer@*
 Copyright @copyright{} 2023 Giacomo Leidi@*
 Copyright @copyright{} 2022 Antero Mejr@*
@@ -33185,79 +33186,187 @@ Audio Services
 Data type representing the configuration of @command{mpd}.
 
 @table @asis
-@item @code{user} (default: @code{"mpd"})
+@item @code{package} (default: @code{mpd}) (type: file-like)
+The MPD package.
+
+@item @code{user} (default: @code{"mpd"}) (type: string)
 The user to run mpd as.
 
-@item @code{music-dir} (default: @code{"~/Music"})
+@item @code{group} (default: @code{"mpd"}) (type: string)
+The group to run mpd as.
+
+@item @code{shepherd-requirement} (default: @code{()}) (type: list-of-symbol)
+This is a list of symbols naming Shepherd services that this service
+will depend on.
+
+@item @code{log-file} (default: @code{"/var/log/mpd/log"}) (type: maybe-string)
+The location of the log file.  Set to @code{syslog} to use the local
+syslog daemon or @code{%unset-value} to omit this directive from the
+configuration file.
+
+@item @code{log-level} (type: maybe-string)
+Supress any messages below this threshold.  Available values:
+@code{notice}, @code{info}, @code{verbose}, @code{warning} and
+@code{error}.
+
+@item @code{music-directory} (type: maybe-string)
 The directory to scan for music files.
 
-@item @code{playlist-dir} (default: @code{"~/.mpd/playlists"})
+@item @code{playlist-directory} (type: maybe-string)
 The directory to store playlists.
 
-@item @code{db-file} (default: @code{"~/.mpd/tag_cache"})
+@item @code{db-file} (type: maybe-string)
 The location of the music database.
 
-@item @code{state-file} (default: @code{"~/.mpd/state"})
+@item @code{state-file} (type: maybe-string)
 The location of the file that stores current MPD's state.
 
-@item @code{sticker-file} (default: @code{"~/.mpd/sticker.sql"})
+@item @code{sticker-file} (type: maybe-string)
 The location of the sticker database.
 
-@item @code{port} (default: @code{"6600"})
-The port to run mpd on.
+@item @code{default-port} (default: @code{6600}) (type: maybe-integer)
+The default port to run mpd on.
+
+@item @code{endpoints} (type: maybe-list-of-string)
+The addresses that mpd will bind to.  A port different from @var{default-port}
+may be specified, e.g. @code{localhost:6602} and IPv6 addresses must be
+enclosed in square brackets when a different port is used.
+To use a Unix domain socket, an absolute path or a path starting with @code{~}
+can be specified here.
+
+@item @code{database} (type: maybe-mpd-plugin)
+MPD database plugin configuration.
+
+@item @code{partitions} (default: @code{()}) (type: list-of-mpd-partition)
+List of MPD "partitions".
 
-@item @code{address} (default: @code{"any"})
-The address that mpd will bind to.  To use a Unix domain socket,
-an absolute path can be specified here.
+@item @code{neighbors} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD neighbor plugin configurations.
 
-@item @code{outputs} (default: @code{"(list (mpd-output))"})
-The audio outputs that MPD can use.  By default this is a single output using pulseaudio.
+@item @code{inputs} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD input plugin configurations.
+
+@item @code{archive-plugins} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD archive plugin configurations.
+
+@item @code{input-cache-size} (type: maybe-string)
+MPD input cache size.
+
+@item @code{decoders} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD decoder plugin configurations.
+
+@item @code{resampler} (type: maybe-mpd-plugin)
+MPD resampler plugin configuration.
+
+@item @code{filters} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD filter plugin configurations.
+
+@item @code{outputs} (type: list-of-mpd-plugin-or-output)
+The audio outputs that MPD can use.  By default this is a single output
+using pulseaudio.
+
+@item @code{playlist-plugins} (default: @code{()}) (type: list-of-mpd-plugin)
+List of MPD playlist plugin configurations.
+
+@item @code{extra-options} (default: @code{()}) (type: alist)
+An association list of option symbols/strings to string values to be
+appended to the configuration.
+
+@end table
+@end deftp
+
+@deftp {Data Type} mpd-plugin
+Data type representing a @command{mpd} plugin.
+
+@table @asis
+@item @code{plugin} (type: maybe-string)
+Plugin name.
+
+@item @code{name} (type: maybe-string)
+Name.
+
+@item @code{enabled?} (type: maybe-boolean)
+Whether the plugin is enabled/disabled.
+
+@item @code{extra-options} (default: @code{()}) (type: alist)
+An association list of option symbols/strings to string values to be
+appended to the plugin configuration.  See
+@uref{https://mpd.readthedocs.io/en/latest/plugins.html,MPD plugin
+reference} for available options.
+
+@end table
+@end deftp
+
+@deftp {Data Type} mpd-partition
+Data type representing a @command{mpd} partition.
+
+@table @asis
+@item @code{name} (type: string)
+Partition name.
+
+@item @code{extra-options} (default: @code{()}) (type: alist)
+An association list of option symbols/strings to string values to be
+appended to the partition configuration.  See
+@uref{https://mpd.readthedocs.io/en/latest/user.html#configuring-partitions,Configuring
+Partitions} for available options.
 
 @end table
 @end deftp
 
 @deftp {Data Type} mpd-output
-Data type representing an @command{mpd} audio output.
+Data type representing a @command{mpd} audio output.
 
 @table @asis
-@item @code{name} (default: @code{"MPD"})
+@item @code{name} (default: @code{"MPD"}) (type: string)
 The name of the audio output.
 
-@item @code{type} (default: @code{"pulse"})
+@item @code{type} (default: @code{"pulse"}) (type: string)
 The type of audio output.
 
-@item @code{enabled?} (default: @code{#t})
+@item @code{enabled?} (default: @code{#t}) (type: boolean)
 Specifies whether this audio output is enabled when MPD is started.  By
 default, all audio outputs are enabled.  This is just the default
 setting when there is no state file; with a state file, the previous
 state is restored.
 
-@item @code{tags?} (default: @code{#t})
+@item @code{format} (type: maybe-string)
+Force a specific audio format on output.  See
+@uref{https://mpd.readthedocs.io/en/latest/user.html#audio-output-format,Global
+Audio Format} for a more detailed description.
+
+@item @code{tags?} (default: @code{#t}) (type: boolean)
 If set to @code{#f}, then MPD will not send tags to this output.  This
 is only useful for output plugins that can receive tags, for example the
 @code{httpd} output plugin.
 
-@item @code{always-on?} (default: @code{#f})
+@item @code{always-on?} (default: @code{#f}) (type: boolean)
 If set to @code{#t}, then MPD attempts to keep this audio output always
-open.  This may be useful for streaming servers, when you don’t want to
+open.  This may be useful for streaming servers, when you don?t want to
 disconnect all listeners even when playback is accidentally stopped.
 
-@item @code{mixer-type}
-This field accepts a symbol that specifies which mixer should be used
+@item @code{mixer-type} (default: @code{"none"}) (type: string)
+This field accepts a string that specifies which mixer should be used
 for this audio output: the @code{hardware} mixer, the @code{software}
 mixer, the @code{null} mixer (allows setting the volume, but with no
 effect; this can be used as a trick to implement an external mixer
 External Mixer) or no mixer (@code{none}).
 
-@item @code{extra-options} (default: @code{'()})
-An association list of option symbols to string values to be appended to
-the audio output configuration.
+@item @code{replay-gain-handler} (type: maybe-string)
+This field accepts a string that specifies how
+@uref{https://mpd.readthedocs.io/en/latest/user.html#replay-gain,Replay
+Gain} is to be applied.  @code{software} uses an internal software
+volume control, @code{mixer} uses the configured (hardware) mixer
+control and @code{none} disables replay gain on this audio output.
+
+@item @code{extra-options} (default: @code{()}) (type: alist)
+An association list of option symbols/strings to string values to be
+appended to the audio output configuration.
 
 @end table
 @end deftp
 
-The following example shows a configuration of @code{mpd} that provides
-an HTTP audio streaming output.
+The following example shows a configuration of @command{mpd} that
+configures some of its plugins and provides a HTTP audio streaming output.
 
 @lisp
 (service mpd-service-type
@@ -33269,7 +33378,19 @@ Audio Services
                      (mixer-type 'null)
                      (extra-options
                       `((encoder . "vorbis")
-                        (port    . "8080"))))))))
+                        (port    . "8080"))))))
+           (decoders
+             (list (mpd-plugin
+                     (plugin "mikmod")
+                     (enabled? #f))
+                   (mpd-plugin
+                     (plugin "openmpt")
+                     (enabled? #t)
+                     (extra-options `((repeat-count . -1)
+                                      (interpolation-filter . 1))))))
+           (resampler (mpd-plugin
+                        (plugin "libsamplerate")
+                        (extra-options `((type . 0)))))))
 @end lisp
 
 
diff --git a/gnu/services/audio.scm b/gnu/services/audio.scm
index b7cb0ebe38..7168320635 100644
--- a/gnu/services/audio.scm
+++ b/gnu/services/audio.scm
@@ -21,20 +21,75 @@
 
 (define-module (gnu services audio)
   #:use-module (guix gexp)
+  #:use-module (guix deprecation)
+  #:use-module (guix diagnostics)
+  #:use-module (guix i18n)
   #:use-module (gnu services)
   #:use-module (gnu services configuration)
   #:use-module (gnu services shepherd)
+  #:use-module (gnu services admin)
   #:use-module (gnu system shadow)
   #:use-module (gnu packages admin)
   #:use-module (gnu packages mpd)
   #:use-module (guix records)
   #:use-module (ice-9 match)
   #:use-module (srfi srfi-1)
+  #:use-module (srfi srfi-8)
   #:use-module (srfi srfi-26)
   #:export (mpd-output
             mpd-output?
+            mpd-output-name
+            mpd-output-type
+            mpd-output-enabled?
+            mpd-output-format
+            mpd-output-tags?
+            mpd-output-always-on?
+            mpd-output-mixer-type
+            mpd-output-replay-gain-handler
+            mpd-output-extra-options
+
+            mpd-plugin
+            mpd-plugin?
+            mpd-plugin-plugin
+            mpd-plugin-name
+            mpd-plugin-enabled?
+            mpd-plugin-extra-options
+
+            mpd-partition
+            mpd-partition?
+            mpd-partition-name
+            mpd-partition-extra-options
+
             mpd-configuration
             mpd-configuration?
+            mpd-configuration-package
+            mpd-configuration-user
+            mpd-configuration-group
+            mpd-configuration-shepherd-requirement
+            mpd-configuration-log-file
+            mpd-configuration-log-level
+            mpd-configuration-music-directory
+            mpd-configuration-music-dir
+            mpd-configuration-playlist-directory
+            mpd-configuration-playlist-dir
+            mpd-configuration-db-file
+            mpd-configuration-state-file
+            mpd-configuration-sticker-file
+            mpd-configuration-default-port
+            mpd-configuration-endpoints
+            mpd-configuration-address
+            mpd-configuration-database
+            mpd-configuration-partitions
+            mpd-configuration-neighbors
+            mpd-configuration-inputs
+            mpd-configuration-archive-plugins
+            mpd-configuration-input-cache-size
+            mpd-configuration-decoders
+            mpd-configuration-resampler
+            mpd-configuration-filters
+            mpd-configuration-outputs
+            mpd-configuration-playlist-plugins
+            mpd-configuration-extra-options
             mpd-service-type))
 
 ;;; Commentary:
@@ -50,13 +105,20 @@ (define (uglify-field-name field-name)
                                    str)
                                #\-) "_")))
 
+(define list-of-string?
+  (list-of string?))
+
+(define list-of-symbol?
+  (list-of symbol?))
+
 (define (mpd-serialize-field field-name value)
-  #~(format #f "~a ~s~%" #$(if (string? field-name)
-                               field-name
-                               (uglify-field-name field-name))
-            #$(if (string? value)
-                  value
-                  (object->string value))))
+  (let ((field (if (string? field-name) field-name
+                   (uglify-field-name field-name)))
+        (value (cond
+                ((boolean? value) (if value "yes" "no"))
+                ((string? value) value)
+                (else (object->string value)))))
+    #~(format #f "~a ~s~%" #$field #$value)))
 
 (define (mpd-serialize-alist field-name value)
   #~(string-append #$@(generic-serialize-alist list mpd-serialize-field
@@ -64,20 +126,103 @@ (define (mpd-serialize-alist field-name value)
 
 (define mpd-serialize-string mpd-serialize-field)
 
-(define (mpd-serialize-boolean field-name value)
-  (mpd-serialize-field field-name (if value "yes" "no")))
+(define mpd-serialize-boolean mpd-serialize-field)
 
-(define (mpd-serialize-list-of-mpd-output field-name value)
-  #~(string-append "\naudio_output {\n"
-                   #$@(map (cut serialize-configuration <>
-                                mpd-output-fields)
-                           value)
-                   "}\n"))
 
-(define (mpd-serialize-configuration configuration)
-  (mixed-text-file
-   "mpd.conf"
-   (serialize-configuration configuration mpd-configuration-fields)))
+(define-maybe string (prefix mpd-))
+(define-maybe list-of-string (prefix mpd-))
+(define-maybe boolean (prefix mpd-))
+
+;;; TODO: Procedures for deprecated fields, to be removed.
+
+(define mpd-deprecated-fields '((music-dir . music-directory)
+                                (playlist-dir . playlist-directory)
+                                (address . endpoints)))
+
+(define (port? value) (or (string? value) (integer? value)))
+
+(define (mpd-serialize-deprecated-field field-name value)
+  (if (maybe-value-set? value)
+      (begin
+        (warn-about-deprecation
+         field-name #f
+         #:replacement (assoc-ref mpd-deprecated-fields field-name))
+        (match field-name
+          ('playlist-dir (mpd-serialize-string "playlist_directory" value))
+          ('music-dir (mpd-serialize-string "music_directory" value))
+          ('address (mpd-serialize-string "bind_to_address" value))))
+      ""))
+
+(define (mpd-serialize-port field-name value)
+  (when (string? value)
+    (warning
+     (G_ "string value for '~a' is deprecated, use integer instead~%")
+     field-name))
+  (mpd-serialize-field "port" value))
+
+(define-maybe port (prefix mpd-))
+
+;;;
+
+;; Generic MPD plugin record, lists only the most prevalent fields.
+(define-configuration mpd-plugin
+  (plugin
+   maybe-string
+   "Plugin name.")
+
+  (name
+   maybe-string
+   "Name.")
+
+  (enabled?
+   maybe-boolean
+   "Whether the plugin is enabled/disabled.")
+
+  (extra-options
+   (alist '())
+   "An association list of option symbols/strings to string values
+to be appended to the plugin configuration. See
+@uref{https://mpd.readthedocs.io/en/latest/plugins.html,MPD plugin reference}
+for available options.")
+
+  (prefix mpd-))
+
+(define (mpd-serialize-mpd-plugin field-name value)
+  #~(format #f "~a {~%~a}~%"
+            '#$field-name
+            #$(serialize-configuration value mpd-plugin-fields)))
+
+(define (mpd-serialize-list-of-mpd-plugin field-name value)
+  #~(string-append #$@(map (cut mpd-serialize-mpd-plugin field-name <>)
+                           value)))
+
+(define list-of-mpd-plugin? (list-of mpd-plugin?))
+
+(define-maybe mpd-plugin (prefix mpd-))
+
+(define-configuration mpd-partition
+  (name
+   string
+   "Partition name.")
+
+  (extra-options
+   (alist '())
+   "An association list of option symbols/strings to string values
+to be appended to the partition configuration. See
+@uref{https://mpd.readthedocs.io/en/latest/user.html#configuring-partitions,Configuring Partitions}
+for available options.")
+
+  (prefix mpd-))
+
+(define (mpd-serialize-mpd-partition field-name value)
+  #~(format #f "partition {~%~a}~%"
+            #$(serialize-configuration value mpd-partition-fields)))
+
+(define (mpd-serialize-list-of-mpd-partition field-name value)
+  #~(string-append #$@(map (cut mpd-serialize-mpd-partition #f <>) value)))
+
+(define list-of-mpd-partition?
+  (list-of mpd-partition?))
 
 (define-configuration mpd-output
   (name
@@ -95,6 +240,12 @@ (define-configuration mpd-output
 setting when there is no state file; with a state file, the previous
 state is restored.")
 
+  (format
+   maybe-string
+   "Force a specific audio format on output. See
+@uref{https://mpd.readthedocs.io/en/latest/user.html#audio-output-format,Global Audio Format}
+for a more detailed description.")
+
   (tags?
    (boolean #t)
    "If set to @code{#f}, then MPD will not send tags to this output. This
@@ -109,125 +260,270 @@ (define-configuration mpd-output
 
   (mixer-type
    (string "none")
-   "This field accepts a symbol that specifies which mixer should be used
+   "This field accepts a string that specifies which mixer should be used
 for this audio output: the @code{hardware} mixer, the @code{software}
 mixer, the @code{null} mixer (allows setting the volume, but with no
 effect; this can be used as a trick to implement an external mixer
 External Mixer) or no mixer (@code{none}).")
 
+  (replay-gain-handler
+   maybe-string
+   "This field accepts a string that specifies how
+@uref{https://mpd.readthedocs.io/en/latest/user.html#replay-gain,Replay Gain}
+is to be applied. @code{software} uses an internal software volume control,
+@code{mixer} uses the configured (hardware) mixer control and @code{none}
+disables replay gain on this audio output.")
+
   (extra-options
    (alist '())
-   "An association list of option symbols to string values to be appended to
-the audio output configuration.")
+   "An association list of option symbols/strings to string values
+to be appended to the audio output configuration.")
 
   (prefix mpd-))
 
-(define list-of-mpd-output?
-  (list-of mpd-output?))
+(define (mpd-serialize-mpd-output field-name value)
+  #~(format #f "audio_output {~%~a}~%"
+            #$(serialize-configuration value mpd-output-fields)))
+
+(define (mpd-serialize-list-of-mpd-plugin-or-output field-name value)
+  (receive (plugins outputs)
+      (partition mpd-plugin? value)
+    #~(string-append #$@(map (cut mpd-serialize-mpd-plugin "audio_output" <>)
+                             plugins)
+                     #$@(map (cut mpd-serialize-mpd-output #f <>) outputs))))
+
+(define list-of-mpd-plugin-or-output?
+  (list-of (lambda (x)
+             (or (mpd-output? x) (mpd-plugin? x)))))
 
 (define-configuration mpd-configuration
+  (package
+   (file-like mpd)
+   "The MPD package."
+   empty-serializer)
+
   (user
    (string "mpd")
    "The user to run mpd as.")
 
-  (music-dir
-   (string "~/Music")
+  (group
+   (string "mpd")
+   "The group to run mpd as.")
+
+  (shepherd-requirement
+   (list-of-symbol '())
+   "This is a list of symbols naming Shepherd services that this service
+will depend on."
+   empty-serializer)
+
+  (log-file
+   (maybe-string "/var/log/mpd/log")
+   "The location of the log file. Set to @code{syslog} to use the
+local syslog daemon or @code{%unset-value} to omit this directive
+from the configuration file.")
+
+  (log-level
+   maybe-string
+   "Supress any messages below this threshold.
+Available values: @code{notice}, @code{info}, @code{verbose},
+@code{warning} and @code{error}.")
+
+  (music-directory
+   maybe-string
+   "The directory to scan for music files.")
+
+  (music-dir ; TODO: deprecated, remove later
+   maybe-string
    "The directory to scan for music files."
-   (lambda (_ x)
-     (mpd-serialize-field "music_directory" x)))
+   mpd-serialize-deprecated-field)
+
+  (playlist-directory
+   maybe-string
+   "The directory to store playlists.")
 
-  (playlist-dir
-   (string "~/.mpd/playlists")
+  (playlist-dir ; TODO: deprecated, remove later
+   maybe-string
    "The directory to store playlists."
-   (lambda (_ x)
-     (mpd-serialize-field "playlist_directory" x)))
+   mpd-serialize-deprecated-field)
 
   (db-file
-   (string "~/.mpd/tag_cache")
+   maybe-string
    "The location of the music database.")
 
   (state-file
-   (string "~/.mpd/state")
+   maybe-string
    "The location of the file that stores current MPD's state.")
 
   (sticker-file
-   (string "~/.mpd/sticker.sql")
+   maybe-string
    "The location of the sticker database.")
 
-  (port
-   (string "6600")
-   "The port to run mpd on.")
+  (default-port
+   (maybe-port 6600)
+   "The default port to run mpd on.")
+
+  (endpoints
+   maybe-list-of-string
+   "The addresses that mpd will bind to. A port different from
+@var{default-port} may be specified, e.g. @code{localhost:6602} and
+IPv6 addresses must be enclosed in square brackets when a different
+port is used.
+To use a Unix domain socket, an absolute path or a path starting with @code{~}
+can be specified here."
+   (lambda (_ x)
+     (if (maybe-value-set? x)
+         #~(string-append #$@(map
+                              (cut mpd-serialize-field "bind_to_address" <>)
+                              x)) "")))
 
-  (address
-   (string "any")
+  (address ; TODO: deprecated, remove later
+   maybe-string
    "The address that mpd will bind to.
 To use a Unix domain socket, an absolute path can be specified here."
+   mpd-serialize-deprecated-field)
+
+  (database
+   maybe-mpd-plugin
+   "MPD database plugin configuration.")
+
+  (partitions
+   (list-of-mpd-partition '())
+   "List of MPD \"partitions\".")
+
+  (neighbors
+   (list-of-mpd-plugin '())
+   "List of MPD neighbor plugin configurations.")
+
+  (inputs
+   (list-of-mpd-plugin '())
+   "List of MPD input plugin configurations."
    (lambda (_ x)
-     (mpd-serialize-field "bind_to_address" x)))
+     (mpd-serialize-list-of-mpd-plugin "input" x)))
+
+  (archive-plugins
+   (list-of-mpd-plugin '())
+   "List of MPD archive plugin configurations."
+   (lambda (_ x)
+     (mpd-serialize-list-of-mpd-plugin "archive_plugin" x)))
+
+  (input-cache-size
+   maybe-string
+   "MPD input cache size."
+   (lambda (_ x)
+     (if (maybe-value-set? x)
+         #~(string-append "\ninput_cache {\n"
+                          #$(mpd-serialize-string "size" x)
+                          "}\n") "")))
+
+  (decoders
+   (list-of-mpd-plugin '())
+   "List of MPD decoder plugin configurations."
+   (lambda (_ x)
+     (mpd-serialize-list-of-mpd-plugin "decoder" x)))
+
+  (resampler
+   maybe-mpd-plugin
+   "MPD resampler plugin configuration.")
+
+  (filters
+   (list-of-mpd-plugin '())
+   "List of MPD filter plugin configurations."
+   (lambda (_ x)
+     (mpd-serialize-list-of-mpd-plugin "filter" x)))
 
   (outputs
-   (list-of-mpd-output (list (mpd-output)))
+   (list-of-mpd-plugin-or-output (list (mpd-output)))
    "The audio outputs that MPD can use.
 By default this is a single output using pulseaudio.")
 
+  (playlist-plugins
+   (list-of-mpd-plugin '())
+   "List of MPD playlist plugin configurations."
+   (lambda (_ x)
+     (mpd-serialize-list-of-mpd-plugin "playlist_plugin" x)))
+
+  (extra-options
+   (alist '())
+   "An association list of option symbols/strings to string values to be
+appended to the configuration.")
+
   (prefix mpd-))
 
-(define (mpd-file-name config file)
-  "Return a path in /var/run/mpd/ that is writable
-   by @code{user} from @code{config}."
-  (string-append "/var/run/mpd/"
-                 (mpd-configuration-user config)
-                 "/" file))
+(define (mpd-serialize-configuration configuration)
+  (mixed-text-file
+   "mpd.conf"
+   (serialize-configuration configuration mpd-configuration-fields)))
+
+(define (mpd-log-rotation config)
+  (match-record config <mpd-configuration> (log-file)
+    (log-rotation
+     (files (list log-file))
+     (post-rotate #~(begin
+                      (use-modules (gnu services herd))
+                      (with-shepherd-action 'mpd ('reopen) #f))))))
 
 (define (mpd-shepherd-service config)
-  (shepherd-service
-   (documentation "Run the MPD (Music Player Daemon)")
-   (requirement '(user-processes))
-   (provision '(mpd))
-   (start #~(make-forkexec-constructor
-             (list #$(file-append mpd "/bin/mpd")
-                   "--no-daemon"
-                   #$(mpd-serialize-configuration config))
-             #:environment-variables
-             ;; Required to detect PulseAudio when run under a user account.
-             (list (string-append
-                    "XDG_RUNTIME_DIR=/run/user/"
-                    (number->string
-                     (passwd:uid
-                      (getpwnam #$(mpd-configuration-user config))))))
-             #:log-file #$(mpd-file-name config "log")))
-   (stop  #~(make-kill-destructor))))
-
-(define (mpd-service-activation config)
-  (with-imported-modules '((guix build utils))
-    #~(begin
-        (use-modules (guix build utils))
-        (define %user
-          (getpw #$(mpd-configuration-user config)))
-
-        (let ((directory #$(mpd-file-name config ".mpd")))
-          (mkdir-p directory)
-          (chown directory (passwd:uid %user) (passwd:gid %user))
-
-          ;; Make /var/run/mpd/USER user-owned as well.
-          (chown (dirname directory)
-                 (passwd:uid %user) (passwd:gid %user))))))
-
-
-(define %mpd-accounts
-  ;; Default account and group for MPD.
-  (list (user-group (name "mpd") (system? #t))
-        (user-account
-         (name "mpd")
-         (group "mpd")
-         (system? #t)
-         (comment "Music Player Daemon (MPD) user")
-
-         ;; Note: /var/run/mpd hosts one sub-directory per user, of which
-         ;; /var/run/mpd/mpd corresponds to the "mpd" user.
-         (home-directory "/var/run/mpd/mpd")
-
-         (shell (file-append shadow "/sbin/nologin")))))
+  (match-record config <mpd-configuration> (user package shepherd-requirement
+                                            log-file playlist-directory
+                                            db-file state-file sticker-file)
+    (let* ((config-file (mpd-serialize-configuration config)))
+      (shepherd-service
+       (documentation "Run the MPD (Music Player Daemon)")
+       (requirement `(user-processes loopback ,@shepherd-requirement))
+       (provision '(mpd))
+       (start #~(begin
+                  (and=> #$(maybe-value log-file)
+                         (compose mkdir-p dirname))
+
+                  (let ((user (getpw #$user)))
+                    (for-each
+                     (lambda (x)
+                       (when (and x (not (file-exists? x)))
+                         (mkdir-p x)
+                         (chown x (passwd:uid user) (passwd:gid user))))
+                     (list #$(maybe-value playlist-directory)
+                           (and=> #$(maybe-value db-file) dirname)
+                           (and=> #$(maybe-value state-file) dirname)
+                           (and=> #$(maybe-value sticker-file) dirname))))
+
+                  (make-forkexec-constructor
+                   (list #$(file-append package "/bin/mpd")
+                         "--no-daemon"
+                         #$config-file)
+                   #:environment-variables
+                   ;; Required to detect PulseAudio when run under a user account.
+                   (list (string-append
+                          "XDG_RUNTIME_DIR=/run/user/"
+                          (number->string (passwd:uid (getpwnam #$user))))))))
+       (stop  #~(make-kill-destructor))
+       (actions
+        (list (shepherd-configuration-action config-file)
+              (shepherd-action
+               (name 'reopen)
+               (documentation "Re-open log files and flush caches.")
+               (procedure
+                #~(lambda (pid)
+                    (if pid
+                        (begin
+                          (kill pid SIGHUP)
+                          (format #t
+                                  "Issued SIGHUP to Service MPD (PID ~a)."
+                                  pid))
+                        (format #t "Service MPD is not running.")))))))))))
+
+(define (mpd-accounts config)
+  (match-record config <mpd-configuration> (user group)
+    (list (user-group
+           (name group)
+           (system? #t))
+          (user-account
+           (name user)
+           (group group)
+           (system? #t)
+           (comment "Music Player Daemon (MPD) user")
+           ;; MPD can use $HOME (or $XDG_CONFIG_HOME) to place its data
+           (home-directory "/var/lib/mpd")
+           (shell (file-append shadow "/sbin/nologin"))))))
 
 (define mpd-service-type
   (service-type
@@ -237,7 +533,7 @@ (define mpd-service-type
     (list (service-extension shepherd-root-service-type
                              (compose list mpd-shepherd-service))
           (service-extension account-service-type
-                             (const %mpd-accounts))
-          (service-extension activation-service-type
-                             mpd-service-activation)))
+                             mpd-accounts)
+          (service-extension rottlog-service-type
+                             (compose list mpd-log-rotation))))
    (default-value (mpd-configuration))))
-- 
2.38.1





^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [bug#59866] [PATCH v6 3/3] services: mpd: Do not hardcode environment variables.
  2023-02-02 20:07 ` [bug#59866] [PATCH v6 1/3] services: mpd: Rewrite using 'define-configuration' Bruno Victal
  2023-02-02 20:07   ` [bug#59866] [PATCH v6 2/3] services: mpd: Refactor MPD service Bruno Victal
@ 2023-02-02 20:07   ` Bruno Victal
  1 sibling, 0 replies; 29+ messages in thread
From: Bruno Victal @ 2023-02-02 20:07 UTC (permalink / raw)
  To: 59866; +Cc: Bruno Victal, liliana.prikler

Services should not expect for XDG_RUNTIME_DIR to be set.
Inferring from the past comment, this seemed to resolve an issue when
the service was launched with an _interactive_ user-account,
which tends to have this variable set by the login-manager.

This is not the case for system accounts and setting this variable
results in PulseAudio (launched by the same system user) failing to start
since it attempts to use a nonexistent directory.

Ideally, this service should have a home-service counterpart but the old
behavior can be emulated by setting the environment-variables field to:

(environment-variables
  (list
    (string-append "XDG_RUNTIME_DIR=/run/user/"
                   (number->string (passwd:uid (getpwnam "myuser"))))))

* gnu/services/audio.scm
(mpd-configuration)[environment-variables]: New field.
(mpd-shepherd-service)[start]: Use new field.
* doc/guix.texi (Audio Services)[Music Player Daemon]: Document it.
---

Notable changes since v5:
  * NEW. Fixes PulseAudio issues resulting from hardcoded environment variables.

 doc/guix.texi          |  3 +++
 gnu/services/audio.scm | 14 ++++++++------
 2 files changed, 11 insertions(+), 6 deletions(-)

diff --git a/doc/guix.texi b/doc/guix.texi
index 8e220e0631..05c216bec4 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -33199,6 +33199,9 @@ Audio Services
 This is a list of symbols naming Shepherd services that this service
 will depend on.
 
+@item @code{environment-variables} (default: @code{()}) (type: list-of-string)
+A list of strings specifying environment variables.
+
 @item @code{log-file} (default: @code{"/var/log/mpd/log"}) (type: maybe-string)
 The location of the log file.  Set to @code{syslog} to use the local
 syslog daemon or @code{%unset-value} to omit this directive from the
diff --git a/gnu/services/audio.scm b/gnu/services/audio.scm
index 7168320635..96b27660e5 100644
--- a/gnu/services/audio.scm
+++ b/gnu/services/audio.scm
@@ -316,6 +316,11 @@ (define-configuration mpd-configuration
 will depend on."
    empty-serializer)
 
+  (environment-variables
+   (list-of-string '())
+   "A list of strings specifying environment variables."
+   empty-serializer)
+
   (log-file
    (maybe-string "/var/log/mpd/log")
    "The location of the log file. Set to @code{syslog} to use the
@@ -465,7 +470,8 @@ (define (mpd-log-rotation config)
 (define (mpd-shepherd-service config)
   (match-record config <mpd-configuration> (user package shepherd-requirement
                                             log-file playlist-directory
-                                            db-file state-file sticker-file)
+                                            db-file state-file sticker-file
+                                            environment-variables)
     (let* ((config-file (mpd-serialize-configuration config)))
       (shepherd-service
        (documentation "Run the MPD (Music Player Daemon)")
@@ -490,11 +496,7 @@ (define (mpd-shepherd-service config)
                    (list #$(file-append package "/bin/mpd")
                          "--no-daemon"
                          #$config-file)
-                   #:environment-variables
-                   ;; Required to detect PulseAudio when run under a user account.
-                   (list (string-append
-                          "XDG_RUNTIME_DIR=/run/user/"
-                          (number->string (passwd:uid (getpwnam #$user))))))))
+                   #:environment-variables '#$environment-variables)))
        (stop  #~(make-kill-destructor))
        (actions
         (list (shepherd-configuration-action config-file)
-- 
2.38.1





^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [bug#59866] [PATCH v6 2/3] services: mpd: Refactor MPD service.
  2023-02-02 20:07   ` [bug#59866] [PATCH v6 2/3] services: mpd: Refactor MPD service Bruno Victal
@ 2023-02-02 21:08     ` Liliana Marie Prikler
  2023-02-05  6:11       ` bug#59866: " Liliana Marie Prikler
  0 siblings, 1 reply; 29+ messages in thread
From: Liliana Marie Prikler @ 2023-02-02 21:08 UTC (permalink / raw)
  To: Bruno Victal, 59866

Not a complete review yet, just some quick pointers.

Am Donnerstag, dem 02.02.2023 um 20:07 +0000 schrieb Bruno Victal:
> Refactor mpd-service-type to support additional mpd.conf directives
> and move activation-service-extension into service constructor.
> 
> * gnu/services/audio.scm
> (mpd-plugin, mpd-partition): New record.
No need to break the line before the paren here.

> (mpd-serialize-boolean, mpd-serialize-field): Integrate serializers
> into a single procedure for alist interop.
That's not a good docstring here.  I think 
  (mpd-serialize-boolean): Delete variable.  Move logic into...
  (mpd-serialize-field): ... this.
explains what's going on better.

> [...]
>    #:use-module (srfi srfi-1)
> +  #:use-module (srfi srfi-8)
>    #:use-module (srfi srfi-26)
We generally prefer SRFI 71.


Cheers




^ permalink raw reply	[flat|nested] 29+ messages in thread

* bug#59866: [PATCH v6 2/3] services: mpd: Refactor MPD service.
  2023-02-02 21:08     ` Liliana Marie Prikler
@ 2023-02-05  6:11       ` Liliana Marie Prikler
  0 siblings, 0 replies; 29+ messages in thread
From: Liliana Marie Prikler @ 2023-02-05  6:11 UTC (permalink / raw)
  To: Bruno Victal, 59866-done

Am Donnerstag, dem 02.02.2023 um 22:08 +0100 schrieb Liliana Marie
Prikler:
> Not a complete review yet, just some quick pointers.
> 
> Am Donnerstag, dem 02.02.2023 um 20:07 +0000 schrieb Bruno Victal:
> > Refactor mpd-service-type to support additional mpd.conf directives
> > and move activation-service-extension into service constructor.
> > 
> > * gnu/services/audio.scm
> > (mpd-plugin, mpd-partition): New record.
> No need to break the line before the paren here.
> 
> > (mpd-serialize-boolean, mpd-serialize-field): Integrate serializers
> > into a single procedure for alist interop.
> That's not a good docstring here.  I think 
>   (mpd-serialize-boolean): Delete variable.  Move logic into...
>   (mpd-serialize-field): ... this.
> explains what's going on better.
It actually didn't, because mpd-serialize-boolean wasn't deleted, but
aliased.

> > [...]
> >    #:use-module (srfi srfi-1)
> > +  #:use-module (srfi srfi-8)
> >    #:use-module (srfi srfi-26)
> We generally prefer SRFI 71.
I fixed up the above, plus some compiler warnings and pushed it.

Cheers




^ permalink raw reply	[flat|nested] 29+ messages in thread

end of thread, other threads:[~2023-02-05  6:12 UTC | newest]

Thread overview: 29+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2022-12-06 23:22 [bug#59866] [PATCH 0/2] services: mpd: Refactor MPD service mirai
2022-12-06 23:25 ` [bug#59866] [PATCH 1/2] services: mpd: use 'define-configuration' mirai
2022-12-06 23:25 ` [bug#59866] [PATCH 2/2] services: mpd: Refactor MPD service mirai
2022-12-07  8:59 ` [bug#59866] [PATCH 0/2] " ( via Guix-patches via
2022-12-07 13:42   ` mirai
2022-12-07 13:43     ` ( via Guix-patches via
2022-12-07 18:27 ` Liliana Marie Prikler
2022-12-08 13:11   ` mirai
2022-12-08 13:35     ` [bug#54986] " Liliana Marie Prikler
2022-12-09 13:44       ` [bug#59866] " mirai
2022-12-09 19:22         ` Liliana Marie Prikler
2022-12-11 12:05           ` mirai
2022-12-08  0:59 ` [bug#59866] [PATCH v2] " mirai
2022-12-16 14:15 ` [bug#59866] [PATCH v3] " mirai
2022-12-21 14:15 ` [bug#59866] [PATCH v4 1/2] services: mpd: use 'define-configuration' mirai
2022-12-21 14:15   ` [bug#59866] [PATCH v4 2/2] services: mpd: Refactor MPD service mirai
2022-12-24 13:51 ` [bug#59866] [PATCH v5 1/2] services: mpd: rewrite using 'define-configuration' mirai
2022-12-24 13:51   ` [bug#59866] [PATCH v5 2/2] services: mpd: Refactor MPD service mirai
2022-12-24 14:11 ` [bug#59866] [PATCH v5.1] " mirai
2022-12-24 17:20   ` Liliana Marie Prikler
2022-12-24 19:17     ` mirai
2022-12-24 18:53 ` [bug#59866] [PATCH v5.2] " mirai
2023-01-03 14:43 ` [bug#59866] Pulseaudio woes mirai
2023-01-03 19:38   ` Liliana Marie Prikler
2023-02-02 20:07 ` [bug#59866] [PATCH v6 1/3] services: mpd: Rewrite using 'define-configuration' Bruno Victal
2023-02-02 20:07   ` [bug#59866] [PATCH v6 2/3] services: mpd: Refactor MPD service Bruno Victal
2023-02-02 21:08     ` Liliana Marie Prikler
2023-02-05  6:11       ` bug#59866: " Liliana Marie Prikler
2023-02-02 20:07   ` [bug#59866] [PATCH v6 3/3] services: mpd: Do not hardcode environment variables Bruno Victal

Code repositories for project(s) associated with this external index

	https://git.savannah.gnu.org/cgit/guix.git

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.