Skip to content

Instantly share code, notes, and snippets.

@igrep
Created February 20, 2021 08:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save igrep/ffd4b91ad80827ab100d9cc719493ec6 to your computer and use it in GitHub Desktop.
Save igrep/ffd4b91ad80827ab100d9cc719493ec6 to your computer and use it in GitHub Desktop.
Download and concatenate videos from a m3u8 file in Twitter etc.
#!/usr/bin/env stack
{- stack --resolver lts-17.4 script
--package=typed-process
--package=filepath
--package=modern-uri
--package=text
-}
{-# LANGUAGE OverloadedStrings #-}
-- Download and concatenate videos from a m3u8 file in Twitter.
-- (But perhaps works for m3u8 files in other sites.)
-- Pass the URL of m3u8 file as the command line argument.
-- Depends on: ffmpeg and wget command
import Control.Monad (unless, guard)
import qualified Data.Text as T
import qualified Data.Text.IO as TI
import Data.Traversable (for)
import qualified Data.List.NonEmpty as NE
import qualified Text.URI as U
import qualified System.Environment as E
import System.Exit (die)
import qualified System.FilePath as F
import qualified System.Process.Typed as P
import qualified Data.Maybe as M
main :: IO ()
main = do
-- Download m3u8 files from the CLI argument
m3uUri <- head <$> E.getArgs
m3uUriObj <- U.mkURI $ T.pack m3uUri
unless (U.isPathAbsolute m3uUriObj) $
die "The given URI isn't absolute!"
P.runProcess_ $ P.proc "wget" [m3uUri]
let m3uFileName =
case U.uriPath m3uUriObj of
Nothing -> "index.m3u8"
Just (hasTrailingSlash, pieces) ->
if hasTrailingSlash
then "index.m3u8"
else T.unpack . U.unRText $ NE.last pieces
-- Download files in the m3u8 file.
let parseM3u8Line ln = do
uriObj <- U.mkURI ln
(hasTrailingSlash, pieces) <- U.uriPath uriObj
let fileName = U.unRText $ NE.last pieces
lookLikeUrlToVideo = T.isSuffixOf ".ts" fileName
guard $ not hasTrailingSlash && lookLikeUrlToVideo
let absUriObj =
if U.isPathAbsolute uriObj
then
uriObj
{ U.uriScheme = U.uriScheme m3uUriObj
, U.uriAuthority = U.uriAuthority m3uUriObj
}
else
uriObj
return (absUriObj, fileName)
videoUriAndNames <- M.mapMaybe parseM3u8Line . T.lines <$> TI.readFile m3uFileName
downloadedVideoNames <- for videoUriAndNames $ \(videoUri, videoName) -> do
P.runProcess_ $ P.proc "wget" ["-O", T.unpack videoName, T.unpack $ U.render videoUri]
return videoName
-- Concatenate downloaded files
let optionsPath = "options-" ++ F.replaceExtension m3uFileName "txt"
outPath = "complete-" ++ F.replaceExtension m3uFileName "ts"
TI.writeFile optionsPath . T.unlines $ map ("file " <>) downloadedVideoNames
P.runProcess_ $ P.proc "ffmpeg" ["-f", "concat", "-i", optionsPath, "-c", "copy", outPath]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment