vimeta-0.2.0.0: src/Vimeta/UI/Common/TV.hs
{-# LANGUAGE OverloadedStrings #-}
{-
This file is part of the vimeta package. It is subject to the license
terms in the LICENSE file found in the top-level directory of this
distribution and at git://pmade.com/vimeta/LICENSE. No part of the
vimeta package, including this file, may be copied, modified,
propagated, or distributed except according to the terms contained in
the LICENSE file.
-}
--------------------------------------------------------------------------------
module Vimeta.UI.Common.TV
( EpisodeSpec (..)
, tagWithMappingFile
, tagWithSpec
, tagWithFileOrder
, episodeSpec
) where
--------------------------------------------------------------------------------
import Control.Applicative
import Control.Monad
import Control.Monad.IO.Class
import Data.Either
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Monoid
import Data.Text (Text)
import qualified Data.Text as Text
import Network.API.TheMovieDB
import Text.Parsec
--------------------------------------------------------------------------------
-- Local imports:
import Vimeta.Core
import qualified Vimeta.Core.MappingFile as MF
--------------------------------------------------------------------------------
-- The following is a kludge to avoid the "redundant import" warning
-- when using GHC >= 7.10.x. This should be removed after we decide
-- to stop supporting GHC < 7.10.x.
import Prelude
--------------------------------------------------------------------------------
-- | A simple way to specify a single episode.
data EpisodeSpec = EpisodeSpec Int Int deriving (Show, Eq, Ord)
--------------------------------------------------------------------------------
-- | An episode along with the season.
data EpisodeCtx = EpisodeCtx TV Season Episode deriving (Show, Eq, Ord)
--------------------------------------------------------------------------------
-- | Tag a single file with the given 'EpisodeCtx'.
tagFileWithEpisode :: (MonadIO m) => FilePath -> EpisodeCtx -> Vimeta m ()
tagFileWithEpisode file (EpisodeCtx tv season episode) = do
context <- ask
let format = configFormatTV (ctxConfig context)
tmdbCfg = ctxTMDBCfg context
withArtwork (seasonPosterURLs tmdbCfg season) $ \artwork ->
case fromFormatString (formatMap artwork) "config.tv" format of
Left e -> die e
Right cmd -> tagFile (Text.unpack cmd)
where
formatMap :: Maybe FilePath -> FormatTable
formatMap artwork = Map.fromList
[ ('Y', formatFullDate $ episodeAirDate episode)
, ('a', Text.pack <$> artwork)
, ('d', Just (Text.take 255 $ episodeOverview episode))
, ('e', Just . Text.pack . show $ episodeNumber episode)
, ('f', Just $ Text.pack file)
, ('n', Just $ tvName tv)
, ('s', Just . Text.pack . show $ episodeSeasonNumber episode)
, ('t', Just $ episodeName episode)
, ('y', formatYear $ episodeAirDate episode)
]
--------------------------------------------------------------------------------
-- | Handy tagging function using mapping files.
tagWithMappingFile :: (MonadIO m) => TV -> FilePath -> Vimeta m ()
tagWithMappingFile tv filename = do
mapping <- parseMappingFile filename episodeSpecParser
tagWithSpec tv mapping
--------------------------------------------------------------------------------
-- | Tag all of the given files with their matching 'EpisodeSpec'.
tagWithSpec :: (MonadIO m)
=> TV -- ^ Full TV series.
-> [(FilePath, EpisodeSpec)] -- ^ File mapping.
-> Vimeta m ()
tagWithSpec tv specs = do
let unmapped = lefts mapping
taggable = rights mapping
unless (null unmapped) $
die ("the following files can't be mapped to episodes " <>
Text.unpack (badFiles unmapped))
mapM_ (uncurry tagFileWithEpisode) taggable
where
table :: Map EpisodeSpec EpisodeCtx
table = makeTVMap tv
mapping :: [Either (FilePath, EpisodeSpec) (FilePath, EpisodeCtx)]
mapping = map (\(f, s) -> check (Map.lookup s table) f s) specs
check :: Maybe EpisodeCtx -> FilePath -> EpisodeSpec
-> Either (FilePath, EpisodeSpec) (FilePath, EpisodeCtx)
check Nothing f s = Left (f, s)
check (Just e) f _ = Right (f, e)
badFiles :: [(FilePath, EpisodeSpec)] -> Text
badFiles = Text.intercalate "\n" .
map (\(f, s) -> Text.pack f <> " " <> episodeSpecAsText s)
--------------------------------------------------------------------------------
-- | Tag the given files, starting at the given 'EpisodeSpec'.
tagWithFileOrder :: (MonadIO m)
=> TV -- ^ Full TV series.
-> EpisodeSpec -- ^ Starting episode.
-> [FilePath] -- ^ List of files to tag.
-> Vimeta m ()
tagWithFileOrder tv spec files = tagWithSpec tv mapping
where
mapping :: [(FilePath, EpisodeSpec)]
mapping = zipWith (\f e -> (f, episodeSpecFromCtx e)) files episodes
episodes :: [EpisodeCtx]
episodes = take (length files) $ startingAt spec $ flattenTV tv
--------------------------------------------------------------------------------
-- | Create an 'EpisodeSpec' from an 'Episode'.
episodeSpec :: Episode -> EpisodeSpec
episodeSpec e = EpisodeSpec (episodeSeasonNumber e) (episodeNumber e)
--------------------------------------------------------------------------------
-- | Create an 'EpisodeSpec' from an 'EpisodeCtx'.
episodeSpecFromCtx :: EpisodeCtx -> EpisodeSpec
episodeSpecFromCtx (EpisodeCtx _ _ e) = episodeSpec e
--------------------------------------------------------------------------------
-- | Turn an 'EpisodeSpec' into something that can be printed.
episodeSpecAsText :: EpisodeSpec -> Text
episodeSpecAsText (EpisodeSpec s e) = "S" <> Text.pack (show s) <>
"E" <> Text.pack (show e)
--------------------------------------------------------------------------------
-- | Flatten a TV/Season/Episode tree into a list of episodes.
flattenTV :: TV -> [EpisodeCtx]
flattenTV t = concatMap (\s -> forSeason s (seasonEpisodes s)) (tvSeasons t)
where
forSeason :: Season -> [Episode] -> [EpisodeCtx]
forSeason s = map (EpisodeCtx t s)
--------------------------------------------------------------------------------
-- | Drop all episodes until the matching 'EpisodeSpec' is found.
startingAt :: EpisodeSpec -> [EpisodeCtx] -> [EpisodeCtx]
startingAt spec = dropWhile (\(EpisodeCtx _ _ e)-> spec /= episodeSpec e)
--------------------------------------------------------------------------------
-- | Make an episode look-up table.
makeTVMap :: TV -> Map EpisodeSpec EpisodeCtx
makeTVMap = foldr insert Map.empty . flattenTV
where
insert :: EpisodeCtx -> Map EpisodeSpec EpisodeCtx -> Map EpisodeSpec EpisodeCtx
insert e = Map.insert (episodeSpecFromCtx e) e
--------------------------------------------------------------------------------
episodeSpecParser :: MF.Parser EpisodeSpec
episodeSpecParser =
do void (oneOf "Ss")
season <- many1 digit
void (oneOf "Ee")
episode <- many1 digit
return $ EpisodeSpec (read season) (read episode)
<?> "episode spec (S#E#)"