Tracker's write back capability

About

Tracker has a limited capability to write metadata back into the data resource. When an application modify some properties in a resource, tracker will try (in some restricted cases) to write those changes back to the original source. In case of a file that means writing it back into the file. For example writing some of the metadata the user sets using a SPARQL Update query back into a JPeg file as EXIF tags.

Announcement

In 0.9 we plan to change the name of the tracker:writeback ontology property to a less ambiguous name. Be prepared for this API change.

Support

Right now the write back capability is under development and only supports a bunch of fields in XML and few image formats (JPEG, PNG and TIFF); the design is extensible, so it is relatively easy to add support for new formats.

Note that when you see tracker:writeback in the ontology, that this doesn't necessarily mean that the field will be written back. Writeback implementers can read below what it is used for, but users should not even look at this to become any wiser about what will be and what wont be written back.

Once a few more formats are supported and testing has begun, we will create a table of supported fields and formats on this page.

Usage

The binary doing the actual writing is tracker-writeback. It will be started automatically when needed (if tracker has been compiled with write-back support!), but feel free to start it manually in a terminal (with verbosity 3) to see what is going on.

The resource we are editing must have the nie:url property set (i.e. it must be an instance of nie:DataObject) and the property we want to write must have tracker:writeback set in the ontology. The latter can't be changed by the user.

The following sparQL command will set a new title in the MP3 (and it will modify the MP3 itself):

tracker-sparql -u -q "
DELETE { ?f nie:title ?unknown }
WHERE { ?f nie:url <file:///my/file.mp3> ;
           nie:title ?unknown } "

