#!/usr/bin/env ruby # # Parses the directories (recursively) or files for lyrics you pass in. # If no files are passed in we use ~/Music # # Usage: musicgrep ? [ | ]? # # where options are # # -a || --artists search just artists # -t || --titles search just titles # -v || --verbose be loud # -h || --help print the help message # -c || --clean clean the lyrics cache # # Uses http://www.lyricsmode.com to find lyrics # To the best of my knowledge, searching them with a program doesn't violate their TOS. # # The author isn't responsible for any damage or mean things you do # with this. But please don't do mean things. # require 'md5' require 'id3' require 'open-uri' # -------------------------------------------------- # Types # -------------------------------------------------- # A simple representation of a song class SongData attr_reader :file, :artist, :title def initialize(file,title = nil, artist = nil) @file = file @title = title ? title : '' @artist = artist ? artist : '' init if !title || !artist end def ready? return @title && @artist && @title != ''&& @artist != '' end def to_s return to_short_s + ' (' + @file + ')' end def to_short_s return @title + ' by ' + @artist end private # @file has to be set def init af = ID3::AudioFile.new @file tag = af.tagID3v1 tag = af.tagID3v2 if !tag if tag @artist = clean_up tag['ARTIST'] @title = clean_up tag['TITLE'] end end def clean_up(s) return '' if !s s = s['text'] if s.is_a? ID3::Frame s = s.strip s = s.gsub /\([^\)]+\)/, '' return s end end class MusicGrep attr_writer :artists, :titles, :verbose def initialize @artists = false @titles = false @verbose = false @base = create_base_db @noted_building_index = false @done = false end # Cleans out the directory def clean_db puts 'Are you sure you want to drop the DB?' q = '' while q == '' print '[y/n] > ' q = STDIN.gets.chomp if /^[yY]/ =~ q Dir.foreach @base do |filename| next if filename == '.' || filename == '..' file = File.join @base, filename File.delete file end Dir.delete @base end end end # Performs the (possibly recursive) grep on all the 'files' for the regex 'words' def music_grep(files,words) @base = create_base_db if files.empty? files.push File.join(ENV['HOME'], 'Music') end q = Array.new files while !@done && !q.empty? f = q.shift if File.directory? f Dir.foreach f do |filename| next if filename == '.' or filename == '..' file = File.join f, filename if File.directory? file q.push file else break if music_grep_file file, words end end else break if music_grep_file f, words end end end private # Creates the base in which we put the lyrics def create_base_db dirname = File.basename $0 dir = File.join ENV['HOME'], '.' + dirname if !File.exists? dir note 'Creating ' + dir Dir.mkdir dir end return dir end # Returns whether this is a music file or not based on the extension def is_music_file(file) return file && /\.mp3$/ =~ File.extname(file) end # Performs a grep in file for words def music_grep_file(file,words) return false if !is_music_file(file) return false if @done begin sd = SongData.new file if sd.ready? if @artists # Maybe just check the artist if /#{words}/ =~ sd.artist print_success sd end elsif @titles # Maybe just check the titles if /#{words}/ =~ sd.title print_success sd end else lyrics_filename = File.join @base, MD5.new(file).to_s if !File.exists? lyrics_filename lyrics = read_from_network sd, lyrics_filename lyrics.each "\n" do |line| if /#{words}/ =~ line print_success sd break end end else IO.foreach lyrics_filename do |line| if /#{words}/ =~ line print_success sd break end end end end end rescue Exception => e log e.to_s return false end return true end # Log if we're verbose def log(s) if @verbose STDERR.puts '*** ' + s end end # Always log def note(s) STDERR.puts s end # Prints a 'success message' for the SongData 'sd' def print_success(sd) puts sd end # Reads and returns the lyrics for SongData sd, while writing to 'file' def read_from_network(sd,file) log 'Building index for ' + sd.to_short_s if !@noted_building_index note 'Building index...' @noted_building_index = true # Check to see if we can do this begin ok = open 'http://jeffpalm.com/musicgrep/check.php' ok = ok.strip if ok != 'OK' note 'Invalid result: ' + ok @done = true return end rescue note 'Invalid result: ' + ok @done = true return end end artist = sd.artist.gsub(/\W+/, '_').downcase title = sd.title.gsub(/\W+/, '_').downcase # First look up the lyrics url = 'http://www.lyricsmode.com/lyrics/' + artist[0,1] + '/' + artist + '/' + title + '.html' txt = '' begin # This parsing kind of sucks, but using just match wasn't working? html = open(url).read in_parse = false html.each "\n" do |line| if !in_parse in_parse = /
/ =~ line end if in_parse txt += line if /<\/div>/ =~ line in_parse = false end end end txt = txt.gsub /<[^>]+>/, '' rescue end # Write this text out to 'file' f = File.new file, 'w' f.puts txt return txt end end # -------------------------------------------------- # Functions # -------------------------------------------------- # Prints the help String def print_help STDERR.puts 'Usage: ' + __FILE__ + '