diff options
-rw-r--r-- | configure.ac | 1 | ||||
-rw-r--r-- | src/plugins/media-export/Makefile.am | 31 | ||||
-rw-r--r-- | src/plugins/media-export/rygel-media-export-extract.vala | 238 | ||||
-rw-r--r-- | src/plugins/media-export/rygel-media-export-harvester.vala | 1 | ||||
-rw-r--r-- | src/plugins/media-export/rygel-media-export-harvesting-task.vala | 39 | ||||
-rw-r--r-- | src/plugins/media-export/rygel-media-export-info-serializer.vala | 215 | ||||
-rw-r--r-- | src/plugins/media-export/rygel-media-export-item-factory.vala | 387 | ||||
-rw-r--r-- | src/plugins/media-export/rygel-media-export-metadata-extractor.vala | 290 |
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, |