diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -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!
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -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)
+```
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/haskell-ffprobe.cabal b/haskell-ffprobe.cabal
new file mode 100644
--- /dev/null
+++ b/haskell-ffprobe.cabal
@@ -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
diff --git a/src/FFProbe.hs b/src/FFProbe.hs
new file mode 100644
--- /dev/null
+++ b/src/FFProbe.hs
@@ -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
diff --git a/src/FFProbe/Data/Chapter.hs b/src/FFProbe/Data/Chapter.hs
new file mode 100644
--- /dev/null
+++ b/src/FFProbe/Data/Chapter.hs
@@ -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 {..}
diff --git a/src/FFProbe/Data/Format.hs b/src/FFProbe/Data/Format.hs
new file mode 100644
--- /dev/null
+++ b/src/FFProbe/Data/Format.hs
@@ -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 {..}
diff --git a/src/FFProbe/Data/Stream.hs b/src/FFProbe/Data/Stream.hs
new file mode 100644
--- /dev/null
+++ b/src/FFProbe/Data/Stream.hs
@@ -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 {..}
diff --git a/src/FFProbe/Data/Tags.hs b/src/FFProbe/Data/Tags.hs
new file mode 100644
--- /dev/null
+++ b/src/FFProbe/Data/Tags.hs
@@ -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)
diff --git a/src/FFProbe/Data/Tags/Internal.hs b/src/FFProbe/Data/Tags/Internal.hs
new file mode 100644
--- /dev/null
+++ b/src/FFProbe/Data/Tags/Internal.hs
@@ -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
diff --git a/src/FFProbe/Exec.hs b/src/FFProbe/Exec.hs
new file mode 100644
--- /dev/null
+++ b/src/FFProbe/Exec.hs
@@ -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"
+            ]
diff --git a/src/FFProbe/Internal.hs b/src/FFProbe/Internal.hs
new file mode 100644
--- /dev/null
+++ b/src/FFProbe/Internal.hs
@@ -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
diff --git a/test/FFProbe/Data/TestChapter.hs b/test/FFProbe/Data/TestChapter.hs
new file mode 100644
--- /dev/null
+++ b/test/FFProbe/Data/TestChapter.hs
@@ -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"
+            )
diff --git a/test/FFProbe/Data/TestFormat.hs b/test/FFProbe/Data/TestFormat.hs
new file mode 100644
--- /dev/null
+++ b/test/FFProbe/Data/TestFormat.hs
@@ -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")
+            )
diff --git a/test/FFProbe/Data/TestStream.hs b/test/FFProbe/Data/TestStream.hs
new file mode 100644
--- /dev/null
+++ b/test/FFProbe/Data/TestStream.hs
@@ -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")
+            )
diff --git a/test/FFProbe/TestFFProbe.hs b/test/FFProbe/TestFFProbe.hs
new file mode 100644
--- /dev/null
+++ b/test/FFProbe/TestFFProbe.hs
@@ -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"
+            )
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -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
diff --git a/test/Utils.hs b/test/Utils.hs
new file mode 100644
--- /dev/null
+++ b/test/Utils.hs
@@ -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
