summaryrefslogtreecommitdiffstats
path: root/xbmc/network/upnp/UPnPServer.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-10 18:07:22 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-10 18:07:22 +0000
commitc04dcc2e7d834218ef2d4194331e383402495ae1 (patch)
tree7333e38d10d75386e60f336b80c2443c1166031d /xbmc/network/upnp/UPnPServer.cpp
parentInitial commit. (diff)
downloadkodi-c04dcc2e7d834218ef2d4194331e383402495ae1.tar.xz
kodi-c04dcc2e7d834218ef2d4194331e383402495ae1.zip
Adding upstream version 2:20.4+dfsg.upstream/2%20.4+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'xbmc/network/upnp/UPnPServer.cpp')
-rw-r--r--xbmc/network/upnp/UPnPServer.cpp1388
1 files changed, 1388 insertions, 0 deletions
diff --git a/xbmc/network/upnp/UPnPServer.cpp b/xbmc/network/upnp/UPnPServer.cpp
new file mode 100644
index 0000000..8e8432e
--- /dev/null
+++ b/xbmc/network/upnp/UPnPServer.cpp
@@ -0,0 +1,1388 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+#include "UPnPServer.h"
+
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "TextureDatabase.h"
+#include "UPnPInternal.h"
+#include "URL.h"
+#include "Util.h"
+#include "filesystem/Directory.h"
+#include "filesystem/MusicDatabaseDirectory.h"
+#include "filesystem/SpecialProtocol.h"
+#include "filesystem/VideoDatabaseDirectory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "interfaces/AnnouncementManager.h"
+#include "music/Artist.h"
+#include "music/MusicDatabase.h"
+#include "music/MusicLibraryQueue.h"
+#include "music/MusicThumbLoader.h"
+#include "music/tags/MusicInfoTag.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/Digest.h"
+#include "utils/FileExtensionProvider.h"
+#include "utils/FileUtils.h"
+#include "utils/SortUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "video/VideoDatabase.h"
+#include "video/VideoLibraryQueue.h"
+#include "video/VideoThumbLoader.h"
+#include "view/GUIViewState.h"
+#include "xbmc/interfaces/AnnouncementManager.h"
+
+#include <Platinum/Source/Platinum/Platinum.h>
+
+NPT_SET_LOCAL_LOGGER("xbmc.upnp.server")
+
+using namespace ANNOUNCEMENT;
+using namespace XFILE;
+using KODI::UTILITY::CDigest;
+
+namespace UPNP
+{
+
+NPT_UInt32 CUPnPServer::m_MaxReturnedItems = 0;
+
+const char* audio_containers[] = { "musicdb://genres/", "musicdb://artists/", "musicdb://albums/",
+ "musicdb://songs/", "musicdb://recentlyaddedalbums/", "musicdb://years/",
+ "musicdb://singles/" };
+
+const char* video_containers[] = { "library://video/movies/titles.xml/", "library://video/tvshows/titles.xml/",
+ "videodb://recentlyaddedmovies/", "videodb://recentlyaddedepisodes/" };
+
+/*----------------------------------------------------------------------
+| CUPnPServer::CUPnPServer
++---------------------------------------------------------------------*/
+CUPnPServer::CUPnPServer(const char* friendly_name, const char* uuid /*= NULL*/, int port /*= 0*/)
+ : PLT_MediaConnect(friendly_name, false, uuid, port),
+ PLT_FileMediaConnectDelegate("/", "/"),
+ m_scanning(CMusicLibraryQueue::GetInstance().IsScanningLibrary() ||
+ CVideoLibraryQueue::GetInstance().IsScanningLibrary()),
+ m_logger(CServiceBroker::GetLogging().GetLogger(
+ StringUtils::Format("CUPnPServer[{}]", friendly_name)))
+{
+}
+
+CUPnPServer::~CUPnPServer()
+{
+ CServiceBroker::GetAnnouncementManager()->RemoveAnnouncer(this);
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::ProcessGetSCPD
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPServer::ProcessGetSCPD(PLT_Service* service,
+ NPT_HttpRequest& request,
+ const NPT_HttpRequestContext& context,
+ NPT_HttpResponse& response)
+{
+ // needed because PLT_MediaConnect only allows Xbox360 & WMP to search
+ return PLT_MediaServer::ProcessGetSCPD(service, request, context, response);
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::SetupServices
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPServer::SetupServices()
+{
+ PLT_MediaConnect::SetupServices();
+ PLT_Service* service = NULL;
+ NPT_Result result = FindServiceById("urn:upnp-org:serviceId:ContentDirectory", service);
+ if (service)
+ {
+ service->SetStateVariable("SearchCapabilities", "upnp:class");
+ 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,upnp:episodeCount,upnp:episodeSeason,xbmc:rating,xbmc:dateadded,xbmc:votes");
+ }
+
+ m_scanning = true;
+ OnScanCompleted(AudioLibrary);
+ m_scanning = true;
+ OnScanCompleted(VideoLibrary);
+
+ // now safe to start passing on new notifications
+ CServiceBroker::GetAnnouncementManager()->AddAnnouncer(this);
+
+ return result;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::OnScanCompleted
++---------------------------------------------------------------------*/
+void
+CUPnPServer::OnScanCompleted(int type)
+{
+ if (type == AudioLibrary) {
+ for (const char* const audio_container : audio_containers)
+ UpdateContainer(audio_container);
+ }
+ else if (type == VideoLibrary) {
+ for (const char* const video_container : video_containers)
+ UpdateContainer(video_container);
+ }
+ else
+ return;
+ m_scanning = false;
+ PropagateUpdates();
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::UpdateContainer
++---------------------------------------------------------------------*/
+void
+CUPnPServer::UpdateContainer(const std::string& id)
+{
+ std::map<std::string, std::pair<bool, unsigned long> >::iterator itr = m_UpdateIDs.find(id);
+ unsigned long count = 0;
+ if (itr != m_UpdateIDs.end())
+ count = ++itr->second.second;
+ m_UpdateIDs[id] = std::make_pair(true, count);
+ PropagateUpdates();
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::PropagateUpdates
++---------------------------------------------------------------------*/
+void
+CUPnPServer::PropagateUpdates()
+{
+ PLT_Service* service = NULL;
+ NPT_String current_ids;
+ std::string buffer;
+ std::map<std::string, std::pair<bool, unsigned long> >::iterator itr;
+
+ if (m_scanning || !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_SERVICES_UPNPANNOUNCE))
+ return;
+
+ NPT_CHECK_LABEL(FindServiceById("urn:upnp-org:serviceId:ContentDirectory", service), failed);
+
+ // we pause, and we must retain any changes which have not been
+ // broadcast yet
+ NPT_CHECK_LABEL(service->PauseEventing(), failed);
+ NPT_CHECK_LABEL(service->GetStateVariableValue("ContainerUpdateIDs", current_ids), failed);
+ buffer = (const char*)current_ids;
+ if (!buffer.empty())
+ buffer.append(",");
+
+ // only broadcast ids with modified bit set
+ for (itr = m_UpdateIDs.begin(); itr != m_UpdateIDs.end(); ++itr) {
+ if (itr->second.first) {
+ buffer.append(StringUtils::Format("{},{},", itr->first, itr->second.second));
+ itr->second.first = false;
+ }
+ }
+
+ // set the value, Platinum will clear ContainerUpdateIDs after sending
+ NPT_CHECK_LABEL(service->SetStateVariable("ContainerUpdateIDs", buffer.substr(0,buffer.size()-1).c_str(), true), failed);
+ NPT_CHECK_LABEL(service->IncStateVariable("SystemUpdateID"), failed);
+
+ service->PauseEventing(false);
+ return;
+
+failed:
+ // should attempt to start eventing on a failure
+ if (service) service->PauseEventing(false);
+ m_logger->error("Unable to propagate updates");
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::SetupIcons
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPServer::SetupIcons()
+{
+ NPT_String file_root = CSpecialProtocol::TranslatePath("special://xbmc/media/").c_str();
+ AddIcon(
+ PLT_DeviceIcon("image/png", 256, 256, 8, "/icon256x256.png"),
+ file_root);
+ AddIcon(
+ PLT_DeviceIcon("image/png", 120, 120, 8, "/icon120x120.png"),
+ file_root);
+ AddIcon(
+ PLT_DeviceIcon("image/png", 48, 48, 8, "/icon48x48.png"),
+ file_root);
+ AddIcon(
+ PLT_DeviceIcon("image/png", 32, 32, 8, "/icon32x32.png"),
+ file_root);
+ AddIcon(
+ PLT_DeviceIcon("image/png", 16, 16, 8, "/icon16x16.png"),
+ file_root);
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::BuildSafeResourceUri
++---------------------------------------------------------------------*/
+NPT_String CUPnPServer::BuildSafeResourceUri(const NPT_HttpUrl &rooturi,
+ const char* host,
+ const char* file_path)
+{
+ CURL url(file_path);
+ std::string md5;
+ std::string mapped_file_path(file_path);
+
+ // determine the filename to provide context to md5'd urls
+ std::string filename;
+ if (url.IsProtocol("image"))
+ {
+ filename = URIUtils::GetFileName(url.GetHostName());
+ // Remove trailing / for Platinum/Neptune to recognize the file extension and use the correct mime type when serving the image
+ URIUtils::RemoveSlashAtEnd(mapped_file_path);
+ }
+ else
+ filename = URIUtils::GetFileName(mapped_file_path);
+
+ filename = CURL::Encode(filename);
+ md5 = CDigest::Calculate(CDigest::Type::MD5, mapped_file_path);
+ md5 += "/" + filename;
+ { NPT_AutoLock lock(m_FileMutex);
+ NPT_CHECK(m_FileMap.Put(md5.c_str(), mapped_file_path.c_str()));
+ }
+ return PLT_FileMediaServer::BuildSafeResourceUri(rooturi, host, md5.c_str());
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::Build
++---------------------------------------------------------------------*/
+PLT_MediaObject* CUPnPServer::Build(const std::shared_ptr<CFileItem>& item,
+ bool with_count,
+ const PLT_HttpRequestContext& context,
+ NPT_Reference<CThumbLoader>& thumb_loader,
+ const char* parent_id /* = NULL */)
+{
+ PLT_MediaObject* object = NULL;
+ NPT_String path = item->GetPath().c_str();
+
+ //HACK: temporary disabling count as it thrashes HDD
+ with_count = false;
+
+ m_logger->debug("Preparing upnp object for item '{}'", (const char*)path);
+
+ if (path.StartsWith("virtualpath://upnproot")) {
+ path.TrimRight("/");
+ item->m_bIsFolder = true;
+ if (path.StartsWith("virtualpath://")) {
+ object = new PLT_MediaContainer;
+ object->m_Title = item->GetLabel().c_str();
+ object->m_ObjectClass.type = "object.container";
+ object->m_ObjectID = path;
+
+ // root
+ object->m_ObjectID = "0";
+ object->m_ParentID = "-1";
+ // root has 5 children
+
+ //This is dead code because of the HACK a few lines up setting with_count to false
+ //if (with_count) {
+ // ((PLT_MediaContainer*)object)->m_ChildrenCount = 5;
+ //}
+ } else {
+ goto failure;
+ }
+
+ } else {
+ // db path handling
+ NPT_String file_path, share_name;
+ file_path = item->GetPath().c_str();
+ share_name = "";
+
+ if (path.StartsWith("musicdb://")) {
+ if (path == "musicdb://" ) {
+ item->SetLabel("Music Library");
+ item->SetLabelPreformatted(true);
+ item->m_bIsFolder = true;
+ } else {
+ if (!item->HasMusicInfoTag()) {
+ MUSICDATABASEDIRECTORY::CQueryParams params;
+ MUSICDATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo((const char*)path, params);
+
+ CMusicDatabase db;
+ if (!db.Open() ) return NULL;
+
+ if (params.GetSongId() >= 0 ) {
+ CSong song;
+ if (db.GetSong(params.GetSongId(), song))
+ item->GetMusicInfoTag()->SetSong(song);
+ }
+ else if (params.GetAlbumId() >= 0 ) {
+ item->m_bIsFolder = true;
+ CAlbum album;
+ if (db.GetAlbum(params.GetAlbumId(), album, false))
+ item->GetMusicInfoTag()->SetAlbum(album);
+ }
+ else if (params.GetArtistId() >= 0 ) {
+ item->m_bIsFolder = true;
+ CArtist artist;
+ if (db.GetArtist(params.GetArtistId(), artist, false))
+ item->GetMusicInfoTag()->SetArtist(artist);
+ }
+ }
+
+ // all items appart from songs (artists, albums, etc) are folders
+ if (!item->HasMusicInfoTag() || item->GetMusicInfoTag()->GetType() != MediaTypeSong)
+ {
+ item->m_bIsFolder = true;
+ }
+
+ if (item->GetLabel().empty()) {
+ /* if no label try to grab it from node type */
+ std::string label;
+ if (CMusicDatabaseDirectory::GetLabel((const char*)path, label)) {
+ item->SetLabel(label);
+ item->SetLabelPreformatted(true);
+ }
+ }
+ }
+ } else if (file_path.StartsWith("library://") || file_path.StartsWith("videodb://")) {
+ if (path == "library://video/" ) {
+ item->SetLabel("Video Library");
+ item->SetLabelPreformatted(true);
+ item->m_bIsFolder = true;
+ } else {
+ if (!item->HasVideoInfoTag()) {
+ VIDEODATABASEDIRECTORY::CQueryParams params;
+ VIDEODATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo((const char*)path, params);
+
+ CVideoDatabase db;
+ if (!db.Open() ) return NULL;
+
+ if (params.GetMovieId() >= 0 )
+ db.GetMovieInfo((const char*)path, *item->GetVideoInfoTag(), params.GetMovieId());
+ else if (params.GetMVideoId() >= 0 )
+ db.GetMusicVideoInfo((const char*)path, *item->GetVideoInfoTag(), params.GetMVideoId());
+ else if (params.GetEpisodeId() >= 0 )
+ db.GetEpisodeInfo((const char*)path, *item->GetVideoInfoTag(), params.GetEpisodeId());
+ else if (params.GetTvShowId() >= 0)
+ {
+ if (params.GetSeason() >= 0)
+ {
+ int idSeason = db.GetSeasonId(params.GetTvShowId(), params.GetSeason());
+ if (idSeason >= 0)
+ db.GetSeasonInfo(idSeason, *item->GetVideoInfoTag());
+ }
+ else
+ db.GetTvShowInfo((const char*)path, *item->GetVideoInfoTag(), params.GetTvShowId());
+ }
+ }
+
+ if (item->GetVideoInfoTag()->m_type == MediaTypeTvShow || item->GetVideoInfoTag()->m_type == MediaTypeSeason) {
+ // for tvshows and seasons, iEpisode and playCount are
+ // invalid
+ item->m_bIsFolder = true;
+ item->GetVideoInfoTag()->m_iEpisode = (int)item->GetProperty("totalepisodes").asInteger();
+ item->GetVideoInfoTag()->SetPlayCount(static_cast<int>(item->GetProperty("watchedepisodes").asInteger()));
+ }
+ // if this is an item in the library without a playable path it most be a folder
+ else if (item->GetVideoInfoTag()->m_strFileNameAndPath.empty())
+ {
+ item->m_bIsFolder = true;
+ }
+
+ // try to grab title from tag
+ if (item->HasVideoInfoTag() && !item->GetVideoInfoTag()->m_strTitle.empty()) {
+ item->SetLabel(item->GetVideoInfoTag()->m_strTitle);
+ item->SetLabelPreformatted(true);
+ }
+
+ // try to grab it from the folder
+ if (item->GetLabel().empty()) {
+ std::string label;
+ if (CVideoDatabaseDirectory::GetLabel((const char*)path, label)) {
+ item->SetLabel(label);
+ item->SetLabelPreformatted(true);
+ }
+ }
+ }
+ }
+ // playlists are folders
+ else if (item->IsPlayList())
+ {
+ item->m_bIsFolder = true;
+ }
+ // audio and not a playlist -> song, so it's not a folder
+ else if (item->IsAudio())
+ {
+ item->m_bIsFolder = false;
+ }
+ // any other type of item -> delegate to CDirectory
+ else
+ {
+ item->m_bIsFolder = CDirectory::Exists(item->GetPath());
+ }
+
+ // not a virtual path directory, new system
+ object = BuildObject(*item.get(), file_path, with_count, thumb_loader, &context, this, UPnPContentDirectory);
+
+ // set parent id if passed, otherwise it should have been determined
+ if (object && parent_id) {
+ object->m_ParentID = parent_id;
+ }
+ }
+
+ if (object) {
+ // remap Root virtualpath://upnproot/ to id "0"
+ if (object->m_ObjectID == "virtualpath://upnproot/")
+ object->m_ObjectID = "0";
+
+ // remap Parent Root virtualpath://upnproot/ to id "0"
+ if (object->m_ParentID == "virtualpath://upnproot/")
+ object->m_ParentID = "0";
+ }
+
+ return object;
+
+failure:
+ delete object;
+ return NULL;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::Announce
++---------------------------------------------------------------------*/
+void CUPnPServer::Announce(AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data)
+{
+ NPT_String path;
+ int item_id;
+ std::string item_type;
+
+ if (sender != CAnnouncementManager::ANNOUNCEMENT_SENDER)
+ return;
+
+ if (message != "OnUpdate" && message != "OnRemove" && message != "OnScanStarted" &&
+ message != "OnScanFinished")
+ return;
+
+ if (data.isNull()) {
+ if (message == "OnScanStarted" || message == "OnCleanStarted")
+ {
+ m_scanning = true;
+ }
+ else if (message == "OnScanFinished" || message == "OnCleanFinished")
+ {
+ OnScanCompleted(flag);
+ }
+ }
+ else {
+ // handle both updates & removals
+ if (!data["item"].isNull()) {
+ item_id = (int)data["item"]["id"].asInteger();
+ item_type = data["item"]["type"].asString();
+ }
+ else {
+ item_id = (int)data["id"].asInteger();
+ item_type = data["type"].asString();
+ }
+
+ // we always update 'recently added' nodes along with the specific container,
+ // as we don't differentiate 'updates' from 'adds' in RPC interface
+ if (flag == VideoLibrary) {
+ if(item_type == MediaTypeEpisode) {
+ CVideoDatabase db;
+ if (!db.Open()) return;
+ int show_id = db.GetTvShowForEpisode(item_id);
+ int season_id = db.GetSeasonForEpisode(item_id);
+ UpdateContainer(StringUtils::Format("videodb://tvshows/titles/{}/", show_id));
+ UpdateContainer(StringUtils::Format("videodb://tvshows/titles/{}/{}/?tvshowid={}",
+ show_id, season_id, show_id));
+ UpdateContainer("videodb://recentlyaddedepisodes/");
+ }
+ else if(item_type == MediaTypeTvShow) {
+ UpdateContainer("library://video/tvshows/titles.xml/");
+ UpdateContainer("videodb://recentlyaddedepisodes/");
+ }
+ else if(item_type == MediaTypeMovie) {
+ UpdateContainer("library://video/movies/titles.xml/");
+ UpdateContainer("videodb://recentlyaddedmovies/");
+ }
+ else if(item_type == MediaTypeMusicVideo) {
+ UpdateContainer("library://video/musicvideos/titles.xml/");
+ UpdateContainer("videodb://recentlyaddedmusicvideos/");
+ }
+ }
+ else if (flag == AudioLibrary && item_type == MediaTypeSong) {
+ // we also update the 'songs' container is maybe a performance drop too
+ // high? would need to check if slow clients even cache at all anyway
+ CMusicDatabase db;
+ CAlbum album;
+ if (!db.Open()) return;
+ if (db.GetAlbumFromSong(item_id, album)) {
+ UpdateContainer(StringUtils::Format("musicdb://albums/{}", album.idAlbum));
+ UpdateContainer("musicdb://songs/");
+ UpdateContainer("musicdb://recentlyaddedalbums/");
+ }
+ }
+ }
+}
+
+/*----------------------------------------------------------------------
+| TranslateWMPObjectId
++---------------------------------------------------------------------*/
+static NPT_String TranslateWMPObjectId(NPT_String id, const Logger& logger)
+{
+ if (id == "0") {
+ id = "virtualpath://upnproot/";
+ } else if (id == "15") {
+ // Xbox 360 asking for videos
+ id = "library://video/";
+ } else if (id == "16") {
+ // Xbox 360 asking for photos
+ } else if (id == "107") {
+ // Sonos uses 107 for artists root container id
+ id = "musicdb://artists/";
+ } else if (id == "7") {
+ // Sonos uses 7 for albums root container id
+ id = "musicdb://albums/";
+ } else if (id == "4") {
+ // Sonos uses 4 for tracks root container id
+ id = "musicdb://songs/";
+ }
+
+ logger->debug("Translated id to '{}'", (const char*)id);
+ return id;
+}
+
+NPT_Result
+ObjectIDValidate(const NPT_String& id)
+{
+ if (CFileUtils::RemoteAccessAllowed(id.GetChars()))
+ return NPT_SUCCESS;
+ return NPT_ERROR_NO_SUCH_FILE;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::OnBrowseMetadata
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPServer::OnBrowseMetadata(PLT_ActionReference& action,
+ const char* object_id,
+ const char* filter,
+ NPT_UInt32 starting_index,
+ NPT_UInt32 requested_count,
+ const char* sort_criteria,
+ const PLT_HttpRequestContext& context)
+{
+ NPT_COMPILER_UNUSED(sort_criteria);
+ NPT_COMPILER_UNUSED(requested_count);
+ NPT_COMPILER_UNUSED(starting_index);
+
+ NPT_String didl;
+ NPT_Reference<PLT_MediaObject> object;
+ NPT_String id = TranslateWMPObjectId(object_id, m_logger);
+ CFileItemPtr item;
+ NPT_Reference<CThumbLoader> thumb_loader;
+
+ m_logger->info("Received UPnP Browse Metadata request for object '{}'", object_id);
+
+ if(NPT_FAILED(ObjectIDValidate(id))) {
+ action->SetError(701, "Incorrect ObjectID.");
+ return NPT_FAILURE;
+ }
+
+ if (id.StartsWith("virtualpath://")) {
+ id.TrimRight("/");
+ if (id == "virtualpath://upnproot") {
+ id += "/";
+ item.reset(new CFileItem((const char*)id, true));
+ item->SetLabel("Root");
+ item->SetLabelPreformatted(true);
+ object = Build(item, true, context, thumb_loader);
+ object->m_ParentID = "-1";
+ } else {
+ return NPT_FAILURE;
+ }
+ } else {
+ item.reset(new CFileItem((const char*)id, false));
+
+ // attempt to determine the parent of this item
+ std::string parent;
+ if (URIUtils::IsVideoDb((const char*)id) || URIUtils::IsMusicDb((const char*)id) || StringUtils::StartsWithNoCase((const char*)id, "library://video/")) {
+ if (!URIUtils::GetParentPath((const char*)id, parent)) {
+ parent = "0";
+ }
+ }
+ else {
+ // non-library objects - playlists / sources
+ //
+ // we could instead store the parents in a hash during every browse
+ // or could handle this in URIUtils::GetParentPath() possibly,
+ // however this is quicker to implement and subsequently purge when a
+ // better solution presents itself
+ std::string child_id((const char*)id);
+ if (StringUtils::StartsWithNoCase(child_id, "special://musicplaylists/")) parent = "musicdb://";
+ else if (StringUtils::StartsWithNoCase(child_id, "special://videoplaylists/")) parent = "library://video/";
+ else if (StringUtils::StartsWithNoCase(child_id, "sources://video/")) parent = "library://video/";
+ else if (StringUtils::StartsWithNoCase(child_id, "special://profile/playlists/music/")) parent = "special://musicplaylists/";
+ else if (StringUtils::StartsWithNoCase(child_id, "special://profile/playlists/video/")) parent = "special://videoplaylists/";
+ else parent = "sources://video/"; // this can only match video sources
+ }
+
+ if (item->IsVideoDb()) {
+ thumb_loader = NPT_Reference<CThumbLoader>(new CVideoThumbLoader());
+ }
+ else if (item->IsMusicDb()) {
+ thumb_loader = NPT_Reference<CThumbLoader>(new CMusicThumbLoader());
+ }
+ if (!thumb_loader.IsNull()) {
+ thumb_loader->OnLoaderStart();
+ }
+ object = Build(item, true, context, thumb_loader, parent.empty()?NULL:parent.c_str());
+ }
+
+ if (object.IsNull()) {
+ /* error */
+ NPT_LOG_WARNING_1("CUPnPServer::OnBrowseMetadata - Object null (%s)", object_id);
+ action->SetError(701, "No Such Object.");
+ return NPT_FAILURE;
+ }
+
+ NPT_String tmp;
+ NPT_CHECK(PLT_Didl::ToDidl(*object.AsPointer(), filter, tmp));
+
+ /* add didl header and footer */
+ didl = didl_header + tmp + didl_footer;
+
+ NPT_CHECK(action->SetArgumentValue("Result", didl));
+ NPT_CHECK(action->SetArgumentValue("NumberReturned", "1"));
+ NPT_CHECK(action->SetArgumentValue("TotalMatches", "1"));
+
+ // update ID may be wrong here, it should be the one of the container?
+ NPT_CHECK(action->SetArgumentValue("UpdateId", "0"));
+
+ //! @todo We need to keep track of the overall SystemUpdateID of the CDS
+
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::OnBrowseDirectChildren
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPServer::OnBrowseDirectChildren(PLT_ActionReference& action,
+ const char* object_id,
+ const char* filter,
+ NPT_UInt32 starting_index,
+ NPT_UInt32 requested_count,
+ const char* sort_criteria,
+ const PLT_HttpRequestContext& context)
+{
+ CFileItemList items;
+ NPT_String parent_id = TranslateWMPObjectId(object_id, m_logger);
+
+ m_logger->info("Received Browse DirectChildren request for object '{}', with sort criteria {}",
+ object_id, sort_criteria);
+
+ if(NPT_FAILED(ObjectIDValidate(parent_id))) {
+ action->SetError(701, "Incorrect ObjectID.");
+ return NPT_FAILURE;
+ }
+
+ items.SetPath(std::string(parent_id));
+
+ // guard against loading while saving to the same cache file
+ // as CArchive currently performs no locking itself
+ bool load;
+ { NPT_AutoLock lock(m_CacheMutex);
+ load = items.Load();
+ }
+
+ if (!load) {
+ // cache anything that takes more than a second to retrieve
+ auto start = std::chrono::steady_clock::now();
+
+ if (parent_id.StartsWith("virtualpath://upnproot")) {
+ CFileItemPtr item;
+
+ // music library
+ item.reset(new CFileItem("musicdb://", true));
+ item->SetLabel("Music Library");
+ item->SetLabelPreformatted(true);
+ items.Add(item);
+
+ // video library
+ item.reset(new CFileItem("library://video/", true));
+ item->SetLabel("Video Library");
+ item->SetLabelPreformatted(true);
+ items.Add(item);
+
+ items.Sort(SortByLabel, SortOrderAscending);
+ } else {
+ // this is the only way to hide unplayable items in the 'files'
+ // view as we cannot tell what context (eg music vs video) the
+ // request came from
+ std::string supported = CServiceBroker::GetFileExtensionProvider().GetPictureExtensions() + "|"
+ + CServiceBroker::GetFileExtensionProvider().GetVideoExtensions() + "|"
+ + CServiceBroker::GetFileExtensionProvider().GetMusicExtensions() + "|"
+ + CServiceBroker::GetFileExtensionProvider().GetPictureExtensions();
+ CDirectory::GetDirectory((const char*)parent_id, items, supported, DIR_FLAG_DEFAULTS);
+ DefaultSortItems(items);
+ }
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ if (items.CacheToDiscAlways() || (items.CacheToDiscIfSlow() && duration.count() > 1000))
+ {
+ NPT_AutoLock lock(m_CacheMutex);
+ items.Save();
+ }
+ }
+
+ // as there's no library://music support, manually add playlists and music
+ // video nodes
+ if (items.GetPath() == "musicdb://") {
+ CFileItemPtr playlists(new CFileItem("special://musicplaylists/", true));
+ playlists->SetLabel(g_localizeStrings.Get(136));
+ items.Add(playlists);
+
+ CVideoDatabase database;
+ database.Open();
+ if (database.HasContent(VideoDbContentType::MUSICVIDEOS))
+ {
+ CFileItemPtr mvideos(new CFileItem("library://video/musicvideos/", true));
+ mvideos->SetLabel(g_localizeStrings.Get(20389));
+ items.Add(mvideos);
+ }
+ }
+
+ // Don't pass parent_id if action is Search not BrowseDirectChildren, as
+ // we want the engine to determine the best parent id, not necessarily the one
+ // passed
+ NPT_String action_name = action->GetActionDesc().GetName();
+ return BuildResponse(
+ action,
+ items,
+ filter,
+ starting_index,
+ requested_count,
+ sort_criteria,
+ context,
+ (action_name.Compare("Search", true)==0)?NULL:parent_id.GetChars());
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::BuildResponse
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPServer::BuildResponse(PLT_ActionReference& action,
+ CFileItemList& items,
+ const char* filter,
+ NPT_UInt32 starting_index,
+ NPT_UInt32 requested_count,
+ const char* sort_criteria,
+ const PLT_HttpRequestContext& context,
+ const char* parent_id /* = NULL */)
+{
+ NPT_COMPILER_UNUSED(sort_criteria);
+
+ m_logger->debug("Building UPnP response with filter '{}', starting @ {} with {} requested",
+ filter, starting_index, requested_count);
+
+ // we will reuse this ThumbLoader for all items
+ NPT_Reference<CThumbLoader> thumb_loader;
+
+ if (URIUtils::IsVideoDb(items.GetPath()) ||
+ StringUtils::StartsWithNoCase(items.GetPath(), "library://video/") ||
+ StringUtils::StartsWithNoCase(items.GetPath(), "special://profile/playlists/video/")) {
+
+ thumb_loader = NPT_Reference<CThumbLoader>(new CVideoThumbLoader());
+ }
+ else if (URIUtils::IsMusicDb(items.GetPath()) ||
+ StringUtils::StartsWithNoCase(items.GetPath(), "special://profile/playlists/music/")) {
+
+ thumb_loader = NPT_Reference<CThumbLoader>(new CMusicThumbLoader());
+ }
+ if (!thumb_loader.IsNull()) {
+ thumb_loader->OnLoaderStart();
+ }
+
+ // this isn't pretty but needed to properly hide the addons node from clients
+ if (StringUtils::StartsWith(items.GetPath(), "library")) {
+ for (int i=0; i<items.Size(); i++) {
+ if (StringUtils::StartsWith(items[i]->GetPath(), "addons") ||
+ StringUtils::EndsWith(items[i]->GetPath(), "/addons.xml/"))
+ items.Remove(i);
+ }
+ }
+
+ // won't return more than UPNP_MAX_RETURNED_ITEMS items at a time to keep things smooth
+ // 0 requested means as many as possible
+ NPT_UInt32 max_count = (requested_count == 0)?m_MaxReturnedItems:std::min((unsigned long)requested_count, (unsigned long)m_MaxReturnedItems);
+ NPT_UInt32 stop_index = std::min((unsigned long)(starting_index + max_count), (unsigned long)items.Size()); // don't return more than we can
+
+ NPT_Cardinal count = 0;
+ NPT_Cardinal total = items.Size();
+ NPT_String didl = didl_header;
+ PLT_MediaObjectReference object;
+ for (unsigned long i=starting_index; i<stop_index; ++i) {
+ object = Build(items[i], true, context, thumb_loader, parent_id);
+ if (object.IsNull()) {
+ // don't tell the client this item ever existed
+ --total;
+ continue;
+ }
+
+ NPT_String tmp;
+ NPT_CHECK(PLT_Didl::ToDidl(*object.AsPointer(), filter, tmp));
+
+ // Neptunes string growing is dead slow for small additions
+ if (didl.GetCapacity() < tmp.GetLength() + didl.GetLength()) {
+ didl.Reserve((tmp.GetLength() + didl.GetLength())*2);
+ }
+ didl += tmp;
+ ++count;
+ }
+
+ didl += didl_footer;
+
+ m_logger->debug("Returning UPnP response with {} items out of {} total matches", count, total);
+
+ NPT_CHECK(action->SetArgumentValue("Result", didl));
+ NPT_CHECK(action->SetArgumentValue("NumberReturned", NPT_String::FromInteger(count)));
+ NPT_CHECK(action->SetArgumentValue("TotalMatches", NPT_String::FromInteger(total)));
+ NPT_CHECK(action->SetArgumentValue("UpdateId", "0"));
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| FindSubCriteria
++---------------------------------------------------------------------*/
+static
+NPT_String
+FindSubCriteria(NPT_String criteria, const char* name)
+{
+ NPT_String result;
+ int search = criteria.Find(name);
+ if (search >= 0) {
+ criteria = criteria.Right(criteria.GetLength() - search - NPT_StringLength(name));
+ criteria.TrimLeft(" ");
+ if (criteria.GetLength()>0 && criteria[0] == '=') {
+ criteria.TrimLeft("= ");
+ if (criteria.GetLength()>0 && criteria[0] == '\"') {
+ search = criteria.Find("\"", 1);
+ if (search > 0) result = criteria.SubString(1, search-1);
+ }
+ }
+ }
+ return result;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::OnSearchContainer
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPServer::OnSearchContainer(PLT_ActionReference& action,
+ const char* object_id,
+ const char* search_criteria,
+ const char* filter,
+ NPT_UInt32 starting_index,
+ NPT_UInt32 requested_count,
+ const char* sort_criteria,
+ const PLT_HttpRequestContext& context)
+{
+ m_logger->debug("Received Search request for object '{}' with search '{}'", object_id,
+ search_criteria);
+
+ NPT_String id = object_id;
+ NPT_String searchClass = NPT_String(search_criteria);
+ if (id.StartsWith("musicdb://")) {
+ // we browse for all tracks given a genre, artist or album
+ if (searchClass.Find("object.item.audioItem") >= 0) {
+ if (!id.EndsWith("/")) id += "/";
+ NPT_Cardinal count = id.SubString(10).Split("/").GetItemCount();
+ // remove extra empty node count
+ count = count?count-1:0;
+
+ // genre
+ if (id.StartsWith("musicdb://genres/")) {
+ // all tracks of all genres
+ if (count == 1)
+ id += "-1/-1/-1/";
+ // all tracks of a specific genre
+ else if (count == 2)
+ id += "-1/-1/";
+ // all tracks of a specific genre of a specific artist
+ else if (count == 3)
+ id += "-1/";
+ }
+ else if (id.StartsWith("musicdb://artists/")) {
+ // all tracks by all artists
+ if (count == 1)
+ id += "-1/-1/";
+ // all tracks of a specific artist
+ else if (count == 2)
+ id += "-1/";
+ }
+ else if (id.StartsWith("musicdb://albums/")) {
+ // all albums ?
+ if (count == 1) id += "-1/";
+ }
+ }
+ return OnBrowseDirectChildren(action, id, filter, starting_index, requested_count, sort_criteria, context);
+ } else if (searchClass.Find("object.item.audioItem") >= 0) {
+ // look for artist, album & genre filters
+ NPT_String genre = FindSubCriteria(searchClass, "upnp:genre");
+ NPT_String album = FindSubCriteria(searchClass, "upnp:album");
+ NPT_String artist = FindSubCriteria(searchClass, "upnp:artist");
+ // sonos looks for microsoft specific stuff
+ artist = artist.GetLength()?artist:FindSubCriteria(searchClass, "microsoft:artistPerformer");
+ artist = artist.GetLength()?artist:FindSubCriteria(searchClass, "microsoft:artistAlbumArtist");
+ artist = artist.GetLength()?artist:FindSubCriteria(searchClass, "microsoft:authorComposer");
+
+ CMusicDatabase database;
+ database.Open();
+
+ if (genre.GetLength() > 0) {
+ // all tracks by genre filtered by artist and/or album
+ std::string strPath = StringUtils::Format(
+ "musicdb://genres/{}/{}/{}/", database.GetGenreByName((const char*)genre),
+ database.GetArtistByName((const char*)artist), // will return -1 if no artist
+ database.GetAlbumByName((const char*)album)); // will return -1 if no album
+
+ return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index, requested_count, sort_criteria, context);
+ } else if (artist.GetLength() > 0) {
+ // all tracks by artist name filtered by album if passed
+ std::string strPath = StringUtils::Format(
+ "musicdb://artists/{}/{}/", database.GetArtistByName((const char*)artist),
+ database.GetAlbumByName((const char*)album)); // will return -1 if no album
+
+ return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index, requested_count, sort_criteria, context);
+ } else if (album.GetLength() > 0) {
+ // all tracks by album name
+ std::string strPath = StringUtils::Format("musicdb://albums/{}/",
+ database.GetAlbumByName((const char*)album));
+
+ return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index, requested_count, sort_criteria, context);
+ }
+
+ // browse all songs
+ return OnBrowseDirectChildren(action, "musicdb://songs/", filter, starting_index, requested_count, sort_criteria, context);
+ } else if (searchClass.Find("object.container.album.musicAlbum") >= 0) {
+ // sonos filters by genre
+ NPT_String genre = FindSubCriteria(searchClass, "upnp:genre");
+
+ // 360 hack: artist/albums using search
+ NPT_String artist = FindSubCriteria(searchClass, "upnp:artist");
+ // sonos looks for microsoft specific stuff
+ artist = artist.GetLength()?artist:FindSubCriteria(searchClass, "microsoft:artistPerformer");
+ artist = artist.GetLength()?artist:FindSubCriteria(searchClass, "microsoft:artistAlbumArtist");
+ artist = artist.GetLength()?artist:FindSubCriteria(searchClass, "microsoft:authorComposer");
+
+ CMusicDatabase database;
+ database.Open();
+
+ if (genre.GetLength() > 0) {
+ std::string strPath = StringUtils::Format(
+ "musicdb://genres/{}/{}/", database.GetGenreByName((const char*)genre),
+ database.GetArtistByName((const char*)artist)); // no artist should return -1
+ return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index,
+ requested_count, sort_criteria, context);
+ } else if (artist.GetLength() > 0) {
+ std::string strPath = StringUtils::Format("musicdb://artists/{}/",
+ database.GetArtistByName((const char*)artist));
+ return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index,
+ requested_count, sort_criteria, context);
+ }
+
+ // all albums
+ return OnBrowseDirectChildren(action, "musicdb://albums/", filter, starting_index, requested_count, sort_criteria, context);
+ } else if (searchClass.Find("object.container.person.musicArtist") >= 0) {
+ // Sonos filters by genre
+ NPT_String genre = FindSubCriteria(searchClass, "upnp:genre");
+ if (genre.GetLength() > 0) {
+ CMusicDatabase database;
+ database.Open();
+ std::string strPath = StringUtils::Format("musicdb://genres/{}/",
+ database.GetGenreByName((const char*)genre));
+ return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index, requested_count, sort_criteria, context);
+ }
+ return OnBrowseDirectChildren(action, "musicdb://artists/", filter, starting_index, requested_count, sort_criteria, context);
+ } else if (searchClass.Find("object.container.genre.musicGenre") >= 0) {
+ return OnBrowseDirectChildren(action, "musicdb://genres/", filter, starting_index, requested_count, sort_criteria, context);
+ } else if (searchClass.Find("object.container.playlistContainer") >= 0) {
+ return OnBrowseDirectChildren(action, "special://musicplaylists/", filter, starting_index, requested_count, sort_criteria, context);
+ } else if (searchClass.Find("object.container.album.videoAlbum.videoBroadcastShow") >= 0) {
+ CVideoDatabase database;
+ if (!database.Open()) {
+ action->SetError(800, "Internal Error");
+ return NPT_SUCCESS;
+ }
+
+ CFileItemList items;
+ if (!database.GetTvShowsByWhere("videodb://tvshows/titles/?local", CDatabase::Filter(),
+ items, SortDescription(), GetRequiredVideoDbDetails(NPT_String(filter)))) {
+ action->SetError(800, "Internal Error");
+ return NPT_SUCCESS;
+ }
+
+ items.SetPath("videodb://tvshows/titles/");
+ return BuildResponse(action, items, filter, starting_index, requested_count, sort_criteria, context, NULL);
+ } else if (searchClass.Find("object.container.album.videoAlbum.videoBroadcastSeason") >= 0) {
+ CVideoDatabase database;
+ if (!database.Open()) {
+ action->SetError(800, "Internal Error");
+ return NPT_SUCCESS;
+ }
+
+ CFileItemList items;
+ if (!database.GetSeasonsByWhere("videodb://tvshows/titles/-1/?local", CDatabase::Filter(), items, true)) {
+ action->SetError(800, "Internal Error");
+ return NPT_SUCCESS;
+ }
+
+ items.SetPath("videodb://tvshows/titles/-1/");
+ return BuildResponse(action, items, filter, starting_index, requested_count, sort_criteria, context, NULL);
+ } else if (searchClass.Find("object.item.videoItem") >= 0) {
+ CFileItemList items, allItems;
+
+ CVideoDatabase database;
+ if (!database.Open()) {
+ action->SetError(800, "Internal Error");
+ return NPT_SUCCESS;
+ }
+
+ bool allVideoItems = searchClass.Compare("object.item.videoItem") == 0;
+
+ // determine the required videodb details to be retrieved
+ int requiredVideoDbDetails = GetRequiredVideoDbDetails(NPT_String(filter));
+
+ if (allVideoItems || searchClass.Find("object.item.videoItem.movie") >= 0)
+ {
+ if (!database.GetMoviesByWhere("videodb://movies/titles/?local", CDatabase::Filter(), items, SortDescription(), requiredVideoDbDetails)) {
+ action->SetError(800, "Internal Error");
+ return NPT_SUCCESS;
+ }
+
+ allItems.Append(items);
+ items.Clear();
+
+ if (!allVideoItems)
+ allItems.SetPath("videodb://movies/titles/");
+ }
+
+ if (allVideoItems || searchClass.Find("object.item.videoItem.videoBroadcast") >= 0)
+ {
+ if (!database.GetEpisodesByWhere("videodb://tvshows/titles/?local", CDatabase::Filter(), items, true, SortDescription(), requiredVideoDbDetails)) {
+ action->SetError(800, "Internal Error");
+ return NPT_SUCCESS;
+ }
+
+ allItems.Append(items);
+ items.Clear();
+
+ if (!allVideoItems)
+ allItems.SetPath("videodb://tvshows/titles/");
+ }
+
+ if (allVideoItems || searchClass.Find("object.item.videoItem.musicVideoClip") >= 0)
+ {
+ if (!database.GetMusicVideosByWhere("videodb://musicvideos/titles/?local", CDatabase::Filter(), items, true, SortDescription(), requiredVideoDbDetails)) {
+ action->SetError(800, "Internal Error");
+ return NPT_SUCCESS;
+ }
+
+ allItems.Append(items);
+ items.Clear();
+
+ if (!allVideoItems)
+ allItems.SetPath("videodb://musicvideos/titles/");
+ }
+
+ if (allVideoItems)
+ allItems.SetPath("videodb://movies/titles/");
+
+ return BuildResponse(action, allItems, filter, starting_index, requested_count, sort_criteria, context, NULL);
+ } else if (searchClass.Find("object.item.imageItem") >= 0) {
+ CFileItemList items;
+ return BuildResponse(action, items, filter, starting_index, requested_count, sort_criteria, context, NULL);
+ }
+
+ return NPT_FAILURE;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::OnUpdateObject
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPServer::OnUpdateObject(PLT_ActionReference& action,
+ const char* object_id,
+ NPT_Map<NPT_String,NPT_String>& current_vals,
+ NPT_Map<NPT_String,NPT_String>& new_vals,
+ const PLT_HttpRequestContext& context)
+{
+ std::string path(CURL::Decode(object_id));
+ CFileItem updated;
+ updated.SetPath(path);
+ m_logger->info("OnUpdateObject: {} from {}", path,
+ (const char*)context.GetRemoteAddress().GetIpAddress().ToString());
+
+ NPT_String playCount, position, lastPlayed;
+ int err;
+ const char* msg = NULL;
+ bool updatelisting(false);
+
+ // we pause eventing as multiple announces may happen in this operation
+ PLT_Service* service = NULL;
+ NPT_CHECK_LABEL(FindServiceById("urn:upnp-org:serviceId:ContentDirectory", service), error);
+ NPT_CHECK_LABEL(service->PauseEventing(), error);
+
+ if (updated.IsVideoDb()) {
+ CVideoDatabase db;
+ NPT_CHECK_LABEL(!db.Open(), error);
+
+ // must first determine type of file from object id
+ VIDEODATABASEDIRECTORY::CQueryParams params;
+ VIDEODATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo(path.c_str(), params);
+
+ int id = -1;
+ VideoDbContentType content_type;
+ if ((id = params.GetMovieId()) >= 0 )
+ content_type = VideoDbContentType::MOVIES;
+ else if ((id = params.GetEpisodeId()) >= 0 )
+ content_type = VideoDbContentType::EPISODES;
+ else if ((id = params.GetMVideoId()) >= 0 )
+ content_type = VideoDbContentType::MUSICVIDEOS;
+ else {
+ err = 701;
+ msg = "No such object";
+ goto failure;
+ }
+
+ std::string file_path;
+ db.GetFilePathById(id, file_path, content_type);
+ CVideoInfoTag tag;
+ db.LoadVideoInfo(file_path, tag);
+ updated.SetFromVideoInfoTag(tag);
+ m_logger->info("Translated to {}", file_path);
+
+ position = new_vals["lastPlaybackPosition"];
+ playCount = new_vals["playCount"];
+ lastPlayed = new_vals["lastPlaybackTime"];
+
+
+ if (!position.IsEmpty()
+ && position.Compare(current_vals["lastPlaybackPosition"]) != 0) {
+ NPT_UInt32 resume;
+ NPT_CHECK_LABEL(position.ToInteger32(resume), args);
+
+ if (resume <= 0)
+ db.ClearBookMarksOfFile(file_path, CBookmark::RESUME);
+ else {
+ CBookmark bookmark;
+ bookmark.timeInSeconds = resume;
+ bookmark.totalTimeInSeconds = resume + 100; // not required to be correct
+ bookmark.playerState = new_vals["lastPlayerState"];
+
+ db.AddBookMarkToFile(file_path, bookmark, CBookmark::RESUME);
+ }
+ if (playCount.IsEmpty()) {
+ CVariant data;
+ data["id"] = updated.GetVideoInfoTag()->m_iDbId;
+ data["type"] = updated.GetVideoInfoTag()->m_type;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::VideoLibrary,
+ "OnUpdate", data);
+ }
+ updatelisting = true;
+ }
+
+ if (!playCount.IsEmpty()
+ && playCount.Compare(current_vals["playCount"]) != 0) {
+
+ NPT_UInt32 count;
+ NPT_CHECK_LABEL(playCount.ToInteger32(count), args);
+
+ CDateTime lastPlayedObj;
+ if (!lastPlayed.IsEmpty() &&
+ lastPlayed.Compare(current_vals["lastPlaybackTime"]) != 0)
+ lastPlayedObj.SetFromW3CDateTime(lastPlayed.GetChars());
+
+ db.SetPlayCount(updated, count, lastPlayedObj);
+ updatelisting = true;
+ }
+
+ // we must load the changed settings before propagating to local UI
+ if (updatelisting) {
+ db.LoadVideoInfo(file_path, tag);
+ updated.SetFromVideoInfoTag(tag);
+ //! TODO: we should find a way to avoid obtaining the artwork just to
+ // update the playcount or similar properties. Maybe a flag in the GUI
+ // update message to inform we should only update the playback properties
+ // without touching other parts of the item.
+ CVideoThumbLoader().FillLibraryArt(updated);
+ }
+
+ } else if (updated.IsMusicDb()) {
+ //! @todo implement this
+
+ } else {
+ err = 701;
+ msg = "No such object";
+ goto failure;
+ }
+
+ if (updatelisting) {
+ updated.SetPath(path);
+ if (updated.IsVideoDb())
+ CUtil::DeleteVideoDatabaseDirectoryCache();
+ else if (updated.IsMusicDb())
+ CUtil::DeleteMusicDatabaseDirectoryCache();
+
+ CFileItemPtr msgItem(new CFileItem(updated));
+ CGUIMessage message(GUI_MSG_NOTIFY_ALL, CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow(), 0, GUI_MSG_UPDATE_ITEM, GUI_MSG_FLAG_UPDATE_LIST, msgItem);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(message);
+ }
+
+ NPT_CHECK_LABEL(service->PauseEventing(false), error);
+ return NPT_SUCCESS;
+
+args:
+ err = 402;
+ msg = "Invalid args";
+ goto failure;
+
+error:
+ err = 501;
+ msg = "Internal error";
+
+failure:
+ m_logger->error("OnUpdateObject failed with err {}: {}", err, msg);
+ action->SetError(err, msg);
+ service->PauseEventing(false);
+ return NPT_FAILURE;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::ServeFile
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPServer::ServeFile(const NPT_HttpRequest& request,
+ const NPT_HttpRequestContext& context,
+ NPT_HttpResponse& response,
+ const NPT_String& md5)
+{
+ // Translate hash to filename
+ NPT_String file_path(md5), *file_path2;
+ { NPT_AutoLock lock(m_FileMutex);
+ if(NPT_SUCCEEDED(m_FileMap.Get(md5, file_path2))) {
+ file_path = *file_path2;
+ m_logger->debug("Received request to serve '{}' = '{}'", (const char*)md5,
+ (const char*)file_path);
+ } else {
+ m_logger->debug("Received request to serve unknown md5 '{}'", (const char*)md5);
+ response.SetStatus(404, "File Not Found");
+ return NPT_SUCCESS;
+ }
+ }
+
+ // File requested
+ NPT_HttpUrl rooturi(context.GetLocalAddress().GetIpAddress().ToString(), context.GetLocalAddress().GetPort(), "/");
+
+ if (file_path.Left(8).Compare("stack://", true) == 0) {
+
+ NPT_List<NPT_String> files = file_path.SubString(8).Split(" , ");
+ if (files.GetItemCount() == 0) {
+ response.SetStatus(404, "File Not Found");
+ return NPT_SUCCESS;
+ }
+
+ NPT_String output;
+ output.Reserve(file_path.GetLength()*2);
+ output += "#EXTM3U\r\n";
+
+ NPT_List<NPT_String>::Iterator url = files.GetFirstItem();
+ for (;url;url++) {
+ output += ("#EXTINF:-1," + URIUtils::GetFileName((const char*)*url)).c_str();
+ output += "\r\n";
+ output += BuildSafeResourceUri(
+ rooturi,
+ context.GetLocalAddress().GetIpAddress().ToString(),
+ *url);
+ output += "\r\n";
+ }
+
+ PLT_HttpHelper::SetBody(response, (const char*)output, output.GetLength());
+ response.GetHeaders().SetHeader("Content-Disposition", "inline; filename=\"stack.m3u\"");
+ return NPT_SUCCESS;
+ }
+
+ if (URIUtils::IsURL(static_cast<const char*>(file_path)))
+ {
+ CURL url(CTextureUtils::UnwrapImageURL(static_cast<const char*>(file_path)));
+ std::string disp = "inline; filename=\"" + URIUtils::GetFileName(url) + "\"";
+ response.GetHeaders().SetHeader("Content-Disposition", disp.c_str());
+ }
+
+ // set getCaptionInfo.sec - sets subtitle uri for Samsung devices
+ const NPT_String* captionInfoHeader = request.GetHeaders().GetHeaderValue("getCaptionInfo.sec");
+ if (captionInfoHeader)
+ {
+ NPT_String *sub_uri, movie;
+ movie = "subtitle://" + md5;
+
+ NPT_AutoLock lock(m_FileMutex);
+ if (NPT_SUCCEEDED(m_FileMap.Get(movie, sub_uri)))
+ {
+ response.GetHeaders().SetHeader("CaptionInfo.sec", sub_uri->GetChars(), false);
+ }
+ }
+
+ return PLT_HttpServer::ServeFile(request,
+ context,
+ response,
+ file_path);
+}
+
+void
+CUPnPServer::DefaultSortItems(CFileItemList& items)
+{
+ CGUIViewState* viewState = CGUIViewState::GetViewState(items.IsVideoDb() ? WINDOW_VIDEO_NAV : -1, items);
+ if (viewState)
+ {
+ SortDescription sorting = viewState->GetSortMethod();
+ items.Sort(sorting.sortBy, sorting.sortOrder, sorting.sortAttributes);
+ delete viewState;
+ }
+}
+
+NPT_Result CUPnPServer::AddSubtitleUriForSecResponse(const NPT_String& movie_md5,
+ const NPT_String& subtitle_uri)
+{
+ /* using existing m_FileMap to store subtitle uri for movie,
+ adding subtitle:// prefix, because there is already entry for movie md5 with movie path */
+ NPT_String movie = "subtitle://" + movie_md5;
+
+ NPT_AutoLock lock(m_FileMutex);
+ NPT_CHECK(m_FileMap.Put(movie, subtitle_uri));
+
+ return NPT_SUCCESS;
+}
+
+int CUPnPServer::GetRequiredVideoDbDetails(const NPT_String& filter)
+{
+ int details = VideoDbDetailsRating;
+ if (filter.Find("res@resolution") >= 0 || filter.Find("res@nrAudioChannels") >= 0)
+ details |= VideoDbDetailsStream;
+ if (filter.Find("upnp:actor") >= 0)
+ details |= VideoDbDetailsCast;
+
+ return details;
+}
+
+} /* namespace UPNP */
+