summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--configure.ac1
-rw-r--r--src/plugins/media-export/Makefile.am31
-rw-r--r--src/plugins/media-export/rygel-media-export-extract.vala238
-rw-r--r--src/plugins/media-export/rygel-media-export-harvester.vala1
-rw-r--r--src/plugins/media-export/rygel-media-export-harvesting-task.vala39
-rw-r--r--src/plugins/media-export/rygel-media-export-info-serializer.vala215
-rw-r--r--src/plugins/media-export/rygel-media-export-item-factory.vala387
-rw-r--r--src/plugins/media-export/rygel-media-export-metadata-extractor.vala290
8 files changed, 903 insertions, 299 deletions
diff --git a/configure.ac b/configure.ac
index a7955aa6..b7815022 100644
--- a/configure.ac
+++ b/configure.ac
@@ -182,6 +182,7 @@ AS_IF([test "x$with_media_engine" = "xgstreamer"],
[
PKG_CHECK_MODULES([RYGEL_PLUGIN_MEDIA_EXPORT_DEPS],
[$RYGEL_COMMON_MODULES gio-2.0 >= $GIO_REQUIRED
+ gio-unix-2.0 >= $GIO_REQUIRED
gupnp-dlna-2.0 >= $GUPNP_DLNA_REQUIRED
gupnp-dlna-gst-2.0 >= $GUPNP_DLNA_REQUIRED
gstreamer-app-1.0 >= $GSTREAMER_APP_REQUIRED
diff --git a/src/plugins/media-export/Makefile.am b/src/plugins/media-export/Makefile.am
index 4916bcc0..ee19f169 100644
--- a/src/plugins/media-export/Makefile.am
+++ b/src/plugins/media-export/Makefile.am
@@ -1,5 +1,33 @@
+if UNINSTALLED
+MX_EXTRACT_PATH=$(abs_builddir)/mx-extract
+else
+MX_EXTRACT_PATH=$(libexecdir)/mx-extract
+endif
+
include $(top_srcdir)/common.am
+## Extraction helper
+pkglibexec_PROGRAMS = mx-extract
+mx_extract_SOURCES = \
+ rygel-media-export-extract.vala \
+ rygel-media-export-info-serializer.vala
+
+mx_extract_VALAFLAGS = \
+ --enable-experimental \
+ --pkg posix \
+ --pkg gio-unix-2.0 \
+ $(RYGEL_PLUGIN_MEDIA_EXPORT_DEPS_VALAFLAGS) \
+ $(RYGEL_COMMON_VALAFLAGS)
+
+mx_extract_CFLAGS = \
+ $(RYGEL_PLUGIN_MEDIA_EXPORT_DEPS_CFLAGS) \
+ $(RYGEL_COMMON_LIBRYGEL_SERVER_CFLAGS) \
+ -DG_LOG_DOMAIN='"MediaExport"'
+
+mx_extract_LDADD = \
+ $(RYGEL_PLUGIN_MEDIA_EXPORT_DEPS_LIBS)
+
+## Plugin
plugin_LTLIBRARIES = librygel-media-export.la
plugin_DATA = media-export.plugin
@@ -43,6 +71,8 @@ librygel_media_export_la_VALAFLAGS = \
--internal-vapi rygel-media-export.vapi \
--internal-header rygel-media-export-internal.h \
--header rygel-media-export.h \
+ --pkg posix \
+ --pkg gio-unix-2.0 \
$(RYGEL_PLUGIN_MEDIA_EXPORT_DEPS_VALAFLAGS) \
$(RYGEL_COMMON_LIBRYGEL_SERVER_VALAFLAGS) \
$(RYGEL_COMMON_VALAFLAGS)
@@ -50,6 +80,7 @@ librygel_media_export_la_VALAFLAGS = \
librygel_media_export_la_CFLAGS = \
$(RYGEL_PLUGIN_MEDIA_EXPORT_DEPS_CFLAGS) \
$(RYGEL_COMMON_LIBRYGEL_SERVER_CFLAGS) \
+ -DMX_EXTRACT_PATH='"$(MX_EXTRACT_PATH)"' \
-DG_LOG_DOMAIN='"MediaExport"'
librygel_media_export_la_LIBADD = \
$(RYGEL_PLUGIN_MEDIA_EXPORT_DEPS_LIBS) \
diff --git a/src/plugins/media-export/rygel-media-export-extract.vala b/src/plugins/media-export/rygel-media-export-extract.vala
new file mode 100644
index 00000000..a1fb0c5e
--- /dev/null
+++ b/src/plugins/media-export/rygel-media-export-extract.vala
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2015 Jens Georg <mail@jensge.org>.
+ *
+ * Author: Jens Georg <mail@jensge.org>
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Rygel is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+using Gst.PbUtils;
+using GUPnPDLNA;
+using Gst;
+
+const string UPNP_CLASS_PHOTO = "object.item.imageItem.photo";
+const string UPNP_CLASS_MUSIC = "object.item.audioItem.musicTrack";
+const string UPNP_CLASS_VIDEO = "object.item.videoItem";
+const string UPNP_CLASS_PLAYLIST = "object.item.playlistItem";
+
+const string STATUS_LINE_TEMPLATE = "RESULT|%s|%" + size_t.FORMAT + "|%s\n";
+const string ERROR_LINE_TEMPLATE = "ERROR|%s|%d|%s\n";
+
+const string FATAL_ERROR_PREFIX = "FATAL_ERROR|";
+const string FATAL_ERROR_SUFFIX = "\n"; //|0|Killed by signal\n";
+
+static int in_fd = 0;
+static int out_fd = 1;
+static int err_fd = 2;
+
+MainLoop loop;
+
+DataInputStream input_stream;
+OutputStream output_stream;
+OutputStream error_stream;
+Rygel.InfoSerializer serializer;
+
+public errordomain MetadataExtractorError {
+ GENERAL
+}
+
+static const OptionEntry[] options = {
+ { "input-fd", 'i', 0, OptionArg.INT, ref in_fd, "File descriptor used for input", null },
+ { "output-fd", 'o', 0, OptionArg.INT, ref out_fd, "File descriptor used for output", null },
+ { "error-fd", 'e', 0, OptionArg.INT, ref err_fd, "File descriptor used for severe errors", null },
+ { null }
+};
+
+Discoverer discoverer;
+ProfileGuesser guesser;
+
+static uint8 last_uri_data[4096];
+size_t last_uri_data_length;
+
+static void segv_handler (int signal) {
+ Posix.write (err_fd, (void *) last_uri_data, last_uri_data_length);
+ Posix.write (err_fd, (void *) FATAL_ERROR_SUFFIX, 1);
+ Posix.fsync (err_fd);
+
+ Posix.exit(-1);
+}
+
+async void run () {
+ while (true) {
+ try {
+ var line = yield input_stream.read_line_async ();
+ if (line == null) {
+ break;
+ }
+
+ if (line.has_prefix ("EXTRACT ")) {
+ debug ("Got command to extract file: %s", line);
+ var uri = line.replace ("EXTRACT ", "").strip ();
+ DiscovererInfo? info = null;
+ try {
+ // Copy current URI to statically allocated memory area to
+ // dump to fd in the signal handler
+ last_uri_data_length = uri.length;
+ GLib.Memory.set (last_uri_data, 0, 4096);
+ GLib.Memory.copy (last_uri_data, (void *) uri, uri.length);
+ info = discoverer.discover_uri (uri);
+
+ debug ("Finished discover on uri %s", uri);
+ yield on_discovered (info);
+ } catch (Error error) {
+ warning (_("Failed to discover uri %s: %s"),
+ uri,
+ error.message);
+ send_error (File.new_for_uri (uri), error);
+
+ // Recreate the discoverer on error
+ discoverer = new Discoverer (10 * Gst.SECOND);
+ }
+ //discoverer.discover_uri_async (uri);
+ } else if (line.has_prefix ("QUIT")) {
+ break;
+ }
+ } catch (Error error) {
+ warning (_("Failed to read from pipe: %s"), error.message);
+
+ break;
+ }
+ }
+
+ loop.quit ();
+}
+
+static void send_extraction_done (File file, Variant v) throws Error {
+ var data = v.get_data_as_bytes ();
+ size_t bytes_written = 0;
+ var status = STATUS_LINE_TEMPLATE.printf (file.get_uri (),
+ data.get_size (),
+ file.get_uri ());
+
+ output_stream.write_all (status.data, out bytes_written);
+ output_stream.write_all (data.get_data (), out bytes_written);
+}
+
+static void send_error (File file, Error err) {
+ size_t bytes_written = 0;
+ var status = ERROR_LINE_TEMPLATE.printf (file.get_uri (),
+ err.code,
+ err.message);
+ try {
+ output_stream.write_all (status.data, out bytes_written);
+ } catch (Error error) {
+ warning (_("Failed to send error to parent: %s"), error.message);
+ }
+}
+
+static async void on_discovered (DiscovererInfo info) {
+ debug ("Discovered %s", info.get_uri ());
+ var file = File.new_for_uri (info.get_uri ());
+ if (info.get_result () == DiscovererResult.TIMEOUT ||
+ info.get_result () == DiscovererResult.BUSY ||
+ info.get_result () == DiscovererResult.MISSING_PLUGINS) {
+ if (info.get_result () == DiscovererResult.MISSING_PLUGINS) {
+ debug ("Plugins are missing for extraction of file %s",
+ info.get_uri ());
+ } else {
+ debug ("Extraction timed out on %s", file.get_uri ());
+ }
+ yield extract_basic_information (file, null, null);
+
+ return;
+ }
+
+ var dlna_info = GUPnPDLNAGst.utils_information_from_discoverer_info (info);
+ var dlna = guesser.guess_profile_from_info (dlna_info);
+ yield extract_basic_information (file, info, dlna);
+}
+
+static async void extract_basic_information (File file,
+ DiscovererInfo? info,
+ GUPnPDLNA.Profile? dlna) {
+ FileInfo file_info;
+
+ try {
+ file_info = yield file.query_info_async (FileAttribute.STANDARD_CONTENT_TYPE
+ + "," +
+ FileAttribute.STANDARD_SIZE + "," +
+ FileAttribute.TIME_MODIFIED + "," +
+ FileAttribute.STANDARD_DISPLAY_NAME,
+ FileQueryInfoFlags.NONE);
+ } catch (Error error) {
+ var uri = file.get_uri ();
+
+ warning (_("Failed to extract basic metadata from %s: %s"),
+ uri,
+ error.message);
+
+ // signal error to parent
+ send_error (file, error);
+
+ return;
+ }
+
+ try {
+ send_extraction_done (file,
+ serializer.serialize (file, file_info, info, dlna));
+ } catch (Error error) {
+ send_error (file, error);
+ }
+}
+
+int main (string[] args) {
+ var ctx = new OptionContext (_("- helper binary for Rygel to extract meta-data"));
+ ctx.add_main_entries (options, null);
+ ctx.add_group (Gst.init_get_option_group ());
+
+ try {
+ ctx.parse (ref args);
+ } catch (Error error) {
+ warning (_("Failed to parse commandline args: %s"), error.message);
+
+ return Posix.EXIT_FAILURE;
+ }
+
+ serializer = new Rygel.InfoSerializer ();
+ Posix.nice (19);
+
+ var action = new Posix.sigaction_t ();
+ action.sa_handler = segv_handler;
+ Posix.sigaction (Posix.SIGSEGV, action, null);
+ Posix.sigaction (Posix.SIGABRT, action, null);
+
+ message ("Started with descriptors %d %d %d", in_fd, out_fd, err_fd);
+
+ input_stream = new DataInputStream (new UnixInputStream (in_fd, false));
+ output_stream = new UnixOutputStream (out_fd, false);
+ error_stream = new UnixOutputStream (err_fd, false);
+
+ loop = new MainLoop ();
+ try {
+ discoverer = new Discoverer (10 * Gst.SECOND);
+ } catch (Error error) {
+ warning (_("Failed to start meta-data discoverer: %s"),
+ error.message);
+ }
+
+ guesser = new ProfileGuesser (true, true);
+
+ run.begin ();
+ loop.run ();
+
+ return 0;
+}
diff --git a/src/plugins/media-export/rygel-media-export-harvester.vala b/src/plugins/media-export/rygel-media-export-harvester.vala
index 1c9038b7..d1e077c0 100644
--- a/src/plugins/media-export/rygel-media-export-harvester.vala
+++ b/src/plugins/media-export/rygel-media-export-harvester.vala
@@ -74,6 +74,7 @@ internal class Rygel.MediaExport.Harvester : GLib.Object {
info.get_content_type () == "application/xml" ||
info.get_content_type () == "text/xml" ||
info.get_content_type () == "text/plain";
+ // Todo: Check blacklist
}
/**
diff --git a/src/plugins/media-export/rygel-media-export-harvesting-task.vala b/src/plugins/media-export/rygel-media-export-harvesting-task.vala
index fd019f49..69bd1038 100644
--- a/src/plugins/media-export/rygel-media-export-harvesting-task.vala
+++ b/src/plugins/media-export/rygel-media-export-harvesting-task.vala
@@ -63,19 +63,24 @@ public class Rygel.MediaExport.HarvestingTask : Rygel.StateMachine,
this.parent = parent;
this.cache = MediaCache.get_default ();
- this.extractor.extraction_done.connect (on_extracted_cb);
- this.extractor.error.connect (on_extractor_error_cb);
+ this.extractor.extraction_done.connect (this.on_extracted_cb);
+ this.extractor.error.connect (this.on_extractor_error_cb);
this.files = new LinkedList<FileQueueEntry> ();
this.containers = new GLib.Queue<MediaContainer> ();
this.monitor = monitor;
}
+ ~HarvestingTask () {
+ this.extractor.stop ();
+ }
+
public void cancel () {
// detach from common cancellable; otherwise everything would be
// cancelled like file monitoring, other harvesters etc.
this.cancellable = new Cancellable ();
this.cancellable.cancel ();
+ this.extractor.stop ();
}
/**
@@ -95,6 +100,8 @@ public class Rygel.MediaExport.HarvestingTask : Rygel.StateMachine,
*/
public async void run () {
try {
+ this.extractor.run.begin ();
+
var info = yield this.origin.query_info_async
(HARVESTER_ATTRIBUTES,
FileQueryInfoFlags.NONE,
@@ -110,6 +117,7 @@ public class Rygel.MediaExport.HarvestingTask : Rygel.StateMachine,
this.completed ();
}
} catch (Error error) {
+ this.extractor.stop ();
if (!(error is IOError.CANCELLED)) {
warning (_("Failed to harvest file %s: %s"),
this.origin.get_uri (),
@@ -283,25 +291,18 @@ public class Rygel.MediaExport.HarvestingTask : Rygel.StateMachine,
}
private void on_extracted_cb (File file,
- DiscovererInfo? discoverer_info,
- GUPnPDLNA.Profile? dlna_profile,
- FileInfo file_info) {
+ Variant info) {
+ if (!file.equal (this.files.peek ().file)) {
+ debug ("Not for us, ignoring");
+ }
+
if (this.cancellable.is_cancelled ()) {
this.completed ();
}
- MediaFileItem item;
- if (discoverer_info == null) {
- item = ItemFactory.create_simple (this.containers.peek_head (),
- file,
- file_info);
- } else {
- item = ItemFactory.create_from_info (this.containers.peek_head (),
- file,
- discoverer_info,
- dlna_profile,
- file_info);
- }
+ var item = ItemFactory.create_from_variant (this.containers.peek_head (),
+ file,
+ info);
if (item != null) {
item.parent_ref = this.containers.peek_head ();
@@ -324,10 +325,12 @@ public class Rygel.MediaExport.HarvestingTask : Rygel.StateMachine,
// failed; there's not much to do here, just print the information and
// go to the next file
- debug ("Skipping %s; extraction completely failed: %s",
+ warning ("Skipping %s; extraction completely failed: %s",
file.get_uri (),
error.message);
+ // TODO: Add to blacklist
+
this.files.poll ();
this.do_update ();
}
diff --git a/src/plugins/media-export/rygel-media-export-info-serializer.vala b/src/plugins/media-export/rygel-media-export-info-serializer.vala
new file mode 100644
index 00000000..3e46caa2
--- /dev/null
+++ b/src/plugins/media-export/rygel-media-export-info-serializer.vala
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2015 Jens Georg <mail@jensge.org>.
+ *
+ * Author: Jens Georg <mail@jensge.org>
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Rygel is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+using Gst;
+using Gst.PbUtils;
+
+internal errordomain InfoSerializerError {
+ INVALID_STREAM,
+ BAD_MIME
+}
+
+internal class Rygel.InfoSerializer {
+ public Variant serialize (File file,
+ FileInfo file_info,
+ DiscovererInfo? info,
+ GUPnPDLNA.Profile? dlna_profile) throws Error {
+ // Guess UPnP class
+ if (info != null) {
+ string? upnp_class = null;
+
+ var audio_streams = (GLib.List<DiscovererAudioInfo>)
+ info.get_audio_streams ();
+ var video_streams = (GLib.List<DiscovererVideoInfo>)
+ info.get_video_streams ();
+ if (audio_streams == null && video_streams == null) {
+ debug ("%s had neither audio nor video/picture " +
+ "streams. Ignoring.",
+ file.get_uri ());
+
+ throw new InfoSerializerError.INVALID_STREAM ("No stream information");
+ }
+
+ if (audio_streams == null && video_streams.data.is_image ()) {
+ upnp_class = UPNP_CLASS_PHOTO;
+ } else if (video_streams != null) {
+ upnp_class = UPNP_CLASS_VIDEO;
+ } else if (audio_streams != null) {
+ upnp_class = UPNP_CLASS_MUSIC;
+ } else {
+ // Uh...
+ }
+
+ return new Variant ("(smvmvmvmvmvmv)",
+ upnp_class,
+ this.serialize_file_info (file_info),
+ this.serialize_dlna_profile (dlna_profile),
+ this.serialize_info (info),
+ this.serialize_audio_info (audio_streams != null ?
+ audio_streams.data : null),
+ this.serialize_video_info (video_streams != null ?
+ video_streams.data : null),
+ this.serialize_meta_data (audio_streams != null ?
+ audio_streams.data : null));
+ } else {
+ string? upnp_class = null;
+ var mime = ContentType.get_mime_type (file_info.get_content_type ());
+ if (mime.has_prefix ("video/")) {
+ upnp_class = UPNP_CLASS_VIDEO;
+ } else if (mime.has_prefix ("image/")) {
+ upnp_class = UPNP_CLASS_PHOTO;
+ } else if (mime.has_prefix ("audio/") || mime == "application/ogg") {
+ upnp_class = UPNP_CLASS_MUSIC;
+ } else if (mime.has_suffix ("/xml")) { // application/xml or text/xml
+ upnp_class = UPNP_CLASS_PLAYLIST;
+ } else {
+ debug ("Unsupported content-type %s, skipping %s…",
+ mime,
+ file.get_uri ());
+
+ throw new InfoSerializerError.BAD_MIME ("Not supported: %s", mime);
+ }
+
+ return new Variant ("(ssmvmvmvmvmvmv)",
+ file.get_uri (),
+ upnp_class,
+ this.serialize_file_info (file_info),
+ null,
+ null,
+ null,
+ null,
+ null);
+ }
+ }
+
+ private Variant serialize_file_info (FileInfo info) {
+ return new Variant ("(sstt)",
+ info.get_display_name (),
+ ContentType.get_mime_type
+ (info.get_content_type ()),
+ info.get_attribute_uint64
+ (FileAttribute.TIME_MODIFIED),
+ info.get_size ());
+ }
+
+ private Variant? serialize_dlna_profile (GUPnPDLNA.Profile? profile) {
+ if (profile == null) {
+ return null;
+ }
+
+ return new Variant ("(ss)", profile.name, profile.mime);
+ }
+
+ private Variant? serialize_info (DiscovererInfo? info) {
+ long duration = -1;
+ if (info.get_duration () > 0) {
+ duration = (long) (info.get_duration () / Gst.SECOND);
+ }
+
+ var tags = info.get_tags ();
+ string? title = null;
+ if (tags != null) {
+ tags.get_string (Tags.TITLE, out title);
+ }
+
+ string date = null;
+ Gst.DateTime? dt = null;
+ if (tags != null && tags.get_date_time (Tags.DATE_TIME, out dt)) {
+ // Make a minimal valid iso8601 date - bgo#702231
+ // This mostly happens with MP3 files which only have a year
+ if (!dt.has_day () || !dt.has_month ()) {
+ date = "%d-%02d-%02d".printf (dt.get_year (),
+ dt.has_month () ?
+ dt.get_month () : 1,
+ dt.has_day () ?
+ dt.get_day () : 1);
+ } else {
+ date = dt.to_iso8601_string ();
+ }
+ }
+
+ return new Variant ("(msmsi)",
+ title,
+ date,
+ duration);
+ }
+
+ private Variant? serialize_video_info (DiscovererVideoInfo? info) {
+ if (info == null) {
+ return null;
+ }
+
+ return new Variant ("(iii)",
+ (int) info.get_width (),
+ (int) info.get_height (),
+ info.get_depth () > 0 ?
+ info.get_depth () : -1);
+ }
+
+ private Variant? serialize_audio_info (DiscovererAudioInfo? info) {
+ if (info == null) {
+ return null;
+ }
+
+ return new Variant ("(ii)",
+ (int) info.get_channels (),
+ (int) info.get_sample_rate ());
+
+ }
+
+ private Variant? serialize_meta_data (DiscovererAudioInfo? info) {
+ if (info == null) {
+ return null;
+ }
+
+ var tags = info.get_tags ();
+ if (tags == null) {
+ return null;
+ }
+
+ string artist = null;
+ tags.get_string (Tags.ARTIST, out artist);
+
+ string album = null;
+ tags.get_string (Tags.ALBUM, out album);
+
+ string genre = null;
+ tags.get_string (Tags.GENRE, out genre);
+
+ uint volume = uint.MAX;
+ tags.get_uint (Tags.ALBUM_VOLUME_NUMBER, out volume);
+
+ uint track = uint.MAX;
+ tags.get_uint (Tags.TRACK_NUMBER, out track);
+
+ uint bitrate = uint.MAX;
+ tags.get_uint (Tags.BITRATE, out bitrate);
+
+ return new Variant ("(msmsmsiii)",
+ artist,
+ album,
+ genre,
+ volume,
+ track,
+ ((int) bitrate) / 8);
+ }
+}
diff --git a/src/plugins/media-export/rygel-media-export-item-factory.vala b/src/plugins/media-export/rygel-media-export-item-factory.vala
index a0866b26..b6f73ed2 100644
--- a/src/plugins/media-export/rygel-media-export-item-factory.vala
+++ b/src/plugins/media-export/rygel-media-export-item-factory.vala
@@ -113,261 +113,248 @@ namespace Rygel.MediaExport.ItemFactory {
}
}
- public static MediaFileItem? create_from_info (MediaContainer parent,
- File file,
- DiscovererInfo info,
- GUPnPDLNA.Profile? profile,
- FileInfo file_info) {
- MediaFileItem item;
- string id = MediaCache.get_id (file);
- GLib.List<DiscovererAudioInfo> audio_streams;
- GLib.List<DiscovererVideoInfo> video_streams;
+ static MediaFileItem? create_from_variant (MediaContainer parent,
+ File file,
+ Variant v) {
+ if (!v.is_of_type (new VariantType ("(smvmvmvmvmvmv)"))) {
+ warning ("Invalid meta-data serialisation, cannot process %s",
+ v.get_type_string ());
+
+ return null;
+ }
- audio_streams = (GLib.List<DiscovererAudioInfo>)
- info.get_audio_streams ();
- video_streams = (GLib.List<DiscovererVideoInfo>)
- info.get_video_streams ();
+ Variant? upnp_class, file_info, dlna_profile, info, video_info, audio_info, meta_data;
- if (audio_streams == null && video_streams == null) {
- debug ("%s had neither audio nor video/picture " +
- "streams. Ignoring.",
- file.get_uri ());
+ var it = v.iterator ();
+ if (it.n_children () != 7) {
+ warning ("Invalid meta-data serialisation: exprected 7 children, got %d", (int) it.n_children ());
return null;
}
- if (audio_streams == null && video_streams.data.is_image ()) {
- item = new PhotoItem (id, parent, "");
- return fill_visual_item (item as PhotoItem,
- file,
- info,
- profile,
- video_streams.data,
- file_info);
- } else if (video_streams != null) {
- item = new VideoItem (id, parent, "");
-
- var audio_info = null as DiscovererAudioInfo;
- if (audio_streams != null) {
- audio_info = audio_streams.data;
- }
+ var id = MediaCache.get_id (file);
- return fill_video_item (item as VideoItem,
- file,
- info,
- profile,
- video_streams.data,
- audio_info,
- file_info);
- } else if (audio_streams != null) {
- item = new MusicItem (id, parent, "");
- return fill_music_item (item as MusicItem,
- file,
- info,
- profile,
- audio_streams.data,
- file_info);
- } else {
- return null;
+ upnp_class = it.next_value ();
+
+ file_info = it.next_value ().get_maybe ();
+ if (file_info != null) {
+ file_info = file_info.get_variant ();
}
- }
- private static void fill_audio_item (AudioItem item,
- DiscovererInfo info,
- DiscovererAudioInfo? audio_info) {
- if (info.get_duration () > 0) {
- item.duration = (long) (info.get_duration () / Gst.SECOND);
- } else {
- item.duration = -1;
+ dlna_profile = it.next_value ().get_maybe ();
+ if (dlna_profile != null) {
+ dlna_profile = dlna_profile.get_variant ();
}
- if (audio_info == null) {
- return;
+ info = it.next_value ().get_maybe ();
+ if (info != null) {
+ info = info.get_variant ();
}
-
- var tags = audio_info.get_tags ();
- if (tags != null) {
- uint tmp;
- tags.get_uint (Tags.BITRATE, out tmp);
- item.bitrate = (int) tmp / 8;
+
+ audio_info = it.next_value ().get_maybe ();
+ if (audio_info != null) {
+ audio_info = audio_info.get_variant ();
}
- item.channels = (int) audio_info.get_channels ();
- item.sample_freq = (int) audio_info.get_sample_rate ();
- }
+ video_info = it.next_value ().get_maybe ();
+ if (video_info != null) {
+ video_info = video_info.get_variant ();
+ }
+ meta_data = it.next_value ().get_maybe ();
+ if (meta_data != null) {
+ meta_data = meta_data.get_variant ();
+ }
- private static MediaFileItem fill_video_item (VideoItem item,
- File file,
- DiscovererInfo info,
- GUPnPDLNA.Profile? profile,
- DiscovererVideoInfo video_info,
- DiscovererAudioInfo? audio_info,
- FileInfo file_info) {
- fill_audio_item (item as AudioItem, info, audio_info);
- fill_visual_item (item as VisualItem,
- file,
- info,
- profile,
- video_info,
- file_info);
+ MediaFileItem item = null;
+ switch (upnp_class.get_string ()) {
+ case Rygel.PhotoItem.UPNP_CLASS:
+ item = new PhotoItem (id, parent, "");
+ break;
+ case Rygel.VideoItem.UPNP_CLASS:
+ item = new VideoItem (id, parent, "");
+ break;
+ case Rygel.MusicItem.UPNP_CLASS:
+ item = new MusicItem (id, parent, "");
+ break;
+ default:
+ return null;
+ }
- return item;
- }
+ item.add_uri (file.get_uri ());
- private static MediaFileItem fill_visual_item (VisualItem item,
- File file,
- DiscovererInfo info,
- GUPnPDLNA.Profile? profile,
- DiscovererVideoInfo video_info,
- FileInfo file_info) {
- fill_media_item (item, file, info, profile, file_info);
+ if (dlna_profile != null) {
+ apply_dlna_profile (item, dlna_profile);
+ }
- item.width = (int) video_info.get_width ();
- item.height = (int) video_info.get_height ();
+ if (file_info != null) {
+ apply_file_info (item, file_info);
+ }
- var color_depth = (int) video_info.get_depth ();
- item.color_depth = (color_depth == 0) ? -1 : color_depth;
+ if (info != null) {
+ apply_info (item, info);
+ }
- return item;
- }
+ if (audio_info != null) {
+ apply_audio_info (item, audio_info);
+ }
- private static MediaFileItem fill_music_item (MusicItem item,
- File file,
- DiscovererInfo info,
- GUPnPDLNA.Profile? profile,
- DiscovererAudioInfo? audio_info,
- FileInfo file_info) {
- fill_audio_item (item as AudioItem, info, audio_info);
- fill_media_item (item, file, info, profile, file_info);
+ if (video_info != null) {
+ apply_video_info (item, video_info);
+ }
- if (audio_info == null) {
- return item;
+ if (meta_data != null) {
+ apply_meta_data (item, meta_data);
}
- var tags = audio_info.get_tags ();
- if (tags == null) {
- return item;
+ // If the date has a timezone offset, make sure it contains a
+ // colon bgo#702231, DLNA 7.3.21.1
+ if ("T" in item.date) {
+ var date = new Soup.Date.from_string (item.date);
+ item.date = date.to_string (Soup.DateFormat.ISO8601_FULL);
+ }
+
+ return item as MediaFileItem;
+ }
+
+ private static void apply_meta_data (MediaFileItem item, Variant v) {
+ if (!v.is_of_type (new VariantType ("(msmsmsiii)"))) {
+ warning ("Invalid meta-data serialisation of meta-data; %s",
+ v.get_type_string ());
+
+ return;
}
- string artist;
- tags.get_string (Tags.ARTIST, out artist);
- item.artist = artist;
+ var it = v.iterator ();
+ var val = it.next_value ().get_maybe ();
+ item.artist = val == null ? null : val.dup_string ();
- string album;
- tags.get_string (Tags.ALBUM, out album);
- item.album = album;
+ // Audio item
+ val = it.next_value ().get_maybe (); // album
+ var album = val == null ? null : val.dup_string ();
- string genre;
- tags.get_string (Tags.GENRE, out genre);
- item.genre = genre;
+ val = it.next_value ().get_maybe ();
+ item.genre = val == null ? null : val.dup_string ();
- uint tmp;
- tags.get_uint (Tags.ALBUM_VOLUME_NUMBER, out tmp);
- item.disc = (int) tmp;
+ // Audio item
+ var disc = it.next_value ().get_int32 ();
- tags.get_uint (Tags.TRACK_NUMBER, out tmp);
- item.track_number = (int) tmp;
+ if (item is AudioItem) {
+ var audio_item = item as AudioItem;
+ var track_number = it.next_value ().get_int32 ();
+ audio_item.bitrate = it.next_value ().get_int32 ();
+ audio_item.album = album;
+ if (item is MusicItem) {
+ var music_item = item as MusicItem;
+ music_item.disc = disc;
+ music_item.track_number = track_number;
+ }
+ }
+ }
- var store = MediaArtStore.get_default ();
+ private static void apply_video_info (MediaFileItem item, Variant v) {
+ if (!v.is_of_type (new VariantType ("(iii)"))) {
+ warning ("Invalid meta-data serialisation of video info; %s",
+ v.get_type_string ());
- Sample sample;
- tags.get_sample (Tags.IMAGE, out sample);
- if (sample == null) {
- tags.get_sample (Tags.PREVIEW_IMAGE, out sample);
+ return;
}
- if (sample == null) {
- store.search_media_art_for_file (item, file);
+ if (!(item is VisualItem)) {
+ return;
+ }
- return item;
+ var visual_item = item as VisualItem;
+ var it = v.iterator ();
+ visual_item.width = it.next_value ().get_int32 ();
+ visual_item.height = it.next_value ().get_int32 ();
+ visual_item.color_depth = it.next_value ().get_int32 ();
+ }
+
+ private static void apply_audio_info (MediaFileItem item, Variant v) {
+ if (!v.is_of_type (new VariantType ("(ii)"))) {
+ warning ("Invalid meta-data serialisation of audio info; %s",
+ v.get_type_string ());
+
+ return;
}
- unowned Structure structure = sample.get_caps ().get_structure (0);
+ if (!(item is AudioItem)) {
+ return;
+ }
- int image_type;
- structure.get_enum ("image-type",
- typeof (Gst.Tag.ImageType),
- out image_type);
- switch (image_type) {
- case Tag.ImageType.UNDEFINED:
- case Tag.ImageType.FRONT_COVER:
- Gst.MapInfo map_info;
- sample.get_buffer ().map (out map_info, Gst.MapFlags.READ);
+ var audio_item = item as AudioItem;
+ var it = v.iterator ();
+ audio_item.channels = it.next_value ().get_int32 ();
+ audio_item.sample_freq = it.next_value ().get_int32 ();
+ }
- // Work-around bgo#739915
- weak uint8[] data = map_info.data;
- data.length = (int) map_info.size;
+ private static void apply_info (MediaFileItem item, Variant v) {
+ if (!v.is_of_type (new VariantType ("(msmsi)"))) {
+ warning ("Invalid meta-data serialisation of general info");
+ }
- store.add (item, file, data, structure.get_name ());
- sample.get_buffer ().unmap (map_info);
- break;
- default:
- break;
+ var it = v.iterator ();
+ var val = it.next_value ().get_maybe ();
+ if (val != null) {
+ item.title = val.dup_string ();
}
- return item;
+ val = it.next_value ().get_maybe ();
+ if (val != null) {
+ item.date = val.dup_string ();
+ }
+
+ if (item is AudioItem) {
+ (item as AudioItem).duration = it.next_value ().get_int32 ();
+ }
}
- private static void fill_media_item (MediaFileItem item,
- File file,
- DiscovererInfo info,
- GUPnPDLNA.Profile? profile,
- FileInfo file_info) {
- string title = null;
-
- var tags = info.get_tags ();
- if (tags == null ||
- !tags.get_string (Tags.TITLE, out title)) {
- title = file_info.get_display_name ();
-
- }
-
- // This assumes the datetime is valid; checking some demuxers this
- Gst.DateTime? dt = null;
- if (tags != null && tags.get_date_time (Tags.DATE_TIME, out dt)) {
- // Make a minimal valid iso8601 date - bgo#702231
- // This mostly happens with MP3 files which only have a year
- if (!dt.has_day () || !dt.has_month ()) {
- item.date = "%d-%02d-%02d".printf (dt.get_year (),
- dt.has_month () ?
- dt.get_month () : 1,
- dt.has_day () ?
- dt.get_day () : 1);
- } else {
- item.date = dt.to_iso8601_string ();
- }
+ private static void apply_dlna_profile (MediaFileItem item, Variant v) {
+ if (!v.is_of_type (new VariantType ("(ss)"))) {
+ warning ("Invalid meta-data serialisation of DLNA profile %s",
+ v.get_type_string ());
+
+ return;
}
- item.title = title;
+ var it = v.iterator ();
+ item.dlna_profile = it.next_value ().dup_string ();
+ item.mime_type = it.next_value ().dup_string ();
+ }
- // use mtime if no time tag was available
- var mtime = file_info.get_attribute_uint64
- (FileAttribute.TIME_MODIFIED);
+ private static void apply_file_info (MediaFileItem item, Variant v) {
+ if (!v.is_of_type (new VariantType ("(sstt)"))) {
+ warning ("Invalid meta-data serialisation of file info %s",
+ v.get_type_string ());
- if (item.date == null) {
- TimeVal tv = { (long) mtime, 0 };
- item.date = tv.to_iso8601 ();
+ return;
}
- // If the date has a timezone offset, make sure it contains a
- // colon bgo#702231, DLNA 7.3.21.1
- if ("T" in item.date) {
- var date = new Soup.Date.from_string (item.date);
- item.date = date.to_string (Soup.DateFormat.ISO8601_FULL);
+ var it = v.iterator ();
+ if (it.n_children () != 4) {
+ warning ("Invalid meta-data serialisation of file info");
+
+ return;
}
- item.size = (int64) file_info.get_size ();
- item.modified = (int64) mtime;
- if (profile != null && profile.name != null) {
- item.dlna_profile = profile.name;
- item.mime_type = profile.mime;
- } else {
- item.mime_type = ContentType.get_mime_type
- (file_info.get_content_type ());
+ Variant display_name;
+ display_name = it.next_value ();
+ if (item.title == null || item.title == "") {
+ item.title = display_name.dup_string ();
}
- item.add_uri (file.get_uri ());
+ var mime = it.next_value ();
+ if (item.mime_type == null) {
+ item.mime_type = mime.dup_string ();
+ }
+
+ item.modified = (int64) it.next_value ().get_uint64 ();
+ if (item.date == null) {
+ TimeVal tv = { (long) item.modified, 0 };
+ item.date = tv.to_iso8601 ();
+ }
+ item.size = (int64) it.next_value ().get_uint64 ();
}
}
diff --git a/src/plugins/media-export/rygel-media-export-metadata-extractor.vala b/src/plugins/media-export/rygel-media-export-metadata-extractor.vala
index 4d007787..1a47de03 100644
--- a/src/plugins/media-export/rygel-media-export-metadata-extractor.vala
+++ b/src/plugins/media-export/rygel-media-export-metadata-extractor.vala
@@ -29,133 +29,261 @@ using Gee;
using GUPnP;
using GUPnPDLNA;
+public errordomain MetadataExtractorError {
+ GENERAL,
+ BLACKLIST
+}
+
/**
* Metadata extractor based on Gstreamer. Just set the URI of the media on the
* uri property, it will extact the metadata for you and emit signal
* metadata_available for each key/value pair extracted.
*/
public class Rygel.MediaExport.MetadataExtractor: GLib.Object {
+ private static VariantType SERIALIZED_DATA_TYPE;
+
/* Signals */
- public signal void extraction_done (File file,
- DiscovererInfo? info,
- GUPnPDLNA.Profile? profile,
- FileInfo file_info);
+ public signal void extraction_done (File file, Variant info);
/**
* Signalize that an error occured during metadata extraction
*/
public signal void error (File file, Error err);
- private Discoverer discoverer;
- private ProfileGuesser guesser;
+ /// Cache for the config value
+ private bool extract_metadata;
- /**
- * We export a GLib.File-based API but GstDiscoverer works with URIs, so
- * we store uri->GLib.File mappings in this hashmap, so that we can get
- * the GLib.File back from the URI in on_discovered().
- */
- private HashMap<string, File> file_hash;
- private uint timeout = 10; /* seconds */
+ /// Stream for feeding input to the child process.
+ private UnixOutputStream input_stream;
- private bool extract_metadata;
+ /// Stream for receiving normal input from the child
+ private DataInputStream output_stream;
+
+ /// Stream for receiving exception events from the child
+ private DataInputStream error_stream;
+
+ /// Cancellable for cancelling child I/O
+ private Cancellable child_io_cancellable;
+
+ /// Launcher for subprocesses
+ private SubprocessLauncher launcher;
+
+ /// URI that caused a fatal error in the extraction process
+ private string error_uri = null;
+
+ [CCode (cheader_filename = "glib-unix.h", cname = "g_unix_open_pipe")]
+ extern static bool open_pipe ([CCode (array_length = false)]int[] fds, int flags) throws GLib.Error;
+
+ static construct {
+ SERIALIZED_DATA_TYPE = new VariantType ("(smvmvmvmvmvmv)");
+ }
public MetadataExtractor () {
- this.file_hash = new HashMap<string, File> ();
+ this.child_io_cancellable = new Cancellable ();
var config = MetaConfig.get_default ();
config.setting_changed.connect (this.on_config_changed);
this.on_config_changed (config, Plugin.NAME, "extract-metadata");
}
- public void extract (File file, string content_type) {
- if (this.extract_metadata && !content_type.has_prefix ("text/")) {
- string uri = file.get_uri ();
+ [CCode (cname="MX_EXTRACT_PATH")]
+ private extern const string MX_EXTRACT_PATH;
+
+ private const string[] MX_EXTRACT_ARGV = {
+ MX_EXTRACT_PATH,
+ "--input-fd=3",
+ "--output-fd=4",
+ "--error-fd=5",
+ null
+ };
+
+ public void stop () {
+ this.child_io_cancellable.cancel ();
+ try {
+ var s = "QUIT\n";
+ this.input_stream.write_all (s.data, null, null);
+ this.input_stream.flush ();
+ } catch (Error error) {
+ warning (_("Failed to gracefully stop the process. Using KILL"));
+ }
+ }
+
+ public async void run () {
+ // We use dedicated fds for all of the communication, otherwise the
+ // commands/responses intermix with the debug output.
+ //
+ // This is still wip, we could also use a domain socket or a private
+ // DBus
+
+ int[] pipe_in = { 0, 0 };
+ int[] pipe_out = { 0, 0 };
+ int[] pipe_err = { 0, 0 };
+
+ bool restart = false;
+ do {
+ restart = false;
try {
- var gst_timeout = (ClockTime) (this.timeout * Gst.SECOND);
+ open_pipe (pipe_in, Posix.FD_CLOEXEC);
+ open_pipe (pipe_out, Posix.FD_CLOEXEC);
+ open_pipe (pipe_err, Posix.FD_CLOEXEC);
+
+ this.launcher = new SubprocessLauncher (SubprocessFlags.NONE);
+ this.launcher.take_fd (pipe_in[0], 3);
+ this.launcher.take_fd (pipe_out[1], 4);
+ this.launcher.take_fd (pipe_err[1], 5);
+
+ this.input_stream = new UnixOutputStream (pipe_in[1], true);
+ this.output_stream = new DataInputStream (
+ new UnixInputStream (pipe_out[0],
+ true));
+ this.error_stream = new DataInputStream (
+ new UnixInputStream (pipe_err[0],
+ true));
+
+ this.child_io_cancellable = new Cancellable ();
+
+ this.output_stream.read_line_async.begin (Priority.DEFAULT,
+ this.child_io_cancellable,
+ this.on_input);
+ this.error_uri = null;
+ this.error_stream.read_line_async.begin (Priority.DEFAULT,
+ this.child_io_cancellable,
+ this.on_child_error);
- this.discoverer = new Discoverer (gst_timeout);
+ var subprocess = launcher.spawnv (MX_EXTRACT_ARGV);
+ try {
+ yield subprocess.wait_check_async ();
+ // Process exitted properly -> That shouldn't really
+ // happen
+ } catch (Error error) {
+ warning (_("Process check_async failed: %s"),
+ error.message);
+
+ // TODO: Handle error/crash/signal etc.
+ restart = true;
+ this.child_io_cancellable.cancel ();
+ var msg = _("Process died while handling URI %s");
+ this.error (File.new_for_uri (this.error_uri),
+ new MetadataExtractorError.BLACKLIST (msg,
+ this.error_uri));
+ }
} catch (Error error) {
- debug ("Failed to create a discoverer. Doing basic extraction.");
- this.extract_basic_information (file, null, null);
+ warning (_("Setting up extraction suprocess failed: %s"),
+ error.message);
+ }
+ } while (restart);
+
+ debug ("Metadata extractor finished.");
+ }
- return;
+ private void on_child_error (GLib.Object? object, AsyncResult result) {
+ var stream = object as DataInputStream;
+ if (stream != null) {
+ try {
+ this.error_uri = stream.read_line_async.end (result);
+ warning (_("Child failed fatally. Last uri was %s"),
+ this.error_uri);
+ } catch (Error error) {
+ if (error is IOError.CANCELLED) {
+ debug ("Reading was cancelled...");
+ } else {
+ warning (_("Reading from child's error stream failed: %s"),
+ error.message);
+ }
}
- this.file_hash.set (uri, file);
- this.discoverer.discovered.connect (this.on_done);
- this.discoverer.start ();
- this.discoverer.discover_uri_async (uri);
- this.guesser = new GUPnPDLNA.ProfileGuesser (true, true);
- } else {
- this.extract_basic_information (file, null, null);
}
}
- private void on_done (DiscovererInfo info, GLib.Error err) {
- this.discoverer = null;
- var file = this.file_hash.get (info.get_uri ());
- if (file == null) {
- warning ("File %s already handled, ignoring event",
- info.get_uri ());
+ private void on_input (GLib.Object? object, AsyncResult result) {
+ try {
+ var stream = object as DataInputStream;
+ var str = stream.read_line_async.end (result);
- return;
- }
+ // XXX: While and Goto language are equivalent. Yuck.
+ do {
+ if (str == null) {
+ break;
+ }
- this.file_hash.unset (info.get_uri ());
+ if (!str.has_prefix ("RESULT|") &&
+ !str.has_prefix ("ERROR|")) {
+ warning (_("Received invalid string from child: %s"), str);
- if (info.get_result () == DiscovererResult.ERROR ||
- info.get_result () == DiscovererResult.URI_INVALID) {
- this.error (file, err);
+ break;
+ }
- return;
- } else if (info.get_result () == DiscovererResult.TIMEOUT ||
- info.get_result () == DiscovererResult.BUSY ||
- info.get_result () == DiscovererResult.MISSING_PLUGINS) {
- if (info.get_result () == DiscovererResult.MISSING_PLUGINS) {
- debug ("Plugins are missing for extraction of file %s",
- file.get_uri ());
- } else {
- debug ("Extraction timed out on %s", file.get_uri ());
- }
- this.extract_basic_information (file, null, null);
+ var parts = str.split ("|");
+ if (parts.length != 4) {
+ warning (_("Received ill-formed response string %s from child…"),
+ str);
- return;
- }
+ break;
+ }
- var dlna_info = GUPnPDLNAGst.utils_information_from_discoverer_info (info);
- var dlna = this.guesser.guess_profile_from_info (dlna_info);
- this.extract_basic_information (file, info, dlna);
- }
+ if (parts[0] == "ERROR") {
+ this.error (File.new_for_uri (parts[1]),
+ new MetadataExtractorError.GENERAL (parts[3]));
- private void extract_basic_information (File file,
- DiscovererInfo? info,
- GUPnPDLNA.Profile? dlna) {
- FileInfo file_info;
+ break;
+ }
- try {
- file_info = file.query_info (FileAttribute.STANDARD_CONTENT_TYPE
- + "," +
- FileAttribute.STANDARD_SIZE + "," +
- FileAttribute.TIME_MODIFIED + "," +
- FileAttribute.STANDARD_DISPLAY_NAME,
- FileQueryInfoFlags.NONE,
- null);
+ var uri = parts[1];
+ var length = uint64.parse (parts[2]);
+
+ debug ("Found serialized data for uri %s", uri);
+ var buf = new uint8[length];
+ size_t bytes;
+ this.output_stream.read_all (buf,
+ out bytes,
+ this.child_io_cancellable);
+ debug ("Expected %" + size_t.FORMAT + " bytes, got %" +
+ size_t.FORMAT,
+ length,
+ bytes);
+
+ var v = Variant.new_from_data<void> (SERIALIZED_DATA_TYPE,
+ (uchar[]) buf,
+ true);
+ this.extraction_done (File.new_for_uri (uri), v);
+ } while (false);
+
+ this.output_stream.read_line_async.begin (Priority.DEFAULT,
+ this.child_io_cancellable,
+ this.on_input);
} catch (Error error) {
- var uri = file.get_uri ();
+ if (error is IOError.CANCELLED) {
+ debug ("Read was cancelled, process probably died…");
+ // No error signalling, this was done in the part that called
+ // cancel
+ } else {
+ warning (_("Read from child failed: %s"), error.message);
+ this.error (File.new_for_uri (this.error_uri),
+ new MetadataExtractorError.GENERAL ("Failed"));
+
+ }
+ }
+ }
- warning (_("Failed to extract basic metadata from %s: %s"),
- uri,
- error.message);
+ public void extract (File file, string content_type) {
+ if (this.child_io_cancellable.is_cancelled ()) {
+ debug ("Child apparently already died, scheduling command for later");
+ Idle.add (() => {
+ this.extract (file, content_type);
- // signal error to parent
- this.error (file, error);
+ return false;
+ });
return;
}
- this.extraction_done (file,
- info,
- dlna,
- file_info);
+ var s = "EXTRACT %s\n".printf (file.get_uri ());
+ try {
+ this.input_stream.write_all (s.data, null, this.child_io_cancellable);
+ this.input_stream.flush ();
+ debug ("Sent command to extractor process: %s", s);
+ } catch (Error error) {
+ warning (_("Failed to send command to child: %s"), error.message);
+ }
}
private void on_config_changed (Configuration config,