Merge pull request #4700 from MartijnKaijser/13.1b2
[vuplus_xbmc] / xbmc / network / upnp / UPnPServer.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 "UPnPServer.h"
21 #include "UPnPInternal.h"
22 #include "Application.h"
23 #include "view/GUIViewState.h"
24 #include "Platinum.h"
25 #include "video/VideoThumbLoader.h"
26 #include "music/Artist.h"
27 #include "music/MusicThumbLoader.h"
28 #include "interfaces/AnnouncementManager.h"
29 #include "filesystem/Directory.h"
30 #include "filesystem/MusicDatabaseDirectory.h"
31 #include "filesystem/SpecialProtocol.h"
32 #include "filesystem/VideoDatabaseDirectory.h"
33 #include "guilib/WindowIDs.h"
34 #include "music/tags/MusicInfoTag.h"
35 #include "settings/AdvancedSettings.h"
36 #include "settings/Settings.h"
37 #include "utils/log.h"
38 #include "utils/md5.h"
39 #include "utils/StringUtils.h"
40 #include "utils/URIUtils.h"
41 #include "Util.h"
42 #include "music/MusicDatabase.h"
43 #include "video/VideoDatabase.h"
44 #include "guilib/GUIWindowManager.h"
45 #include "xbmc/GUIUserMessages.h"
46 #include "utils/FileUtils.h"
47
48 using namespace std;
49 using namespace ANNOUNCEMENT;
50 using namespace XFILE;
51
52 namespace UPNP
53 {
54
55 NPT_UInt32 CUPnPServer::m_MaxReturnedItems = 0;
56
57 const char* audio_containers[] = { "musicdb://genres/", "musicdb://artists/", "musicdb://albums/",
58                                    "musicdb://songs/", "musicdb://recentlyaddedalbums/", "musicdb://years/",
59                                    "musicdb://singles/" };
60
61 const char* video_containers[] = { "library://video/movies/titles.xml/", "library://video/tvshows/titles.xml/",
62                                    "videodb://recentlyaddedmovies/", "videodb://recentlyaddedepisodes/"  };
63
64 /*----------------------------------------------------------------------
65 |   CUPnPServer::CUPnPServer
66 +---------------------------------------------------------------------*/
67 CUPnPServer::CUPnPServer(const char* friendly_name, const char* uuid /*= NULL*/, int port /*= 0*/) :
68     PLT_MediaConnect(friendly_name, false, uuid, port),
69     PLT_FileMediaConnectDelegate("/", "/"),
70     m_scanning(g_application.IsMusicScanning() || g_application.IsVideoScanning())
71 {
72 }
73
74 CUPnPServer::~CUPnPServer()
75 {
76     ANNOUNCEMENT::CAnnouncementManager::RemoveAnnouncer(this);
77 }
78
79 /*----------------------------------------------------------------------
80 |   CUPnPServer::ProcessGetSCPD
81 +---------------------------------------------------------------------*/
82 NPT_Result
83 CUPnPServer::ProcessGetSCPD(PLT_Service*                  service,
84                             NPT_HttpRequest&              request,
85                             const NPT_HttpRequestContext& context,
86                             NPT_HttpResponse&             response)
87 {
88   // needed because PLT_MediaConnect only allows Xbox360 & WMP to search
89   return PLT_MediaServer::ProcessGetSCPD(service, request, context, response);
90 }
91
92 /*----------------------------------------------------------------------
93 |   CUPnPServer::SetupServices
94 +---------------------------------------------------------------------*/
95 NPT_Result
96 CUPnPServer::SetupServices()
97 {
98     PLT_MediaConnect::SetupServices();
99     PLT_Service* service = NULL;
100     NPT_Result result = FindServiceById("urn:upnp-org:serviceId:ContentDirectory", service);
101     if (service)
102       service->SetStateVariable("SortCapabilities", "res@duration,res@size,res@bitrate,dc:date,dc:title,dc:size,upnp:album,upnp:artist,upnp:albumArtist,upnp:episodeNumber,upnp:genre,upnp:originalTrackNumber,upnp:rating");
103
104     m_scanning = true;
105     OnScanCompleted(AudioLibrary);
106     m_scanning = true;
107     OnScanCompleted(VideoLibrary);
108
109     // now safe to start passing on new notifications
110     ANNOUNCEMENT::CAnnouncementManager::AddAnnouncer(this);
111
112     return result;
113 }
114
115 /*----------------------------------------------------------------------
116 |   CUPnPServer::OnScanCompleted
117 +---------------------------------------------------------------------*/
118 void
119 CUPnPServer::OnScanCompleted(int type)
120 {
121     if (type == AudioLibrary) {
122         for (size_t i = 0; i < sizeof(audio_containers)/sizeof(audio_containers[0]); i++)
123             UpdateContainer(audio_containers[i]);
124     }
125     else if (type == VideoLibrary) {
126         for (size_t i = 0; i < sizeof(video_containers)/sizeof(video_containers[0]); i++)
127             UpdateContainer(video_containers[i]);
128     }
129     else
130         return;
131     m_scanning = false;
132     PropagateUpdates();
133 }
134
135 /*----------------------------------------------------------------------
136 |   CUPnPServer::UpdateContainer
137 +---------------------------------------------------------------------*/
138 void
139 CUPnPServer::UpdateContainer(const string& id)
140 {
141     map<string,pair<bool, unsigned long> >::iterator itr = m_UpdateIDs.find(id);
142     unsigned long count = 0;
143     if (itr != m_UpdateIDs.end())
144         count = ++itr->second.second;
145     m_UpdateIDs[id] = make_pair(true, count);
146     PropagateUpdates();
147 }
148
149 /*----------------------------------------------------------------------
150 |   CUPnPServer::PropagateUpdates
151 +---------------------------------------------------------------------*/
152 void
153 CUPnPServer::PropagateUpdates()
154 {
155     PLT_Service* service = NULL;
156     NPT_String current_ids;
157     string buffer;
158     map<string,pair<bool, unsigned long> >::iterator itr;
159
160     if (m_scanning || !CSettings::Get().GetBool("services.upnpannounce"))
161         return;
162
163     NPT_CHECK_LABEL(FindServiceById("urn:upnp-org:serviceId:ContentDirectory", service), failed);
164
165     // we pause, and we must retain any changes which have not been
166     // broadcast yet
167     NPT_CHECK_LABEL(service->PauseEventing(), failed);
168     NPT_CHECK_LABEL(service->GetStateVariableValue("ContainerUpdateIDs", current_ids), failed);
169     buffer = (const char*)current_ids;
170     if (!buffer.empty())
171         buffer.append(",");
172
173     // only broadcast ids with modified bit set
174     for (itr = m_UpdateIDs.begin(); itr != m_UpdateIDs.end(); ++itr) {
175         if (itr->second.first) {
176             buffer.append(StringUtils::Format("%s,%ld,", itr->first.c_str(), itr->second.second).c_str());
177             itr->second.first = false;
178         }
179     }
180
181     // set the value, Platinum will clear ContainerUpdateIDs after sending
182     NPT_CHECK_LABEL(service->SetStateVariable("ContainerUpdateIDs", buffer.substr(0,buffer.size()-1).c_str(), true), failed);
183     NPT_CHECK_LABEL(service->IncStateVariable("SystemUpdateID"), failed);
184
185     service->PauseEventing(false);
186     return;
187
188 failed:
189     // should attempt to start eventing on a failure
190     if (service) service->PauseEventing(false);
191     CLog::Log(LOGERROR, "UPNP: Unable to propagate updates");
192 }
193
194 /*----------------------------------------------------------------------
195 |   CUPnPServer::SetupIcons
196 +---------------------------------------------------------------------*/
197 NPT_Result
198 CUPnPServer::SetupIcons()
199 {
200     NPT_String file_root = CSpecialProtocol::TranslatePath("special://xbmc/media/").c_str();
201     AddIcon(
202         PLT_DeviceIcon("image/png", 256, 256, 8, "/icon256x256.png"),
203         file_root);
204     AddIcon(
205         PLT_DeviceIcon("image/png", 120, 120, 8, "/icon120x120.png"),
206         file_root);
207     AddIcon(
208         PLT_DeviceIcon("image/png", 48, 48, 8, "/icon48x48.png"),
209         file_root);
210     AddIcon(
211         PLT_DeviceIcon("image/png", 32, 32, 8, "/icon32x32.png"),
212         file_root);
213     AddIcon(
214         PLT_DeviceIcon("image/png", 16, 16, 8, "/icon16x16.png"),
215         file_root);
216     return NPT_SUCCESS;
217 }
218
219 /*----------------------------------------------------------------------
220 |   CUPnPServer::BuildSafeResourceUri
221 +---------------------------------------------------------------------*/
222 NPT_String CUPnPServer::BuildSafeResourceUri(const NPT_HttpUrl &rooturi,
223                                              const char* host,
224                                              const char* file_path)
225 {
226     CURL url(file_path);
227     CStdString md5;
228     XBMC::XBMC_MD5 md5state;
229
230     // determine the filename to provide context to md5'd urls
231     CStdString filename;
232     if (url.GetProtocol() == "image")
233       filename = URIUtils::GetFileName(url.GetHostName());
234     else
235       filename = URIUtils::GetFileName(file_path);
236
237     filename = CURL::Encode(filename);
238     md5state.append(file_path);
239     md5state.getDigest(md5);
240     md5 += "/" + filename;
241     { NPT_AutoLock lock(m_FileMutex);
242       NPT_CHECK(m_FileMap.Put(md5.c_str(), file_path));
243     }
244     return PLT_FileMediaServer::BuildSafeResourceUri(rooturi, host, md5.c_str());
245 }
246
247 /*----------------------------------------------------------------------
248 |   CUPnPServer::Build
249 +---------------------------------------------------------------------*/
250 PLT_MediaObject*
251 CUPnPServer::Build(CFileItemPtr                  item,
252                    bool                          with_count,
253                    const PLT_HttpRequestContext& context,
254                    NPT_Reference<CThumbLoader>&  thumb_loader,
255                    const char*                   parent_id /* = NULL */)
256 {
257     PLT_MediaObject* object = NULL;
258     NPT_String       path = item->GetPath().c_str();
259
260     //HACK: temporary disabling count as it thrashes HDD
261     with_count = false;
262
263     CLog::Log(LOGDEBUG, "Preparing upnp object for item '%s'", (const char*)path);
264
265     if (path == "virtualpath://upnproot") {
266         path.TrimRight("/");
267         if (path.StartsWith("virtualpath://")) {
268             object = new PLT_MediaContainer;
269             object->m_Title = item->GetLabel();
270             object->m_ObjectClass.type = "object.container";
271             object->m_ObjectID = path;
272
273             // root
274             object->m_ObjectID = "0";
275             object->m_ParentID = "-1";
276             // root has 5 children
277             if (with_count) {
278                 ((PLT_MediaContainer*)object)->m_ChildrenCount = 5;
279             }
280         } else {
281             goto failure;
282         }
283
284     } else {
285         // db path handling
286         NPT_String file_path, share_name;
287         file_path = item->GetPath();
288         share_name = "";
289
290         if (path.StartsWith("musicdb://")) {
291             if (path == "musicdb://" ) {
292                 item->SetLabel("Music Library");
293                 item->SetLabelPreformated(true);
294             } else {
295                 if (!item->HasMusicInfoTag()) {
296                     MUSICDATABASEDIRECTORY::CQueryParams params;
297                     MUSICDATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo((const char*)path, params);
298
299                     CMusicDatabase db;
300                     if (!db.Open() ) return NULL;
301
302                     if (params.GetSongId() >= 0 ) {
303                         CSong song;
304                         if (db.GetSong(params.GetSongId(), song))
305                             item->GetMusicInfoTag()->SetSong(song);
306                     }
307                     else if (params.GetAlbumId() >= 0 ) {
308                         CAlbum album;
309                         if (db.GetAlbum(params.GetAlbumId(), album, false))
310                             item->GetMusicInfoTag()->SetAlbum(album);
311                     }
312                     else if (params.GetArtistId() >= 0 ) {
313                         CArtist artist;
314                         if (db.GetArtist(params.GetArtistId(), artist, false))
315                             item->GetMusicInfoTag()->SetArtist(artist);
316                     }
317                 }
318
319
320                 if (item->GetLabel().empty()) {
321                     /* if no label try to grab it from node type */
322                     CStdString label;
323                     if (CMusicDatabaseDirectory::GetLabel((const char*)path, label)) {
324                         item->SetLabel(label);
325                         item->SetLabelPreformated(true);
326                     }
327                 }
328             }
329         } else if (file_path.StartsWith("library://") || file_path.StartsWith("videodb://")) {
330             if (path == "library://video/" ) {
331                 item->SetLabel("Video Library");
332                 item->SetLabelPreformated(true);
333             } else {
334                 if (!item->HasVideoInfoTag()) {
335                     VIDEODATABASEDIRECTORY::CQueryParams params;
336                     VIDEODATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo((const char*)path, params);
337
338                     CVideoDatabase db;
339                     if (!db.Open() ) return NULL;
340
341                     if (params.GetMovieId() >= 0 )
342                         db.GetMovieInfo((const char*)path, *item->GetVideoInfoTag(), params.GetMovieId());
343                     else if (params.GetMVideoId() >= 0 )
344                         db.GetMusicVideoInfo((const char*)path, *item->GetVideoInfoTag(), params.GetMVideoId());
345                     else if (params.GetEpisodeId() >= 0 )
346                         db.GetEpisodeInfo((const char*)path, *item->GetVideoInfoTag(), params.GetEpisodeId());
347                     else if (params.GetTvShowId() >= 0 )
348                         db.GetTvShowInfo((const char*)path, *item->GetVideoInfoTag(), params.GetTvShowId());
349                 }
350
351                 if (item->GetVideoInfoTag()->m_type == "tvshow" || item->GetVideoInfoTag()->m_type == "season") {
352                     // for tvshows and seasons, iEpisode and playCount are
353                     // invalid
354                     item->GetVideoInfoTag()->m_iEpisode = (int)item->GetProperty("totalepisodes").asInteger();
355                     item->GetVideoInfoTag()->m_playCount = (int)item->GetProperty("watchedepisodes").asInteger();
356                 }
357
358                 // try to grab title from tag
359                 if (item->HasVideoInfoTag() && !item->GetVideoInfoTag()->m_strTitle.empty()) {
360                     item->SetLabel(item->GetVideoInfoTag()->m_strTitle);
361                     item->SetLabelPreformated(true);
362                 }
363
364                 // try to grab it from the folder
365                 if (item->GetLabel().empty()) {
366                     CStdString label;
367                     if (CVideoDatabaseDirectory::GetLabel((const char*)path, label)) {
368                         item->SetLabel(label);
369                         item->SetLabelPreformated(true);
370                     }
371                 }
372             }
373         }
374
375         // not a virtual path directory, new system
376         object = BuildObject(*item.get(), file_path, with_count, thumb_loader, &context, this);
377
378         // set parent id if passed, otherwise it should have been determined
379         if (object && parent_id) {
380             object->m_ParentID = parent_id;
381         }
382     }
383
384     if (object) {
385         // remap Root virtualpath://upnproot/ to id "0"
386         if (object->m_ObjectID == "virtualpath://upnproot/")
387             object->m_ObjectID = "0";
388
389         // remap Parent Root virtualpath://upnproot/ to id "0"
390         if (object->m_ParentID == "virtualpath://upnproot/")
391             object->m_ParentID = "0";
392     }
393
394     return object;
395
396 failure:
397     delete object;
398     return NULL;
399 }
400
401 /*----------------------------------------------------------------------
402 |   CUPnPServer::Announce
403 +---------------------------------------------------------------------*/
404 void
405 CUPnPServer::Announce(AnnouncementFlag flag, const char *sender, const char *message, const CVariant &data)
406 {
407     NPT_String path;
408     int item_id;
409     string item_type;
410
411     if (strcmp(sender, "xbmc"))
412         return;
413
414     if (strcmp(message, "OnUpdate") && strcmp(message, "OnRemove")
415         && strcmp(message, "OnScanStarted") && strcmp(message, "OnScanFinished"))
416         return;
417
418     if (data.isNull()) {
419         if (!strcmp(message, "OnScanStarted") || !strcmp(message, "OnCleanStarted")) {
420             m_scanning = true;
421         }
422         else if (!strcmp(message, "OnScanFinished") || !strcmp(message, "OnCleanFinished")) {
423             OnScanCompleted(flag);
424         }
425     }
426     else {
427         // handle both updates & removals
428         if (!data["item"].isNull()) {
429             item_id = (int)data["item"]["id"].asInteger();
430             item_type = data["item"]["type"].asString();
431         }
432         else {
433             item_id = (int)data["id"].asInteger();
434             item_type = data["type"].asString();
435         }
436
437         // we always update 'recently added' nodes along with the specific container,
438         // as we don't differentiate 'updates' from 'adds' in RPC interface
439         if (flag == VideoLibrary) {
440             if(item_type == "episode") {
441                 CVideoDatabase db;
442                 if (!db.Open()) return;
443                 int show_id = db.GetTvShowForEpisode(item_id);
444                 int season_id = db.GetSeasonForEpisode(item_id);
445                 UpdateContainer(StringUtils::Format("videodb://tvshows/titles/%d/", show_id));
446                 UpdateContainer(StringUtils::Format("videodb://tvshows/titles/%d/%d/?tvshowid=%d", show_id, season_id, show_id));
447                 UpdateContainer("videodb://recentlyaddedepisodes/");
448             }
449             else if(item_type == "tvshow") {
450                 UpdateContainer("library://video/tvshows/titles.xml/");
451                 UpdateContainer("videodb://recentlyaddedepisodes/");
452             }
453             else if(item_type == "movie") {
454                 UpdateContainer("library://video/movies/titles.xml/");
455                 UpdateContainer("videodb://recentlyaddedmovies/");
456             }
457             else if(item_type == "musicvideo") {
458                 UpdateContainer("library://video/musicvideos/titles.xml/");
459                 UpdateContainer("videodb://recentlyaddedmusicvideos/");
460             }
461         }
462         else if (flag == AudioLibrary && item_type == "song") {
463             // we also update the 'songs' container is maybe a performance drop too
464             // high? would need to check if slow clients even cache at all anyway
465             CMusicDatabase db;
466             CAlbum album;
467             if (!db.Open()) return;
468             if (db.GetAlbumFromSong(item_id, album)) {
469                 UpdateContainer(StringUtils::Format("musicdb://albums/%ld", album.idAlbum));
470                 UpdateContainer("musicdb://songs/");
471                 UpdateContainer("musicdb://recentlyaddedalbums/");
472             }
473         }
474     }
475 }
476
477 /*----------------------------------------------------------------------
478 |   TranslateWMPObjectId
479 +---------------------------------------------------------------------*/
480 static NPT_String TranslateWMPObjectId(NPT_String id)
481 {
482     if (id == "0") {
483         id = "virtualpath://upnproot/";
484     } else if (id == "15") {
485         // Xbox 360 asking for videos
486         id = "library://video/";
487     } else if (id == "16") {
488         // Xbox 360 asking for photos
489     } else if (id == "107") {
490         // Sonos uses 107 for artists root container id
491         id = "musicdb://artists/";
492     } else if (id == "7") {
493         // Sonos uses 7 for albums root container id
494         id = "musicdb://albums/";
495     } else if (id == "4") {
496         // Sonos uses 4 for tracks root container id
497         id = "musicdb://songs/";
498     }
499
500     CLog::Log(LOGDEBUG, "UPnP Translated id to '%s'", (const char*)id);
501     return id;
502 }
503
504 NPT_Result
505 ObjectIDValidate(const NPT_String& id)
506 {
507     if (CFileUtils::RemoteAccessAllowed(id.GetChars()))
508         return NPT_SUCCESS;
509     return NPT_ERROR_NO_SUCH_FILE;
510 }
511
512 /*----------------------------------------------------------------------
513 |   CUPnPServer::OnBrowseMetadata
514 +---------------------------------------------------------------------*/
515 NPT_Result
516 CUPnPServer::OnBrowseMetadata(PLT_ActionReference&          action,
517                               const char*                   object_id,
518                               const char*                   filter,
519                               NPT_UInt32                    starting_index,
520                               NPT_UInt32                    requested_count,
521                               const char*                   sort_criteria,
522                               const PLT_HttpRequestContext& context)
523 {
524     NPT_COMPILER_UNUSED(sort_criteria);
525     NPT_COMPILER_UNUSED(requested_count);
526     NPT_COMPILER_UNUSED(starting_index);
527
528     NPT_String                     didl;
529     NPT_Reference<PLT_MediaObject> object;
530     NPT_String                     id = TranslateWMPObjectId(object_id);
531     CFileItemPtr                   item;
532     NPT_Reference<CThumbLoader>    thumb_loader;
533
534     CLog::Log(LOGINFO, "Received UPnP Browse Metadata request for object '%s'", (const char*)object_id);
535
536     if(NPT_FAILED(ObjectIDValidate(id))) {
537         action->SetError(701, "Incorrect ObjectID.");
538         return NPT_FAILURE;
539     }
540
541     if (id.StartsWith("virtualpath://")) {
542         id.TrimRight("/");
543         if (id == "virtualpath://upnproot") {
544             id += "/";
545             item.reset(new CFileItem((const char*)id, true));
546             item->SetLabel("Root");
547             item->SetLabelPreformated(true);
548             object = Build(item, true, context, thumb_loader);
549             object->m_ParentID = "-1";
550         } else {
551             return NPT_FAILURE;
552         }
553     } else {
554         // determine if it's a container by calling CDirectory::Exists
555         item.reset(new CFileItem((const char*)id, CDirectory::Exists((const char*)id)));
556
557         // attempt to determine the parent of this item
558         CStdString parent;
559         if (URIUtils::IsVideoDb((const char*)id) || URIUtils::IsMusicDb((const char*)id) || StringUtils::StartsWithNoCase((const char*)id, "library://video/")) {
560             if (!URIUtils::GetParentPath((const char*)id, parent)) {
561                 parent = "0";
562             }
563         }
564         else {
565             // non-library objects - playlists / sources
566             //
567             // we could instead store the parents in a hash during every browse
568             // or could handle this in URIUtils::GetParentPath() possibly,
569             // however this is quicker to implement and subsequently purge when a
570             // better solution presents itself
571             CStdString child_id((const char*)id);
572             if      (StringUtils::StartsWithNoCase(child_id, "special://musicplaylists/"))          parent = "musicdb://";
573             else if (StringUtils::StartsWithNoCase(child_id, "special://videoplaylists/"))          parent = "library://video/";
574             else if (StringUtils::StartsWithNoCase(child_id, "sources://video/"))                   parent = "library://video/";
575             else if (StringUtils::StartsWithNoCase(child_id, "special://profile/playlists/music/")) parent = "special://musicplaylists/";
576             else if (StringUtils::StartsWithNoCase(child_id, "special://profile/playlists/video/")) parent = "special://videoplaylists/";
577             else parent = "sources://video/"; // this can only match video sources
578         }
579
580         if (item->IsVideoDb()) {
581             thumb_loader = NPT_Reference<CThumbLoader>(new CVideoThumbLoader());
582         }
583         else if (item->IsMusicDb()) {
584             thumb_loader = NPT_Reference<CThumbLoader>(new CMusicThumbLoader());
585         }
586         if (!thumb_loader.IsNull()) {
587             thumb_loader->OnLoaderStart();
588         }
589         object = Build(item, true, context, thumb_loader, parent.empty()?NULL:parent.c_str());
590     }
591
592     if (object.IsNull()) {
593         /* error */
594         NPT_LOG_WARNING_1("CUPnPServer::OnBrowseMetadata - Object null (%s)", object_id);
595         action->SetError(701, "No Such Object.");
596         return NPT_FAILURE;
597     }
598
599     NPT_String tmp;
600     NPT_CHECK(PLT_Didl::ToDidl(*object.AsPointer(), filter, tmp));
601
602     /* add didl header and footer */
603     didl = didl_header + tmp + didl_footer;
604
605     NPT_CHECK(action->SetArgumentValue("Result", didl));
606     NPT_CHECK(action->SetArgumentValue("NumberReturned", "1"));
607     NPT_CHECK(action->SetArgumentValue("TotalMatches", "1"));
608
609     // update ID may be wrong here, it should be the one of the container?
610     NPT_CHECK(action->SetArgumentValue("UpdateId", "0"));
611
612     // TODO: We need to keep track of the overall SystemUpdateID of the CDS
613
614     return NPT_SUCCESS;
615 }
616
617 /*----------------------------------------------------------------------
618 |   CUPnPServer::OnBrowseDirectChildren
619 +---------------------------------------------------------------------*/
620 NPT_Result
621 CUPnPServer::OnBrowseDirectChildren(PLT_ActionReference&          action,
622                                     const char*                   object_id,
623                                     const char*                   filter,
624                                     NPT_UInt32                    starting_index,
625                                     NPT_UInt32                    requested_count,
626                                     const char*                   sort_criteria,
627                                     const PLT_HttpRequestContext& context)
628 {
629     CFileItemList items;
630     NPT_String    parent_id = TranslateWMPObjectId(object_id);
631
632     CLog::Log(LOGINFO, "UPnP: Received Browse DirectChildren request for object '%s', with sort criteria %s", object_id, sort_criteria);
633
634     if(NPT_FAILED(ObjectIDValidate(parent_id))) {
635         action->SetError(701, "Incorrect ObjectID.");
636         return NPT_FAILURE;
637     }
638
639     items.SetPath(CStdString(parent_id));
640
641     // guard against loading while saving to the same cache file
642     // as CArchive currently performs no locking itself
643     bool load;
644     { NPT_AutoLock lock(m_CacheMutex);
645       load = items.Load();
646     }
647
648     if (!load) {
649         // cache anything that takes more than a second to retrieve
650         unsigned int time = XbmcThreads::SystemClockMillis();
651
652         if (parent_id.StartsWith("virtualpath://upnproot")) {
653             CFileItemPtr item;
654
655             // music library
656             item.reset(new CFileItem("musicdb://", true));
657             item->SetLabel("Music Library");
658             item->SetLabelPreformated(true);
659             items.Add(item);
660
661             // video library
662             item.reset(new CFileItem("library://video/", true));
663             item->SetLabel("Video Library");
664             item->SetLabelPreformated(true);
665             items.Add(item);
666
667             items.Sort(SortByLabel, SortOrderAscending);
668         } else {
669             // this is the only way to hide unplayable items in the 'files'
670             // view as we cannot tell what context (eg music vs video) the
671             // request came from
672             string supported = g_advancedSettings.m_pictureExtensions + "|"
673                              + g_advancedSettings.m_videoExtensions + "|"
674                              + g_advancedSettings.m_musicExtensions + "|"
675                              + g_advancedSettings.m_discStubExtensions;
676             CDirectory::GetDirectory((const char*)parent_id, items, supported);
677             DefaultSortItems(items);
678         }
679
680         if (items.CacheToDiscAlways() || (items.CacheToDiscIfSlow() && (XbmcThreads::SystemClockMillis() - time) > 1000 )) {
681             NPT_AutoLock lock(m_CacheMutex);
682             items.Save();
683         }
684     }
685
686     // as there's no library://music support, manually add playlists and music
687     // video nodes
688     if (items.GetPath() == "musicdb://") {
689       CFileItemPtr playlists(new CFileItem("special://musicplaylists/", true));
690       playlists->SetLabel(g_localizeStrings.Get(136));
691       items.Add(playlists);
692
693       CVideoDatabase database;
694       database.Open();
695       if (database.HasContent(VIDEODB_CONTENT_MUSICVIDEOS)) {
696           CFileItemPtr mvideos(new CFileItem("library://video/musicvideos/", true));
697           mvideos->SetLabel(g_localizeStrings.Get(20389));
698           items.Add(mvideos);
699       }
700     }
701
702     // Don't pass parent_id if action is Search not BrowseDirectChildren, as
703     // we want the engine to determine the best parent id, not necessarily the one
704     // passed
705     NPT_String action_name = action->GetActionDesc().GetName();
706     return BuildResponse(
707         action,
708         items,
709         filter,
710         starting_index,
711         requested_count,
712         sort_criteria,
713         context,
714         (action_name.Compare("Search", true)==0)?NULL:parent_id.GetChars());
715 }
716
717 /*----------------------------------------------------------------------
718 |   CUPnPServer::BuildResponse
719 +---------------------------------------------------------------------*/
720 NPT_Result
721 CUPnPServer::BuildResponse(PLT_ActionReference&          action,
722                            CFileItemList&                items,
723                            const char*                   filter,
724                            NPT_UInt32                    starting_index,
725                            NPT_UInt32                    requested_count,
726                            const char*                   sort_criteria,
727                            const PLT_HttpRequestContext& context,
728                            const char*                   parent_id /* = NULL */)
729 {
730     NPT_COMPILER_UNUSED(sort_criteria);
731
732     CLog::Log(LOGDEBUG, "Building UPnP response with filter '%s', starting @ %d with %d requested",
733         (const char*)filter,
734         starting_index,
735         requested_count);
736
737     // we will reuse this ThumbLoader for all items
738     NPT_Reference<CThumbLoader> thumb_loader;
739
740     if (URIUtils::IsVideoDb(items.GetPath()) ||
741         StringUtils::StartsWithNoCase(items.GetPath(), "library://video/") ||
742         StringUtils::StartsWithNoCase(items.GetPath(), "special://profile/playlists/video/")) {
743
744         thumb_loader = NPT_Reference<CThumbLoader>(new CVideoThumbLoader());
745     }
746     else if (URIUtils::IsMusicDb(items.GetPath()) ||
747         StringUtils::StartsWithNoCase(items.GetPath(), "special://profile/playlists/music/")) {
748
749         thumb_loader = NPT_Reference<CThumbLoader>(new CMusicThumbLoader());
750     }
751     if (!thumb_loader.IsNull()) {
752         thumb_loader->OnLoaderStart();
753     }
754
755     // this isn't pretty but needed to properly hide the addons node from clients
756     if (StringUtils::StartsWith(items.GetPath(), "library")) {
757         for (int i=0; i<items.Size(); i++) {
758             if (StringUtils::StartsWith(items[i]->GetPath(), "addons") ||
759                 StringUtils::EndsWith(items[i]->GetPath(), "/addons.xml/"))
760                 items.Remove(i);
761         }
762     }
763
764     // won't return more than UPNP_MAX_RETURNED_ITEMS items at a time to keep things smooth
765     // 0 requested means as many as possible
766     NPT_UInt32 max_count  = (requested_count == 0)?m_MaxReturnedItems:min((unsigned long)requested_count, (unsigned long)m_MaxReturnedItems);
767     NPT_UInt32 stop_index = min((unsigned long)(starting_index + max_count), (unsigned long)items.Size()); // don't return more than we can
768
769     NPT_Cardinal count = 0;
770     NPT_Cardinal total = items.Size();
771     NPT_String didl = didl_header;
772     PLT_MediaObjectReference object;
773     for (unsigned long i=starting_index; i<stop_index; ++i) {
774         object = Build(items[i], true, context, thumb_loader, parent_id);
775         if (object.IsNull()) {
776             // don't tell the client this item ever existed
777             --total;
778             continue;
779         }
780
781         NPT_String tmp;
782         NPT_CHECK(PLT_Didl::ToDidl(*object.AsPointer(), filter, tmp));
783
784         // Neptunes string growing is dead slow for small additions
785         if (didl.GetCapacity() < tmp.GetLength() + didl.GetLength()) {
786             didl.Reserve((tmp.GetLength() + didl.GetLength())*2);
787         }
788         didl += tmp;
789         ++count;
790     }
791
792     didl += didl_footer;
793
794     CLog::Log(LOGDEBUG, "Returning UPnP response with %d items out of %d total matches",
795         count,
796         total);
797
798     NPT_CHECK(action->SetArgumentValue("Result", didl));
799     NPT_CHECK(action->SetArgumentValue("NumberReturned", NPT_String::FromInteger(count)));
800     NPT_CHECK(action->SetArgumentValue("TotalMatches", NPT_String::FromInteger(total)));
801     NPT_CHECK(action->SetArgumentValue("UpdateId", "0"));
802     return NPT_SUCCESS;
803 }
804
805 /*----------------------------------------------------------------------
806 |   FindSubCriteria
807 +---------------------------------------------------------------------*/
808 static
809 NPT_String
810 FindSubCriteria(NPT_String criteria, const char* name)
811 {
812     NPT_String result;
813     int search = criteria.Find(name);
814     if (search >= 0) {
815         criteria = criteria.Right(criteria.GetLength() - search - NPT_StringLength(name));
816         criteria.TrimLeft(" ");
817         if (criteria.GetLength()>0 && criteria[0] == '=') {
818             criteria.TrimLeft("= ");
819             if (criteria.GetLength()>0 && criteria[0] == '\"') {
820                 search = criteria.Find("\"", 1);
821                 if (search > 0) result = criteria.SubString(1, search-1);
822             }
823         }
824     }
825     return result;
826 }
827
828 /*----------------------------------------------------------------------
829 |   CUPnPServer::OnSearchContainer
830 +---------------------------------------------------------------------*/
831 NPT_Result
832 CUPnPServer::OnSearchContainer(PLT_ActionReference&          action,
833                                const char*                   object_id,
834                                const char*                   search_criteria,
835                                const char*                   filter,
836                                NPT_UInt32                    starting_index,
837                                NPT_UInt32                    requested_count,
838                                const char*                   sort_criteria,
839                                const PLT_HttpRequestContext& context)
840 {
841     CLog::Log(LOGDEBUG, "Received Search request for object '%s' with search '%s'",
842         (const char*)object_id,
843         (const char*)search_criteria);
844
845     NPT_String id = object_id;
846     if (id.StartsWith("musicdb://")) {
847         // we browse for all tracks given a genre, artist or album
848         if (NPT_String(search_criteria).Find("object.item.audioItem") >= 0) {
849             if (!id.EndsWith("/")) id += "/";
850             NPT_Cardinal count = id.SubString(10).Split("/").GetItemCount();
851             // remove extra empty node count
852             count = count?count-1:0;
853
854             // genre
855             if (id.StartsWith("musicdb://genres/")) {
856                 // all tracks of all genres
857                 if (count == 1)
858                     id += "-1/-1/-1/";
859                 // all tracks of a specific genre
860                 else if (count == 2)
861                     id += "-1/-1/";
862                 // all tracks of a specific genre of a specfic artist
863                 else if (count == 3)
864                     id += "-1/";
865             } else if (id.StartsWith("musicdb://artists/")) {
866                 // all tracks by all artists
867                 if (count == 1)
868                     id += "-1/-1/";
869                 // all tracks of a specific artist
870                 else if (count == 2)
871                     id += "-1/";
872             } else if (id.StartsWith("musicdb://albums/")) {
873                 // all albums ?
874                 if (count == 1) id += "-1/";
875             }
876         }
877         return OnBrowseDirectChildren(action, id, filter, starting_index, requested_count, sort_criteria, context);
878     } else if (NPT_String(search_criteria).Find("object.item.audioItem") >= 0) {
879         // look for artist, album & genre filters
880         NPT_String genre = FindSubCriteria(search_criteria, "upnp:genre");
881         NPT_String album = FindSubCriteria(search_criteria, "upnp:album");
882         NPT_String artist = FindSubCriteria(search_criteria, "upnp:artist");
883         // sonos looks for microsoft specific stuff
884         artist = artist.GetLength()?artist:FindSubCriteria(search_criteria, "microsoft:artistPerformer");
885         artist = artist.GetLength()?artist:FindSubCriteria(search_criteria, "microsoft:artistAlbumArtist");
886         artist = artist.GetLength()?artist:FindSubCriteria(search_criteria, "microsoft:authorComposer");
887
888         CMusicDatabase database;
889         database.Open();
890
891         if (genre.GetLength() > 0) {
892             // all tracks by genre filtered by artist and/or album
893             CStdString strPath;
894             strPath = StringUtils::Format("musicdb://genres/%ld/%ld/%ld/",
895                                           database.GetGenreByName((const char*)genre),
896                                           database.GetArtistByName((const char*)artist), // will return -1 if no artist
897                                           database.GetAlbumByName((const char*)album));  // will return -1 if no album
898
899             return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index, requested_count, sort_criteria, context);
900         } else if (artist.GetLength() > 0) {
901             // all tracks by artist name filtered by album if passed
902             CStdString strPath;
903             strPath = StringUtils::Format("musicdb://artists/%ld/%ld/",
904                                           database.GetArtistByName((const char*)artist),
905                                           database.GetAlbumByName((const char*)album)); // will return -1 if no album
906
907             return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index, requested_count, sort_criteria, context);
908         } else if (album.GetLength() > 0) {
909             // all tracks by album name
910             CStdString strPath = StringUtils::Format("musicdb://albums/%ld/",
911                                                      database.GetAlbumByName((const char*)album));
912
913             return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index, requested_count, sort_criteria, context);
914         }
915
916         // browse all songs
917         return OnBrowseDirectChildren(action, "musicdb://songs/", filter, starting_index, requested_count, sort_criteria, context);
918     } else if (NPT_String(search_criteria).Find("object.container.album.musicAlbum") >= 0) {
919         // sonos filters by genre
920         NPT_String genre = FindSubCriteria(search_criteria, "upnp:genre");
921
922         // 360 hack: artist/albums using search
923         NPT_String artist = FindSubCriteria(search_criteria, "upnp:artist");
924         // sonos looks for microsoft specific stuff
925         artist = artist.GetLength()?artist:FindSubCriteria(search_criteria, "microsoft:artistPerformer");
926         artist = artist.GetLength()?artist:FindSubCriteria(search_criteria, "microsoft:artistAlbumArtist");
927         artist = artist.GetLength()?artist:FindSubCriteria(search_criteria, "microsoft:authorComposer");
928
929         CMusicDatabase database;
930         database.Open();
931
932         if (genre.GetLength() > 0) {
933             CStdString strPath = StringUtils::Format("musicdb://genres/%ld/%ld/",
934                                                      database.GetGenreByName((const char*)genre),
935                                                      database.GetArtistByName((const char*)artist)); // no artist should return -1
936             return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index, requested_count, sort_criteria, context);
937         } else if (artist.GetLength() > 0) {
938             CStdString strPath = StringUtils::Format("musicdb://artists/%ld/",
939                                                      database.GetArtistByName((const char*)artist));
940             return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index, requested_count, sort_criteria, context);
941         }
942
943         // all albums
944         return OnBrowseDirectChildren(action, "musicdb://albums/", filter, starting_index, requested_count, sort_criteria, context);
945     } else if (NPT_String(search_criteria).Find("object.container.person.musicArtist") >= 0) {
946         // Sonos filters by genre
947         NPT_String genre = FindSubCriteria(search_criteria, "upnp:genre");
948         if (genre.GetLength() > 0) {
949             CMusicDatabase database;
950             database.Open();
951             CStdString strPath = StringUtils::Format("musicdb://genres/%ld/", database.GetGenreByName((const char*)genre));
952             return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index, requested_count, sort_criteria, context);
953         }
954         return OnBrowseDirectChildren(action, "musicdb://artists/", filter, starting_index, requested_count, sort_criteria, context);
955     }  else if (NPT_String(search_criteria).Find("object.container.genre.musicGenre") >= 0) {
956         return OnBrowseDirectChildren(action, "musicdb://genres/", filter, starting_index, requested_count, sort_criteria, context);
957     } else if (NPT_String(search_criteria).Find("object.container.playlistContainer") >= 0) {
958         return OnBrowseDirectChildren(action, "special://musicplaylists/", filter, starting_index, requested_count, sort_criteria, context);
959     } else if (NPT_String(search_criteria).Find("object.item.videoItem") >= 0) {
960       CFileItemList items, itemsall;
961
962       CVideoDatabase database;
963       if (!database.Open()) {
964         action->SetError(800, "Internal Error");
965         return NPT_SUCCESS;
966       }
967
968       if (!database.GetMoviesNav("videodb://movies/titles/", items)) {
969         action->SetError(800, "Internal Error");
970         return NPT_SUCCESS;
971       }
972       itemsall.Append(items);
973       items.Clear();
974
975       if (!database.GetEpisodesByWhere("videodb://tvshows/titles/", "", items)) {
976         action->SetError(800, "Internal Error");
977         return NPT_SUCCESS;
978       }
979       itemsall.Append(items);
980       items.Clear();
981
982       return BuildResponse(action, itemsall, filter, starting_index, requested_count, sort_criteria, context, NULL);
983   } else if (NPT_String(search_criteria).Find("object.item.imageItem") >= 0) {
984       CFileItemList items;
985       return BuildResponse(action, items, filter, starting_index, requested_count, sort_criteria, context, NULL);;
986   }
987
988   return NPT_FAILURE;
989 }
990
991 /*----------------------------------------------------------------------
992 |   CUPnPServer::OnUpdateObject
993 +---------------------------------------------------------------------*/
994 NPT_Result
995 CUPnPServer::OnUpdateObject(PLT_ActionReference&             action,
996                             const char*                      object_id,
997                             NPT_Map<NPT_String,NPT_String>&  current_vals,
998                             NPT_Map<NPT_String,NPT_String>&  new_vals,
999                             const PLT_HttpRequestContext&    context)
1000 {
1001     CStdString path(CURL::Decode(object_id));
1002     CFileItem updated;
1003     updated.SetPath(path);
1004     CLog::Log(LOGINFO, "UPnP: OnUpdateObject: %s from %s", path.c_str(),
1005                        (const char*) context.GetRemoteAddress().GetIpAddress().ToString());
1006
1007     NPT_String playCount, position;
1008     int err;
1009     const char* msg = NULL;
1010     bool updatelisting(false);
1011
1012     // we pause eventing as multiple announces may happen in this operation
1013     PLT_Service* service = NULL;
1014     NPT_CHECK_LABEL(FindServiceById("urn:upnp-org:serviceId:ContentDirectory", service), error);
1015     NPT_CHECK_LABEL(service->PauseEventing(), error);
1016
1017     if (updated.IsVideoDb()) {
1018         CVideoDatabase db;
1019         NPT_CHECK_LABEL(!db.Open(), error);
1020
1021         // must first determine type of file from object id
1022         VIDEODATABASEDIRECTORY::CQueryParams params;
1023         VIDEODATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo(path.c_str(), params);
1024
1025         int id = -1;
1026         VIDEODB_CONTENT_TYPE content_type;
1027         if ((id = params.GetMovieId()) >= 0 )
1028             content_type = VIDEODB_CONTENT_MOVIES;
1029         else if ((id = params.GetEpisodeId()) >= 0 )
1030             content_type = VIDEODB_CONTENT_EPISODES;
1031         else if ((id = params.GetMVideoId()) >= 0 )
1032             content_type = VIDEODB_CONTENT_MUSICVIDEOS;
1033         else {
1034             err = 701;
1035             msg = "No such object";
1036             goto failure;
1037         }
1038
1039         CStdString file_path;
1040         db.GetFilePathById(id, file_path, content_type);
1041         CVideoInfoTag tag;
1042         db.LoadVideoInfo(file_path, tag);
1043         updated.SetFromVideoInfoTag(tag);
1044         CLog::Log(LOGINFO, "UPNP: Translated to %s", file_path.c_str());
1045
1046         position = new_vals["lastPlaybackPosition"];
1047         playCount = new_vals["playCount"];
1048
1049         if (!position.IsEmpty()
1050               && position.Compare(current_vals["lastPlaybackPosition"]) != 0) {
1051             NPT_UInt32 resume;
1052             NPT_CHECK_LABEL(position.ToInteger32(resume), args);
1053
1054             if (resume <= 0)
1055                 db.ClearBookMarksOfFile(file_path, CBookmark::RESUME);
1056             else {
1057                 CBookmark bookmark;
1058                 bookmark.timeInSeconds = resume;
1059                 bookmark.totalTimeInSeconds = resume + 100; // not required to be correct
1060
1061                 db.AddBookMarkToFile(file_path, bookmark, CBookmark::RESUME);
1062             }
1063             if (playCount.IsEmpty()) {
1064               CVariant data;
1065               data["id"] = updated.GetVideoInfoTag()->m_iDbId;
1066               data["type"] = updated.GetVideoInfoTag()->m_type;
1067               ANNOUNCEMENT::CAnnouncementManager::Announce(ANNOUNCEMENT::VideoLibrary, "xbmc", "OnUpdate", data);
1068             }
1069             updatelisting = true;
1070         }
1071
1072         if (!playCount.IsEmpty()
1073               && playCount.Compare(current_vals["playCount"]) != 0) {
1074
1075             NPT_UInt32 count;
1076             NPT_CHECK_LABEL(playCount.ToInteger32(count), args);
1077             db.SetPlayCount(updated, count);
1078             updatelisting = true;
1079         }
1080
1081         // we must load the changed settings before propagating to local UI
1082         if (updatelisting) {
1083             db.LoadVideoInfo(file_path, tag);
1084             updated.SetFromVideoInfoTag(tag);
1085         }
1086
1087     } else if (updated.IsMusicDb()) {
1088       //TODO implement this
1089
1090     } else {
1091         err = 701;
1092         msg = "No such object";
1093         goto failure;
1094     }
1095
1096     if (updatelisting) {
1097         updated.SetPath(path);
1098         if (updated.IsVideoDb())
1099              CUtil::DeleteVideoDatabaseDirectoryCache();
1100         else if (updated.IsMusicDb())
1101              CUtil::DeleteMusicDatabaseDirectoryCache();
1102
1103         CFileItemPtr msgItem(new CFileItem(updated));
1104         CGUIMessage message(GUI_MSG_NOTIFY_ALL, g_windowManager.GetActiveWindow(), 0, GUI_MSG_UPDATE_ITEM, 1, msgItem);
1105         g_windowManager.SendThreadMessage(message);
1106     }
1107
1108     NPT_CHECK_LABEL(service->PauseEventing(false), error);
1109     return NPT_SUCCESS;
1110
1111 args:
1112     err = 402;
1113     msg = "Invalid args";
1114     goto failure;
1115
1116 error:
1117     err = 501;
1118     msg = "Internal error";
1119
1120 failure:
1121     CLog::Log(LOGERROR, "UPNP: OnUpdateObject failed with err %d:%s", err, msg);
1122     action->SetError(err, msg);
1123     service->PauseEventing(false);
1124     return NPT_FAILURE;
1125 }
1126
1127 /*----------------------------------------------------------------------
1128 |   CUPnPServer::ServeFile
1129 +---------------------------------------------------------------------*/
1130 NPT_Result
1131 CUPnPServer::ServeFile(const NPT_HttpRequest&              request,
1132                        const NPT_HttpRequestContext& context,
1133                        NPT_HttpResponse&             response,
1134                        const NPT_String&             md5)
1135 {
1136     // Translate hash to filename
1137     NPT_String file_path(md5), *file_path2;
1138     { NPT_AutoLock lock(m_FileMutex);
1139       if(NPT_SUCCEEDED(m_FileMap.Get(md5, file_path2))) {
1140         file_path = *file_path2;
1141         CLog::Log(LOGDEBUG, "Received request to serve '%s' = '%s'", (const char*)md5, (const char*)file_path);
1142       } else {
1143         CLog::Log(LOGDEBUG, "Received request to serve unknown md5 '%s'", (const char*)md5);
1144         response.SetStatus(404, "File Not Found");
1145         return NPT_SUCCESS;
1146       }
1147     }
1148
1149     // File requested
1150     NPT_HttpUrl rooturi(context.GetLocalAddress().GetIpAddress().ToString(), context.GetLocalAddress().GetPort(), "/");
1151
1152     if (file_path.Left(8).Compare("stack://", true) == 0) {
1153
1154         NPT_List<NPT_String> files = file_path.SubString(8).Split(" , ");
1155         if (files.GetItemCount() == 0) {
1156             response.SetStatus(404, "File Not Found");
1157             return NPT_SUCCESS;
1158         }
1159
1160         NPT_String output;
1161         output.Reserve(file_path.GetLength()*2);
1162         output += "#EXTM3U\r\n";
1163
1164         NPT_List<NPT_String>::Iterator url = files.GetFirstItem();
1165         for (;url;url++) {
1166             output += "#EXTINF:-1," + URIUtils::GetFileName((const char*)*url);
1167             output += "\r\n";
1168             output += BuildSafeResourceUri(
1169                           rooturi,
1170                           context.GetLocalAddress().GetIpAddress().ToString(),
1171                           *url);
1172             output += "\r\n";
1173         }
1174
1175         PLT_HttpHelper::SetBody(response, (const char*)output, output.GetLength());
1176         response.GetHeaders().SetHeader("Content-Disposition", "inline; filename=\"stack.m3u\"");
1177         return NPT_SUCCESS;
1178     }
1179
1180     if(URIUtils::IsURL((const char*)file_path))
1181     {
1182       CStdString disp = "inline; filename=\"" + URIUtils::GetFileName((const char*)file_path) + "\"";
1183       response.GetHeaders().SetHeader("Content-Disposition", disp.c_str());
1184     }
1185
1186     return PLT_HttpServer::ServeFile(request,
1187                                        context,
1188                                        response,
1189                                        file_path);
1190 }
1191
1192 /*----------------------------------------------------------------------
1193 |   CUPnPServer::SortItems
1194 |
1195 |   Only support upnp: & dc: namespaces for now.
1196 |   Other servers add their own vendor-specific sort methods. This could
1197 |   possibly be handled with 'quirks' in the long run.
1198 |
1199 |   return true if sort criteria was matched
1200 +---------------------------------------------------------------------*/
1201 bool
1202 CUPnPServer::SortItems(CFileItemList& items, const char* sort_criteria)
1203 {
1204   CStdString criteria(sort_criteria);
1205   if (criteria.empty()) {
1206     return false;
1207   }
1208
1209   bool sorted = false;
1210   CStdStringArray tokens = StringUtils::SplitString(criteria, ",");
1211   for (vector<CStdString>::reverse_iterator itr = tokens.rbegin(); itr != tokens.rend(); itr++) {
1212     SortDescription sorting;
1213     /* Platinum guarantees 1st char is - or + */
1214     sorting.sortOrder = StringUtils::StartsWith(*itr, "+") ? SortOrderAscending : SortOrderDescending;
1215     CStdString method = itr->substr(1);
1216
1217     /* resource specific */
1218     if (method.Equals("res@duration"))
1219       sorting.sortBy = SortByTime;
1220     else if (method.Equals("res@size"))
1221       sorting.sortBy = SortBySize;
1222     else if (method.Equals("res@bitrate"))
1223       sorting.sortBy = SortByBitrate;
1224
1225     /* dc: */
1226     else if (method.Equals("dc:date"))
1227       sorting.sortBy = SortByDate;
1228     else if (method.Equals("dc:title"))
1229     {
1230       sorting.sortBy = SortByTitle;
1231       sorting.sortAttributes = SortAttributeIgnoreArticle;
1232     }
1233
1234     /* upnp: */
1235     else if (method.Equals("upnp:album"))
1236       sorting.sortBy = SortByAlbum;
1237     else if (method.Equals("upnp:artist") || method.Equals("upnp:albumArtist"))
1238       sorting.sortBy = SortByArtist;
1239     else if (method.Equals("upnp:episodeNumber"))
1240       sorting.sortBy = SortByEpisodeNumber;
1241     else if (method.Equals("upnp:genre"))
1242       sorting.sortBy = SortByGenre;
1243     else if (method.Equals("upnp:originalTrackNumber"))
1244       sorting.sortBy = SortByTrackNumber;
1245     else if(method.Equals("upnp:rating"))
1246       sorting.sortBy = SortByRating;
1247     else {
1248       CLog::Log(LOGINFO, "UPnP: unsupported sort criteria '%s' passed", method.c_str());
1249       continue; // needed so unidentified sort methods don't re-sort by label
1250     }
1251
1252     CLog::Log(LOGINFO, "UPnP: Sorting by method %d, order %d, attributes %d", sorting.sortBy, sorting.sortOrder, sorting.sortAttributes);
1253     items.Sort(sorting);
1254     sorted = true;
1255   }
1256
1257   return sorted;
1258 }
1259
1260 void
1261 CUPnPServer::DefaultSortItems(CFileItemList& items)
1262 {
1263   CGUIViewState* viewState = CGUIViewState::GetViewState(items.IsVideoDb() ? WINDOW_VIDEO_NAV : -1, items);
1264   if (viewState)
1265   {
1266     SortDescription sorting = viewState->GetSortMethod();
1267     items.Sort(sorting.sortBy, sorting.sortOrder, sorting.sortAttributes);
1268     delete viewState;
1269   }
1270 }
1271
1272 } /* namespace UPNP */
1273