tracker-sparql -u -q "
INSERT { ?f nie:title 'My test' }
WHERE { ?f nie:url <file:///my/file.mp3> } "

Developing your own support for write back

About

This is a guide for adding fields and formats to Tracker's write back capability. Intended audience are software developers.

Adding RDF properties that you want to support

If you take a look at the data/ontologies directory you'll find .ontology files. These files contain the description of all the classes and properties in the Nepomuk ontology. When you want to add a property that must be written back you need to set the boolean tracker:writeback to true for the property. Adding this boolean to the property means that the Writeback signal will be emitted whenever that property of a resource changes. The actual decision whether or not an actual write back of the field takes place is made by the writeback modules and implementers. It's not made in the ontology. This explains the earlier comment that users don't get any wiser by looking at tracker:writeback in the ontology.

You can find an example at nie:title in 30-nie.ontology:

nie:title a rdf:Property ;
        rdfs:label "Title" ;
        rdfs:comment "The title of the document" ;
        rdfs:subPropertyOf dc:title ;
        nrl:maxCardinality 1 ;
        rdfs:domain nie:InformationElement ;
        rdfs:range xsd:string ;
        tracker:fulltextIndexed true ;
        tracker:weight 10 ;
        tracker:writeback true .

Adding support for a format

For local files

Some boilerplate plugin code. Replace Mine, MINE and mine with something more sensible for your format. Take a look at tracker-writeback-mp3.c and tracker-writeback-xmp.c for examples. Also replace that TRACKER_NFO_PREFIX MyClassThatISupport at the bottom. For example with TRACKER_NFO_PREFIX Image or TRACKER_NFO_PREFIX Audio.

#include <mine/mine.h>

#include <libtracker-common/tracker-ontology.h>

#include "tracker-writeback-file.h"

#define TRACKER_TYPE_WRITEBACK_MINE    (tracker_writeback_mine_get_type ())

typedef struct TrackerWritebackMINE TrackerWritebackMINE;
typedef struct TrackerWritebackMINEClass TrackerWritebackMINEClass;

struct TrackerWritebackMINE {
        TrackerWritebackFile parent_instance;
};

struct TrackerWritebackMINEClass {
        TrackerWritebackFileClass parent_class;
};

static GType         tracker_writeback_mine_get_type             (void) G_GNUC_CONST;
static gboolean      tracker_writeback_mine_update_file_metadata (TrackerWritebackFile    *writeback_file,
                                                                  GFile                   *file,
                                                                  GPtrArray               *values,
                                                                  TrackerSparqlConnection *connection);
static const gchar** tracker_writeback_mine_content_types        (TrackerWritebackFile *writeback_file);

G_DEFINE_DYNAMIC_TYPE (TrackerWritebackMINE, tracker_writeback_mine, TRACKER_TYPE_WRITEBACK_FILE);

static void
tracker_writeback_mine_class_init (TrackerWritebackMINEClass *klass)
{
        TrackerWritebackFileClass *writeback_file_class = TRACKER_WRITEBACK_FILE_CLASS (klass);

        writeback_file_class->update_file_metadata = tracker_writeback_mine_update_file_metadata;
        writeback_file_class->content_types = tracker_writeback_mine_content_types;
}

static void
tracker_writeback_mine_class_finalize (TrackerWritebackMINEClass *klass)
{
}

static void
tracker_writeback_mine_init (TrackerWritebackMINE *mine)
{
}


TrackerWriteback *
writeback_module_create (GTypeModule *module)
{
        tracker_writeback_mine_register_type (module);

        return g_object_new (TRACKER_TYPE_WRITEBACK_MINE, NULL);
}

Implement this for the actual writing. This is the query from where values comes:

SELECT ?url 'the subject' ?predicate ?object {
    <the subject> ?predicate ?object ;
                  nie:url ?url .
    ?predicate tracker:writeback true
}

static gboolean
tracker_writeback_mine_update_file_metadata (TrackerWritebackFile    *writeback_file,
                                             GFile                   *file,
                                             GPtrArray               *values,
                                             TrackerSparqlConnection *connection)
{
        gchar *path;
        guint n;

        path = g_file_get_path (file);

        for (n = 0; n < values->len; n++) {
                const GStrv row = g_ptr_array_index (values, n);

                /* You're allowed to do things like this: 
                  TrackerSparqlCursor *cursor;
                  cursor = tracker_sparql_connection_query (connection, query, NULL,
                                                            &error); */

                /* row[0] is file's DataObject nie:url */
                /* row[1] is file's DataObject subject */
                /* row[2] is the property */
                /* row[3] is the value */

                if (g_strcmp0 (row[2], TRACKER_NIE_PREFIX "title") == 0) {
                        /* Write row[3] as title to the file */
                }
        }

        g_free (path);

        return TRUE;
}

Implement this function to indicate which rdf:types that you support, Examples for the class are TRACKER_NFO_PREFIX "Audio" and TRACKER_NFO_PREFIX "Image".

const gchar**
writeback_module_get_rdftypes (void)
{
        static const gchar *rdftypes[] = { TRACKER_NFO_PREFIX "MyClassThatISupport",
                                           TRACKER_NFO_PREFIX "MyOtherClassThatISupport",
                                           NULL };

        return rdftypes;
}

Implement this function to indicate which content-types that you support, Examples for the class are "image/png" and "image/jpeg".

static const gchar**
tracker_writeback_mine_content_types (TrackerWritebackFile *writeback_file)
{
        static const gchar *content_types[] = { "mine/x-what-i", 
                                                "mine/x-support",
                                                NULL };

        return content_types;
}

For non-local resources

This is much the same as for local files. You just don't have some infrastructure, like the GFile and the content-type stuff. That's because for remote resources you have to deal with that yourself.

Some boilerplate plugin code. Replace Mine, MINE and mine with something more sensible for your format. Also replace that TRACKER_NFO_PREFIX MyClassThatISupport at the bottom.

#include <mine/mine.h>

#include <libtracker-common/tracker-ontology.h>

#include "tracker-writeback.h"

typedef struct TrackerWritebackMINE TrackerWritebackMINE;
typedef struct TrackerWritebackMINEClass TrackerWritebackMINEClass;

struct TrackerWritebackMINE {
        TrackerWriteback parent_instance;
};

struct TrackerWritebackMINEClass {
        TrackerWritebackClass parent_class;
};

static gboolean tracker_writeback_mine_update_metadata (TrackerWriteback *writeback,
                                                        GPtrArray        *values);

G_DEFINE_DYNAMIC_TYPE (TrackerWritebackMINE, tracker_writeback_mine, TRACKER_TYPE_WRITEBACK);


static void
tracker_writeback_mine_class_init (TrackerWritebackMineClass *klass)
{
        TrackerWritebackClass *writeback_class = TRACKER_WRITEBACK_CLASS (klass);

        writeback_class->update_metadata = tracker_writeback_mine_update_metadata;
}

static void
tracker_writeback_mine_init (TrackerWritebackMine *writeback_mine)
{
}

Implement this for the actual writing. This is the query from where values comes:

SELECT ?url 'the subject' ?predicate ?object {
    <the subject> ?predicate ?object ;
                  nie:url ?url .
    ?predicate tracker:writeback true
}

static gboolean
tracker_writeback_miner_update_metadata (TrackerWriteback        *writeback,
                                         GPtrArray               *values
                                         TrackerSparqlConnection *connection)
{
        guint n;

        for (n = 0; n < values->len; n++) {
                const GStrv row = g_ptr_array_index (values, n);

                /* row[0] is file's DataObject nie:url */
                /* row[1] is resource's DataObject subject */
                /* row[2] is the property */
                /* row[3] is the value */

                if (g_strcmp0 (row[2], TRACKER_NIE_PREFIX "title") == 0) {
                        /* Write row[3] as title to the remote resource */
                }
        }
    
        return TRUE;
}

Implement this function to indicate which rdf:types that you support.

const gchar**
writeback_module_get_rdftypes (void)
{
        static const gchar *rdftypes[] = { TRACKER_SOME_PREFIX "MyClassThatISupport",
                                           TRACKER_SOME_PREFIX "MyOtherClassThatISupport",
                                           NULL };

        return rdftypes;
}

Listening for the Writeback signal yourself

We don't recommend this. But you can also subscribe your own program to Tracker's Writeback signal. The signal tells you when tracker-store stored triples that affect properties that are set with tracker:writeback true. The signal has the subject as first and an array of rdf:types as second parameter. You can find the signal at the /org/freedesktop/Tracker1/Resources object in tracker-store's DBus API.

When saving in local files you should do something like this (else you'll confuse the FS miner of Tracker):

#include <libtracker-common/tracker-file-utils.h>
#include <libtracker-miner/tracker-miner-manager.h>

static gboolean
file_unlock_cb (gpointer user_data)
{
        GFile *file;
        file = user_data;
        tracker_file_unlock (file);
        g_object_unref (file);
        return FALSE;
}

static void
my_save_file (GFile *file, GPtrArray *my_metadata)
{
  gchar *urls[2] = { NULL, NULL };

  tracker_file_lock (file);

  urls[0] = g_file_get_uri (file);

  /* This is important: if you don't tell this to the FS miner of Tracker,
   * your file-write will make it reindex the file. Which means
   * that you'll get another Writeback signal shortly after. I don't
   * think you want the quagmire of having to solve that yourself? */

  tracker_miner_manager_ignore_next_update (miner_manager,
                                            "org.freedesktop.Tracker1.Miner.Files",
                                            urls);

  /* You can also just call IgnoreNextUpdate() on org.freedesktop.Tracker1.Miner.Files,
   * if you don't want to depend on libtracker-miner for this */

  /* write your file, you can also use rename() at the end
   * for atomicy */

  g_timeout_add_seconds (3, file_unlock_cb, g_object_ref (file));

  g_strfreev (urls);
}

I think it's safe to conclude that you should read the code of tracker-writeback thoroughly before attempting to write your own write back software. Also question yourself a few times in a row why you aren't simply writing a write back module for it, like described above. The Tracker team will question that, in any case.

Testing

The DELETE is just so you can repeat the test multiple times.

tracker-sparql -u -q "
DELETE { <file:///my/file.mine> nie:title ?unknown1 ;
                                nie:url ?unknown2 }
WHERE { <file:///my/file.mine> nie:title ?unknown1 ;
                               nie:url ?unknown2 } 
INSERT { <file:///my/file.mine> a nfo:MyClassThatISupport; 
                                nie:url <file:///my/file.mine> ; 
                                nie:title 'My test'  }"

Attic/Tracker/Documentation/Writeback (last edited 2023-08-14 12:50:13 by CarlosGarnacho)