2 * Copyright (C) 2012 Nokia Corporation.
3 * Copyright (C) 2012 Intel Corporation.
5 * Author: Jens Georg <jensg@openismus.com>
7 * This file is part of Rygel.
9 * Rygel is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU Lesser General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
14 * Rygel is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU Lesser General Public License for more details.
19 * You should have received a copy of the GNU Lesser General Public License
20 * along with this program; if not, write to the Free Software Foundation,
21 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
24 [CCode (cname = "uuid_generate", cheader_filename = "uuid/uuid.h")]
25 internal extern static void uuid_generate ([CCode (array_length = false)]
27 [CCode (cname = "uuid_unparse", cheader_filename = "uuid/uuid.h")]
28 internal extern static void uuid_unparse ([CCode (array_length = false)]
30 [CCode (array_length = false)]
33 public const string DIDL_ITEM = """<?xml version="1.0" encoding="UTF-8"?>
35 xmlns:dc="http://purl.org/dc/elements/1.1/"
36 xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"
37 xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"
38 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
39 xsi:schemaLocation="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/
40 http://www.upnp.org/schemas/av/didl-lite-v2-20060531.xsd
41 urn:schemas-upnp-org:metadata-1-0/upnp/
42 http://www.upnp.org/schemas/av/upnp-v2-20060531.xsd">
43 <item id="" parentID="0" restricted="0">
44 <dc:title>New Song</dc:title>
45 <upnp:class>object.item.audioItem</upnp:class>
46 <res protocolInfo="*:*:*:*" />
50 public class Rygel.ServiceAction : GLib.Object {
51 public int error_code;
52 public string error_message;
54 public string elements;
56 public ServiceAction (string? container_id,
58 this.id = container_id;
59 this.elements = elements;
62 public void @return() {}
63 public void return_error (int code, string message) {
64 this.error_code = code;
65 this.error_message = message;
68 public new void @get (string arg1_name,
73 out string arg2_val) {
74 assert (arg1_name == "ContainerID");
75 assert (arg1_type == typeof (string));
78 assert (arg2_name == "Elements");
79 assert (arg2_type == typeof (string));
83 public new void @set (string arg1_name,
89 assert (arg1_name == "ObjectID");
90 assert (arg1_type == typeof (string));
92 assert (arg2_name == "Result");
93 assert (arg2_type == typeof (string));
97 public class Rygel.HTTPServer : GLib.Object {
100 public class Rygel.ItemRemovalQueue : GLib.Object {
101 public static ItemRemovalQueue get_default () {
102 return new ItemRemovalQueue ();
105 public void queue (MediaItem item, Cancellable? cancellable) {
109 public class Rygel.MediaObject : GLib.Object {
111 public string ref_id;
112 public unowned MediaContainer parent;
113 public string upnp_class;
115 public GUPnP.OCMFlags ocm_flags;
116 public Gee.ArrayList<string> uris;
118 public void add_uri (string uri) {
123 public class Rygel.MediaItem : Rygel.MediaObject {
124 public string dlna_profile;
125 public string mime_type;
127 public bool place_holder;
130 public MediaItem (string id, MediaContainer parent, string title) {
132 this.parent = parent;
136 public void serialize (GUPnP.DIDLLiteWriter writer, HTTPServer server) {
140 public class Rygel.MusicItem : Rygel.AudioItem {
141 public const string UPNP_CLASS = "object.item.audioItem.musicTrack";
143 public MusicItem (string id, MediaContainer parent, string title) {
144 base (id, parent, title);
148 public class Rygel.AudioItem : Rygel.MediaItem {
149 public const string UPNP_CLASS = "object.item.audioItem";
150 public string artist;
153 public AudioItem (string id, MediaContainer parent, string title) {
154 base (id, parent, title);
157 public class Rygel.ImageItem : Rygel.MediaItem {
158 public const string UPNP_CLASS = "object.item.imageItem";
159 public ImageItem (string id, MediaContainer parent, string title) {
160 base (id, parent, title);
164 public class Rygel.VideoItem : Rygel.MediaItem {
165 public const string UPNP_CLASS = "object.item.videoItem";
166 public VideoItem (string id, MediaContainer parent, string title) {
167 base (id, parent, title);
171 public class Rygel.PhotoItem : Rygel.MediaItem {
172 public const string UPNP_CLASS = "object.item.imageItem.photo";
173 public string creator;
175 public PhotoItem (string id, MediaContainer parent, string title) {
176 base (id, parent, title);
179 public class Rygel.ContentDirectory : GLib.Object {
180 public Cancellable cancellable;
181 public MediaContainer root_container;
182 public HTTPServer http_server;
185 public class Rygel.MediaContainer : Rygel.MediaObject {
186 public Gee.ArrayList<string> create_classes = new Gee.ArrayList<string> ();
187 public int child_count;
188 public string sort_criteria = "+dc:title";
191 public MediaObject found_object = null;
193 public async MediaObject? find_object (string id,
194 Cancellable? cancellable = null) {
195 Idle.add (() => { find_object.callback (); return false; });
201 public signal void container_updated (MediaContainer container);
204 public class Rygel.MediaObjects : Gee.ArrayList<MediaObject> {
207 public class Rygel.WritableContainer : Rygel.MediaContainer {
208 public bool can_create (string upnp_class) {
209 return this.create_classes.contains (upnp_class);
212 public async File? get_writable (Cancellable? cancellable = null) {
213 return File.new_for_commandline_arg ("/tmp");
216 public async void add_item (MediaItem item,
217 Cancellable? cancellable = null) {
221 public class Rygel.SearchableContainer : Rygel.MediaContainer {
222 public MediaObjects result = new MediaObjects ();
224 public async MediaObjects search (SearchExpression expression,
227 out int total_matches,
228 string soer_criteria,
229 Cancellable? cancellable = null) {
230 Idle.add (() => { search.callback (); return false; });
237 public errordomain Rygel.ContentDirectoryError {
245 public class Rygel.Transcoder {
248 public class Rygel.TestMediaEngine : Rygel.MediaEngine {
249 public override unowned GLib.List<DLNAProfile> get_dlna_profiles () {
253 public override unowned GLib.List<Transcoder>? get_transcoders () {
257 public override DataSource create_data_source (string uri) {
262 public class Rygel.EngineLoader {
263 public EngineLoader () { }
265 public MediaEngine load_engine () {
266 return new TestMediaEngine ();
270 public static void log_func (string? domain,
274 // Ignore critical of gee 0.6 and recent glib
275 if (message.has_prefix ("Read-only property 'read-only-view' on class")) {
276 Log.default_handler (domain, flags, message);
281 if (LogLevelFlags.LEVEL_CRITICAL in flags ||
282 LogLevelFlags.LEVEL_ERROR in flags ||
283 LogLevelFlags.FLAG_FATAL in flags) {
284 print ("======> FAILED: %s: %s\n", domain ?? "", message);
285 assert_not_reached ();
289 public class Rygel.HTTPItemCreatorTest : GLib.Object {
291 public static int main (string[] args) {
292 Log.set_default_handler (log_func);
293 var test = new HTTPItemCreatorTest ();
294 test.test_parse_args ();
295 test.test_didl_parsing ();
296 test.test_fetch_container ();
302 Error no_such_object;
303 Error restricted_parent;
307 public HTTPItemCreatorTest () {
308 this.no_such_object = new ContentDirectoryError.NO_SUCH_OBJECT("");
309 this.restricted_parent = new ContentDirectoryError.RESTRICTED_PARENT("");
310 this.bad_metadata = new ContentDirectoryError.BAD_METADATA("");
311 this.invalid_args = new ContentDirectoryError.INVALID_ARGS("");
314 private void test_parse_args () {
315 // check null container id
316 var content_directory = new ContentDirectory ();
318 var action = new ServiceAction (null, "");
319 var creator = new ItemCreator (content_directory, action);
321 assert (action.error_code == no_such_object.code);
323 // check elements containing a comment
324 action = new ServiceAction ("0", "<!-- This is an XML comment -->");
325 creator = new ItemCreator (content_directory, action);
327 assert (action.error_code == bad_metadata.code);
329 // check null elements
330 action = new ServiceAction ("0", null);
331 creator = new ItemCreator (content_directory, action);
333 assert (action.error_code == bad_metadata.code);
336 private void test_didl_parsing_step (Xml.Doc *doc, int expected_code) {
339 doc->dump_memory_enc (out xml);
340 var action = new ServiceAction ("0", xml);
341 var content_directory = new ContentDirectory ();
342 var creator = new ItemCreator (content_directory, action);
344 assert (action.error_code == expected_code);
347 private void test_didl_parsing () {
348 var xml = Xml.Parser.read_memory (DIDL_ITEM,
352 Xml.ParserOption.RECOVER |
353 Xml.ParserOption.NOBLANKS);
354 var didl_node = xml->children;
355 var item_node = didl_node->children;
356 var content_directory = new ContentDirectory ();
359 var action = new ServiceAction ("0", "");
360 var creator = new ItemCreator (content_directory, action);
362 assert (action.error_code == bad_metadata.code);
363 assert (action.error_message == "Bad metadata");
366 item_node->unlink ();
367 didl_node->set_content (" ");
368 this.test_didl_parsing_step (xml, bad_metadata.code);
370 // test item node with missing restricted attribute
371 var tmp = item_node->copy (1);
372 tmp->unset_prop ("restricted");
373 didl_node->add_child (tmp);
374 this.test_didl_parsing_step (xml, bad_metadata.code);
376 // test item node with restricted=1
377 tmp->set_prop ("restricted", "1");
378 this.test_didl_parsing_step (xml, invalid_args.code);
380 // test item node with invalid id
382 tmp = item_node->copy (1);
383 tmp->set_prop ("id", "InvalidItemId");
384 didl_node->add_child (tmp);
385 this.test_didl_parsing_step (xml, bad_metadata.code);
387 // test item node with missing id
388 tmp->unset_prop ("id");
389 this.test_didl_parsing_step (xml, bad_metadata.code);
391 // test item node with missing title
393 tmp = item_node->copy (1);
394 var title_node = tmp->children;
395 title_node->unlink ();
396 didl_node->add_child (tmp);
397 this.test_didl_parsing_step (xml, bad_metadata.code);
399 // test missing, empty or non-item upnp class
401 tmp = item_node->copy (1);
402 var class_node = tmp->children->next;
403 class_node->set_content ("object.container");
404 didl_node->add_child (tmp);
405 this.test_didl_parsing_step (xml, bad_metadata.code);
407 class_node->set_content ("");
408 this.test_didl_parsing_step (xml, bad_metadata.code);
410 class_node->unlink ();
411 this.test_didl_parsing_step (xml, bad_metadata.code);
414 private void test_fetch_container_run (ItemCreator creator) {
415 var main_loop = new MainLoop (null, false);
416 creator.run.begin ( () => { main_loop.quit (); });
420 private void test_fetch_container () {
421 // check case when object is not found
422 var content_directory = new ContentDirectory ();
423 var root_container = new SearchableContainer ();
424 content_directory.root_container = root_container;
425 var action = new ServiceAction ("0", DIDL_ITEM);
426 var creator = new ItemCreator (content_directory, action);
427 this.test_fetch_container_run (creator);
428 assert (action.error_code == no_such_object.code);
430 // check case when found object is not a container → Error 710
431 // cf. ContentDirectory:2 spec, Table 2-22
432 root_container.found_object = new MediaObject ();
433 this.test_fetch_container_run (creator);
434 assert (action.error_code == no_such_object.code);
436 // check case when found container does not have OCMUpload set
437 root_container.found_object = new MediaContainer ();
438 this.test_fetch_container_run (creator);
439 assert (action.error_code == restricted_parent.code);
441 // check case when found container is not a writable container
442 root_container.found_object.ocm_flags |= GUPnP.OCMFlags.UPLOAD;
443 this.test_fetch_container_run (creator);
444 assert (action.error_code == restricted_parent.code);
446 // check when found container does not have the correct create class
447 var container = new WritableContainer ();
448 container.create_classes.add ("object.item.imageItem.musicTrack");
449 container.ocm_flags |= GUPnP.OCMFlags.UPLOAD;
450 root_container.found_object = container;
451 this.test_fetch_container_run (creator);
452 assert (action.error_code == bad_metadata.code);
454 // check DLNA.ORG_AnyContainer when root container is not searchable
455 content_directory.root_container = new MediaContainer ();
456 action.id = "DLNA.ORG_AnyContainer";
457 this.test_fetch_container_run (creator);
458 assert (action.error_code == no_such_object.code);
460 // check DLNA.ORG_AnyContainer when no writable container is found
461 content_directory.root_container = new SearchableContainer ();
462 this.test_fetch_container_run (creator);
463 // We cannot distinguish this case from the "create-class doesn't match"
465 assert (action.error_code == bad_metadata.code);