2 * Copyright (C) 2012-2013 Team XBMC
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)
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.
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/>.
20 #include "UPnPServer.h"
21 #include "UPnPInternal.h"
22 #include "Application.h"
23 #include "view/GUIViewState.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"
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"
49 using namespace ANNOUNCEMENT;
50 using namespace XFILE;
55 NPT_UInt32 CUPnPServer::m_MaxReturnedItems = 0;
57 const char* audio_containers[] = { "musicdb://genres/", "musicdb://artists/", "musicdb://albums/",
58 "musicdb://songs/", "musicdb://recentlyaddedalbums/", "musicdb://years/",
59 "musicdb://singles/" };
61 const char* video_containers[] = { "library://video/movies/titles.xml/", "library://video/tvshows/titles.xml/",
62 "videodb://recentlyaddedmovies/", "videodb://recentlyaddedepisodes/" };
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())
74 CUPnPServer::~CUPnPServer()
76 ANNOUNCEMENT::CAnnouncementManager::RemoveAnnouncer(this);
79 /*----------------------------------------------------------------------
80 | CUPnPServer::ProcessGetSCPD
81 +---------------------------------------------------------------------*/
83 CUPnPServer::ProcessGetSCPD(PLT_Service* service,
84 NPT_HttpRequest& request,
85 const NPT_HttpRequestContext& context,
86 NPT_HttpResponse& response)
88 // needed because PLT_MediaConnect only allows Xbox360 & WMP to search
89 return PLT_MediaServer::ProcessGetSCPD(service, request, context, response);
92 /*----------------------------------------------------------------------
93 | CUPnPServer::SetupServices
94 +---------------------------------------------------------------------*/
96 CUPnPServer::SetupServices()
98 PLT_MediaConnect::SetupServices();
99 PLT_Service* service = NULL;
100 NPT_Result result = FindServiceById("urn:upnp-org:serviceId:ContentDirectory", 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");
105 OnScanCompleted(AudioLibrary);
107 OnScanCompleted(VideoLibrary);
109 // now safe to start passing on new notifications
110 ANNOUNCEMENT::CAnnouncementManager::AddAnnouncer(this);
115 /*----------------------------------------------------------------------
116 | CUPnPServer::OnScanCompleted
117 +---------------------------------------------------------------------*/
119 CUPnPServer::OnScanCompleted(int type)
121 if (type == AudioLibrary) {
122 for (size_t i = 0; i < sizeof(audio_containers)/sizeof(audio_containers[0]); i++)
123 UpdateContainer(audio_containers[i]);
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]);
135 /*----------------------------------------------------------------------
136 | CUPnPServer::UpdateContainer
137 +---------------------------------------------------------------------*/
139 CUPnPServer::UpdateContainer(const string& id)
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);
149 /*----------------------------------------------------------------------
150 | CUPnPServer::PropagateUpdates
151 +---------------------------------------------------------------------*/
153 CUPnPServer::PropagateUpdates()
155 PLT_Service* service = NULL;
156 NPT_String current_ids;
158 map<string,pair<bool, unsigned long> >::iterator itr;
160 if (m_scanning || !CSettings::Get().GetBool("services.upnpannounce"))
163 NPT_CHECK_LABEL(FindServiceById("urn:upnp-org:serviceId:ContentDirectory", service), failed);
165 // we pause, and we must retain any changes which have not been
167 NPT_CHECK_LABEL(service->PauseEventing(), failed);
168 NPT_CHECK_LABEL(service->GetStateVariableValue("ContainerUpdateIDs", current_ids), failed);
169 buffer = (const char*)current_ids;
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;
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);
185 service->PauseEventing(false);
189 // should attempt to start eventing on a failure
190 if (service) service->PauseEventing(false);
191 CLog::Log(LOGERROR, "UPNP: Unable to propagate updates");
194 /*----------------------------------------------------------------------
195 | CUPnPServer::SetupIcons
196 +---------------------------------------------------------------------*/
198 CUPnPServer::SetupIcons()
200 NPT_String file_root = CSpecialProtocol::TranslatePath("special://xbmc/media/").c_str();
202 PLT_DeviceIcon("image/png", 256, 256, 8, "/icon256x256.png"),
205 PLT_DeviceIcon("image/png", 120, 120, 8, "/icon120x120.png"),
208 PLT_DeviceIcon("image/png", 48, 48, 8, "/icon48x48.png"),
211 PLT_DeviceIcon("image/png", 32, 32, 8, "/icon32x32.png"),
214 PLT_DeviceIcon("image/png", 16, 16, 8, "/icon16x16.png"),
219 /*----------------------------------------------------------------------
220 | CUPnPServer::BuildSafeResourceUri
221 +---------------------------------------------------------------------*/
222 NPT_String CUPnPServer::BuildSafeResourceUri(const NPT_HttpUrl &rooturi,
224 const char* file_path)
228 XBMC::XBMC_MD5 md5state;
230 // determine the filename to provide context to md5'd urls
232 if (url.GetProtocol() == "image")
233 filename = URIUtils::GetFileName(url.GetHostName());
235 filename = URIUtils::GetFileName(file_path);
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));
244 return PLT_FileMediaServer::BuildSafeResourceUri(rooturi, host, md5.c_str());
247 /*----------------------------------------------------------------------
249 +---------------------------------------------------------------------*/
251 CUPnPServer::Build(CFileItemPtr item,
253 const PLT_HttpRequestContext& context,
254 NPT_Reference<CThumbLoader>& thumb_loader,
255 const char* parent_id /* = NULL */)
257 PLT_MediaObject* object = NULL;
258 NPT_String path = item->GetPath().c_str();
260 //HACK: temporary disabling count as it thrashes HDD
263 CLog::Log(LOGDEBUG, "Preparing upnp object for item '%s'", (const char*)path);
265 if (path == "virtualpath://upnproot") {
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;
274 object->m_ObjectID = "0";
275 object->m_ParentID = "-1";
276 // root has 5 children
278 ((PLT_MediaContainer*)object)->m_ChildrenCount = 5;
286 NPT_String file_path, share_name;
287 file_path = item->GetPath();
290 if (path.StartsWith("musicdb://")) {
291 if (path == "musicdb://" ) {
292 item->SetLabel("Music Library");
293 item->SetLabelPreformated(true);
295 if (!item->HasMusicInfoTag()) {
296 MUSICDATABASEDIRECTORY::CQueryParams params;
297 MUSICDATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo((const char*)path, params);
300 if (!db.Open() ) return NULL;
302 if (params.GetSongId() >= 0 ) {
304 if (db.GetSong(params.GetSongId(), song))
305 item->GetMusicInfoTag()->SetSong(song);
307 else if (params.GetAlbumId() >= 0 ) {
309 if (db.GetAlbum(params.GetAlbumId(), album, false))
310 item->GetMusicInfoTag()->SetAlbum(album);
312 else if (params.GetArtistId() >= 0 ) {
314 if (db.GetArtist(params.GetArtistId(), artist, false))
315 item->GetMusicInfoTag()->SetArtist(artist);
320 if (item->GetLabel().empty()) {
321 /* if no label try to grab it from node type */
323 if (CMusicDatabaseDirectory::GetLabel((const char*)path, label)) {
324 item->SetLabel(label);
325 item->SetLabelPreformated(true);
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);
334 if (!item->HasVideoInfoTag()) {
335 VIDEODATABASEDIRECTORY::CQueryParams params;
336 VIDEODATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo((const char*)path, params);
339 if (!db.Open() ) return NULL;
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());
351 if (item->GetVideoInfoTag()->m_type == "tvshow" || item->GetVideoInfoTag()->m_type == "season") {
352 // for tvshows and seasons, iEpisode and playCount are
354 item->GetVideoInfoTag()->m_iEpisode = (int)item->GetProperty("totalepisodes").asInteger();
355 item->GetVideoInfoTag()->m_playCount = (int)item->GetProperty("watchedepisodes").asInteger();
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);
364 // try to grab it from the folder
365 if (item->GetLabel().empty()) {
367 if (CVideoDatabaseDirectory::GetLabel((const char*)path, label)) {
368 item->SetLabel(label);
369 item->SetLabelPreformated(true);
375 // not a virtual path directory, new system
376 object = BuildObject(*item.get(), file_path, with_count, thumb_loader, &context, this);
378 // set parent id if passed, otherwise it should have been determined
379 if (object && parent_id) {
380 object->m_ParentID = parent_id;
385 // remap Root virtualpath://upnproot/ to id "0"
386 if (object->m_ObjectID == "virtualpath://upnproot/")
387 object->m_ObjectID = "0";
389 // remap Parent Root virtualpath://upnproot/ to id "0"
390 if (object->m_ParentID == "virtualpath://upnproot/")
391 object->m_ParentID = "0";
401 /*----------------------------------------------------------------------
402 | CUPnPServer::Announce
403 +---------------------------------------------------------------------*/
405 CUPnPServer::Announce(AnnouncementFlag flag, const char *sender, const char *message, const CVariant &data)
411 if (strcmp(sender, "xbmc"))
414 if (strcmp(message, "OnUpdate") && strcmp(message, "OnRemove")
415 && strcmp(message, "OnScanStarted") && strcmp(message, "OnScanFinished"))
419 if (!strcmp(message, "OnScanStarted") || !strcmp(message, "OnCleanStarted")) {
422 else if (!strcmp(message, "OnScanFinished") || !strcmp(message, "OnCleanFinished")) {
423 OnScanCompleted(flag);
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();
433 item_id = (int)data["id"].asInteger();
434 item_type = data["type"].asString();
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") {
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/");
449 else if(item_type == "tvshow") {
450 UpdateContainer("library://video/tvshows/titles.xml/");
451 UpdateContainer("videodb://recentlyaddedepisodes/");
453 else if(item_type == "movie") {
454 UpdateContainer("library://video/movies/titles.xml/");
455 UpdateContainer("videodb://recentlyaddedmovies/");
457 else if(item_type == "musicvideo") {
458 UpdateContainer("library://video/musicvideos/titles.xml/");
459 UpdateContainer("videodb://recentlyaddedmusicvideos/");
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
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/");
477 /*----------------------------------------------------------------------
478 | TranslateWMPObjectId
479 +---------------------------------------------------------------------*/
480 static NPT_String TranslateWMPObjectId(NPT_String id)
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/";
500 CLog::Log(LOGDEBUG, "UPnP Translated id to '%s'", (const char*)id);
505 ObjectIDValidate(const NPT_String& id)
507 if (CFileUtils::RemoteAccessAllowed(id.GetChars()))
509 return NPT_ERROR_NO_SUCH_FILE;
512 /*----------------------------------------------------------------------
513 | CUPnPServer::OnBrowseMetadata
514 +---------------------------------------------------------------------*/
516 CUPnPServer::OnBrowseMetadata(PLT_ActionReference& action,
517 const char* object_id,
519 NPT_UInt32 starting_index,
520 NPT_UInt32 requested_count,
521 const char* sort_criteria,
522 const PLT_HttpRequestContext& context)
524 NPT_COMPILER_UNUSED(sort_criteria);
525 NPT_COMPILER_UNUSED(requested_count);
526 NPT_COMPILER_UNUSED(starting_index);
529 NPT_Reference<PLT_MediaObject> object;
530 NPT_String id = TranslateWMPObjectId(object_id);
532 NPT_Reference<CThumbLoader> thumb_loader;
534 CLog::Log(LOGINFO, "Received UPnP Browse Metadata request for object '%s'", (const char*)object_id);
536 if(NPT_FAILED(ObjectIDValidate(id))) {
537 action->SetError(701, "Incorrect ObjectID.");
541 if (id.StartsWith("virtualpath://")) {
543 if (id == "virtualpath://upnproot") {
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";
554 // determine if it's a container by calling CDirectory::Exists
555 item.reset(new CFileItem((const char*)id, CDirectory::Exists((const char*)id)));
557 // attempt to determine the parent of this item
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)) {
565 // non-library objects - playlists / sources
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
580 if (item->IsVideoDb()) {
581 thumb_loader = NPT_Reference<CThumbLoader>(new CVideoThumbLoader());
583 else if (item->IsMusicDb()) {
584 thumb_loader = NPT_Reference<CThumbLoader>(new CMusicThumbLoader());
586 if (!thumb_loader.IsNull()) {
587 thumb_loader->OnLoaderStart();
589 object = Build(item, true, context, thumb_loader, parent.empty()?NULL:parent.c_str());
592 if (object.IsNull()) {
594 NPT_LOG_WARNING_1("CUPnPServer::OnBrowseMetadata - Object null (%s)", object_id);
595 action->SetError(701, "No Such Object.");
600 NPT_CHECK(PLT_Didl::ToDidl(*object.AsPointer(), filter, tmp));
602 /* add didl header and footer */
603 didl = didl_header + tmp + didl_footer;
605 NPT_CHECK(action->SetArgumentValue("Result", didl));
606 NPT_CHECK(action->SetArgumentValue("NumberReturned", "1"));
607 NPT_CHECK(action->SetArgumentValue("TotalMatches", "1"));
609 // update ID may be wrong here, it should be the one of the container?
610 NPT_CHECK(action->SetArgumentValue("UpdateId", "0"));
612 // TODO: We need to keep track of the overall SystemUpdateID of the CDS
617 /*----------------------------------------------------------------------
618 | CUPnPServer::OnBrowseDirectChildren
619 +---------------------------------------------------------------------*/
621 CUPnPServer::OnBrowseDirectChildren(PLT_ActionReference& action,
622 const char* object_id,
624 NPT_UInt32 starting_index,
625 NPT_UInt32 requested_count,
626 const char* sort_criteria,
627 const PLT_HttpRequestContext& context)
630 NPT_String parent_id = TranslateWMPObjectId(object_id);
632 CLog::Log(LOGINFO, "UPnP: Received Browse DirectChildren request for object '%s', with sort criteria %s", object_id, sort_criteria);
634 if(NPT_FAILED(ObjectIDValidate(parent_id))) {
635 action->SetError(701, "Incorrect ObjectID.");
639 items.SetPath(CStdString(parent_id));
641 // guard against loading while saving to the same cache file
642 // as CArchive currently performs no locking itself
644 { NPT_AutoLock lock(m_CacheMutex);
649 // cache anything that takes more than a second to retrieve
650 unsigned int time = XbmcThreads::SystemClockMillis();
652 if (parent_id.StartsWith("virtualpath://upnproot")) {
656 item.reset(new CFileItem("musicdb://", true));
657 item->SetLabel("Music Library");
658 item->SetLabelPreformated(true);
662 item.reset(new CFileItem("library://video/", true));
663 item->SetLabel("Video Library");
664 item->SetLabelPreformated(true);
667 items.Sort(SortByLabel, SortOrderAscending);
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
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);
680 if (items.CacheToDiscAlways() || (items.CacheToDiscIfSlow() && (XbmcThreads::SystemClockMillis() - time) > 1000 )) {
681 NPT_AutoLock lock(m_CacheMutex);
686 // as there's no library://music support, manually add playlists and music
688 if (items.GetPath() == "musicdb://") {
689 CFileItemPtr playlists(new CFileItem("special://musicplaylists/", true));
690 playlists->SetLabel(g_localizeStrings.Get(136));
691 items.Add(playlists);
693 CVideoDatabase database;
695 if (database.HasContent(VIDEODB_CONTENT_MUSICVIDEOS)) {
696 CFileItemPtr mvideos(new CFileItem("library://video/musicvideos/", true));
697 mvideos->SetLabel(g_localizeStrings.Get(20389));
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
705 NPT_String action_name = action->GetActionDesc().GetName();
706 return BuildResponse(
714 (action_name.Compare("Search", true)==0)?NULL:parent_id.GetChars());
717 /*----------------------------------------------------------------------
718 | CUPnPServer::BuildResponse
719 +---------------------------------------------------------------------*/
721 CUPnPServer::BuildResponse(PLT_ActionReference& action,
722 CFileItemList& items,
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 */)
730 NPT_COMPILER_UNUSED(sort_criteria);
732 CLog::Log(LOGDEBUG, "Building UPnP response with filter '%s', starting @ %d with %d requested",
737 // we will reuse this ThumbLoader for all items
738 NPT_Reference<CThumbLoader> thumb_loader;
740 if (URIUtils::IsVideoDb(items.GetPath()) ||
741 StringUtils::StartsWithNoCase(items.GetPath(), "library://video/") ||
742 StringUtils::StartsWithNoCase(items.GetPath(), "special://profile/playlists/video/")) {
744 thumb_loader = NPT_Reference<CThumbLoader>(new CVideoThumbLoader());
746 else if (URIUtils::IsMusicDb(items.GetPath()) ||
747 StringUtils::StartsWithNoCase(items.GetPath(), "special://profile/playlists/music/")) {
749 thumb_loader = NPT_Reference<CThumbLoader>(new CMusicThumbLoader());
751 if (!thumb_loader.IsNull()) {
752 thumb_loader->OnLoaderStart();
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/"))
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
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
782 NPT_CHECK(PLT_Didl::ToDidl(*object.AsPointer(), filter, tmp));
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);
794 CLog::Log(LOGDEBUG, "Returning UPnP response with %d items out of %d total matches",
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"));
805 /*----------------------------------------------------------------------
807 +---------------------------------------------------------------------*/
810 FindSubCriteria(NPT_String criteria, const char* name)
813 int search = criteria.Find(name);
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);
828 /*----------------------------------------------------------------------
829 | CUPnPServer::OnSearchContainer
830 +---------------------------------------------------------------------*/
832 CUPnPServer::OnSearchContainer(PLT_ActionReference& action,
833 const char* object_id,
834 const char* search_criteria,
836 NPT_UInt32 starting_index,
837 NPT_UInt32 requested_count,
838 const char* sort_criteria,
839 const PLT_HttpRequestContext& context)
841 CLog::Log(LOGDEBUG, "Received Search request for object '%s' with search '%s'",
842 (const char*)object_id,
843 (const char*)search_criteria);
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;
855 if (id.StartsWith("musicdb://genres/")) {
856 // all tracks of all genres
859 // all tracks of a specific genre
862 // all tracks of a specific genre of a specfic artist
865 } else if (id.StartsWith("musicdb://artists/")) {
866 // all tracks by all artists
869 // all tracks of a specific artist
872 } else if (id.StartsWith("musicdb://albums/")) {
874 if (count == 1) id += "-1/";
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");
888 CMusicDatabase database;
891 if (genre.GetLength() > 0) {
892 // all tracks by genre filtered by artist and/or album
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
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
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
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));
913 return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index, requested_count, sort_criteria, context);
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");
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");
929 CMusicDatabase database;
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);
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;
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);
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;
962 CVideoDatabase database;
963 if (!database.Open()) {
964 action->SetError(800, "Internal Error");
968 if (!database.GetMoviesNav("videodb://movies/titles/", items)) {
969 action->SetError(800, "Internal Error");
972 itemsall.Append(items);
975 if (!database.GetEpisodesByWhere("videodb://tvshows/titles/", "", items)) {
976 action->SetError(800, "Internal Error");
979 itemsall.Append(items);
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) {
985 return BuildResponse(action, items, filter, starting_index, requested_count, sort_criteria, context, NULL);;
991 /*----------------------------------------------------------------------
992 | CUPnPServer::OnUpdateObject
993 +---------------------------------------------------------------------*/
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)
1001 CStdString path(CURL::Decode(object_id));
1003 updated.SetPath(path);
1004 CLog::Log(LOGINFO, "UPnP: OnUpdateObject: %s from %s", path.c_str(),
1005 (const char*) context.GetRemoteAddress().GetIpAddress().ToString());
1007 NPT_String playCount, position;
1009 const char* msg = NULL;
1010 bool updatelisting(false);
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);
1017 if (updated.IsVideoDb()) {
1019 NPT_CHECK_LABEL(!db.Open(), error);
1021 // must first determine type of file from object id
1022 VIDEODATABASEDIRECTORY::CQueryParams params;
1023 VIDEODATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo(path.c_str(), params);
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;
1035 msg = "No such object";
1039 CStdString file_path;
1040 db.GetFilePathById(id, file_path, content_type);
1042 db.LoadVideoInfo(file_path, tag);
1043 updated.SetFromVideoInfoTag(tag);
1044 CLog::Log(LOGINFO, "UPNP: Translated to %s", file_path.c_str());
1046 position = new_vals["lastPlaybackPosition"];
1047 playCount = new_vals["playCount"];
1049 if (!position.IsEmpty()
1050 && position.Compare(current_vals["lastPlaybackPosition"]) != 0) {
1052 NPT_CHECK_LABEL(position.ToInteger32(resume), args);
1055 db.ClearBookMarksOfFile(file_path, CBookmark::RESUME);
1058 bookmark.timeInSeconds = resume;
1059 bookmark.totalTimeInSeconds = resume + 100; // not required to be correct
1061 db.AddBookMarkToFile(file_path, bookmark, CBookmark::RESUME);
1063 if (playCount.IsEmpty()) {
1065 data["id"] = updated.GetVideoInfoTag()->m_iDbId;
1066 data["type"] = updated.GetVideoInfoTag()->m_type;
1067 ANNOUNCEMENT::CAnnouncementManager::Announce(ANNOUNCEMENT::VideoLibrary, "xbmc", "OnUpdate", data);
1069 updatelisting = true;
1072 if (!playCount.IsEmpty()
1073 && playCount.Compare(current_vals["playCount"]) != 0) {
1076 NPT_CHECK_LABEL(playCount.ToInteger32(count), args);
1077 db.SetPlayCount(updated, count);
1078 updatelisting = true;
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);
1087 } else if (updated.IsMusicDb()) {
1088 //TODO implement this
1092 msg = "No such object";
1096 if (updatelisting) {
1097 updated.SetPath(path);
1098 if (updated.IsVideoDb())
1099 CUtil::DeleteVideoDatabaseDirectoryCache();
1100 else if (updated.IsMusicDb())
1101 CUtil::DeleteMusicDatabaseDirectoryCache();
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);
1108 NPT_CHECK_LABEL(service->PauseEventing(false), error);
1113 msg = "Invalid args";
1118 msg = "Internal error";
1121 CLog::Log(LOGERROR, "UPNP: OnUpdateObject failed with err %d:%s", err, msg);
1122 action->SetError(err, msg);
1123 service->PauseEventing(false);
1127 /*----------------------------------------------------------------------
1128 | CUPnPServer::ServeFile
1129 +---------------------------------------------------------------------*/
1131 CUPnPServer::ServeFile(const NPT_HttpRequest& request,
1132 const NPT_HttpRequestContext& context,
1133 NPT_HttpResponse& response,
1134 const NPT_String& md5)
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);
1143 CLog::Log(LOGDEBUG, "Received request to serve unknown md5 '%s'", (const char*)md5);
1144 response.SetStatus(404, "File Not Found");
1150 NPT_HttpUrl rooturi(context.GetLocalAddress().GetIpAddress().ToString(), context.GetLocalAddress().GetPort(), "/");
1152 if (file_path.Left(8).Compare("stack://", true) == 0) {
1154 NPT_List<NPT_String> files = file_path.SubString(8).Split(" , ");
1155 if (files.GetItemCount() == 0) {
1156 response.SetStatus(404, "File Not Found");
1161 output.Reserve(file_path.GetLength()*2);
1162 output += "#EXTM3U\r\n";
1164 NPT_List<NPT_String>::Iterator url = files.GetFirstItem();
1166 output += "#EXTINF:-1," + URIUtils::GetFileName((const char*)*url);
1168 output += BuildSafeResourceUri(
1170 context.GetLocalAddress().GetIpAddress().ToString(),
1175 PLT_HttpHelper::SetBody(response, (const char*)output, output.GetLength());
1176 response.GetHeaders().SetHeader("Content-Disposition", "inline; filename=\"stack.m3u\"");
1180 if(URIUtils::IsURL((const char*)file_path))
1182 CStdString disp = "inline; filename=\"" + URIUtils::GetFileName((const char*)file_path) + "\"";
1183 response.GetHeaders().SetHeader("Content-Disposition", disp.c_str());
1186 return PLT_HttpServer::ServeFile(request,
1192 /*----------------------------------------------------------------------
1193 | CUPnPServer::SortItems
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.
1199 | return true if sort criteria was matched
1200 +---------------------------------------------------------------------*/
1202 CUPnPServer::SortItems(CFileItemList& items, const char* sort_criteria)
1204 CStdString criteria(sort_criteria);
1205 if (criteria.empty()) {
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);
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;
1226 else if (method.Equals("dc:date"))
1227 sorting.sortBy = SortByDate;
1228 else if (method.Equals("dc:title"))
1230 sorting.sortBy = SortByTitle;
1231 sorting.sortAttributes = SortAttributeIgnoreArticle;
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;
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
1252 CLog::Log(LOGINFO, "UPnP: Sorting by method %d, order %d, attributes %d", sorting.sortBy, sorting.sortOrder, sorting.sortAttributes);
1253 items.Sort(sorting);
1261 CUPnPServer::DefaultSortItems(CFileItemList& items)
1263 CGUIViewState* viewState = CGUIViewState::GetViewState(items.IsVideoDb() ? WINDOW_VIDEO_NAV : -1, items);
1266 SortDescription sorting = viewState->GetSortMethod();
1267 items.Sort(sorting.sortBy, sorting.sortOrder, sorting.sortAttributes);
1272 } /* namespace UPNP */