PageRenderTime 52ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

/xbmc/network/upnp/UPnPServer.cpp

https://github.com/myromiles/xbmc
C++ | 1043 lines | 768 code | 131 blank | 144 comment | 246 complexity | e3961b01267be5bfedbc5aaf9cd65cc1 MD5 | raw file
Possible License(s): GPL-2.0, LGPL-3.0, GPL-3.0, CC-BY-SA-3.0, BSD-3-Clause, Unlicense, AGPL-1.0, LGPL-2.1, 0BSD, LGPL-2.0
  1. #include "UPnPServer.h"
  2. #include "UPnPInternal.h"
  3. #include "Application.h"
  4. #include "GUIViewState.h"
  5. #include "Platinum.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/Key.h"
  15. #include "music/tags/MusicInfoTag.h"
  16. #include "settings/GUISettings.h"
  17. #include "utils/log.h"
  18. #include "utils/md5.h"
  19. #include "utils/StringUtils.h"
  20. #include "utils/URIUtils.h"
  21. #include "music/MusicDatabase.h"
  22. #include "video/VideoDatabase.h"
  23. using namespace std;
  24. using namespace ANNOUNCEMENT;
  25. using namespace XFILE;
  26. namespace UPNP
  27. {
  28. NPT_UInt32 CUPnPServer::m_MaxReturnedItems = 0;
  29. const char* audio_containers[] = { "musicdb://1/", "musicdb://2/", "musicdb://3/",
  30. "musicdb://4/", "musicdb://6/", "musicdb://9/",
  31. "musicdb://10/" };
  32. const char* video_containers[] = { "videodb://1/2/", "videodb://2/2/", "videodb://4/",
  33. "videodb://5/" };
  34. /*----------------------------------------------------------------------
  35. | CUPnPServer::CUPnPServer
  36. +---------------------------------------------------------------------*/
  37. CUPnPServer::CUPnPServer(const char* friendly_name, const char* uuid /*= NULL*/, int port /*= 0*/) :
  38. PLT_MediaConnect(friendly_name, false, uuid, port),
  39. PLT_FileMediaConnectDelegate("/", "/"),
  40. m_scanning(g_application.IsMusicScanning() || g_application.IsVideoScanning())
  41. {
  42. }
  43. CUPnPServer::~CUPnPServer()
  44. {
  45. ANNOUNCEMENT::CAnnouncementManager::RemoveAnnouncer(this);
  46. }
  47. /*----------------------------------------------------------------------
  48. | CUPnPServer::ProcessGetSCPD
  49. +---------------------------------------------------------------------*/
  50. NPT_Result
  51. CUPnPServer::ProcessGetSCPD(PLT_Service* service,
  52. NPT_HttpRequest& request,
  53. const NPT_HttpRequestContext& context,
  54. NPT_HttpResponse& response)
  55. {
  56. // needed because PLT_MediaConnect only allows Xbox360 & WMP to search
  57. return PLT_MediaServer::ProcessGetSCPD(service, request, context, response);
  58. }
  59. /*----------------------------------------------------------------------
  60. | CUPnPServer::SetupServices
  61. +---------------------------------------------------------------------*/
  62. NPT_Result
  63. CUPnPServer::SetupServices()
  64. {
  65. PLT_MediaConnect::SetupServices();
  66. PLT_Service* service = NULL;
  67. NPT_Result result = FindServiceById("urn:upnp-org:serviceId:ContentDirectory", service);
  68. if (service)
  69. 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");
  70. m_scanning = true;
  71. OnScanCompleted(AudioLibrary);
  72. m_scanning = true;
  73. OnScanCompleted(VideoLibrary);
  74. // now safe to start passing on new notifications
  75. ANNOUNCEMENT::CAnnouncementManager::AddAnnouncer(this);
  76. return result;
  77. }
  78. /*----------------------------------------------------------------------
  79. | CUPnPServer::OnScanCompleted
  80. +---------------------------------------------------------------------*/
  81. void
  82. CUPnPServer::OnScanCompleted(int type)
  83. {
  84. if (type == AudioLibrary) {
  85. for (size_t i = 0; i < sizeof(audio_containers)/sizeof(audio_containers[0]); i++)
  86. UpdateContainer(audio_containers[i]);
  87. }
  88. else if (type == VideoLibrary) {
  89. for (size_t i = 0; i < sizeof(video_containers)/sizeof(video_containers[0]); i++)
  90. UpdateContainer(video_containers[i]);
  91. }
  92. else
  93. return;
  94. m_scanning = false;
  95. PropagateUpdates();
  96. }
  97. /*----------------------------------------------------------------------
  98. | CUPnPServer::UpdateContainer
  99. +---------------------------------------------------------------------*/
  100. void
  101. CUPnPServer::UpdateContainer(const string& id)
  102. {
  103. map<string,pair<bool, unsigned long> >::iterator itr = m_UpdateIDs.find(id);
  104. unsigned long count = 0;
  105. if (itr != m_UpdateIDs.end())
  106. count = ++itr->second.second;
  107. m_UpdateIDs[id] = make_pair(true, count);
  108. PropagateUpdates();
  109. }
  110. /*----------------------------------------------------------------------
  111. | CUPnPServer::PropagateUpdates
  112. +---------------------------------------------------------------------*/
  113. void
  114. CUPnPServer::PropagateUpdates()
  115. {
  116. PLT_Service* service = NULL;
  117. NPT_String current_ids;
  118. string buffer;
  119. map<string,pair<bool, unsigned long> >::iterator itr;
  120. if (m_scanning || !g_guiSettings.GetBool("services.upnpannounce"))
  121. return;
  122. NPT_CHECK_LABEL(FindServiceById("urn:upnp-org:serviceId:ContentDirectory", service), failed);
  123. // we pause, and we must retain any changes which have not been
  124. // broadcast yet
  125. NPT_CHECK_LABEL(service->PauseEventing(), failed);
  126. NPT_CHECK_LABEL(service->GetStateVariableValue("ContainerUpdateIDs", current_ids), failed);
  127. buffer = (const char*)current_ids;
  128. if (!buffer.empty())
  129. buffer.append(",");
  130. // only broadcast ids with modified bit set
  131. for (itr = m_UpdateIDs.begin(); itr != m_UpdateIDs.end(); ++itr) {
  132. if (itr->second.first) {
  133. buffer.append(StringUtils::Format("%s,%ld,", itr->first.c_str(), itr->second.second).c_str());
  134. itr->second.first = false;
  135. }
  136. }
  137. // set the value, Platinum will clear ContainerUpdateIDs after sending
  138. NPT_CHECK_LABEL(service->SetStateVariable("ContainerUpdateIDs", buffer.substr(0,buffer.size()-1).c_str(), true), failed);
  139. NPT_CHECK_LABEL(service->IncStateVariable("SystemUpdateID"), failed);
  140. service->PauseEventing(false);
  141. return;
  142. failed:
  143. // should attempt to start eventing on a failure
  144. if (service) service->PauseEventing(false);
  145. CLog::Log(LOGERROR, "UPNP: Unable to propagate updates");
  146. }
  147. /*----------------------------------------------------------------------
  148. | CUPnPServer::SetupIcons
  149. +---------------------------------------------------------------------*/
  150. NPT_Result
  151. CUPnPServer::SetupIcons()
  152. {
  153. NPT_String file_root = CSpecialProtocol::TranslatePath("special://xbmc/media/").c_str();
  154. AddIcon(
  155. PLT_DeviceIcon("image/png", 256, 256, 24, "/icon-flat-256x256.png"),
  156. file_root);
  157. AddIcon(
  158. PLT_DeviceIcon("image/png", 120, 120, 24, "/icon-flat-120x120.png"),
  159. file_root);
  160. return NPT_SUCCESS;
  161. }
  162. /*----------------------------------------------------------------------
  163. | CUPnPServer::BuildSafeResourceUri
  164. +---------------------------------------------------------------------*/
  165. NPT_String CUPnPServer::BuildSafeResourceUri(const NPT_HttpUrl &rooturi,
  166. const char* host,
  167. const char* file_path)
  168. {
  169. CURL url(file_path);
  170. CStdString md5;
  171. XBMC::XBMC_MD5 md5state;
  172. // determine the filename to provide context to md5'd urls
  173. CStdString filename;
  174. if (url.GetProtocol() == "image")
  175. filename = URIUtils::GetFileName(url.GetHostName());
  176. else
  177. filename = URIUtils::GetFileName(file_path);
  178. md5state.append(file_path);
  179. md5state.getDigest(md5);
  180. md5 += "/" + filename;
  181. { NPT_AutoLock lock(m_FileMutex);
  182. NPT_CHECK(m_FileMap.Put(md5.c_str(), file_path));
  183. }
  184. return PLT_FileMediaServer::BuildSafeResourceUri(rooturi, host, md5.c_str());
  185. }
  186. /*----------------------------------------------------------------------
  187. | CUPnPServer::Build
  188. +---------------------------------------------------------------------*/
  189. PLT_MediaObject*
  190. CUPnPServer::Build(CFileItemPtr item,
  191. bool with_count,
  192. const PLT_HttpRequestContext& context,
  193. NPT_Reference<CThumbLoader>& thumb_loader,
  194. const char* parent_id /* = NULL */)
  195. {
  196. PLT_MediaObject* object = NULL;
  197. NPT_String path = item->GetPath().c_str();
  198. //HACK: temporary disabling count as it thrashes HDD
  199. with_count = false;
  200. CLog::Log(LOGDEBUG, "Preparing upnp object for item '%s'", (const char*)path);
  201. if (path == "virtualpath://upnproot") {
  202. path.TrimRight("/");
  203. if (path.StartsWith("virtualpath://")) {
  204. object = new PLT_MediaContainer;
  205. object->m_Title = item->GetLabel();
  206. object->m_ObjectClass.type = "object.container";
  207. object->m_ObjectID = path;
  208. // root
  209. object->m_ObjectID = "0";
  210. object->m_ParentID = "-1";
  211. // root has 5 children
  212. if (with_count) {
  213. ((PLT_MediaContainer*)object)->m_ChildrenCount = 5;
  214. }
  215. } else {
  216. goto failure;
  217. }
  218. } else {
  219. // db path handling
  220. NPT_String file_path, share_name;
  221. file_path = item->GetPath();
  222. share_name = "";
  223. if (path.StartsWith("musicdb://")) {
  224. if (path == "musicdb://" ) {
  225. item->SetLabel("Music Library");
  226. item->SetLabelPreformated(true);
  227. } else {
  228. if (!item->HasMusicInfoTag()) {
  229. MUSICDATABASEDIRECTORY::CQueryParams params;
  230. MUSICDATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo((const char*)path, params);
  231. CMusicDatabase db;
  232. if (!db.Open() ) return NULL;
  233. if (params.GetSongId() >= 0 ) {
  234. CSong song;
  235. if (db.GetSongById(params.GetSongId(), song))
  236. item->GetMusicInfoTag()->SetSong(song);
  237. }
  238. else if (params.GetAlbumId() >= 0 ) {
  239. CAlbum album;
  240. if (db.GetAlbumInfo(params.GetAlbumId(), album, NULL))
  241. item->GetMusicInfoTag()->SetAlbum(album);
  242. }
  243. else if (params.GetArtistId() >= 0 ) {
  244. CArtist artist;
  245. if (db.GetArtistInfo(params.GetArtistId(), artist, false))
  246. item->GetMusicInfoTag()->SetArtist(artist);
  247. }
  248. }
  249. if (item->GetLabel().IsEmpty()) {
  250. /* if no label try to grab it from node type */
  251. CStdString label;
  252. if (CMusicDatabaseDirectory::GetLabel((const char*)path, label)) {
  253. item->SetLabel(label);
  254. item->SetLabelPreformated(true);
  255. }
  256. }
  257. }
  258. } else if (file_path.StartsWith("library://") || file_path.StartsWith("videodb://")) {
  259. if (path == "library://video" ) {
  260. item->SetLabel("Video Library");
  261. item->SetLabelPreformated(true);
  262. } else {
  263. if (!item->HasVideoInfoTag()) {
  264. VIDEODATABASEDIRECTORY::CQueryParams params;
  265. VIDEODATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo((const char*)path, params);
  266. CVideoDatabase db;
  267. if (!db.Open() ) return NULL;
  268. if (params.GetMovieId() >= 0 )
  269. db.GetMovieInfo((const char*)path, *item->GetVideoInfoTag(), params.GetMovieId());
  270. else if (params.GetEpisodeId() >= 0 )
  271. db.GetEpisodeInfo((const char*)path, *item->GetVideoInfoTag(), params.GetEpisodeId());
  272. else if (params.GetTvShowId() >= 0 )
  273. db.GetTvShowInfo((const char*)path, *item->GetVideoInfoTag(), params.GetTvShowId());
  274. }
  275. if (item->GetVideoInfoTag()->m_type == "tvshow" || item->GetVideoInfoTag()->m_type == "season") {
  276. // for tvshows and seasons, iEpisode and playCount are
  277. // invalid
  278. item->GetVideoInfoTag()->m_iEpisode = (int)item->GetProperty("totalepisodes").asInteger();
  279. item->GetVideoInfoTag()->m_playCount = (int)item->GetProperty("watchedepisodes").asInteger();
  280. }
  281. // try to grab title from tag
  282. if (item->HasVideoInfoTag() && !item->GetVideoInfoTag()->m_strTitle.IsEmpty()) {
  283. item->SetLabel(item->GetVideoInfoTag()->m_strTitle);
  284. item->SetLabelPreformated(true);
  285. }
  286. // try to grab it from the folder
  287. if (item->GetLabel().IsEmpty()) {
  288. CStdString label;
  289. if (CVideoDatabaseDirectory::GetLabel((const char*)path, label)) {
  290. item->SetLabel(label);
  291. item->SetLabelPreformated(true);
  292. }
  293. }
  294. }
  295. }
  296. // not a virtual path directory, new system
  297. object = BuildObject(*item.get(), file_path, with_count, thumb_loader, &context, this);
  298. // set parent id if passed, otherwise it should have been determined
  299. if (object && parent_id) {
  300. object->m_ParentID = parent_id;
  301. }
  302. }
  303. if (object) {
  304. // remap Root virtualpath://upnproot/ to id "0"
  305. if (object->m_ObjectID == "virtualpath://upnproot/")
  306. object->m_ObjectID = "0";
  307. // remap Parent Root virtualpath://upnproot/ to id "0"
  308. if (object->m_ParentID == "virtualpath://upnproot/")
  309. object->m_ParentID = "0";
  310. }
  311. return object;
  312. failure:
  313. delete object;
  314. return NULL;
  315. }
  316. /*----------------------------------------------------------------------
  317. | CUPnPServer::Announce
  318. +---------------------------------------------------------------------*/
  319. void
  320. CUPnPServer::Announce(AnnouncementFlag flag, const char *sender, const char *message, const CVariant &data)
  321. {
  322. NPT_String path;
  323. int item_id;
  324. string item_type;
  325. if (strcmp(sender, "xbmc"))
  326. return;
  327. if (strcmp(message, "OnUpdate") && strcmp(message, "OnRemove")
  328. && strcmp(message, "OnScanStarted") && strcmp(message, "OnScanFinished"))
  329. return;
  330. if (data.isNull()) {
  331. if (!strcmp(message, "OnScanStarted") || !strcmp(message, "OnCleanStarted")) {
  332. m_scanning = true;
  333. }
  334. else if (!strcmp(message, "OnScanFinished") || !strcmp(message, "OnCleanFinished")) {
  335. OnScanCompleted(flag);
  336. }
  337. }
  338. else {
  339. // handle both updates & removals
  340. if (!data["item"].isNull()) {
  341. item_id = (int)data["item"]["id"].asInteger();
  342. item_type = data["item"]["type"].asString();
  343. }
  344. else {
  345. item_id = (int)data["id"].asInteger();
  346. item_type = data["type"].asString();
  347. }
  348. // we always update 'recently added' nodes along with the specific container,
  349. // as we don't differentiate 'updates' from 'adds' in RPC interface
  350. if (flag == VideoLibrary) {
  351. if(item_type == "episode") {
  352. CVideoDatabase db;
  353. if (!db.Open()) return;
  354. int show_id = db.GetTvShowForEpisode(item_id);
  355. int season_id = db.GetSeasonForEpisode(item_id);
  356. UpdateContainer(StringUtils::Format("videodb://2/2/%d/", show_id));
  357. UpdateContainer(StringUtils::Format("videodb://2/2/%d/%d/?tvshowid=%d", show_id, season_id, show_id));
  358. UpdateContainer("videodb://5/");
  359. }
  360. else if(item_type == "tvshow") {
  361. UpdateContainer("videodb://2/2/");
  362. UpdateContainer("videodb://5/");
  363. }
  364. else if(item_type == "movie") {
  365. UpdateContainer("videodb://1/2/");
  366. UpdateContainer("videodb://4/");
  367. }
  368. else if(item_type == "musicvideo") {
  369. UpdateContainer("videodb://4/");
  370. }
  371. }
  372. else if (flag == AudioLibrary && item_type == "song") {
  373. // we also update the 'songs' container is maybe a performance drop too
  374. // high? would need to check if slow clients even cache at all anyway
  375. CMusicDatabase db;
  376. CAlbum album;
  377. if (!db.Open()) return;
  378. if (db.GetAlbumFromSong(item_id, album)) {
  379. UpdateContainer(StringUtils::Format("musicdb://3/%ld", album.idAlbum));
  380. UpdateContainer("musicdb://4/");
  381. UpdateContainer("musicdb://6/");
  382. }
  383. }
  384. }
  385. }
  386. /*----------------------------------------------------------------------
  387. | TranslateWMPObjectId
  388. +---------------------------------------------------------------------*/
  389. static NPT_String TranslateWMPObjectId(NPT_String id)
  390. {
  391. if (id == "0") {
  392. id = "virtualpath://upnproot/";
  393. } else if (id == "15") {
  394. // Xbox 360 asking for videos
  395. id = "library://video";
  396. } else if (id == "16") {
  397. // Xbox 360 asking for photos
  398. } else if (id == "107") {
  399. // Sonos uses 107 for artists root container id
  400. id = "musicdb://2/";
  401. } else if (id == "7") {
  402. // Sonos uses 7 for albums root container id
  403. id = "musicdb://3/";
  404. } else if (id == "4") {
  405. // Sonos uses 4 for tracks root container id
  406. id = "musicdb://4/";
  407. }
  408. CLog::Log(LOGDEBUG, "UPnP Translated id to '%s'", (const char*)id);
  409. return id;
  410. }
  411. /*----------------------------------------------------------------------
  412. | CUPnPServer::OnBrowseMetadata
  413. +---------------------------------------------------------------------*/
  414. NPT_Result
  415. CUPnPServer::OnBrowseMetadata(PLT_ActionReference& action,
  416. const char* object_id,
  417. const char* filter,
  418. NPT_UInt32 starting_index,
  419. NPT_UInt32 requested_count,
  420. const char* sort_criteria,
  421. const PLT_HttpRequestContext& context)
  422. {
  423. NPT_COMPILER_UNUSED(sort_criteria);
  424. NPT_COMPILER_UNUSED(requested_count);
  425. NPT_COMPILER_UNUSED(starting_index);
  426. NPT_String didl;
  427. NPT_Reference<PLT_MediaObject> object;
  428. NPT_String id = TranslateWMPObjectId(object_id);
  429. vector<CStdString> paths;
  430. CFileItemPtr item;
  431. NPT_Reference<CThumbLoader> thumb_loader;
  432. CLog::Log(LOGINFO, "Received UPnP Browse Metadata request for object '%s'", (const char*)object_id);
  433. if (id.StartsWith("virtualpath://")) {
  434. id.TrimRight("/");
  435. if (id == "virtualpath://upnproot") {
  436. id += "/";
  437. item.reset(new CFileItem((const char*)id, true));
  438. item->SetLabel("Root");
  439. item->SetLabelPreformated(true);
  440. object = Build(item, true, context, thumb_loader);
  441. } else {
  442. return NPT_FAILURE;
  443. }
  444. } else {
  445. // determine if it's a container by calling CDirectory::Exists
  446. item.reset(new CFileItem((const char*)id, CDirectory::Exists((const char*)id)));
  447. // determine parent id for shared paths only
  448. // otherwise let db find out
  449. CStdString parent;
  450. if (!URIUtils::GetParentPath((const char*)id, parent)) parent = "0";
  451. //#ifdef WMP_ID_MAPPING
  452. // if (!id.StartsWith("musicdb://") && !id.StartsWith("videodb://")) {
  453. // parent = "";
  454. // }
  455. //#endif
  456. if (item->IsVideoDb()) {
  457. thumb_loader = NPT_Reference<CThumbLoader>(new CVideoThumbLoader());
  458. }
  459. else if (item->IsMusicDb()) {
  460. thumb_loader = NPT_Reference<CThumbLoader>(new CMusicThumbLoader());
  461. }
  462. if (!thumb_loader.IsNull()) {
  463. thumb_loader->Initialize();
  464. }
  465. object = Build(item, true, context, thumb_loader, parent.empty()?NULL:parent.c_str());
  466. }
  467. if (object.IsNull()) {
  468. /* error */
  469. NPT_LOG_WARNING_1("CUPnPServer::OnBrowseMetadata - Object null (%s)", object_id);
  470. action->SetError(701, "No Such Object.");
  471. return NPT_FAILURE;
  472. }
  473. NPT_String tmp;
  474. NPT_CHECK(PLT_Didl::ToDidl(*object.AsPointer(), filter, tmp));
  475. /* add didl header and footer */
  476. didl = didl_header + tmp + didl_footer;
  477. NPT_CHECK(action->SetArgumentValue("Result", didl));
  478. NPT_CHECK(action->SetArgumentValue("NumberReturned", "1"));
  479. NPT_CHECK(action->SetArgumentValue("TotalMatches", "1"));
  480. // update ID may be wrong here, it should be the one of the container?
  481. NPT_CHECK(action->SetArgumentValue("UpdateId", "0"));
  482. // TODO: We need to keep track of the overall SystemUpdateID of the CDS
  483. return NPT_SUCCESS;
  484. }
  485. /*----------------------------------------------------------------------
  486. | CUPnPServer::OnBrowseDirectChildren
  487. +---------------------------------------------------------------------*/
  488. NPT_Result
  489. CUPnPServer::OnBrowseDirectChildren(PLT_ActionReference& action,
  490. const char* object_id,
  491. const char* filter,
  492. NPT_UInt32 starting_index,
  493. NPT_UInt32 requested_count,
  494. const char* sort_criteria,
  495. const PLT_HttpRequestContext& context)
  496. {
  497. CFileItemList items;
  498. NPT_String parent_id = TranslateWMPObjectId(object_id);
  499. CLog::Log(LOGINFO, "UPnP: Received Browse DirectChildren request for object '%s', with sort criteria %s", object_id, sort_criteria);
  500. items.SetPath(CStdString(parent_id));
  501. // guard against loading while saving to the same cache file
  502. // as CArchive currently performs no locking itself
  503. bool load;
  504. { NPT_AutoLock lock(m_CacheMutex);
  505. load = items.Load();
  506. }
  507. if (!load) {
  508. // cache anything that takes more than a second to retrieve
  509. unsigned int time = XbmcThreads::SystemClockMillis();
  510. if (parent_id.StartsWith("virtualpath://upnproot")) {
  511. CFileItemPtr item;
  512. // music library
  513. item.reset(new CFileItem("musicdb://", true));
  514. item->SetLabel("Music Library");
  515. item->SetLabelPreformated(true);
  516. items.Add(item);
  517. // video library
  518. item.reset(new CFileItem("library://video", true));
  519. item->SetLabel("Video Library");
  520. item->SetLabelPreformated(true);
  521. items.Add(item);
  522. items.Sort(SORT_METHOD_LABEL, SortOrderAscending);
  523. } else {
  524. CDirectory::GetDirectory((const char*)parent_id, items);
  525. DefaultSortItems(items);
  526. }
  527. if (items.CacheToDiscAlways() || (items.CacheToDiscIfSlow() && (XbmcThreads::SystemClockMillis() - time) > 1000 )) {
  528. NPT_AutoLock lock(m_CacheMutex);
  529. items.Save();
  530. }
  531. }
  532. // Don't pass parent_id if action is Search not BrowseDirectChildren, as
  533. // we want the engine to determine the best parent id, not necessarily the one
  534. // passed
  535. NPT_String action_name = action->GetActionDesc().GetName();
  536. return BuildResponse(
  537. action,
  538. items,
  539. filter,
  540. starting_index,
  541. requested_count,
  542. sort_criteria,
  543. context,
  544. (action_name.Compare("Search", true)==0)?NULL:parent_id.GetChars());
  545. }
  546. /*----------------------------------------------------------------------
  547. | CUPnPServer::BuildResponse
  548. +---------------------------------------------------------------------*/
  549. NPT_Result
  550. CUPnPServer::BuildResponse(PLT_ActionReference& action,
  551. CFileItemList& items,
  552. const char* filter,
  553. NPT_UInt32 starting_index,
  554. NPT_UInt32 requested_count,
  555. const char* sort_criteria,
  556. const PLT_HttpRequestContext& context,
  557. const char* parent_id /* = NULL */)
  558. {
  559. NPT_COMPILER_UNUSED(sort_criteria);
  560. CLog::Log(LOGDEBUG, "Building UPnP response with filter '%s', starting @ %d with %d requested",
  561. (const char*)filter,
  562. starting_index,
  563. requested_count);
  564. // we will reuse this ThumbLoader for all items
  565. NPT_Reference<CThumbLoader> thumb_loader;
  566. if (URIUtils::IsVideoDb(items.GetPath()) || items.GetPath().Left(15) == "library://video") {
  567. thumb_loader = NPT_Reference<CThumbLoader>(new CVideoThumbLoader());
  568. }
  569. else if (URIUtils::IsMusicDb(items.GetPath())) {
  570. thumb_loader = NPT_Reference<CThumbLoader>(new CMusicThumbLoader());
  571. }
  572. if (!thumb_loader.IsNull()) {
  573. thumb_loader->Initialize();
  574. }
  575. // this isn't pretty but needed to properly hide the addons node from clients
  576. if (items.GetPath().Left(7) == "library") {
  577. for (int i=0; i<items.Size(); i++) {
  578. if (items[i]->GetPath().Left(6) == "addons")
  579. items.Remove(i);
  580. }
  581. }
  582. // won't return more than UPNP_MAX_RETURNED_ITEMS items at a time to keep things smooth
  583. // 0 requested means as many as possible
  584. NPT_UInt32 max_count = (requested_count == 0)?m_MaxReturnedItems:min((unsigned long)requested_count, (unsigned long)m_MaxReturnedItems);
  585. NPT_UInt32 stop_index = min((unsigned long)(starting_index + max_count), (unsigned long)items.Size()); // don't return more than we can
  586. NPT_Cardinal count = 0;
  587. NPT_Cardinal total = items.Size();
  588. NPT_String didl = didl_header;
  589. PLT_MediaObjectReference object;
  590. for (unsigned long i=starting_index; i<stop_index; ++i) {
  591. object = Build(items[i], true, context, thumb_loader, parent_id);
  592. if (object.IsNull()) {
  593. // don't tell the client this item ever existed
  594. --total;
  595. continue;
  596. }
  597. NPT_String tmp;
  598. NPT_CHECK(PLT_Didl::ToDidl(*object.AsPointer(), filter, tmp));
  599. // Neptunes string growing is dead slow for small additions
  600. if (didl.GetCapacity() < tmp.GetLength() + didl.GetLength()) {
  601. didl.Reserve((tmp.GetLength() + didl.GetLength())*2);
  602. }
  603. didl += tmp;
  604. ++count;
  605. }
  606. didl += didl_footer;
  607. CLog::Log(LOGDEBUG, "Returning UPnP response with %d items out of %d total matches",
  608. count,
  609. total);
  610. NPT_CHECK(action->SetArgumentValue("Result", didl));
  611. NPT_CHECK(action->SetArgumentValue("NumberReturned", NPT_String::FromInteger(count)));
  612. NPT_CHECK(action->SetArgumentValue("TotalMatches", NPT_String::FromInteger(total)));
  613. NPT_CHECK(action->SetArgumentValue("UpdateId", "0"));
  614. return NPT_SUCCESS;
  615. }
  616. /*----------------------------------------------------------------------
  617. | FindSubCriteria
  618. +---------------------------------------------------------------------*/
  619. static
  620. NPT_String
  621. FindSubCriteria(NPT_String criteria, const char* name)
  622. {
  623. NPT_String result;
  624. int search = criteria.Find(name);
  625. if (search >= 0) {
  626. criteria = criteria.Right(criteria.GetLength() - search - NPT_StringLength(name));
  627. criteria.TrimLeft(" ");
  628. if (criteria.GetLength()>0 && criteria[0] == '=') {
  629. criteria.TrimLeft("= ");
  630. if (criteria.GetLength()>0 && criteria[0] == '\"') {
  631. search = criteria.Find("\"", 1);
  632. if (search > 0) result = criteria.SubString(1, search-1);
  633. }
  634. }
  635. }
  636. return result;
  637. }
  638. /*----------------------------------------------------------------------
  639. | CUPnPServer::OnSearchContainer
  640. +---------------------------------------------------------------------*/
  641. NPT_Result
  642. CUPnPServer::OnSearchContainer(PLT_ActionReference& action,
  643. const char* object_id,
  644. const char* search_criteria,
  645. const char* filter,
  646. NPT_UInt32 starting_index,
  647. NPT_UInt32 requested_count,
  648. const char* sort_criteria,
  649. const PLT_HttpRequestContext& context)
  650. {
  651. CLog::Log(LOGDEBUG, "Received Search request for object '%s' with search '%s'",
  652. (const char*)object_id,
  653. (const char*)search_criteria);
  654. NPT_String id = object_id;
  655. if (id.StartsWith("musicdb://")) {
  656. // we browse for all tracks given a genre, artist or album
  657. if (NPT_String(search_criteria).Find("object.item.audioItem") >= 0) {
  658. if (!id.EndsWith("/")) id += "/";
  659. NPT_Cardinal count = id.SubString(10).Split("/").GetItemCount();
  660. // remove extra empty node count
  661. count = count?count-1:0;
  662. // genre
  663. if (id.StartsWith("musicdb://1/")) {
  664. // all tracks of all genres
  665. if (count == 1)
  666. id += "-1/-1/-1/";
  667. // all tracks of a specific genre
  668. else if (count == 2)
  669. id += "-1/-1/";
  670. // all tracks of a specific genre of a specfic artist
  671. else if (count == 3)
  672. id += "-1/";
  673. } else if (id.StartsWith("musicdb://2/")) {
  674. // all tracks by all artists
  675. if (count == 1)
  676. id += "-1/-1/";
  677. // all tracks of a specific artist
  678. else if (count == 2)
  679. id += "-1/";
  680. } else if (id.StartsWith("musicdb://3/")) {
  681. // all albums ?
  682. if (count == 1) id += "-1/";
  683. }
  684. }
  685. return OnBrowseDirectChildren(action, id, filter, starting_index, requested_count, sort_criteria, context);
  686. } else if (NPT_String(search_criteria).Find("object.item.audioItem") >= 0) {
  687. // look for artist, album & genre filters
  688. NPT_String genre = FindSubCriteria(search_criteria, "upnp:genre");
  689. NPT_String album = FindSubCriteria(search_criteria, "upnp:album");
  690. NPT_String artist = FindSubCriteria(search_criteria, "upnp:artist");
  691. // sonos looks for microsoft specific stuff
  692. artist = artist.GetLength()?artist:FindSubCriteria(search_criteria, "microsoft:artistPerformer");
  693. artist = artist.GetLength()?artist:FindSubCriteria(search_criteria, "microsoft:artistAlbumArtist");
  694. artist = artist.GetLength()?artist:FindSubCriteria(search_criteria, "microsoft:authorComposer");
  695. CMusicDatabase database;
  696. database.Open();
  697. if (genre.GetLength() > 0) {
  698. // all tracks by genre filtered by artist and/or album
  699. CStdString strPath;
  700. strPath.Format("musicdb://1/%ld/%ld/%ld/",
  701. database.GetGenreByName((const char*)genre),
  702. database.GetArtistByName((const char*)artist), // will return -1 if no artist
  703. database.GetAlbumByName((const char*)album)); // will return -1 if no album
  704. return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index, requested_count, sort_criteria, context);
  705. } else if (artist.GetLength() > 0) {
  706. // all tracks by artist name filtered by album if passed
  707. CStdString strPath;
  708. strPath.Format("musicdb://2/%ld/%ld/",
  709. database.GetArtistByName((const char*)artist),
  710. database.GetAlbumByName((const char*)album)); // will return -1 if no album
  711. return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index, requested_count, sort_criteria, context);
  712. } else if (album.GetLength() > 0) {
  713. // all tracks by album name
  714. CStdString strPath;
  715. strPath.Format("musicdb://3/%ld/",
  716. database.GetAlbumByName((const char*)album));
  717. return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index, requested_count, sort_criteria, context);
  718. }
  719. // browse all songs
  720. return OnBrowseDirectChildren(action, "musicdb://4/", filter, starting_index, requested_count, sort_criteria, context);
  721. } else if (NPT_String(search_criteria).Find("object.container.album.musicAlbum") >= 0) {
  722. // sonos filters by genre
  723. NPT_String genre = FindSubCriteria(search_criteria, "upnp:genre");
  724. // 360 hack: artist/albums using search
  725. NPT_String artist = FindSubCriteria(search_criteria, "upnp:artist");
  726. // sonos looks for microsoft specific stuff
  727. artist = artist.GetLength()?artist:FindSubCriteria(search_criteria, "microsoft:artistPerformer");
  728. artist = artist.GetLength()?artist:FindSubCriteria(search_criteria, "microsoft:artistAlbumArtist");
  729. artist = artist.GetLength()?artist:FindSubCriteria(search_criteria, "microsoft:authorComposer");
  730. CMusicDatabase database;
  731. database.Open();
  732. if (genre.GetLength() > 0) {
  733. CStdString strPath;
  734. strPath.Format("musicdb://1/%ld/%ld/",
  735. database.GetGenreByName((const char*)genre),
  736. database.GetArtistByName((const char*)artist)); // no artist should return -1
  737. return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index, requested_count, sort_criteria, context);
  738. } else if (artist.GetLength() > 0) {
  739. CStdString strPath;
  740. strPath.Format("musicdb://2/%ld/",
  741. database.GetArtistByName((const char*)artist));
  742. return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index, requested_count, sort_criteria, context);
  743. }
  744. // all albums
  745. return OnBrowseDirectChildren(action, "musicdb://3/", filter, starting_index, requested_count, sort_criteria, context);
  746. } else if (NPT_String(search_criteria).Find("object.container.person.musicArtist") >= 0) {
  747. // Sonos filters by genre
  748. NPT_String genre = FindSubCriteria(search_criteria, "upnp:genre");
  749. if (genre.GetLength() > 0) {
  750. CMusicDatabase database;
  751. database.Open();
  752. CStdString strPath;
  753. strPath.Format("musicdb://1/%ld/", database.GetGenreByName((const char*)genre));
  754. return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index, requested_count, sort_criteria, context);
  755. }
  756. return OnBrowseDirectChildren(action, "musicdb://2/", filter, starting_index, requested_count, sort_criteria, context);
  757. } else if (NPT_String(search_criteria).Find("object.container.genre.musicGenre") >= 0) {
  758. return OnBrowseDirectChildren(action, "musicdb://1/", filter, starting_index, requested_count, sort_criteria, context);
  759. } else if (NPT_String(search_criteria).Find("object.container.playlistContainer") >= 0) {
  760. return OnBrowseDirectChildren(action, "special://musicplaylists/", filter, starting_index, requested_count, sort_criteria, context);
  761. } else if (NPT_String(search_criteria).Find("object.item.videoItem") >= 0) {
  762. CFileItemList items, itemsall;
  763. CVideoDatabase database;
  764. if (!database.Open()) {
  765. action->SetError(800, "Internal Error");
  766. return NPT_SUCCESS;
  767. }
  768. if (!database.GetMoviesNav("videodb://1/2/", items)) {
  769. action->SetError(800, "Internal Error");
  770. return NPT_SUCCESS;
  771. }
  772. itemsall.Append(items);
  773. items.Clear();
  774. // TODO - set proper base url for this
  775. if (!database.GetEpisodesByWhere("videodb://2/0/", "", items, false)) {
  776. action->SetError(800, "Internal Error");
  777. return NPT_SUCCESS;
  778. }
  779. itemsall.Append(items);
  780. items.Clear();
  781. return BuildResponse(action, itemsall, filter, starting_index, requested_count, sort_criteria, context, NULL);
  782. } else if (NPT_String(search_criteria).Find("object.item.imageItem") >= 0) {
  783. CFileItemList items;
  784. return BuildResponse(action, items, filter, starting_index, requested_count, sort_criteria, context, NULL);;
  785. }
  786. return NPT_FAILURE;
  787. }
  788. /*----------------------------------------------------------------------
  789. | CUPnPServer::ServeFile
  790. +---------------------------------------------------------------------*/
  791. NPT_Result
  792. CUPnPServer::ServeFile(const NPT_HttpRequest& request,
  793. const NPT_HttpRequestContext& context,
  794. NPT_HttpResponse& response,
  795. const NPT_String& md5)
  796. {
  797. // Translate hash to filename
  798. NPT_String file_path(md5), *file_path2;
  799. { NPT_AutoLock lock(m_FileMutex);
  800. if(NPT_SUCCEEDED(m_FileMap.Get(md5, file_path2))) {
  801. file_path = *file_path2;
  802. CLog::Log(LOGDEBUG, "Received request to serve '%s' = '%s'", (const char*)md5, (const char*)file_path);
  803. } else {
  804. CLog::Log(LOGDEBUG, "Received request to serve unknown md5 '%s'", (const char*)md5);
  805. response.SetStatus(404, "File Not Found");
  806. return NPT_SUCCESS;
  807. }
  808. }
  809. // File requested
  810. NPT_HttpUrl rooturi(context.GetLocalAddress().GetIpAddress().ToString(), context.GetLocalAddress().GetPort(), "/");
  811. if (file_path.Left(8).Compare("stack://", true) == 0) {
  812. NPT_List<NPT_String> files = file_path.SubString(8).Split(" , ");
  813. if (files.GetItemCount() == 0) {
  814. response.SetStatus(404, "File Not Found");
  815. return NPT_SUCCESS;
  816. }
  817. NPT_String output;
  818. output.Reserve(file_path.GetLength()*2);
  819. output += "#EXTM3U\r\n";
  820. NPT_List<NPT_String>::Iterator url = files.GetFirstItem();
  821. for (;url;url++) {
  822. output += "#EXTINF:-1," + URIUtils::GetFileName((const char*)*url);
  823. output += "\r\n";
  824. output += BuildSafeResourceUri(
  825. rooturi,
  826. context.GetLocalAddress().GetIpAddress().ToString(),
  827. *url);
  828. output += "\r\n";
  829. }
  830. PLT_HttpHelper::SetBody(response, (const char*)output, output.GetLength());
  831. response.GetHeaders().SetHeader("Content-Disposition", "inline; filename=\"stack.m3u\"");
  832. return NPT_SUCCESS;
  833. }
  834. if(URIUtils::IsURL((const char*)file_path))
  835. {
  836. CStdString disp = "inline; filename=\"" + URIUtils::GetFileName((const char*)file_path) + "\"";
  837. response.GetHeaders().SetHeader("Content-Disposition", disp.c_str());
  838. }
  839. return PLT_HttpServer::ServeFile(request,
  840. context,
  841. response,
  842. file_path);
  843. }
  844. /*----------------------------------------------------------------------
  845. | CUPnPServer::SortItems
  846. |
  847. | Only support upnp: & dc: namespaces for now.
  848. | Other servers add their own vendor-specific sort methods. This could
  849. | possibly be handled with 'quirks' in the long run.
  850. |
  851. | return true if sort criteria was matched
  852. +---------------------------------------------------------------------*/
  853. bool
  854. CUPnPServer::SortItems(CFileItemList& items, const char* sort_criteria)
  855. {
  856. CStdString criteria(sort_criteria);
  857. if (criteria.IsEmpty()) {
  858. return false;
  859. }
  860. bool sorted = false;
  861. CStdStringArray tokens = StringUtils::SplitString(criteria, ",");
  862. for (vector<CStdString>::reverse_iterator itr = tokens.rbegin(); itr != tokens.rend(); itr++) {
  863. /* Platinum guarantees 1st char is - or + */
  864. SortOrder order = itr->Left(1).Equals("+") ? SortOrderAscending : SortOrderDescending;
  865. CStdString method = itr->Mid(1);
  866. SORT_METHOD scheme = SORT_METHOD_LABEL_IGNORE_THE;
  867. /* resource specific */
  868. if (method.Equals("res@duration"))
  869. scheme = SORT_METHOD_DURATION;
  870. else if (method.Equals("res@size"))
  871. scheme = SORT_METHOD_SIZE;
  872. else if (method.Equals("res@bitrate"))
  873. scheme = SORT_METHOD_BITRATE;
  874. /* dc: */
  875. else if (method.Equals("dc:date"))
  876. scheme = SORT_METHOD_DATE;
  877. else if (method.Equals("dc:title"))
  878. scheme = SORT_METHOD_TITLE_IGNORE_THE;
  879. /* upnp: */
  880. else if (method.Equals("upnp:album"))
  881. scheme = SORT_METHOD_ALBUM;
  882. else if (method.Equals("upnp:artist") || method.Equals("upnp:albumArtist"))
  883. scheme = SORT_METHOD_ARTIST;
  884. else if (method.Equals("upnp:episodeNumber"))
  885. scheme = SORT_METHOD_EPISODE;
  886. else if (method.Equals("upnp:genre"))
  887. scheme = SORT_METHOD_GENRE;
  888. else if (method.Equals("upnp:originalTrackNumber"))
  889. scheme = SORT_METHOD_TRACKNUM;
  890. else if(method.Equals("upnp:rating"))
  891. scheme = SORT_METHOD_SONG_RATING;
  892. else {
  893. CLog::Log(LOGINFO, "UPnP: unsupported sort criteria '%s' passed", method.c_str());
  894. continue; // needed so unidentified sort methods don't re-sort by label
  895. }
  896. CLog::Log(LOGINFO, "UPnP: Sorting by %d, %d", scheme, order);
  897. items.Sort(scheme, order);
  898. sorted = true;
  899. }
  900. return sorted;
  901. }
  902. void
  903. CUPnPServer::DefaultSortItems(CFileItemList& items)
  904. {
  905. CGUIViewState* viewState = CGUIViewState::GetViewState(items.IsVideoDb() ? WINDOW_VIDEO_NAV : -1, items);
  906. if (viewState)
  907. {
  908. items.Sort(viewState->GetSortMethod(), viewState->GetSortOrder());
  909. delete viewState;
  910. }
  911. }
  912. } /* namespace UPNP */