Ruby mp3 Id3 parsing
http://www.hakubi.us/ruby-taglib/
I used this for a project and it worked quite well. Wrapper around taglib, which is very portable.
Read ID3 Tags of Remote MP3 File in Ruby/Rails?
which Ruby version are you using?
which ID3 Tag version are you trying to read?
ID3v1 tags are at the end of a file, in the last 128 bytes. With Net::HTTP it doesn't seem to be possible to seek forward towards the end of the file and read only the last N bytes. If you try that, usingheaders = {"Range" => "bytes=128-"}
, it always seems to download the complete file. resp.body.size => file-size
. But no big loss, because ID3 version 1 is pretty much outdated at this point because of it's limitations, such as fixed length format, only ASCII text, ...). iTunes uses ID3 version 2.2.0.
ID3v2 tags are at the beginning of a file - to support streaming - you can download the initial part of the MP3 file, which contains the ID3v2 header, via HTTP protocol >= 1.1
The short answer:
require 'net/http'
require 'uri'
require 'id3' # id3 RUby library
require 'hexdump'
file_url = 'http://example.com/filename.mp3'
uri = URI(file_url)
size = 1000 # ID3v2 tags can be considerably larger, because of embedded album pictures
Net::HTTP.version_1_2 # make sure we use higher HTTP protocol version than 1.0
http = Net::HTTP.new(uri.host, uri.port)
resp = http.get( file_url , {'Range' => "bytes=0-#{size}"} )
# should check the response status codes here..
if resp.body =~ /^ID3/ # we most likely only read a small portion of the ID3v2 tag..
# file has ID3v2 tag
puts resp.body.hexdump
tag2 = ID3::Tag2.new
tag2.read_from_buffer( resp.body )
@id3_tag_size = tag2.ID3v2tag_size # that's the size of the whole ID3v2 tag
# we should now re-fetch the tag with the correct / known size
# ...
end
e.g.:
index 0 1 2 3 4 5 6 7 8 9 A B C D E F
00000000 ["49443302"] ["00000000"] ["11015454"] ["3200000d"] ID3.......TT2...
00000010 ["004b6167"] ["75796120"] ["48696d65"] ["00545031"] .Kaguya Hime.TP1
00000020 ["00000e00"] ["4a756e6f"] ["20726561"] ["63746f72"] ....Juno reactor
00000030 ["0054414c"] ["00001100"] ["4269626c"] ["65206f66"] .TAL....Bible of
00000040 ["20447265"] ["616d7300"] ["54524b00"] ["00050036"] Dreams.TRK....6
00000050 ["2f390054"] ["59450000"] ["06003139"] ["39370054"] /9.TYE....1997.T
00000060 ["434f0000"] ["1300456c"] ["65637472"] ["6f6e6963"] CO....Electronic
00000070 ["612f4461"] ["6e636500"] ["54454e00"] ["000d0069"] a/Dance.TEN....i
00000080 ["54756e65"] ["73207632"] ["2e300043"] ["4f4d0000"] Tunes v2.0.COM..
00000090 ["3e00656e"] ["67695475"] ["6e65735f"] ["43444442"] >.engiTunes_CDDB
000000a0 ["5f494473"] ["00392b36"] ["34374334"] ["36373436"] _IDs.9+647C46746
000000b0 ["38413234"] ["38313733"] ["41344132"] ["30334544"] 8A248173A4A203ED
000000c0 ["32323034"] ["4341422b"] ["31363333"] ["39390000"] 2204CAB+163399..
000000d0 ["00000000"] ["00000000"] ["00000000"] ["00000000"] ................
The long answer looks something like this: (you'll need id3 library version 1.0.0_pre or newer)
require 'net/http'
require 'uri'
require 'id3' # id3 RUby library
require 'hexdump'
file_url = 'http://example.com/filename.mp3'
def get_remote_id3v2_tag( file_url ) # you would call this..
id3v2tag_size = get_remote_id3v2_tag_size( file_url )
if id3v2tag_size > 0
buffer = get_remote_bytes(file_url, id3v2tag_size )
tag2 = ID3::Tag2.new
tag2.read_from_buffer( buffer )
return tag2
else
return nil
end
end
private
def get_remote_id3v2_tag_size( file_url )
buffer = get_remote_bytes( file_url, 100 )
if buffer.bytesize > 0
return buffer.ID3v2tag_size
else
return 0
end
end
private
def get_remote_bytes( file_url, n)
uri = URI(file_url)
size = n # ID3v2 tags can be considerably larger, because of embedded album pictures
Net::HTTP.version_1_2 # make sure we use higher HTTP protocol version than 1.0
http = Net::HTTP.new(uri.host, uri.port)
resp = http.get( file_url , {'Range' => "bytes=0-#{size-1}"} )
resp_code = resp.code.to_i
if (resp_code >= 200 && resp_code < 300) then
return resp.body
else
return ''
end
end
get_remote_id3v2_tag_size( file_url )
=> 2262
See:
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35
http://en.wikipedia.org/wiki/Byte_serving
some examples how to download in parts files can be found here:
but please note that there seems to be no way to start downloading "in the middle"
How do I download a binary file over HTTP?
http://unixgods.org/~tilo/Ruby/ID3/docs/index.html
Use ruby mp3info to read mp3 ID3 from external site (without loading whole file)
Well this solution works for id3v2 (the current standard). ID3V1 doesn't have the metadata at the beginning of the file, so it wouldn't work in those cases.
This reads the first 4096 bytes of the file, which is arbitrary. As far as I could tell from the ID3 documentation, there is no limit to the size, but 4kb was when I stopped getting parsing errors in my library.
I was able to build a simple dropbox audio player, which can be seen here:
soundstash.heroku.com
and open-sourced the code here: github.com/miketucker/Dropbox-Audio-Player
require 'open-uri'
require 'stringio'
require 'net/http'
require 'uri'
require 'mp3info'
url = URI.parse('http://example.com/filename.mp3') # turn the string into a URI
http = Net::HTTP.new(url.host, url.port)
req = Net::HTTP::Get.new(url.path) # init a request with the url
req.range = (0..4096) # limit the load to only 4096 bytes
res = http.request(req) # load the mp3 file
child = {} # prepare an empty array to store the metadata we grab
Mp3Info.open( StringIO.open(res.body) ) do |m| #do the parsing
child['title'] = m.tag.title
child['album'] = m.tag.album
child['artist'] = m.tag.artist
child['length'] = m.length
end
Access More Info of File using Ruby
In general, the answer is no: OS X is using specific libraries to access the metadata of the files based on type. These are not stored in a common attribute manner in the filesystem, but are inherent to the data. For example, PNG and JPG files record their height and width differently and can store different types of metadata about the image. The OS is reading these files and extracting this information for the More Info section.
In specific, however, the answer is yes: you want an ID3 library for Ruby like taglib-ruby or ruby-taglib. See the question Ruby mp3 Id3 parsing for more information.
Read audio file metadata with ruby
Taglib has Ruby bindings and does what you want.
c# MP3 ID3 data (without taglib)
The code you provided only supports ID3v1. That version does not support images.
However, the ID3v2 tag does support images. See section 4.15 of the 2.3 informal standard for an explanation of the "attached image" tag. Note that you'll need to write a full ID3v2 parser if you wish to read out the image without a tag library.
Good luck!
How symbian V3 get id3 info from a MP3 file
You can get the ID3 data from an MP3 using the CMetaDataUtility API. It's not part of the public SDK, but you can download the SDK API Plug-in via this Nokia Developer page that also has example code.
This will work in S60 3rd edition, 5th edition, and Symbian^3.
How to normalise the filename to just the mp3 filename with no path ? Ruby
Preliminary remark : post all the code and only the minimum of code, so that we can copy-paste and execute it to reproduce the error. An RSpec tag and the version of RSpec would also be useful in this case.
When I execute your code :
No such file or directory @ dir_chdir - ./spec/fixtures/mp3s
# ./lib/t_a.rb:14:in `chdir'
the error is in the statement at line 14 :
Dir.chdir(@path)
This gives a clue that chdir
does not find the requested subdirectory in the current working directory. Why ? Add a trace to display the current working directory :
def files
puts "in files, path=#{@path}"
puts "wd=...#{Dir.getwd.sub(/.*ruby(.*)/, '\1')}"
current_dir = Dir.getwd
Dir.chdir(@path)
...
and run the tests (I'm working in ...devl/ruby/zintlist/mp3_importer
) :
$ rspec
MP3Importer
#initialize
accepts a file path to parse mp3 files from
#files
in files, path=./spec/fixtures/mp3s
wd=.../zintlist/mp3_importer
loads all the mp3 files in the path directory
#xxxx
in files, path=./spec/fixtures/mp3s
wd=.../zintlist/mp3_importer/spec/fixtures/mp3s
and you see the difference :
wd=.../zintlist/mp3_importer
wd=.../zintlist/mp3_importer/spec/fixtures/mp3s
When executing files
, you have a side effect : the current directory is changed. In the second execution of files
, Dir.chdir
starts searching in the current directory left by the first execution, that is .../mp3_importer/spec/fixtures/mp3s
, and mp3s
of course does not contain ./spec/fixtures/mp3s
, hence the error No such file or directory
.
The solution is to restore the directory which is current when entering the method :
def files
puts "in files, path=#{@path}"
puts "wd=...#{Dir.getwd.sub(/.*ruby(.*)/, '\1')}"
current_dir = Dir.getwd
Dir.chdir(@path)
filenames = Dir.glob("*.mp3")
Dir.chdir(current_dir)
filenames
end
Then the trace shows that it has been restored :
wd=.../zintlist/mp3_importer
...
wd=.../zintlist/mp3_importer
You may already know that if you process a file inside a File.open ... do ... end
block, the file is closed when the block exits. The same works for restoring the current directory. From The Pickaxe Dir.chdir :
If a block is given, it is passed the name of the new current
directory, and the block is executed with that as the current
directory. The original working directory is restored when the block
exits.
Given these files :
#file t.rb
class MP3Importer
attr_accessor :path
def initialize(path)
@path = path
end
def files
# puts "in files, path=#{@path}"
# puts "wd=#{Dir.getwd.sub(/.*ruby(.*)/, '\1')}"
filenames = Dir.chdir(@path) do | path |
# puts path
Dir.glob("*.mp3")
end
puts "names=#{filenames}"
filenames
end
end
.
# file t_spec.rb
require 't'
RSpec.describe MP3Importer do
let(:test_music_path) { "./spec/fixtures/mp3s" }
let(:music_importer) { MP3Importer.new(test_music_path) }
describe '#initialize' do
it 'accepts a file path to parse mp3 files from' do
expect(music_importer.path).to eq(test_music_path)
end
end
describe '#files' do
it 'loads all the mp3 files in the path directory' do
expect(music_importer.files.size).to eq(4)
end
end
describe '#xxxx' do
it 'normalizes the filename to just the mp3 filename with no path' do
expect(music_importer.files).to include('f4.mp3')
end
end
end
Execution :
$ ruby -v
ruby 2.4.0rc1 (2016-12-12 trunk 57064) [x86_64-darwin15]
$ rspec -v
RSpec 3.6.0.beta2
- rspec-core 3.6.0.beta2
- rspec-expectations 3.6.0.beta2
- rspec-mocks 3.6.0.beta2
- rspec-support 3.6.0.beta2
$ rspec
MP3Importer
#initialize
accepts a file path to parse mp3 files from
#files
names=["f1.mp3", "f2.mp3", "f3.mp3", "f4.mp3"]
loads all the mp3 files in the path directory
#xxxx
names=["f1.mp3", "f2.mp3", "f3.mp3", "f4.mp3"]
normalizes the filename to just the mp3 filename with no path
Finished in 0.00315 seconds (files took 0.09868 seconds to load)
3 examples, 0 failures
All tests are green.
As the method return value is that of the last executed expression, you can simplify files
like so :
def files
Dir.chdir(@path) do | path |
Dir.glob("*.mp3")
end
end
What does the statement "Normalise ... mean ?
I don't know. I suppose it's collecting only the files whose name correspond to a certain pattern, here *.mp3
.
What I can say is that RDoc takes input file names from the command line and passes them to a routine called normalized_file_list
:
# file rdoc.rb
##
# Given a list of files and directories, create a list of all the Ruby
# files they contain.
#
# If +force_doc+ is true we always add the given files, if false, only
# add files that we guarantee we can parse. It is true when looking at
# files given on the command line, false when recursing through
# subdirectories.
#
# The effect of this is that if you want a file with a non-standard
# extension parsed, you must name it explicitly.
def normalized_file_list(relative_files, force_doc = false,
exclude_pattern = nil)
file_list = []
relative_files.each do |rel_file_name|
next if rel_file_name.end_with? 'created.rid'
next if exclude_pattern && exclude_pattern =~ rel_file_name
stat = File.stat rel_file_name rescue next
case type = stat.ftype
when "file" then
next if last_modified = @last_modified[rel_file_name] and
stat.mtime.to_i <= last_modified.to_i
if force_doc or RDoc::Parser.can_parse(rel_file_name) then
file_list << rel_file_name.sub(/^\.\//, '')
@last_modified[rel_file_name] = stat.mtime
end
when "directory" then
next if rel_file_name == "CVS" || rel_file_name == ".svn"
created_rid = File.join rel_file_name, "created.rid"
next if File.file? created_rid
dot_doc = File.join rel_file_name, RDoc::DOT_DOC_FILENAME
if File.file? dot_doc then
file_list << parse_dot_doc_file(rel_file_name, dot_doc)
else
file_list << list_files_in_directory(rel_file_name)
end
else
warn "rdoc can't parse the #{type} #{rel_file_name}"
end
end
file_list.flatten
end
##
# Return a list of the files to be processed in a directory. We know that
# this directory doesn't have a .document file, so we're looking for real
# files. However we may well contain subdirectories which must be tested
# for .document files.
def list_files_in_directory dir
files = Dir.glob File.join(dir, "*")
normalized_file_list files, false, @options.exclude
end
Related Topics
Understanding Io.Select When Reading Socket in Ruby
Ruby on Rails and JSON Parser from Url
How to Validate Ssl Certificate Chain in Ruby with Net/Http
Rails 4.2 - Sidekiq Not Sending Emails in Development
Bundle Install: Error: Failed to Build Gem Native Extension. Nio4R Gem
How to Install Rvm Without Root Access
Using Polymorphic Paths with Nested Associations
Error About Nokogiri While Capistrano Deployment on Ubuntu Server
How to Test 'Create' Controller Actions
What Are the Things You Would Like Improved in the Ruby Language
Help Refactoring This Nasty Ruby If/Else Statement
Rails: How to Autocomplete Search for Name But Save Id
Paginate Multiple Models in Kaminari
How Does Rails Implement Before_Filter
How to Get the Width of Terminal Window in Ruby
Rails: How to Write Tests for a Ruby Module
Suitability of Rails, Padrino, and Sinatra for Building a Prepaid Mobile Service