From c04dcc2e7d834218ef2d4194331e383402495ae1 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 10 Apr 2024 20:07:22 +0200 Subject: Adding upstream version 2:20.4+dfsg. Signed-off-by: Daniel Baumann --- xbmc/network/upnp/UPnPServer.cpp | 1388 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 1388 insertions(+) create mode 100644 xbmc/network/upnp/UPnPServer.cpp (limited to 'xbmc/network/upnp/UPnPServer.cpp') 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 + +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 >::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 >::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& item, + bool with_count, + const PLT_HttpRequestContext& context, + NPT_Reference& 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(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 object; + NPT_String id = TranslateWMPObjectId(object_id, m_logger); + CFileItemPtr item; + NPT_Reference 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(new CVideoThumbLoader()); + } + else if (item->IsMusicDb()) { + thumb_loader = NPT_Reference(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(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 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(new CVideoThumbLoader()); + } + else if (URIUtils::IsMusicDb(items.GetPath()) || + StringUtils::StartsWithNoCase(items.GetPath(), "special://profile/playlists/music/")) { + + thumb_loader = NPT_Reference(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; iGetPath(), "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; idebug("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& current_vals, + NPT_Map& 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 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::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(file_path))) + { + CURL url(CTextureUtils::UnwrapImageURL(static_cast(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 */ + -- cgit v1.2.3