1 #include "UPnPServer.h"
2 #include "UPnPInternal.h"
3 #include "Application.h"
4 #include "view/GUIViewState.h"
6 #include "video/VideoThumbLoader.h"
7 #include "music/Artist.h"
8 #include "music/MusicThumbLoader.h"
9 #include "interfaces/AnnouncementManager.h"
10 #include "filesystem/Directory.h"
11 #include "filesystem/MusicDatabaseDirectory.h"
12 #include "filesystem/SpecialProtocol.h"
13 #include "filesystem/VideoDatabaseDirectory.h"
14 #include "guilib/WindowIDs.h"
15 #include "music/tags/MusicInfoTag.h"
16 #include "settings/AdvancedSettings.h"
17 #include "settings/Settings.h"
18 #include "utils/log.h"
19 #include "utils/md5.h"
20 #include "utils/StringUtils.h"
21 #include "utils/URIUtils.h"
23 #include "music/MusicDatabase.h"
24 #include "video/VideoDatabase.h"
25 #include "guilib/GUIWindowManager.h"
26 #include "xbmc/GUIUserMessages.h"
27 #include "utils/FileUtils.h"
30 using namespace ANNOUNCEMENT;
31 using namespace XFILE;
36 NPT_UInt32 CUPnPServer::m_MaxReturnedItems = 0;
38 const char* audio_containers[] = { "musicdb://genres/", "musicdb://artists/", "musicdb://albums/",
39 "musicdb://songs/", "musicdb://recentlyaddedalbums/", "musicdb://years/",
40 "musicdb://singles/" };
42 const char* video_containers[] = { "library://video/movies/titles.xml/", "library://video/tvshows/titles.xml/",
43 "videodb://recentlyaddedmovies/", "videodb://recentlyaddedepisodes/" };
45 /*----------------------------------------------------------------------
46 | CUPnPServer::CUPnPServer
47 +---------------------------------------------------------------------*/
48 CUPnPServer::CUPnPServer(const char* friendly_name, const char* uuid /*= NULL*/, int port /*= 0*/) :
49 PLT_MediaConnect(friendly_name, false, uuid, port),
50 PLT_FileMediaConnectDelegate("/", "/"),
51 m_scanning(g_application.IsMusicScanning() || g_application.IsVideoScanning())
55 CUPnPServer::~CUPnPServer()
57 ANNOUNCEMENT::CAnnouncementManager::RemoveAnnouncer(this);
60 /*----------------------------------------------------------------------
61 | CUPnPServer::ProcessGetSCPD
62 +---------------------------------------------------------------------*/
64 CUPnPServer::ProcessGetSCPD(PLT_Service* service,
65 NPT_HttpRequest& request,
66 const NPT_HttpRequestContext& context,
67 NPT_HttpResponse& response)
69 // needed because PLT_MediaConnect only allows Xbox360 & WMP to search
70 return PLT_MediaServer::ProcessGetSCPD(service, request, context, response);
73 /*----------------------------------------------------------------------
74 | CUPnPServer::SetupServices
75 +---------------------------------------------------------------------*/
77 CUPnPServer::SetupServices()
79 PLT_MediaConnect::SetupServices();
80 PLT_Service* service = NULL;
81 NPT_Result result = FindServiceById("urn:upnp-org:serviceId:ContentDirectory", service);
83 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");
86 OnScanCompleted(AudioLibrary);
88 OnScanCompleted(VideoLibrary);
90 // now safe to start passing on new notifications
91 ANNOUNCEMENT::CAnnouncementManager::AddAnnouncer(this);
96 /*----------------------------------------------------------------------
97 | CUPnPServer::OnScanCompleted
98 +---------------------------------------------------------------------*/
100 CUPnPServer::OnScanCompleted(int type)
102 if (type == AudioLibrary) {
103 for (size_t i = 0; i < sizeof(audio_containers)/sizeof(audio_containers[0]); i++)
104 UpdateContainer(audio_containers[i]);
106 else if (type == VideoLibrary) {
107 for (size_t i = 0; i < sizeof(video_containers)/sizeof(video_containers[0]); i++)
108 UpdateContainer(video_containers[i]);
116 /*----------------------------------------------------------------------
117 | CUPnPServer::UpdateContainer
118 +---------------------------------------------------------------------*/
120 CUPnPServer::UpdateContainer(const string& id)
122 map<string,pair<bool, unsigned long> >::iterator itr = m_UpdateIDs.find(id);
123 unsigned long count = 0;
124 if (itr != m_UpdateIDs.end())
125 count = ++itr->second.second;
126 m_UpdateIDs[id] = make_pair(true, count);
130 /*----------------------------------------------------------------------
131 | CUPnPServer::PropagateUpdates
132 +---------------------------------------------------------------------*/
134 CUPnPServer::PropagateUpdates()
136 PLT_Service* service = NULL;
137 NPT_String current_ids;
139 map<string,pair<bool, unsigned long> >::iterator itr;
141 if (m_scanning || !CSettings::Get().GetBool("services.upnpannounce"))
144 NPT_CHECK_LABEL(FindServiceById("urn:upnp-org:serviceId:ContentDirectory", service), failed);
146 // we pause, and we must retain any changes which have not been
148 NPT_CHECK_LABEL(service->PauseEventing(), failed);
149 NPT_CHECK_LABEL(service->GetStateVariableValue("ContainerUpdateIDs", current_ids), failed);
150 buffer = (const char*)current_ids;
154 // only broadcast ids with modified bit set
155 for (itr = m_UpdateIDs.begin(); itr != m_UpdateIDs.end(); ++itr) {
156 if (itr->second.first) {
157 buffer.append(StringUtils::Format("%s,%ld,", itr->first.c_str(), itr->second.second).c_str());
158 itr->second.first = false;
162 // set the value, Platinum will clear ContainerUpdateIDs after sending
163 NPT_CHECK_LABEL(service->SetStateVariable("ContainerUpdateIDs", buffer.substr(0,buffer.size()-1).c_str(), true), failed);
164 NPT_CHECK_LABEL(service->IncStateVariable("SystemUpdateID"), failed);
166 service->PauseEventing(false);
170 // should attempt to start eventing on a failure
171 if (service) service->PauseEventing(false);
172 CLog::Log(LOGERROR, "UPNP: Unable to propagate updates");
175 /*----------------------------------------------------------------------
176 | CUPnPServer::SetupIcons
177 +---------------------------------------------------------------------*/
179 CUPnPServer::SetupIcons()
181 NPT_String file_root = CSpecialProtocol::TranslatePath("special://xbmc/media/").c_str();
183 PLT_DeviceIcon("image/png", 256, 256, 24, "/icon-flat-256x256.png"),
186 PLT_DeviceIcon("image/png", 120, 120, 24, "/icon-flat-120x120.png"),
191 /*----------------------------------------------------------------------
192 | CUPnPServer::BuildSafeResourceUri
193 +---------------------------------------------------------------------*/
194 NPT_String CUPnPServer::BuildSafeResourceUri(const NPT_HttpUrl &rooturi,
196 const char* file_path)
200 XBMC::XBMC_MD5 md5state;
202 // determine the filename to provide context to md5'd urls
204 if (url.GetProtocol() == "image")
205 filename = URIUtils::GetFileName(url.GetHostName());
207 filename = URIUtils::GetFileName(file_path);
209 CURL::Encode(filename);
210 md5state.append(file_path);
211 md5state.getDigest(md5);
212 md5 += "/" + filename;
213 { NPT_AutoLock lock(m_FileMutex);
214 NPT_CHECK(m_FileMap.Put(md5.c_str(), file_path));
216 return PLT_FileMediaServer::BuildSafeResourceUri(rooturi, host, md5.c_str());
219 /*----------------------------------------------------------------------
221 +---------------------------------------------------------------------*/
223 CUPnPServer::Build(CFileItemPtr item,
225 const PLT_HttpRequestContext& context,
226 NPT_Reference<CThumbLoader>& thumb_loader,
227 const char* parent_id /* = NULL */)
229 PLT_MediaObject* object = NULL;
230 NPT_String path = item->GetPath().c_str();
232 //HACK: temporary disabling count as it thrashes HDD
235 CLog::Log(LOGDEBUG, "Preparing upnp object for item '%s'", (const char*)path);
237 if (path == "virtualpath://upnproot") {
239 if (path.StartsWith("virtualpath://")) {
240 object = new PLT_MediaContainer;
241 object->m_Title = item->GetLabel();
242 object->m_ObjectClass.type = "object.container";
243 object->m_ObjectID = path;
246 object->m_ObjectID = "0";
247 object->m_ParentID = "-1";
248 // root has 5 children
250 ((PLT_MediaContainer*)object)->m_ChildrenCount = 5;
258 NPT_String file_path, share_name;
259 file_path = item->GetPath();
262 if (path.StartsWith("musicdb://")) {
263 if (path == "musicdb://" ) {
264 item->SetLabel("Music Library");
265 item->SetLabelPreformated(true);
267 if (!item->HasMusicInfoTag()) {
268 MUSICDATABASEDIRECTORY::CQueryParams params;
269 MUSICDATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo((const char*)path, params);
272 if (!db.Open() ) return NULL;
274 if (params.GetSongId() >= 0 ) {
276 if (db.GetSong(params.GetSongId(), song))
277 item->GetMusicInfoTag()->SetSong(song);
279 else if (params.GetAlbumId() >= 0 ) {
281 if (db.GetAlbumInfo(params.GetAlbumId(), album, NULL))
282 item->GetMusicInfoTag()->SetAlbum(album);
284 else if (params.GetArtistId() >= 0 ) {
286 if (db.GetArtistInfo(params.GetArtistId(), artist, false))
287 item->GetMusicInfoTag()->SetArtist(artist);
292 if (item->GetLabel().IsEmpty()) {
293 /* if no label try to grab it from node type */
295 if (CMusicDatabaseDirectory::GetLabel((const char*)path, label)) {
296 item->SetLabel(label);
297 item->SetLabelPreformated(true);
301 } else if (file_path.StartsWith("library://") || file_path.StartsWith("videodb://")) {
302 if (path == "library://video" ) {
303 item->SetLabel("Video Library");
304 item->SetLabelPreformated(true);
306 if (!item->HasVideoInfoTag()) {
307 VIDEODATABASEDIRECTORY::CQueryParams params;
308 VIDEODATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo((const char*)path, params);
311 if (!db.Open() ) return NULL;
313 if (params.GetMovieId() >= 0 )
314 db.GetMovieInfo((const char*)path, *item->GetVideoInfoTag(), params.GetMovieId());
315 else if (params.GetMVideoId() >= 0 )
316 db.GetMusicVideoInfo((const char*)path, *item->GetVideoInfoTag(), params.GetMVideoId());
317 else if (params.GetEpisodeId() >= 0 )
318 db.GetEpisodeInfo((const char*)path, *item->GetVideoInfoTag(), params.GetEpisodeId());
319 else if (params.GetTvShowId() >= 0 )
320 db.GetTvShowInfo((const char*)path, *item->GetVideoInfoTag(), params.GetTvShowId());
323 if (item->GetVideoInfoTag()->m_type == "tvshow" || item->GetVideoInfoTag()->m_type == "season") {
324 // for tvshows and seasons, iEpisode and playCount are
326 item->GetVideoInfoTag()->m_iEpisode = (int)item->GetProperty("totalepisodes").asInteger();
327 item->GetVideoInfoTag()->m_playCount = (int)item->GetProperty("watchedepisodes").asInteger();
330 // try to grab title from tag
331 if (item->HasVideoInfoTag() && !item->GetVideoInfoTag()->m_strTitle.IsEmpty()) {
332 item->SetLabel(item->GetVideoInfoTag()->m_strTitle);
333 item->SetLabelPreformated(true);
336 // try to grab it from the folder
337 if (item->GetLabel().IsEmpty()) {
339 if (CVideoDatabaseDirectory::GetLabel((const char*)path, label)) {
340 item->SetLabel(label);
341 item->SetLabelPreformated(true);
347 // not a virtual path directory, new system
348 object = BuildObject(*item.get(), file_path, with_count, thumb_loader, &context, this);
350 // set parent id if passed, otherwise it should have been determined
351 if (object && parent_id) {
352 object->m_ParentID = parent_id;
357 // remap Root virtualpath://upnproot/ to id "0"
358 if (object->m_ObjectID == "virtualpath://upnproot/")
359 object->m_ObjectID = "0";
361 // remap Parent Root virtualpath://upnproot/ to id "0"
362 if (object->m_ParentID == "virtualpath://upnproot/")
363 object->m_ParentID = "0";
373 /*----------------------------------------------------------------------
374 | CUPnPServer::Announce
375 +---------------------------------------------------------------------*/
377 CUPnPServer::Announce(AnnouncementFlag flag, const char *sender, const char *message, const CVariant &data)
383 if (strcmp(sender, "xbmc"))
386 if (strcmp(message, "OnUpdate") && strcmp(message, "OnRemove")
387 && strcmp(message, "OnScanStarted") && strcmp(message, "OnScanFinished"))
391 if (!strcmp(message, "OnScanStarted") || !strcmp(message, "OnCleanStarted")) {
394 else if (!strcmp(message, "OnScanFinished") || !strcmp(message, "OnCleanFinished")) {
395 OnScanCompleted(flag);
399 // handle both updates & removals
400 if (!data["item"].isNull()) {
401 item_id = (int)data["item"]["id"].asInteger();
402 item_type = data["item"]["type"].asString();
405 item_id = (int)data["id"].asInteger();
406 item_type = data["type"].asString();
409 // we always update 'recently added' nodes along with the specific container,
410 // as we don't differentiate 'updates' from 'adds' in RPC interface
411 if (flag == VideoLibrary) {
412 if(item_type == "episode") {
414 if (!db.Open()) return;
415 int show_id = db.GetTvShowForEpisode(item_id);
416 int season_id = db.GetSeasonForEpisode(item_id);
417 UpdateContainer(StringUtils::Format("videodb://tvshows/titles/%d/", show_id));
418 UpdateContainer(StringUtils::Format("videodb://tvshows/titles/%d/%d/?tvshowid=%d", show_id, season_id, show_id));
419 UpdateContainer("videodb://recentlyaddedepisodes/");
421 else if(item_type == "tvshow") {
422 UpdateContainer("library://video/tvshows/titles.xml/");
423 UpdateContainer("videodb://recentlyaddedepisodes/");
425 else if(item_type == "movie") {
426 UpdateContainer("library://video/movies/titles.xml/");
427 UpdateContainer("videodb://recentlyaddedmovies/");
429 else if(item_type == "musicvideo") {
430 UpdateContainer("library://video/musicvideos/titles.xml/");
431 UpdateContainer("videodb://recentlyaddedmusicvideos/");
434 else if (flag == AudioLibrary && item_type == "song") {
435 // we also update the 'songs' container is maybe a performance drop too
436 // high? would need to check if slow clients even cache at all anyway
439 if (!db.Open()) return;
440 if (db.GetAlbumFromSong(item_id, album)) {
441 UpdateContainer(StringUtils::Format("musicdb://albums/%ld", album.idAlbum));
442 UpdateContainer("musicdb://songs/");
443 UpdateContainer("musicdb://recentlyaddedalbums/");
449 /*----------------------------------------------------------------------
450 | TranslateWMPObjectId
451 +---------------------------------------------------------------------*/
452 static NPT_String TranslateWMPObjectId(NPT_String id)
455 id = "virtualpath://upnproot/";
456 } else if (id == "15") {
457 // Xbox 360 asking for videos
458 id = "library://video";
459 } else if (id == "16") {
460 // Xbox 360 asking for photos
461 } else if (id == "107") {
462 // Sonos uses 107 for artists root container id
463 id = "musicdb://artists/";
464 } else if (id == "7") {
465 // Sonos uses 7 for albums root container id
466 id = "musicdb://albums/";
467 } else if (id == "4") {
468 // Sonos uses 4 for tracks root container id
469 id = "musicdb://songs/";
472 CLog::Log(LOGDEBUG, "UPnP Translated id to '%s'", (const char*)id);
477 ObjectIDValidate(const NPT_String& id)
479 if (CFileUtils::RemoteAccessAllowed(id.GetChars()))
481 return NPT_ERROR_NO_SUCH_FILE;
484 /*----------------------------------------------------------------------
485 | CUPnPServer::OnBrowseMetadata
486 +---------------------------------------------------------------------*/
488 CUPnPServer::OnBrowseMetadata(PLT_ActionReference& action,
489 const char* object_id,
491 NPT_UInt32 starting_index,
492 NPT_UInt32 requested_count,
493 const char* sort_criteria,
494 const PLT_HttpRequestContext& context)
496 NPT_COMPILER_UNUSED(sort_criteria);
497 NPT_COMPILER_UNUSED(requested_count);
498 NPT_COMPILER_UNUSED(starting_index);
501 NPT_Reference<PLT_MediaObject> object;
502 NPT_String id = TranslateWMPObjectId(object_id);
503 vector<CStdString> paths;
505 NPT_Reference<CThumbLoader> thumb_loader;
507 CLog::Log(LOGINFO, "Received UPnP Browse Metadata request for object '%s'", (const char*)object_id);
509 if(NPT_FAILED(ObjectIDValidate(id))) {
510 action->SetError(701, "Incorrect ObjectID.");
514 if (id.StartsWith("virtualpath://")) {
516 if (id == "virtualpath://upnproot") {
518 item.reset(new CFileItem((const char*)id, true));
519 item->SetLabel("Root");
520 item->SetLabelPreformated(true);
521 object = Build(item, true, context, thumb_loader);
522 object->m_ParentID = "-1";
527 // determine if it's a container by calling CDirectory::Exists
528 item.reset(new CFileItem((const char*)id, CDirectory::Exists((const char*)id)));
530 // determine parent id for shared paths only
531 // otherwise let db find out
533 if (!URIUtils::GetParentPath((const char*)id, parent)) parent = "0";
535 //#ifdef WMP_ID_MAPPING
536 // if (!id.StartsWith("musicdb://") && !id.StartsWith("videodb://")) {
541 if (item->IsVideoDb()) {
542 thumb_loader = NPT_Reference<CThumbLoader>(new CVideoThumbLoader());
544 else if (item->IsMusicDb()) {
545 thumb_loader = NPT_Reference<CThumbLoader>(new CMusicThumbLoader());
547 if (!thumb_loader.IsNull()) {
548 thumb_loader->OnLoaderStart();
550 object = Build(item, true, context, thumb_loader, parent.empty()?NULL:parent.c_str());
553 if (object.IsNull()) {
555 NPT_LOG_WARNING_1("CUPnPServer::OnBrowseMetadata - Object null (%s)", object_id);
556 action->SetError(701, "No Such Object.");
561 NPT_CHECK(PLT_Didl::ToDidl(*object.AsPointer(), filter, tmp));
563 /* add didl header and footer */
564 didl = didl_header + tmp + didl_footer;
566 NPT_CHECK(action->SetArgumentValue("Result", didl));
567 NPT_CHECK(action->SetArgumentValue("NumberReturned", "1"));
568 NPT_CHECK(action->SetArgumentValue("TotalMatches", "1"));
570 // update ID may be wrong here, it should be the one of the container?
571 NPT_CHECK(action->SetArgumentValue("UpdateId", "0"));
573 // TODO: We need to keep track of the overall SystemUpdateID of the CDS
578 /*----------------------------------------------------------------------
579 | CUPnPServer::OnBrowseDirectChildren
580 +---------------------------------------------------------------------*/
582 CUPnPServer::OnBrowseDirectChildren(PLT_ActionReference& action,
583 const char* object_id,
585 NPT_UInt32 starting_index,
586 NPT_UInt32 requested_count,
587 const char* sort_criteria,
588 const PLT_HttpRequestContext& context)
591 NPT_String parent_id = TranslateWMPObjectId(object_id);
593 CLog::Log(LOGINFO, "UPnP: Received Browse DirectChildren request for object '%s', with sort criteria %s", object_id, sort_criteria);
595 if(NPT_FAILED(ObjectIDValidate(parent_id))) {
596 action->SetError(701, "Incorrect ObjectID.");
600 items.SetPath(CStdString(parent_id));
602 // guard against loading while saving to the same cache file
603 // as CArchive currently performs no locking itself
605 { NPT_AutoLock lock(m_CacheMutex);
610 // cache anything that takes more than a second to retrieve
611 unsigned int time = XbmcThreads::SystemClockMillis();
613 if (parent_id.StartsWith("virtualpath://upnproot")) {
617 item.reset(new CFileItem("musicdb://", true));
618 item->SetLabel("Music Library");
619 item->SetLabelPreformated(true);
623 item.reset(new CFileItem("library://video", true));
624 item->SetLabel("Video Library");
625 item->SetLabelPreformated(true);
628 items.Sort(SORT_METHOD_LABEL, SortOrderAscending);
630 // this is the only way to hide unplayable items in the 'files'
631 // view as we cannot tell what context (eg music vs video) the
633 string supported = g_advancedSettings.m_pictureExtensions + "|"
634 + g_advancedSettings.m_videoExtensions + "|"
635 + g_advancedSettings.m_musicExtensions + "|"
636 + g_advancedSettings.m_discStubExtensions;
637 CDirectory::GetDirectory((const char*)parent_id, items, supported);
638 DefaultSortItems(items);
641 if (items.CacheToDiscAlways() || (items.CacheToDiscIfSlow() && (XbmcThreads::SystemClockMillis() - time) > 1000 )) {
642 NPT_AutoLock lock(m_CacheMutex);
647 // as there's no library://music support, manually add playlists and music
649 if (items.GetPath() == "musicdb://") {
650 CFileItemPtr playlists(new CFileItem("special://musicplaylists/", true));
651 playlists->SetLabel(g_localizeStrings.Get(136));
652 items.Add(playlists);
654 CVideoDatabase database;
656 if (database.HasContent(VIDEODB_CONTENT_MUSICVIDEOS)) {
657 CFileItemPtr mvideos(new CFileItem("library://video/musicvideos/", true));
658 mvideos->SetLabel(g_localizeStrings.Get(20389));
663 // Don't pass parent_id if action is Search not BrowseDirectChildren, as
664 // we want the engine to determine the best parent id, not necessarily the one
666 NPT_String action_name = action->GetActionDesc().GetName();
667 return BuildResponse(
675 (action_name.Compare("Search", true)==0)?NULL:parent_id.GetChars());
678 /*----------------------------------------------------------------------
679 | CUPnPServer::BuildResponse
680 +---------------------------------------------------------------------*/
682 CUPnPServer::BuildResponse(PLT_ActionReference& action,
683 CFileItemList& items,
685 NPT_UInt32 starting_index,
686 NPT_UInt32 requested_count,
687 const char* sort_criteria,
688 const PLT_HttpRequestContext& context,
689 const char* parent_id /* = NULL */)
691 NPT_COMPILER_UNUSED(sort_criteria);
693 CLog::Log(LOGDEBUG, "Building UPnP response with filter '%s', starting @ %d with %d requested",
698 // we will reuse this ThumbLoader for all items
699 NPT_Reference<CThumbLoader> thumb_loader;
701 if (URIUtils::IsVideoDb(items.GetPath()) ||
702 StringUtils::StartsWith(items.GetPath(), "library://video") ||
703 StringUtils::StartsWith(items.GetPath(), "special://profile/playlists/video/")) {
705 thumb_loader = NPT_Reference<CThumbLoader>(new CVideoThumbLoader());
707 else if (URIUtils::IsMusicDb(items.GetPath()) ||
708 StringUtils::StartsWith(items.GetPath(), "special://profile/playlists/music/")) {
710 thumb_loader = NPT_Reference<CThumbLoader>(new CMusicThumbLoader());
712 if (!thumb_loader.IsNull()) {
713 thumb_loader->OnLoaderStart();
716 // this isn't pretty but needed to properly hide the addons node from clients
717 if (items.GetPath().Left(7) == "library") {
718 for (int i=0; i<items.Size(); i++) {
719 if (items[i]->GetPath().Left(6) == "addons")
724 // won't return more than UPNP_MAX_RETURNED_ITEMS items at a time to keep things smooth
725 // 0 requested means as many as possible
726 NPT_UInt32 max_count = (requested_count == 0)?m_MaxReturnedItems:min((unsigned long)requested_count, (unsigned long)m_MaxReturnedItems);
727 NPT_UInt32 stop_index = min((unsigned long)(starting_index + max_count), (unsigned long)items.Size()); // don't return more than we can
729 NPT_Cardinal count = 0;
730 NPT_Cardinal total = items.Size();
731 NPT_String didl = didl_header;
732 PLT_MediaObjectReference object;
733 for (unsigned long i=starting_index; i<stop_index; ++i) {
734 object = Build(items[i], true, context, thumb_loader, parent_id);
735 if (object.IsNull()) {
736 // don't tell the client this item ever existed
742 NPT_CHECK(PLT_Didl::ToDidl(*object.AsPointer(), filter, tmp));
744 // Neptunes string growing is dead slow for small additions
745 if (didl.GetCapacity() < tmp.GetLength() + didl.GetLength()) {
746 didl.Reserve((tmp.GetLength() + didl.GetLength())*2);
754 CLog::Log(LOGDEBUG, "Returning UPnP response with %d items out of %d total matches",
758 NPT_CHECK(action->SetArgumentValue("Result", didl));
759 NPT_CHECK(action->SetArgumentValue("NumberReturned", NPT_String::FromInteger(count)));
760 NPT_CHECK(action->SetArgumentValue("TotalMatches", NPT_String::FromInteger(total)));
761 NPT_CHECK(action->SetArgumentValue("UpdateId", "0"));
765 /*----------------------------------------------------------------------
767 +---------------------------------------------------------------------*/
770 FindSubCriteria(NPT_String criteria, const char* name)
773 int search = criteria.Find(name);
775 criteria = criteria.Right(criteria.GetLength() - search - NPT_StringLength(name));
776 criteria.TrimLeft(" ");
777 if (criteria.GetLength()>0 && criteria[0] == '=') {
778 criteria.TrimLeft("= ");
779 if (criteria.GetLength()>0 && criteria[0] == '\"') {
780 search = criteria.Find("\"", 1);
781 if (search > 0) result = criteria.SubString(1, search-1);
788 /*----------------------------------------------------------------------
789 | CUPnPServer::OnSearchContainer
790 +---------------------------------------------------------------------*/
792 CUPnPServer::OnSearchContainer(PLT_ActionReference& action,
793 const char* object_id,
794 const char* search_criteria,
796 NPT_UInt32 starting_index,
797 NPT_UInt32 requested_count,
798 const char* sort_criteria,
799 const PLT_HttpRequestContext& context)
801 CLog::Log(LOGDEBUG, "Received Search request for object '%s' with search '%s'",
802 (const char*)object_id,
803 (const char*)search_criteria);
805 NPT_String id = object_id;
806 if (id.StartsWith("musicdb://")) {
807 // we browse for all tracks given a genre, artist or album
808 if (NPT_String(search_criteria).Find("object.item.audioItem") >= 0) {
809 if (!id.EndsWith("/")) id += "/";
810 NPT_Cardinal count = id.SubString(10).Split("/").GetItemCount();
811 // remove extra empty node count
812 count = count?count-1:0;
815 if (id.StartsWith("musicdb://genres/")) {
816 // all tracks of all genres
819 // all tracks of a specific genre
822 // all tracks of a specific genre of a specfic artist
825 } else if (id.StartsWith("musicdb://artists/")) {
826 // all tracks by all artists
829 // all tracks of a specific artist
832 } else if (id.StartsWith("musicdb://albums/")) {
834 if (count == 1) id += "-1/";
837 return OnBrowseDirectChildren(action, id, filter, starting_index, requested_count, sort_criteria, context);
838 } else if (NPT_String(search_criteria).Find("object.item.audioItem") >= 0) {
839 // look for artist, album & genre filters
840 NPT_String genre = FindSubCriteria(search_criteria, "upnp:genre");
841 NPT_String album = FindSubCriteria(search_criteria, "upnp:album");
842 NPT_String artist = FindSubCriteria(search_criteria, "upnp:artist");
843 // sonos looks for microsoft specific stuff
844 artist = artist.GetLength()?artist:FindSubCriteria(search_criteria, "microsoft:artistPerformer");
845 artist = artist.GetLength()?artist:FindSubCriteria(search_criteria, "microsoft:artistAlbumArtist");
846 artist = artist.GetLength()?artist:FindSubCriteria(search_criteria, "microsoft:authorComposer");
848 CMusicDatabase database;
851 if (genre.GetLength() > 0) {
852 // all tracks by genre filtered by artist and/or album
854 strPath.Format("musicdb://genres/%ld/%ld/%ld/",
855 database.GetGenreByName((const char*)genre),
856 database.GetArtistByName((const char*)artist), // will return -1 if no artist
857 database.GetAlbumByName((const char*)album)); // will return -1 if no album
859 return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index, requested_count, sort_criteria, context);
860 } else if (artist.GetLength() > 0) {
861 // all tracks by artist name filtered by album if passed
863 strPath.Format("musicdb://artists/%ld/%ld/",
864 database.GetArtistByName((const char*)artist),
865 database.GetAlbumByName((const char*)album)); // will return -1 if no album
867 return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index, requested_count, sort_criteria, context);
868 } else if (album.GetLength() > 0) {
869 // all tracks by album name
871 strPath.Format("musicdb://albums/%ld/",
872 database.GetAlbumByName((const char*)album));
874 return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index, requested_count, sort_criteria, context);
878 return OnBrowseDirectChildren(action, "musicdb://songs/", filter, starting_index, requested_count, sort_criteria, context);
879 } else if (NPT_String(search_criteria).Find("object.container.album.musicAlbum") >= 0) {
880 // sonos filters by genre
881 NPT_String genre = FindSubCriteria(search_criteria, "upnp:genre");
883 // 360 hack: artist/albums using search
884 NPT_String artist = FindSubCriteria(search_criteria, "upnp:artist");
885 // sonos looks for microsoft specific stuff
886 artist = artist.GetLength()?artist:FindSubCriteria(search_criteria, "microsoft:artistPerformer");
887 artist = artist.GetLength()?artist:FindSubCriteria(search_criteria, "microsoft:artistAlbumArtist");
888 artist = artist.GetLength()?artist:FindSubCriteria(search_criteria, "microsoft:authorComposer");
890 CMusicDatabase database;
893 if (genre.GetLength() > 0) {
895 strPath.Format("musicdb://genres/%ld/%ld/",
896 database.GetGenreByName((const char*)genre),
897 database.GetArtistByName((const char*)artist)); // no artist should return -1
898 return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index, requested_count, sort_criteria, context);
899 } else if (artist.GetLength() > 0) {
901 strPath.Format("musicdb://artists/%ld/",
902 database.GetArtistByName((const char*)artist));
903 return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index, requested_count, sort_criteria, context);
907 return OnBrowseDirectChildren(action, "musicdb://albums/", filter, starting_index, requested_count, sort_criteria, context);
908 } else if (NPT_String(search_criteria).Find("object.container.person.musicArtist") >= 0) {
909 // Sonos filters by genre
910 NPT_String genre = FindSubCriteria(search_criteria, "upnp:genre");
911 if (genre.GetLength() > 0) {
912 CMusicDatabase database;
915 strPath.Format("musicdb://genres/%ld/", database.GetGenreByName((const char*)genre));
916 return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index, requested_count, sort_criteria, context);
918 return OnBrowseDirectChildren(action, "musicdb://artists/", filter, starting_index, requested_count, sort_criteria, context);
919 } else if (NPT_String(search_criteria).Find("object.container.genre.musicGenre") >= 0) {
920 return OnBrowseDirectChildren(action, "musicdb://genres/", filter, starting_index, requested_count, sort_criteria, context);
921 } else if (NPT_String(search_criteria).Find("object.container.playlistContainer") >= 0) {
922 return OnBrowseDirectChildren(action, "special://musicplaylists/", filter, starting_index, requested_count, sort_criteria, context);
923 } else if (NPT_String(search_criteria).Find("object.item.videoItem") >= 0) {
924 CFileItemList items, itemsall;
926 CVideoDatabase database;
927 if (!database.Open()) {
928 action->SetError(800, "Internal Error");
932 if (!database.GetMoviesNav("videodb://movies/titles/", items)) {
933 action->SetError(800, "Internal Error");
936 itemsall.Append(items);
939 if (!database.GetEpisodesByWhere("videodb://tvshows/titles/", "", items)) {
940 action->SetError(800, "Internal Error");
943 itemsall.Append(items);
946 return BuildResponse(action, itemsall, filter, starting_index, requested_count, sort_criteria, context, NULL);
947 } else if (NPT_String(search_criteria).Find("object.item.imageItem") >= 0) {
949 return BuildResponse(action, items, filter, starting_index, requested_count, sort_criteria, context, NULL);;
955 /*----------------------------------------------------------------------
956 | CUPnPServer::OnUpdateObject
957 +---------------------------------------------------------------------*/
959 CUPnPServer::OnUpdateObject(PLT_ActionReference& action,
960 const char* object_id,
961 NPT_Map<NPT_String,NPT_String>& current_vals,
962 NPT_Map<NPT_String,NPT_String>& new_vals,
963 const PLT_HttpRequestContext& context)
965 CStdString path = CURL::Decode(object_id);
967 updated.SetPath(path);
968 CLog::Log(LOGINFO, "UPnP: OnUpdateObject: %s from %s", path.c_str(),
969 (const char*) context.GetRemoteAddress().GetIpAddress().ToString());
971 NPT_String playCount, position;
973 const char* msg = NULL;
974 bool updatelisting(false);
976 // we pause eventing as multiple announces may happen in this operation
977 PLT_Service* service = NULL;
978 NPT_CHECK_LABEL(FindServiceById("urn:upnp-org:serviceId:ContentDirectory", service), error);
979 NPT_CHECK_LABEL(service->PauseEventing(), error);
981 if (updated.IsVideoDb()) {
983 NPT_CHECK_LABEL(!db.Open(), error);
985 // must first determine type of file from object id
986 VIDEODATABASEDIRECTORY::CQueryParams params;
987 VIDEODATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo(path.c_str(), params);
990 VIDEODB_CONTENT_TYPE content_type;
991 if ((id = params.GetMovieId()) >= 0 )
992 content_type = VIDEODB_CONTENT_MOVIES;
993 else if ((id = params.GetEpisodeId()) >= 0 )
994 content_type = VIDEODB_CONTENT_EPISODES;
995 else if ((id = params.GetMVideoId()) >= 0 )
996 content_type = VIDEODB_CONTENT_MUSICVIDEOS;
999 msg = "No such object";
1003 CStdString file_path;
1004 db.GetFilePathById(id, file_path, content_type);
1006 db.LoadVideoInfo(file_path, tag);
1007 updated.SetFromVideoInfoTag(tag);
1008 CLog::Log(LOGINFO, "UPNP: Translated to %s", file_path.c_str());
1010 position = new_vals["lastPlaybackPosition"];
1011 playCount = new_vals["playCount"];
1013 if (!position.IsEmpty()
1014 && position.Compare(current_vals["lastPlaybackPosition"]) != 0) {
1016 NPT_CHECK_LABEL(position.ToInteger32(resume), args);
1019 db.ClearBookMarksOfFile(file_path, CBookmark::RESUME);
1022 bookmark.timeInSeconds = resume;
1023 bookmark.totalTimeInSeconds = resume + 100; // not required to be correct
1025 db.AddBookMarkToFile(file_path, bookmark, CBookmark::RESUME);
1027 if (playCount.IsEmpty()) {
1029 data["id"] = updated.GetVideoInfoTag()->m_iDbId;
1030 data["type"] = updated.GetVideoInfoTag()->m_type;
1031 ANNOUNCEMENT::CAnnouncementManager::Announce(ANNOUNCEMENT::VideoLibrary, "xbmc", "OnUpdate", data);
1033 updatelisting = true;
1036 if (!playCount.IsEmpty()
1037 && playCount.Compare(current_vals["playCount"]) != 0) {
1040 NPT_CHECK_LABEL(playCount.ToInteger32(count), args);
1041 db.SetPlayCount(updated, count);
1042 updatelisting = true;
1045 // we must load the changed settings before propagating to local UI
1046 if (updatelisting) {
1047 db.LoadVideoInfo(file_path, tag);
1048 updated.SetFromVideoInfoTag(tag);
1051 } else if (updated.IsMusicDb()) {
1052 //TODO implement this
1056 msg = "No such object";
1060 if (updatelisting) {
1061 updated.SetPath(path);
1062 if (updated.IsVideoDb())
1063 CUtil::DeleteVideoDatabaseDirectoryCache();
1064 else if (updated.IsMusicDb())
1065 CUtil::DeleteMusicDatabaseDirectoryCache();
1067 CFileItemPtr msgItem(new CFileItem(updated));
1068 CGUIMessage message(GUI_MSG_NOTIFY_ALL, g_windowManager.GetActiveWindow(), 0, GUI_MSG_UPDATE_ITEM, 1, msgItem);
1069 g_windowManager.SendThreadMessage(message);
1072 NPT_CHECK_LABEL(service->PauseEventing(false), error);
1077 msg = "Invalid args";
1082 msg = "Internal error";
1085 CLog::Log(LOGERROR, "UPNP: OnUpdateObject failed with err %d:%s", err, msg);
1086 action->SetError(err, msg);
1087 service->PauseEventing(false);
1091 /*----------------------------------------------------------------------
1092 | CUPnPServer::ServeFile
1093 +---------------------------------------------------------------------*/
1095 CUPnPServer::ServeFile(const NPT_HttpRequest& request,
1096 const NPT_HttpRequestContext& context,
1097 NPT_HttpResponse& response,
1098 const NPT_String& md5)
1100 // Translate hash to filename
1101 NPT_String file_path(md5), *file_path2;
1102 { NPT_AutoLock lock(m_FileMutex);
1103 if(NPT_SUCCEEDED(m_FileMap.Get(md5, file_path2))) {
1104 file_path = *file_path2;
1105 CLog::Log(LOGDEBUG, "Received request to serve '%s' = '%s'", (const char*)md5, (const char*)file_path);
1107 CLog::Log(LOGDEBUG, "Received request to serve unknown md5 '%s'", (const char*)md5);
1108 response.SetStatus(404, "File Not Found");
1114 NPT_HttpUrl rooturi(context.GetLocalAddress().GetIpAddress().ToString(), context.GetLocalAddress().GetPort(), "/");
1116 if (file_path.Left(8).Compare("stack://", true) == 0) {
1118 NPT_List<NPT_String> files = file_path.SubString(8).Split(" , ");
1119 if (files.GetItemCount() == 0) {
1120 response.SetStatus(404, "File Not Found");
1125 output.Reserve(file_path.GetLength()*2);
1126 output += "#EXTM3U\r\n";
1128 NPT_List<NPT_String>::Iterator url = files.GetFirstItem();
1130 output += "#EXTINF:-1," + URIUtils::GetFileName((const char*)*url);
1132 output += BuildSafeResourceUri(
1134 context.GetLocalAddress().GetIpAddress().ToString(),
1139 PLT_HttpHelper::SetBody(response, (const char*)output, output.GetLength());
1140 response.GetHeaders().SetHeader("Content-Disposition", "inline; filename=\"stack.m3u\"");
1144 if(URIUtils::IsURL((const char*)file_path))
1146 CStdString disp = "inline; filename=\"" + URIUtils::GetFileName((const char*)file_path) + "\"";
1147 response.GetHeaders().SetHeader("Content-Disposition", disp.c_str());
1150 return PLT_HttpServer::ServeFile(request,
1156 /*----------------------------------------------------------------------
1157 | CUPnPServer::SortItems
1159 | Only support upnp: & dc: namespaces for now.
1160 | Other servers add their own vendor-specific sort methods. This could
1161 | possibly be handled with 'quirks' in the long run.
1163 | return true if sort criteria was matched
1164 +---------------------------------------------------------------------*/
1166 CUPnPServer::SortItems(CFileItemList& items, const char* sort_criteria)
1168 CStdString criteria(sort_criteria);
1169 if (criteria.IsEmpty()) {
1173 bool sorted = false;
1174 CStdStringArray tokens = StringUtils::SplitString(criteria, ",");
1175 for (vector<CStdString>::reverse_iterator itr = tokens.rbegin(); itr != tokens.rend(); itr++) {
1176 /* Platinum guarantees 1st char is - or + */
1177 SortOrder order = itr->Left(1).Equals("+") ? SortOrderAscending : SortOrderDescending;
1178 CStdString method = itr->Mid(1);
1180 SORT_METHOD scheme = SORT_METHOD_LABEL_IGNORE_THE;
1182 /* resource specific */
1183 if (method.Equals("res@duration"))
1184 scheme = SORT_METHOD_DURATION;
1185 else if (method.Equals("res@size"))
1186 scheme = SORT_METHOD_SIZE;
1187 else if (method.Equals("res@bitrate"))
1188 scheme = SORT_METHOD_BITRATE;
1191 else if (method.Equals("dc:date"))
1192 scheme = SORT_METHOD_DATE;
1193 else if (method.Equals("dc:title"))
1194 scheme = SORT_METHOD_TITLE_IGNORE_THE;
1197 else if (method.Equals("upnp:album"))
1198 scheme = SORT_METHOD_ALBUM;
1199 else if (method.Equals("upnp:artist") || method.Equals("upnp:albumArtist"))
1200 scheme = SORT_METHOD_ARTIST;
1201 else if (method.Equals("upnp:episodeNumber"))
1202 scheme = SORT_METHOD_EPISODE;
1203 else if (method.Equals("upnp:genre"))
1204 scheme = SORT_METHOD_GENRE;
1205 else if (method.Equals("upnp:originalTrackNumber"))
1206 scheme = SORT_METHOD_TRACKNUM;
1207 else if(method.Equals("upnp:rating"))
1208 scheme = SORT_METHOD_SONG_RATING;
1210 CLog::Log(LOGINFO, "UPnP: unsupported sort criteria '%s' passed", method.c_str());
1211 continue; // needed so unidentified sort methods don't re-sort by label
1214 CLog::Log(LOGINFO, "UPnP: Sorting by %d, %d", scheme, order);
1215 items.Sort(scheme, order);
1223 CUPnPServer::DefaultSortItems(CFileItemList& items)
1225 CGUIViewState* viewState = CGUIViewState::GetViewState(items.IsVideoDb() ? WINDOW_VIDEO_NAV : -1, items);
1228 items.Sort(viewState->GetSortMethod(), viewState->GetSortOrder());
1233 } /* namespace UPNP */