The flac tagger.
authorAndre Noll <maan@systemlinux.org>
Wed, 25 Dec 2013 02:49:00 +0000 (02:49 +0000)
committerAndre Noll <maan@tuebingen.mpg.de>
Sun, 26 Apr 2015 12:13:34 +0000 (14:13 +0200)
This commit adds support to modify the vorbis comments of a flac
file. As usual, only the artist, title, album, year and comment tags
are supported.

As for the implementation, we first locate the vorbis comment
block by iterating over all metadata blocks. In this block
we replace each comment by the tags provided as an argument to
flac_rewrite_tags(). To work around a bug in old flac versions we
avoid to call FLAC__metadata_object_vorbiscomment_replace_comment(),
which would not replace tags as advertised. Instead we first delete
the comment and then append a new tag.

Once all tags have been replaced this way, we write out the temporary
file using FLAC__metadata_chain_write_with_callbacks_and_tempfile().
To do this we needed to convince libflac to not modify the original
file in place, which turned out to be more difficult than it should be.

error.h
flac_afh.c

diff --git a/error.h b/error.h
index 17c4a293d3236d2cf025a88d8b128c406a4411f4..697b545af27b3087970ea846b8dad89cfa137307 100644 (file)
--- a/error.h
+++ b/error.h
@@ -98,6 +98,8 @@ extern const char **para_errlist[];
        PARA_ERROR(FLAC_SKIP_META, "could not skip metadata"), \
        PARA_ERROR(FLAC_DECODE_POS, "could not get decode position"), \
        PARA_ERROR(FLAC_STREAMINFO, "could not read stream info meta block"), \
+       PARA_ERROR(FLAC_REPLACE_COMMENT, "could not replace vorbis comment"), \
+       PARA_ERROR(FLAC_WRITE_CHAIN, "failed to write metadata chain"), \
 
 
 #define AFH_RECV_ERRORS \
index d72eb83b315f6bd3e78460e83d66ec37c83e5fed..51c3c314bf9e265110e82eabae9e94918e1bc7e3 100644 (file)
 #include "error.h"
 #include "afh.h"
 #include "string.h"
+#include "fd.h"
 
 struct private_flac_afh_data {
-       char *map;
+       const char *map;
        size_t map_bytes;
        size_t fpos;
        struct afh_info *afhi;
@@ -88,6 +89,15 @@ static int meta_close_cb(FLAC__IOHandle __a_unused handle)
        return 0;
 }
 
+static const FLAC__IOCallbacks meta_callbacks = {
+       .read = meta_read_cb,
+       .write = NULL,
+       .seek = meta_seek_cb,
+       .tell = meta_tell_cb,
+       .eof = meta_eof_cb,
+       .close = meta_close_cb
+};
+
 static void free_tags(struct taginfo *tags)
 {
        freep(&tags->artist);
@@ -131,22 +141,95 @@ static void flac_read_vorbis_comments(FLAC__StreamMetadata_VorbisComment *vc,
        }
 }
 
