{"id":779,"date":"2012-07-17T21:10:53","date_gmt":"2012-07-18T01:10:53","guid":{"rendered":"http:\/\/www.jetmore.org\/john\/blog\/?p=779"},"modified":"2012-07-17T21:10:53","modified_gmt":"2012-07-18T01:10:53","slug":"patch-allow-id3v21-to-include-colons-in-comment-fields","status":"publish","type":"post","link":"https:\/\/www.jetmore.org\/john\/blog\/2012\/07\/patch-allow-id3v21-to-include-colons-in-comment-fields\/","title":{"rendered":"Patch: Allow id3v2(1) to Include Colons in Comment Fields"},"content":{"rendered":"<p>After a disagreement with Rhythmbox about how ID3 comments on my music collection should be handled, I went looking for a more fine-grained tool to manipulate them.  A quick browse through aptitude later, I chose a tool named &#8216;id3v2&#8217;, a frustratingly un-googleable name.  The <a href=\"http:\/\/id3v2.sourceforge.net\/\">official distribution<\/a> is available from SourceForge, and I&#8217;m using the <a href=\"http:\/\/packages.debian.org\/wheezy\/id3v2\">Debian package<\/a> locally.  The tool worked well, but it was unable to properly handle comment text which contained a colon (:).<br \/>\n<!--more--><\/p>\n<h2>What&#8217;s the problem?<\/h2>\n<p>Here&#8217;s the syntax for adding a comment to an MP3 using <code>id3v2<\/code>:<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\nid3v2 -c DESCRIPTION:COMMENT:LANGUAGE file.mp3\r\n<\/pre>\n<p>Here&#8217;s that command run on a real file, and the resulting comment inserted into the file.  The &#8220;LANGUAGE&#8221; field is always truncated to 3 characters, so I&#8217;m switching to &#8220;LNG&#8221;.<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\n$ id3v2 -c DESCRIPTION:COMMENT:LNG t.mp3\r\n$ id3v2 -l t.mp3 | grep ^COMM\r\nCOMM (Comments): (DESCRIPTION)[LNG]: COMMENT\r\n<\/pre>\n<p>Now here&#8217;s a similar set of commands showing various attempts to use colon(s) in the tag, including an example that&#8217;s very close to my actual use case.  Note that I am running <code>id3v2 -r COMM t.mp3<\/code> between these commands to remove the existing comments, I&#8217;m just not showing it since it doesn&#8217;t add anything to the discussion.<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\n$ id3v2 -c DESCRIPTION:'COM: MENT':LNG t.mp3 \r\n$ id3v2 -l t.mp3 | grep ^COMM\r\nCOMM (Comments): (DESCRIPTION)[ ME]: COM\r\n\r\n\r\n$ id3v2 -c DESCRIPTION:'COM\\: MENT':LNG t.mp3 \r\n$ id3v2 -l t.mp3 | grep ^COMM\r\nCOMM (Comments): (DESCRIPTION)[ ME]: COM\\\r\n\r\n$ id3v2 -c '':'From\\: CD, 20120717; TAG-GOOD; ':'' t.mp3 \r\n$ id3v2 -l t.mp3 | grep ^COMM\r\nCOMM (Comments): ()[ CD]: From\\\r\n<\/pre>\n<h2>What&#8217;s the history?<\/h2>\n<p>Unfortunately I couldn&#8217;t find any work around for this.  The <a href=\"http:\/\/id3v2.git.sourceforge.net\/git\/gitweb.cgi?p=id3v2\/id3v2;a=blob;f=id3v2.cpp;h=29083378910ce9e1f2fcbda408b1446852df3991;hb=HEAD\">code<\/a> is using strchr(3) to find colons in the argument to <code>-c<\/code> and there&#8217;s no checking for any sort of escape character.  So the bad news is that there&#8217;s no toehold in the compiled binary to try and fudge it.<\/p>\n<p>The good news is that it isn&#8217;t extremely hard to write some code that can check for escapes (\\:) and allow them to be text rather than field delimiters.<\/p>\n<p>The <code>id3v2<\/code> package is not actively maintained.  It has an owner, and I wouldn&#8217;t say the project is abandoned, but there&#8217;s not very much activity.  This has been a bug for a while and been noted in several places, but it&#8217;s never had an official fix.  Some bugs are logged against the lyrics field instead of the comment field, but they actually use the same code path internally, so it&#8217;s the same bug.  There are three related bugs in the SourceForge project tracker (IDs <a href=\"http:\/\/sourceforge.net\/tracker\/?func=detail&#038;aid=907469&#038;group_id=4193&#038;atid=104193\">907469<\/a> (2004), <a href=\"http:\/\/sourceforge.net\/tracker\/?func=detail&#038;aid=1588861&#038;group_id=4193&#038;atid=104193\">1588861<\/a> (2006), and <a href=\"http:\/\/sourceforge.net\/tracker\/?func=detail&#038;aid=3033559&#038;group_id=4193&#038;atid=104193\">3033559<\/a> (2010)), and there&#8217;s a <a href=\"http:\/\/bugs.debian.org\/cgi-bin\/bugreport.cgi?bug=390781\">Debian bug<\/a> also outstanding.  The Debian bug references <a href=\"http:\/\/sourceforge.net\/tracker\/?func=detail&#038;aid=1111747&#038;group_id=4193&#038;atid=304193\">a patch<\/a> on the SourceForge site, but the submitter never tried the patch or responded to the Debian maintainer&#8217;s request for feedback.<\/p>\n<h2>What&#8217;s the solution?<\/h2>\n<p>I did look at the SourceForge patch, but it&#8217;s not especially well written.  It doesn&#8217;t handle colons in all fields, and it doesn&#8217;t remove the escaping-&#8216;\\&#8217; from the text.<\/p>\n<p>It&#8217;s a bit confusing figuring out which code base to start with when modifying <code>id3v2<\/code>.  The <a href=\"http:\/\/id3v2.git.sourceforge.net\/git\/gitweb.cgi?p=id3v2\/id3v2;a=tree\">SourceForge repo<\/a> is presumably authoritative, but the project site itself is a ghost town.  There are multiple <code>id3v2<\/code> forks on github, including 2 that appear to be owned by the SourceForge project admins (<a href=\"https:\/\/github.com\/myers\/id3v2\">myers<\/a> and <a href=\"https:\/\/github.com\/nagilo\/id3v2\">nagilo<\/a>).  However, all of those forks appear to be based off of the 0.1.11 release, and 0.1.12 is current.  Also, the two repos owned by the SF admins were old compared to some other forks.  When I was researching all of this last night the github network graph for <code>id3v2<\/code> wouldn&#8217;t load, making this harder than it should have been.<\/p>\n<p>I decided to go with the group that seemed to be responding to issues with <code>id3v2<\/code>, Debian, and patch their version and submit the patch to them.  I did so, resulting in Debian bug <a href=\"http:\/\/bugs.debian.org\/cgi-bin\/bugreport.cgi?bug=681847\">681847<\/a>.  Today while nosing around again I realized that github user <a href=\"https:\/\/github.com\/cockroach\">cockroach<\/a> is actually Stefan Ott, the Debian package maintainer.  Therefore I also forked <a href=\"https:\/\/github.com\/cockroach\/id3v2\">his repo<\/a>, pushed <a href=\"https:\/\/github.com\/jetmore\/id3v2\/commit\/c0bbc40c124aff85d0143bc5e82cb7137f181db4#id3v2.cpp\">my patch<\/a>, and sent him a <a href=\"https:\/\/github.com\/cockroach\/id3v2\/pull\/1\">pull request<\/a>.  The Debian bug and the github pull request were probably redundant, but I like having the patch in guthub and I missed that cockroach was Stefan Ott last night.<\/p>\n<p>Finally, I&#8217;m putting the patch inline here for (hopefully) easy Googling.<\/p>\n<pre class=\"brush: diff; title: ; notranslate\" title=\"\">\r\n--- id3v2-0.1.12.dist\/id3v2.cpp\t2012-07-17 00:25:08.000000000 -0400\r\n+++ id3v2-0.1.12\/id3v2.cpp\t2012-07-17 00:33:28.000000000 -0400\r\n@@ -512,29 +512,35 @@\r\n         case ID3FID_COMMENT:\r\n         case ID3FID_UNSYNCEDLYRICS:\r\n         {\r\n-          \/\/ split the string at the ':' remember if no : then leave\r\n-          \/\/ descrip\/lang empty\r\n-          char *text;\r\n-          text = strchr(frameList[ii].data, ':');\r\n-          if (text == NULL) \r\n-          {\r\n-            myFrame-&gt;Field(ID3FN_TEXT) = frameList[ii].data;\r\n-          } else {\r\n-         \t*text = '&#92;&#48;';\r\n-          \ttext++;\r\n-          \tchar *lang;\r\n-          \tlang = strchr(text, ':');\r\n-          \tif (lang == NULL) \r\n-          \t{\r\n-          \t  myFrame-&gt;Field(ID3FN_DESCRIPTION) = frameList[ii].data;\r\n-          \t  myFrame-&gt;Field(ID3FN_TEXT) = text;\r\n-          \t} else {\r\n-          \t  *lang = '&#92;&#48;';\r\n-          \t  lang++;\r\n-          \t  myFrame-&gt;Field(ID3FN_DESCRIPTION) = frameList[ii].data;\r\n-              myFrame-&gt;Field(ID3FN_TEXT) = text;\r\n-              myFrame-&gt;Field(ID3FN_LANGUAGE) = lang;\r\n+          \/\/ split the string at the ':' (ignoring &quot;\\:&quot; and splitting into no more than 3 pieces\r\n+          \/\/ remember if no ':' then leave descrip\/lang empty\r\n+          char *pieces[3] = { frameList[ii].data, NULL, NULL };\r\n+          if (strchr(frameList[ii].data, ':')) {\r\n+            size_t read_at = 0, inst_at = 0, piece = 0;\r\n+            unsigned int olen = strlen(frameList[ii].data);\r\n+            for (read_at = 0; read_at &lt; olen; read_at++, inst_at++) {\r\n+              if (frameList[ii].data[read_at] == '\\\\' &amp;&amp; frameList[ii].data[read_at+1] == ':') {\r\n+                read_at++;\r\n+              }\r\n+              else if (frameList[ii].data[read_at] == ':' &amp;&amp; piece &lt; 2) {\r\n+                frameList[ii].data[read_at] = '&#92;&#48;';\r\n+                pieces[++piece] = frameList[ii].data + inst_at + 1;\r\n+              }\r\n+              frameList[ii].data[inst_at] = frameList[ii].data[read_at];\r\n             }\r\n+            frameList[ii].data[inst_at] = '&#92;&#48;';\r\n+          }\r\n+          \/\/ fprintf(stdout, &quot;1: %s, 2: %s, 3: %s\\n&quot;, pieces[0], pieces[1], pieces[2]);\r\n+          \r\n+          if (pieces[1] == NULL) {\r\n+            myFrame-&gt;Field(ID3FN_TEXT) = pieces[0];\r\n+          } else if (pieces[2] == NULL) {\r\n+            myFrame-&gt;Field(ID3FN_DESCRIPTION) = pieces[0];\r\n+            myFrame-&gt;Field(ID3FN_TEXT) = pieces[1];\r\n+          } else {\r\n+            myFrame-&gt;Field(ID3FN_DESCRIPTION) = pieces[0];\r\n+            myFrame-&gt;Field(ID3FN_TEXT) = pieces[1];\r\n+            myFrame-&gt;Field(ID3FN_LANGUAGE) = pieces[2];\r\n           }\r\n           \/* debug\r\n           std::cout &lt;&lt; ID3_GetString(myFrame, ID3FN_DESCRIPTION) &lt;&lt; std::endl\r\n<\/pre>\n","protected":false},"excerpt":{"rendered":"<p>After a disagreement with Rhythmbox about how ID3 comments on my music collection should be handled, I went looking for a more fine-grained tool to manipulate them. A quick browse through aptitude later, I chose a tool named &#8216;id3v2&#8217;, a &hellip; <a href=\"https:\/\/www.jetmore.org\/john\/blog\/2012\/07\/patch-allow-id3v21-to-include-colons-in-comment-fields\/\">Continue reading <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":[],"categories":[5],"tags":[8,14],"_links":{"self":[{"href":"https:\/\/www.jetmore.org\/john\/blog\/wp-json\/wp\/v2\/posts\/779"}],"collection":[{"href":"https:\/\/www.jetmore.org\/john\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.jetmore.org\/john\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.jetmore.org\/john\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.jetmore.org\/john\/blog\/wp-json\/wp\/v2\/comments?post=779"}],"version-history":[{"count":10,"href":"https:\/\/www.jetmore.org\/john\/blog\/wp-json\/wp\/v2\/posts\/779\/revisions"}],"predecessor-version":[{"id":805,"href":"https:\/\/www.jetmore.org\/john\/blog\/wp-json\/wp\/v2\/posts\/779\/revisions\/805"}],"wp:attachment":[{"href":"https:\/\/www.jetmore.org\/john\/blog\/wp-json\/wp\/v2\/media?parent=779"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.jetmore.org\/john\/blog\/wp-json\/wp\/v2\/categories?post=779"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.jetmore.org\/john\/blog\/wp-json\/wp\/v2\/tags?post=779"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}