#!/usr/bin/ruby # # Flac to MP3 transcoder. Looks for flac files that don't have corresponding # mp3 files and transcodes them. Expects files in the format the the unix # utility abcde creates which is Artist-Album/TrackNumber.Title.flac require 'test/unit' require 'net/http' require 'rexml/document' class AmazonImageGetter def AmazonImageGetter.getAlbumImageURL(artist, title) artist.gsub!("'", "") artist.gsub!(".", "") artist.gsub!(" ", "%20") title.gsub!("'", "") title.gsub!(".", "") title.gsub!(" ", "%20") print "Searching Amazon for \"#{artist}\" and \"#{title}\"\n" host = "webservices.amazon.com" baseurl = "/onca/xml" service = "AWSECommerceService" subscriptionid = "02TMPF2KCXM2R667KGG2" operation = "ItemSearch" searchindex = "Music" responsegroup = "Request,Medium" version = "2004-11-10" # TODO: if the first attempt is empty then try again using keywords instead of the Title uri = "#{baseurl}?Service=#{service}&SubscriptionId=#{subscriptionid}&Operation=#{operation}&SearchIndex=#{searchindex}&ResponseGroup=#{responsegroup}&Version=#{version}&Artist=#{artist}&Title=#{title}" h = Net::HTTP.new(host, 80) response = h.get(uri, nil) puts response.body if response.message == "OK" xml = REXML::Document.new(response.body) xml.elements.each("ItemSearchResponse/*/Item/MediumImage/URL") { |item| return item[0] # Return the first, how can I do this without a loop? } end end end ####################################################################################### ####################################################################################### ####################################################################################### class Song attr_reader :dir, :file, :originalFile def initialize(originalDir, originalFile) @originalFile = originalFile @originalDir = originalDir @dir = originalDir @file = originalFile fileWithPath = File.join(@dir, @file) #puts fileWithPath metaflacLines = `metaflac --list --block-type=VORBIS_COMMENT \"#{fileWithPath}\"`.split(/\n/) @metadata = Hash.new for line in metaflacLines if(line =~ /comment\[\d+\]:\s+([A-Z]+)=(.+)$/) key = $1 value = $2 @metadata[key] = value #print "#{key}\t#{value}\n" end end end #################################################################################### def getTitle title = @metadata["TITLE"] or Song.titleFromPath(@file) return title end def getArtist artist = @metadata["ARTIST"] or Song.artistFromPath(@file) return artist end def getAlbum album = @metadata["ALBUM"] or Song.albumFromPath(@file) return album end def getTrackNumber return @metadata["TRACKNUMBER"] end def getCDDB return @metadata["CDDB"] end def getDate return @metadata["DATE"] end def getGenre return @metadata["GENRE"] end #################################################################################### # Helper functions #def Song.escapeForCommandLine(str) # return str.gsub("\"", "\\\"").gsub("(", "\\\(").gsub(")", "\\\)").gsub(",", "\,").gsub("&", "\\\\&").gsub(";", "\\;") #end def Song.titleFromPath(path) title = File.split(path)[1].gsub("_", " ") if(title =~ /^\d+\.(.+)\.flac/) title = $1 end return title end def Song.artistFromPath(path) artist = File.split(path)[0] artist = File.split(artist)[1] if(artist.count("-") == 1) if(artist =~ /(\S+)-(\S+)/) artist = $1 end end return artist end def Song.albumFromPath(path) album = File.split(path)[0] album = File.split(album)[1] if(album.count("-") == 1) if(album =~ /(\S+)-(\S+)/) album = $2 end end return album end end class TestSong < Test::Unit::TestCase def test_simple flac = "/home/guido/tmp-music/flac/Blackalicious-Nia/12.Trouble_(Something,Eve_of_Destruction).flac" assert_equal("Trouble (Eve of Destruction)", Song.titleFromPath(flac)) assert_equal("Nia", Song.albumFromPath(flac)) assert_equal("Blackalicious", Song.artistFromPath(flac)) end end ############################################################################# ############################################################################# ############################################################################# class Album attr_reader :flacDirectory, :mp3Directory, :songs def initialize(flacDirectory, mp3Directory) @flacDirectory = flacDirectory @mp3Directory = mp3Directory @flacFiles = Dir.entries(flacDirectory).delete_if { |f| f !~ /\.flac$/ } #@mp3Files = Dir.entries(mp3Directory).delete_if { |f| f !~ /\.flac$/ } @songs = Array.new for flac in @flacFiles @songs.push(Song.new(@flacDirectory, flac)) end end def verify for song in @songs song.file.each_byte { |c| puts "ERROR FILENAME HAS EXTENDED CHARACTERS: #{@flacDirectory}/#{song.file}" if c > 127 } title = song.getTitle artist = song.getArtist album = song.getAlbum # NOTE: This is debug stuff that I used to figure out that the bad characters in # the flac metadata didn't come from metaflac, and that they weren't multi-byte # characters, but were LATIN-1 instead. #puts title.dump #print "number of characters length = #{title.length}\n" #print "number of characters from split = #{title.split(//).length}\n" #for c in title.split(//) # a = "#{c}"[0] # print "#{c}\t#{a}\n" #end track = song.getTrackNumber cddb = song.getCDDB date = song.getDate genre = song.getGenre print "#{artist}\t#{album}\t#{track}\t#{title}\t#{date}\t#{genre}\n" if(title == nil or title.empty?) print "ERROR: Title is empty in the flac metadata in \"#{@flacDirectory}/#{song.file}\"\n" end if(artist == nil or artist.empty?) print "ERROR: Artist is empty in the flac metadata in \"#{@flacDirectory}/#{song.file}\"\n" end if(album == nil or album.empty?) print "ERROR: Album is empty in the flac metadata in \"#{@flacDirectory}/#{song.file}\"\n" end end end def getArtistName for song in @songs artist = song.getArtist return artist if artist != nil and !artist.empty? end end def getAlbumName for song in @songs album = song.getAlbum return album if album != nil and !album.empty? end end def transcode Dir.mkdir(@mp3Directory) unless File.exists?(@mp3Directory) for song in @songs flac = song.file print " Working on #{flac}... " mp3 = File.join(@mp3Directory, flac).sub(".flac", ".mp3") log = File.join(@mp3Directory, "log") flac = File.join(@flacDirectory, flac) imagefile = File.join(@flacDirectory, "album-cover.jpg") #loginfo = `grep ^FINISHED \"#{log}\" | grep \"#{mp3}\"` if(File.exists?(log)) # TODO: Need to figure how to not transcode if it's not needed if(File.exists?(mp3)) # What about something like this: or !loginfo.empty?) print "\nDoing nothing.\n" else print "encoding.\n" title = song.getTitle artist = song.getArtist album = song.getAlbum track = song.getTrackNumber cddb = song.getCDDB date = song.getDate title = if title.nil? then "" else "--tt \"#{title}\"" end artist = if artist.nil? then "" else "--ta \"#{artist}\"" end album = if album.nil? then "" else "--tl \"#{album}\"" end track = if track.nil? then "" else "--tn #{track}" end cddb = if cddb.nil? then "" else "--tc \"cddb=#{cddb}\"" end date = if date.nil? then "" else "--ty #{date}" end #genre = song.getGenre # Don't specify genre since it's wrong most of the time and lame fails: --tg \"#{genre}\" cmd = "flac --decode --stdout \"#{flac}\" | lame --quiet -V 0 -h -F -b 128 -B 256 #{title} #{artist} #{album} #{track} #{cddb} #{date} - \"#{mp3}\"" print "#{cmd}\n" # TODO: handle exception SystemCallError success = false begin success = Kernel.system("time #{cmd} 2>&1 | tee -a \"#{log}\" > /dev/null ") rescue SystemCallError $stderr.print "System call Failed: " + $! Kernel.exit end if(success) print("FINISHED: #{mp3}") Kernel.system("echo \"\nFINISHED: #{mp3}\" >> #{log}") else print("ERROR: #{$?}") Kernel.system("echo \"\nERROR: #{$?}\" >> #{log}") end if(File.exists?(imagefile) and !File.exists?(File.join(@mp3Directory, "album-cover.jpg"))) FileUtils.copy_file(imagefile, @mp3Directory) end end end #print "Sleeping for 10 seconds..." #Kernel.sleep(10) end end class AlbumCollection attr_reader :albums def initialize(root) @flacRoot = File.join(root, "flac") @mp3Root = File.join(root, "mp3") #print "Using \"#{@flacRoot}\" as flac directory\n" #print "Using \"#{@mp3Root}\" as mp3 directory\n" raise "#{@flacRoot} doesn't exist" if !File.exists?(@flacRoot) or !File.directory?(@flacRoot) raise "#{@mp3Root} doesn't exist" if !File.exists?(@mp3Root) or !File.directory?(@mp3Root) Dir.chdir(@flacRoot) albumDirectories = Dir.entries(".").delete_if { |f| File.file?(f) or f.eql?(".") or f.eql?("..") } @albums = Array.new for albumDirectory in albumDirectories @albums.push(Album.new(File.join(@flacRoot, albumDirectory), File.join(@mp3Root, albumDirectory))) end end end ####################################################################### ####################################################################### ####################################################################### myCollection = AlbumCollection.new(ARGV[0]) # TODO: fix this -verify command line arg if(false) #if(ARGV.size > 1 and ARGV[1] == "-verify") myCollection.albums.each { |album| print "Verifying #{album.flacDirectory}\n" album.verify } Kernel.exit end if(false) myCollection.albums.each { |album| imagefile = File.join(album.flacDirectory, "album-cover.jpg") print "Looking for #{imagefile}\n" if(!File.exists?(imagefile)) imageurl = AmazonImageGetter.getAlbumImageURL(album.getArtistName, album.getAlbumName) print "Getting #{imageurl}\n" if(imageurl == nil or imageurl.empty?) print "ERROR: Couldn't find image for #{album.getArtistName} and #{album.getAlbumName}\n" else Kernel.system("wget --output-document=#{imagefile} #{imageurl}") end end } end if(ARGV.size == 1 or ARGV[1] != "-no-transcode") myCollection.albums.each { |album| album.transcode } end