cut-the-crap 1.4.2 → 2.0.0
raw patch · 9 files changed
+203/−85 lines, 9 filesdep +network-uridep +randomPVP ok
version bump matches the API change (PVP)
Dependencies added: network-uri, random
API changes (from Hackage documentation)
- Cut.Ffmpeg: ffmpeg :: FilePath -> [Text] -> Sh [Text]
- Cut.Ffmpeg: ffmpeg' :: [Text] -> Sh [Text]
- Cut.Ffmpeg: floatToText :: Double -> Text
- Cut.Options: data ListenCutOptions
- Cut.Options: instance GHC.Generics.Generic Cut.Options.FileIO
- Cut.Options: instance GHC.Generics.Generic Cut.Options.ListenCutOptions
- Cut.Options: instance GHC.Generics.Generic Cut.Options.ProgramOptions
- Cut.Options: instance GHC.Show.Show Cut.Options.FileIO
- Cut.Options: instance GHC.Show.Show Cut.Options.ListenCutOptions
- Cut.Options: instance GHC.Show.Show Cut.Options.ProgramOptions
+ Cut.Options: GenerateSubtitles :: FileIO a -> ProgramOptions a
+ Cut.Options: ListenCut :: ListenCutOptionsT a -> ProgramOptions a
+ Cut.Options: data InputSource
+ Cut.Options: data ListenCutOptionsT a
+ Cut.Options: input_src_local_file :: Prism' InputSource FilePath
+ Cut.Options: input_src_remote :: Prism' InputSource URI
+ Cut.Options: instance GHC.Generics.Generic (Cut.Options.FileIO a)
+ Cut.Options: instance GHC.Generics.Generic (Cut.Options.ListenCutOptionsT a)
+ Cut.Options: instance GHC.Generics.Generic (Cut.Options.ProgramOptions a)
+ Cut.Options: instance GHC.Generics.Generic Cut.Options.InputSource
+ Cut.Options: instance GHC.Show.Show Cut.Options.InputSource
+ Cut.Options: instance GHC.Show.Show a => GHC.Show.Show (Cut.Options.FileIO a)
+ Cut.Options: instance GHC.Show.Show a => GHC.Show.Show (Cut.Options.ListenCutOptionsT a)
+ Cut.Options: instance GHC.Show.Show a => GHC.Show.Show (Cut.Options.ProgramOptions a)
+ Cut.Options: type ListenCutOptions = ListenCutOptionsT FilePath
+ Cut.Shell: ffmpeg :: FilePath -> [Text] -> Sh [Text]
+ Cut.Shell: ffmpeg' :: [Text] -> Sh [Text]
+ Cut.Shell: floatToText :: Double -> Text
+ Cut.Shell: youtube_dl :: URI -> FilePath -> Sh [Text]
- Cut.Crap: entryPoint :: (MonadMask m, MonadUnliftIO m) => m ()
+ Cut.Crap: entryPoint :: MonadMask m => MonadUnliftIO m => m ()
- Cut.Crap: runGenSubs :: MonadIO m => FileIO -> m ()
+ Cut.Crap: runGenSubs :: MonadIO m => FileIO FilePath -> m ()
- Cut.Crap: runListenCut :: (MonadMask m, MonadUnliftIO m) => ListenCutOptions -> m ()
+ Cut.Crap: runListenCut :: MonadMask m => MonadUnliftIO m => ListenCutOptions -> m [Interval Sound]
- Cut.Options: cut_noise :: Lens' ListenCutOptions Bool
+ Cut.Options: cut_noise :: Lens' (ListenCutOptionsT a) Bool
- Cut.Options: data FileIO
+ Cut.Options: data FileIO a
- Cut.Options: data ProgramOptions
+ Cut.Options: data ProgramOptions a
- Cut.Options: detect_margin :: Lens' ListenCutOptions Double
+ Cut.Options: detect_margin :: Lens' (ListenCutOptionsT a) Double
- Cut.Options: getOutFileName :: ListenCutOptions -> FilePath
+ Cut.Options: getOutFileName :: ListenCutOptionsT a -> FilePath
- Cut.Options: gnerate_sub_prism :: Prism' ProgramOptions FileIO
+ Cut.Options: gnerate_sub_prism :: Prism' (ProgramOptions a) (FileIO a)
- Cut.Options: in_file :: Lens' FileIO FilePath
+ Cut.Options: in_file :: Lens (FileIO a) (FileIO b) a b
- Cut.Options: lc_fileio :: Lens' ListenCutOptions FileIO
+ Cut.Options: lc_fileio :: Lens (ListenCutOptionsT a) (ListenCutOptionsT b) (FileIO a) (FileIO b)
- Cut.Options: listen_cut_prism :: Prism' ProgramOptions ListenCutOptions
+ Cut.Options: listen_cut_prism :: Prism' (ProgramOptions a) (ListenCutOptionsT a)
- Cut.Options: music_track :: Lens' ListenCutOptions (Maybe Int)
+ Cut.Options: music_track :: Lens' (ListenCutOptionsT a) (Maybe Int)
- Cut.Options: out_file :: Lens' FileIO FilePath
+ Cut.Options: out_file :: Lens' (FileIO a) FilePath
- Cut.Options: parseProgram :: Parser ProgramOptions
+ Cut.Options: parseProgram :: Parser (ProgramOptions InputSource)
- Cut.Options: seg_size :: Lens' ListenCutOptions Int
+ Cut.Options: seg_size :: Lens' (ListenCutOptionsT a) Int
- Cut.Options: silent_duration :: Lens' ListenCutOptions Double
+ Cut.Options: silent_duration :: Lens' (ListenCutOptionsT a) Double
- Cut.Options: silent_treshold :: Lens' ListenCutOptions Double
+ Cut.Options: silent_treshold :: Lens' (ListenCutOptionsT a) Double
- Cut.Options: simpleOptions :: ListenCutOptions
+ Cut.Options: simpleOptions :: ListenCutOptionsT InputSource
- Cut.Options: specifyTracks :: ListenCutOptions -> [Text]
+ Cut.Options: specifyTracks :: ListenCutOptionsT a -> [Text]
- Cut.Options: voice_track :: Lens' ListenCutOptions Int
+ Cut.Options: voice_track :: Lens' (ListenCutOptionsT a) Int
- Cut.Options: voice_track_map :: ListenCutOptions -> Text
+ Cut.Options: voice_track_map :: ListenCutOptionsT a -> Text
- Cut.Options: work_dir :: Lens' FileIO (Maybe FilePath)
+ Cut.Options: work_dir :: Lens' (FileIO a) (Maybe FilePath)
Files
- Changelog.md +8/−0
- Readme.md +15/−2
- cut-the-crap.cabal +10/−4
- src/Cut/Analyze.hs +2/−1
- src/Cut/Crap.hs +55/−10
- src/Cut/CutVideo.hs +1/−1
- src/Cut/Ffmpeg.hs +0/−27
- src/Cut/Options.hs +72/−40
- src/Cut/Shell.hs +40/−0
Changelog.md view
@@ -1,5 +1,13 @@ # Change log for cut-the-crap +## Version 2.0.0 - 2020.10.09++- Better install instructions+- `defaut.nix` in project root now links to ffmpeg+- return parse result from `runListenCut`+- Make input file and output file positional+- Add support for parsing uri and downloading automatically+ ## Version 1.4.2 - 2020.08.31 - add c2hs to build tools so we don't need to modify nixpkgs
Readme.md view
@@ -30,10 +30,23 @@ # Install +## From source+Install the [nix package](https://nixos.org/download.html) manager.++```shell+git clone https://github.com/jappeace/cut-the-crap+cd cut-the-crap+nix-build .+result/bin/cut-the-crap+```+ ## Nix/Nixos-+ Apply [overlay](https://nixos.wiki/wiki/Overlays) found [here](https://github.com/jappeace/cut-the-crap/tree/master/overlay).-+ Run `nix-env -iA cut-the-crap` or add to systemPackages.+++ Run `nix-env -iA nixos.haskellPackages.cut-the-crap` or add to systemPackages. + simply run `cut-the-crap` to display usage instructions.++This only works for nixpkgs that have cut-the-crap >= 1.4.2 or =< 1.3+There were some build issues with 1.4.0 and 1.4.1 (now fixed) ## Ubuntu Download the .deb file from the [release page](https://github.com/jappeace/cut-the-crap/releases).
cut-the-crap.cabal view
@@ -4,10 +4,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: 1691afc2c7f5ffdaccba0cc99cb49f19c204ab772b4f3d5412758a6a5748fcb7+-- hash: 0f25306419959045fb637431751e1f2ae5212ba5c47b5ccc91dca4436a8a7943 name: cut-the-crap-version: 1.4.2+version: 2.0.0 synopsis: Cuts out uninteresting parts of videos by detecting silences. description: Cut the crap is an automatic video editing program for streamers. It can cut out uninteresting parts by detecting silences. This was inspired by [jumpcutter](https://github.com/carykh/jumpcutter), where this program can get better quality results by using an (optional) dedicated microphone track. This prevents cutting of [quieter consonants](https://youtu.be/DQ8orIurGxw?t=675) for example. Using ffmpeg more efficiently also produces faster results and is less error prone. category: video@@ -27,8 +27,8 @@ Cut.Analyze Cut.Crap Cut.CutVideo- Cut.Ffmpeg Cut.Options+ Cut.Shell Cut.SpeechRecognition other-modules: Paths_cut_the_crap@@ -52,7 +52,9 @@ , exceptions , generic-lens , lens+ , network-uri , optparse-applicative+ , random , regex-tdfa , shelly , system-filepath@@ -87,7 +89,9 @@ , exceptions , generic-lens , lens+ , network-uri , optparse-applicative+ , random , regex-tdfa , shelly , system-filepath@@ -107,8 +111,8 @@ Cut.Analyze Cut.Crap Cut.CutVideo- Cut.Ffmpeg Cut.Options+ Cut.Shell Cut.SpeechRecognition Paths_cut_the_crap hs-source-dirs:@@ -135,8 +139,10 @@ , hspec , hspec-core , lens+ , network-uri , optparse-applicative , quickcheck-classes+ , random , regex-tdfa , shelly , system-filepath
src/Cut/Analyze.hs view
@@ -15,7 +15,7 @@ import Control.Monad import Control.Monad.Catch import Control.Monad.IO.Unlift-import Cut.Ffmpeg+import Cut.Shell import Cut.Options import Cut.SpeechRecognition import Data.Coerce@@ -51,6 +51,7 @@ liftIO $ putStrLn "-----------------actual lines-----------------" liftIO $ putStrLn "-----------------------------------------" liftIO $ Text.putStrLn $ Text.unlines lines'+ liftIO $ putStrLn "-----------------------------------------" liftIO $ putStrLn "-----------------filtered lines-----------------" liftIO $ putStrLn "-----------------------------------------"
src/Cut/Crap.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE TypeOperators #-}+{-# LANGUAGE ScopedTypeVariables #-} -- | This is where it all started module Cut.Crap@@ -12,6 +13,7 @@ ) where +import Cut.Shell import Control.Lens import Control.Monad import Control.Monad.Catch@@ -19,7 +21,6 @@ import Control.Monad.IO.Unlift import Cut.Analyze import Cut.CutVideo-import Cut.Ffmpeg import Cut.Options import Cut.SpeechRecognition import Data.Foldable (fold, foldl', traverse_)@@ -34,24 +35,68 @@ import Options.Applicative import Shelly hiding (FilePath) import System.IO.Temp+import Network.URI(URI)+import System.Random+import Data.Word +runYoutubeDL :: FileIO a -> URI -> IO FilePath+runYoutubeDL opts x = do++ inputNumbers :: Word32 <- randomIO+ let inputChars :: String+ -- youtube-dl doesn't do .mkv (not supported)+ inputChars = (show inputNumbers) <> ".mp4"+ filePath :: String+ filePath = fromMaybe inputChars $ opts ^. work_dir+ void $ shelly $ youtube_dl x filePath+ pure filePath++++downloadIfNeccisary :: FileIO InputSource -> IO (FileIO FilePath)+downloadIfNeccisary x = do+ result <- sequence $ (runYoutube <|> alreadyLocal)+ case result of+ Nothing -> error $ "Couldn't find " <> show x+ Just y -> pure $ x & in_file .~ y+ where+ runYoutube :: Maybe (IO FilePath)+ runYoutube = x ^? in_file . input_src_remote . to (runYoutubeDL x)++ alreadyLocal :: Maybe (IO FilePath)+ alreadyLocal = preview (in_file . input_src_local_file . to pure) x++downloadCutifNeccisary :: ListenCutOptionsT InputSource -> IO (ListenCutOptionsT FilePath)+downloadCutifNeccisary cut = repackRes+ where+ downloadRes :: IO (FileIO FilePath)+ downloadRes = downloadIfNeccisary $ cut ^. lc_fileio+ repackRes :: IO (ListenCutOptionsT (FilePath))+ repackRes = downloadRes <&> \y -> lc_fileio .~ y $ cut+ -- | reads settings from terminal and runs whatever command was -- given in program options-entryPoint :: (MonadMask m, MonadUnliftIO m) => m ()+entryPoint :: MonadMask m => MonadUnliftIO m => m () entryPoint = do result <- liftIO readSettings+ betterResult <- liftIO $ case result of+ ListenCut cut -> ListenCut <$> downloadCutifNeccisary cut+ GenerateSubtitles x -> GenerateSubtitles <$> downloadIfNeccisary x+ -- I'm mr meeseeks look at me!- sequence_ $ result ^? listen_cut_prism . to runListenCut- <|> result ^? gnerate_sub_prism . to runGenSubs+ sequence_ $ betterResult ^? listen_cut_prism . to (void . runListenCut)+ <|> betterResult ^? gnerate_sub_prism . to runGenSubs+ pure () -runGenSubs :: MonadIO m => FileIO -> m ()+runGenSubs :: MonadIO m => FileIO FilePath -> m () runGenSubs options = liftIO $ withTempDir options $ \tmp -> do- result <- shelly $ detectSpeech (set voice_track 1 simpleOptions) tmp $ options ^. in_file+ betterOptions <- downloadCutifNeccisary simpleOptions+ result <- shelly $ detectSpeech (set voice_track 1 betterOptions) tmp $ options ^. in_file print result traverse_ (T.writeFile (options ^. out_file) . makeSrt) result -- | Runs cut-the-crap with provided `ListenCutOptions`-runListenCut :: (MonadMask m, MonadUnliftIO m) => ListenCutOptions -> m ()+runListenCut :: MonadMask m => MonadUnliftIO m => ListenCutOptions -> m [Interval Sound] runListenCut options = do liftIO $ putStr "started with options: " liftIO $ print options@@ -60,14 +105,14 @@ parsed <- detectSoundInterval options -- then do stuff to it- case parsed of+ parsed <$ case parsed of [] -> liftIO $ putStr "\n\nNo silence in input video detected. There is nothing to be cut so exiting.\n\n" _ -> liftIO $ withTempDir (options ^. lc_fileio) $ runEdit options parsed -withTempDir :: FileIO -> (FilePath -> IO ()) -> IO ()+withTempDir :: FileIO FilePath -> (FilePath -> IO ()) -> IO () withTempDir filioOpts fun = maybe (withTempDirectory "/tmp" "streamedit" fun) fun $ filioOpts ^. work_dir @@ -86,7 +131,7 @@ writefile (fromText $ Text.pack $ tempDir <> "/input.txt") paths combine tempDir -readSettings :: IO ProgramOptions+readSettings :: IO (ProgramOptions InputSource) readSettings = customExecParser (prefs showHelpOnError) $ info (parseProgram <**> helper) (fullDesc <> header "Cut the crap" <> progDesc
src/Cut/CutVideo.hs view
@@ -15,7 +15,7 @@ import Control.Monad import Control.Monad.Catch import Cut.Analyze-import Cut.Ffmpeg+import Cut.Shell import Cut.Options import Data.Foldable import Data.Text (Text)
− src/Cut/Ffmpeg.hs
@@ -1,27 +0,0 @@-module Cut.Ffmpeg- ( ffmpeg- , ffmpeg'- , floatToText- )-where--import Data.Text (Text)-import qualified Data.Text as Text-import Numeric-import Shelly---- | Wrap ffmpeg for convenience and logging--- technically supports multiple inputs but for convenice we threw that.-ffmpeg :: Prelude.FilePath -> [Text] -> Sh [Text]-ffmpeg file args = ffmpeg' ("-y" : "-i" : Text.pack file : args)--ffmpeg' :: [Text] -> Sh [Text]-ffmpeg' args = do- liftIO $ putStr "Running: "- liftIO $ print $ "ffmpeg " <> Text.unwords args- run_ "ffmpeg" args- Text.lines <$> lastStderr---- | Format floats for cli-floatToText :: Double -> Text-floatToText = Text.pack . flip (showFFloat (Just 10)) ""
src/Cut/Options.hs view
@@ -1,15 +1,27 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeOperators #-}+{-# LANGUAGE DeriveAnyClass #-} +-- | This module defines which options exists, and provides+-- functions for parsing cli options. module Cut.Options- ( ProgramOptions+ ( parseProgram+ , specifyTracks+ , getOutFileName+ -- * Program options+ , ProgramOptions(..)+ , gnerate_sub_prism+ , listen_cut_prism+ -- * fileio, deal with input output files , FileIO- , ListenCutOptions- , parseProgram , lc_fileio , in_file , out_file+ , work_dir+ -- * listen cut, options for video editing by audio+ , ListenCutOptionsT+ , ListenCutOptions , seg_size , silent_treshold , detect_margin@@ -17,39 +29,41 @@ , music_track , silent_duration , cut_noise- , work_dir- , simpleOptions , voice_track_map- , specifyTracks- , getOutFileName- , gnerate_sub_prism- , listen_cut_prism+ -- * input source prisms+ , InputSource+ , input_src_remote+ , input_src_local_file+ -- * defaults+ , simpleOptions ) where -import Control.Lens+import Control.Lens hiding (argument) import Data.Generics.Product.Fields import Data.Generics.Sum import qualified Data.Text as Text import Data.Text.Lens import GHC.Generics hiding (to) import Options.Applicative+import Network.URI -simpleFileIO :: FileIO-simpleFileIO = FileIO { fi_inFile = "in.mkv" , fi_outFile = "out.mkv"+simpleFileIO :: (FileIO InputSource)+simpleFileIO = FileIO { fi_inFile = LocalFile "in.mkv" , fi_outFile = "out.mkv" , fi_workDir = Nothing } -in_file :: Lens' FileIO FilePath+-- type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t+in_file :: Lens (FileIO a) (FileIO b) a b in_file = field @"fi_inFile" -out_file :: Lens' FileIO FilePath+out_file :: Lens' (FileIO a) FilePath out_file = field @"fi_outFile" -work_dir :: Lens' FileIO (Maybe FilePath)+work_dir :: Lens' (FileIO a) (Maybe FilePath) work_dir = field @"fi_workDir" -simpleOptions :: ListenCutOptions+simpleOptions :: ListenCutOptionsT InputSource simpleOptions = ListenCutOptions { lc_fileIO = simpleFileIO , lc_segmentSize = _Just # def_seg_size@@ -61,20 +75,22 @@ , lc_cutNoise = def_cut_noise } -getOutFileName :: ListenCutOptions -> FilePath+getOutFileName :: ListenCutOptionsT a -> FilePath getOutFileName = reverse . takeWhile ('/' /=) . reverse . view (lc_fileio . out_file) -- | Deals with having an input file and a target output file-data FileIO = FileIO- { fi_inFile :: FilePath+data FileIO a = FileIO+ { fi_inFile :: a , fi_outFile :: FilePath , fi_workDir :: Maybe FilePath -- ^ for consistency (or debugging) we may want to specify this. } deriving (Show, Generic) +type ListenCutOptions = ListenCutOptionsT FilePath+ -- | Cut out by listening to sound options-data ListenCutOptions = ListenCutOptions- { lc_fileIO :: FileIO+data ListenCutOptionsT a = ListenCutOptions+ { lc_fileIO :: FileIO a , lc_segmentSize :: Maybe Int , lc_silentTreshold :: Maybe Double , lc_silentDuration :: Maybe Double@@ -85,14 +101,14 @@ } deriving (Show, Generic) -data ProgramOptions = ListenCut ListenCutOptions- | GenerateSubtitles FileIO+data ProgramOptions a = ListenCut (ListenCutOptionsT a)+ | GenerateSubtitles (FileIO a) deriving (Show, Generic) -listen_cut_prism :: Prism' ProgramOptions ListenCutOptions+listen_cut_prism :: Prism' (ProgramOptions a) (ListenCutOptionsT a) listen_cut_prism = _Ctor @"ListenCut" -gnerate_sub_prism :: Prism' ProgramOptions FileIO+gnerate_sub_prism :: Prism' (ProgramOptions a) (FileIO a) gnerate_sub_prism = _Ctor @"GenerateSubtitles" def_seg_size :: Int@@ -113,34 +129,34 @@ def_voice :: Int def_voice = 1 -lc_fileio :: Lens' ListenCutOptions FileIO+lc_fileio :: Lens (ListenCutOptionsT a) (ListenCutOptionsT b) (FileIO a) (FileIO b) lc_fileio = field @"lc_fileIO" -seg_size :: Lens' ListenCutOptions Int+seg_size :: Lens' (ListenCutOptionsT a) Int seg_size = field @"lc_segmentSize" . non def_seg_size -detect_margin :: Lens' ListenCutOptions Double+detect_margin :: Lens' (ListenCutOptionsT a) Double detect_margin = field @"lc_detectMargin" . non def_margin -silent_treshold :: Lens' ListenCutOptions Double+silent_treshold :: Lens' (ListenCutOptionsT a) Double silent_treshold = field @"lc_silentTreshold" . non def_silent -silent_duration :: Lens' ListenCutOptions Double+silent_duration :: Lens' (ListenCutOptionsT a) Double silent_duration = field @"lc_silentDuration" . non def_duration -voice_track :: Lens' ListenCutOptions Int+voice_track :: Lens' (ListenCutOptionsT a) Int voice_track = field @"lc_voiceTrack" . non def_voice -music_track :: Lens' ListenCutOptions (Maybe Int)+music_track :: Lens' (ListenCutOptionsT a) (Maybe Int) music_track = field @"lc_musicTrack" -cut_noise :: Lens' ListenCutOptions Bool+cut_noise :: Lens' (ListenCutOptionsT a) Bool cut_noise = field @"lc_cutNoise" -voice_track_map :: ListenCutOptions -> Text.Text+voice_track_map :: (ListenCutOptionsT a) -> Text.Text voice_track_map = mappend "0:" . view (voice_track . to show . packed) -specifyTracks :: ListenCutOptions -> [Text.Text]+specifyTracks :: (ListenCutOptionsT a) -> [Text.Text] specifyTracks options = [ "-map" , "0:0"@@ -148,10 +164,26 @@ , voice_track_map options ] -parseFile :: Parser FileIO+data InputSource = LocalFile FilePath+ | Remote URI+ deriving (Show, Generic)++input_src_local_file :: Prism' InputSource FilePath+input_src_local_file = _Ctor @"LocalFile"++input_src_remote :: Prism' InputSource URI+input_src_remote = _Ctor @"Remote"++readFileSource :: ReadM InputSource+readFileSource = eitherReader $+ \x ->+ maybe (Left "unlikely error") Right $+ (Remote <$> parseURI x) <|> Just (LocalFile x)++parseFile :: Parser (FileIO InputSource) parseFile = FileIO- <$> option str (long "inFile" <> help "The input video")- <*> option str (long "outFile" <> help "The output name without format")+ <$> argument readFileSource (metavar "INPUT" <> help "The input video, either a file or a uri")+ <*> argument str (metavar "OUTPUT_FILE" <> help "The output name without format") <*> optional (option str@@ -161,14 +193,14 @@ ) ) -parseProgram :: Parser ProgramOptions+parseProgram :: Parser (ProgramOptions InputSource) parseProgram = subparser $ command "listen" (info (ListenCut <$> parseSound) $ progDesc "Cut out by listening to sound options. We listen for silences and cut out the parts that are silenced.") <> command "subtitles" (info (GenerateSubtitles <$> parseFile) $ progDesc "Generate subtiles for a video. This is an intermediate (but usefull) feature developed for recognizing human speech vs background noise.") -parseSound :: Parser ListenCutOptions+parseSound :: Parser (ListenCutOptionsT InputSource) parseSound = ListenCutOptions <$> parseFile <*> optional
+ src/Cut/Shell.hs view
@@ -0,0 +1,40 @@++-- | Run shell programs+module Cut.Shell+ ( ffmpeg+ , ffmpeg'+ , floatToText+ , youtube_dl+ )+where++import Data.Text (Text)+import qualified Data.Text as Text+import Numeric+import Shelly+import Network.URI(URI)++-- | Wrap ffmpeg for convenience and logging+-- technically supports multiple inputs but for convenice we threw that.+ffmpeg :: Prelude.FilePath -> [Text] -> Sh [Text]+ffmpeg file args = ffmpeg' ("-y" : "-i" : Text.pack file : args)++ffmpeg' :: [Text] -> Sh [Text]+ffmpeg' args = do+ liftIO $ putStr "Running: "+ liftIO $ print $ "ffmpeg " <> Text.unwords args+ run_ "ffmpeg" args+ Text.lines <$> lastStderr++-- | Format floats for cli+floatToText :: Double -> Text+floatToText = Text.pack . flip (showFFloat (Just 10)) ""++youtube_dl :: URI -> FilePath -> Sh [Text]+youtube_dl uri path' = do+ liftIO $ print $ "running youtube-dl " <> Text.unwords args+ run_ "youtube-dl" args+ Text.lines <$> lastStderr+ where+ args = [Text.pack $ show uri,+ "-o", Text.pack path']