packages feed

mpd-current-json-3.0.0.0: lib/MPD/Current/JSON/Types.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE NoFieldSelectors #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedRecordDot #-}

module MPD.Current.JSON.Types
    ( Tags(..)
    , TagField(..)
    , Status(..)
    , Playlist(..)
    , File(..)
    , State(..)
    , MPDPath(..)
    , MPDPlaybackState(..)
    , MPDId(..)
    ) where

import Network.MPD qualified as MPD

import Data.Aeson.Types
import Deriving.Aeson
import Data.List qualified as L
import Data.Char
import Control.Applicative
import Data.Text qualified as T
import Data.String


-- | Deriving.Aeson ghost type
data MPDCurrentJSONTag

{- | Custom field label string modifier for Tags

Lowercase @musicbrainz@ fields by separating them with @_@, otherwise
default to CamelToSnake.
-}
instance StringModifier MPDCurrentJSONTag where
  getStringModifier s =
    case L.stripPrefix "musicbrainz" s of
      Just xs -> "musicbrainz_" ++ map toLower xs
      Nothing -> camelTo2 '_' s


{- | Sum type for either a single string or list of strings.

`Network.MPD.sgGetTag' always returns a list of values for the given
`Network.MPD.Metadata', so to make the output JSON only use direct
strings vs an array, use this sum type.
-}
data TagField = SingleTagField !String
              | MultiTagField ![String]
  deriving stock (Show, Eq, Generic)