-static int flac_read_meta(struct private_flac_afh_data *pfad)
+/*
+ * FLAC__metadata_object_vorbiscomment_replace_comment() is buggy in some
+ * libFLAC versions (see commit e95399c1 in the flac git repository). Hence we
+ * use delete and add as a workaround.
+ */
+static int flac_replace_vorbis_comment(FLAC__StreamMetadata *b,
+               const char *tag, const char* val)
 {
+       FLAC__bool ok;
+       FLAC__StreamMetadata_VorbisComment_Entry entry;
        int ret;
-       FLAC__IOCallbacks meta_callbacks = {
-               .read = meta_read_cb,
-               .write = NULL,
-               .seek = meta_seek_cb,
-               .tell = meta_tell_cb,
-               .eof = meta_eof_cb,
-               .close = meta_close_cb
-       };
+
+       PARA_INFO_LOG("replacing %s\n", tag);
+       ret = FLAC__metadata_object_vorbiscomment_remove_entries_matching(
+               b, tag);
+       if (ret < 0)
+               return -E_FLAC_REPLACE_COMMENT;
+       ok = FLAC__metadata_object_vorbiscomment_entry_from_name_value_pair(
+               &entry, tag, val? val : "");
+       if (!ok)
+               return -E_FLAC_REPLACE_COMMENT;
+       ok = FLAC__metadata_object_vorbiscomment_append_comment(b, entry,
+               false /* no copy */);
+       if (!ok) {
+               free(entry.entry);
+               return -E_FLAC_REPLACE_COMMENT;
+       }
+       return 1;
+}
+
+static int flac_replace_vorbis_comments(FLAC__Metadata_Chain *chain,
+               FLAC__StreamMetadata *b, struct taginfo *tags)
+{
+       FLAC__bool ok;
+       int ret;
+
+       ret = flac_replace_vorbis_comment(b, "artist", tags->artist);
+       if (ret < 0)
+               return ret;
+       ret = flac_replace_vorbis_comment(b, "title", tags->title);
+       if (ret < 0)
+               return ret;
+       ret = flac_replace_vorbis_comment(b, "album", tags->album);
+       if (ret < 0)
+               return ret;
+       ret = flac_replace_vorbis_comment(b, "year", tags->year);
+       if (ret < 0)
+               return ret;
+       ret = flac_replace_vorbis_comment(b, "comment", tags->comment);
+       if (ret < 0)
+               return ret;
+       /*
+        * Even if padding is disabled, libflac will try to modify the original
+        * file inplace if the metadata size has not changed. This won't work
+        * here though, because the original file is mapped read-only. Since
+        * there is no option to force the use of a temp file we work around
+        * this shortcoming by adding a dummy entry which increases the size of
+        * the meta data. If the entry already exists, we simply remove it.
+        */
+       ok = FLAC__metadata_chain_check_if_tempfile_needed(chain,
+               false /* no padding */);
+       if (!ok) {
+               PARA_INFO_LOG("adding/removing dummy comment\n");
+               ret = FLAC__metadata_object_vorbiscomment_remove_entries_matching(
+                       b, "comment2");
+               if (ret < 0)
+                       return -E_FLAC_REPLACE_COMMENT;
+               if (ret == 0) { /* nothing was removed */
+                       ret = flac_replace_vorbis_comment(b, "comment2",
+                               "avoid inplace write");
+                       if (ret < 0)
+                               return ret;
+               }
+               assert(FLAC__metadata_chain_check_if_tempfile_needed(chain,
+                       false /* no padding */));
+       }
+       return 1;
+}
+
+static int flac_init_meta(struct private_flac_afh_data *pfad,
+               FLAC__Metadata_Chain **chainp, FLAC__Metadata_Iterator **iterp)
+{
+       int ret;
+       FLAC__bool ok;
        FLAC__Metadata_Chain *chain;
        FLAC__Metadata_Iterator *iter;
-       FLAC__StreamMetadata_StreamInfo *info = NULL;
-       FLAC__bool ok;
 
+       *chainp = NULL;
+       *iterp = NULL;
        chain = FLAC__metadata_chain_new();
        if (!chain)
                return -E_FLAC_CHAIN_ALLOC;
@@ -160,6 +243,25 @@ static int flac_read_meta(struct private_flac_afh_data *pfad)
        if (!iter)
                goto free_chain;
        FLAC__metadata_iterator_init(iter, chain);
+       *iterp = iter;
+       *chainp = chain;
+       return 1;
+free_chain:
+       FLAC__metadata_chain_delete(chain);
+       return ret;
+}
+
+static int flac_read_meta(struct private_flac_afh_data *pfad)
+{
+       int ret;
+       FLAC__Metadata_Chain *chain;
+       FLAC__Metadata_Iterator *iter;
+       FLAC__StreamMetadata_StreamInfo *info = NULL;
+       FLAC__bool ok;
+
+       ret = flac_init_meta(pfad, &chain, &iter);
+       if (ret < 0)
+               return ret;
        for (;;) {
                FLAC__StreamMetadata *b;
                b = FLAC__metadata_iterator_get_block(iter);
@@ -184,7 +286,6 @@ static int flac_read_meta(struct private_flac_afh_data *pfad)
        ret = info? 0: -E_FLAC_STREAMINFO;
 free_iter:
        FLAC__metadata_iterator_delete(iter);
-free_chain:
        FLAC__metadata_chain_delete(chain);
        if (ret < 0)
                free_tags(&pfad->afhi->tags);
@@ -332,6 +433,88 @@ static int flac_get_file_info(char *map, size_t map_bytes, __a_unused int fd,
        return 1;
 }
 
+static size_t temp_write_cb(const void *ptr, size_t size, size_t nmemb,
+       FLAC__IOHandle handle)
+{
+       int ret, fd = *(int *)handle;
+       size_t n = size * nmemb; /* FIXME: possible overflow */
+
+       ret = write_all(fd, ptr, n);
+
+       /*
+        * libflac expects POSIX semantics: If an error occurs, or the end of
+        * the file is reached, the return value is a short item count or zero.
+        */
+       if (ret < 0) {
+               PARA_ERROR_LOG("%s\n", para_strerror(-ret));
+               return 0;
+       }
+       return nmemb;
+}
+
+/* only the write callback needs to be supplied for writing the temp file. */
+static const FLAC__IOCallbacks temp_callbacks = {
+       .write = temp_write_cb,
+};
+
+static int flac_write_chain(FLAC__Metadata_Chain *chain,
+               struct private_flac_afh_data *pfad, int fd)
+{
+       FLAC__bool ok;
+
+       ok = FLAC__metadata_chain_write_with_callbacks_and_tempfile(chain,
+               false /* no padding*/, pfad,
+               meta_callbacks, &fd, temp_callbacks);
+       if (!ok) {
+               FLAC__Metadata_ChainStatus st;
+               st = FLAC__metadata_chain_status(chain);
+               PARA_ERROR_LOG("chain status: %d\n", st);
+               if (st == FLAC__METADATA_CHAIN_STATUS_READ_ERROR)
+                       PARA_ERROR_LOG("read error\n");
+               return -E_FLAC_WRITE_CHAIN;
+       }
+       return 1;
+}
+
+static int flac_rewrite_tags(const char *map, size_t map_bytes,
+               struct taginfo *tags, int fd, __a_unused const char *filename)
+{
+       int ret;
+       FLAC__Metadata_Chain *chain;
+       FLAC__Metadata_Iterator *iter;
+       FLAC__StreamMetadata *b = NULL;
+       FLAC__bool ok;
+       struct private_flac_afh_data *pfad = para_calloc(sizeof(*pfad));
+
+       pfad->map = map;
+       pfad->map_bytes = map_bytes;
+       pfad->fpos = 0;
+
+       ret = flac_init_meta(pfad, &chain, &iter);
+       if (ret < 0)
+               goto free_pfad;
+       for (ok = true; ok; ok = FLAC__metadata_iterator_next(iter)) {
+               b = FLAC__metadata_iterator_get_block(iter);
+               assert(b);
+               if (b->type == FLAC__METADATA_TYPE_VORBIS_COMMENT)
+                       break;
+               b = NULL;
+       }
+       ret = -E_FLAC_REPLACE_COMMENT;
+       if (!b)
+               goto out;
+       ret = flac_replace_vorbis_comments(chain, b, tags);
+       if (ret < 0)
+               goto out;
+       ret = flac_write_chain(chain, pfad, fd);
+out:
+       FLAC__metadata_iterator_delete(iter);
+       FLAC__metadata_chain_delete(chain);
+free_pfad:
+       free(pfad);
+       return ret;
+}
+
 static const char* flac_suffixes[] = {"flac", NULL};
 
 /**
@@ -343,4 +526,5 @@ void flac_afh_init(struct audio_format_handler *afh)
 {
        afh->get_file_info = flac_get_file_info,
        afh->suffixes = flac_suffixes;
+       afh->rewrite_tags = flac_rewrite_tags;
 }