{-# LANGUAGE OverloadedStrings #-} module Network.Radio.BronyRadioGermany.Streaming (-- * Simple information requests getStreamInfo, getTrackInfo, -- * Track streams getTrackList, getHistory, -- * Voting postUpVote, postUpVoteFresh, postDownVote, postDownVoteFresh, -- * AutoDJ requests postAutoDJRequest, postAutoDJTrackRequest, -- * Audio streams radioStream, -- * Errors AutoDjRequestError(..)) where import Control.Exception import Control.Monad import Control.Monad.IO.Class import Control.Monad.Trans import Data.Aeson hiding (encode) import Data.Time.Clock import qualified Data.Text as T import Data.Text.Encoding import Streaming import qualified Streaming.Prelude as S import qualified Data.ByteString.Streaming.Char8 as Q import Data.ByteString.Streaming.HTTP import Data.ByteString.Streaming.Aeson import Network.Radio.BronyRadioGermany.Types as BRG import Network.Radio.BronyRadioGermany.URI as BRG import Network.HTTP.Types.URI import Data.UUID import Data.UUID.V4 import qualified Data.ByteString as BS import qualified Data.CaseInsensitive as CI request :: (FromJSON a, MonadIO m) => T.Text -> m (Maybe a) request uri = handleResponse <$> request' uri where handleResponse (Just (Successful a _ _ _)) = Just a handleResponse _ = Nothing request' :: (FromJSON a, MonadIO m) => T.Text -> m (Maybe (BRG.Response a)) request' uri = liftIO $ do m <- newManager tlsManagerSettings req <- parseRequest (T.unpack uri) withHTTP req m $ \resp -> S.head_ $ decoded $ responseBody resp multipageRequest :: (FromJSON a, MonadIO m) => Int -> (Int -> T.Text) -> Stream (Of a) m () multipageRequest page uri = do mr <- liftIO $ request' $ uri page case mr of Nothing -> return () Just (Successful rs (Just next) _ _) -> do S.each rs `const` head rs -- head forces rs to be a list multipageRequest (page+1) uri Just (Successful rs Nothing _ _) -> S.each rs Just (Errorful msg) -> fail (T.unpack msg) -- | Get information about the given channel getStreamInfo :: MonadIO m => Mountpoint -> m (Maybe StreamInfo) getStreamInfo = request . brgStreamInfoURI -- | Get information about the given track getTrackInfo :: (HasTrackId t, MonadIO m) => t -> m (Maybe TrackInfo) getTrackInfo = request . brgTrackInfoURI -- | Get the AutoDJ's track list getTrackList :: MonadIO m => Maybe T.Text -- ^ Title mask (use % as a wildcard) -> Maybe T.Text -- ^ Artist mask (use % as a wildcard) -> Stream (Of Track) m () getTrackList mtitle martist = do mlist <- request $ brgTrackListURI mtitle martist case mlist of Just list -> S.each list `const` head list -- head forces list to be a list Nothing -> return () -- | Get the song history of the main channel getHistory :: MonadIO m => Maybe UTCTime -- ^ start time -> Maybe UTCTime -- ^ end time -> Maybe T.Text -- ^ title mask (use % as a wildcard) -> Maybe T.Text -- ^ artist mask -> Stream (Of HistoryItem) m () getHistory mstart mend mtitle martist = do end' <- case mend of Just end -> return end Nothing -> liftIO getCurrentTime multipageRequest 1 $ \i -> brgHistoryURI i mstart (Just end') mtitle martist -- | Check Vote status for a given UUID and mountpoint getVoteStatus :: MonadIO m => UUID -> Mountpoint -> m (Maybe VoteCheckResult) getVoteStatus uuid mp = request $ brgVoteCheckURI uuid mp -- | Post an upvote using the given UUID (caution: even though this is logically a post, the HTTP method is GET) postUpVote :: MonadIO m => UUID -> Mountpoint -> m (Maybe VoteResult) postUpVote uuid mp = request $ brgUpVoteURI uuid mp -- | Post a downvote using the given UUID (caution: even though this is logically a post, the HTTP method is GET) postDownVote :: MonadIO m => UUID -> Mountpoint -> m (Maybe VoteResult) postDownVote uuid mp = request $ brgDownVoteURI uuid mp -- | Post an upvote using a fresh UUID postUpVoteFresh :: MonadIO m => Mountpoint -> m (Maybe VoteResult) postUpVoteFresh mp = do uuid <- liftIO nextRandom postUpVote uuid mp -- | Post a downvote using a fresh UUID postDownVoteFresh :: MonadIO m => Mountpoint -> m (Maybe VoteResult) postDownVoteFresh mp = do uuid <- liftIO nextRandom postDownVote uuid mp -- | Thrown if the AutoDJ song request has been rejected, i.e. due to quota violation data AutoDjRequestError = AutoDjRequestRejected deriving (Eq,Show,Read) instance Exception AutoDjRequestError -- | Request a song from the AutoDJ. Throws AutoDjRequestRejected if the request is rejected by the server, i.e. the quota is violated postAutoDJRequest :: (MonadIO m,MonadThrow m) => T.Text -- ^ your nickname -> T.Text -- ^ track title -> T.Text -- ^ track artist -> m () postAutoDJRequest nick title artist = do let body = renderQuery False [("title", Just (encodeUtf8 title)), ("artist", Just (encodeUtf8 artist)), ("nickname", Just (encodeUtf8 nick))] req <- parseRequest $ T.unpack brgAutoDJRequestURI m <- liftIO $ newManager tlsManagerSettings uuid <- liftIO nextRandom let req' = req{ method = "POST", requestBody = RequestBodyBS body} req'' = addRequestHeader "cookie" ("brg-player-voting-uuid="<>toASCIIBytes uuid) $ addRequestHeader "content-type" "application/x-www-form-urlencoded; charset=UTF-8" $ addRequestHeader "referer" "https://www.bronyradiogermany.com/request-v2/include/request.php" $ addRequestHeader "origin" "https://www.bronyradiogermany.com" $ addRequestHeader "x-requested-with" "XMLHttpRequest" req' resp <- liftIO $ withHTTP req'' m $ \resp -> Q.toStrict_ $ responseBody resp if resp == "Request wurde eingereicht." then return () else throwM AutoDjRequestRejected -- | Request a song from the AutoDJ postAutoDJTrackRequest :: (MonadIO m, MonadThrow m, HasTrack t) => T.Text -- ^ your nickname -> t -- ^ requested track -> m () postAutoDJTrackRequest nick track = postAutoDJRequest nick (trackTitle track) (trackArtist track) -- | Raw radio stream. Use an audio library to extract frames, or pipe it into an audio device radioStream :: MonadResource m => Mountpoint -> Q.ByteString m () radioStream mp = do req <- parseRequest $ T.unpack $ brgStreamURI mp m <- liftIO $ newManager tlsManagerSettings resp <- lift $ http req m responseBody resp addRequestHeader :: BS.ByteString -> BS.ByteString -> Request -> Request addRequestHeader key val req = let key' = CI.mk key h = (key', val) in req{requestHeaders= h : requestHeaders req}