How to refactor this code combining IO and Either?

You could use plain exceptions, i.e. readLibFromFile_ and mkPlaylist could simply fail or throwIO on error, and then you get a straightforward

handleMakePlaylist :: MakePlaylistOptions -> IO ()
handleMakePlaylist opts = do
    lib <- readLibFromFile_ opts.libraryPath
    plist <- mkPlaylist lib' opts.firstTrack opts.maxTracks
    putStr $ renderResolvedPlaylist plist'

Or you can make all functions pure with Either String ... results and chain them using the plain Either monad:

handleMakePlaylist :: MakePlaylistOptions -> IO ()
handleMakePlaylist opts = do
    contents <- readFile opts.libraryPath -- throws, could be wrapped with `try`
    putStr $ either ("Error: " <>) id $ do 
        lib <- parseLib contents 
        plist <- mkPlaylist lib' opts.firstTrack opts.maxTracks
        Right $ renderResolvedPlaylist plist'

Local use of ExceptT is fine too, but I find the two approaches above easier.