Skip to content

Commit ef4097f

Browse files
committed
new app videoChapter
1 parent b74ddfb commit ef4097f

File tree

5 files changed

+334
-0
lines changed

5 files changed

+334
-0
lines changed

flake.nix

+2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
booknote = callPackage ./packages/booknote.nix { inherit pdftc; };
3939
mdtopdf = callPackage ./packages/mdtopdf.nix { };
4040
newcover = haskell.packages.ghc948.callCabal2nix "" ./newcover { };
41+
videoChapter =
42+
haskell.packages.ghc96.callCabal2nix "" ./videoChapter { };
4143
pdftc = callPackage ./packages/pdftc.nix { };
4244
seder = callPackage ./packages/seder.nix { };
4345
srtcpy = callPackage ./packages/srtcpy.nix { };

videoChapter/Main.hs

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
{-# LANGUAGE OverloadedStrings #-}
2+
3+
module Main where
4+
5+
import Data.Aeson
6+
import qualified Data.ByteString.Lazy as BL
7+
import Data.Map hiding (mapMaybe, split)
8+
import Data.Maybe
9+
import Data.Text (Text, split, toLower, unpack)
10+
import Data.Text.Lazy ()
11+
import Data.Time.Clock
12+
import Data.Time.Format
13+
import Fmt
14+
import System.IO (stdin)
15+
import Text.Read
16+
17+
newtype Media = Media {track :: [Track]}
18+
deriving (Show)
19+
20+
data Audio = Audio deriving (Show)
21+
data Video = Video deriving (Show)
22+
data General = General deriving (Show)
23+
data Chapter = Chapter Text (Maybe NominalDiffTime) deriving (Show)
24+
25+
instance Buildable Chapter where
26+
build (Chapter title mTime) =
27+
padRightF 10 ' ' (maybe "" f mTime) +| toLower title |+ ""
28+
where
29+
f = formatTime defaultTimeLocale "%02M:%02S"
30+
31+
newtype Menu = Menu {chapters :: [Chapter]} deriving (Show)
32+
33+
data Track = GeneralT General | MenuT Menu | VideoT Video | AudioT Audio
34+
deriving (Show)
35+
36+
instance FromJSON Media where
37+
parseJSON = withObject "root" $ \v -> do
38+
mediaObj <- v .: "media"
39+
ts <- mediaObj .: "track"
40+
pure $ Media ts
41+
42+
instance FromJSON Track where
43+
parseJSON = withObject "track" $ \v -> do
44+
t <- v .: "@type"
45+
case t of
46+
"General" -> pure $ GeneralT General
47+
"Video" -> pure $ VideoT Video
48+
"Audio" -> pure $ AudioT Audio
49+
"Menu" -> do
50+
e <- v .: "extra"
51+
pure . MenuT . Menu $ uncurry toChapter <$> toList e
52+
_ -> fail $ "no good" <> t
53+
readTimeStamp :: Text -> Maybe NominalDiffTime
54+
readTimeStamp stamp =
55+
let xs = split ('_' ==) stamp
56+
in if length xs == 5
57+
then
58+
let
59+
hours :: Maybe Integer
60+
hours = readMaybe . unpack $ xs !! 1
61+
mins = readMaybe . unpack $ xs !! 2
62+
secs = readMaybe . unpack $ xs !! 3
63+
timeSum h m s = h * 3600 + m * 60 + s
64+
in
65+
fromIntegral <$> (timeSum <$> hours <*> mins <*> secs)
66+
else Nothing
67+
68+
toChapter :: Text -> Text -> Chapter
69+
toChapter k v = Chapter v (readTimeStamp k)
70+
71+
toMenu :: Track -> Maybe Menu
72+
toMenu (MenuT m) = Just m
73+
toMenu _ = Nothing
74+
75+
main :: IO ()
76+
main = do
77+
input <- BL.hGetContents stdin
78+
case eitherDecode input of
79+
Left err -> putStrLn $ "Error parsing JSON: " <> err
80+
Right media -> do
81+
fmt . blockListF $ concatMap chapters (mapMaybe toMenu (track media))

videoChapter/concept.md

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[[begin concept]]
2+
ConceptName
3+
Name: video TOC
4+
5+
Purpose: display the chapters of a video as a textual table of contents
6+
7+
State: the only state is the input video name, for now just a file
8+
9+
Actions:
10+
- display(video): text based display of chapters, numbered, with second
11+
resolution timestamps
12+
13+
Operating Principles:
14+
- after a new video is requested, the output is updated. If no chapter
15+
information is found, a display of "no TOC available" is shown
16+
[[end concept]]

videoChapter/technical-plan.md

+207
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
[[begin technical plan]]
2+
3+
[[begin invocation]]
4+
We are going to do all of this from the linux command line. We are going to use
5+
the following command
6+
7+
```
8+
mediainfo --Output=JSON videofile.webm
9+
```
10+
11+
to create a json output of the file. From there we will use a linux pipe to feed
12+
the output json in a haskell execuable.
13+
[[end invocation]]
14+
15+
16+
[[begin haskell executable]]
17+
the haskell executable is build using cabal. it will make use of the aeson
18+
library to parse the json. it will make use of the fmt library to create a
19+
builder for the output. It will make use of optparse-applicative to get the
20+
file name of the video from user input, with a helpful cli based UI to respond
21+
to errors and help requests
22+
[[end haskell executable]]
23+
24+
[[begin sample input 1]]
25+
{
26+
"creatingLibrary":{"name":"MediaInfoLib","version":"24.12","url":"https://mediaarea.net/MediaInfo"},
27+
"media":{"@ref":"/home/hippoid/videos/Rebuilding my NixOS config - Part 17: How to Use NixOS Specializations [fhE08-ZJ-Sk].webm","track":[{"@type":"General","VideoCount":"1",
28+
"AudioCount":"1",
29+
"MenuCount":"1",
30+
"FileExtension":"webm",
31+
"Format":"WebM",
32+
"Format_Version":"4",
33+
"FileSize":"117874810",
34+
"Duration":"975.408",
35+
"OverallBitRate":"966773",
36+
"FrameRate":"30.000",
37+
"FrameCount":"29261",
38+
"IsStreamable":"Yes",
39+
"File_Modified_Date":"2024-11-04 07:47:04 UTC",
40+
"File_Modified_Date_Local":"2024-11-04 01:47:04",
41+
"Encoded_Application":"Lavf61.7.100",
42+
"Encoded_Library":"Lavf61.7.100"},{"@type":"Video","StreamOrder":"0",
43+
"ID":"1",
44+
"UniqueID":"17333972104246241731",
45+
"Format":"VP9",
46+
"Format_Profile":"0",
47+
"CodecID":"V_VP9",
48+
"Duration":"975.366000000",
49+
"Width":"2560",
50+
"Height":"1440",
51+
"Sampled_Width":"2560",
52+
"Sampled_Height":"1440",
53+
"PixelAspectRatio":"1.000",
54+
"DisplayAspectRatio":"1.778",
55+
"FrameRate_Mode":"CFR",
56+
"FrameRate":"30.000",
57+
"FrameCount":"29261",
58+
"ColorSpace":"YUV",
59+
"ChromaSubsampling":"4:2:0",
60+
"BitDepth":"8",
61+
"Delay":"0.000",
62+
"Delay_Source":"Container",
63+
"Language":"en",
64+
"Default":"Yes",
65+
"Forced":"No",
66+
"colour_description_present":"Yes",
67+
"colour_description_present_Source":"Container",
68+
"colour_range":"Limited",
69+
"colour_range_Source":"Container / Stream",
70+
"colour_primaries":"BT.709",
71+
"colour_primaries_Source":"Container",
72+
"transfer_characteristics":"BT.709",
73+
"transfer_characteristics_Source":"Container",
74+
"matrix_coefficients":"BT.709",
75+
"matrix_coefficients_Source":"Container / Stream"},{"@type":"Audio","StreamOrder":"1",
76+
"ID":"2",
77+
"UniqueID":"17934552372420491549",
78+
"Format":"Opus",
79+
"CodecID":"A_OPUS",
80+
"Duration":"975.401000000",
81+
"Channels":"2",
82+
"ChannelPositions":"Front: L R",
83+
"ChannelLayout":"L R",
84+
"SamplingRate":"48000",
85+
"SamplingCount":"46819248",
86+
"BitDepth":"32",
87+
"Compression_Mode":"Lossy",
88+
"Delay":"0.007",
89+
"Delay_Source":"Container",
90+
"Video_Delay":"0.007",
91+
"Language":"en",
92+
"Default":"Yes",
93+
"Forced":"No"},{"@type":"Menu","extra":{"_00_00_00_000":"Introduction to NixOS Specializations","_00_01_04_000":"Setting Up Your First Specialization","_00_03_00_000":"Booting and Testing Specializations","_00_05_20_000":"Switching Between Specializations","_00_10_45_000":"Advanced Specialization Configurations","_00_10_54_000":"Practical Use Cases for Specializations","_00_13_35_000":"Checking Specialization Status","_00_15_21_000":"Conclusion and Final Thoughts"}}]}
94+
}
95+
[[end sample input 1]]
96+
97+
[[begin sample output 1]]
98+
1 introduction to nixos specializations 00:00:00
99+
2 setting up your first specialization 00:01:04
100+
2 booting and testing specializations 00:03:00
101+
4 switching between specializations 00:05:20
102+
5 advanced specialization configurations 00:10:45
103+
6 practical use cases for specializations 00:10:54
104+
7 checking specialization status 00:13:35
105+
8 conclusion and final thoughts 00:15:21
106+
[[end sample input 1]]
107+
108+
[[begin sample input 2]]
109+
{
110+
"creatingLibrary":{"name":"MediaInfoLib","version":"24.12","url":"https://mediaarea.net/MediaInfo"},
111+
"media":{"@ref":"/home/hippoid/videos/Improving Quality of Life with YubiKeys on NixOS [3CeXbONjIgE].webm","track":[{"@type":"General","VideoCount":"1",
112+
"AudioCount":"1",
113+
"MenuCount":"1",
114+
"FileExtension":"webm",
115+
"Format":"WebM",
116+
"Format_Version":"4",
117+
"FileSize":"182515611",
118+
"Duration":"1291.068",
119+
"OverallBitRate":"1130943",
120+
"FrameRate":"30.000",
121+
"FrameCount":"38732",
122+
"IsStreamable":"Yes",
123+
"File_Modified_Date":"2024-10-10 00:44:18 UTC",
124+
"File_Modified_Date_Local":"2024-10-09 19:44:18",
125+
"Encoded_Application":"Lavf61.7.100",
126+
"Encoded_Library":"Lavf61.7.100"},{"@type":"Video","StreamOrder":"0",
127+
"ID":"1",
128+
"UniqueID":"8363614795069664182",
129+
"Format":"VP9",
130+
"Format_Profile":"0",
131+
"CodecID":"V_VP9",
132+
"Duration":"1291.066000000",
133+
"Width":"2560",
134+
"Height":"1440",
135+
"Sampled_Width":"2560",
136+
"Sampled_Height":"1440",
137+
"PixelAspectRatio":"1.000",
138+
"DisplayAspectRatio":"1.778",
139+
"FrameRate_Mode":"CFR",
140+
"FrameRate":"30.000",
141+
"FrameCount":"38732",
142+
"ColorSpace":"YUV",
143+
"ChromaSubsampling":"4:2:0",
144+
"BitDepth":"8",
145+
"Delay":"0.000",
146+
"Delay_Source":"Container",
147+
"Language":"en",
148+
"Default":"Yes",
149+
"Forced":"No",
150+
"colour_description_present":"Yes",
151+
"colour_description_present_Source":"Container",
152+
"colour_range":"Limited",
153+
"colour_range_Source":"Container / Stream",
154+
"colour_primaries":"BT.709",
155+
"colour_primaries_Source":"Container",
156+
"transfer_characteristics":"BT.709",
157+
"transfer_characteristics_Source":"Container",
158+
"matrix_coefficients":"BT.709",
159+
"matrix_coefficients_Source":"Container / Stream"},{"@type":"Audio","StreamOrder":"1",
160+
"ID":"2",
161+
"UniqueID":"3357261617258377654",
162+
"Format":"Opus",
163+
"CodecID":"A_OPUS",
164+
"Duration":"1291.061000000",
165+
"Channels":"2",
166+
"ChannelPositions":"Front: L R",
167+
"ChannelLayout":"L R",
168+
"SamplingRate":"48000",
169+
"SamplingCount":"61970928",
170+
"BitDepth":"32",
171+
"Compression_Mode":"Lossy",
172+
"Delay":"0.007",
173+
"Delay_Source":"Container",
174+
"Video_Delay":"0.007",
175+
"Language":"en",
176+
"Default":"Yes",
177+
"Forced":"No"},{"@type":"Menu","extra":{"_00_00_00_000":"Introduction","_00_01_19_000":"YubiKey overview","_00_01_54_000":"Assumptions","_00_02_14_000":"Yubico apps we'll be using","_00_02_46_000":"Creating a YubiKey module","_00_04_26_000":"Setting a FIDO PIN for YubiKeys","_00_05_15_000":"Generating ssh keys for YubiKeys","_00_06_41_000":"Storing pub keys in nix-config and symlinking them","_00_07_53_000":"Declaring pub keys as authorized_keys","_00_08_32_000":"Storing private keys in nix-secrets, auto-extraction, and symlinking","_00_09_23_000":"Confirming public and private key symlinking","_00_10_35_000":"Handling multiple YubiKeys without the wait time","_00_13_13_000":"Associating the keys to hosts using ssh match blocks","_00_14_29_000":"Testing a passwordless ssh connection","_00_14_47_000":"Touch-based sudo using U2F","_00_16_58_000":"Testing touch-based sudo","_00_17_47_000":"Extras: Notifications when touch is requested","_00_19_08_000":"Extras: Git commit signing","_00_19_32_000":"Extras: LUKS2 decryption","_00_20_32_000":"Extras: Improved physical access to your YubiKey","_00_21_03_000":"Thank you!"}}]}
178+
}
179+
}
180+
[[end sample input 2]]
181+
182+
[[begin sample output 2]]
183+
Menu
184+
1 introduction 00:00:00.000
185+
2 yubikey overview 00:01:19.000
186+
3 assumptions 00:01:54.000
187+
4 yubico apps we'll be using 00:02:14.000
188+
6 creating a yubikey module 00:02:46.000
189+
7 setting a fido pin for yubikeys 00:04:26.000
190+
8 generating ssh keys for yubikeys 00:05:15.000
191+
9 storing pub keys in nix-config and symlinking them 00:06:41.000
192+
10 declaring pub keys as authorized_keys 00:07:53.000
193+
11 storing private keys in nix-secrets, auto-extraction, and symlinking 00:08:32.000
194+
12 confirming public and private key symlinking 00:09:23.000
195+
13 handling multiple yubikeys without the wait time 00:10:35.000
196+
14 associating the keys to hosts using ssh match blocks 00:13:13.000
197+
15 testing a passwordless ssh connection 00:14:29.000
198+
16 touch-based sudo using u2f 00:14:47.000
199+
17 testing touch-based sudo 00:16:58.000
200+
18 extras: notifications when touch is requested 00:17:47.000
201+
19 extras: git commit signing 00:19:08.000
202+
20 extras: luks2 decryption 00:19:32.000
203+
21 extras: improved physical access to your yubikey 00:20:32.000
204+
22 thank you! 00:21:03.000
205+
[[end sample input 2]]
206+
207+
[[end technical plan]]

videoChapter/videoChapter.cabal

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
cabal-version: 3.0
2+
name: videoChapter
3+
version: 0.1.0.0
4+
synopsis: Extract and display video chapter information
5+
description:
6+
extract and display chapter information from video files using mediainfo
7+
8+
license: MIT
9+
10+
common warnings
11+
ghc-options: -Wall
12+
default-language: Haskell2010
13+
default-extensions: NoStarIsType
14+
15+
executable videoChapter
16+
import: warnings
17+
main-is: Main.hs
18+
build-depends:
19+
, aeson
20+
, base ^>=4.18.2.1
21+
, bytestring
22+
, containers
23+
, fmt
24+
, optparse-applicative
25+
, text
26+
, time
27+
28+
ghc-options: -Wall

0 commit comments

Comments
 (0)