packages feed

musicScroll-0.3.0.0: src/MusicScroll/Pipeline.hs

{-# language PatternSynonyms #-}
module MusicScroll.Pipeline where

import Data.Foldable (traverse_)
import Control.Concurrent.Async
import Control.Concurrent.MVar
import Control.Concurrent.STM.TVar (TVar)
import Database.SQLite.Simple
import Pipes.Concurrent
import Pipes
import qualified Pipes.Prelude as PP
import Data.Functor.Contravariant.Divisible

import MusicScroll.LyricsPipeline
import MusicScroll.UIContext (UIContext(..), dischargeOnUI, dischargeOnUISingle)
import MusicScroll.TrackInfo (TrackIdentifier, cleanTrack,
                              pattern OnlyMissingArtist)
import MusicScroll.TrackSuplement

data DBusSignal = Song TrackIdentifier | Error ErrorCause | NoInfo
  deriving (Show)

data AppState = AppState
  { apUI :: UIContext
  , apDB :: MVar Connection -- ^ Enforce mutual exclusion zone
  , apSupl :: TVar (Maybe TrackSuplement)
  , apStaticinput :: (Input TrackIdentifier, Input ErrorCause)
  , apEphemeralInput :: Producer DBusSignal IO () -- ^ Emits only once.
  }

staticPipeline :: AppState -> IO ()
staticPipeline (AppState ctx db svar (dbusTrack, dbusErr) _) =
  let songP = fromInput dbusTrack >-> addSuplArtist svar >-> noRepeatedSongs
              >-> cleanTrack
      errP  = fromInput dbusErr
      errorPipe = errP >-> PP.map ErrorOn >-> dischargeOnUI ctx
  in withAsync (songPipe db ctx songP) $ \songA ->
       withAsync (runEffect errorPipe) $ \errorA ->
         void $ waitAnyCancel [ songA, errorA ]

songPipe :: MVar Connection -> UIContext -> Producer TrackIdentifier IO () -> IO ()
songPipe db ctx = PP.foldM go (pure Nothing) (traverse_ cancel)
  where
    go :: Maybe (Async ()) -> TrackIdentifier -> IO (Maybe (Async ()))
    go asyncVar track =
      do traverse_ cancel asyncVar
         let network = yield track >-> getLyricsFromAnywhere db
                         >-> saveOnDb db >-> dischargeOnUI ctx
         Just <$> async (runEffect network)

suplementPipeline :: TrackSuplement -> AppState -> IO ()
suplementPipeline supl (AppState ctx db _ _ signal) =
  let justTracks a = case a of { Song track -> Just track ; _ -> Nothing }
      songP = signal >-> PP.mapFoldable justTracks
      pipeline = songP >-> mergeSuplement supl >-> getLyricsOnlyFromWeb
          >-> saveOnDb db >-> dischargeOnUISingle ctx
  in runEffect pipeline

debugPS :: Show a => String -> Pipe a a IO ()
debugPS tag = PP.chain (\a -> putStr tag *> print a)

-- | Use the `Output` Divisible instance to create a network. These are
--   1) An output for songs.
--   2) One for errors
--   3) A merge from the previous two.
-- The last one is special as it's non-work-stealing, so we can pass it to
-- multiple listeners and all will receive a signal. But we have to be
-- careful of only taking a single value of it, as it basically a `TVar a`.
musicSpawn :: IO ( Input TrackIdentifier, Input ErrorCause
                 , Producer DBusSignal IO ()
                 , Output TrackIdentifier, Output ErrorCause)
musicSpawn = do
  (protoTrackout, trackin) <- spawn (newest 1)
  (protoErrorout, errorin) <- spawn (newest 1)
  (allout, allin) <- spawn (latest NoInfo)

  let realTrackout = divide (\a -> (a, Song a)) protoTrackout allout
      realErrorout = divide (\a -> (a, Error a)) protoErrorout allout
      singleProd = fromInput allin >-> PP.take 1

  pure $ (trackin, errorin, singleProd, realTrackout, realErrorout)

addSuplArtist :: TVar (Maybe TrackSuplement) -> Pipe TrackIdentifier TrackIdentifier IO a
addSuplArtist svar = PP.mapM go
  where go :: TrackIdentifier -> IO TrackIdentifier
        go signal@(Left OnlyMissingArtist) = atomically (readTVar svar) >>=
            pure . maybe signal (flip suplementOnlyArtist signal)
        go other = pure other