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, 24, "/icon-flat-256x256.png"),
205 PLT_DeviceIcon("image/png", 120, 120, 24, "/icon-flat-120x120.png"),
210 /*----------------------------------------------------------------------
211 | CUPnPServer::BuildSafeResourceUri
212 +---------------------------------------------------------------------*/
213 NPT_String CUPnPServer::BuildSafeResourceUri(const NPT_HttpUrl &rooturi,
215 const char* file_path)
219 XBMC::XBMC_MD5 md5state;
221 // determine the filename to provide context to md5'd urls
223 if (url.GetProtocol() == "image")
224 filename = URIUtils::GetFileName(url.GetHostName());
226 filename = URIUtils::GetFileName(file_path);
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));
235 return PLT_FileMediaServer::BuildSafeResourceUri(rooturi, host, md5.c_str());
238 /*----------------------------------------------------------------------
240 +---------------------------------------------------------------------*/
242 CUPnPServer::Build(CFileItemPtr item,
244 const PLT_HttpRequestContext& context,
245 NPT_Reference<CThumbLoader>& thumb_loader,
246 const char* parent_id /* = NULL */)
248 PLT_MediaObject* object = NULL;
249 NPT_String path = item->GetPath().c_str();
251 //HACK: temporary disabling count as it thrashes HDD
254 CLog::Log(LOGDEBUG, "Preparing upnp object for item '%s'", (const char*)path);
256 if (path == "virtualpath://upnproot") {
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;
265 object->m_ObjectID = "0";
266 object->m_ParentID = "-1";
267 // root has 5 children
269 ((PLT_MediaContainer*)object)->m_ChildrenCount = 5;
277 NPT_String file_path, share_name;
278 file_path = item->GetPath();
281 if (path.StartsWith("musicdb://")) {
282 if (path == "musicdb://" ) {
283 item->SetLabel("Music Library");
284 item->SetLabelPreformated(true);
286 if (!item->HasMusicInfoTag()) {
287 MUSICDATABASEDIRECTORY::CQueryParams params;
288 MUSICDATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo((const char*)path, params);
291 if (!db.Open() ) return NULL;
293 if (params.GetSongId() >= 0 ) {
295 if (db.GetSong(params.GetSongId(), song))
296 item->GetMusicInfoTag()->SetSong(song);
298 else if (params.GetAlbumId() >= 0 ) {
300 if (db.GetAlbumInfo(params.GetAlbumId(), album, NULL))
301 item->GetMusicInfoTag()->SetAlbum(album);
303 else if (params.GetArtistId() >= 0 ) {
305 if (db.GetArtistInfo(params.GetArtistId(), artist, false))
306 item->GetMusicInfoTag()->SetArtist(artist);
311 if (item->GetLabel().IsEmpty()) {
312 /* if no label try to grab it from node type */
314 if (CMusicDatabaseDirectory::GetLabel((const char*)path, label)) {
315 item->SetLabel(label);
316 item->SetLabelPreformated(true);
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);
325 if (!item->HasVideoInfoTag()) {
326 VIDEODATABASEDIRECTORY::CQueryParams params;
327 VIDEODATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo((const char*)path, params);
330 if (!db.Open() ) return NULL;
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());
342 if (item->GetVideoInfoTag()->m_type == "tvshow" || item->GetVideoInfoTag()->m_type == "season") {
343 // for tvshows and seasons, iEpisode and playCount are
345 item->GetVideoInfoTag()->m_iEpisode = (int)item->GetProperty("totalepisodes").asInteger();
346 item->GetVideoInfoTag()->m_playCount = (int)item->GetProperty("watchedepisodes").asInteger();
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);
355 // try to grab it from the folder
356 if (item->GetLabel().IsEmpty()) {
358 if (CVideoDatabaseDirectory::GetLabel((const char*)path, label)) {
359 item->SetLabel(label);
360 item->SetLabelPreformated(true);
366 // not a virtual path directory, new system
367 object = BuildObject(*item.get(), file_path, with_count, thumb_loader, &context, this);
369 // set parent id if passed, otherwise it should have been determined
370 if (object && parent_id) {
371 object->m_ParentID = parent_id;
376 // remap Root virtualpath://upnproot/ to id "0"
377 if (object->m_ObjectID == "virtualpath://upnproot/")
378 object->m_ObjectID = "0";
380 // remap Parent Root virtualpath://upnproot/ to id "0"
381 if (object->m_ParentID == "virtualpath://upnproot/")
382 object->m_ParentID = "0";
392 /*----------------------------------------------------------------------
393 | CUPnPServer::Announce
394 +---------------------------------------------------------------------*/
396 CUPnPServer::Announce(AnnouncementFlag flag, const char *sender, const char *message, const CVariant &data)
402 if (strcmp(sender, "xbmc"))
405 if (strcmp(message, "OnUpdate") && strcmp(message, "OnRemove")
406 && strcmp(message, "OnScanStarted") && strcmp(message, "OnScanFinished"))
410 if (!strcmp(message, "OnScanStarted") || !strcmp(message, "OnCleanStarted")) {
413 else if (!strcmp(message, "OnScanFinished") || !strcmp(message, "OnCleanFinished")) {
414 OnScanCompleted(flag);
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();
424 item_id = (int)data["id"].asInteger();
425 item_type = data["type"].asString();
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") {
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/");
440 else if(item_type == "tvshow") {
441 UpdateContainer("library://video/tvshows/titles.xml/");
442 UpdateContainer("videodb://recentlyaddedepisodes/");
444 else if(item_type == "movie") {
445 UpdateContainer("library://video/movies/titles.xml/");
446 UpdateContainer("videodb://recentlyaddedmovies/");
448 else if(item_type == "musicvideo") {
449 UpdateContainer("library://video/musicvideos/titles.xml/");
450 UpdateContainer("videodb://recentlyaddedmusicvideos/");
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
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/");
468 /*----------------------------------------------------------------------
469 | TranslateWMPObjectId
470 +---------------------------------------------------------------------*/
471 static NPT_String TranslateWMPObjectId(NPT_String id)
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/";
491 CLog::Log(LOGDEBUG, "UPnP Translated id to '%s'", (const char*)id);
496 ObjectIDValidate(const NPT_String& id)
498 if (CFileUtils::RemoteAccessAllowed(id.GetChars()))
500 return NPT_ERROR_NO_SUCH_FILE;
503 /*----------------------------------------------------------------------
504 | CUPnPServer::OnBrowseMetadata
505 +---------------------------------------------------------------------*/
507 CUPnPServer::OnBrowseMetadata(PLT_ActionReference& action,
508 const char* object_id,
510 NPT_UInt32 starting_index,
511 NPT_UInt32 requested_count,
512 const char* sort_criteria,
513 const PLT_HttpRequestContext& context)
515 NPT_COMPILER_UNUSED(sort_criteria);
516 NPT_COMPILER_UNUSED(requested_count);
517 NPT_COMPILER_UNUSED(starting_index);
520 NPT_Reference<PLT_MediaObject> object;
521 NPT_String id = TranslateWMPObjectId(object_id);
523 NPT_Reference<CThumbLoader> thumb_loader;
525 CLog::Log(LOGINFO, "Received UPnP Browse Metadata request for object '%s'", (const char*)object_id);
527 if(NPT_FAILED(ObjectIDValidate(id))) {
528 action->SetError(701, "Incorrect ObjectID.");
532 if (id.StartsWith("virtualpath://")) {
534 if (id == "virtualpath://upnproot") {
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";
545 // determine if it's a container by calling CDirectory::Exists
546 item.reset(new CFileItem((const char*)id, CDirectory::Exists((const char*)id)));
548 // attempt to determine the parent of this item
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)) {
556 // non-library objects - playlists / sources
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
571 if (item->IsVideoDb()) {
572 thumb_loader = NPT_Reference<CThumbLoader>(new CVideoThumbLoader());
574 else if (item->IsMusicDb()) {
575 thumb_loader = NPT_Reference<CThumbLoader>(new CMusicThumbLoader());
577 if (!thumb_loader.IsNull()) {
578 thumb_loader->OnLoaderStart();
580 object = Build(item, true, context, thumb_loader, parent.empty()?NULL:parent.c_str());
583 if (object.IsNull()) {
585 NPT_LOG_WARNING_1("CUPnPServer::OnBrowseMetadata - Object null (%s)", object_id);
586 action->SetError(701, "No Such Object.");
591 NPT_CHECK(PLT_Didl::ToDidl(*object.AsPointer(), filter, tmp));
593 /* add didl header and footer */
594 didl = didl_header + tmp + didl_footer;
596 NPT_CHECK(action->SetArgumentValue("Result", didl));
597 NPT_CHECK(action->SetArgumentValue("NumberReturned", "1"));
598 NPT_CHECK(action->SetArgumentValue("TotalMatches", "1"));
600 // update ID may be wrong here, it should be the one of the container?
601 NPT_CHECK(action->SetArgumentValue("UpdateId", "0"));
603 // TODO: We need to keep track of the overall SystemUpdateID of the CDS
608 /*----------------------------------------------------------------------
609 | CUPnPServer::OnBrowseDirectChildren
610 +---------------------------------------------------------------------*/
612 CUPnPServer::OnBrowseDirectChildren(PLT_ActionReference& action,
613 const char* object_id,
615 NPT_UInt32 starting_index,
616 NPT_UInt32 requested_count,
617 const char* sort_criteria,
618 const PLT_HttpRequestContext& context)
621 NPT_String parent_id = TranslateWMPObjectId(object_id);
623 CLog::Log(LOGINFO, "UPnP: Received Browse DirectChildren request for object '%s', with sort criteria %s", object_id, sort_criteria);
625 if(NPT_FAILED(ObjectIDValidate(parent_id))) {
626 action->SetError(701, "Incorrect ObjectID.");
630 items.SetPath(CStdString(parent_id));
632 // guard against loading while saving to the same cache file
633 // as CArchive currently performs no locking itself
635 { NPT_AutoLock lock(m_CacheMutex);
640 // cache anything that takes more than a second to retrieve
641 unsigned int time = XbmcThreads::SystemClockMillis();
643 if (parent_id.StartsWith("virtualpath://upnproot")) {
647 item.reset(new CFileItem("musicdb://", true));
648 item->SetLabel("Music Library");
649 item->SetLabelPreformated(true);
653 item.reset(new CFileItem("library://video/", true));
654 item->SetLabel("Video Library");
655 item->SetLabelPreformated(true);
658 items.Sort(SortByLabel, SortOrderAscending);
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
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);
671 if (items.CacheToDiscAlways() || (items.CacheToDiscIfSlow() && (XbmcThreads::SystemClockMillis() - time) > 1000 )) {
672 NPT_AutoLock lock(m_CacheMutex);
677 // as there's no library://music support, manually add playlists and music
679 if (items.GetPath() == "musicdb://") {
680 CFileItemPtr playlists(new CFileItem("special://musicplaylists/", true));
681 playlists->SetLabel(g_localizeStrings.Get(136));
682 items.Add(playlists);
684 CVideoDatabase database;
686 if (database.HasContent(VIDEODB_CONTENT_MUSICVIDEOS)) {
687 CFileItemPtr mvideos(new CFileItem("library://video/musicvideos/", true));
688 mvideos->SetLabel(g_localizeStrings.Get(20389));
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
696 NPT_String action_name = action->GetActionDesc().GetName();
697 return BuildResponse(
705 (action_name.Compare("Search", true)==0)?NULL:parent_id.GetChars());
708 /*----------------------------------------------------------------------
709 | CUPnPServer::BuildResponse
710 +---------------------------------------------------------------------*/
712 CUPnPServer::BuildResponse(PLT_ActionReference& action,
713 CFileItemList& items,
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 */)
721 NPT_COMPILER_UNUSED(sort_criteria);
723 CLog::Log(LOGDEBUG, "Building UPnP response with filter '%s', starting @ %d with %d requested",
728 // we will reuse this ThumbLoader for all items
729 NPT_Reference<CThumbLoader> thumb_loader;
731 if (URIUtils::IsVideoDb(items.GetPath()) ||
732 StringUtils::StartsWithNoCase(items.GetPath(), "library://video/") ||
733 StringUtils::StartsWithNoCase(items.GetPath(), "special://profile/playlists/video/")) {
735 thumb_loader = NPT_Reference<CThumbLoader>(new CVideoThumbLoader());
737 else if (URIUtils::IsMusicDb(items.GetPath()) ||
738 StringUtils::StartsWithNoCase(items.GetPath(), "special://profile/playlists/music/")) {
740 thumb_loader = NPT_Reference<CThumbLoader>(new CMusicThumbLoader());
742 if (!thumb_loader.IsNull()) {
743 thumb_loader->OnLoaderStart();
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")
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
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
772 NPT_CHECK(PLT_Didl::ToDidl(*object.AsPointer(), filter, tmp));
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);
784 CLog::Log(LOGDEBUG, "Returning UPnP response with %d items out of %d total matches",
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"));
795 /*----------------------------------------------------------------------
797 +---------------------------------------------------------------------*/
800 FindSubCriteria(NPT_String criteria, const char* name)
803 int search = criteria.Find(name);
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);
818 /*----------------------------------------------------------------------
819 | CUPnPServer::OnSearchContainer
820 +---------------------------------------------------------------------*/
822 CUPnPServer::OnSearchContainer(PLT_ActionReference& action,
823 const char* object_id,
824 const char* search_criteria,
826 NPT_UInt32 starting_index,
827 NPT_UInt32 requested_count,
828 const char* sort_criteria,
829 const PLT_HttpRequestContext& context)
831 CLog::Log(LOGDEBUG, "Received Search request for object '%s' with search '%s'",
832 (const char*)object_id,
833 (const char*)search_criteria);
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;
845 if (id.StartsWith("musicdb://genres/")) {
846 // all tracks of all genres
849 // all tracks of a specific genre
852 // all tracks of a specific genre of a specfic artist
855 } else if (id.StartsWith("musicdb://artists/")) {
856 // all tracks by all artists
859 // all tracks of a specific artist
862 } else if (id.StartsWith("musicdb://albums/")) {
864 if (count == 1) id += "-1/";
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");
878 CMusicDatabase database;
881 if (genre.GetLength() > 0) {
882 // all tracks by genre filtered by artist and/or album
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
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
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
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
901 strPath.Format("musicdb://albums/%ld/",
902 database.GetAlbumByName((const char*)album));
904 return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index, requested_count, sort_criteria, context);
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");
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");
920 CMusicDatabase database;
923 if (genre.GetLength() > 0) {
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) {
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);
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;
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);
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;
956 CVideoDatabase database;
957 if (!database.Open()) {
958 action->SetError(800, "Internal Error");
962 if (!database.GetMoviesNav("videodb://movies/titles/", items)) {
963 action->SetError(800, "Internal Error");
966 itemsall.Append(items);
969 if (!database.GetEpisodesByWhere("videodb://tvshows/titles/", "", items)) {
970 action->SetError(800, "Internal Error");
973 itemsall.Append(items);
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) {
979 return BuildResponse(action, items, filter, starting_index, requested_count, sort_criteria, context, NULL);;
985 /*----------------------------------------------------------------------
986 | CUPnPServer::OnUpdateObject
987 +---------------------------------------------------------------------*/
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)
995 CStdString path = CURL::Decode(object_id);
997 updated.SetPath(path);
998 CLog::Log(LOGINFO, "UPnP: OnUpdateObject: %s from %s", path.c_str(),
999 (const char*) context.GetRemoteAddress().GetIpAddress().ToString());
1001 NPT_String playCount, position;
1003 const char* msg = NULL;
1004 bool updatelisting(false);
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);
1011 if (updated.IsVideoDb()) {
1013 NPT_CHECK_LABEL(!db.Open(), error);
1015 // must first determine type of file from object id
1016 VIDEODATABASEDIRECTORY::CQueryParams params;
1017 VIDEODATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo(path.c_str(), params);
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;
1029 msg = "No such object";
1033 CStdString file_path;
1034 db.GetFilePathById(id, file_path, content_type);
1036 db.LoadVideoInfo(file_path, tag);
1037 updated.SetFromVideoInfoTag(tag);
1038 CLog::Log(LOGINFO, "UPNP: Translated to %s", file_path.c_str());
1040 position = new_vals["lastPlaybackPosition"];
1041 playCount = new_vals["playCount"];
1043 if (!position.IsEmpty()
1044 && position.Compare(current_vals["lastPlaybackPosition"]) != 0) {
1046 NPT_CHECK_LABEL(position.ToInteger32(resume), args);
1049 db.ClearBookMarksOfFile(file_path, CBookmark::RESUME);
1052 bookmark.timeInSeconds = resume;
1053 bookmark.totalTimeInSeconds = resume + 100; // not required to be correct
1055 db.AddBookMarkToFile(file_path, bookmark, CBookmark::RESUME);
1057 if (playCount.IsEmpty()) {
1059 data["id"] = updated.GetVideoInfoTag()->m_iDbId;
1060 data["type"] = updated.GetVideoInfoTag()->m_type;
1061 ANNOUNCEMENT::CAnnouncementManager::Announce(ANNOUNCEMENT::VideoLibrary, "xbmc", "OnUpdate", data);
1063 updatelisting = true;
1066 if (!playCount.IsEmpty()
1067 && playCount.Compare(current_vals["playCount"]) != 0) {
1070 NPT_CHECK_LABEL(playCount.ToInteger32(count), args);
1071 db.SetPlayCount(updated, count);
1072 updatelisting = true;
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);
1081 } else if (updated.IsMusicDb()) {
1082 //TODO implement this
1086 msg = "No such object";
1090 if (updatelisting) {
1091 updated.SetPath(path);
1092 if (updated.IsVideoDb())
1093 CUtil::DeleteVideoDatabaseDirectoryCache();
1094 else if (updated.IsMusicDb())
1095 CUtil::DeleteMusicDatabaseDirectoryCache();
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);
1102 NPT_CHECK_LABEL(service->PauseEventing(false), error);
1107 msg = "Invalid args";
1112 msg = "Internal error";
1115 CLog::Log(LOGERROR, "UPNP: OnUpdateObject failed with err %d:%s", err, msg);
1116 action->SetError(err, msg);
1117 service->PauseEventing(false);
1121 /*----------------------------------------------------------------------
1122 | CUPnPServer::ServeFile
1123 +---------------------------------------------------------------------*/
1125 CUPnPServer::ServeFile(const NPT_HttpRequest& request,
1126 const NPT_HttpRequestContext& context,
1127 NPT_HttpResponse& response,
1128 const NPT_String& md5)
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);
1137 CLog::Log(LOGDEBUG, "Received request to serve unknown md5 '%s'", (const char*)md5);
1138 response.SetStatus(404, "File Not Found");
1144 NPT_HttpUrl rooturi(context.GetLocalAddress().GetIpAddress().ToString(), context.GetLocalAddress().GetPort(), "/");
1146 if (file_path.Left(8).Compare("stack://", true) == 0) {
1148 NPT_List<NPT_String> files = file_path.SubString(8).Split(" , ");
1149 if (files.GetItemCount() == 0) {
1150 response.SetStatus(404, "File Not Found");
1155 output.Reserve(file_path.GetLength()*2);
1156 output += "#EXTM3U\r\n";
1158 NPT_List<NPT_String>::Iterator url = files.GetFirstItem();
1160 output += "#EXTINF:-1," + URIUtils::GetFileName((const char*)*url);
1162 output += BuildSafeResourceUri(
1164 context.GetLocalAddress().GetIpAddress().ToString(),
1169 PLT_HttpHelper::SetBody(response, (const char*)output, output.GetLength());
1170 response.GetHeaders().SetHeader("Content-Disposition", "inline; filename=\"stack.m3u\"");
1174 if(URIUtils::IsURL((const char*)file_path))
1176 CStdString disp = "inline; filename=\"" + URIUtils::GetFileName((const char*)file_path) + "\"";
1177 response.GetHeaders().SetHeader("Content-Disposition", disp.c_str());
1180 return PLT_HttpServer::ServeFile(request,
1186 /*----------------------------------------------------------------------
1187 | CUPnPServer::SortItems
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.
1193 | return true if sort criteria was matched
1194 +---------------------------------------------------------------------*/
1196 CUPnPServer::SortItems(CFileItemList& items, const char* sort_criteria)
1198 CStdString criteria(sort_criteria);
1199 if (criteria.IsEmpty()) {
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);
1208 SortDescription sorting;
1209 /* Platinum guarantees 1st char is - or + */
1210 sorting.sortOrder = StringUtils::StartsWith(*itr, "+") ? SortOrderAscending : SortOrderDescending;
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;
1221 else if (method.Equals("dc:date"))
1222 sorting.sortBy = SortByDate;
1223 else if (method.Equals("dc:title"))
1225 sorting.sortBy = SortByTitle;
1226 sorting.sortAttributes = SortAttributeIgnoreArticle;
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;
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
1247 CLog::Log(LOGINFO, "UPnP: Sorting by method %d, order %d, attributes %d", sorting.sortBy, sorting.sortOrder, sorting.sortAttributes);
1248 items.Sort(sorting);
1256 CUPnPServer::DefaultSortItems(CFileItemList& items)
1258 CGUIViewState* viewState = CGUIViewState::GetViewState(items.IsVideoDb() ? WINDOW_VIDEO_NAV : -1, items);
1261 SortDescription sorting = viewState->GetSortMethod();
1262 items.Sort(sorting.sortBy, sorting.sortOrder, sorting.sortAttributes);
1267 } /* namespace UPNP */