packages feed

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 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']