az-tags
¶The complete source for the program can be found in
examples/az-tags/main.cc in the source distribution. The logic
is simple enough to fit completely in main
. The usage is:
az-tags [-h] [-v] file [file...]
Skipping command line parsing, we begin by initializing the library:
// ... #include <scribbu/scribbu.hh> // ... int main(int argc, char * argv[]) { // Parse command-line options... scribbu::static_initialize()
libscribbu
needs to carry out assorted initialization; rather
than deal with the static initialization problem, it just depends on the
caller to explicitly initialize the library.
At this point, the first filename is waiting in argv[optind]
, so
we can set the basic structure of the program:
for (int i = optind; i < argc; ++i) { // ... }
For each file, we will open it & parse it into its ID3v2 tags, track data and ID3v1 tag:
for (int i = optind; i < argc; ++i) { fs::ifstream ifs(argv[i], ios_base::binary); // 1 vector<unique_ptr<scribbu::id3v2_tag>> id3v2; scribbu::read_all_id3v2(ifs, back_inserter(id3v2)); // 2 scribbu::track_data td((istream&)ifs); // 3 unique_ptr<scribbu::id3v1_tag> pid3v1 = scribbu::process_id3v1(ifs); // 4
At 1
, we open the file, taking care to use binary mode so as to avoid
newline translation. At 2
we ask libscribbu
to read any and
all ID3v2 tags into id3v2
. We’ve used a vector
here, but we
can use any container providing a forward output iterator.
At this point, the file pointer is pointing just past the last ID3v2
tag (if an– there may be none, in which case the file pointer remains
at the beginning of the file and id3v2
is empty). The easiest
way to consume the track data is to construct a track_data
isntace with it. This will collect some data about the track and
advance the file pointer to the one-past-the-end point.
There may or may not be some kind of ID3v1 tag waiting for us. That
is why process_id3v1
returns a unique_ptr
– if there
is no ID3v1 tag, a null pointer will be returned.
We now have zero or more ID3v2 tags to be processed in id3v2
:
for (auto &ptag: id3v2) { // `ptag' is a reference to a unique_ptr<id3v2_tag> // how to get at its frames? }
It is at this point that the libscribbu
API turns out to be
less than ergonmic. The issue is that read_all_id3v2
returns
the tags typed as pointers to id3v2_tag
; this is a base class
providing a “generic” interface supported by all ID3v2 tags, but the
API for iterating over frames is provided individually by each
sub-class (id3v2_2_tag
, id3v2_3_tag
&
id3v2_4_tag
).
Perhaps it would be worth it to provide an interface on the base class
to do this, but for now, I simply dynamic_cast
& dispatch to a
template function process_tag
:
for (auto &ptag: id3v2) { switch (ptag->version()) { case 2: { // ID3v2.2 tag scribbu::id3v2_2_tag &p = dynamic_cast<scribbu::id3v2_2_tag&>(*ptag); process_tag(p); break; } case 3: { // ID3v2.3 tag scribbu::id3v2_3_tag &p = dynamic_cast<scribbu::id3v2_3_tag&>(*ptag); process_tag(p); break; } case 4: { // ID3v2.4 tag scribbu::id3v2_4_tag &p = dynamic_cast<scribbu::id3v2_4_tag&>(*ptag); process_tag(p); break; } default: cerr << "Unknown ID3v2 revision " << ptag->version() << endl; abort(); } }
The template parameter is the id3v2_tag
sub-class. Since there
are only three, I can factor out the ID3v2-version-specific logic
into a traits class:
template <class tag_type> void process_tag(tag_type &T) { ... for (auto fp: T) { // 1 if (traits_type::COMMID == fp->id()) { // 2 id3v2_frame &F = fp; comm_type &C = dynamic_cast<comm_type&>(F); // 3 string dsc = C.template description<string>(); if (dsc.empty()) { string txt = C.template text<string>(); if ("Amazon.com Song ID" == txt.substr(0, 18)) { cout << "updating the comment frame containing " << txt << endl; fp = traits_type::replace(C); } } } } }
Each concreate id3v2_tag
subclass implements begin
&
end
, so we can use instances thereof as targets in for range
loops like 1. fp
is actually a mutable proxy for an
ID3v2-version-specific id3v2_frame
subclass. At 2 we have
factored out the precise frame ID to select for comments frames.
Each ID3v2 version has a concrete comment frame type, to which we again dynamically cast (I really need to re-evaluate this interface) at 3.
The rest of the logic is straightforward– if there is no description field in the comments frame, and the comment text begins with “Amazon.com Song ID”, replace the frame.