fix filename self-references
[vlc.git] / modules / services_discovery / upnp.cpp
1 /*****************************************************************************
2  * upnp.cpp :  UPnP discovery module (libupnp)
3  *****************************************************************************
4  * Copyright (C) 2004-2011 the VideoLAN team
5  * $Id$
6  *
7  * Authors: RĂ©mi Denis-Courmont <rem # videolan.org> (original plugin)
8  *          Christian Henz <henz # c-lab.de>
9  *          Mirsal Ennaime <mirsal dot ennaime at gmail dot com>
10  *
11  * UPnP Plugin using the Intel SDK (libupnp) instead of CyberLink
12  *
13  * This program is free software; you can redistribute it and/or modify
14  * it under the terms of the GNU General Public License as published by
15  * the Free Software Foundation; either version 2 of the License, or
16  * (at your option) any later version.
17  *
18  * This program is distributed in the hope that it will be useful,
19  * but WITHOUT ANY WARRANTY; without even the implied warranty of
20  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21  * GNU General Public License for more details.
22  *
23  * You should have received a copy of the GNU General Public License
24  * along with this program; if not, write to the Free Software
25  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
26  *****************************************************************************/
27
28 #define __STDC_CONSTANT_MACROS 1
29
30 #undef PACKAGE_NAME
31 #ifdef HAVE_CONFIG_H
32 # include "config.h"
33 #endif
34
35 #include "upnp.hpp"
36
37 #include <vlc_plugin.h>
38 #include <vlc_services_discovery.h>
39
40 #include <assert.h>
41
42 /*
43  * Constants
44 */
45 const char* MEDIA_SERVER_DEVICE_TYPE = "urn:schemas-upnp-org:device:MediaServer:1";
46 const char* CONTENT_DIRECTORY_SERVICE_TYPE = "urn:schemas-upnp-org:service:ContentDirectory:1";
47
48 /*
49  * VLC handle
50  */
51 struct services_discovery_sys_t
52 {
53     UpnpClient_Handle client_handle;
54     MediaServerList* p_server_list;
55     vlc_mutex_t callback_lock;
56 };
57
58 /*
59  * VLC callback prototypes
60  */
61 static int Open( vlc_object_t* );
62 static void Close( vlc_object_t* );
63 VLC_SD_PROBE_HELPER( "upnp", "Universal Plug'n'Play", SD_CAT_LAN )
64
65 /*
66  * Module descriptor
67  */
68 vlc_module_begin();
69     set_shortname( "UPnP" );
70     set_description( N_( "Universal Plug'n'Play" ) );
71     set_category( CAT_PLAYLIST );
72     set_subcategory( SUBCAT_PLAYLIST_SD );
73     set_capability( "services_discovery", 0 );
74     set_callbacks( Open, Close );
75
76     VLC_SD_PROBE_SUBMODULE
77 vlc_module_end();
78
79 /*
80  * Local prototypes
81  */
82 static int Callback( Upnp_EventType event_type, void* p_event, void* p_user_data );
83
84 const char* xml_getChildElementValue( IXML_Element* p_parent,
85                                       const char*   psz_tag_name );
86
87 const char* xml_getChildElementAttributeValue( IXML_Element* p_parent,
88                                         const char* psz_tag_name_,
89                                         const char* psz_attribute_ );
90
91 IXML_Document* parseBrowseResult( IXML_Document* p_doc );
92
93 /*
94  * Initializes UPNP instance.
95  */
96 static int Open( vlc_object_t *p_this )
97 {
98     int i_res;
99     services_discovery_t *p_sd = ( services_discovery_t* )p_this;
100     services_discovery_sys_t *p_sys  = ( services_discovery_sys_t * )
101             calloc( 1, sizeof( services_discovery_sys_t ) );
102
103     if( !( p_sd->p_sys = p_sys ) )
104         return VLC_ENOMEM;
105
106     /* Initialize on first IPv4-capable adapter and first open port
107      * TODO: use UpnpInit2() to utilize IPv6.
108      */
109     i_res = UpnpInit( 0, 0 );
110     if( i_res != UPNP_E_SUCCESS )
111     {
112         msg_Err( p_sd, "Initialization failed: %s", UpnpGetErrorMessage( i_res ) );
113         free( p_sys );
114         return VLC_EGENERIC;
115     }
116
117     p_sys->p_server_list = new MediaServerList( p_sd );
118     vlc_mutex_init( &p_sys->callback_lock );
119
120     /* Register a control point */
121     i_res = UpnpRegisterClient( Callback, p_sd, &p_sys->client_handle );
122     if( i_res != UPNP_E_SUCCESS )
123     {
124         msg_Err( p_sd, "Client registration failed: %s", UpnpGetErrorMessage( i_res ) );
125         Close( (vlc_object_t*) p_sd );
126         return VLC_EGENERIC;
127     }
128
129     /* Search for media servers */
130     i_res = UpnpSearchAsync( p_sys->client_handle, 5,
131             MEDIA_SERVER_DEVICE_TYPE, p_sd );
132     if( i_res != UPNP_E_SUCCESS )
133     {
134         msg_Err( p_sd, "Error sending search request: %s", UpnpGetErrorMessage( i_res ) );
135         Close( (vlc_object_t*) p_sd );
136         return VLC_EGENERIC;
137     }
138
139     i_res = UpnpSetMaxContentLength( 0 );
140     if( i_res != UPNP_E_SUCCESS )
141     {
142         msg_Err( p_sd, "Failed to set maximum content length: %s", UpnpGetErrorMessage( i_res ) );
143         Close( (vlc_object_t*) p_sd );
144         return VLC_EGENERIC;
145     }
146
147     return VLC_SUCCESS;
148 }
149
150 /*
151  * Releases resources.
152  */
153 static void Close( vlc_object_t *p_this )
154 {
155     services_discovery_t *p_sd = ( services_discovery_t* )p_this;
156
157     UpnpUnRegisterClient( p_sd->p_sys->client_handle );
158     UpnpFinish();
159
160     delete p_sd->p_sys->p_server_list;
161     vlc_mutex_destroy( &p_sd->p_sys->callback_lock );
162
163     free( p_sd->p_sys );
164 }
165
166 /* XML utility functions */
167
168 /*
169  * Returns the value of a child element, or NULL on error
170  */
171 const char* xml_getChildElementValue( IXML_Element* p_parent,
172                                       const char*   psz_tag_name_ )
173 {
174     if ( !p_parent ) return NULL;
175     if ( !psz_tag_name_ ) return NULL;
176
177     IXML_NodeList* p_node_list = ixmlElement_getElementsByTagName( p_parent, psz_tag_name_ );
178     if ( !p_node_list ) return NULL;
179
180     IXML_Node* p_element = ixmlNodeList_item( p_node_list, 0 );
181     ixmlNodeList_free( p_node_list );
182     if ( !p_element ) return NULL;
183
184     IXML_Node* p_text_node = ixmlNode_getFirstChild( p_element );
185     if ( !p_text_node ) return NULL;
186
187     return ixmlNode_getNodeValue( p_text_node );
188 }
189
190 /*
191  * Returns the value of a child element's attribute, or NULL on error
192  */
193 const char* xml_getChildElementAttributeValue( IXML_Element* p_parent,
194                                         const char* psz_tag_name_,
195                                         const char* psz_attribute_ )
196 {
197     if ( !p_parent ) return NULL;
198     if ( !psz_tag_name_ ) return NULL;
199     if ( !psz_attribute_ ) return NULL;
200
201     IXML_NodeList* p_node_list = ixmlElement_getElementsByTagName( p_parent, psz_tag_name_ );
202     if ( !p_node_list ) return NULL;
203
204     IXML_Node* p_element = ixmlNodeList_item( p_node_list, 0 );
205     ixmlNodeList_free( p_node_list );
206     if ( !p_element ) return NULL;
207
208     return ixmlElement_getAttribute( (IXML_Element*) p_element, psz_attribute_ );
209 }
210
211 /*
212  * Extracts the result document from a SOAP response
213  */
214 IXML_Document* parseBrowseResult( IXML_Document* p_doc )
215 {
216     ixmlRelaxParser( 1 );
217
218     if ( !p_doc ) return 0;
219
220     IXML_NodeList* p_result_list = ixmlDocument_getElementsByTagName( p_doc,
221                                                                    "Result" );
222
223     if ( !p_result_list ) return 0;
224
225     IXML_Node* p_result_node = ixmlNodeList_item( p_result_list, 0 );
226
227     ixmlNodeList_free( p_result_list );
228
229     if ( !p_result_node ) return 0;
230
231     IXML_Node* p_text_node = ixmlNode_getFirstChild( p_result_node );
232     if ( !p_text_node ) return 0;
233
234     const char* psz_result_string = ixmlNode_getNodeValue( p_text_node );
235
236     IXML_Document* p_browse_doc = ixmlParseBuffer( psz_result_string );
237
238     return p_browse_doc;
239 }
240
241
242 /*
243  * Handles all UPnP events
244  */
245 static int Callback( Upnp_EventType event_type, void* p_event, void* p_user_data )
246 {
247     services_discovery_t* p_sd = ( services_discovery_t* ) p_user_data;
248     services_discovery_sys_t* p_sys = p_sd->p_sys;
249     vlc_mutex_locker locker( &p_sys->callback_lock );
250
251     switch( event_type )
252     {
253     case UPNP_DISCOVERY_ADVERTISEMENT_ALIVE:
254     case UPNP_DISCOVERY_SEARCH_RESULT:
255     {
256         struct Upnp_Discovery* p_discovery = ( struct Upnp_Discovery* )p_event;
257
258         IXML_Document *p_description_doc = 0;
259
260         int i_res;
261         i_res = UpnpDownloadXmlDoc( p_discovery->Location, &p_description_doc );
262         if ( i_res != UPNP_E_SUCCESS )
263         {
264             msg_Warn( p_sd, "Could not download device description! "
265                             "Fetching data from %s failed: %s",
266                             p_discovery->Location, UpnpGetErrorMessage( i_res ) );
267             return i_res;
268         }
269
270         MediaServer::parseDeviceDescription( p_description_doc,
271                 p_discovery->Location, p_sd );
272
273         ixmlDocument_free( p_description_doc );
274     }
275     break;
276
277     case UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE:
278     {
279         struct Upnp_Discovery* p_discovery = ( struct Upnp_Discovery* )p_event;
280
281         p_sys->p_server_list->removeServer( p_discovery->DeviceId );
282
283     }
284     break;
285
286     case UPNP_EVENT_RECEIVED:
287     {
288         Upnp_Event* p_e = ( Upnp_Event* )p_event;
289
290         MediaServer* p_server = p_sys->p_server_list->getServerBySID( p_e->Sid );
291         if ( p_server ) p_server->fetchContents();
292     }
293     break;
294
295     case UPNP_EVENT_AUTORENEWAL_FAILED:
296     case UPNP_EVENT_SUBSCRIPTION_EXPIRED:
297     {
298         /* Re-subscribe. */
299
300         Upnp_Event_Subscribe* p_s = ( Upnp_Event_Subscribe* )p_event;
301
302         MediaServer* p_server = p_sys->p_server_list->getServerBySID( p_s->Sid );
303         if ( p_server ) p_server->subscribeToContentDirectory();
304     }
305     break;
306
307     case UPNP_EVENT_SUBSCRIBE_COMPLETE:
308         msg_Warn( p_sd, "subscription complete" );
309         break;
310
311     case UPNP_DISCOVERY_SEARCH_TIMEOUT:
312         msg_Warn( p_sd, "search timeout" );
313         break;
314
315     default:
316         msg_Err( p_sd, "Unhandled event, please report ( type=%d )", event_type );
317         break;
318     }
319
320     return UPNP_E_SUCCESS;
321 }
322
323
324 /*
325  * Local class implementations.
326  */
327
328 /*
329  * MediaServer
330  */
331
332 void MediaServer::parseDeviceDescription( IXML_Document* p_doc,
333                                           const char*    p_location,
334                                           services_discovery_t* p_sd )
335 {
336     if ( !p_doc )
337     {
338         msg_Err( p_sd, "Null IXML_Document" );
339         return;
340     }
341
342     if ( !p_location )
343     {
344         msg_Err( p_sd, "Null location" );
345         return;
346     }
347
348     const char* psz_base_url = p_location;
349
350     /* Try to extract baseURL */
351     IXML_NodeList* p_url_list = ixmlDocument_getElementsByTagName( p_doc, "baseURL" );
352     if ( p_url_list )
353     {
354
355         if ( IXML_Node* p_url_node = ixmlNodeList_item( p_url_list, 0 ) )
356         {
357             IXML_Node* p_text_node = ixmlNode_getFirstChild( p_url_node );
358             if ( p_text_node ) psz_base_url = ixmlNode_getNodeValue( p_text_node );
359         }
360
361         ixmlNodeList_free( p_url_list );
362     }
363
364     /* Get devices */
365     IXML_NodeList* p_device_list =
366                 ixmlDocument_getElementsByTagName( p_doc, "device" );
367
368     if ( p_device_list )
369     {
370         for ( unsigned int i = 0; i < ixmlNodeList_length( p_device_list ); i++ )
371         {
372             IXML_Element* p_device_element =
373                    ( IXML_Element* ) ixmlNodeList_item( p_device_list, i );
374
375             const char* psz_device_type = xml_getChildElementValue( p_device_element,
376                                                                "deviceType" );
377             if ( !psz_device_type )
378             {
379                 msg_Warn( p_sd, "No deviceType found!" );
380                 continue;
381             }
382
383             if ( strcmp( MEDIA_SERVER_DEVICE_TYPE, psz_device_type ) != 0 )
384                 continue;
385
386             const char* psz_udn = xml_getChildElementValue( p_device_element, "UDN" );
387             if ( !psz_udn )
388             {
389                 msg_Warn( p_sd, "No UDN!" );
390                 continue;
391             }
392
393             /* Check if server is already added */
394             if ( p_sd->p_sys->p_server_list->getServer( psz_udn ) != 0 )
395             {
396                 msg_Warn( p_sd, "Server with uuid '%s' already exists.", psz_udn );
397                 continue;
398             }
399
400             const char* psz_friendly_name =
401                        xml_getChildElementValue( p_device_element,
402                                                  "friendlyName" );
403
404             if ( !psz_friendly_name )
405             {
406                 msg_Dbg( p_sd, "No friendlyName!" );
407                 continue;
408             }
409
410             MediaServer* p_server = new MediaServer( psz_udn, psz_friendly_name, p_sd );
411
412             if ( !p_sd->p_sys->p_server_list->addServer( p_server ) )
413             {
414                 delete p_server;
415                 p_server = 0;
416                 continue;
417             }
418
419             /* Check for ContentDirectory service. */
420             IXML_NodeList* p_service_list =
421                        ixmlElement_getElementsByTagName( p_device_element,
422                                                          "service" );
423             if ( p_service_list )
424             {
425                 for ( unsigned int j = 0;
426                       j < ixmlNodeList_length( p_service_list ); j++ )
427                 {
428                     IXML_Element* p_service_element =
429                         ( IXML_Element* ) ixmlNodeList_item( p_service_list, j );
430
431                     const char* psz_service_type =
432                         xml_getChildElementValue( p_service_element,
433                                                   "serviceType" );
434                     if ( !psz_service_type )
435                     {
436                         msg_Warn( p_sd, "No service type found." );
437                         continue;
438                     }
439
440                     if ( strcmp( CONTENT_DIRECTORY_SERVICE_TYPE,
441                                 psz_service_type ) != 0 )
442                         continue;
443
444                     const char* psz_event_sub_url =
445                         xml_getChildElementValue( p_service_element,
446                                                   "eventSubURL" );
447                     if ( !psz_event_sub_url )
448                     {
449                         msg_Warn( p_sd, "No event subscription url found." );
450                         continue;
451                     }
452
453                     const char* psz_control_url =
454                         xml_getChildElementValue( p_service_element,
455                                                   "controlURL" );
456                     if ( !psz_control_url )
457                     {
458                         msg_Warn( p_sd, "No control url found." );
459                         continue;
460                     }
461
462                     /* Try to subscribe to ContentDirectory service */
463
464                     char* psz_url = ( char* ) malloc( strlen( psz_base_url ) +
465                             strlen( psz_event_sub_url ) + 1 );
466                     if ( psz_url )
467                     {
468                         if ( UpnpResolveURL( psz_base_url, psz_event_sub_url, psz_url ) ==
469                                 UPNP_E_SUCCESS )
470                         {
471                             p_server->setContentDirectoryEventURL( psz_url );
472                             p_server->subscribeToContentDirectory();
473                         }
474
475                         free( psz_url );
476                     }
477
478                     /* Try to browse content directory. */
479
480                     psz_url = ( char* ) malloc( strlen( psz_base_url ) +
481                             strlen( psz_control_url ) + 1 );
482                     if ( psz_url )
483                     {
484                         if ( UpnpResolveURL( psz_base_url, psz_control_url, psz_url ) ==
485                                 UPNP_E_SUCCESS )
486                         {
487                             p_server->setContentDirectoryControlURL( psz_url );
488                             p_server->fetchContents();
489                         }
490
491                         free( psz_url );
492                     }
493                }
494                ixmlNodeList_free( p_service_list );
495            }
496        }
497        ixmlNodeList_free( p_device_list );
498     }
499 }
500
501 MediaServer::MediaServer( const char* psz_udn,
502                           const char* psz_friendly_name,
503                           services_discovery_t* p_sd )
504 {
505     _p_sd = p_sd;
506
507     _UDN = psz_udn;
508     _friendly_name = psz_friendly_name;
509
510     _p_contents = NULL;
511     _p_input_item = NULL;
512 }
513
514 MediaServer::~MediaServer()
515 {
516     delete _p_contents;
517 }
518
519 const char* MediaServer::getUDN() const
520 {
521     return _UDN.c_str();
522 }
523
524 const char* MediaServer::getFriendlyName() const
525 {
526     return _friendly_name.c_str();
527 }
528
529 void MediaServer::setContentDirectoryEventURL( const char* psz_url )
530 {
531     _content_directory_event_url = psz_url;
532 }
533
534 const char* MediaServer::getContentDirectoryEventURL() const
535 {
536     return _content_directory_event_url.c_str();
537 }
538
539 void MediaServer::setContentDirectoryControlURL( const char* psz_url )
540 {
541     _content_directory_control_url = psz_url;
542 }
543
544 const char* MediaServer::getContentDirectoryControlURL() const
545 {
546     return _content_directory_control_url.c_str();
547 }
548
549 /**
550  * Subscribes current client handle to Content Directory Service.
551  * CDS exports the server shares to clients.
552  */
553 void MediaServer::subscribeToContentDirectory()
554 {
555     const char* psz_url = getContentDirectoryEventURL();
556     if ( !psz_url )
557     {
558         msg_Dbg( _p_sd, "No subscription url set!" );
559         return;
560     }
561
562     int i_timeout = 1810;
563     Upnp_SID sid;
564
565     int i_res = UpnpSubscribe( _p_sd->p_sys->client_handle, psz_url, &i_timeout, sid );
566
567     if ( i_res == UPNP_E_SUCCESS )
568     {
569         _i_subscription_timeout = i_timeout;
570         memcpy( _subscription_id, sid, sizeof( Upnp_SID ) );
571     }
572     else
573     {
574         msg_Dbg( _p_sd, "Subscribe failed: '%s': %s",
575                 getFriendlyName(), UpnpGetErrorMessage( i_res ) );
576     }
577 }
578 /*
579  * Constructs UpnpAction to browse available content.
580  */
581 IXML_Document* MediaServer::_browseAction( const char* psz_object_id_,
582                                            const char* psz_browser_flag_,
583                                            const char* psz_filter_,
584                                            const char* psz_starting_index_,
585                                            const char* psz_requested_count_,
586                                            const char* psz_sort_criteria_ )
587 {
588     IXML_Document* p_action = 0;
589     IXML_Document* p_response = 0;
590     const char* psz_url = getContentDirectoryControlURL();
591
592     if ( !psz_url )
593     {
594         msg_Dbg( _p_sd, "No subscription url set!" );
595         return 0;
596     }
597
598     char* psz_service_type = strdup( CONTENT_DIRECTORY_SERVICE_TYPE );
599
600     int i_res;
601
602     i_res = UpnpAddToAction( &p_action, "Browse",
603             psz_service_type, "ObjectID", psz_object_id_ );
604
605     if ( i_res != UPNP_E_SUCCESS )
606     {
607         msg_Dbg( _p_sd, "AddToAction 'ObjectID' failed: %s",
608                 UpnpGetErrorMessage( i_res ) );
609         goto browseActionCleanup;
610     }
611
612     i_res = UpnpAddToAction( &p_action, "Browse",
613             psz_service_type, "BrowseFlag", psz_browser_flag_ );
614
615     if ( i_res != UPNP_E_SUCCESS )
616     {
617         msg_Dbg( _p_sd, "AddToAction 'BrowseFlag' failed: %s", 
618                 UpnpGetErrorMessage( i_res ) );
619         goto browseActionCleanup;
620     }
621
622     i_res = UpnpAddToAction( &p_action, "Browse",
623             psz_service_type, "Filter", psz_filter_ );
624
625     if ( i_res != UPNP_E_SUCCESS )
626     {
627         msg_Dbg( _p_sd, "AddToAction 'Filter' failed: %s",
628                 UpnpGetErrorMessage( i_res ) );
629         goto browseActionCleanup;
630     }
631
632     i_res = UpnpAddToAction( &p_action, "Browse",
633             psz_service_type, "StartingIndex", psz_starting_index_ );
634
635     if ( i_res != UPNP_E_SUCCESS )
636     {
637         msg_Dbg( _p_sd, "AddToAction 'StartingIndex' failed: %s",
638                 UpnpGetErrorMessage( i_res ) );
639         goto browseActionCleanup;
640     }
641
642     i_res = UpnpAddToAction( &p_action, "Browse",
643             psz_service_type, "RequestedCount", psz_requested_count_ );
644
645     if ( i_res != UPNP_E_SUCCESS )
646     {
647         msg_Dbg( _p_sd, "AddToAction 'RequestedCount' failed: %s",
648                 UpnpGetErrorMessage( i_res ) );
649         goto browseActionCleanup;
650     }
651
652     i_res = UpnpAddToAction( &p_action, "Browse",
653             psz_service_type, "SortCriteria", psz_sort_criteria_ );
654
655     if ( i_res != UPNP_E_SUCCESS )
656     {
657         msg_Dbg( _p_sd, "AddToAction 'SortCriteria' failed: %s",
658                 UpnpGetErrorMessage( i_res ) );
659         goto browseActionCleanup;
660     }
661
662     i_res = UpnpSendAction( _p_sd->p_sys->client_handle,
663               psz_url,
664               psz_service_type,
665               0, /* ignored in SDK, must be NULL */
666               p_action,
667               &p_response );
668
669     if ( i_res != UPNP_E_SUCCESS )
670     {
671         msg_Err( _p_sd, "%s when trying the send() action with URL: %s",
672                 UpnpGetErrorMessage( i_res ), psz_url );
673
674         ixmlDocument_free( p_response );
675         p_response = 0;
676     }
677
678 browseActionCleanup:
679
680     free( psz_service_type );
681
682     ixmlDocument_free( p_action );
683     return p_response;
684 }
685
686 void MediaServer::fetchContents()
687 {
688     /* Delete previous contents to prevent duplicate entries */
689     if ( _p_contents )
690     {
691         delete _p_contents;
692         services_discovery_RemoveItem( _p_sd, _p_input_item );
693         services_discovery_AddItem( _p_sd, _p_input_item, NULL );
694     }
695
696     Container* root = new Container( 0, "0", getFriendlyName() );
697
698     _fetchContents( root );
699
700     _p_contents = root;
701     _p_contents->setInputItem( _p_input_item );
702
703     _buildPlaylist( _p_contents, NULL );
704 }
705
706 /*
707  * Fetches and parses the UPNP response
708  */
709 bool MediaServer::_fetchContents( Container* p_parent )
710 {
711     if (!p_parent)
712     {
713         msg_Err( _p_sd, "No parent" );
714         return false;
715     }
716
717     IXML_Document* p_response = _browseAction( p_parent->getObjectID(),
718                                       "BrowseDirectChildren",
719                                       "*", "0", "0", "" );
720     if ( !p_response )
721     {
722         msg_Err( _p_sd, "No response from browse() action" );
723         return false;
724     }
725
726     IXML_Document* p_result = parseBrowseResult( p_response );
727     ixmlDocument_free( p_response );
728
729     if ( !p_result )
730     {
731         msg_Err( _p_sd, "browse() response parsing failed" );
732         return false;
733     }
734 #ifndef NDEBUG
735     else
736     {
737         msg_Dbg( _p_sd, "Got DIDL document: %s",
738                 ixmlPrintDocument( p_result ) );
739     }
740 #endif
741
742     IXML_NodeList* containerNodeList =
743                 ixmlDocument_getElementsByTagName( p_result, "container" );
744
745     if ( containerNodeList )
746     {
747         for ( unsigned int i = 0;
748                 i < ixmlNodeList_length( containerNodeList ); i++ )
749         {
750             IXML_Element* containerElement =
751                   ( IXML_Element* )ixmlNodeList_item( containerNodeList, i );
752
753             const char* objectID = ixmlElement_getAttribute( containerElement,
754                                                              "id" );
755             if ( !objectID )
756                 continue;
757
758             const char* title = xml_getChildElementValue( containerElement,
759                                                           "dc:title" );
760
761             if ( !title )
762                 continue;
763
764             Container* container = new Container( p_parent, objectID, title );
765             p_parent->addContainer( container );
766             _fetchContents( container );
767         }
768         ixmlNodeList_free( containerNodeList );
769     }
770
771     IXML_NodeList* itemNodeList = ixmlDocument_getElementsByTagName( p_result,
772                                                                      "item" );
773     if ( itemNodeList )
774     {
775         for ( unsigned int i = 0; i < ixmlNodeList_length( itemNodeList ); i++ )
776         {
777             IXML_Element* itemElement =
778                         ( IXML_Element* )ixmlNodeList_item( itemNodeList, i );
779
780             const char* objectID =
781                         ixmlElement_getAttribute( itemElement, "id" );
782
783             if ( !objectID )
784                 continue;
785
786             const char* title =
787                         xml_getChildElementValue( itemElement, "dc:title" );
788
789             if ( !title )
790                 continue;
791
792             const char* resource =
793                         xml_getChildElementValue( itemElement, "res" );
794
795             if ( !resource )
796                 continue;
797
798             const char* psz_duration = xml_getChildElementAttributeValue( itemElement,
799                                                                     "res",
800                                                                     "duration" );
801
802             mtime_t i_duration = -1;
803             int i_hours, i_minutes, i_seconds, i_decis;
804
805             if ( psz_duration )
806             {
807                 if( sscanf( psz_duration, "%02d:%02d:%02d.%d",
808                         &i_hours, &i_minutes, &i_seconds, &i_decis ))
809                     i_duration = INT64_C(1000000) * ( i_hours*3600 +
810                                                       i_minutes*60 +
811                                                       i_seconds ) +
812                                  INT64_C(100000) * i_decis;
813             }
814
815             Item* item = new Item( p_parent, objectID, title, resource, i_duration );
816             p_parent->addItem( item );
817         }
818         ixmlNodeList_free( itemNodeList );
819     }
820
821     ixmlDocument_free( p_result );
822     return true;
823 }
824
825 // TODO: Create a permanent fix for the item duplication bug. The current fix
826 // is essentially only a small hack. Although it fixes the problem, it introduces
827 // annoying cosmetic issues with the playlist. For example, when the UPnP Server
828 // rebroadcasts it's directory structure, the VLC Client deletes the old directory
829 // structure, causing the user to go back to the root node of the directory. The
830 // directory is then rebuilt, and the user is forced to traverse through the directory
831 // to find the item they were looking for. Some servers may not push the directory
832 // structure too often, but we cannot rely on this fix.
833 //
834 // I have thought up another fix, but this would require certain features to
835 // be present within the VLC services discovery. Currently, services_discovery_AddItem
836 // does not allow the programmer to nest items. It only allows a "2 deep" scope.
837 // An example of the limitation is below:
838 //
839 // Root Directory
840 // + Item 1
841 // + Item 2
842 //
843 // services_discovery_AddItem will not let the programmer specify a child-node to
844 // insert items into, so we would not be able to do the following:
845 //
846 // Root Directory
847 // + Item 1
848 //   + Sub Item 1
849 // + Item 2
850 //   + Sub Item 1 of Item 2
851 //     + Sub-Sub Item 1 of Sub Item 1
852 //
853 // This creates a HUGE limitation on what we are able to do. If we were able to do
854 // the above, we could simply preserve the old directory listing, and compare what items
855 // do not exist in the new directory listing, then remove them from the shown listing using
856 // services_discovery_RemoveItem. If new files were introduced within an already existing
857 // container, we could simply do so with services_discovery_AddItem.
858
859 /*
860  * Builds playlist based on available input items.
861  */
862 void MediaServer::_buildPlaylist( Container* p_parent, input_item_node_t *p_input_node )
863 {
864     bool b_send = p_input_node == NULL;
865     if( b_send )
866         p_input_node = input_item_node_Create( p_parent->getInputItem() );
867
868     for ( unsigned int i = 0; i < p_parent->getNumContainers(); i++ )
869     {
870         Container* p_container = p_parent->getContainer( i );
871
872         input_item_t* p_input_item = input_item_New( "vlc://nop",
873                                                     p_container->getTitle() );
874         input_item_node_t *p_new_node =
875             input_item_node_AppendItem( p_input_node, p_input_item );
876
877         p_container->setInputItem( p_input_item );
878         _buildPlaylist( p_container, p_new_node );
879     }
880
881     for ( unsigned int i = 0; i < p_parent->getNumItems(); i++ )
882     {
883         Item* p_item = p_parent->getItem( i );
884
885         input_item_t* p_input_item = input_item_NewExt( p_item->getResource(),
886                                                p_item->getTitle(),
887                                                0,
888                                                NULL,
889                                                0,
890                                                p_item->getDuration() );
891
892         assert( p_input_item );
893         input_item_node_AppendItem( p_input_node, p_input_item );
894         p_item->setInputItem( p_input_item );
895     }
896
897     if( b_send )
898         input_item_node_PostAndDelete( p_input_node );
899 }
900
901 void MediaServer::setInputItem( input_item_t* p_input_item )
902 {
903     if( _p_input_item == p_input_item )
904         return;
905
906     if( _p_input_item )
907         vlc_gc_decref( _p_input_item );
908
909     vlc_gc_incref( p_input_item );
910     _p_input_item = p_input_item;
911 }
912
913 input_item_t* MediaServer::getInputItem() const
914 {
915     return _p_input_item;
916 }
917
918 bool MediaServer::compareSID( const char* psz_sid )
919 {
920     return ( strncmp( _subscription_id, psz_sid, sizeof( Upnp_SID ) ) == 0 );
921 }
922
923
924 /*
925  * MediaServerList class
926  */
927 MediaServerList::MediaServerList( services_discovery_t* p_sd )
928 {
929     _p_sd = p_sd;
930 }
931
932 MediaServerList::~MediaServerList()
933 {
934     for ( unsigned int i = 0; i < _list.size(); i++ )
935     {
936         delete _list[i];
937     }
938 }
939
940 bool MediaServerList::addServer( MediaServer* p_server )
941 {
942     input_item_t* p_input_item = NULL;
943     if ( getServer( p_server->getUDN() ) != 0 ) return false;
944
945     msg_Dbg( _p_sd, "Adding server '%s' with uuid '%s'", p_server->getFriendlyName(), p_server->getUDN() );
946
947     p_input_item = input_item_New( "vlc://nop", p_server->getFriendlyName() );
948
949     input_item_SetDescription( p_input_item, p_server->getUDN() );
950
951     p_server->setInputItem( p_input_item );
952
953     services_discovery_AddItem( _p_sd, p_input_item, NULL );
954
955     _list.push_back( p_server );
956
957     return true;
958 }
959
960 MediaServer* MediaServerList::getServer( const char* psz_udn )
961 {
962     MediaServer* p_result = 0;
963
964     for ( unsigned int i = 0; i < _list.size(); i++ )
965     {
966         if( strcmp( psz_udn, _list[i]->getUDN() ) == 0 )
967         {
968             p_result = _list[i];
969             break;
970         }
971     }
972
973     return p_result;
974 }
975
976 MediaServer* MediaServerList::getServerBySID( const char* psz_sid )
977 {
978     MediaServer* p_server = 0;
979
980     for ( unsigned int i = 0; i < _list.size(); i++ )
981     {
982         if ( _list[i]->compareSID( psz_sid ) )
983         {
984             p_server = _list[i];
985             break;
986         }
987     }
988
989     return p_server;
990 }
991
992 void MediaServerList::removeServer( const char* psz_udn )
993 {
994     MediaServer* p_server = getServer( psz_udn );
995     if ( !p_server ) return;
996
997     msg_Dbg( _p_sd, "Removing server '%s'", p_server->getFriendlyName() );
998
999     services_discovery_RemoveItem( _p_sd, p_server->getInputItem() );
1000
1001     std::vector<MediaServer*>::iterator it;
1002     for ( it = _list.begin(); it != _list.end(); ++it )
1003     {
1004         if ( *it == p_server )
1005         {
1006             _list.erase( it );
1007             delete p_server;
1008             break;
1009         }
1010     }
1011 }
1012
1013
1014 /*
1015  * Item class
1016  */
1017 Item::Item( Container* p_parent, const char* psz_object_id, const char* psz_title,
1018            const char* psz_resource, mtime_t i_duration )
1019 {
1020     _parent = p_parent;
1021
1022     _objectID = psz_object_id;
1023     _title = psz_title;
1024     _resource = psz_resource;
1025     _duration = i_duration;
1026
1027     _p_input_item = NULL;
1028 }
1029
1030 Item::~Item()
1031 {
1032     if( _p_input_item )
1033         vlc_gc_decref( _p_input_item );
1034 }
1035
1036 const char* Item::getObjectID() const
1037 {
1038     return _objectID.c_str();
1039 }
1040
1041 const char* Item::getTitle() const
1042 {
1043     return _title.c_str();
1044 }
1045
1046 const char* Item::getResource() const
1047 {
1048     return _resource.c_str();
1049 }
1050
1051 mtime_t Item::getDuration() const
1052 {
1053     return _duration;
1054 }
1055
1056 void Item::setInputItem( input_item_t* p_input_item )
1057 {
1058     if( _p_input_item == p_input_item )
1059         return;
1060
1061     if( _p_input_item )
1062         vlc_gc_decref( _p_input_item );
1063
1064     vlc_gc_incref( p_input_item );
1065     _p_input_item = p_input_item;
1066 }
1067
1068 /*
1069  * Container class
1070  */
1071 Container::Container( Container*  p_parent,
1072                       const char* psz_object_id,
1073                       const char* psz_title )
1074 {
1075     _parent = p_parent;
1076
1077     _objectID = psz_object_id;
1078     _title = psz_title;
1079
1080     _p_input_item = NULL;
1081 }
1082
1083 Container::~Container()
1084 {
1085     for ( unsigned int i = 0; i < _containers.size(); i++ )
1086     {
1087         delete _containers[i];
1088     }
1089
1090     for ( unsigned int i = 0; i < _items.size(); i++ )
1091     {
1092         delete _items[i];
1093     }
1094
1095     if( _p_input_item )
1096         vlc_gc_decref( _p_input_item );
1097 }
1098
1099 void Container::addItem( Item* item )
1100 {
1101     _items.push_back( item );
1102 }
1103
1104 void Container::addContainer( Container* p_container )
1105 {
1106     _containers.push_back( p_container );
1107 }
1108
1109 const char* Container::getObjectID() const
1110 {
1111     return _objectID.c_str();
1112 }
1113
1114 const char* Container::getTitle() const
1115 {
1116     return _title.c_str();
1117 }
1118
1119 unsigned int Container::getNumItems() const
1120 {
1121     return _items.size();
1122 }
1123
1124 unsigned int Container::getNumContainers() const
1125 {
1126     return _containers.size();
1127 }
1128
1129 Item* Container::getItem( unsigned int i_index ) const
1130 {
1131     if ( i_index < _items.size() ) return _items[i_index];
1132     return 0;
1133 }
1134
1135 Container* Container::getContainer( unsigned int i_index ) const
1136 {
1137     if ( i_index < _containers.size() ) return _containers[i_index];
1138     return 0;
1139 }
1140
1141 Container* Container::getParent()
1142 {
1143     return _parent;
1144 }
1145
1146 void Container::setInputItem( input_item_t* p_input_item )
1147 {
1148     if( _p_input_item == p_input_item )
1149         return;
1150
1151     if( _p_input_item )
1152         vlc_gc_decref( _p_input_item );
1153
1154     vlc_gc_incref( p_input_item );
1155     _p_input_item = p_input_item;
1156 }
1157
1158 input_item_t* Container::getInputItem() const
1159 {
1160     return _p_input_item;
1161 }