packages feed

haskell-ffprobe (empty) → 0.1.0.0

raw patch · 19 files changed

+730/−0 lines, 19 filesdep +aesondep +basedep +bytestringsetup-changed

Dependencies added: aeson, base, bytestring, haskell-ffprobe, hspec, hspec-expectations, process, scientific, text

Files

+ CHANGELOG.md view
@@ -0,0 +1,11 @@+# Changelog for `haskell-ffprobe`++All notable changes to this project will be documented in this file.++The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),+and this project adheres to the+[Haskell Package Versioning Policy](https://pvp.haskell.org/).++## 0.1.0.0 - 2024-07-01++First release!
+ LICENSE view
@@ -0,0 +1,26 @@+Copyright 2024 Author name here++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.++3.  Neither the name of the copyright holder nor the names of its contributors+    may be used to endorse or promote products derived from this software+    without specific prior written permission.++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 HOLDER 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,24 @@+# haskell-ffprobe++This package provides Haskell bindings for the `ffprobe` command.++## Example++```haskell+import FFProbe+import FFProbe.Data.Format (duration, formatName)+import FFProbe.Data.Stream (codecLongName)+import System.Environment++main :: IO ()+main = do+    fileName:_ <- getArgs+    ffprobeRes <- ffprobe fileName+    case ffprobeRes of+        Left err -> putStrLn $ "An error occured: " ++ err+        Right ffprobeData -> do+            print $ formatName (format ffprobeData)+            print $ duration (format ffprobeData)+            print $ length (chapters ffprobeData)+            print $ codecLongName $ head (streams ffprobeData)+```
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ haskell-ffprobe.cabal view
@@ -0,0 +1,88 @@+cabal-version: 2.2++-- This file has been generated from package.yaml by hpack version 0.36.0.+--+-- see: https://github.com/sol/hpack++name:           haskell-ffprobe+version:        0.1.0.0+synopsis:       Haskell bindings for ffprobe+description:    Please see the README on GitHub at <https://github.com/Arthi-chaud/haskell-ffprobe#readme>+category:       Bindings+homepage:       https://github.com/Arthi-chaud/haskell-ffprobe#readme+bug-reports:    https://github.com/Arthi-chaud/haskell-ffprobe/issues+author:         Arthi-chaud+maintainer:     Arthi-chaud+copyright:      2024 Arthi-chaud+license:        BSD-3-Clause+license-file:   LICENSE+build-type:     Simple+extra-doc-files:+    README.md+    CHANGELOG.md++source-repository head+  type: git+  location: https://github.com/Arthi-chaud/haskell-ffprobe++library+  exposed-modules:+      FFProbe+      FFProbe.Data.Chapter+      FFProbe.Data.Format+      FFProbe.Data.Stream+      FFProbe.Data.Tags+  other-modules:+      FFProbe.Data.Tags.Internal+      FFProbe.Exec+      FFProbe.Internal+      Paths_haskell_ffprobe+  autogen-modules:+      Paths_haskell_ffprobe+  hs-source-dirs:+      src+  default-extensions:+      OverloadedStrings+      LambdaCase+      FlexibleInstances+      RecordWildCards+      DeriveGeneric+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints+  build-depends:+      aeson >=2.1.2.1 && <=2.2.3.0+    , base >=4.7 && <5+    , bytestring >=0.11.5 && <0.12+    , process >=1.6.18 && <1.7+    , scientific >=0.3.7 && <=0.3.8.0+    , text >=2.0.2 && <2.1+  default-language: Haskell2010++test-suite haskell-ffprobe-test+  type: exitcode-stdio-1.0+  main-is: Spec.hs+  other-modules:+      FFProbe.Data.TestChapter+      FFProbe.Data.TestFormat+      FFProbe.Data.TestStream+      FFProbe.TestFFProbe+      Utils+      Paths_haskell_ffprobe+  autogen-modules:+      Paths_haskell_ffprobe+  hs-source-dirs:+      test+  default-extensions:+      OverloadedStrings+      LambdaCase+      FlexibleInstances+      RecordWildCards+      DeriveGeneric+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N+  build-depends:+      aeson >=2.1.2.1 && <=2.2.3.0+    , base >=4.7 && <5+    , bytestring >=0.11.5 && <0.12+    , haskell-ffprobe+    , hspec+    , hspec-expectations+  default-language: Haskell2010
+ src/FFProbe.hs view
@@ -0,0 +1,29 @@+module FFProbe (+    ffprobe,+    FFProbeData (..),+) where++import Data.Aeson (FromJSON, eitherDecodeStrict)+import Data.ByteString.Char8 (pack)+import FFProbe.Data.Chapter (Chapter)+import FFProbe.Data.Format (Format)+import FFProbe.Data.Stream (Stream)+import FFProbe.Exec (execFFProbe)+import GHC.Generics (Generic)++data FFProbeData = FFProbeData+    { streams :: [Stream],+      chapters :: [Chapter],+      format :: Format+    }+    deriving (Generic)++instance FromJSON FFProbeData++-- | Runs the ffprobe coomands, and parse its output+ffprobe :: String -> IO (Either String FFProbeData)+ffprobe path = do+    probeRes <- execFFProbe path+    return $ case probeRes of+        Right rawJson -> eitherDecodeStrict (pack rawJson)+        Left err -> Left err
+ src/FFProbe/Data/Chapter.hs view
@@ -0,0 +1,46 @@+module FFProbe.Data.Chapter (Chapter (..), duration, title) where++import Data.Aeson+import FFProbe.Data.Tags+import FFProbe.Data.Tags.Internal+import FFProbe.Internal (parseReadable)+import Prelude hiding (id)++data Chapter = Chapter+    { id :: Integer,+      -- | Example: "1/1000000000"+      timeBase :: String,+      -- | The timestamp, in seconds, of the start of the chapter+      startTime :: Float,+      -- | The timestamp, in seconds, of the end of the chapter+      endTime :: Float,+      -- | Additional tags+      tags :: TagList,+      -- | The aeson object for the entire JSON received from ffprobe.+      raw :: Object+    }++instance HasTags Chapter where+    getTags = tags++-- | Gets the duration of the chapter, in seconds+duration :: Chapter -> Float+duration chapter = endTime chapter - startTime chapter++-- | Retrieves the title of the chapter, using its tags+title :: Chapter -> Maybe String+title chapter = do+    value <- lookupTag "title" chapter+    case value of+        StringTag t -> return t+        _ -> Nothing++instance FromJSON Chapter where+    parseJSON = withObject "Chapter Entry" $ \v -> do+        let raw = v+        id <- v .: "id"+        timeBase <- v .: "time_base"+        startTime <- parseReadable =<< v .: "start_time"+        endTime <- parseReadable =<< v .: "end_time"+        tags <- parseTags =<< v .: "tags"+        return Chapter {..}
+ src/FFProbe/Data/Format.hs view
@@ -0,0 +1,45 @@+module FFProbe.Data.Format (Format (..)) where++import Data.Aeson.Types+import FFProbe.Data.Tags+import FFProbe.Data.Tags.Internal+import FFProbe.Internal (parseReadable)++data Format = Format+    { filename :: String,+      streamsCount :: Integer,+      programsCount :: Integer,+      streamGroupsCount :: Integer,+      formatName :: String,+      formatLongName :: String,+      startTime :: Float,+      -- | Duration in seconds+      duration :: Float,+      -- | Size of the file, in bytes+      size :: Integer,+      bitrate :: Integer,+      probeScore :: Integer,+      tags :: TagList,+      -- | The aeson object for the entire JSON received from ffprobe.+      raw :: Object+    }++instance HasTags Format where+    getTags = tags++instance FromJSON Format where+    parseJSON = withObject "Format" $ \v -> do+        let raw = v+        filename <- v .: "filename"+        streamsCount <- v .: "nb_streams"+        programsCount <- v .: "nb_programs"+        streamGroupsCount <- v .: "nb_stream_groups"+        formatName <- v .: "format_name"+        formatLongName <- v .: "format_long_name"+        startTime <- parseReadable =<< v .: "start_time"+        duration <- parseReadable =<< v .: "duration"+        size <- parseReadable =<< v .: "size"+        bitrate <- parseReadable =<< v .: "bit_rate"+        probeScore <- v .: "probe_score"+        tags <- parseTags =<< v .: "tags"+        return Format {..}
+ src/FFProbe/Data/Stream.hs view
@@ -0,0 +1,181 @@+module FFProbe.Data.Stream (+    Stream (..),+    StreamType (..),+    isVideoStream,+    isAudioStream,+    isSubtitleStream,+    isStreamOfType,+    StreamDisposition (..),+) where++import Control.Applicative ((<|>))+import Data.Aeson+import Data.Aeson.Types (Parser)+import Data.Text hiding (index)+import FFProbe.Data.Tags (HasTags (..), TagList)+import FFProbe.Data.Tags.Internal+import FFProbe.Internal+import Prelude hiding (id)++data StreamType+    = VideoStream+    | AudioStream+    | SubtitleStream+    | DataStream+    | Attachment+    | Other String+    deriving (Eq, Show)++instance FromJSON StreamType where+    parseJSON (String "video") = return VideoStream+    parseJSON (String "audio") = return AudioStream+    parseJSON (String "subtitle") = return SubtitleStream+    parseJSON (String "data") = return DataStream+    parseJSON (String "attachment") = return Attachment+    parseJSON (String s) = return $ Other (unpack s)+    parseJSON x = return $ Other (show x)++data StreamDisposition = StreamDisposition+    { isDefault :: Bool,+      isDub :: Bool,+      isOriginal :: Bool,+      isComment :: Bool,+      isLyrics :: Bool,+      isKaraoke :: Bool,+      isForced :: Bool,+      isHearingImpaired :: Bool,+      isVisualImpaired :: Bool,+      isCleanEffects :: Bool,+      isAttachedPic :: Bool,+      isTimedThumbnails :: Bool,+      isNonDiegetic :: Bool,+      isCaptions :: Bool,+      isDescriptions :: Bool,+      isMetadata :: Bool,+      isDependent :: Bool,+      isStillImage :: Bool+    }++isVideoStream :: Stream -> Bool+isVideoStream = isStreamOfType VideoStream++isAudioStream :: Stream -> Bool+isAudioStream = isStreamOfType AudioStream++isSubtitleStream :: Stream -> Bool+isSubtitleStream = isStreamOfType SubtitleStream++isStreamOfType :: StreamType -> Stream -> Bool+isStreamOfType stype stream = stype == streamType stream++instance FromJSON StreamDisposition where+    parseJSON = withObject "Disposition" $ \v -> do+        let getValue key = (parseDispositionValue =<< v .: key) <|> pure False+        isDefault <- getValue "default"+        isDub <- getValue "dub"+        isOriginal <- getValue "original"+        isComment <- getValue "comment"+        isLyrics <- getValue "lyrics"+        isKaraoke <- getValue "karaoke"+        isForced <- getValue "forced"+        isHearingImpaired <- getValue "hearing_impaired"+        isVisualImpaired <- getValue "visual_impaired"+        isCleanEffects <- getValue "clean_effects"+        isAttachedPic <- getValue "attached_pic"+        isTimedThumbnails <- getValue "attached_pic"+        isNonDiegetic <- getValue "non_diegetic"+        isCaptions <- getValue "captions"+        isDescriptions <- getValue "descriptions"+        isMetadata <- getValue "metadata"+        isDependent <- getValue "dependent"+        isStillImage <- getValue "still_image"+        return StreamDisposition {..}+        where+            parseDispositionValue :: Int -> Parser Bool+            parseDispositionValue 0 = return False+            parseDispositionValue 1 = return True+            parseDispositionValue n = fail $ "Expected 0 or 1. Got " ++ show n++data Stream = Stream+    { index :: Integer,+      codecName :: String,+      codecLongName :: String,+      codecType :: String,+      streamType :: StreamType,+      codecTagString :: String,+      -- | Example: "0x0000"+      codecTag :: String,+      rFrameRate :: String,+      averageFrameRate :: String,+      timeBase :: String,+      startPts :: Integer,+      startTime :: Float,+      -- | Duration of the stream, in seconds+      duration :: Maybe Float,+      bitRate :: Maybe Integer,+      bitsPerRawSample :: Maybe Integer,+      bitsPerSample :: Maybe Integer,+      framesCount :: Maybe Integer,+      tags :: TagList,+      disposition :: StreamDisposition,+      fieldOrder :: Maybe String,+      profile :: Maybe String,+      width :: Maybe Integer,+      height :: Maybe Integer,+      hasBFrames :: Maybe Integer,+      -- TODO RATIO+      sampleAspectRatio :: Maybe String,+      displayAspectRatio :: Maybe String,+      pixFmt :: Maybe String,+      level :: Maybe Integer,+      colorRange :: Maybe String,+      colorSpace :: Maybe String,+      sampleFmt :: Maybe String,+      sampleRate :: Maybe Integer,+      channels :: Maybe Integer,+      channelLayout :: Maybe String,+      -- | The aeson object for the entire JSON received from ffprobe.+      raw :: Object+    }++instance HasTags Stream where+    getTags = tags++instance FromJSON Stream where+    parseJSON = withObject "Stream" $ \o -> do+        let raw = o+        index <- o .: "index"+        codecName <- o .: "codec_name"+        codecLongName <- o .: "codec_long_name"+        codecType <- o .: "codec_type"+        streamType <- parseJSON (String $ pack codecType)+        codecTagString <- o .: "codec_tag_string"+        codecTag <- o .: "codec_tag"+        rFrameRate <- o .: "r_frame_rate"+        averageFrameRate <- o .: "avg_frame_rate"+        timeBase <- o .: "time_base"+        startPts <- o .: "start_pts"+        startTime <- parseReadable =<< o .: "start_time"+        duration <- parseOptionalValue =<< o .:? "duration"+        bitRate <- parseOptionalValue =<< o .:? "bit_rate"+        bitsPerRawSample <- parseOptionalValue =<< o .:? "bits_per_raw_sample"+        bitsPerSample <- o .:? "bits_per_sample"+        framesCount <- parseOptionalValue =<< o .:? "nb_frames"+        tags <- parseTags =<< o .: "tags"+        disposition <- o .: "disposition"+        fieldOrder <- o .:? "field_order"+        profile <- o .:? "profile"+        width <- o .:? "width"+        height <- o .:? "height"+        hasBFrames <- o .:? "has_b_frames"+        sampleAspectRatio <- o .:? "sample_aspect_ratio"+        displayAspectRatio <- o .:? "display_aspect_ratio"+        pixFmt <- o .:? "pix_fmt"+        level <- o .:? "level"+        colorRange <- o .:? "color_range"+        colorSpace <- o .:? "color_space"+        sampleFmt <- o .:? "sample_fmt"+        sampleRate <- parseOptionalValue =<< o .:? "sample_rate"+        channels <- o .:? "channels"+        channelLayout <- o .:? "channel_layout"+        return Stream {..}
+ src/FFProbe/Data/Tags.hs view
@@ -0,0 +1,29 @@+module FFProbe.Data.Tags (TagList, TagValue (..), HasTags (..), lookupTag) where++import Data.Aeson+import Data.Scientific (floatingOrInteger)+import Data.Text (unpack)++type TagList = [(String, TagValue)]++data TagValue+    = StringTag String+    | IntTag Integer+    | FloatTag Float+    | -- | If the constructor is 'Other', the String is a JSON representation of the value+      Other String+    | Null+    deriving (Show, Eq)++instance FromJSON TagValue where+    parseJSON (String v) = return $ StringTag $ unpack v+    parseJSON (Number v) = return $ either FloatTag IntTag (floatingOrInteger v)+    parseJSON Data.Aeson.Null = return FFProbe.Data.Tags.Null+    parseJSON x = return $ Other $ show x++class HasTags a where+    getTags :: a -> TagList++-- | Lookup a tag in a TagList, using a key+lookupTag :: (HasTags a) => String -> a -> Maybe TagValue+lookupTag key obj = lookup key (getTags obj)
+ src/FFProbe/Data/Tags/Internal.hs view
@@ -0,0 +1,16 @@+module FFProbe.Data.Tags.Internal (parseTags) where++import Data.Aeson+import Data.Aeson.Key (toString)+import Data.Aeson.KeyMap (toList)+import Data.Aeson.Types (Parser)+import Data.Bifunctor (Bifunctor (bimap))+import Data.Text (unpack)+import FFProbe.Data.Tags++parseTags :: Value -> Parser TagList+parseTags = withObject "Tags" $ \kvmap -> do+    pure $ map (bimap toString parseTag) (toList kvmap)+    where+        parseTag (String v) = StringTag $ unpack v+        parseTag x = Other $ show x
+ src/FFProbe/Exec.hs view
@@ -0,0 +1,24 @@+module FFProbe.Exec (execFFProbe) where++import Control.Exception (IOException, try)+import Data.Bifunctor (first)+import Data.Functor ((<&>))+import System.Process (proc, readCreateProcess)++-- | Runs ffprobes, returns the output of the command+execFFProbe :: String -> IO (Either String String)+execFFProbe path = try_ (readCreateProcess process input) <&> first show+    where+        try_ :: IO String -> IO (Either IOException String)+        try_ = try+        input = ""+        process = proc "ffprobe" (ffprobeArgs ++ [path])+        ffprobeArgs =+            [ "-v",+              "quiet",+              "-print_format",+              "json",+              "-show_format",+              "-show_streams",+              "-show_chapters"+            ]
+ src/FFProbe/Internal.hs view
@@ -0,0 +1,15 @@+module FFProbe.Internal (parseReadable, parseOptionalValue) where++import Data.Aeson.Types (Parser)+import Text.Read (readMaybe)++-- | applies `readMaybe` to a value, and turns the result into a Parser+parseReadable :: (Read a) => String -> Parser a+parseReadable n = case readMaybe n of+    Nothing -> fail "Failed to read value"+    Just x -> pure x++-- | applies `readMaybe` to a value, and turns the result into a Parser+parseOptionalValue :: (Read a) => Maybe String -> Parser (Maybe a)+parseOptionalValue Nothing = pure Nothing+parseOptionalValue (Just v) = Just <$> parseReadable v
+ test/FFProbe/Data/TestChapter.hs view
@@ -0,0 +1,22 @@+module FFProbe.Data.TestChapter (specs) where++import Data.Aeson+import FFProbe.Data.Chapter+import Test.Hspec+import Utils+import Prelude hiding (id)++specs :: Spec+specs = describe "Chapter Parsing" $ do+    it "Should Parse the correct values" $ do+        rawJson <- getAssetContent "chapter.json"+        shouldBeRight+            (eitherDecodeStrict rawJson)+            ( \chapter -> do+                FFProbe.Data.Chapter.id chapter `shouldBe` 8+                timeBase chapter `shouldBe` "1/1000000000"+                startTime chapter `shouldBe` 0.0+                endTime chapter `shouldBe` 117.56+                length (tags chapter) `shouldBe` 1+                title chapter `shouldBe` Just "Chapter 01"+            )
+ test/FFProbe/Data/TestFormat.hs view
@@ -0,0 +1,28 @@+module FFProbe.Data.TestFormat (specs) where++import Data.Aeson+import FFProbe.Data.Format+import FFProbe.Data.Tags+import Test.Hspec+import Utils+import Prelude hiding (id)++specs :: Spec+specs = describe "Format Parsing" $ do+    it "Should Parse the correct values" $ do+        rawJson <- getAssetContent "format.json"+        shouldBeRight+            (eitherDecodeStrict rawJson)+            ( \format -> do+                filename format `shouldBe` "A.mkv"+                streamsCount format `shouldBe` 5+                programsCount format `shouldBe` 0+                streamGroupsCount format `shouldBe` 0+                formatName format `shouldBe` "matroska,webm"+                formatLongName format `shouldBe` "Matroska / WebM"+                startTime format `shouldBe` 0.0+                duration format `shouldBe` 5272.96+                bitrate format `shouldBe` 5406763+                length (tags format) `shouldBe` 1+                lookupTag "ENCODER" format `shouldBe` Just (StringTag "Lavf60.16.100")+            )
+ test/FFProbe/Data/TestStream.hs view
@@ -0,0 +1,94 @@+module FFProbe.Data.TestStream (specs) where++import Data.Aeson+import FFProbe.Data.Stream+import FFProbe.Data.Tags (TagValue (StringTag), lookupTag)+import Test.Hspec+import Utils+import Prelude hiding (id)++specs :: Spec+specs = describe "Stream Parsing" $ do+    it "Audio Stream" $ do+        rawJson <- getAssetContent "stream-audio.json"+        shouldBeRight+            (eitherDecodeStrict rawJson)+            ( \stream -> do+                index stream `shouldBe` 1+                codecName stream `shouldBe` "pcm_s16le"+                codecLongName stream `shouldBe` "PCM signed 16-bit little-endian"+                codecType stream `shouldBe` "audio"+                streamType stream `shouldBe` AudioStream+                codecTagString stream `shouldBe` "[0][0][0][0]"+                codecTag stream `shouldBe` "0x0000"+                sampleFmt stream `shouldBe` Just "s16"+                sampleRate stream `shouldBe` Just 48000+                channels stream `shouldBe` Just 2+                bitsPerSample stream `shouldBe` Just 16+                rFrameRate stream `shouldBe` "0/0"+                averageFrameRate stream `shouldBe` "0/0"+                rFrameRate stream `shouldBe` "0/0"+                timeBase stream `shouldBe` "1/1000"+                startPts stream `shouldBe` 0+                startTime stream `shouldBe` 0.0+                bitRate stream `shouldBe` Just 1536000+                isDefault (disposition stream) `shouldBe` True+                isForced (disposition stream) `shouldBe` False+                isNonDiegetic (disposition stream) `shouldBe` False+                lookupTag "language" stream `shouldBe` Just (StringTag "eng")+            )+    it "Video Stream" $ do+        rawJson <- getAssetContent "stream-video.json"+        shouldBeRight+            (eitherDecodeStrict rawJson)+            ( \stream -> do+                index stream `shouldBe` 0+                codecName stream `shouldBe` "h264"+                codecLongName stream `shouldBe` "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10"+                codecType stream `shouldBe` "video"+                streamType stream `shouldBe` VideoStream+                codecTagString stream `shouldBe` "[0][0][0][0]"+                codecTag stream `shouldBe` "0x0000"+                width stream `shouldBe` Just 720+                height stream `shouldBe` Just 576+                hasBFrames stream `shouldBe` Just 2+                sampleAspectRatio stream `shouldBe` Just "64:45"+                displayAspectRatio stream `shouldBe` Just "16:9"+                pixFmt stream `shouldBe` Just "yuv420p"+                level stream `shouldBe` Just 30+                colorRange stream `shouldBe` Just "tv"+                fieldOrder stream `shouldBe` Just "tb"+                bitsPerSample stream `shouldBe` Nothing+                rFrameRate stream `shouldBe` "25/1"+                averageFrameRate stream `shouldBe` "25/1"+                timeBase stream `shouldBe` "1/1000"+                startPts stream `shouldBe` 0+                startTime stream `shouldBe` 0.0+                bitRate stream `shouldBe` Nothing+                bitsPerRawSample stream `shouldBe` Just 8+                isDefault (disposition stream) `shouldBe` False+                lookupTag "ENCODER" stream `shouldBe` Just (StringTag "Lavc60.31.102 libx264")+            )+    it "Subtitle Stream" $ do+        rawJson <- getAssetContent "stream-subtitle.json"+        shouldBeRight+            (eitherDecodeStrict rawJson)+            ( \stream -> do+                index stream `shouldBe` 3+                codecName stream `shouldBe` "dvd_subtitle"+                codecLongName stream `shouldBe` "DVD subtitles"+                codecType stream `shouldBe` "subtitle"+                streamType stream `shouldBe` SubtitleStream+                codecTagString stream `shouldBe` "[0][0][0][0]"+                codecTag stream `shouldBe` "0x0000"+                rFrameRate stream `shouldBe` "0/0"+                averageFrameRate stream `shouldBe` "0/0"+                rFrameRate stream `shouldBe` "0/0"+                timeBase stream `shouldBe` "1/1000"+                startPts stream `shouldBe` 0+                startTime stream `shouldBe` 0.0+                duration stream `shouldBe` Just 5272.96+                isDefault (disposition stream) `shouldBe` True+                isForced (disposition stream) `shouldBe` False+                lookupTag "language" stream `shouldBe` Just (StringTag "eng")+            )
+ test/FFProbe/TestFFProbe.hs view
@@ -0,0 +1,27 @@+module FFProbe.TestFFProbe (specs) where++import FFProbe+import FFProbe.Data.Chapter+import FFProbe.Data.Stream+import Test.Hspec+import Utils (shouldBeRight)++specs :: Spec+specs = describe "Running ffprobe" $ do+    it "Should Run ffprobe and give the correct values" $ do+        res <- ffprobe "test/assets/test.mp4"+        shouldBeRight+            res+            ( \ffprobeData -> do+                -- Testing streams+                length (streams ffprobeData) `shouldBe` 3+                let videoStream = filter isVideoStream (streams ffprobeData)+                length videoStream `shouldBe` 1+                let audioStream = filter isAudioStream (streams ffprobeData)+                length audioStream `shouldBe` 1+                -- Testing Chapters+                length (chapters ffprobeData) `shouldBe` 3+                title (head (chapters ffprobeData)) `shouldBe` Just "Start"+                title (chapters ffprobeData !! 1) `shouldBe` Just "Middle"+                title (chapters ffprobeData !! 2) `shouldBe` Just "End"+            )
+ test/Spec.hs view
@@ -0,0 +1,12 @@+import qualified FFProbe.Data.TestChapter as TestChapter+import qualified FFProbe.Data.TestFormat as TestFormat+import qualified FFProbe.Data.TestStream as TestStream+import qualified FFProbe.TestFFProbe as TestFFProbe+import Test.Hspec++main :: IO ()+main = hspec $ do+    TestChapter.specs+    TestFormat.specs+    TestStream.specs+    TestFFProbe.specs
+ test/Utils.hs view
@@ -0,0 +1,11 @@+module Utils where++import Data.ByteString+import Test.Hspec.Expectations++getAssetContent :: String -> IO ByteString+getAssetContent fp = Data.ByteString.readFile $ "test/assets/" <> fp++shouldBeRight :: Either String a -> (a -> Expectation) -> Expectation+shouldBeRight (Left err) _ = expectationFailure err+shouldBeRight (Right value) f = f value