FLAC to MP3 via Haskell, Shelly and ffmpeg

I have a large digitized music collection, primarily encoded in lossless FLAC. Great for listening at home. But lossy MP3 is best for mobile devices, in terms of size and the ability of players to handle encoding formats.

So I wrote a script to make parallel MP3 tree of my canonical music collection (which also includes FLAC, WAV, AIFF, MP3, …).

I wrote it in Haskell, with the help of the Shelly shell scripting DSL and ffmpeg.

I am not an experienced Haskell/Shelly programmer. If you have suggestions for improvements please let me know (including useful ffmpeg settings).


Other haskell shell examples: http://www.haskell.org/haskellwiki/Applications_and_libraries/Operating_system#Haskell_shell_examples

sudo cabal install shelly shelly-extra



sudo port search ffmpeg
sudo port install ffmpeg @1.2.2
ffmpeg -i "/Volumes/my-music/Ahmad Jamal/The Awakening/01-The Awakening.flac" -qscale:a 0 "/tmp/JUNK.mp3"

the code


{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ExtendedDefaultRules #-}
{-# OPTIONS_GHC -fno-warn-type-defaults #-}

import           Control.Applicative
import           Control.Exception (bracket, handle, SomeException)
import           Control.Monad
import           Control.Monad.IO.Class
import           Data.Maybe (fromJust)
import qualified Data.Text as T
import           Shelly
import           System.Directory
import           System.FilePath
import           System.IO (IOMode(..), hClose, hFileSize, openFile)
default (T.Text)

fromRoot = "."
toRoot   = "/tmp/JUNK/"

main = shelly $ verbosely $ do
    processDir fromRoot

processDir fromPath = do
    contents <- ls fromPath
    forM_ contents $ \from -> do
            isDir <- test_d from
            if isDir
                then do { maybeCreateDir from; processDir from}
                else processFile from $ takeExtension $ fpToString from

maybeCreateDir from = do
    let to = mkToFilePath from
    dirExists <- test_d to
    if dirExists
        then                  say "DIR EXISTS"  to
        else do { mkdir_p to; say "DIR CREATED" to }

processFile from ".flac" = maybeDo convert True  False from
processFile from ".mp3"  = maybeDo copy    False True  from
processFile from ".jpg"  = maybeDo copy    False True  from
processFile from _       = say "IGNORED" from

maybeDo f extP sizeP from = do
    let to = mkToFilePath $ if extP then (fromText (T.pack (replaceExtension (fpToString from) ".mp3"))) else from
    fileExists <- test_f to
    if fileExists
        then doIf f sizeP from to
        else      f       from to

doIf f sizeP from to = do
    fromSize <- lio getFileSize         from
    fromTime <- lio getModificationTime from
    toSize   <- lio getFileSize         to
    toTime   <- lio getModificationTime to
    if fromTime > toTime || (sizeP && (fromJust fromSize) /= (fromJust toSize))
        then f from to
        else say "FILE EXISTS" to

convert from to = do
    flacToMp3 (toTextIgnore from) (toTextIgnore to)
    say "FILE CONVERTED" to
    flacToMp3 from to = run_ "ffmpeg" ["-i", from, "-qscale:a", "0", to]

copy from to = do
    cp from to
    say "FILE COPIED" to

mkToFilePath path =
    (fpToString toRoot) Shelly.</> (fpToString path)

fpToString fp = T.unpack $ toTextIgnore fp

say msg fp =
    liftIO $ putStrLn $ show (fpToString fp) ++ " " ++ msg

lio f fp =
    liftIO . f $ fpToString fp

-- from Real World Haskell
getFileSize path = handle handler $
    bracket (openFile path ReadMode) (hClose) (\h -> do
        size <- hFileSize h
        return $ Just size)
    handler :: SomeException -> IO (Maybe Integer)
    handler _ = return Nothing

-- End of file.


In the present version, cd to the root of the canonical music collection and run the script. The output location is hardwired in the code.

export MP3=~/make-mp3-copies
alias m3='$MP3/MakeMP3Copies'
cd $MP3
ghc MakeMP3Copies.hs
export PATH=.:$PATH
pushd "/Volumes/my-music/Ahmad Jamal/"
comments powered by Disqus