first working version
This commit is contained in:
parent
d5c2dc517e
commit
18fdfd9780
|
@ -1,33 +0,0 @@
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
|
||||||
|
|
||||||
module Clients where
|
|
||||||
|
|
||||||
import Control.Monad (replicateM)
|
|
||||||
import System.Random (randomRIO)
|
|
||||||
import qualified Database.Redis as R
|
|
||||||
import qualified Data.ByteString.Char8 as BC
|
|
||||||
|
|
||||||
idIsFree :: BC.ByteString -> R.Connection -> IO Bool
|
|
||||||
idIsFree id rConn = do
|
|
||||||
response <- R.runRedis rConn (R.get id)
|
|
||||||
pure $ case response of
|
|
||||||
(Left _) -> False -- maybe error here? look into what causes this more
|
|
||||||
(Right x) ->
|
|
||||||
case x of
|
|
||||||
(Just _) -> True
|
|
||||||
_ -> False
|
|
||||||
|
|
||||||
genClientId :: IO String
|
|
||||||
genClientId = replicateM 8 (randomRIO ('a', 'z'))
|
|
||||||
|
|
||||||
-- todo: config file
|
|
||||||
maxClients :: Int
|
|
||||||
maxClients = 100
|
|
||||||
|
|
||||||
getClients :: IO Int
|
|
||||||
getClients = undefined
|
|
||||||
|
|
||||||
acceptingClients :: IO Bool
|
|
||||||
acceptingClients = do
|
|
||||||
clients <- getClients
|
|
||||||
pure $ clients < maxClients
|
|
|
@ -1,10 +1,12 @@
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
module Helpers where
|
module Helpers where
|
||||||
|
|
||||||
import YTDL
|
import YTDL
|
||||||
import qualified Data.Text.Lazy as TL
|
import qualified Data.Text.Lazy as TL
|
||||||
import Network.URI (parseURI)
|
import Network.URI (parseURI)
|
||||||
|
|
||||||
getRes :: String -> Maybe Resolution
|
getRes :: TL.Text -> Maybe Resolution
|
||||||
getRes ("144p") = Just P144
|
getRes ("144p") = Just P144
|
||||||
getRes ("240p") = Just P240
|
getRes ("240p") = Just P240
|
||||||
getRes ("360p") = Just P360
|
getRes ("360p") = Just P360
|
||||||
|
@ -16,7 +18,7 @@ getRes ("audio") = Just Audio
|
||||||
getRes _ = Nothing
|
getRes _ = Nothing
|
||||||
|
|
||||||
isRes :: TL.Text -> Bool
|
isRes :: TL.Text -> Bool
|
||||||
isRes res = case getRes (TL.unpack res) of
|
isRes res = case getRes res of
|
||||||
(Just _) -> True
|
(Just _) -> True
|
||||||
_ -> False
|
_ -> False
|
||||||
|
|
||||||
|
|
46
src/Main.hs
46
src/Main.hs
|
@ -3,46 +3,40 @@
|
||||||
module Main where
|
module Main where
|
||||||
|
|
||||||
import Templates.Index
|
import Templates.Index
|
||||||
import Templates.Loading
|
|
||||||
import Templates.Error
|
import Templates.Error
|
||||||
import YTDL
|
import YTDL
|
||||||
import Clients
|
|
||||||
import Helpers
|
import Helpers
|
||||||
|
|
||||||
import Control.Monad.IO.Class
|
import Control.Monad.IO.Class
|
||||||
import qualified Database.Redis as R
|
|
||||||
import qualified Data.Text.Lazy as TL
|
import qualified Data.Text.Lazy as TL
|
||||||
import Web.Scotty
|
import Web.Scotty
|
||||||
|
|
||||||
-- todo ReaderT
|
safeDownloadAction :: ActionM ()
|
||||||
app :: R.Connection -> ScottyM ()
|
safeDownloadAction = downloadAction `rescue` (html . errorPage)
|
||||||
app rConn = do
|
|
||||||
get "/" $ do
|
|
||||||
html indexPage
|
|
||||||
|
|
||||||
post "/" $ do
|
downloadAction :: ActionM ()
|
||||||
|
downloadAction = do
|
||||||
url <- param "url"
|
url <- param "url"
|
||||||
res <- param "resolution"
|
res <- param "resolution"
|
||||||
if (isURL url) && (isRes res)
|
if (isURL url) && (isRes res)
|
||||||
then do
|
then do
|
||||||
queueOK <- liftIO acceptingClients
|
let (Just res') = getRes res -- safe cause we checked with isRes
|
||||||
if queueOK
|
ytdlRes <- liftIO $ ytdl (TL.unpack url) res'
|
||||||
then do
|
case ytdlRes of
|
||||||
html loadingPage
|
(Right filePath) -> do
|
||||||
-- set redis stuff and id here
|
setHeader "content-type" "video/mp4"
|
||||||
-- redirect $ TL.pack $ '/':id
|
file filePath
|
||||||
else
|
(Left err) -> html $ errorPage (TL.pack err)
|
||||||
html $ errorPage "Too many clients right now. Try again later!"
|
-- liftIO $ ytdlClean ident
|
||||||
else
|
else
|
||||||
html $ errorPage "Invalid input!"
|
html $ errorPage "Invalid input!"
|
||||||
|
|
||||||
get "/:id" $ do
|
-- todo ReaderT
|
||||||
-- grab id and process video if not already done
|
app :: ScottyM ()
|
||||||
id <- param "id"
|
app = do
|
||||||
html id
|
get "/" $ html indexPage
|
||||||
|
get "/video.mp4" safeDownloadAction
|
||||||
|
get "/audio.mp3" safeDownloadAction
|
||||||
|
|
||||||
main :: IO ()
|
main :: IO ()
|
||||||
main = do
|
main = scotty 3000 app
|
||||||
-- todo: parse connection config
|
|
||||||
rConn <- R.connect R.defaultConnectInfo
|
|
||||||
scotty 3000 (app rConn)
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ indexPage = [r|
|
||||||
<tr>
|
<tr>
|
||||||
<td valign="top">
|
<td valign="top">
|
||||||
<h2>Video download</h2>
|
<h2>Video download</h2>
|
||||||
<form method="post" action="/">
|
<form method="get" action="/video.mp4">
|
||||||
<input required name="url" type="text" placeholder="Enter url here"><br>
|
<input required name="url" type="text" placeholder="Enter url here"><br>
|
||||||
<label for="resolution">res</label>
|
<label for="resolution">res</label>
|
||||||
<select required name="resolution">
|
<select required name="resolution">
|
||||||
|
@ -39,7 +39,7 @@ indexPage = [r|
|
||||||
</td>
|
</td>
|
||||||
<td valign="top">
|
<td valign="top">
|
||||||
<h2>Audio only download</h2>
|
<h2>Audio only download</h2>
|
||||||
<form method="post" action="/">
|
<form method="get" action="/audio.mp3">
|
||||||
<input required name="url" type="text" placeholder="Enter url here"><br>
|
<input required name="url" type="text" placeholder="Enter url here"><br>
|
||||||
<input type="hidden" name="resolution" value="audio">
|
<input type="hidden" name="resolution" value="audio">
|
||||||
<input type="submit" value="Download">
|
<input type="submit" value="Download">
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
|
||||||
{-# LANGUAGE QuasiQuotes #-}
|
|
||||||
|
|
||||||
module Templates.Loading (loadingPage) where
|
|
||||||
|
|
||||||
import qualified Data.Text.Lazy as TL
|
|
||||||
import Text.RawString.QQ
|
|
||||||
|
|
||||||
loadingPage :: TL.Text
|
|
||||||
loadingPage = [r|
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<head>
|
|
||||||
<meta cherset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>viddl</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<center>
|
|
||||||
<h1>viddl</h1>
|
|
||||||
<p>Your request is being processed... Please wait...</p>
|
|
||||||
<hr>
|
|
||||||
<p>viddl is free <a href="https://github.com/depsterr/viddl">open source</a> software and is powered by <a href="https://yt-dl.org/">youtube-dl</a>.</p>
|
|
||||||
</center>
|
|
||||||
</body>
|
|
||||||
|]
|
|
72
src/YTDL.hs
72
src/YTDL.hs
|
@ -1,5 +1,8 @@
|
||||||
module YTDL (downloadVideo, downloadAudio, Resolution(..), ytdl) where
|
module YTDL (Resolution(..), ytdl) where
|
||||||
|
|
||||||
|
import Control.Concurrent (forkIO, threadDelay)
|
||||||
|
import Data.Digest.Pure.MD5
|
||||||
|
import qualified Data.ByteString.Lazy.UTF8 as BCU
|
||||||
import System.Directory
|
import System.Directory
|
||||||
import System.Exit
|
import System.Exit
|
||||||
import System.Process
|
import System.Process
|
||||||
|
@ -30,38 +33,49 @@ resToArgs (P1080) = wrapResString "1080"
|
||||||
resToArgs (PMAX) = ["-f", "bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4"]
|
resToArgs (PMAX) = ["-f", "bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4"]
|
||||||
resToArgs (Audio) = ["-x", "--audio-format", "mp3"]
|
resToArgs (Audio) = ["-x", "--audio-format", "mp3"]
|
||||||
|
|
||||||
downloadVideo :: String -> String -> Resolution -> IO (Either String FilePath)
|
ytdl :: String -> Resolution -> IO (Either String FilePath)
|
||||||
downloadVideo client url res = ytdl client url res
|
ytdl url res = do
|
||||||
|
|
||||||
downloadAudio :: String -> String -> IO (Either String FilePath)
|
|
||||||
downloadAudio client url = ytdl client url Audio
|
|
||||||
|
|
||||||
ytdl :: String -> String -> Resolution -> IO (Either String FilePath)
|
|
||||||
ytdl client url res = do
|
|
||||||
let ext = case res of { Audio -> ".mp3"; _ -> ".mp4" }
|
let ext = case res of { Audio -> ".mp3"; _ -> ".mp4" }
|
||||||
|
|
||||||
|
let ident = show . md5 . BCU.fromString $ url <> show res
|
||||||
|
|
||||||
-- todo: config for path
|
-- todo: config for path
|
||||||
tmpdir <- getTemporaryDirectory
|
tmpdir <- getTemporaryDirectory
|
||||||
let dir = concat [tmpdir, "/viddl/", client]
|
let dir = concat [tmpdir, "/viddl/", ident]
|
||||||
let fileName = concat [dir, "/", client, ext]
|
let fileName = concat [dir, "/", ident, ext]
|
||||||
createDirectoryIfMissing True dir
|
|
||||||
|
|
||||||
ytdlProc <- createProcess (proc "youtube-dl" (resToArgs res <> ["-o", fileName, url]))
|
-- todo: implement file locking for deleting and uploading file
|
||||||
{ std_out = CreatePipe
|
processed <- doesFileExist fileName
|
||||||
, std_err = CreatePipe }
|
if processed
|
||||||
|
then do
|
||||||
|
putStrLn $ "Returning existing ident " <> ident
|
||||||
|
pure (Right fileName)
|
||||||
|
else do
|
||||||
|
putStrLn $ "Processing new ident " <> ident
|
||||||
|
|
||||||
case ytdlProc of
|
createDirectoryIfMissing True dir
|
||||||
(_, _, Just herr, ph) -> do
|
|
||||||
err <- hGetContents herr
|
|
||||||
exitCode <- waitForProcess ph
|
|
||||||
case exitCode of
|
|
||||||
ExitSuccess -> do
|
|
||||||
exists <- doesFileExist fileName
|
|
||||||
if exists
|
|
||||||
then pure (Right fileName)
|
|
||||||
else do
|
|
||||||
removeDirectory dir
|
|
||||||
pure (Left "An unknown error prevented the output file from being created")
|
|
||||||
|
|
||||||
(ExitFailure status) -> pure (Left (concat ["execution failed with status ", show status, ": ", err]))
|
ytdlProc <- createProcess (proc "youtube-dl" (resToArgs res <> ["-o", fileName, url]))
|
||||||
|
{ std_out = CreatePipe
|
||||||
|
, std_err = CreatePipe }
|
||||||
|
|
||||||
_ -> pure (Left "Unable to create ytdlProcess for downloading video")
|
case ytdlProc of
|
||||||
|
(_, _, Just herr, ph) -> do
|
||||||
|
err <- hGetContents herr
|
||||||
|
exitCode <- waitForProcess ph
|
||||||
|
case exitCode of
|
||||||
|
ExitSuccess -> do
|
||||||
|
exists <- doesFileExist fileName
|
||||||
|
if exists
|
||||||
|
then do
|
||||||
|
-- wait 5 minutes and then delete the directory
|
||||||
|
-- todo: implement file locking for deleting and uploading file
|
||||||
|
_ <- forkIO $ threadDelay 300000000 >> removeDirectoryRecursive dir
|
||||||
|
pure (Right fileName)
|
||||||
|
else do
|
||||||
|
removeDirectoryRecursive dir
|
||||||
|
pure (Left "An unknown error prevented the output file from being created")
|
||||||
|
|
||||||
|
(ExitFailure status) -> pure (Left (concat ["execution failed with status ", show status, ": ", err]))
|
||||||
|
|
||||||
|
_ -> pure (Left "Unable to create ytdlProcess for downloading video")
|
||||||
|
|
7
todo
7
todo
|
@ -1,5 +1,2 @@
|
||||||
bindings to python youtube-dl library, avoid invoking it as a program at all costs
|
configuration for scotty
|
||||||
|
cache results somehow
|
||||||
configuration for scotty and redis, and some variables in the program
|
|
||||||
|
|
||||||
maybe add some basic editing functionality eventually (ffmpeg?)
|
|
||||||
|
|
19
viddl.cabal
19
viddl.cabal
|
@ -14,32 +14,39 @@ build-type: Simple
|
||||||
|
|
||||||
library
|
library
|
||||||
exposed-modules: Templates.Index
|
exposed-modules: Templates.Index
|
||||||
, Templates.Loading
|
|
||||||
, Templates.Error
|
, Templates.Error
|
||||||
, YTDL
|
, YTDL
|
||||||
, Clients
|
|
||||||
, Helpers
|
, Helpers
|
||||||
ghc-options: -Wall -O2
|
ghc-options: -Wall -O2
|
||||||
build-depends: base ^>=4.14.1.0
|
build-depends: base ^>=4.14.1.0
|
||||||
, scotty
|
, scotty
|
||||||
, hedis
|
|
||||||
, transformers
|
|
||||||
, text
|
, text
|
||||||
, raw-strings-qq
|
, raw-strings-qq
|
||||||
, network-uri
|
, network-uri
|
||||||
, directory
|
, directory
|
||||||
, process
|
, process
|
||||||
, bytestring
|
, bytestring
|
||||||
, random
|
, pureMD5
|
||||||
|
, utf8-string
|
||||||
hs-source-dirs: src
|
hs-source-dirs: src
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
|
|
||||||
executable viddl
|
executable viddl
|
||||||
main-is: Main.hs
|
main-is: Main.hs
|
||||||
|
other-modules: Templates.Index
|
||||||
|
, Templates.Error
|
||||||
|
, Helpers
|
||||||
|
, YTDL
|
||||||
ghc-options: -Wall -O2
|
ghc-options: -Wall -O2
|
||||||
build-depends: base ^>=4.14.1.0
|
build-depends: base ^>=4.14.1.0
|
||||||
, scotty
|
, scotty
|
||||||
, hedis
|
|
||||||
, text
|
, text
|
||||||
|
, raw-strings-qq
|
||||||
|
, network-uri
|
||||||
|
, directory
|
||||||
|
, process
|
||||||
|
, bytestring
|
||||||
|
, pureMD5
|
||||||
|
, utf8-string
|
||||||
hs-source-dirs: src
|
hs-source-dirs: src
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
|
|
Loading…
Reference in New Issue
Block a user