upnp: fix parsing of <dc:date> (which is in W3C format)
[vuplus_xbmc] / xbmc / network / upnp / UPnPInternal.cpp
1 /*
2  *      Copyright (C) 2012-2013 Team XBMC
3  *      http://xbmc.org
4  *
5  *  This Program is free software; you can redistribute it and/or modify
6  *  it under the terms of the GNU General Public License as published by
7  *  the Free Software Foundation; either version 2, or (at your option)
8  *  any later version.
9  *
10  *  This Program is distributed in the hope that it will be useful,
11  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  *  GNU General Public License for more details.
14  *
15  *  You should have received a copy of the GNU General Public License
16  *  along with XBMC; see the file COPYING.  If not, see
17  *  <http://www.gnu.org/licenses/>.
18  *
19  */
20 #include "UPnPInternal.h"
21 #include "UPnP.h"
22 #include "UPnPServer.h"
23 #include "Platinum.h"
24 #include "URL.h"
25 #include "Util.h"
26 #include "settings/AdvancedSettings.h"
27 #include "utils/log.h"
28 #include "utils/StringUtils.h"
29 #include "utils/URIUtils.h"
30 #include "FileItem.h"
31 #include "filesystem/File.h"
32 #include "filesystem/StackDirectory.h"
33 #include "filesystem/MusicDatabaseDirectory.h"
34 #include "filesystem/VideoDatabaseDirectory.h"
35 #include "video/VideoDatabase.h"
36 #include "video/VideoInfoTag.h"
37 #include "music/MusicDatabase.h"
38 #include "music/tags/MusicInfoTag.h"
39 #include "TextureDatabase.h"
40 #include "ThumbLoader.h"
41 #include "utils/URIUtils.h"
42
43 using namespace MUSIC_INFO;
44 using namespace XFILE;
45
46 namespace UPNP
47 {
48
49 /*----------------------------------------------------------------------
50 |  GetClientQuirks
51 +---------------------------------------------------------------------*/
52 EClientQuirks GetClientQuirks(const PLT_HttpRequestContext* context)
53 {
54   if(context == NULL)
55       return ECLIENTQUIRKS_NONE;
56
57   unsigned int quirks = 0;
58   const NPT_String* user_agent = context->GetRequest().GetHeaders().GetHeaderValue(NPT_HTTP_HEADER_USER_AGENT);
59   const NPT_String* server     = context->GetRequest().GetHeaders().GetHeaderValue(NPT_HTTP_HEADER_SERVER);
60
61   if (user_agent) {
62       if (user_agent->Find("XBox", 0, true) >= 0 ||
63           user_agent->Find("Xenon", 0, true) >= 0)
64           quirks |= ECLIENTQUIRKS_ONLYSTORAGEFOLDER | ECLIENTQUIRKS_BASICVIDEOCLASS;
65
66       if (user_agent->Find("Windows-Media-Player", 0, true) >= 0)
67           quirks |= ECLIENTQUIRKS_UNKNOWNSERIES;
68
69   }
70   if (server) {
71       if (server->Find("Xbox", 0, true) >= 0)
72           quirks |= ECLIENTQUIRKS_ONLYSTORAGEFOLDER | ECLIENTQUIRKS_BASICVIDEOCLASS;
73   }
74
75   return (EClientQuirks)quirks;
76 }
77
78 /*----------------------------------------------------------------------
79 |   GetMimeType
80 +---------------------------------------------------------------------*/
81 NPT_String
82 GetMimeType(const char* filename,
83             const PLT_HttpRequestContext* context /* = NULL */)
84 {
85     NPT_String ext = URIUtils::GetExtension(filename).c_str();
86     ext.TrimLeft('.');
87     ext = ext.ToLowercase();
88
89     return PLT_MimeType::GetMimeTypeFromExtension(ext, context);
90 }
91
92 /*----------------------------------------------------------------------
93 |   GetMimeType
94 +---------------------------------------------------------------------*/
95 NPT_String
96 GetMimeType(const CFileItem& item,
97             const PLT_HttpRequestContext* context /* = NULL */)
98 {
99     CStdString path = item.GetPath();
100     if (item.HasVideoInfoTag() && !item.GetVideoInfoTag()->GetPath().empty()) {
101         path = item.GetVideoInfoTag()->GetPath();
102     } else if (item.HasMusicInfoTag() && !item.GetMusicInfoTag()->GetURL().empty()) {
103         path = item.GetMusicInfoTag()->GetURL();
104     }
105
106     if (URIUtils::IsStack(path))
107         path = XFILE::CStackDirectory::GetFirstStackedFile(path);
108
109     NPT_String ext = URIUtils::GetExtension(path).c_str();
110     ext.TrimLeft('.');
111     ext = ext.ToLowercase();
112
113     NPT_String mime;
114
115     /* We always use Platinum mime type first
116        as it is defined to map extension to DLNA compliant mime type
117        or custom according to context (who asked for it) */
118     if (!ext.IsEmpty()) {
119         mime = PLT_MimeType::GetMimeTypeFromExtension(ext, context);
120         if (mime == "application/octet-stream") mime = "";
121     }
122
123     /* if Platinum couldn't map it, default to XBMC mapping */
124     if (mime.IsEmpty()) {
125         NPT_String mime = item.GetMimeType().c_str();
126         if (mime == "application/octet-stream") mime = "";
127     }
128
129     /* fallback to generic mime type if not found */
130     if (mime.IsEmpty()) {
131         if (item.IsVideo() || item.IsVideoDb() )
132             mime = "video/" + ext;
133         else if (item.IsAudio() || item.IsMusicDb() )
134             mime = "audio/" + ext;
135         else if (item.IsPicture() )
136             mime = "image/" + ext;
137     }
138
139     /* nothing we can figure out */
140     if (mime.IsEmpty()) {
141         mime = "application/octet-stream";
142     }
143
144     return mime;
145 }
146
147 /*----------------------------------------------------------------------
148 |   GetProtocolInfo
149 +---------------------------------------------------------------------*/
150 const NPT_String
151 GetProtocolInfo(const CFileItem&              item,
152                 const char*                   protocol,
153                 const PLT_HttpRequestContext* context /* = NULL */)
154 {
155     NPT_String proto = protocol;
156
157     /* fixup the protocol just in case nothing was passed */
158     if (proto.IsEmpty()) {
159         proto = item.GetAsUrl().GetProtocol();
160     }
161
162     /*
163        map protocol to right prefix and use xbmc-get for
164        unsupported UPnP protocols for other xbmc clients
165        TODO: add rtsp ?
166     */
167     if (proto == "http") {
168         proto = "http-get";
169     } else {
170         proto = "xbmc-get";
171     }
172
173     /* we need a valid extension to retrieve the mimetype for the protocol info */
174     NPT_String mime = GetMimeType(item, context);
175     proto += ":*:" + mime + ":" + PLT_ProtocolInfo::GetDlnaExtension(mime, context);
176     return proto;
177 }
178
179   /*----------------------------------------------------------------------
180    |   CResourceFinder
181    +---------------------------------------------------------------------*/
182 CResourceFinder::CResourceFinder(const char* protocol, const char* content)
183   : m_Protocol(protocol)
184   , m_Content(content)
185 {
186 }
187
188 bool CResourceFinder::operator()(const PLT_MediaItemResource& resource) const {
189     if (m_Content.IsEmpty())
190         return (resource.m_ProtocolInfo.GetProtocol().Compare(m_Protocol, true) == 0);
191     else
192         return ((resource.m_ProtocolInfo.GetProtocol().Compare(m_Protocol, true) == 0)
193               && resource.m_ProtocolInfo.GetContentType().StartsWith(m_Content, true));
194 }
195
196 /*----------------------------------------------------------------------
197 |   PopulateObjectFromTag
198 +---------------------------------------------------------------------*/
199 NPT_Result
200 PopulateObjectFromTag(CMusicInfoTag&         tag,
201                       PLT_MediaObject&       object,
202                       NPT_String*            file_path, /* = NULL */
203                       PLT_MediaItemResource* resource,  /* = NULL */
204                       EClientQuirks          quirks)
205 {
206     if (!tag.GetURL().empty() && file_path)
207       *file_path = tag.GetURL();
208
209     std::vector<std::string> genres = tag.GetGenre();
210     for (unsigned int index = 0; index < genres.size(); index++)
211       object.m_Affiliation.genres.Add(genres.at(index).c_str());
212     object.m_Title = tag.GetTitle();
213     object.m_Affiliation.album = tag.GetAlbum();
214     for (unsigned int index = 0; index < tag.GetArtist().size(); index++)
215     {
216       object.m_People.artists.Add(tag.GetArtist().at(index).c_str());
217       object.m_People.artists.Add(tag.GetArtist().at(index).c_str(), "Performer");
218     }
219     object.m_People.artists.Add(StringUtils::Join(!tag.GetAlbumArtist().empty() ? tag.GetAlbumArtist() : tag.GetArtist(), g_advancedSettings.m_musicItemSeparator).c_str(), "AlbumArtist");
220     if(tag.GetAlbumArtist().empty())
221         object.m_Creator = StringUtils::Join(tag.GetArtist(), g_advancedSettings.m_musicItemSeparator);
222     else
223         object.m_Creator = StringUtils::Join(tag.GetAlbumArtist(), g_advancedSettings.m_musicItemSeparator);
224     object.m_MiscInfo.original_track_number = tag.GetTrackNumber();
225     if(tag.GetDatabaseId() >= 0) {
226       object.m_ReferenceID = NPT_String::Format("musicdb://songs/%i%s", tag.GetDatabaseId(), URIUtils::GetExtension(tag.GetURL()).c_str());
227     }
228     if (object.m_ReferenceID == object.m_ObjectID)
229         object.m_ReferenceID = "";
230
231     object.m_MiscInfo.last_time = tag.GetLastPlayed().GetAsDBDate();
232     object.m_MiscInfo.play_count = tag.GetPlayCount();
233
234     if (resource) resource->m_Duration = tag.GetDuration();
235
236     return NPT_SUCCESS;
237 }
238
239 /*----------------------------------------------------------------------
240 |   PopulateObjectFromTag
241 +---------------------------------------------------------------------*/
242 NPT_Result
243 PopulateObjectFromTag(CVideoInfoTag&         tag,
244                       PLT_MediaObject&       object,
245                       NPT_String*            file_path, /* = NULL */
246                       PLT_MediaItemResource* resource,  /* = NULL */
247                       EClientQuirks          quirks)
248 {
249     // some usefull buffers
250     CStdStringArray strings;
251
252     if (!tag.m_strFileNameAndPath.empty() && file_path)
253       *file_path = tag.m_strFileNameAndPath;
254
255     if (tag.m_iDbId != -1 ) {
256         if (tag.m_type == "musicvideo") {
257           object.m_ObjectClass.type = "object.item.videoItem.musicVideoClip";
258           object.m_Creator = StringUtils::Join(tag.m_artist, g_advancedSettings.m_videoItemSeparator);
259           object.m_Title = tag.m_strTitle;
260           object.m_ReferenceID = NPT_String::Format("videodb://musicvideos/titles/%i", tag.m_iDbId);
261         } else if (tag.m_type == "movie") {
262           object.m_ObjectClass.type = "object.item.videoItem.movie";
263           object.m_Title = tag.m_strTitle;
264           object.m_Date = NPT_String::FromInteger(tag.m_iYear) + "-01-01";
265           object.m_ReferenceID = NPT_String::Format("videodb://movies/titles/%i", tag.m_iDbId);
266         } else {
267           object.m_ObjectClass.type = "object.item.videoItem.videoBroadcast";
268           object.m_Recorded.program_title  = "S" + ("0" + NPT_String::FromInteger(tag.m_iSeason)).Right(2);
269           object.m_Recorded.program_title += "E" + ("0" + NPT_String::FromInteger(tag.m_iEpisode)).Right(2);
270           object.m_Recorded.program_title += " : " + tag.m_strTitle;
271           object.m_Recorded.series_title = tag.m_strShowTitle;
272           int season = tag.m_iSeason > 1 ? tag.m_iSeason : 1;
273           object.m_Recorded.episode_number = season * 100 + tag.m_iEpisode;
274           object.m_Title = object.m_Recorded.series_title + " - " + object.m_Recorded.program_title;
275           object.m_Date = tag.m_firstAired.GetAsDBDate();
276           if(tag.m_iSeason != -1)
277               object.m_ReferenceID = NPT_String::Format("videodb://tvshows/0/%i", tag.m_iDbId);
278         }
279     }
280
281     if(quirks & ECLIENTQUIRKS_BASICVIDEOCLASS)
282         object.m_ObjectClass.type = "object.item.videoItem";
283
284     if(object.m_ReferenceID == object.m_ObjectID)
285         object.m_ReferenceID = "";
286
287     for (unsigned int index = 0; index < tag.m_genre.size(); index++)
288       object.m_Affiliation.genres.Add(tag.m_genre.at(index).c_str());
289
290     for(CVideoInfoTag::iCast it = tag.m_cast.begin();it != tag.m_cast.end();it++) {
291         object.m_People.actors.Add(it->strName.c_str(), it->strRole.c_str());
292     }
293
294     for (unsigned int index = 0; index < tag.m_director.size(); index++)
295       object.m_People.directors.Add(tag.m_director[index].c_str());
296
297     for (unsigned int index = 0; index < tag.m_writingCredits.size(); index++)
298       object.m_People.authors.Add(tag.m_writingCredits[index].c_str());
299
300     object.m_Description.description = tag.m_strTagLine;
301     object.m_Description.long_description = tag.m_strPlot;
302     object.m_Description.rating = tag.m_strMPAARating;
303     object.m_MiscInfo.last_position = (NPT_UInt32)tag.m_resumePoint.timeInSeconds;
304     object.m_MiscInfo.last_time = tag.m_lastPlayed.GetAsDBDate();
305     object.m_MiscInfo.play_count = tag.m_playCount;
306     if (resource) {
307         resource->m_Duration = tag.GetDuration();
308         if (tag.HasStreamDetails()) {
309             const CStreamDetails &details = tag.m_streamDetails;
310             resource->m_Resolution = NPT_String::FromInteger(details.GetVideoWidth()) + "x" + NPT_String::FromInteger(details.GetVideoHeight());
311         }
312     }
313
314     return NPT_SUCCESS;
315 }
316
317 /*----------------------------------------------------------------------
318 |   BuildObject
319 +---------------------------------------------------------------------*/
320 PLT_MediaObject*
321 BuildObject(CFileItem&                    item,
322             NPT_String&                   file_path,
323             bool                          with_count,
324             NPT_Reference<CThumbLoader>&  thumb_loader,
325             const PLT_HttpRequestContext* context /* = NULL */,
326             CUPnPServer*                  upnp_server /* = NULL */)
327 {
328     PLT_MediaItemResource resource;
329     PLT_MediaObject*      object = NULL;
330     std::string thumb, fanart;
331
332     CLog::Log(LOGDEBUG, "UPnP: Building didl for object '%s'", (const char*)item.GetPath());
333
334     EClientQuirks quirks = GetClientQuirks(context);
335
336     // get list of ip addresses
337     NPT_List<NPT_IpAddress> ips;
338     NPT_HttpUrl rooturi;
339     NPT_CHECK_LABEL(PLT_UPnPMessageHelper::GetIPAddresses(ips), failure);
340
341     // if we're passed an interface where we received the request from
342     // move the ip to the top
343     if (context && context->GetLocalAddress().GetIpAddress().ToString() != "0.0.0.0") {
344         rooturi = NPT_HttpUrl(context->GetLocalAddress().GetIpAddress().ToString(), context->GetLocalAddress().GetPort(), "/");
345         ips.Remove(context->GetLocalAddress().GetIpAddress());
346         ips.Insert(ips.GetFirstItem(), context->GetLocalAddress().GetIpAddress());
347     } else if(upnp_server) {
348         rooturi = NPT_HttpUrl("localhost", upnp_server->GetPort(), "/");
349     }
350
351     if (!item.m_bIsFolder) {
352         object = new PLT_MediaItem();
353         object->m_ObjectID = item.GetPath();
354
355         /* Setup object type */
356         if (item.IsMusicDb() || item.IsAudio()) {
357             object->m_ObjectClass.type = "object.item.audioItem.musicTrack";
358
359             if (item.HasMusicInfoTag()) {
360                 CMusicInfoTag *tag = (CMusicInfoTag*)item.GetMusicInfoTag();
361                 PopulateObjectFromTag(*tag, *object, &file_path, &resource, quirks);
362             }
363         } else if (item.IsVideoDb() || item.IsVideo()) {
364             object->m_ObjectClass.type = "object.item.videoItem";
365
366             if(quirks & ECLIENTQUIRKS_UNKNOWNSERIES)
367                 object->m_Affiliation.album = "[Unknown Series]";
368
369             if (item.HasVideoInfoTag()) {
370                 CVideoInfoTag *tag = (CVideoInfoTag*)item.GetVideoInfoTag();
371                 PopulateObjectFromTag(*tag, *object, &file_path, &resource, quirks);
372             }
373         } else if (item.IsPicture()) {
374             object->m_ObjectClass.type = "object.item.imageItem.photo";
375         } else {
376             object->m_ObjectClass.type = "object.item";
377         }
378
379         // duration of zero is invalid
380         if (resource.m_Duration == 0) resource.m_Duration = -1;
381
382         // Set the resource file size
383         resource.m_Size = item.m_dwSize;
384         if(resource.m_Size == 0)
385           resource.m_Size = (NPT_LargeSize)-1;
386
387         // set date
388         if (object->m_Date.IsEmpty() && item.m_dateTime.IsValid()) {
389             object->m_Date = item.m_dateTime.GetAsDBDate();
390         }
391
392         if (upnp_server) {
393             upnp_server->AddSafeResourceUri(object, rooturi, ips, file_path, GetProtocolInfo(item, "http", context));
394         }
395
396         // if the item is remote, add a direct link to the item
397         if (URIUtils::IsRemote((const char*)file_path)) {
398             resource.m_ProtocolInfo = PLT_ProtocolInfo(GetProtocolInfo(item, item.GetAsUrl().GetProtocol(), context));
399             resource.m_Uri = file_path;
400
401             // if the direct link can be served directly using http, then push it in front
402             // otherwise keep the xbmc-get resource last and let a compatible client look for it
403             if (resource.m_ProtocolInfo.ToString().StartsWith("xbmc", true)) {
404                 object->m_Resources.Add(resource);
405             } else {
406                 object->m_Resources.Insert(object->m_Resources.GetFirstItem(), resource);
407             }
408         }
409
410         // copy across the known metadata
411         for(unsigned i=0; i<object->m_Resources.GetItemCount(); i++) {
412             object->m_Resources[i].m_Size       = resource.m_Size;
413             object->m_Resources[i].m_Duration   = resource.m_Duration;
414             object->m_Resources[i].m_Resolution = resource.m_Resolution;
415         }
416
417         // Some upnp clients expect all audio items to have parent root id 4
418 #ifdef WMP_ID_MAPPING
419         object->m_ParentID = "4";
420 #endif
421     } else {
422         PLT_MediaContainer* container = new PLT_MediaContainer;
423         object = container;
424
425         /* Assign a title and id for this container */
426         container->m_ObjectID = item.GetPath();
427         container->m_ObjectClass.type = "object.container";
428         container->m_ChildrenCount = -1;
429
430         CStdStringArray strings;
431
432         /* this might be overkill, but hey */
433         if (item.IsMusicDb()) {
434             MUSICDATABASEDIRECTORY::NODE_TYPE node = CMusicDatabaseDirectory::GetDirectoryType(item.GetPath());
435             switch(node) {
436                 case MUSICDATABASEDIRECTORY::NODE_TYPE_ARTIST: {
437                       container->m_ObjectClass.type += ".person.musicArtist";
438                       CMusicInfoTag *tag = (CMusicInfoTag*)item.GetMusicInfoTag();
439                       if (tag) {
440                           container->m_People.artists.Add(
441                               CorrectAllItemsSortHack(StringUtils::Join(tag->GetArtist(), g_advancedSettings.m_musicItemSeparator)).c_str(), "Performer");
442                           container->m_People.artists.Add(
443                               CorrectAllItemsSortHack(StringUtils::Join(!tag->GetAlbumArtist().empty() ? tag->GetAlbumArtist() : tag->GetArtist(), g_advancedSettings.m_musicItemSeparator)).c_str(), "AlbumArtist");
444                       }
445 #ifdef WMP_ID_MAPPING
446                       // Some upnp clients expect all artists to have parent root id 107
447                       container->m_ParentID = "107";
448 #endif
449                   }
450                   break;
451                 case MUSICDATABASEDIRECTORY::NODE_TYPE_ALBUM:
452                 case MUSICDATABASEDIRECTORY::NODE_TYPE_ALBUM_COMPILATIONS:
453                 case MUSICDATABASEDIRECTORY::NODE_TYPE_ALBUM_RECENTLY_ADDED:
454                 case MUSICDATABASEDIRECTORY::NODE_TYPE_YEAR_ALBUM: {
455                       container->m_ObjectClass.type += ".album.musicAlbum";
456                       // for Sonos to be happy
457                       CMusicInfoTag *tag = (CMusicInfoTag*)item.GetMusicInfoTag();
458                       if (tag) {
459                           container->m_People.artists.Add(
460                               CorrectAllItemsSortHack(StringUtils::Join(tag->GetArtist(), g_advancedSettings.m_musicItemSeparator)).c_str(), "Performer");
461                           container->m_People.artists.Add(
462                               CorrectAllItemsSortHack(StringUtils::Join(!tag->GetAlbumArtist().empty() ? tag->GetAlbumArtist() : tag->GetArtist(), g_advancedSettings.m_musicItemSeparator)).c_str(), "AlbumArtist");
463                           container->m_Affiliation.album = CorrectAllItemsSortHack(tag->GetAlbum()).c_str();
464                       }
465 #ifdef WMP_ID_MAPPING
466                       // Some upnp clients expect all albums to have parent root id 7
467                       container->m_ParentID = "7";
468 #endif
469                   }
470                   break;
471                 case MUSICDATABASEDIRECTORY::NODE_TYPE_GENRE:
472                   container->m_ObjectClass.type += ".genre.musicGenre";
473                   break;
474                 default:
475                   break;
476             }
477         } else if (item.IsVideoDb()) {
478             VIDEODATABASEDIRECTORY::NODE_TYPE node = CVideoDatabaseDirectory::GetDirectoryType(item.GetPath());
479             CVideoInfoTag &tag = *(CVideoInfoTag*)item.GetVideoInfoTag();
480             switch(node) {
481                 case VIDEODATABASEDIRECTORY::NODE_TYPE_GENRE:
482                   container->m_ObjectClass.type += ".genre.movieGenre";
483                   break;
484                 case VIDEODATABASEDIRECTORY::NODE_TYPE_ACTOR:
485                   container->m_ObjectClass.type += ".person.videoArtist";
486                   container->m_Creator = StringUtils::Join(tag.m_artist, g_advancedSettings.m_videoItemSeparator);
487                   container->m_Title   = tag.m_strTitle;
488                   break;
489                 case VIDEODATABASEDIRECTORY::NODE_TYPE_SEASONS:
490                 case VIDEODATABASEDIRECTORY::NODE_TYPE_TITLE_TVSHOWS:
491                   container->m_ObjectClass.type += ".album.videoAlbum";
492                   container->m_Recorded.series_title = tag.m_strShowTitle;
493                   container->m_Recorded.episode_number = tag.m_iEpisode;
494                   container->m_MiscInfo.play_count = tag.m_playCount;
495                   container->m_Title = tag.m_strTitle;
496                   if(!tag.m_premiered.IsValid() && tag.m_iYear)
497                     container->m_Date = NPT_String::FromInteger(tag.m_iYear) + "-01-01";
498                   else
499                     container->m_Date = tag.m_premiered.GetAsDBDate();
500
501                   for (unsigned int index = 0; index < tag.m_genre.size(); index++)
502                     container->m_Affiliation.genres.Add(tag.m_genre.at(index).c_str());
503
504                   for(CVideoInfoTag::iCast it = tag.m_cast.begin();it != tag.m_cast.end();it++) {
505                       container->m_People.actors.Add(it->strName.c_str(), it->strRole.c_str());
506                   }
507
508                   for (unsigned int index = 0; index < tag.m_director.size(); index++)
509                     container->m_People.directors.Add(tag.m_director[index].c_str());
510                   for (unsigned int index = 0; index < tag.m_writingCredits.size(); index++)
511                     container->m_People.authors.Add(tag.m_writingCredits[index].c_str());
512
513                   container->m_Description.description = tag.m_strTagLine;
514                   container->m_Description.long_description = tag.m_strPlot;
515
516                   break;
517                 default:
518                   container->m_ObjectClass.type += ".storageFolder";
519                   break;
520             }
521         } else if (item.IsPlayList() || item.IsSmartPlayList()) {
522             container->m_ObjectClass.type += ".playlistContainer";
523         }
524
525         if(quirks & ECLIENTQUIRKS_ONLYSTORAGEFOLDER) {
526           container->m_ObjectClass.type = "object.container.storageFolder";
527         }
528
529         /* Get the number of children for this container */
530         if (with_count && upnp_server) {
531             if (object->m_ObjectID.StartsWith("virtualpath://")) {
532                 NPT_LargeSize count = 0;
533                 NPT_CHECK_LABEL(NPT_File::GetSize(file_path, count), failure);
534                 container->m_ChildrenCount = (NPT_Int32)count;
535             } else {
536                 /* this should be a standard path */
537                 // TODO - get file count of this directory
538             }
539         }
540     }
541
542     // set a title for the object
543     if (object->m_Title.IsEmpty()) {
544         if (!item.GetLabel().empty()) {
545             CStdString title = item.GetLabel();
546             if (item.IsPlayList() || !item.m_bIsFolder) URIUtils::RemoveExtension(title);
547             object->m_Title = title;
548         }
549     }
550
551     // determine the correct artwork for this item
552     if (!thumb_loader.IsNull())
553         thumb_loader->LoadItem(&item);
554
555     // finally apply the found artwork
556     thumb = item.GetArt("thumb");
557     if (upnp_server && !thumb.empty()) {
558         PLT_AlbumArtInfo art;
559         art.uri = upnp_server->BuildSafeResourceUri(
560             rooturi,
561             (*ips.GetFirstItem()).ToString(),
562             CTextureUtils::GetWrappedImageURL(thumb).c_str());
563
564         // Set DLNA profileID by extension, defaulting to JPEG.
565         if (URIUtils::HasExtension(thumb, ".png")) {
566             art.dlna_profile = "PNG_TN";
567         } else {
568             art.dlna_profile = "JPEG_TN";
569         }
570         object->m_ExtraInfo.album_arts.Add(art);
571     }
572
573     fanart = item.GetArt("fanart");
574     if (upnp_server && !fanart.empty())
575         upnp_server->AddSafeResourceUri(object, rooturi, ips, CTextureUtils::GetWrappedImageURL(fanart), "xbmc.org:*:fanart:*");
576
577     return object;
578
579 failure:
580     delete object;
581     return NULL;
582 }
583
584 /*----------------------------------------------------------------------
585 |   CUPnPServer::CorrectAllItemsSortHack
586 +---------------------------------------------------------------------*/
587 const CStdString&
588 CorrectAllItemsSortHack(const CStdString &item)
589 {
590     // This is required as in order for the "* All Albums" etc. items to sort
591     // correctly, they must have fake artist/album etc. information generated.
592     // This looks nasty if we attempt to render it to the GUI, thus this (further)
593     // workaround
594     if ((item.size() == 1 && item[0] == 0x01) || (item.size() > 1 && ((unsigned char) item[1]) == 0xff))
595         return StringUtils::EmptyString;
596
597     return item;
598 }
599
600 int
601 PopulateTagFromObject(CMusicInfoTag&          tag,
602                       PLT_MediaObject&       object,
603                       PLT_MediaItemResource* resource /* = NULL */)
604 {
605     tag.SetTitle((const char*)object.m_Title);
606     tag.SetArtist((const char*)object.m_Creator);
607     for(PLT_PersonRoles::Iterator it = object.m_People.artists.GetFirstItem(); it; it++) {
608         if     (it->role == "")            tag.SetArtist((const char*)it->name);
609         else if(it->role == "Performer")   tag.SetArtist((const char*)it->name);
610         else if(it->role == "AlbumArtist") tag.SetAlbumArtist((const char*)it->name);
611     }
612     tag.SetTrackNumber(object.m_MiscInfo.original_track_number);
613
614     for (NPT_List<NPT_String>::Iterator it = object.m_Affiliation.genres.GetFirstItem(); it; it++)
615         tag.SetGenre((const char*) *it);
616
617     tag.SetAlbum((const char*)object.m_Affiliation.album);
618     CDateTime last;
619     last.SetFromDateString((const char*)object.m_MiscInfo.last_time);
620     tag.SetLastPlayed(last);
621     tag.SetPlayCount(object.m_MiscInfo.play_count);
622     if(resource)
623         tag.SetDuration(resource->m_Duration);
624     tag.SetLoaded();
625     return NPT_SUCCESS;
626 }
627
628 int
629 PopulateTagFromObject(CVideoInfoTag&         tag,
630                       PLT_MediaObject&       object,
631                       PLT_MediaItemResource* resource /* = NULL */)
632 {
633     CDateTime date;
634     date.SetFromW3CDate((const char*)object.m_Date);
635
636     if(!object.m_Recorded.program_title.IsEmpty())
637     {
638         tag.m_type = "episode";
639         int episode;
640         int season;
641         int title = object.m_Recorded.program_title.Find(" : ");
642         if(sscanf(object.m_Recorded.program_title, "S%2dE%2d", &season, &episode) == 2 && title >= 0) {
643             tag.m_strTitle = object.m_Recorded.program_title.SubString(title + 3);
644             tag.m_iEpisode = episode;
645             tag.m_iSeason  = season;
646         } else {
647             tag.m_strTitle = object.m_Recorded.program_title;
648             tag.m_iSeason  = object.m_Recorded.episode_number / 100;
649             tag.m_iEpisode = object.m_Recorded.episode_number % 100;
650         }
651         tag.m_firstAired = date;
652     }
653     else if (!object.m_Recorded.series_title.IsEmpty()) {
654         tag.m_type= "season";
655         tag.m_strTitle = object.m_Title; // because could be TV show Title, or Season 1 etc
656         tag.m_iSeason  = object.m_Recorded.episode_number / 100;
657         tag.m_iEpisode = object.m_Recorded.episode_number % 100;
658         tag.m_premiered = date;
659     }
660     else if(object.m_ObjectClass.type == "object.item.videoItem.musicVideoClip") {
661         tag.m_type = "musicvideo";
662     }
663     else
664     {
665         tag.m_type         = "movie";
666         tag.m_strTitle     = object.m_Title;
667         tag.m_premiered    = date;
668     }
669     tag.m_iYear       = date.GetYear();
670     for (unsigned int index = 0; index < object.m_Affiliation.genres.GetItemCount(); index++)
671       tag.m_genre.push_back(object.m_Affiliation.genres.GetItem(index)->GetChars());
672     for (unsigned int index = 0; index < object.m_People.directors.GetItemCount(); index++)
673       tag.m_director.push_back(object.m_People.directors.GetItem(index)->name.GetChars());
674     for (unsigned int index = 0; index < object.m_People.authors.GetItemCount(); index++)
675       tag.m_writingCredits.push_back(object.m_People.authors.GetItem(index)->name.GetChars());
676     tag.m_strTagLine  = object.m_Description.description;
677     tag.m_strPlot     = object.m_Description.long_description;
678     tag.m_strMPAARating = object.m_Description.rating;
679     tag.m_strShowTitle = object.m_Recorded.series_title;
680     tag.m_lastPlayed.SetFromDateString((const char*)object.m_MiscInfo.last_time);
681     tag.m_playCount = object.m_MiscInfo.play_count;
682
683     if(resource)
684     {
685       if (resource->m_Duration)
686         tag.m_duration = resource->m_Duration;
687       if (object.m_MiscInfo.last_position > 0 )
688       {
689         tag.m_resumePoint.totalTimeInSeconds = resource->m_Duration;
690         tag.m_resumePoint.timeInSeconds = object.m_MiscInfo.last_position;
691       }
692     }
693     return NPT_SUCCESS;
694 }
695
696 CFileItemPtr BuildObject(PLT_MediaObject* entry)
697 {
698   NPT_String ObjectClass = entry->m_ObjectClass.type.ToLowercase();
699
700   CFileItemPtr pItem(new CFileItem((const char*)entry->m_Title));
701   pItem->SetLabelPreformated(true);
702   pItem->m_strTitle = (const char*)entry->m_Title;
703   pItem->m_bIsFolder = entry->IsContainer();
704
705   // if it's a container, format a string as upnp://uuid/object_id
706   if (pItem->m_bIsFolder) {
707
708     // look for metadata
709     if( ObjectClass.StartsWith("object.container.album.videoalbum") ) {
710       pItem->SetLabelPreformated(false);
711       UPNP::PopulateTagFromObject(*pItem->GetVideoInfoTag(), *entry, NULL);
712
713     } else if( ObjectClass.StartsWith("object.container.album.photoalbum")) {
714       //CPictureInfoTag* tag = pItem->GetPictureInfoTag();
715
716     } else if( ObjectClass.StartsWith("object.container.album") ) {
717       pItem->SetLabelPreformated(false);
718       UPNP::PopulateTagFromObject(*pItem->GetMusicInfoTag(), *entry, NULL);
719     }
720
721   } else {
722     bool audio = false
723        , image = false
724        , video = false;
725     // set a general content type
726     const char* content = NULL;
727     if (ObjectClass.StartsWith("object.item.videoitem")) {
728       pItem->SetMimeType("video/octet-stream");
729       content = "video";
730       video = true;
731     }
732     else if(ObjectClass.StartsWith("object.item.audioitem")) {
733       pItem->SetMimeType("audio/octet-stream");
734       content = "audio";
735       audio = true;
736     }
737     else if(ObjectClass.StartsWith("object.item.imageitem")) {
738       pItem->SetMimeType("image/octet-stream");
739       content = "image";
740       image = true;
741     }
742
743     // attempt to find a valid resource (may be multiple)
744     PLT_MediaItemResource resource, *res = NULL;
745     if(NPT_SUCCEEDED(NPT_ContainerFind(entry->m_Resources,
746                                        CResourceFinder("http-get", content), resource))) {
747
748       // set metadata
749       if (resource.m_Size != (NPT_LargeSize)-1) {
750         pItem->m_dwSize  = resource.m_Size;
751       }
752       res = &resource;
753     }
754     // look for metadata
755     if(video) {
756         pItem->SetLabelPreformated(false);
757         UPNP::PopulateTagFromObject(*pItem->GetVideoInfoTag(), *entry, res);
758
759     } else if(audio) {
760         pItem->SetLabelPreformated(false);
761         UPNP::PopulateTagFromObject(*pItem->GetMusicInfoTag(), *entry, res);
762
763     } else if(image) {
764         //CPictureInfoTag* tag = pItem->GetPictureInfoTag();
765
766     }
767   }
768
769   // look for date?
770   if(entry->m_Description.date.GetLength()) {
771     SYSTEMTIME time = {};
772     sscanf(entry->m_Description.date, "%hu-%hu-%huT%hu:%hu:%hu",
773            &time.wYear, &time.wMonth, &time.wDay, &time.wHour, &time.wMinute, &time.wSecond);
774     pItem->m_dateTime = time;
775   }
776
777   // if there is a thumbnail available set it here
778   if(entry->m_ExtraInfo.album_arts.GetItem(0))
779     // only considers first album art
780     pItem->SetArt("thumb", (const char*) entry->m_ExtraInfo.album_arts.GetItem(0)->uri);
781   else if(entry->m_Description.icon_uri.GetLength())
782     pItem->SetArt("thumb", (const char*) entry->m_Description.icon_uri);
783
784   PLT_ProtocolInfo fanart_mask("xbmc.org", "*", "fanart", "*");
785   for(unsigned i = 0; i < entry->m_Resources.GetItemCount(); ++i) {
786     PLT_MediaItemResource& res = entry->m_Resources[i];
787     if(res.m_ProtocolInfo.Match(fanart_mask)) {
788       pItem->SetArt("fanart", (const char*)res.m_Uri);
789       break;
790     }
791   }
792   // set the watched overlay, as this will not be set later due to
793   // content set on file item list
794   if (pItem->HasVideoInfoTag()) {
795     int episodes = pItem->GetVideoInfoTag()->m_iEpisode;
796     int played   = pItem->GetVideoInfoTag()->m_playCount;
797     const std::string& type = pItem->GetVideoInfoTag()->m_type;
798     bool watched(false);
799     if (type == "tvshow" || type == "season") {
800       pItem->SetProperty("totalepisodes", episodes);
801       pItem->SetProperty("numepisodes", episodes);
802       pItem->SetProperty("watchedepisodes", played);
803       pItem->SetProperty("unwatchedepisodes", episodes - played);
804       watched = (episodes && played == episodes);
805     }
806     else if (type == "episode" || type == "movie")
807       watched = (played > 0);
808     pItem->SetOverlayImage(CGUIListItem::ICON_OVERLAY_UNWATCHED, watched);
809   }
810   return pItem;
811 }
812
813 struct ResourcePrioritySort
814 {
815   ResourcePrioritySort(const PLT_MediaObject* entry)
816   {
817     if (entry->m_ObjectClass.type.StartsWith("object.item.audioItem"))
818         m_content = "audio";
819     else if (entry->m_ObjectClass.type.StartsWith("object.item.imageItem"))
820         m_content = "image";
821     else if (entry->m_ObjectClass.type.StartsWith("object.item.videoItem"))
822         m_content = "video";
823   }
824
825   int  GetPriority(const PLT_MediaItemResource& res) const
826   {
827     int prio = 0;
828
829     if (m_content != "" && res.m_ProtocolInfo.GetContentType().StartsWith(m_content))
830         prio += 400;
831
832     NPT_Url url(res.m_Uri);
833     if (URIUtils::IsHostOnLAN((const char*)url.GetHost(), false))
834         prio += 300;
835
836     if (res.m_ProtocolInfo.GetProtocol() == "xbmc-get")
837         prio += 200;
838     else if (res.m_ProtocolInfo.GetProtocol() == "http-get")
839         prio += 100;
840
841     return prio;
842   }
843
844   int operator()(const PLT_MediaItemResource& lh, const PLT_MediaItemResource& rh) const
845   {
846     if(GetPriority(lh) < GetPriority(rh))
847         return 1;
848     else
849         return 0;
850   }
851
852   NPT_String m_content;
853 };
854
855 bool GetResource(const PLT_MediaObject* entry, CFileItem& item)
856 {
857   PLT_MediaItemResource resource;
858
859   // store original path so we remember it
860   item.SetProperty("original_listitem_url",  item.GetPath());
861   item.SetProperty("original_listitem_mime", item.GetMimeType());
862
863   // get a sorted list based on our preference
864   NPT_List<PLT_MediaItemResource> sorted;
865   for (NPT_Cardinal i = 0; i < entry->m_Resources.GetItemCount(); ++i) {
866       sorted.Add(entry->m_Resources[i]);
867   }
868   sorted.Sort(ResourcePrioritySort(entry));
869
870   if(sorted.GetItemCount() == 0)
871     return false;
872
873   resource = *sorted.GetFirstItem();
874
875   // if it's an item, path is the first url to the item
876   // we hope the server made the first one reachable for us
877   // (it could be a format we dont know how to play however)
878   item.SetPath((const char*) resource.m_Uri);
879
880   // look for content type in protocol info
881   if (resource.m_ProtocolInfo.IsValid()) {
882     CLog::Log(LOGDEBUG, "CUPnPDirectory::GetResource - resource protocol info '%s'",
883               (const char*)(resource.m_ProtocolInfo.ToString()));
884
885     if (resource.m_ProtocolInfo.GetContentType().Compare("application/octet-stream") != 0) {
886       item.SetMimeType((const char*)resource.m_ProtocolInfo.GetContentType());
887     }
888   } else {
889     CLog::Log(LOGERROR, "CUPnPDirectory::GetResource - invalid protocol info '%s'",
890               (const char*)(resource.m_ProtocolInfo.ToString()));
891   }
892
893   // look for subtitles
894   unsigned subs = 0;
895   for(unsigned r = 0; r < entry->m_Resources.GetItemCount(); r++)
896   {
897     const PLT_MediaItemResource& res  = entry->m_Resources[r];
898     const PLT_ProtocolInfo&      info = res.m_ProtocolInfo;
899     static const char* allowed[] = { "text/srt"
900       , "text/ssa"
901       , "text/sub"
902       , "text/idx" };
903     for(unsigned type = 0; type < sizeof(allowed)/sizeof(allowed[0]); type++)
904     {
905       if(info.Match(PLT_ProtocolInfo("*", "*", allowed[type], "*")))
906       {
907         CStdString prop = StringUtils::Format("upnp:subtitle:%d", ++subs);
908         item.SetProperty(prop, (const char*)res.m_Uri);
909         break;
910       }
911     }
912   }
913   return true;
914 }
915
916 CFileItemPtr GetFileItem(const NPT_String& uri, const NPT_String& meta)
917 {
918     PLT_MediaObjectListReference list;
919     PLT_MediaObject*             object = NULL;
920     CFileItemPtr                 item;
921
922     if (NPT_SUCCEEDED(PLT_Didl::FromDidl(meta, list))) {
923         list->Get(0, object);
924     }
925
926     if (object) {
927         item = BuildObject(object);
928     }
929
930     if (item) {
931         item->SetPath((const char*)uri);
932         GetResource(object, *item);
933     } else {
934         item.reset(new CFileItem((const char*)uri, false));
935     }
936     return item;
937 }
938
939 } /* namespace UPNP */
940