Download and concatenate videos from a m3u8 file in Twitter etc.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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