vimeta (empty) → 0.2.0.0
raw patch · 27 files changed
+2241/−0 lines, 27 filesdep +aesondep +basedep +bylinesetup-changed
Dependencies added: aeson, base, byline, bytestring, containers, directory, either, filepath, http-client, http-client-tls, http-types, mtl, old-locale, optparse-applicative, parsec, process, temporary, text, themoviedb, time, time-locale-compat, transformers, vimeta, xdg-basedir, yaml
Files
- CHANGES +11/−0
- LICENSE +26/−0
- README.md +259/−0
- Setup.hs +2/−0
- TODO.org +27/−0
- src/Main.hs +20/−0
- src/Vimeta/Core.hs +26/−0
- src/Vimeta/Core/Cache.hs +95/−0
- src/Vimeta/Core/Config.hs +131/−0
- src/Vimeta/Core/Download.hs +129/−0
- src/Vimeta/Core/Format.hs +164/−0
- src/Vimeta/Core/MappingFile.hs +138/−0
- src/Vimeta/Core/Process.hs +54/−0
- src/Vimeta/Core/Tagger.hs +68/−0
- src/Vimeta/Core/Vimeta.hs +147/−0
- src/Vimeta/UI/CommandLine.hs +85/−0
- src/Vimeta/UI/CommandLine/Common.hs +49/−0
- src/Vimeta/UI/CommandLine/Config.hs +86/−0
- src/Vimeta/UI/CommandLine/Movie.hs +76/−0
- src/Vimeta/UI/CommandLine/TV.hs +90/−0
- src/Vimeta/UI/Common/Movie.hs +50/−0
- src/Vimeta/UI/Common/TV.hs +184/−0
- src/Vimeta/UI/Common/Util.hs +43/−0
- src/Vimeta/UI/Term/Common.hs +53/−0
- src/Vimeta/UI/Term/Movie.hs +65/−0
- src/Vimeta/UI/Term/TV.hs +57/−0
- vimeta.cabal +106/−0
+ CHANGES view
@@ -0,0 +1,11 @@+-*- org -*-+#+title: Version History+#+startup: showall++* 0.2.0.0 (July 22, 2015)++ - Initial public release.++* 0.1.0.0 (Never publicly released)++ - Internal release only.
+ LICENSE view
@@ -0,0 +1,26 @@+Copyright (c) 2013-2015, Peter J. Jones <pjones@devalot.com>+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are+met:++1. Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++2. Redistributions in binary form must reproduce the above copyright+ notice, this list of conditions and the following disclaimer in the+ documentation and/or other materials provided with the+ distribution.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,259 @@+% VIMETA(1) Vimeta User Manual+% Peter J. Jones+% May 19, 2015++# NAME++vimeta - frontend for video metadata tagging tools++# SYNOPSIS++vimeta --version++vimeta config [*options*]++vimeta movie [*options*] *FILE*++vimeta tv [*options*] [*FILE*]...++# DESCRIPTION++Vimeta is a tool to fetch video metadata from the interwebs and update+video files using a tagging tool.++[TheMovieDB][] is used as the source for movie and TV series metadata.+This means that an API key for TheMovieDB is required in order to use+Vimeta. Sign up for an account at TheMovieDB.com and create an API+key. See the *config* command for details on how to create a+configuration file that includes your API key.++[AtomicParsley][] is the default tagging tool used to update the+metadata in a video file. Any video tagging utility can be used as+long as it can be called from the command line. See the CONFIGURATION+section for details on specifying a tagging tool to Vimeta. If you+decide to use AtomicParsley it's highly recommended that you use the+fork maintained by `wez` at <https://bitbucket.org/wez/atomicparsley>.++# OPTIONS++The following command-line options are available to all of the+commands listed in the COMMANDS section.++`--verbose`+: Enable verbose output++`-d`, `--dry-run`+: Don't actually do anything. Instead, print out what would be+ done. Automatically enables `--verbose`.++# COMMANDS++## vimeta \--version++Display the Vimeta version number and then exit.++## vimeta config [*options*]++Create a configuration file for Vimeta.++`-k`, `--key`=*STRING*+: The API key for TheMovieDB to record into the configuration file.++## vimeta movie [*options*] *FILE*++Look up movie metadata from TheMovieDB and tag a video file. Vimeta+will interactively prompt for a movie name and provide a list of+matching movies to choose from.++`-i`, `--id`=*ID*+: Avoid interactive prompting by supplying a movie ID+ assigned by TheMovieDB.com.++## vimeta tv [*options*] [*FILE*]...++Look up TV series metadata from TheMovieDB and tag one or more video+files. Episode video files can be given on the command line or+specified using a mapping file. See MAPPING FILES for more+information about creating a file that maps episodes to video files.++Vimeta will interactively prompt for a search string and then list+matching TV series. The starting season number and starting episode+number should be given on the command line or they will default to 1.+Each video file will be assigned the next episode number in the+series. Vimeta is smart enough to switch to the next season after the+last episode of the specified season.++`-s`, `--season`=*NUMBER*+: Specify the starting season number to tag into the episode files.+ The season number will be incremented after the last episode of a+ season is tagged. If the season number isn't specified it will+ default to 1.++`-e`, `--episode`=*NUMBER*+: Specify the staring episode number. If not specified it defaults+ to 1.++`-m`, `--map`=*FILE*+: Use *FILE* as a mapping file. The mapping file should contain+ two columns, the first specifies the season and episode numbers+ and the second the file name for the episode video file. See the+ MAPPING FILE section for more details about mapping files.++`-i`, `--id`=*ID*+: Avoid interactive prompting by supplying a TV series ID+ assigned by TheMovieDB.com.++# CONFIGURATION++The Vimeta configuration file is a YAML file containing a few keys.+You should begin by creating a default configuration file using the+*config* command.++## Description of Keys++`tmdb_key`+: The API key issued by TheMovieDB.++`cmd_movie`+: A format string used to execute a tagging tool for movie files.+ See FORMAT STRINGS for more information.++`cmd_tv`+: A format string used to execute a tagging tool for TV episode+ files. See FORMAT STRINGS for more information.++# FORMAT STRINGS++## Description++Vimeta uses format strings to specify the command to run in order to+tag video files, and the arguments needed to pass metadata to the+tagging utility. These format strings are similar to those used with+the `printf` family of functions and utilities.++In format stings, the percent character (`%`), followed by a single+character, is replaced by one of the metadata attributes. Percent+characters are removed and do not appear in the output string.+Therefore, if you need a literal percent character to appear in the+output string, you should use two percent characters (`%%`).++Format strings in Vimeta can also contain conditional parts. These+are introduced with the `%{` format specifier, which must be+terminated with the `%}` format specifier. The text and any format+specifiers enclosed inside the opening and closing brackets are only+injected into the output string if all of the enclosed format+specifiers have valid values.++As an example, not all movies have release dates. Supplying a+command-line flag along with a blank release date to a tagging tool+will most likely cause a problem. It would be best to avoid using the+command-line flag altogether. The following example does just this:++ somecommand %{ --release-date %Y %}++Here, `%Y` stands in for the full release date. If it's not present+in the metadata then none of the text between the brackets will be+injected into the output string. If multiple format specifiers are+used, all of them have to be present in the metadata for any of the+text between the brackets to be included in the output text.++## Format Specifiers for Movies++The following format specifiers are available for tagging movie files.+Not all specifiers are guaranteed to be present. Consider using+conditional specifiers.++`%Y`+: Full release date in XML schema notation.++`%a`+: Path to a temporary file containing a movie poster image file.++`%d`+: Movie description (summary text, truncated to 255 characters).++`%f`+: Path to the video file to be tagged.++`%g`+: Name of first listed genre.++`%t`+: Movie title.++`%y`+: Release year.++## Format Specifiers for Episodes++The following format specifiers are available for tagging TV episode+files. Not all specifiers are guaranteed to be present. Consider+using conditional specifiers.++`%Y`+: Full air date in XML schema notation.++`%a`+: Path to a temporary file containing a season poster image file.++`%d`+: Episode description (summary text, truncated to 255 characters).++`%e`+: Episode number.++`%f`+: Path to the video file to be tagged.++`%n`+: Name of the TV series.++`%s`+: Season number.++`%t`+: Episode name.++`%y`+: Year that the episode aired.++# MAPPING FILES++Vimeta can use mapping files that specify hints about metadata and+which video files to tag. For example, the *tv* command can use+mapping files to link video files to specific season and episode+numbers.++Mapping files are plain text files that contain two columns separated+by whitespace. Comments and blank lines are ignored. Comments begin+with a pound sign (`#`) and continue to the end of the line.++The first column should contain information relevant to the command+using the mapping file. For example, the *tv* command expects the+first column to specify the season and episode number.++The second column is always a file name. If the file doesn't exist+and the file name lacks a file extension, `.m4v` will be added to the+file name automatically.++Example mapping file for the *tv* command:++ # This is a comment.+ S2E3 track03.m4v # Season 2, Episode 3+ S3E1 track01.m4v # Season 3, Episode 1++# EXAMPLES++## Tag Several Episode Files++ vimeta tv -s 3 -e 10 file01.m4v file02.m4v++With this command, Vimeta will prompt for the name of the TV series+and then search TheMovieDB. It will then generate a menu where the+correct TV series can be chosen. Both files will be tagged as+belonging to season 3. The first file is episode 10 and the second+file is episode 11. (For more control over how season and episode+numbers are assigned to files, see the section on MAPPING FILES.)++[TheMovieDB]: http://www.themoviedb.org/+[AtomicParsley]: https://bitbucket.org/wez/atomicparsley
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ TODO.org view
@@ -0,0 +1,27 @@+* Milestone: Release 0.2.0.0+** DONE Cache TMDB.Configuration+ CLOSED: [2015-07-22 Wed 19:27]+** DONE Add support for --version+ CLOSED: [2015-07-21 Tue 21:00]+** DONE Search for FIXME and undefined and remove+ CLOSED: [2015-05-20 Wed 16:30]+** DONE "a search term is required" shouldn't be used twice+ CLOSED: [2015-05-20 Wed 16:23]+** DONE Trim description to 255 characters+ CLOSED: [2015-05-20 Wed 16:19]+** DONE Reorg files and reduce imports+ CLOSED: [2015-05-20 Wed 16:17]+** DONE Write a man page in README.md+ CLOSED: [2015-05-20 Wed 09:04]+** DONE Don't draw menus for empty lists+ CLOSED: [2015-05-19 Tue 15:10]+** DONE Missing space in movie menu before paren+ CLOSED: [2015-05-19 Tue 15:19]+** DONE Print the movie/tv ID after it's found so that someone can reuse it if necessary+ CLOSED: [2015-05-19 Tue 15:19]+** DONE Add interactive search+ CLOSED: [2015-05-09 Sat 13:41]+** DONE One configuration file (and place for keys)+ CLOSED: [2015-05-09 Sat 13:41]+** DONE Use format strings to support other media editors+ CLOSED: [2015-05-09 Sat 13:41]
+ src/Main.hs view
@@ -0,0 +1,20 @@+{-++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 Main (main) where++--------------------------------------------------------------------------------+import qualified Vimeta.UI.CommandLine as CommandLine++--------------------------------------------------------------------------------+main :: IO ()+main = CommandLine.run
+ src/Vimeta/Core.hs view
@@ -0,0 +1,26 @@+{-++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.Core (module Export) where++--------------------------------------------------------------------------------+import Vimeta.Core.Config as Export+import Vimeta.Core.Download as Export+import Vimeta.Core.Format as Export+import Vimeta.Core.MappingFile as Export hiding (Parser)+import Vimeta.Core.Process as Export+import Vimeta.Core.Tagger as Export++--------------------------------------------------------------------------------+-- Only export the public interface from the Vimeta module.+import Vimeta.Core.Vimeta as Export (Vimeta)+import Vimeta.Core.Vimeta as Export hiding (Vimeta)
+ src/Vimeta/Core/Cache.hs view
@@ -0,0 +1,95 @@+{-# 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.++-}++--------------------------------------------------------------------------------+-- | Caching functions.+module Vimeta.Core.Cache+ ( cacheTMDBConfig+ ) where++--------------------------------------------------------------------------------+import Control.Monad.IO.Class+import Data.Aeson as Aeson+import qualified Data.ByteString.Lazy as BL+import Data.Time.Calendar+import Data.Time.Clock+import qualified Network.API.TheMovieDB as TheMovieDB+import System.Directory (doesFileExist, getModificationTime, createDirectoryIfMissing)+import System.Environment.XDG.BaseDir (getUserCacheFile)+import System.FilePath (takeDirectory)++--------------------------------------------------------------------------------+-- | Manage cache file expiration.+data Age = MaxDays Int -- ^ Cap to N days.++--------------------------------------------------------------------------------+ageAsTime :: Age -> UTCTime -> UTCTime+ageAsTime (MaxDays days) now =+ now {utctDay = addDays (fromIntegral (-days)) (utctDay now)}++--------------------------------------------------------------------------------+-- | The file name for catching @TheMovieDB.Configuration@.+tmdbCacheFile :: IO FilePath+tmdbCacheFile = getUserCacheFile "vimeta" "tmdb-config.json"++--------------------------------------------------------------------------------+-- | Produce a cached version of @TheMovieDB.Configuration@ or use+-- the given action to create a cache a new value.+cacheTMDBConfig :: (MonadIO m)+ => m (Either e TheMovieDB.Configuration)+ -> m (Either e TheMovieDB.Configuration)+cacheTMDBConfig action = do+ file <- liftIO tmdbCacheFile+ cache file (MaxDays 3) action++--------------------------------------------------------------------------------+-- | Generic cache reader.+readCache :: (MonadIO m, FromJSON a) => FilePath -> Age -> m (Maybe a)+readCache filename age = do+ exists <- liftIO (doesFileExist filename)+ if not exists then return Nothing else go++ where+ go = do+ now <- liftIO getCurrentTime+ modtime <- liftIO (getModificationTime filename)++ if fresh now modtime+ then Aeson.decode' <$> liftIO (BL.readFile filename)+ else return Nothing++ fresh :: UTCTime -> UTCTime -> Bool+ fresh now modtime = ageAsTime age now <= modtime++--------------------------------------------------------------------------------+-- | Generic cache writer.+writeCache :: (MonadIO m, ToJSON a) => FilePath -> a -> m ()+writeCache filename value = liftIO $ do+ createDirectoryIfMissing True (takeDirectory filename)+ BL.writeFile filename (Aeson.encode value)++--------------------------------------------------------------------------------+-- | Generic caching function.+cache :: (MonadIO m, FromJSON a, ToJSON a)+ => FilePath -- ^ Cache file.+ -> Age -- ^ Age of cache file.+ -> m (Either e a) -- ^ Action to generate new value.+ -> m (Either e a) -- ^ Cached or new value.+cache file age action = do+ cached <- liftIO (readCache file age)++ case cached of+ Just c -> return (Right c)+ Nothing -> do result <- action+ either (const $ return ()) (writeCache file) result+ return result
+ src/Vimeta/Core/Config.hs view
@@ -0,0 +1,131 @@+{-# LANGUAGE OverloadedStrings,ScopedTypeVariables #-}++{-++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.++-}++--------------------------------------------------------------------------------+-- | The configuration file.+module Vimeta.Core.Config+ ( Config (..)+ , defaultConfig+ , configFileName+ , readConfig+ , writeConfig+ ) where++--------------------------------------------------------------------------------+-- Library imports:+import Control.Applicative+import Control.Monad+import Control.Monad.IO.Class+import Control.Monad.Trans.Either+import Data.Aeson+import Data.Aeson.Types (typeMismatch)+import Data.Text (Text)+import Data.Yaml (decodeFileEither, encodeFile)+import Network.API.TheMovieDB (Key)+import System.Directory (doesFileExist, createDirectoryIfMissing)+import System.Environment.XDG.BaseDir (getUserConfigFile)+import System.FilePath (takeDirectory)++--------------------------------------------------------------------------------+-- Local imports:+import Vimeta.Core.Tagger++--------------------------------------------------------------------------------+-- 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++--------------------------------------------------------------------------------+-- | Vimeta configuration.+data Config = Config+ { configTMDBKey :: Key+ , configFormatMovie :: Text+ , configFormatTV :: Text+ , configVerbose :: Bool+ , configDryRun :: Bool+ }++--------------------------------------------------------------------------------+instance FromJSON Config where+ parseJSON (Object v) =+ Config <$> v .: "tmdb_key"+ <*> v .: "cmd_movie"+ <*> v .: "cmd_tv"+ <*> v .:? "verbose" .!= False+ <*> v .:? "dryrun" .!= False+ parseJSON x = typeMismatch "configuration" x++--------------------------------------------------------------------------------+instance ToJSON Config where+ toJSON c = object [ "tmdb_key" .= configTMDBKey c+ , "cmd_movie" .= configFormatMovie c+ , "cmd_tv" .= configFormatTV c+ ]++--------------------------------------------------------------------------------+defaultConfig :: Tagger -> Config+defaultConfig tagger =+ Config { configTMDBKey = "your API key goes here"+ , configFormatMovie = fmtMovie+ , configFormatTV = fmtTV+ , configVerbose = False+ , configDryRun = False+ }++ where (fmtMovie, fmtTV) = formatStringsForTagger tagger++--------------------------------------------------------------------------------+-- | Get the name of the configuration file.+configFileName :: IO FilePath+configFileName = getUserConfigFile "vimeta" "config.yml"++--------------------------------------------------------------------------------+-- | Read the configuration file and return a 'Config' value or an error.+readConfig :: (MonadIO m) => EitherT String m Config+readConfig = do+ filename <- liftIO configFileName+ exists <- liftIO (doesFileExist filename)++ if exists+ then decodeConfig filename+ else left $ missingFile filename++ where+ decodeConfig :: (MonadIO m) => FilePath -> EitherT String m Config+ decodeConfig fn = do result <- liftIO $ decodeFileEither fn+ case result of+ Left e -> left (show e)+ Right a -> return a++ missingFile :: FilePath -> String+ missingFile fn = "no config file found, use the `config' command " +++ "to create " ++ fn++--------------------------------------------------------------------------------+writeConfig :: (MonadIO m) => Config -> EitherT String m FilePath+writeConfig c = do+ (filename, exists) <- liftIO $ do+ fn <- configFileName+ ex <- doesFileExist fn+ return (fn, ex)++ when exists $ left (existError filename)++ liftIO (createDirectoryIfMissing True (takeDirectory filename))+ liftIO (encodeFile filename c)+ return filename++ where+ existError :: FilePath -> String+ existError fn = "please remove the existing config file first: " ++ fn
+ src/Vimeta/Core/Download.hs view
@@ -0,0 +1,129 @@+{-# LANGUAGE OverloadedStrings, ScopedTypeVariables #-}++{-++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.++-}++--------------------------------------------------------------------------------+-- | Utility functions for downloading files.+module Vimeta.Core.Download+ ( withArtwork+ , withDownload+ ) where++--------------------------------------------------------------------------------+-- Library imports:+import qualified Data.ByteString.Lazy as BS+import Data.Maybe+import Data.Monoid+import Data.Text (Text)+import qualified Data.Text as Text+import qualified Network.HTTP.Client as HC+import System.FilePath+import System.IO (Handle, hFlush)+import System.IO.Temp (withSystemTempFile)++--------------------------------------------------------------------------------+-- Local imports:+import Vimeta.Core.Config+import Vimeta.Core.Vimeta++--------------------------------------------------------------------------------+-- | Try to download artwork and run the given function. The+-- function will be passed a 'FilePath' if the artwork was downloaded.+--+-- See the 'withDownload' function for more details.+withArtwork :: (MonadIO m)+ => [Text]+ -> (Maybe FilePath -> Vimeta IO a)+ -> Vimeta m a+withArtwork urls = withDownload (listToMaybe $ candidates urls)+ where+ candidates :: [Text] -> [Text]+ candidates = filter checkExtension . reverse++ checkExtension :: Text -> Bool+ checkExtension = goodExtension . takeExtension . Text.unpack . Text.toLower++ goodExtension :: String -> Bool+ goodExtension ext = ext == ".jpg" || ext == ".png"++--------------------------------------------------------------------------------+-- | Download the given URL to a temporary file and pass the file+-- name to the given function.+--+-- The reason a function needs to be passed to 'withDownload' is the+-- result of using 'withSystemTempFile' to store the downloaded file.+-- The file will be automatically removed after the given function+-- completes.+withDownload :: (MonadIO m)+ => Maybe Text+ -- ^ URL.++ -> (Maybe FilePath -> Vimeta IO a)+ -- ^ Function to call and pass the file name to.++ -> Vimeta m a+ -- ^ Result of above function.++withDownload Nothing f = do+ verbose "no URL to download"+ runIOE $ runVimeta (f Nothing)++withDownload url f = do+ context <- ask++ let dryRun = configDryRun $ ctxConfig context+ manager = ctxManager context++ case (dryRun, url) of+ (True, Nothing) -> verbose "dry-run: nothing to download" >>+ runWithoutTempFile f++ (False, Nothing) -> verbose "nothing to download" >>+ runWithoutTempFile f++ (True, Just u) -> verbose ("dry-run:" <> u) >>+ runWithoutTempFile f++ (False, Just u) -> verbose u >>+ runWithTempFile u manager f++--------------------------------------------------------------------------------+-- | Helper function to run the download action with a temporary file.+runWithTempFile :: (MonadIO m)+ => Text+ -> HC.Manager+ -> (Maybe FilePath -> Vimeta IO a)+ -> Vimeta m a+runWithTempFile url manager vio = do+ context <- ask++ runIOE $ withSystemTempFile "vimeta" $ \name h -> do+ downloadToHandle manager (Text.unpack url) h+ execVimetaWithContext context $ vio (Just name)++--------------------------------------------------------------------------------+-- | Helper function to run an action without needing a temporary file.+runWithoutTempFile :: (MonadIO m)+ => (Maybe FilePath -> Vimeta IO a)+ -> Vimeta m a+runWithoutTempFile vio = do+ context <- ask+ runIOE $ execVimetaWithContext context $ vio Nothing++--------------------------------------------------------------------------------+-- | Helper function to the actual HTTP downloading into a file handle.+downloadToHandle :: HC.Manager -> String -> Handle -> IO ()+downloadToHandle manager url handle = do+ request <- HC.parseUrl url+ response <- HC.httpLbs request manager+ BS.hPut handle (HC.responseBody response)+ hFlush handle
+ src/Vimeta/Core/Format.hs view
@@ -0,0 +1,164 @@+{-# LANGUAGE OverloadedStrings, TupleSections #-}++{-++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.Core.Format+ ( FormatTable+ , fromFormatString+ , formatYear+ , formatFullDate+ ) where++--------------------------------------------------------------------------------+-- Library imports:+import Control.Applicative hiding ((<|>)) -- Use the one from Parsec.+import Control.Monad+import Control.Monad.Reader+import Data.Map (Map)+import qualified Data.Map as Map+import Data.Maybe+import Data.Monoid+import Data.Text (Text)+import qualified Data.Text as Text+import Data.Time (Day(..), formatTime)+import Data.Time.Locale.Compat (defaultTimeLocale)+import System.Process.Internals (translate)+import Text.Parsec++--------------------------------------------------------------------------------+-- 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++--------------------------------------------------------------------------------+-- | Mapping of format characters to their possible replacement text.+type FormatTable = Map Char (Maybe Text)++--------------------------------------------------------------------------------+-- | Syntax tree for format strings.+data Replacement = Replace Char+ -- ^ Replace the given character.++ | Condition [(Text, Replacement)]+ -- ^ Conditional section.++ | EndOfInput+ -- ^ End of input (or condition).++--------------------------------------------------------------------------------+-- | Parser type.+type Parser a = ParsecT Text () (Reader FormatTable) a++--------------------------------------------------------------------------------+-- | Replace format characters prefixed with a @%@ with the+-- replacement text found in the given 'Map'.+fromFormatString :: FormatTable -- ^ Format character mapping.+ -> String -- ^ Name of format string.+ -> Text -- ^ Input text.+ -> Either String Text -- ^ Output text or error.+fromFormatString table name input =+ case runReader (runParserT parseFormatString () name input) table of+ Left e -> Left (show e)+ Right t -> Right t++--------------------------------------------------------------------------------+-- | Format a 'Day' using the XML schema notation.+formatFullDate :: Maybe Day -> Maybe Text+formatFullDate = formatDay "%Y-%m-%dT00:00:00Z"++--------------------------------------------------------------------------------+-- | Format a 'Day' displaying just the year.+formatYear :: Maybe Day -> Maybe Text+formatYear = formatDay "%Y"++--------------------------------------------------------------------------------+formatDay :: String -> Maybe Day -> Maybe Text+formatDay fmt d = Text.pack . formatTime defaultTimeLocale fmt <$> d++--------------------------------------------------------------------------------+parseFormatString :: Parser Text+parseFormatString = manyTill go eof >>= renderFormatString+ where go = findFormatCharacter >>= mkReplacement++--------------------------------------------------------------------------------+-- | Render a format string syntax table as a 'Text' value.+renderFormatString :: [(Text, Replacement)] -> Parser Text+renderFormatString rs = do table <- ask+ return (Text.concat $ map (render table) rs)+ where+ escape :: Text -> Text+ escape = Text.pack . translate . Text.unpack++ findChar :: FormatTable -> Char -> Text+ findChar t c = fromMaybe "" $ join (Map.lookup c t)++ render :: FormatTable -> (Text, Replacement) -> Text+ render tbl (txt, Replace c) = txt <> escape (findChar tbl c)+ render tbl (txt, Condition c) = txt <> renderCondition tbl c+ render _ (txt, EndOfInput) = txt++ renderCondition :: FormatTable -> [(Text, Replacement)] -> Text+ renderCondition tbl conds =+ if all (checkCondition tbl) conds+ then Text.concat $ map (render tbl) conds+ else Text.empty++ checkCondition :: FormatTable -> (Text, Replacement) -> Bool+ checkCondition tbl (_, Replace c) = isJust (join $ Map.lookup c tbl)+ checkCondition tbl (_, Condition c) = all (checkCondition tbl) c+ checkCondition _ (_, EndOfInput) = True++--------------------------------------------------------------------------------+-- | Location a format character preceded by a @'%'@ character.+-- Returns the text leading up to the format character and the+-- character itself.+findFormatCharacter :: Parser (Text, Maybe Char)+findFormatCharacter = do+ beforeText <- Text.pack <$> manyTill anyChar (try eofOrFormatChar)+ formatChar <- try $ (Just <$> anyChar) <|> return Nothing+ return (beforeText, formatChar)++ where+ eofOrFormatChar :: Parser ()+ eofOrFormatChar = eof <|> void (char '%')++--------------------------------------------------------------------------------+-- | Translate the output from 'findFormatCharacter' into a syntax node.+mkReplacement :: (Text, Maybe Char) -> Parser (Text, Replacement)+mkReplacement (beforeText, formatChar) =+ case formatChar of+ Nothing -> return (beforeText, EndOfInput)+ Just '{' -> (beforeText,) <$> (Condition <$> parseConditional)+ Just c -> return (beforeText, Replace c)++--------------------------------------------------------------------------------+-- | Parse a conditional section out of a format string.+parseConditional :: Parser [(Text, Replacement)]+parseConditional = do+ (beforeText, formatChar) <- findFormatCharacter++ case formatChar of+ -- Reached the end of the format string.+ Nothing -> unexpected "end of format string, expected `%}'"++ -- Start another conditional.+ Just '{' -> do other <- parseConditional+ return [(beforeText, Condition other)]++ -- End this conditional.+ Just '}' -> return [(beforeText, EndOfInput)]++ -- Add this replacement to the list, fetch the next one.+ Just c -> do next <- parseConditional+ return ((beforeText, Replace c) : next)
+ src/Vimeta/Core/MappingFile.hs view
@@ -0,0 +1,138 @@+{-# LANGUAGE ScopedTypeVariables #-}++{-++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.++-}++--------------------------------------------------------------------------------+-- | Mapping files can be used to map file names to other information.+module Vimeta.Core.MappingFile+ ( Parser+ , parseMappingFile+ ) where++--------------------------------------------------------------------------------+-- Library imports:+import Control.Applicative hiding ((<|>))+import Control.Monad.Identity+import Data.Char (isSpace)+import Data.Either+import Data.List+import Data.Text (Text)+import qualified Data.Text.IO as Text+import System.Directory (doesFileExist)+import System.FilePath (takeExtension)+import Text.Parsec++--------------------------------------------------------------------------------+-- Local imports:+import Vimeta.Core.Vimeta++--------------------------------------------------------------------------------+-- 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++--------------------------------------------------------------------------------+-- | Parser type.+type Parser a = ParsecT Text () Identity a++--------------------------------------------------------------------------------+-- | Internal token used for parsing.+data Token a = Comment | Entry FilePath a++--------------------------------------------------------------------------------+-- | Parse a mapping file.+parseMappingFile :: (MonadIO m)+ => FilePath -- ^ File name for the mapping file.+ -> Parser a -- ^ Parser for the second column.+ -> Vimeta m [(FilePath, a)]+parseMappingFile filename p = do+ contents <- runIO $ Text.readFile filename++ case runIdentity $ runParserT (mapping p) () filename contents of+ Left e -> die (show e)+ Right m -> checkFileMappingOrDie m++--------------------------------------------------------------------------------+checkFileMappingOrDie :: (MonadIO m)+ => [(FilePath, a)]+ -> Vimeta m [(FilePath, a)]+checkFileMappingOrDie xs =+ do ys <- checkFileMapping xs+ if null (lefts ys)+ then return (rights ys)+ else die $ report (lefts ys)+ where+ report :: [(FilePath, a)] -> String+ report fs = "the following files are listed in the mapping file " +++ "but they don't exist: \n" ++ intercalate "\n" (map fst fs)++--------------------------------------------------------------------------------+-- | Checks to see that all of the file names mentioned exist. If a+-- file doesn't exist the @m4v@ file extension is added to it and the+-- existence checking happens again.+checkFileMapping :: (MonadIO m)+ => [(FilePath, a)] -- ^ The mapping.+ -> Vimeta m [Either (FilePath, a) (FilePath, a)]+checkFileMapping = mapM checkFile where+ checkFile :: (MonadIO m) => (FilePath, a)+ -> Vimeta m (Either (FilePath, a) (FilePath, a))+ checkFile f@(filename, a) = do+ let ext = takeExtension filename+ exists <- runIO (doesFileExist filename)++ case exists of+ False | null ext -> checkFile (filename ++ ".m4v", a)+ | otherwise -> return $ Left f+ True -> return $ Right f++--------------------------------------------------------------------------------+-- | The actual file parser.+mapping :: Parser a -> Parser [(FilePath, a)]+mapping p = entries <$> manyTill (whitespace <|> comment <|> fileName p) eof+ where+ entries :: [Token a] -> [(FilePath, a)]+ entries = concatMap extract . filter predicate++ predicate :: Token a -> Bool+ predicate (Entry _ _) = True+ predicate Comment = False++ extract :: Token a -> [(FilePath, a)]+ extract (Entry f a) = [(f, a)]+ extract Comment = []++--------------------------------------------------------------------------------+-- | Parse a file name followed by whatever the second column parser+-- extracts.+fileName :: Parser a -> Parser (Token a)+fileName p =+ do first <- anyChar+ others <- manyTill anyChar (lookAhead space)+ a <- spaceWithoutNewline >> p+ return $ Entry (first:others) a+ <?> "filename and mapping"++--------------------------------------------------------------------------------+-- | Skip whitespace.+whitespace :: Parser (Token a)+whitespace = skipMany1 space >> return Comment++--------------------------------------------------------------------------------+-- | Like whitespace, but doesn't span multiple lines.+spaceWithoutNewline :: Parser ()+spaceWithoutNewline = skipMany1 $ satisfy (\c -> isSpace c && c /= '\n')++--------------------------------------------------------------------------------+-- | Skip comments.+comment :: Parser (Token a)+comment = (char '#' >> manyTill anyChar newline >> return Comment) <?> "comment"
+ src/Vimeta/Core/Process.hs view
@@ -0,0 +1,54 @@+{-# 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.++-}++--------------------------------------------------------------------------------+-- | Utility functions for running external commands.+module Vimeta.Core.Process+ ( tagFile+ ) where++--------------------------------------------------------------------------------+-- Library imports:+import Control.Applicative+import qualified Data.Text as Text+import System.Exit hiding (die)+import System.Process++--------------------------------------------------------------------------------+-- Local imports:+import Vimeta.Core.Config+import Vimeta.Core.Vimeta++--------------------------------------------------------------------------------+-- 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++--------------------------------------------------------------------------------+-- | Run the tagging command unless dry-run mode is in effect.+tagFile :: String -> Vimeta IO ()+tagFile cmd = do+ dryRun <- configDryRun <$> asks ctxConfig+ if dryRun then doDryRun else doRealRun++ where+ doDryRun :: Vimeta IO ()+ doDryRun = verbose "dry run: skipping tagging command" >>+ verbose (Text.pack cmd)++ doRealRun :: Vimeta IO ()+ doRealRun = do code <- liftIO (spawnCommand cmd >>= waitForProcess)+ case code of+ ExitSuccess -> return ()+ ExitFailure _ -> die ("command failed: " ++ cmd)
+ src/Vimeta/Core/Tagger.hs view
@@ -0,0 +1,68 @@+{-# 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.Core.Tagger+ ( Tagger (..)+ , formatStringsForTagger+ ) where++--------------------------------------------------------------------------------+import Data.Text (Text)+import qualified Data.Text as Text++--------------------------------------------------------------------------------+data Tagger = AtomicParsley++--------------------------------------------------------------------------------+formatStringsForTagger :: Tagger -> (Text, Text)+formatStringsForTagger AtomicParsley = (apMovie, apTV)++--------------------------------------------------------------------------------+-- | Common strings for AtomicParsley.+apPrefix, apSuffix :: Text+apPrefix = "AtomicParsley"+apSuffix = "--overWrite"++--------------------------------------------------------------------------------+-- | Format string for movies.+apMovie :: Text+apMovie = Text.intercalate " "+ [ apPrefix+ , "%f"+ , "--stik value=9"+ , "%{--year %Y%}"+ , "--title %t"+ , "--description %d"+ , "%{--genre %G%}"+ , "--artwork REMOVE_ALL %{--artwork %a%}"+ , apSuffix+ ]++--------------------------------------------------------------------------------+-- | Format string for TV episodes.+apTV :: Text+apTV = Text.intercalate " "+ [ apPrefix+ , "%f"+ , "--stik 'TV Show'"+ , "%{--year %Y%}"+ , "--title %t"+ , "--description %d"+ , "--TVShowName %n"+ , "--TVSeasonNum %s"+ , "--TVEpisodeNum %e"+ , "--tracknum %e"+ , "--artwork REMOVE_ALL %{--artwork %a%}"+ , apSuffix+ ]
+ src/Vimeta/Core/Vimeta.hs view
@@ -0,0 +1,147 @@+{-# LANGUAGE GeneralizedNewtypeDeriving, ScopedTypeVariables #-}++{-++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.Core.Vimeta+ ( Vimeta (..)+ , Context (..)+ , MonadIO+ , die+ , runIO+ , runIOE+ , tmdb+ , verbose+ , execVimetaWithContext+ , execVimeta+ , runVimeta+ , ask+ , asks+ , liftIO+ ) where++--------------------------------------------------------------------------------+-- Library imports:+import Control.Applicative+import Control.Exception+import Control.Monad.Reader+import Control.Monad.Trans.Either+import Data.Text (Text)+import qualified Data.Text.IO as Text+import Network.API.TheMovieDB (TheMovieDB, Key, runTheMovieDBWithManager)+import qualified Network.API.TheMovieDB as TheMovieDB+import Network.HTTP.Client (Manager, newManager)+import Network.HTTP.Client.TLS (tlsManagerSettings)+import System.IO (Handle, stdout)++--------------------------------------------------------------------------------+-- Local imports:+import Vimeta.Core.Cache+import Vimeta.Core.Config++--------------------------------------------------------------------------------+-- 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++--------------------------------------------------------------------------------+data Context = Context+ { ctxManager :: Manager+ , ctxConfig :: Config+ , ctxTMDBCfg :: TheMovieDB.Configuration+ , ctxVerboseH :: Handle+ }++--------------------------------------------------------------------------------+newtype Vimeta m a =+ Vimeta {unV :: ReaderT Context (EitherT String m) a}+ deriving (Functor, Applicative, Monad, MonadIO, MonadReader Context)++--------------------------------------------------------------------------------+-- | Terminate a 'Vimeta' session with an error message.+die :: (Monad m) => String -> Vimeta m a+die message = Vimeta $ lift (left message)++--------------------------------------------------------------------------------+runIO :: (MonadIO m) => IO a -> Vimeta m a+runIO io = liftIO (try io) >>= sinkIO+ where sinkIO :: (Monad m) => Either SomeException a -> Vimeta m a+ sinkIO (Left e) = die (show e)+ sinkIO (Right a) = return a++--------------------------------------------------------------------------------+runIOE :: (MonadIO m) => IO (Either String a) -> Vimeta m a+runIOE io = runIO io >>= either (die . show) return++--------------------------------------------------------------------------------+-- | Run a 'TheMovieDB' operation.+tmdb :: (MonadIO m) => TheMovieDB a -> Vimeta m a+tmdb t = do+ context' <- ask++ let manager = ctxManager context'+ key = configTMDBKey (ctxConfig context')++ result <- liftIO (runTheMovieDBWithManager manager key t)++ case result of+ Left e -> die (show e)+ Right r -> return r++--------------------------------------------------------------------------------+verbose :: (MonadIO m) => Text -> Vimeta m ()+verbose msg = do+ context <- ask++ let okay = configVerbose (ctxConfig context) ||+ configDryRun (ctxConfig context)++ when okay $ liftIO $ Text.hPutStrLn (ctxVerboseH context) msg++--------------------------------------------------------------------------------+loadTMDBConfig :: (MonadIO m) => Manager -> Key -> EitherT String m TheMovieDB.Configuration+loadTMDBConfig manager key = do+ result <- cacheTMDBConfig (liftIO $ runTheMovieDBWithManager manager key TheMovieDB.config)++ case result of+ Left e -> left (show e)+ Right c -> return c++--------------------------------------------------------------------------------+-- | Very primitive way of running a 'Vimeta' value with the given 'Context'.+-- Mostly useful for running vimeta action within another vimeta+-- action.+execVimetaWithContext :: (MonadIO m)+ => Context+ -> Vimeta m a+ -> m (Either String a)+execVimetaWithContext context vimeta =+ runEitherT $ runReaderT (unV vimeta) context++--------------------------------------------------------------------------------+-- | Run a 'Vimeta' operation after loading the configuration file+-- from disk.+execVimeta :: (MonadIO m)+ => (Config -> Config) -- ^ Modify configuration before running.+ -> Vimeta m a -- ^ The Vimeta value to execute.+ -> m (Either String a) -- ^ The result.+execVimeta cf vimeta = runEitherT $ do+ config <- cf <$> readConfig+ manager <- liftIO $ newManager tlsManagerSettings+ tc <- loadTMDBConfig manager (configTMDBKey config)+ EitherT $ execVimetaWithContext (Context manager config tc stdout) vimeta++--------------------------------------------------------------------------------+-- | Simple wrapper around 'execVimeta'.+runVimeta :: (MonadIO m) => Vimeta m a -> m (Either String a)+runVimeta = execVimeta id
+ src/Vimeta/UI/CommandLine.hs view
@@ -0,0 +1,85 @@+{-++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.CommandLine (run) where++--------------------------------------------------------------------------------+import Data.Monoid+import Data.Version (showVersion)+import Options.Applicative+import System.Exit++--------------------------------------------------------------------------------+import qualified Vimeta.UI.CommandLine.Config as Config+import qualified Vimeta.UI.CommandLine.Movie as Movie+import qualified Vimeta.UI.CommandLine.TV as TV++--------------------------------------------------------------------------------+-- Cabal generated source files:+import Paths_vimeta (version)++--------------------------------------------------------------------------------+-- 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++--------------------------------------------------------------------------------+data Command = CmdVersion+ | CmdConfig Config.Options+ | CmdMovie Movie.Options+ | CmdTV TV.Options++--------------------------------------------------------------------------------+optionsParser :: Parser Command+optionsParser = verbose <|> commands+ where+ verbose =+ flag' CmdVersion+ (long "version" <> help "Print version and exit")++ commands =+ subparser $ mconcat [config, movie, tv]++ config =+ command "config"+ (info (CmdConfig <$> Config.optionsParser) (progDesc configDesc))++ movie =+ command "movie"+ (info (CmdMovie <$> Movie.optionsParser) (progDesc movieDesc))++ tv =+ command "tv"+ (info (CmdTV <$> TV.optionsParser) (progDesc tvDesc))++ configDesc =+ "Create a new configuration file"++ movieDesc =+ "Tag a movie file using data from TheMovieDB.org"++ tvDesc =+ "Tag episode files using data from TheMovieDB.org"++--------------------------------------------------------------------------------+run :: IO ()+run = do+ options <- execParser $ info (optionsParser <**> helper) idm++ case options of+ CmdVersion -> putStrLn (showVersion version)+ CmdConfig o -> Config.run o+ CmdMovie o -> Movie.run o+ CmdTV o -> TV.run o++ exitSuccess
+ src/Vimeta/UI/CommandLine/Common.hs view
@@ -0,0 +1,49 @@+{-# 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.++-}++--------------------------------------------------------------------------------+-- | Common types/functions used in the command line interface.+module Vimeta.UI.CommandLine.Common+ ( CommonOptions+ , commonOptions+ , updateConfig+ ) where++--------------------------------------------------------------------------------+import Options.Applicative+import Vimeta.Core++--------------------------------------------------------------------------------+-- | Common command line options among all of the apps.+data CommonOptions = CommonOptions+ { optsVerbose :: Bool+ , optsDryRun :: Bool+ }++--------------------------------------------------------------------------------+-- | Common option parser.+commonOptions :: Parser CommonOptions+commonOptions = CommonOptions <$> switch infoVerbose+ <*> switch infoDryRun++ where+ infoVerbose = long "verbose" <> help "Enable verbose output"+ infoDryRun = short 'd' <> long "dry-run" <>+ help "Don't tag files, implies --verbose"++--------------------------------------------------------------------------------+-- | Update the configuration file base on the common command line options.+updateConfig :: CommonOptions -> Config -> Config+updateConfig o c = c { configVerbose = configVerbose c || optsVerbose o+ , configDryRun = configDryRun c || optsDryRun o+ }
+ src/Vimeta/UI/CommandLine/Config.hs view
@@ -0,0 +1,86 @@+{-# 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.CommandLine.Config+ ( Options+ , optionsParser+ , run+ ) where++--------------------------------------------------------------------------------+import Control.Monad+import Control.Monad.Trans.Either+import Data.Monoid+import Data.Text (Text)+import qualified Data.Text as Text+import Options.Applicative+import System.Console.Byline+import System.Exit+import Vimeta.Core++--------------------------------------------------------------------------------+-- 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++--------------------------------------------------------------------------------+data Options = Options+ { optsKey :: Maybe Text+ , optsTagger :: Tagger+ }++--------------------------------------------------------------------------------+optionsParser :: Parser Options+optionsParser = Options <$> optional (Text.pack <$> strOption getKey)+ <*> pure AtomicParsley++ where+ -- Parser options for @optsKey@+ getKey = mconcat [ short 'k'+ , long "key"+ , metavar "KEY"+ , help "Set the API key to KEY"+ ]++--------------------------------------------------------------------------------+run :: Options -> IO ()+run opts = do+ let def = defaultConfig (optsTagger opts)+ config = case optsKey opts of+ Nothing -> def+ Just k -> def {configTMDBKey = k}++ result <- runEitherT (app opts config)++ case result of+ Left e -> byline Error e >> exitFailure+ Right (Just w) -> byline Warning w+ Right Nothing -> return ()++ where+ byline :: ReportType -> String -> IO ()+ byline rt = void . runByline . reportLn rt . text . Text.pack++--------------------------------------------------------------------------------+app :: Options -> Config -> EitherT String IO (Maybe String)+app opts config = do+ filename <- writeConfig config++ return $ case optsKey opts of+ Just _ -> Nothing -- No warnings.+ Nothing -> Just (missingKey filename)++ where+ missingKey = ("please edit the config file and set the API key: " ++)
+ src/Vimeta/UI/CommandLine/Movie.hs view
@@ -0,0 +1,76 @@+{-# 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.CommandLine.Movie+ ( Options+ , optionsParser+ , run+ ) where++--------------------------------------------------------------------------------+import Data.Monoid+import Data.Text (Text)+import qualified Data.Text as Text+import Network.API.TheMovieDB+import Options.Applicative+import System.FilePath+import Vimeta.Core+import Vimeta.UI.CommandLine.Common+import Vimeta.UI.Common.Movie+import Vimeta.UI.Term.Common+import Vimeta.UI.Term.Movie++--------------------------------------------------------------------------------+-- 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++--------------------------------------------------------------------------------+data Options = Options+ { optsMovieID :: Maybe ItemID+ , optsFile :: FilePath+ , optsCommon :: CommonOptions+ }++--------------------------------------------------------------------------------+optionsParser :: Parser Options+optionsParser = Options <$> optional (option auto getMovieID)+ <*> argument str (metavar "FILE")+ <*> commonOptions++ where+ -- Parser options for @optsMovieID@.+ getMovieID = mconcat [ short 'i'+ , long "id"+ , metavar "ID"+ , help "Movie ID assigned by TheMovieDB.org"+ ]++--------------------------------------------------------------------------------+run :: Options -> IO ()+run opts = execVimetaBylineApp (updateConfig $ optsCommon opts) $+ case optsMovieID opts of+ Just mid -> do+ movie <- tmdb (fetchMovie mid)+ tagMovie (optsFile opts) movie++ Nothing -> do+ movie <- movieSearch initialTitle+ tagMovie (optsFile opts) movie++ where+ -- Calculate an initial search title from the file name.+ initialTitle :: Text+ initialTitle = Text.pack $ dropExtension (takeFileName $ optsFile opts)
+ src/Vimeta/UI/CommandLine/TV.hs view
@@ -0,0 +1,90 @@+{-# 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.CommandLine.TV+ ( Options+ , optionsParser+ , run+ ) where++--------------------------------------------------------------------------------+import Control.Monad+import Network.API.TheMovieDB+import Options.Applicative+import Vimeta.Core+import Vimeta.UI.CommandLine.Common+import Vimeta.UI.Common.TV+import Vimeta.UI.Term.Common+import Vimeta.UI.Term.TV++--------------------------------------------------------------------------------+data Options = Options+ { optsTVID :: Maybe ItemID+ , optsStartSeason :: Maybe Int+ , optsStartEpisode :: Maybe Int+ , optsMappingFile :: Maybe FilePath+ , optsFiles :: [FilePath]+ , optsCommon :: CommonOptions+ }++--------------------------------------------------------------------------------+optionsParser :: Parser Options+optionsParser = Options <$> optional (option auto infoTVID)+ <*> optional (option auto infoStartSeason)+ <*> optional (option auto infoStartEpisode)+ <*> optional (strOption infoMappingFile)+ <*> many (argument str (metavar "[FILE...]"))+ <*> commonOptions+ where+ infoTVID =+ short 'i' <> long "id" <> metavar "ID" <>+ help "Series ID assigned by TheMovieDB.org"++ infoStartSeason =+ short 's' <> long "season" <> metavar "NUM" <>+ help "Starting season number"++ infoStartEpisode =+ short 'e' <> long "episode" <> metavar "NUM" <>+ help "Starting episode number"++ infoMappingFile =+ short 'm' <> long "map" <> metavar "FILE" <>+ help "File to map files to seasons/episodes"++--------------------------------------------------------------------------------+run :: Options -> IO ()+run opts = execVimetaBylineApp (updateConfig $ optsCommon opts) $ do+ tv <- case optsTVID opts of+ Nothing -> tvSearch+ Just n -> tmdb (fetchFullTVSeries n)++ case optsMappingFile opts of+ Nothing -> fromFiles opts tv+ Just fn -> fromMappingFile opts tv fn++--------------------------------------------------------------------------------+fromFiles :: (MonadIO m) => Options -> TV -> Vimeta m ()+fromFiles opts tv = case (optsStartSeason opts, optsStartEpisode opts) of+ (Just s, Nothing) -> tagWithFileOrder tv (EpisodeSpec s 1) (optsFiles opts)+ (Just s, Just e) -> tagWithFileOrder tv (EpisodeSpec s e) (optsFiles opts)+ (_, _) -> die "please use the --season option"++--------------------------------------------------------------------------------+fromMappingFile :: (MonadIO m) => Options -> TV -> FilePath -> Vimeta m ()+fromMappingFile opts tv filename = do+ unless (null $ optsFiles opts) $+ die "don't give file arguments when using a mapping file"++ tagWithMappingFile tv filename
+ src/Vimeta/UI/Common/Movie.hs view
@@ -0,0 +1,50 @@+{-# 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.Movie+ ( tagMovie+ ) where++--------------------------------------------------------------------------------+import qualified Data.Map as Map+import Data.Maybe+import qualified Data.Text as Text+import Network.API.TheMovieDB+import Vimeta.Core++--------------------------------------------------------------------------------+-- | Run the tagger for the given file/movie combo.+tagMovie :: (MonadIO m) => FilePath -> Movie -> Vimeta m ()+tagMovie filename movie = do+ context <- ask++ let format = configFormatMovie (ctxConfig context)+ tmdbCfg = ctxTMDBCfg context++ withArtwork (moviePosterURLs tmdbCfg movie) $ \artwork ->+ case fromFormatString (formatMap artwork) "config.cmd_movie" format of+ Left e -> die e+ Right cmd -> tagFile (Text.unpack cmd)++ where+ formatMap :: Maybe FilePath -> FormatTable+ formatMap artwork = Map.fromList+ [ ('Y', formatFullDate $ movieReleaseDate movie)+ , ('a', Text.pack <$> artwork)+ , ('d', Just (Text.take 255 $ movieOverview movie))+ , ('g', genreName <$> listToMaybe (movieGenres movie))+ , ('t', Just $ movieTitle movie)+ , ('y', formatYear $ movieReleaseDate movie)+ , ('f', Just $ Text.pack filename)+ ]
+ src/Vimeta/UI/Common/TV.hs view
@@ -0,0 +1,184 @@+{-# 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#)"
+ src/Vimeta/UI/Common/Util.hs view
@@ -0,0 +1,43 @@+{-# 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.Util+ ( parens+ , dayAsYear+ , dayRange+ ) where++--------------------------------------------------------------------------------+import Data.Monoid+import Data.Text (Text)+import qualified Data.Text as Text+import Data.Time (Day, formatTime)+import Data.Time.Locale.Compat (defaultTimeLocale)++--------------------------------------------------------------------------------+-- | Wrap some text with parenthesis.+parens :: Text -> Text+parens t = " (" <> t <> ")"++--------------------------------------------------------------------------------+-- | Format a 'Maybe Day' as a year ('Text').+dayAsYear :: Maybe Day -> Text+dayAsYear Nothing = "----"+dayAsYear (Just d) = Text.pack (formatTime defaultTimeLocale "%Y" d)++--------------------------------------------------------------------------------+-- | Given a start 'Day' and an end 'Day', produce a string+-- representing a range.+dayRange :: Maybe Day -> Maybe Day -> Text+dayRange d1 d2 = dayAsYear d1 <> " - " <> dayAsYear d2
+ src/Vimeta/UI/Term/Common.hs view
@@ -0,0 +1,53 @@+{-# 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.Term.Common+ ( byline+ , notEmpty+ , execVimetaBylineApp+ ) where++--------------------------------------------------------------------------------+import Control.Monad+import Control.Monad.Trans.Class (lift)+import Data.Text (Text)+import qualified Data.Text as Text+import System.Console.Byline hiding (ask)+import System.Exit (exitSuccess, exitFailure)+import Vimeta.Core+import Vimeta.Core.Vimeta (Vimeta(..))++--------------------------------------------------------------------------------+-- | Run a 'Byline' operation.+byline :: Byline IO a -> Vimeta (Byline IO) a+byline = Vimeta . lift . lift++--------------------------------------------------------------------------------+notEmpty :: Stylized -> Text -> IO (Either Stylized Text)+notEmpty errortxt input = return $ if Text.length clean > 0+ then Right clean+ else Left errortxt+ where+ clean :: Text+ clean = Text.strip input++--------------------------------------------------------------------------------+-- | Helper function to run a 'Vimeta' value based in 'Byline'.+execVimetaBylineApp :: (Config -> Config) -> Vimeta (Byline IO) () -> IO ()+execVimetaBylineApp cf vimeta = void $ runByline $ do+ v <- execVimeta cf vimeta++ case v of+ Right _ -> liftIO exitSuccess+ Left e -> reportLn Error (text $ Text.pack e) >> liftIO exitFailure
+ src/Vimeta/UI/Term/Movie.hs view
@@ -0,0 +1,65 @@+{-# 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.++-}++--------------------------------------------------------------------------------+-- | Search for a movie and interact with the user through the terminal.+module Vimeta.UI.Term.Movie+ ( movieSearch+ ) where++--------------------------------------------------------------------------------+import Data.Monoid+import Data.Text (Text)+import qualified Data.Text as Text+import Network.API.TheMovieDB+import System.Console.Byline+import Vimeta.Core hiding (ask)+import Vimeta.UI.Common.Util+import Vimeta.UI.Term.Common++--------------------------------------------------------------------------------+-- | Search for a movie and interact with the user through the terminal.+movieSearch :: Text -> Vimeta (Byline IO) Movie+movieSearch initial = do+ name <- byline $ askUntil searchPrompt (Just initial) (notEmpty searchErr)+ movies <- tmdb (searchMovies name)+ answer <- byline $ askWithMenuRepeatedly (mkMenu movies) prompt eprompt++ case answer of+ Match movie -> logID movie >> tmdb (fetchMovie (movieID movie))+ _ -> die "you need to pick a valid movie"++ where+ -- The Menu.+ mkMenu movies = banner "Choose a movie:" (menu movies displayMovie)++ -- Search prompt.+ searchPrompt = "search (movie name): "++ -- Search error text.+ searchErr = "please enter a valid search term" <> fg red++ -- Menu prompt.+ prompt = "Which is the correct movie? "++ -- Prompt when someone fails to pick a movie.+ eprompt = "please choose a valid movie" <> fg red++ -- Menu item display for a movie.+ displayMovie m = mconcat [ text (movieTitle m)+ , text (parens $ dayAsYear $ movieReleaseDate m)+ ]++ -- Log a movie ID.+ logID movie = verbose $ "using movie ID: " <>+ Text.pack (show $ movieID movie)
+ src/Vimeta/UI/Term/TV.hs view
@@ -0,0 +1,57 @@+{-# 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.++-}++--------------------------------------------------------------------------------+-- | Search for a TV series by interacting with the user through the terminal.+module Vimeta.UI.Term.TV+ ( tvSearch+ ) where++--------------------------------------------------------------------------------+import Data.Monoid+import qualified Data.Text as Text+import Network.API.TheMovieDB+import System.Console.Byline+import Vimeta.Core hiding (ask)+import Vimeta.UI.Common.Util+import Vimeta.UI.Term.Common++--------------------------------------------------------------------------------+tvSearch :: Vimeta (Byline IO) TV+tvSearch = do+ let prompt = "search (series name): "+ mprompt = "Which is the correct TV series? "+ sprompt = "a search term is required" <> fg red+ eprompt = "please choose a TV series" <> fg red++ name <- byline (askUntil prompt Nothing $ notEmpty sprompt)+ series <- tmdb (searchTV name)+ answer <- byline $ askWithMenuRepeatedly (mkMenu series) mprompt eprompt++ case answer of+ Match tv -> logID tv >> tmdb (fetchFullTVSeries (tvID tv))+ _ -> die "you need to pick a valid TV series"+++ where+ -- The menu.+ mkMenu series = banner "Choose a TV series:" (menu series displayTV)++ -- Function to display possible matches.+ displayTV series =+ mconcat [ text (tvName series)+ , text (parens $ dayRange (tvFirstAirDate series) (tvLastAirDate series))+ ]++ -- Log the TV ID.+ logID tv = verbose $ "using TV ID: " <> Text.pack (show $ tvID tv)
+ vimeta.cabal view
@@ -0,0 +1,106 @@+--------------------------------------------------------------------------------+name: vimeta+version: 0.2.0.0+synopsis: Frontend for video metadata tagging tools+homepage: http://github.com/pjones/vimeta+bug-reports: http://github.com/pjones/vimeta/issues+license: BSD2+license-file: LICENSE+author: Peter Jones <pjones@devalot.com>+maintainer: Peter Jones <pjones@devalot.com>+copyright: Copyright (c) 2013-2015 Peter Jones+category: Video+stability: experimental+tested-with: GHC == 7.8.4, GHC == 7.10.1+build-type: Simple+cabal-version: >=1.10+description:+ Vimeta is a tool to fetch video metadata from the interwebs and+ update video files using a tagging tool.++--------------------------------------------------------------------------------+extra-source-files:+ CHANGES+ README.md+ TODO.org++--------------------------------------------------------------------------------+source-repository head+ type: git+ location: git://github.com/pjones/vimeta.git++--------------------------------------------------------------------------------+flag maintainer+ description: Enable settings for the package maintainer.+ manual: True+ default: False++--------------------------------------------------------------------------------+library+ exposed-modules:+ Vimeta.Core+ Vimeta.Core.Cache+ Vimeta.Core.Config+ Vimeta.Core.Download+ Vimeta.Core.Format+ Vimeta.Core.MappingFile+ Vimeta.Core.Process+ Vimeta.Core.Tagger+ Vimeta.Core.Vimeta+ Vimeta.UI.CommandLine+ Vimeta.UI.CommandLine.Common+ Vimeta.UI.CommandLine.Config+ Vimeta.UI.CommandLine.Movie+ Vimeta.UI.CommandLine.TV+ Vimeta.UI.Common.Movie+ Vimeta.UI.Common.TV+ Vimeta.UI.Common.Util+ Vimeta.UI.Term.Common+ Vimeta.UI.Term.Movie+ Vimeta.UI.Term.TV++ other-modules:+ Paths_vimeta++ hs-source-dirs: src+ default-language: Haskell2010+ ghc-options: -Wall -fwarn-incomplete-uni-patterns+ ghc-prof-options: -prof -auto-all++ if flag(maintainer)+ ghc-options: -Werror++ build-depends: aeson >= 0.8 && < 0.10+ , base >= 4.6 && < 5.0+ , byline >= 0.1 && < 0.3+ , bytestring >= 0.10 && < 0.11+ , containers >= 0.5 && < 0.6+ , directory >= 1.2 && < 1.3+ , either >= 4.3 && < 4.5+ , filepath >= 1.3 && < 1.5+ , http-client >= 0.4 && < 0.5+ , http-client-tls >= 0.2.2 && < 0.3+ , http-types >= 0.8 && < 0.9+ , mtl >= 2.1 && < 2.3+ , old-locale >= 1.0 && < 1.1+ , optparse-applicative >= 0.11 && < 0.12+ , parsec >= 3.1 && < 3.2+ , process >= 1.1 && < 1.3+ , temporary >= 1.1 && < 1.3+ , text >= 0.11 && < 1.3+ , themoviedb >= 1.1 && < 1.2+ , time >= 1.2 && < 1.6+ , time-locale-compat >= 0.1 && < 0.2+ , transformers >= 0.3 && < 0.5+ , xdg-basedir >= 0.2 && < 0.3+ , yaml >= 0.8 && < 0.9++--------------------------------------------------------------------------------+executable vimeta+ default-language: Haskell2010+ main-is: src/Main.hs+ build-depends: base, vimeta+ ghc-options: -Wall -fwarn-incomplete-uni-patterns++ if flag(maintainer)+ ghc-options: -Werror