make a parallel directory tree containing mp3 copies of flac, wav, mp3, etc., files in another tree
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Failed to load latest commit information.

FLAC to MP3 via Haskell, Shelly and ffmpeg

Last Modified : 2016 Dec 10 (Sat) 11:28:48 by Harold Carr.

— title: FLAC to MP3 via Haskell, Shelly and ffmpeg tags: haskell, shelly, flac, mp3, 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:

sudo cabal install shelly shelly-extra


@@html: @@

sudo port search ffmpeg
sudo port install ffmpeg @1.2.2

brew search ffmpeg
brew install ffmpeg

ffmpeg -i "/Volumes/my-music/Ahmad Jamal/The Awakening/01-The Awakening.flac" -qscale:a 0 "/tmp/JUNK.mp3"

the code

A Main.main:

Then the real code:

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

module MakeMP3Copies where

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)

-- TODO: get this from Main
toRoot   = "/tmp/JUNK/"

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.

# change next line to location of code
export MP3=~/make-mp3-copies
alias m3='$MP3/.stack-work/dist/x86_64-osx/Cabal-'
pushd "/Volumes/my-music/Ahmad Jamal/"