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