Skip to content

Instantly share code, notes, and snippets.

@zaphar
Created May 18, 2011 05:28
Show Gist options
  • Save zaphar/978039 to your computer and use it in GitHub Desktop.
Save zaphar/978039 to your computer and use it in GitHub Desktop.
A simple haskell script to copy movie files from a directory to a storage area
#!/usr/bin/env runhaskell
import System.Directory
import Data.Char
import Data.Monoid
import Data.Maybe
import System.IO
import System (getArgs)
import Text.Printf
data VidFile = VidFile {
showTitle :: String
, year :: Maybe Integer
, season :: Maybe Integer
, episode :: Maybe Integer
, fileExt :: String
, file :: FilePath
}
deriving (Show)
data ParseState = ParseState VidFile String
deriving (Show)
emptyVidFile = VidFile {
showTitle = ""
, year = Nothing
, season = Nothing
, episode = Nothing
, fileExt = ""
, file = ""
}
concatMaybe (Just a) Nothing = Just a
concatMaybe _ Nothing = Nothing
concatMaybe _ (Just b) = Just b
combineVidFiles vf vf' = vf { showTitle = (showTitle vf) ++ (showTitle vf')
, year = concatMaybe (year vf) $ year vf'
, season = concatMaybe (season vf) $ season vf'
, episode = concatMaybe (episode vf) $ episode vf'
, fileExt = (fileExt vf) ++ (fileExt vf')
, file = (file vf) ++ (file vf')
}
instance Monoid VidFile where
mempty = emptyVidFile
mappend = combineVidFiles
isIntString :: String -> Bool
isIntString s = isIntString' True s
where isIntString' b "" = b
isIntString' b (c : tl) =
b && case c of
'1' -> isIntString' True tl
'2' -> isIntString' True tl
'3' -> isIntString' True tl
'4' -> isIntString' True tl
'5' -> isIntString' True tl
'6' -> isIntString' True tl
'7' -> isIntString' True tl
'8' -> isIntString' True tl
'9' -> isIntString' True tl
'0' -> isIntString' True tl
_ -> False
isSeasonString :: String -> Bool
isSeasonString ('S' : tl) = (length tl == 2) && (isIntString tl)
isSeasonString ('s' : tl) = (length tl == 2) && (isIntString tl)
parseTitle :: ParseState -> ParseState
parseTitle (ParseState vf "") = ParseState vf ""
parseTitle (ParseState vf rest) =
let p = take 4 rest
suf = drop 4 rest
in case p of
('S' : tl) -> handleSeasonCase ('S':tl)
('s' : tl) -> handleSeasonCase ('s':tl)
_ -> if isIntString p -- probably a year
then ParseState (stripDot vf) rest -- terminataion state
else recurse rest -- tail recursion
where recurse s = let hd = take 1 s
tl = tail s
vf' = mappend vf $ emptyVidFile {showTitle = hd}
in parseTitle $ ParseState vf' tl
stripDot vf =
let title = showTitle vf
len = (length title) - 1
in if last (showTitle vf) == '.'
then vf {showTitle = take len title}
else vf
handleSeasonCase p =
if isSeasonString (take 3 p) -- season information
then ParseState (stripDot vf) rest -- termination state
else recurse rest -- tail recursion
parseSeason :: ParseState -> ParseState
parseSeason (ParseState vf (c:tl))
| c == 'S' || c == 's' =
let p = take 2 tl
suf = drop 2 tl
vf' = mappend vf $ emptyVidFile {season = Just (read p::Integer)}
in if isIntString p
then ParseState vf' suf
else ParseState vf (c:tl)
| otherwise = ParseState vf (c:tl)
parseEpisode :: ParseState -> ParseState
parseEpisode (ParseState vf (c:tl))
| c == 'E' || c == 'e' =
let p = take 2 tl
suf = drop 2 tl
vf' = mappend vf $ emptyVidFile {episode = Just (read p::Integer)}
in if isIntString p
then ParseState vf' suf
else ParseState vf (c:tl)
| otherwise = ParseState vf (c:tl)
parseSeasonAndEpisode = parseEpisode . parseSeason
parseYear :: ParseState -> ParseState
parseYear (ParseState vf rest) =
let yr = take 4 rest
suf = drop 4 rest
vf' = mappend vf $ emptyVidFile {year = Just (read yr::Integer)}
in if isIntString yr
then ParseState vf' suf
else ParseState vf rest
parseYearOrSeason (ParseState vf (c:tl))
| c == 'S' || c == 's' = parseSeasonAndEpisode $ ParseState vf (c:tl)
| otherwise = parseYear $ ParseState vf (c:tl)
parseExtension :: ParseState -> ParseState
parseExtension (ParseState vf rest) =
let readExt acc (c:tl) =
if c == '.'
then acc
else readExt (c:acc) tl
ext = readExt "" $ reverse rest
len = (length rest) - (length ext)
vf' = mappend vf $ emptyVidFile {fileExt = ext}
in ParseState vf' $ take len rest
dropDot (ParseState vf ('.':tl)) = ParseState vf tl
dropDot p = p
parseAll s =
parseExtension $
parseYearOrSeason $ dropDot $
parseYearOrSeason $ dropDot $
parseTitle (ParseState emptyVidFile s)
parse s = let ParseState vf _ = parseAll s
in vf {file = s}
isFileType :: String -> String -> Bool
isFileType t fp = let t' = ('.' : t)
sz = ((length fp) - (length t'))
fpEnd = drop sz fp
in if fpEnd == t'
then True
else False
getFilesOfType d t = do
fs <- getDirectoryContents d
let fs' = filter (isFileType t) fs
return fs'
myWords :: String -> [String]
myWords s = case dropWhile isDot s of
"" -> []
s' -> w : myWords s''
where (w, s'') =
break isDot s'
where isDot = (\c -> c == '.')
dotize [] = ""
dotize [w] = w
dotize (w:ws) = w ++ '.' : dotize ws
capitalize = dotize . map capitalize' . myWords
where capitalize' "" = ""
capitalize' (c:tl) = toUpper c : map toLower tl
fileName vf = let title = capitalize (showTitle vf)
Just s = (season vf)
Just e = (episode vf)
ext = (fileExt vf)
in title ++ ".S" ++ (printf "%02d" s)
++ "E" ++ (printf "%02d" e)
++ "." ++ ext
writeFileToDest :: FilePath -> VidFile -> IO ()
writeFileToDest path vf = do
let title = capitalize (showTitle vf)
f = file vf
dest = path ++ "/" ++ title
f' = dest ++ "/" ++ (fileName vf)
createDirectoryIfMissing True dest
hPutStrLn stderr ("copying " ++ f ++ " to " ++ f')
copyFile f f'
writeFilesToDest :: FilePath -> [VidFile] -> IO ()
writeFilesToDest path [] = do return ()
writeFilesToDest path (h:tl) = do
writeFileToDest path h
writeFilesToDest path tl
run :: FilePath -> IO ()
run path = do
d <- getCurrentDirectory
fs <- getFilesOfType d "avi"
let vfs = map parse fs
writeFilesToDest path vfs
putStrLn $ show $ map file vfs
main :: IO ()
main = do
args <- getArgs
let dest = head args
run dest
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment