4.1.1 Implementing 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.