{- | Store the parsed output of 'getTag'.

Each field represents a supported MPD tag.

@Maybe@ is used so `Deriving.Aeson.OmitNothingFields' can skip fields
that would otherwise be null in the encoded JSON.
-}
data Tags = Tags
  { artist                    :: !(Maybe TagField)
  , artistSort                :: !(Maybe TagField)
  , album                     :: !(Maybe TagField)
  , albumSort                 :: !(Maybe TagField)
  , albumArtist               :: !(Maybe TagField)
  , albumArtistSort           :: !(Maybe TagField)
  , title                     :: !(Maybe TagField)
  , track                     :: !(Maybe TagField)
  , name                      :: !(Maybe TagField)
  , genre                     :: !(Maybe TagField)
  , date                      :: !(Maybe TagField)
  , originalDate              :: !(Maybe TagField)
  , composer                  :: !(Maybe TagField)
  , performer                 :: !(Maybe TagField)
  , conductor                 :: !(Maybe TagField)
  , work                      :: !(Maybe TagField)
  , grouping                  :: !(Maybe TagField)
  , comment                   :: !(Maybe TagField)
  , disc                      :: !(Maybe TagField)
  , label                     :: !(Maybe TagField)
  , musicbrainzArtistId       :: !(Maybe TagField)
  , musicbrainzAlbumId        :: !(Maybe TagField)
  , musicbrainzAlbumartistId  :: !(Maybe TagField)
  , musicbrainzTrackId        :: !(Maybe TagField)
  , musicbrainzReleasetrackId :: !(Maybe TagField)
  , musicbrainzWorkId         :: !(Maybe TagField)
  }
  deriving stock (Show, Eq, Generic)
  deriving (ToJSON, FromJSON) via CustomJSON
  '[ FieldLabelModifier '[ MPDCurrentJSONTag ]
   , OmitNothingFields
   ] Tags

instance ToJSON TagField where
  toJSON :: TagField -> Value
  toJSON (SingleTagField s) = toJSON s
  toJSON (MultiTagField maybeList) = toJSON maybeList

instance FromJSON TagField where
  parseJSON v =
        (SingleTagField <$> parseJSON v)
    <|> (MultiTagField <$> parseJSON v)

data Status = Status
  { state          :: !MPDPlaybackState
  , repeat         :: !Bool
  , random         :: !Bool
  , single         :: !Bool
  , consume        :: !Bool
  , duration       :: !(Maybe MPD.FractionalSeconds)  -- Double
  , elapsed        :: !(Maybe MPD.FractionalSeconds)
  , elapsedPercent :: !(Maybe Double)
  , volume         :: !(Maybe Int)
  , audioFormat    :: !(Maybe (Int, Int, Int))
  , bitrate        :: !(Maybe Int)
  , crossfade      :: !(Maybe Int)
  , mixRampDb      :: !(Maybe Double)
  , mixRampDelay   :: !(Maybe Double)
  , updatingDb     :: !(Maybe Int)
  , error          :: !(Maybe String)
  }
  deriving stock (Show, Eq, Generic)
  deriving (ToJSON, FromJSON) via CustomJSON
  '[ FieldLabelModifier '[ Rename "mixRampDb" "mixramp_db"
                         , Rename "mixRampDelay" "mixramp_delay"
                         , CamelToSnake
                         ]
   , OmitNothingFields
   ] Status

-- | @newtype@ wrapper for otherwise orphan instance. Address warning GHC-90177.
newtype MPDPlaybackState = MPDPlaybackState MPD.PlaybackState
  deriving stock (Show, Eq, Generic)

instance ToJSON MPDPlaybackState where
  toJSON :: MPDPlaybackState -> Value
  toJSON (MPDPlaybackState MPD.Playing) = "playing"
  toJSON (MPDPlaybackState MPD.Paused)  = "paused"
  toJSON (MPDPlaybackState MPD.Stopped) = "stopped"

instance FromJSON MPDPlaybackState where
  parseJSON = withText "MPD.PlaybackState" $ \state -> do
    case state of
      "playing" -> pure (MPDPlaybackState MPD.Playing)
      "paused"  -> pure (MPDPlaybackState MPD.Paused)
      "stopped" -> pure (MPDPlaybackState MPD.Playing)
      _         -> fail $ "Unknown playback state: " ++ show state

data Playlist = Playlist
  { position     :: !(Maybe MPD.Position)
  , nextPosition :: !(Maybe MPD.Position)
  , id           :: !(Maybe MPDId)
  , nextId       :: !(Maybe MPDId)
  , length       :: !Int
  }
  deriving stock (Show, Eq, Generic)
  deriving (ToJSON, FromJSON) via CustomJSON
  '[ FieldLabelModifier '[ CamelToSnake ]
   , OmitNothingFields
   ] Playlist

-- | @newtype@ wrapper for otherwise orphan instance. Address warning GHC-90177.
newtype MPDId = MPDId MPD.Id
  deriving stock (Show, Eq, Generic)

instance ToJSON MPDId where
  toJSON :: MPDId -> Value
  toJSON (MPDId (MPD.Id i)) = toJSON i

instance FromJSON MPDId where
  parseJSON v = MPDId . MPD.Id <$> parseJSON v

data File = File
  { currentFile :: !(Maybe MPDPath)  -- ^ current song file path
  , nextFile    :: !(Maybe MPDPath)  -- ^ next song file path
  }
  deriving stock (Show, Eq, Generic)
  deriving (ToJSON, FromJSON) via CustomJSON
  '[ FieldLabelModifier '[ Rename "currentFile" "filename"
                         , Rename "nextFile" "next_filename" ]
   , OmitNothingFields
   ] File


-- | @newtype@ wrapper for otherwise orphan instance. Address warning GHC-90177.
newtype MPDPath = MPDPath MPD.Path
  deriving stock (Show, Eq, Generic)

instance ToJSON MPDPath where
  toJSON :: MPDPath -> Value
  toJSON (MPDPath p) = toJSON $ MPD.toString p

instance FromJSON MPDPath where
  parseJSON = withText "MPD.Path" $ \path -> do
    pure . MPDPath . fromString . T.unpack $ path

-- | Complete MPD State. Where other states will be stored into and JSON encoded.
data State = State
  { mpdFile     :: !File
  , mpdStatus   :: !Status
  , mpdPlaylist :: !Playlist
  , mpdTags     :: !Tags
  , mpdNextTags :: !(Maybe Tags)
  }
  deriving stock (Show, Eq, Generic)

-- | Custom output for encoded 'State'.
instance ToJSON State where
  toJSON :: State -> Value
  toJSON state = object $
    [ "filename" .= toJSON state.mpdFile.currentFile
    , "status"   .= toJSON state.mpdStatus
    , "playlist" .= toJSON state.mpdPlaylist
    , "tags"     .= state.mpdTags
    ] <> case state.mpdNextTags of
           Nothing -> []
           Just nextTags -> [ "next" .= object
                              [ "filename" .= toJSON state.mpdFile.nextFile
                              , "tags" .= nextTags
                              ] ]