From a7fca1c9fa5500e1faf526f961eb190415261b14 Mon Sep 17 00:00:00 2001 From: Eugen Wissner Date: Tue, 20 Jan 2026 19:13:31 +0100 Subject: Rewrite 7digital in haskell --- 7digital/Main.hs | 105 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 99 insertions(+), 6 deletions(-) (limited to '7digital/Main.hs') diff --git a/7digital/Main.hs b/7digital/Main.hs index 26cdeb3..9f1800e 100644 --- a/7digital/Main.hs +++ b/7digital/Main.hs @@ -1,9 +1,102 @@ -import Control.Monad (void) -import System.Process (rawSystem) +module Main + ( main + ) where + +import Codec.Archive.Zip + ( saveEntry + , unEntrySelector + , forEntries + , withArchive + , getEntryName + ) +import Data.Text (StrictText) +import qualified Data.Text as StrictText +import qualified Data.Text.IO as StrictText +import System.Directory (createDirectoryIfMissing, listDirectory, renameFile) +import System.Process (readProcessWithExitCode) import System.Environment (getArgs) +import System.IO (hPutStrLn, stderr) +import System.FilePath ((<.>), (), takeFileName, takeExtension) +import System.Exit (ExitCode(..), exitWith) +import Control.Monad (foldM, when) +import Control.Monad.IO.Class (MonadIO(..)) +import Data.List (find) +import Data.Bifunctor (Bifunctor(..)) +import Data.Map (Map) +import qualified Data.Map as Map main :: IO () -main = do - arguments <- getArgs - void $ rawSystem "bundle" - $ "exec" : "./bin/7digital.rb" : arguments +main = getArgs >>= withArguments + where + withArguments [sourceArchive, musicDirectory] + = createDirectoryIfMissing True musicDirectory + >> extract sourceArchive musicDirectory + >> probeDirectory musicDirectory + >>= withMetadata musicDirectory + withArguments _ = hPutStrLn stderr "Usage: 7digital MUSIC_ARCHIVE.zip DIRECTORY" + >> exitWith (ExitFailure 1) + withMetadata musicDirectory (metadata, anyErrors) + = Map.traverseWithKey (renameSong musicDirectory) metadata + >> when anyErrors (exitWith (ExitFailure 2)) + +data Song = Song + { album :: StrictText + , title :: StrictText + , track :: StrictText + } deriving (Eq, Show) + +renameSong :: FilePath -> FilePath -> Song -> IO () +renameSong musicDirectory songPath Song{..} = + let newFileName = StrictText.unpack (paddedTrack <> " - " <> title) + <.> takeExtension songPath + newDirectory = musicDirectory StrictText.unpack album + in createDirectoryIfMissing False newDirectory + >> renameFile songPath (newDirectory newFileName) + >> putStrLn ("\"" <> newFileName <> "\"" <> " renamed.") + where + paddedTrack + | StrictText.length track <= 1 = "0" <> track + | otherwise = track + +probeDirectory :: FilePath -> IO (Map FilePath Song, Bool) +probeDirectory musicDirectory = + listDirectory musicDirectory >>= foldM probeEach (Map.empty, False) + where + probeEach (metadata, _) "MissingFiles.txt" + = StrictText.readFile (musicDirectory "MissingFiles.txt") + >>= StrictText.hPutStr stderr + >> pure (metadata, True) + probeEach metadata songFile = + let metadataKey = musicDirectory songFile + in readProcessWithExitCode "ffprobe" [metadataKey] "" + >>= handleProbeResult metadata metadataKey . parseProbe + handleProbeResult (metadata, anyErrors) metadataKey (Right songProbe) = + pure (Map.insert metadataKey songProbe metadata, anyErrors) + handleProbeResult (metadata, _) metadataKey (Left missingField) = + let message = "Metadata \"" <> missingField + <> "\" is not available for " <> StrictText.pack metadataKey + <> "." + in StrictText.hPutStrLn stderr message >> pure (metadata, True) + parseProbe (_, _, probeOutput) = createSong + $ filter fieldFilter + $ bimap normalizeName stripValue . StrictText.breakOn ":" + <$> StrictText.lines (StrictText.pack probeOutput) + fieldFilter (name, _) = name `elem` ["title", "album", "track"] + createSong songFields = Song + <$> findByField "album" songFields + <*> findByField "title" songFields + <*> findByField "track" songFields + findByField needle songFields + | Just found <- find ((== needle) . fst) songFields = Right $ snd found + | otherwise = Left needle + normalizeName = StrictText.toLower . StrictText.strip + stripValue = StrictText.strip . StrictText.drop 1 + +extract :: MonadIO m => FilePath -> FilePath -> m () +extract sourceArchive musicDirectory = do + withArchive sourceArchive (forEntries forSong) + where + forSong selector = + let entryName = takeFileName (unEntrySelector selector) + in liftIO (StrictText.putStrLn $ "Inflating " <> getEntryName selector) + >> saveEntry selector (musicDirectory entryName) -- cgit v1.2.3