- [Installation](#org5e537eb)
- [Usage](#orgdbbe2b9)
- [Files](#orgb11dc5a)
- [Changelog](#orgbd9297a)
# Installation
```
git clone https://codeberg.org/useless-utils/mpd-current-json
cd mpd-current-json
```
and to install the executable to `./dist`, in the current directory:
cabal install --install-method=copy --overwrite-policy=always --installdir=dist
or to install to `${CABAL_DIR}/bin` remove the `--installdir=dist` argument. `CABAL_DIR` defaults to `~/.local/share/cabal`.
# Usage
get values
mpd-current-json | jaq .tags.album
mpd-current-json | jaq .status.elapsed_percent
provide host and port with
mpd-current-json -h 'localhost' -p 4321
# Files
## Source
### Main.hs
1. Pragma language extensions
```haskell
{-# LANGUAGE OverloadedStrings #-}
```
2. Module declaration
```haskell
module Main ( main,
getStatusItem,
getTag,
processSong,
headMay,
valueToStringMay,
(.=?) ) where
```
3. Imports
Import for the `libmpd` library, added as `libmpd == 0.10.*` to [mpd-current-json.cabal](#org78142c9).
```haskell
import qualified Network.MPD as MPD
import Network.MPD
( Metadata(..), Song, PlaybackState(Stopped, Playing, Paused) )
import Data.Maybe ( catMaybes )
import Data.Aeson ( object, Key, KeyValue(..), ToJSON )
import Data.Aeson.Encode.Pretty ( encodePretty )
import qualified Data.ByteString.Lazy.Char8 as C
import Text.Printf ( printf )
import Options
( optsParserInfo, execParser, Opts(optPass, optHost, optPort) )
```
4. Main
```haskell
{- | Where the program connects to MPD and uses the helper functions to
extract values, organize them into a list of key/value pairs, make
them a 'Data.Aeson.Value' using 'Data.Aeson.object', then encode it to
a conventional JSON @ByteString@ with
'Data.Aeson.Encode.Pretty.encodePretty' for the pretty-print version.
-}
main :: IO ()
main = do
```
Parse the command-line options and bind the result to `opts`.
```haskell
opts <- execParser optsParserInfo
```
Connect to MPD using either the provided arguments from the command-line or the default values, as defined in [`Parser Opts` definition](#orgab73638).
```haskell
cs <- MPD.withMPDEx (optHost opts) (optPort opts) (optPass opts) MPD.currentSong
st <- MPD.withMPDEx (optHost opts) (optPort opts) (optPass opts) MPD.status
```
where `currentSong` returns a `Maybe (Just (Song {...}))` and `status` returns `Maybe (Status {...})` to be parsed.
The data record `Song` from the command `currentSong` contains a field label "`sgTags`" that contains all embedded metadata tags in a `fromList [...]`, in this `let` statement store the parser `getTag` function calls to be placed in the JSON object later:
```haskell
let artist = getTag Artist cs
artistSort = getTag ArtistSort cs
album = getTag Album cs
albumSort = getTag AlbumSort cs
albumArtist = getTag AlbumArtist cs
albumArtistSort = getTag AlbumArtistSort cs
title = getTag Title cs
track = getTag Track cs
name = getTag Name cs
genre = getTag Genre cs
date = getTag Date cs
originalDate = getTag OriginalDate cs
composer = getTag Composer cs
performer = getTag Performer cs
conductor = getTag Conductor cs
work = getTag Work cs
grouping = getTag Grouping cs
comment = getTag Comment cs
disc = getTag Disc cs
label = getTag Label cs
musicbrainz_Artistid = getTag MUSICBRAINZ_ARTISTID cs
musicbrainz_Albumid = getTag MUSICBRAINZ_ALBUMID cs
musicbrainz_Albumartistid = getTag MUSICBRAINZ_ALBUMARTISTID cs
musicbrainz_Trackid = getTag MUSICBRAINZ_TRACKID cs
musicbrainz_Releasetrackid = getTag MUSICBRAINZ_RELEASETRACKID cs
musicbrainz_Workid = getTag MUSICBRAINZ_WORKID cs
```
Likewise, `getStatusItem` parses values from `Status {...}` returned by `status`, some may require additional `Maybe` checks to get the desired values.
```haskell
let state :: Maybe String
state = case getStatusItem st MPD.stState of
Just ps -> case ps of
Playing -> Just "play" -- same as mpc
Paused -> Just "pause" -- same as mpc
Stopped -> Just "stopped"
Nothing -> Nothing
time = getStatusItem st MPD.stTime
elapsed = case time of
Just t -> case t of
Just (e, _) -> Just e
_ -> Nothing
Nothing -> Nothing
duration = case time of
Just t -> case t of
Just (_, d) -> Just d
_ -> Nothing
Nothing -> Nothing
elapsedPercent :: Maybe Double
elapsedPercent = case time of
Just t -> case t of
Just t1 -> Just (read $ printf "%.2f" (uncurry (/) t1 * 100))
Nothing -> Just 0
Nothing -> Nothing
repeatSt = getStatusItem st MPD.stRepeat
randomSt = getStatusItem st MPD.stRandom
singleSt = getStatusItem st MPD.stSingle
consumeSt = getStatusItem st MPD.stConsume
pos = getStatusItem st MPD.stSongPos
playlistLength = getStatusItem st MPD.stPlaylistLength
bitrate = getStatusItem st MPD.stBitrate
audioFormat = getStatusItem st MPD.stAudio
errorSt = getStatusItem st MPD.stError
```
The `object . catMaybes` constructs a JSON object by combining a list of key/value pairs. The `.=?` operator is used to create each key/value pair. If the value is `Just`, the key/value pair is included in the list; if the value is `Nothing`, it is filtered out using `catMaybes` to prevent generating fields with a value of `null` in the final JSON object. Then, the `object` function converts the list of key/value pairs `[Pair]` into a `Value` data structure that can be 'encoded' using `Data.Aeson`'s "`encode`" or `Data.Aeson.Encode.Pretty`'s "`encodePretty`".
```haskell
-- sgTags
let jTags = object . catMaybes $
[ "artist" .=? artist
, "artist_sort" .=? artistSort
, "album" .=? album
, "album_sort" .=? albumSort
, "album_artist" .=? albumArtist
, "album_artist_sort" .=? albumArtistSort
, "title" .=? title
, "track" .=? track
, "name" .=? name
, "genre" .=? genre
, "date" .=? date
, "original_date" .=? originalDate
, "composer" .=? composer
, "performer" .=? performer
, "conductor" .=? conductor
, "work" .=? work
, "grouping" .=? grouping
, "comment" .=? comment
, "disc" .=? disc
, "label" .=? label
, "musicbrainz_artistid" .=? musicbrainz_Artistid
, "musicbrainz_albumid" .=? musicbrainz_Albumid
, "musicbrainz_albumartistid" .=? musicbrainz_Albumartistid
, "musicbrainz_trackid" .=? musicbrainz_Trackid
, "musicbrainz_releasetrackid" .=? musicbrainz_Releasetrackid
, "musicbrainz_workid" .=? musicbrainz_Workid
]
-- status
let jStatus = object . catMaybes $
[ "state" .=? state
, "repeat" .=? repeatSt
, "elapsed" .=? elapsed
, "duration" .=? duration
, "elapsed_percent" .=? elapsedPercent
, "random" .=? randomSt
, "single" .=? singleSt
, "consume" .=? consumeSt
, "song_position" .=? pos
, "playlist_length" .=? playlistLength
, "bitrate" .=? bitrate
, "audio_format" .=? audioFormat
, "error" .=? errorSt
]
```
Having two objects, one for "tags" and other for "status", create a nested JSON with labels before each of them.
```haskell
let jObject = object [ "tags" .= jTags
, "status" .= jStatus ]
```
e.g. so they can be parsed as "`.tags.title`" or "`.status.elapsed_percent`".
Finally, encode it to real JSON and print it to the terminal. `Data.Aeson`'s encoding is returned as a `ByteString` so use the `Data.ByteString...` import that provides an implementation of `putStrLn` that supports `ByteString`s.
```haskell
C.putStrLn $ encodePretty jObject
```
1. Utility Functions
The `getStatusItem` function takes an `Either MPD.MPDError MPD.Status` value and a field label function `f` as arguments. It returns `Just (f st)` if the input status is `Right st`, where `st` is the `MPD.Status` value. This function helps to extract a specific field from the status data record by providing the corresponding field label function. If the input status is not `Right st`, indicating an error, or the field label function is not applicable, it returns `Nothing`.
```haskell
{- | Extract a field from the returned MPD.Status data record.
This takes an @Either@ 'Network.MPD.MPDError' 'Network.MPD.Status'
value and a field label function @f@ as arguments. It returns @Just
(f st)@ if the input status is @Right st@, where @st@ is the
'Network.MPD.Status' value. This function helps to extract a
specific field from the @MPD.Status@ data record by providing the
corresponding field label function. If the input status "@st@" is
not @Right st@, indicating an error, or the field label function is
not applicable, it returns @Nothing@.
-}
getStatusItem :: Either MPD.MPDError MPD.Status -> (MPD.Status -> a) -> Maybe a
getStatusItem (Right st) f = Just (f st)
getStatusItem _ _ = Nothing
```
The `getTag` function takes a metadata type `t` and an `Either` value `c` containing a `Maybe Song`. It checks if the `Either` value is `Left _`, indicating an error, and returns `Nothing`. If the `Either` value is `Right song`, it calls the `processSong` function with the metadata type `t` and the `Just song` value, which extracts the tag value from the song. The `getTag` function helps to retrieve a specific tag value from the song if it exists.
```haskell
{- | @Either@ check for the returned value of 'Network.MPD.currentSong',
then call 'processSong' or return @Nothing@.
-}
getTag :: Metadata -> Either a (Maybe Song) -> Maybe String
getTag t c =
case c of
Left _ -> Nothing
Right song -> processSong t song
```
The `processSong` function takes a metadata type `tag` and a `Maybe Song`. If the `Maybe Song` value is `Nothing`, indicating an empty value, it returns `Nothing`. If the `Maybe Song` value is `Just song`, it retrieves the tag value using the `MPD.sgGetTag` function with the provided metadata type and song. It then applies the `headMay` function to extract the first element from the list of tag values and the `valueToStringMay` function to convert the value to a string within a `Maybe` context. This function helps to process the tag values of a song and convert them to strings if they exist.
```haskell
{- | Use 'Network.MPD.sgGetTag' to extract a @tag@ from a @song@, safely
get only the head item of the returned @Maybe@ list, then safely
convert it to a string.
-}
processSong :: Metadata -> Maybe Song -> Maybe String
processSong _ Nothing = Nothing
processSong tag (Just song) = do
let tagVal = MPD.sgGetTag tag song
valueToStringMay =<< (headMay =<< tagVal)
```
The `headMay` function is a utility function that safely gets the head of a list. It takes a list as input and returns `Nothing` if the list is empty or `Just x` where `x` is the first element of the list.
```haskell
{- | Safely get the head of a list. Same as 'Safe.headMay'.
-}
headMay :: [a] -> Maybe a
headMay [] = Nothing
headMay (x:_) = Just x
```
The `valueToStringMay` function is a utility function that converts a `MPD.Value` to a `String` within a `Maybe` context. It takes a `MPD.Value` as input and returns `Just (MPD.toString x)` where `x` is the input value converted to a string.
```haskell
{- | Convert 'Network.MPD.Value' to @String@ within a @Maybe@ context.
This @Value@ is from 'Network.MPD' and is basically the same as a
@String@ but used internally to store metadata values.
__Example__:
@
processSong :: Metadata -> Maybe Song -> Maybe String
processSong _ Nothing = Nothing
processSong tag (Just song) = do
let tagVal = MPD.sgGetTag tag song
valueToStringMay =<< (headMay =<< tagVal)
@
'MPD.sgGetTag' returns a @Maybe [Value]@. 'Network.MPD' also provides
'Network.MPD.toString' that can convert, along other types, a
'Network.MPD.Value' to a @String@.
-}
valueToStringMay :: MPD.Value -> Maybe String
valueToStringMay x = Just (MPD.toString x)
```
The `.=?` operator is a utility function to define optional fields in the key-value pairs of a JSON object. It takes a `Key` and a `Maybe` value `v` as input. If the `Maybe` value is `Just value`, it returns `Just (key .= value)`, where `key` is the input key and `value` is the input value. If the `Maybe` value is `Nothing`, it returns `Nothing`. This operator helps to conditionally include or exclude fields in the JSON object based on the presence or absence of values.
```haskell
{- | Check if @Maybe v@ exists and is of type expected by
'Data.Aeson.object' as defined in 'Data.Aeson.Value', if it is return
both the @key@ and @value@ within the @Maybe@ context tied with
'Data.Aeson..='. This gives support to \'optional\' fields using
'Data.Maybe.catMaybes' that discard @Nothing@ values and is meant to
prevent creating JSON key/value pairs with @null@ values, e.g.:
@
jsonTags = object . catMaybes $
[ "artist" .=? artist
, "album" .=? album
, "title" .=? title
]
@
Where if a value on the right is @Nothing@ that key/value pair will
not be included in 'Data.Aeson.object' because of
'Data.Maybe.catMaybes'.
-}
(.=?) :: (KeyValue a, ToJSON v) => Key -> Maybe v -> Maybe a
key .=? Just value = Just (key .= value)
_ .=? Nothing = Nothing
```
### Options.hs
```haskell
module Options
( Opts(..)
, execParser
, prefs
, showHelpOnEmpty
, optsParser
, optsParserInfo ) where
import Options.Applicative
( (<**>),
auto,
fullDesc,
header,
help,
info,
long,
metavar,
option,
strOption,
prefs,
progDesc,
short,
showHelpOnEmpty,
value,
execParser,
Parser,
ParserInfo,
infoOption,
hidden )
import Options.Applicative.Extra ( helperWith )
import Version ( versionStr, progName )
import Data.Kind (Type)
```
1. Data record for holding parsed 'Parser' values
```haskell
data Opts = Opts -- ^ Custom data record for storing 'Options.Applicative.Parser' values
{ optPort :: Integer -- ^ MPD port to connect.
, optHost :: String -- ^ MPD host address to connect.
, optPass :: String -- ^ Plain text password to connect to MPD.
, optVersion :: Type -> Type -- ^ Print program version.
}
```
2. `Parser Opts` definition
> A [Parser](https://hackage.haskell.org/package/optparse-applicative-0.18.1.0/docs/Options-Applicative.html#t:Parser) a is an option parser returning a value of type a.
Specify how `Options.Applicative` should parse arguments. Their returned values are stored in the custom defined data record `Opts`.
```haskell
optsParser :: Parser Opts
optsParser
= Opts
<$> portOptParser
<*> hostOptParser
<*> passOptParser
<*> versionOptParse
portOptParser :: Parser Integer
portOptParser
= option auto
$ long "port"
<> short 'p'
<> metavar "PORTNUM"
<> value 6600
<> help "Port number"
hostOptParser :: Parser String
hostOptParser
= strOption
$ metavar "ADDRESS"
<> long "host"
<> short 'h'
<> value "localhost"
<> help "Host address"
passOptParser :: Parser String
passOptParser
= option auto
$ metavar "PASSWORD"
<> long "password"
<> short 'P'
<> value ""
<> help "Password for connecting (will be sent as plain text)"
versionOptParse :: Parser (a -> a)
versionOptParse =
infoOption versionStr
$ long "version"
<> short 'V'
<> help "Display the version number"
```
3. Create ParserInfo
> A [ParserInfo](https://hackage.haskell.org/package/optparse-applicative-0.18.1.0/docs/Options-Applicative.html#t:ParserInfo) describes a command line program, used to generate a help screen. — [Options.Applicative](https://hackage.haskell.org/package/optparse-applicative-0.18.1.0/docs/Options-Applicative.html#g:8)
- `optsParserInfo`
Utility function for `Options.Applicative`'s "`info`" that create a `ParserInfo` given a [`Parser`](https://hackage.haskell.org/package/optparse-applicative-0.18.1.0/docs/Options-Applicative.html#t:Parser) and a modifier, where `Parser`s are defined using a [custom data record](#orga7622d9).
```haskell
optsParserInfo :: ParserInfo Opts
optsParserInfo = info (optsParser <**> helper')
$ fullDesc
<> progDesc "Print currently playing song information as JSON"
<> header (progName ++ " - " ++ "Current MPD song information as JSON")
```
4. Custom helper
> Like helper, but with a minimal set of modifiers that can be extended as desired.
>
> ```haskell
> opts :: ParserInfo Sample
> opts = info (sample <**> helperWith (mconcat [
> long "help",
> short 'h',
> help "Show this help text",
> hidden
> ])) mempty
> ```
>
> — source of [Options.Applicative#helper](https://hackage.haskell.org/package/optparse-applicative-0.18.1.0/docs/Options-Applicative.html#v:helper)
Define a helper command that only accepts long `--help`:
```haskell
helper' :: Parser (a -> a)
helper' = helperWith
$ long "help"
-- <> help "Show this help text"
<> hidden -- don't show in help messages
```
### Version.hs
```haskell
module Version ( versionStr,
progName ) where
import Data.Version (showVersion)
import Paths_mpd_current_json (version) -- generated by Cabal
progName :: [Char]
progName = "mpd-current-json"
versionStr :: [Char]
versionStr = progName ++ " version " ++ (showVersion version)
```
### Setup.hs
Allow `runhaskell` to use `cabal`
```haskell
import Distribution.Simple
main = defaultMain
```
## Extra
### mpd-current-json.cabal
```haskell-cabal
cabal-version: 3.0
name: mpd-current-json
-- The package version.
-- See the Haskell package versioning policy (PVP) for standards
-- guiding when and how versions should be incremented.
-- https://pvp.haskell.org
-- PVP summary: +-+------- breaking API changes
-- | | +----- non-breaking API additions
-- | | | +--- code changes with no API change
version: 1.1.0.1
synopsis: Print current MPD song and status as json
-- A longer description of the package.
description: Print currently playing MPD's song metadata and status as JSON
homepage: https://codeberg.org/useless-utils/mpd-current-json
-- A URL where users can report bugs.
-- bug-reports:
license: Unlicense
license-file: UNLICENSE
author: Lucas G
maintainer: g@11xx.org
-- A copyright notice.
-- copyright:
category: Network
extra-source-files: CHANGELOG.md
README.md
source-repository head
type: git
location: https://codeberg.org/useless-utils/mpd-current-json
executable mpd-current-json
main-is: Main.hs
-- Modules included in this executable, other than Main.
other-modules: Options
Paths_mpd_current_json
Version
autogen-modules: Paths_mpd_current_json
-- LANGUAGE extensions used by modules in this package.
-- other-extensions:
build-depends: base ^>=4.16.4.0
, libmpd == 0.10.*
, optparse-applicative == 0.18.*
, aeson == 2.1.*
, bytestring == 0.11.*
, aeson-pretty == 0.8.*
-- Directories containing source files.
hs-source-dirs: src
default-language: Haskell2010
-- [[https://kowainik.github.io/posts/2019-02-06-style-guide#ghc-options][Haskell Style Guide :: Kowainik]]
ghc-options: -Wall
-Wcompat
-Widentities
-Wincomplete-uni-patterns
-Wincomplete-record-updates
-Wredundant-constraints
-Wmissing-export-lists
-Wpartial-fields
-Wmissing-deriving-strategies
-Wunused-packages
-fwrite-ide-info
-hiedir=.hie
```
# Changelog
```markdown
# v1.1.0.1
[comment]: # (2023-10-17)
- Added haddock comments
- Addressed `cabal check` warnings;
- setup for uploading as a Hackage package.
# v1.1.0.0
[comment]: # (2023-06-11)
- Remove `-h` from `--help` and use `-h` for `--host`
- Make `--help` option hidden in the help message
# v1.0.0.0
[comment]: # (2023-06-08)
Initial working version
- Added conditional tags printing, only non-empty values are printed
- Accept host, port and password
- Nested json objects for `status` and `tags`
- Added `elapsed_percent` key shortcut for `elapsed / duration * 100`
# v0.0.1.0
[comment]: # (2023-06-01)
- initial connection and parsing values
- First version. Released on an unsuspecting world.
```