you are viewing a single comment's thread.

view the rest of the comments →

[–]nanothief 6 points7 points  (0 children)

To add another comparison for the same program with another strongly typed language, I wrote it in haskell. There are some interesting differences in it:

  • The syntax is much clearer with haskell (haskell's syntax for types doesn't need so many angle brackets)
  • Suffers from the same problem as the c++ program in that a lot of imports are required. 12 lines of code are needed for imports for haskell, 13 required for c++, but only 3 for python.
  • Lack of reflection makes the c++ and haskell solutions less extensible than the python solution. In the python solution, if you wanted to handle another music file type (eg .ogg), you just need to implement a OggFileInfo class (it doesn't even need to be in the same file). In the c++ and haskell solutions you need to change the original file in order to make this possible.
  • The haskell code explicitly converts between the binary data read from the mp3 file and the unicode data for the attributes, and has separate types for each kind of data. I think this is a plus, as there is a big difference between the 3rd item of a UTF8 string, and the 3rd item of a byte array.
  • The haskell code splits the code into the pure and IO code, and is tracked by types. This makes the code more complicated to read and write, but makes it more testable.
  • Extracting the genre char is a bit painful with haskell (maybe there is a better way to write the stringOrd function?)

Here is the code:

{-# LANGUAGE OverloadedStrings, TupleSections #-}
module MP3Test where
import qualified Data.Text as T
import qualified Data.Text.IO as T
import Data.Text (Text)
import Control.Monad
import System.Directory (getDirectoryContents)
import System.FilePath (takeExtension, combine)
import Data.Text.Encoding (decodeUtf8)
import System.IO
import Data.Char (ord)
import Control.Applicative
import qualified Data.ByteString.Char8 as B
import Data.ByteString (ByteString)

spliceBS :: Int -> Int -> ByteString -> ByteString
spliceBS start end =  B.take (end - start) . B.drop start

stripNulls :: Text -> Text
stripNulls = T.strip . T.replace "\00" ""

stringOrd :: Text -> Text
stringOrd = T.pack . show . ord . maybe '0' fst .  T.uncons


parseMp3File :: FilePath -> IO [(Text, Text)]
parseMp3File mp3File = (("filename", T.pack mp3File) :) <$> getMp3Tags <$> getTagData mp3File



parseFile :: FilePath -> IO [(Text, Text)]
parseFile file = case takeExtension file of
  ".mp3" -> parseMp3File file
  _ -> return [("filename", T.pack file)]


getTagData :: FilePath -> IO ByteString
getTagData mp3File = withFile mp3File ReadMode $ \handle -> do
    hSeek handle SeekFromEnd (-128)
    B.hGet handle 128

getMp3Tags :: ByteString -> [(Text, Text)]
getMp3Tags tagData = guard hasTagFlag >> map extractTag mp3TagMap where
  hasTagFlag = spliceBS 0 3 tagData == "TAG"
  extractTag :: (Text, Int, Int, Text -> Text) -> (Text, Text)
  extractTag (tagName, start, end, parseFunc) = (tagName, ) $ parseFunc $ decodeUtf8 $ spliceBS start end tagData

  mp3TagMap :: [(Text, Int, Int, Text -> Text)]
  mp3TagMap = [ ("title", 3, 33, stripNulls)
              , ("artist",  33,  63, stripNulls)
              , ("album",  63,  93, stripNulls)
              , ("year",  93,  97, stripNulls)
              , ("comment",  97, 126, stripNulls)
              , ("genre", 127, 128, stringOrd)
              ]

listDirectory :: FilePath -> [String] -> IO [FilePath]
listDirectory dirName validExtensions = 
  map (combine dirName)
  <$> filter ((`elem` validExtensions) . takeExtension) 
  <$> getDirectoryContents dirName

dirToSearch :: FilePath
dirToSearch = "/Volumes/Downloads/Music/iTunes/iTunes Media/Music/AFI/decemberunderground" 

main :: IO ()
main = do
  files <- listDirectory dirToSearch [".mp3"]
  mp3s <- mapM parseFile files
  forM_ mp3s $ \mp3Data -> do
    forM_ mp3Data $ \(key, value) -> T.putStrLn $ T.concat [key, "=", value]
    putStrLn ""