summaryrefslogtreecommitdiffstats
path: root/xbmc/network/upnp/UPnPInternal.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'xbmc/network/upnp/UPnPInternal.cpp')
-rw-r--r--xbmc/network/upnp/UPnPInternal.cpp1245
1 files changed, 1245 insertions, 0 deletions
diff --git a/xbmc/network/upnp/UPnPInternal.cpp b/xbmc/network/upnp/UPnPInternal.cpp
new file mode 100644
index 0000000..4066157
--- /dev/null
+++ b/xbmc/network/upnp/UPnPInternal.cpp
@@ -0,0 +1,1245 @@
+/*
+ * 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 "UPnPInternal.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "TextureDatabase.h"
+#include "ThumbLoader.h"
+#include "UPnPServer.h"
+#include "URL.h"
+#include "Util.h"
+#include "filesystem/MusicDatabaseDirectory.h"
+#include "filesystem/StackDirectory.h"
+#include "filesystem/VideoDatabaseDirectory.h"
+#include "music/tags/MusicInfoTag.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "utils/ContentUtils.h"
+#include "utils/LangCodeExpander.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+#include "video/VideoInfoTag.h"
+
+#include <algorithm>
+#include <array>
+#include <string_view>
+
+#include <Platinum/Source/Platinum/Platinum.h>
+
+using namespace MUSIC_INFO;
+using namespace XFILE;
+
+namespace UPNP
+{
+
+// the original version of content type here,eg: text/srt,which was defined 10 years ago (year 2013,commit 56519bec #L1158-L1161 )
+// is not a standard mime type. according to the specs of UPNP
+// http://upnp.org/specs/av/UPnP-av-ConnectionManager-v3-Service.pdf chapter "A.1.1 ProtocolInfo Definition"
+// "The <contentFormat> part for HTTP GET is described by a MIME type RFC https://www.ietf.org/rfc/rfc1341.txt"
+// all the pre-defined "text/*" MIME by IANA is here https://www.iana.org/assignments/media-types/media-types.xhtml#text
+// there is not any subtitle MIME for now (year 2022), we used to use text/srt|ssa|sub|idx, but,
+// kodi support SUP subtitle now, and SUP subtitle is not really a text(see below), to keep it
+// compatible, we suggest only to match the extension
+//
+// main purpose of this array is to share supported real subtitle formats when kodi act as a UPNP
+// server or UPNP/DLNA media render
+constexpr std::array<std::string_view, 9> SupportedSubFormats = {
+ "txt", "srt", "ssa", "ass", "sub", "smi", "vtt",
+ // "sup" subtitle is not a real TEXT,
+ // and there is no real STD subtitle RFC of DLNA,
+ // so we only match the extension of the "fake" content type
+ "sup", "idx"};
+
+// Map defining extensions for mimetypes not available in Platinum mimetype map
+// or that the application wants to override. These definitions take precedence
+// over all other possible mime type definitions.
+constexpr NPT_HttpFileRequestHandler_DefaultFileTypeMapEntry kodiPlatinumMimeTypeExtensions[] = {
+ {"m2ts", "video/vnd.dlna.mpeg-tts"}};
+
+/*----------------------------------------------------------------------
+| GetClientQuirks
++---------------------------------------------------------------------*/
+EClientQuirks GetClientQuirks(const PLT_HttpRequestContext* context)
+{
+ if(context == NULL)
+ return ECLIENTQUIRKS_NONE;
+
+ unsigned int quirks = 0;
+ const NPT_String* user_agent = context->GetRequest().GetHeaders().GetHeaderValue(NPT_HTTP_HEADER_USER_AGENT);
+ const NPT_String* server = context->GetRequest().GetHeaders().GetHeaderValue(NPT_HTTP_HEADER_SERVER);
+
+ if (user_agent) {
+ if (user_agent->Find("XBox", 0, true) >= 0 ||
+ user_agent->Find("Xenon", 0, true) >= 0)
+ quirks |= ECLIENTQUIRKS_ONLYSTORAGEFOLDER | ECLIENTQUIRKS_BASICVIDEOCLASS;
+
+ if (user_agent->Find("Windows-Media-Player", 0, true) >= 0)
+ quirks |= ECLIENTQUIRKS_UNKNOWNSERIES;
+
+ }
+ if (server) {
+ if (server->Find("Xbox", 0, true) >= 0)
+ quirks |= ECLIENTQUIRKS_ONLYSTORAGEFOLDER | ECLIENTQUIRKS_BASICVIDEOCLASS;
+ }
+
+ return (EClientQuirks)quirks;
+}
+
+/*----------------------------------------------------------------------
+| GetMediaControllerQuirks
++---------------------------------------------------------------------*/
+EMediaControllerQuirks GetMediaControllerQuirks(const PLT_DeviceData *device)
+{
+ if (device == NULL)
+ return EMEDIACONTROLLERQUIRKS_NONE;
+
+ unsigned int quirks = 0;
+
+ if (device->m_Manufacturer.Find("Samsung Electronics") >= 0)
+ quirks |= EMEDIACONTROLLERQUIRKS_X_MKV;
+
+ return (EMediaControllerQuirks)quirks;
+}
+
+/*----------------------------------------------------------------------
+| GetMimeType
++---------------------------------------------------------------------*/
+NPT_String
+GetMimeType(const char* filename,
+ const PLT_HttpRequestContext* context /* = NULL */)
+{
+ NPT_String ext = URIUtils::GetExtension(filename).c_str();
+ ext.TrimLeft('.');
+ ext = ext.ToLowercase();
+
+ return PLT_MimeType::GetMimeTypeFromExtension(ext, context);
+}
+
+/*----------------------------------------------------------------------
+| GetMimeType
++---------------------------------------------------------------------*/
+NPT_String
+GetMimeType(const CFileItem& item,
+ const PLT_HttpRequestContext* context /* = NULL */)
+{
+ std::string path = item.GetPath();
+ if (item.HasVideoInfoTag() && !item.GetVideoInfoTag()->GetPath().empty()) {
+ path = item.GetVideoInfoTag()->GetPath();
+ } else if (item.HasMusicInfoTag() && !item.GetMusicInfoTag()->GetURL().empty()) {
+ path = item.GetMusicInfoTag()->GetURL();
+ }
+
+ if (URIUtils::IsStack(path))
+ path = XFILE::CStackDirectory::GetFirstStackedFile(path);
+
+ NPT_String ext = URIUtils::GetExtension(path).c_str();
+ ext.TrimLeft('.');
+ ext = ext.ToLowercase();
+
+ NPT_String mime;
+
+ if (!ext.IsEmpty())
+ {
+ /* We look first to our extensions/overrides of libplatinum mimetypes. If not found, fallback to
+ Platinum definitions.
+ */
+ const auto kodiOverrideMimeType = std::find_if(
+ std::begin(kodiPlatinumMimeTypeExtensions), std::end(kodiPlatinumMimeTypeExtensions),
+ [&](const auto& mimeTypeEntry) { return mimeTypeEntry.extension == ext; });
+ if (kodiOverrideMimeType != std::end(kodiPlatinumMimeTypeExtensions))
+ {
+ mime = kodiOverrideMimeType->mime_type;
+ }
+ else
+ {
+ /* Give priority to Platinum mime types as they are defined to map extension to DLNA compliant mime types
+ or custom types according to context (who asked for it)
+ */
+ mime = PLT_MimeType::GetMimeTypeFromExtension(ext, context);
+ if (mime == "application/octet-stream")
+ {
+ mime = "";
+ }
+ }
+ }
+
+ /* if Platinum couldn't map it, default to Kodi internal mapping */
+ if (mime.IsEmpty()) {
+ NPT_String mime = item.GetMimeType().c_str();
+ if (mime == "application/octet-stream") mime = "";
+ }
+
+ /* fallback to generic mime type if not found */
+ if (mime.IsEmpty()) {
+ if (item.IsVideo() || item.IsVideoDb() )
+ mime = "video/" + ext;
+ else if (item.IsAudio() || item.IsMusicDb() )
+ mime = "audio/" + ext;
+ else if (item.IsPicture() )
+ mime = "image/" + ext;
+ else if (item.IsSubtitle())
+ mime = "text/" + ext;
+ }
+
+ /* nothing we can figure out */
+ if (mime.IsEmpty()) {
+ mime = "application/octet-stream";
+ }
+
+ return mime;
+}
+
+/*----------------------------------------------------------------------
+| GetProtocolInfo
++---------------------------------------------------------------------*/
+const NPT_String
+GetProtocolInfo(const CFileItem& item,
+ const char* protocol,
+ const PLT_HttpRequestContext* context /* = NULL */)
+{
+ NPT_String proto = protocol;
+
+ //! @todo fixup the protocol just in case nothing was passed
+ if (proto.IsEmpty()) {
+ proto = item.GetURL().GetProtocol().c_str();
+ }
+
+ /**
+ * map protocol to right prefix and use xbmc-get for
+ * unsupported UPnP protocols for other xbmc clients
+ * @todo add rtsp ?
+ */
+ if (proto == "http") {
+ proto = "http-get";
+ } else {
+ proto = "xbmc-get";
+ }
+
+ /* we need a valid extension to retrieve the mimetype for the protocol info */
+ NPT_String mime = GetMimeType(item, context);
+ proto += ":*:" + mime + ":" + PLT_ProtocolInfo::GetDlnaExtension(mime, context);
+ return proto;
+}
+
+ /*----------------------------------------------------------------------
+ | CResourceFinder
+ +---------------------------------------------------------------------*/
+CResourceFinder::CResourceFinder(const char* protocol, const char* content)
+ : m_Protocol(protocol)
+ , m_Content(content)
+{
+}
+
+bool CResourceFinder::operator()(const PLT_MediaItemResource& resource) const {
+ if (m_Content.IsEmpty())
+ return (resource.m_ProtocolInfo.GetProtocol().Compare(m_Protocol, true) == 0);
+ else
+ return ((resource.m_ProtocolInfo.GetProtocol().Compare(m_Protocol, true) == 0)
+ && resource.m_ProtocolInfo.GetContentType().StartsWith(m_Content, true));
+}
+
+/*----------------------------------------------------------------------
+| PopulateObjectFromTag
++---------------------------------------------------------------------*/
+NPT_Result
+PopulateObjectFromTag(CMusicInfoTag& tag,
+ PLT_MediaObject& object,
+ NPT_String* file_path,
+ PLT_MediaItemResource* resource,
+ EClientQuirks quirks,
+ UPnPService service /* = UPnPServiceNone */)
+{
+ if (!tag.GetURL().empty() && file_path)
+ *file_path = tag.GetURL().c_str();
+
+ std::vector<std::string> genres = tag.GetGenre();
+ for (unsigned int index = 0; index < genres.size(); index++)
+ object.m_Affiliation.genres.Add(genres.at(index).c_str());
+ object.m_Title = tag.GetTitle().c_str();
+ object.m_Affiliation.album = tag.GetAlbum().c_str();
+ for (unsigned int index = 0; index < tag.GetArtist().size(); index++)
+ {
+ object.m_People.artists.Add(tag.GetArtist().at(index).c_str());
+ object.m_People.artists.Add(tag.GetArtist().at(index).c_str(), "Performer");
+ }
+ object.m_People.artists.Add((!tag.GetAlbumArtistString().empty() ? tag.GetAlbumArtistString() : tag.GetArtistString()).c_str(), "AlbumArtist");
+ if(tag.GetAlbumArtistString().empty())
+ object.m_Creator = tag.GetArtistString().c_str();
+ else
+ object.m_Creator = tag.GetAlbumArtistString().c_str();
+ object.m_MiscInfo.original_track_number = tag.GetTrackNumber();
+ if(tag.GetDatabaseId() >= 0) {
+ object.m_ReferenceID = NPT_String::Format("musicdb://songs/%i%s", tag.GetDatabaseId(), URIUtils::GetExtension(tag.GetURL()).c_str());
+ }
+ if (object.m_ReferenceID == object.m_ObjectID)
+ object.m_ReferenceID = "";
+
+ object.m_MiscInfo.last_time = tag.GetLastPlayed().GetAsW3CDateTime().c_str();
+ object.m_MiscInfo.play_count = tag.GetPlayCount();
+
+ if (resource) resource->m_Duration = tag.GetDuration();
+
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| PopulateObjectFromTag
++---------------------------------------------------------------------*/
+NPT_Result
+PopulateObjectFromTag(CVideoInfoTag& tag,
+ PLT_MediaObject& object,
+ NPT_String* file_path,
+ PLT_MediaItemResource* resource,
+ EClientQuirks quirks,
+ UPnPService service /* = UPnPServiceNone */)
+{
+ if (!tag.m_strFileNameAndPath.empty() && file_path)
+ *file_path = tag.m_strFileNameAndPath.c_str();
+
+ if (tag.m_iDbId != -1 ) {
+ if (tag.m_type == MediaTypeMusicVideo) {
+ object.m_ObjectClass.type = "object.item.videoItem.musicVideoClip";
+ object.m_Creator = StringUtils::Join(tag.m_artist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator).c_str();
+ for (const auto& itArtist : tag.m_artist)
+ object.m_People.artists.Add(itArtist.c_str());
+ object.m_Affiliation.album = tag.m_strAlbum.c_str();
+ object.m_Title = tag.m_strTitle.c_str();
+ object.m_Date = tag.GetPremiered().GetAsW3CDate().c_str();
+ object.m_ReferenceID = NPT_String::Format("videodb://musicvideos/titles/%i", tag.m_iDbId);
+ } else if (tag.m_type == MediaTypeMovie) {
+ object.m_ObjectClass.type = "object.item.videoItem.movie";
+ object.m_Title = tag.m_strTitle.c_str();
+ object.m_Date = tag.GetPremiered().GetAsW3CDate().c_str();
+ object.m_ReferenceID = NPT_String::Format("videodb://movies/titles/%i", tag.m_iDbId);
+ } else {
+ object.m_Recorded.series_title = tag.m_strShowTitle.c_str();
+
+ if (tag.m_type == MediaTypeTvShow) {
+ object.m_ObjectClass.type = "object.container.album.videoAlbum.videoBroadcastShow";
+ object.m_Title = tag.m_strTitle.c_str();
+ object.m_Recorded.episode_number = tag.m_iEpisode;
+ object.m_Recorded.episode_count = tag.m_iEpisode;
+ if (!tag.m_premiered.IsValid() && tag.GetYear() > 0)
+ object.m_Date = CDateTime(tag.GetYear(), 1, 1, 0, 0, 0).GetAsW3CDate().c_str();
+ else
+ object.m_Date = tag.m_premiered.GetAsW3CDate().c_str();
+ object.m_ReferenceID = NPT_String::Format("videodb://tvshows/titles/%i", tag.m_iDbId);
+ } else if (tag.m_type == MediaTypeSeason) {
+ object.m_ObjectClass.type = "object.container.album.videoAlbum.videoBroadcastSeason";
+ object.m_Title = tag.m_strTitle.c_str();
+ object.m_Recorded.episode_season = tag.m_iSeason;
+ object.m_Recorded.episode_count = tag.m_iEpisode;
+ if (!tag.m_premiered.IsValid() && tag.GetYear() > 0)
+ object.m_Date = CDateTime(tag.GetYear(), 1, 1, 0, 0, 0).GetAsW3CDate().c_str();
+ else
+ object.m_Date = tag.m_premiered.GetAsW3CDate().c_str();
+ object.m_ReferenceID = NPT_String::Format("videodb://tvshows/titles/%i/%i", tag.m_iIdShow, tag.m_iSeason);
+ } else {
+ object.m_ObjectClass.type = "object.item.videoItem.videoBroadcast";
+ object.m_Recorded.program_title = "S" + ("0" + NPT_String::FromInteger(tag.m_iSeason)).Right(2);
+ object.m_Recorded.program_title += "E" + ("0" + NPT_String::FromInteger(tag.m_iEpisode)).Right(2);
+ object.m_Recorded.program_title += (" : " + tag.m_strTitle).c_str();
+ object.m_Recorded.episode_number = tag.m_iEpisode;
+ object.m_Recorded.episode_season = tag.m_iSeason;
+ object.m_Title = object.m_Recorded.series_title + " - " + object.m_Recorded.program_title;
+ object.m_ReferenceID = NPT_String::Format("videodb://tvshows/titles/%i/%i/%i", tag.m_iIdShow, tag.m_iSeason, tag.m_iDbId);
+ object.m_Date = tag.m_firstAired.GetAsW3CDate().c_str();
+ }
+ }
+ }
+
+ if(quirks & ECLIENTQUIRKS_BASICVIDEOCLASS)
+ object.m_ObjectClass.type = "object.item.videoItem";
+
+ if(object.m_ReferenceID == object.m_ObjectID)
+ object.m_ReferenceID = "";
+
+ for (unsigned int index = 0; index < tag.m_studio.size(); index++)
+ object.m_People.publisher.Add(tag.m_studio[index].c_str());
+
+ object.m_XbmcInfo.date_added = tag.m_dateAdded.GetAsW3CDate().c_str();
+ object.m_XbmcInfo.rating = tag.GetRating().rating;
+ object.m_XbmcInfo.votes = tag.GetRating().votes;
+ object.m_XbmcInfo.unique_identifier = tag.GetUniqueID().c_str();
+ for (const auto& country : tag.m_country)
+ object.m_XbmcInfo.countries.Add(country.c_str());
+ object.m_XbmcInfo.user_rating = tag.m_iUserRating;
+
+ for (unsigned int index = 0; index < tag.m_genre.size(); index++)
+ object.m_Affiliation.genres.Add(tag.m_genre.at(index).c_str());
+
+ for (CVideoInfoTag::iCast it = tag.m_cast.begin(); it != tag.m_cast.end(); ++it)
+ {
+ object.m_People.actors.Add(it->strName.c_str(), it->strRole.c_str());
+ }
+
+ for (unsigned int index = 0; index < tag.m_director.size(); index++)
+ object.m_People.directors.Add(tag.m_director[index].c_str());
+
+ for (unsigned int index = 0; index < tag.m_writingCredits.size(); index++)
+ object.m_People.authors.Add(tag.m_writingCredits[index].c_str());
+
+ object.m_Description.description = tag.m_strTagLine.c_str();
+ object.m_Description.long_description = tag.m_strPlot.c_str();
+ object.m_Description.rating = tag.m_strMPAARating.c_str();
+ object.m_MiscInfo.last_position = (NPT_UInt32)tag.GetResumePoint().timeInSeconds;
+ object.m_XbmcInfo.last_playerstate = tag.GetResumePoint().playerState.c_str();
+ object.m_MiscInfo.last_time = tag.m_lastPlayed.GetAsW3CDateTime().c_str();
+ object.m_MiscInfo.play_count = tag.GetPlayCount();
+ if (resource) {
+ resource->m_Duration = tag.GetDuration();
+ if (tag.HasStreamDetails()) {
+ const CStreamDetails &details = tag.m_streamDetails;
+ resource->m_Resolution = NPT_String::FromInteger(details.GetVideoWidth()) + "x" + NPT_String::FromInteger(details.GetVideoHeight());
+ resource->m_NbAudioChannels = details.GetAudioChannels();
+ }
+ }
+
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| BuildObject
++---------------------------------------------------------------------*/
+PLT_MediaObject*
+BuildObject(CFileItem& item,
+ NPT_String& file_path,
+ bool with_count,
+ NPT_Reference<CThumbLoader>& thumb_loader,
+ const PLT_HttpRequestContext* context /* = NULL */,
+ CUPnPServer* upnp_server /* = NULL */,
+ UPnPService upnp_service /* = UPnPServiceNone */)
+{
+ static Logger logger = CServiceBroker::GetLogging().GetLogger("UPNP::BuildObject");
+
+ PLT_MediaItemResource resource;
+ PLT_MediaObject* object = NULL;
+ std::string thumb;
+
+ logger->debug("Building didl for object '{}'", item.GetPath());
+
+ auto settingsComponent = CServiceBroker::GetSettingsComponent();
+ if (!settingsComponent)
+ return nullptr;
+
+ auto settings = settingsComponent->GetSettings();
+ if (!settings)
+ return nullptr;
+
+ EClientQuirks quirks = GetClientQuirks(context);
+
+ // get list of ip addresses
+ NPT_List<NPT_IpAddress> ips;
+ NPT_HttpUrl rooturi;
+ NPT_CHECK_LABEL(PLT_UPnPMessageHelper::GetIPAddresses(ips), failure);
+
+ // if we're passed an interface where we received the request from
+ // move the ip to the top
+ if (context && context->GetLocalAddress().GetIpAddress().ToString() != "0.0.0.0")
+ {
+ rooturi = NPT_HttpUrl(context->GetLocalAddress().GetIpAddress().ToString(),
+ context->GetLocalAddress().GetPort(), "/");
+ ips.Remove(context->GetLocalAddress().GetIpAddress());
+ ips.Insert(ips.GetFirstItem(), context->GetLocalAddress().GetIpAddress());
+ } else if(upnp_server) {
+ rooturi = NPT_HttpUrl("localhost", upnp_server->GetPort(), "/");
+ }
+
+ if (!item.m_bIsFolder) {
+ object = new PLT_MediaItem();
+ object->m_ObjectID = item.GetPath().c_str();
+
+ /* Setup object type */
+ if (item.IsMusicDb() || item.IsAudio()) {
+ object->m_ObjectClass.type = "object.item.audioItem.musicTrack";
+
+ if (item.HasMusicInfoTag()) {
+ CMusicInfoTag *tag = item.GetMusicInfoTag();
+ PopulateObjectFromTag(*tag, *object, &file_path, &resource, quirks, upnp_service);
+ }
+ } else if (item.IsVideoDb() || item.IsVideo()) {
+ object->m_ObjectClass.type = "object.item.videoItem";
+
+ if(quirks & ECLIENTQUIRKS_UNKNOWNSERIES)
+ object->m_Affiliation.album = "[Unknown Series]";
+
+ if (item.HasVideoInfoTag()) {
+ CVideoInfoTag *tag = item.GetVideoInfoTag();
+ PopulateObjectFromTag(*tag, *object, &file_path, &resource, quirks, upnp_service);
+ }
+ } else if (item.IsPicture()) {
+ object->m_ObjectClass.type = "object.item.imageItem.photo";
+ } else {
+ object->m_ObjectClass.type = "object.item";
+ }
+
+ // duration of zero is invalid
+ if (resource.m_Duration == 0) resource.m_Duration = -1;
+
+ // Set the resource file size
+ resource.m_Size = item.m_dwSize;
+ if(resource.m_Size == 0)
+ resource.m_Size = (NPT_LargeSize)-1;
+
+ // set date
+ if (object->m_Date.IsEmpty() && item.m_dateTime.IsValid()) {
+ object->m_Date = item.m_dateTime.GetAsW3CDate().c_str();
+ }
+
+ if (upnp_server) {
+ upnp_server->AddSafeResourceUri(object, rooturi, ips, file_path, GetProtocolInfo(item, "http", context));
+ }
+
+ // if the item is remote, add a direct link to the item
+ if (URIUtils::IsRemote((const char*)file_path)) {
+ resource.m_ProtocolInfo = PLT_ProtocolInfo(GetProtocolInfo(item, item.GetURL().GetProtocol().c_str(), context));
+ resource.m_Uri = file_path;
+
+ // if the direct link can be served directly using http, then push it in front
+ // otherwise keep the xbmc-get resource last and let a compatible client look for it
+ if (resource.m_ProtocolInfo.ToString().StartsWith("xbmc", true)) {
+ object->m_Resources.Add(resource);
+ } else {
+ object->m_Resources.Insert(object->m_Resources.GetFirstItem(), resource);
+ }
+ }
+
+ // copy across the known metadata
+ for(unsigned i=0; i<object->m_Resources.GetItemCount(); i++) {
+ object->m_Resources[i].m_Size = resource.m_Size;
+ object->m_Resources[i].m_Duration = resource.m_Duration;
+ object->m_Resources[i].m_Resolution = resource.m_Resolution;
+ }
+
+ // Some upnp clients expect all audio items to have parent root id 4
+#ifdef WMP_ID_MAPPING
+ object->m_ParentID = "4";
+#endif
+ } else {
+ PLT_MediaContainer* container = new PLT_MediaContainer;
+ object = container;
+
+ /* Assign a title and id for this container */
+ container->m_ObjectID = item.GetPath().c_str();
+ container->m_ObjectClass.type = "object.container";
+ container->m_ChildrenCount = -1;
+
+ /* this might be overkill, but hey */
+ if (item.IsMusicDb()) {
+ MUSICDATABASEDIRECTORY::NODE_TYPE node = CMusicDatabaseDirectory::GetDirectoryType(item.GetPath());
+ switch(node) {
+ case MUSICDATABASEDIRECTORY::NODE_TYPE_ARTIST: {
+ container->m_ObjectClass.type += ".person.musicArtist";
+ CMusicInfoTag *tag = item.GetMusicInfoTag();
+ if (tag) {
+ container->m_People.artists.Add(
+ CorrectAllItemsSortHack(tag->GetArtistString()).c_str(), "Performer");
+ container->m_People.artists.Add(
+ CorrectAllItemsSortHack((!tag->GetAlbumArtistString().empty() ? tag->GetAlbumArtistString() : tag->GetArtistString())).c_str(), "AlbumArtist");
+ }
+#ifdef WMP_ID_MAPPING
+ // Some upnp clients expect all artists to have parent root id 107
+ container->m_ParentID = "107";
+#endif
+ }
+ break;
+ case MUSICDATABASEDIRECTORY::NODE_TYPE_ALBUM:
+ case MUSICDATABASEDIRECTORY::NODE_TYPE_ALBUM_RECENTLY_ADDED: {
+ container->m_ObjectClass.type += ".album.musicAlbum";
+ // for Sonos to be happy
+ CMusicInfoTag *tag = item.GetMusicInfoTag();
+ if (tag) {
+ container->m_People.artists.Add(
+ CorrectAllItemsSortHack(tag->GetArtistString()).c_str(), "Performer");
+ container->m_People.artists.Add(
+ CorrectAllItemsSortHack(!tag->GetAlbumArtistString().empty() ? tag->GetAlbumArtistString() : tag->GetArtistString()).c_str(), "AlbumArtist");
+ container->m_Affiliation.album = CorrectAllItemsSortHack(tag->GetAlbum()).c_str();
+ }
+#ifdef WMP_ID_MAPPING
+ // Some upnp clients expect all albums to have parent root id 7
+ container->m_ParentID = "7";
+#endif
+ }
+ break;
+ case MUSICDATABASEDIRECTORY::NODE_TYPE_GENRE:
+ container->m_ObjectClass.type += ".genre.musicGenre";
+ break;
+ default:
+ break;
+ }
+ } else if (item.IsVideoDb()) {
+ VIDEODATABASEDIRECTORY::NODE_TYPE node = CVideoDatabaseDirectory::GetDirectoryType(item.GetPath());
+ CVideoInfoTag &tag = *item.GetVideoInfoTag();
+ switch(node) {
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_GENRE:
+ container->m_ObjectClass.type += ".genre.movieGenre";
+ break;
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_ACTOR:
+ container->m_ObjectClass.type += ".person.videoArtist";
+ container->m_Creator =
+ StringUtils::Join(
+ tag.m_artist,
+ settingsComponent->GetAdvancedSettings()->m_videoItemSeparator)
+ .c_str();
+ container->m_Title = tag.m_strTitle.c_str();
+ break;
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_SEASONS:
+ container->m_ObjectClass.type += ".album.videoAlbum.videoBroadcastSeason";
+ if (item.HasVideoInfoTag()) {
+ CVideoInfoTag *tag = (CVideoInfoTag*)item.GetVideoInfoTag();
+ PopulateObjectFromTag(*tag, *container, &file_path, &resource, quirks);
+ }
+ break;
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_TITLE_TVSHOWS:
+ container->m_ObjectClass.type += ".album.videoAlbum.videoBroadcastShow";
+ if (item.HasVideoInfoTag()) {
+ CVideoInfoTag *tag = (CVideoInfoTag*)item.GetVideoInfoTag();
+ PopulateObjectFromTag(*tag, *container, &file_path, &resource, quirks);
+ }
+ break;
+ default:
+ container->m_ObjectClass.type += ".storageFolder";
+ break;
+ }
+ } else if (item.IsPlayList() || item.IsSmartPlayList()) {
+ container->m_ObjectClass.type += ".playlistContainer";
+ }
+
+ if(quirks & ECLIENTQUIRKS_ONLYSTORAGEFOLDER) {
+ container->m_ObjectClass.type = "object.container.storageFolder";
+ }
+
+ /* Get the number of children for this container */
+ if (with_count && upnp_server) {
+ if (object->m_ObjectID.StartsWith("virtualpath://")) {
+ NPT_LargeSize count = 0;
+ NPT_CHECK_LABEL(NPT_File::GetSize(file_path, count), failure);
+ container->m_ChildrenCount = (NPT_Int32)count;
+ } else {
+ /* this should be a standard path */
+ //! @todo - get file count of this directory
+ }
+ }
+ }
+
+ // set a title for the object
+ if (object->m_Title.IsEmpty()) {
+ if (!item.GetLabel().empty()) {
+ std::string title = item.GetLabel();
+ if (item.IsPlayList() || !item.m_bIsFolder) URIUtils::RemoveExtension(title);
+ object->m_Title = title.c_str();
+ }
+ }
+
+ if (upnp_server) {
+ // determine the correct artwork for this item
+ if (!thumb_loader.IsNull())
+ thumb_loader->LoadItem(&item);
+
+ // we have to decide the best art type to serve to the client - use ContentUtils
+ // to get it since it depends on the mediatype of the item being served
+ thumb = ContentUtils::GetPreferredArtImage(item);
+
+ if (!thumb.empty()) {
+ PLT_AlbumArtInfo art;
+ // Set DLNA profileID by extension, defaulting to JPEG.
+ if (URIUtils::HasExtension(thumb, ".png"))
+ {
+ art.dlna_profile = "PNG_TN";
+ }
+ else
+ {
+ art.dlna_profile = "JPEG_TN";
+ }
+ // append /thumb to the safe resource uri to avoid clients flagging the item with
+ // the incorrect mimetype (derived from the file extension)
+ art.uri = upnp_server->BuildSafeResourceUri(
+ rooturi, (*ips.GetFirstItem()).ToString(),
+ std::string(CTextureUtils::GetWrappedImageURL(thumb) + "/thumb").c_str());
+ object->m_ExtraInfo.album_arts.Add(art);
+ }
+
+ for (const auto& itArtwork : item.GetArt())
+ {
+ if (!itArtwork.first.empty() && !itArtwork.second.empty())
+ {
+ std::string wrappedUrl = CTextureUtils::GetWrappedImageURL(itArtwork.second);
+ object->m_XbmcInfo.artwork.Add(
+ itArtwork.first.c_str(),
+ upnp_server->BuildSafeResourceUri(rooturi, (*ips.GetFirstItem()).ToString(),
+ wrappedUrl.c_str()));
+ upnp_server->AddSafeResourceUri(object, rooturi, ips, wrappedUrl.c_str(),
+ ("xbmc.org:*:" + itArtwork.first + ":*").c_str());
+ }
+ }
+ }
+
+ // look for and add external subtitle if we are processing a video file and
+ // we are being called by a UPnP player or renderer or the user has chosen
+ // to look for external subtitles
+ if (upnp_server != NULL && item.IsVideo() &&
+ (upnp_service == UPnPPlayer || upnp_service == UPnPRenderer ||
+ settings->GetBool(CSettings::SETTING_SERVICES_UPNPLOOKFOREXTERNALSUBTITLES)))
+ {
+ // find any available external subtitles
+ std::vector<std::string> filenames;
+ std::vector<std::string> subtitles;
+ CUtil::ScanForExternalSubtitles(file_path.GetChars(), filenames);
+
+ std::string ext;
+ for (unsigned int i = 0; i < filenames.size(); i++)
+ {
+ ext = URIUtils::GetExtension(filenames[i]).c_str();
+ ext = ext.substr(1);
+ std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
+ /* Hardcoded check for extension is not the best way, but it can't be allowed to pass all
+ subtitle extension (ex. rar or zip). There are the most popular extensions support by UPnP devices.*/
+ for (std::string_view type : SupportedSubFormats)
+ {
+ if (type == ext)
+ {
+ subtitles.push_back(filenames[i]);
+ }
+ }
+ }
+
+ std::string subtitlePath;
+
+ if (subtitles.size() == 1)
+ {
+ subtitlePath = subtitles[0];
+ }
+ else if (!subtitles.empty())
+ {
+ std::string preferredLanguage{"en"};
+
+ /* trying to find subtitle with preferred language settings */
+ auto setting = settings->GetSetting("locale.subtitlelanguage");
+ if (!setting)
+ CLog::Log(LOGERROR, "Failed to load setting for: {}", "locale.subtitlelanguage");
+ else
+ preferredLanguage = setting->ToString();
+
+ std::string preferredLanguageCode;
+ g_LangCodeExpander.ConvertToISO6392B(preferredLanguage, preferredLanguageCode);
+
+ for (unsigned int i = 0; i < subtitles.size(); i++)
+ {
+ ExternalStreamInfo info =
+ CUtil::GetExternalStreamDetailsFromFilename(file_path.GetChars(), subtitles[i]);
+
+ if (preferredLanguageCode == info.language)
+ {
+ subtitlePath = subtitles[i];
+ break;
+ }
+ }
+ /* if not found subtitle with preferred language, get the first one */
+ if (subtitlePath.empty())
+ {
+ subtitlePath = subtitles[0];
+ }
+ }
+
+ if (!subtitlePath.empty())
+ {
+ /* subtitles are added as 2 resources, 2 sec resources and 1 addon to video resource, to be compatible with
+ the most of the devices; all UPnP devices take the last one it could handle,
+ and skip ones it doesn't "understand" */
+ // add subtitle resource with standard protocolInfo
+ NPT_String protocolInfo = GetProtocolInfo(CFileItem(subtitlePath, false), "http", context);
+ upnp_server->AddSafeResourceUri(object, rooturi, ips, NPT_String(subtitlePath.c_str()), protocolInfo);
+ // add subtitle resource with smi/caption protocol info (some devices)
+ PLT_ProtocolInfo protInfo = PLT_ProtocolInfo(protocolInfo);
+ protocolInfo = protInfo.GetProtocol() + ":" + protInfo.GetMask() + ":smi/caption:" + protInfo.GetExtra();
+ upnp_server->AddSafeResourceUri(object, rooturi, ips, NPT_String(subtitlePath.c_str()), protocolInfo);
+
+ ext = URIUtils::GetExtension(subtitlePath).c_str();
+ ext = ext.substr(1);
+ std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
+
+ NPT_String subtitle_uri = object->m_Resources[object->m_Resources.GetItemCount() - 1].m_Uri;
+
+ // add subtitle to video resource (the first one) (for some devices)
+ object->m_Resources[0].m_CustomData["xmlns:pv"] = "http://www.pv.com/pvns/";
+ object->m_Resources[0].m_CustomData["pv:subtitleFileUri"] = subtitle_uri;
+ object->m_Resources[0].m_CustomData["pv:subtitleFileType"] = ext.c_str();
+
+ // for samsung devices
+ PLT_SecResource sec_res;
+ sec_res.name = "CaptionInfoEx";
+ sec_res.value = subtitle_uri;
+ sec_res.attributes["type"] = ext.c_str();
+ object->m_SecResources.Add(sec_res);
+ sec_res.name = "CaptionInfo";
+ object->m_SecResources.Add(sec_res);
+
+ // adding subtitle uri for movie md5, for later use in http response
+ NPT_String movie_md5 = object->m_Resources[0].m_Uri;
+ movie_md5 = movie_md5.Right(movie_md5.GetLength() - movie_md5.Find("/%25/") - 5);
+ upnp_server->AddSubtitleUriForSecResponse(movie_md5, subtitle_uri);
+ }
+ }
+
+ return object;
+
+failure:
+ delete object;
+ return NULL;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::CorrectAllItemsSortHack
++---------------------------------------------------------------------*/
+const std::string&
+CorrectAllItemsSortHack(const std::string &item)
+{
+ // This is required as in order for the "* All Albums" etc. items to sort
+ // correctly, they must have fake artist/album etc. information generated.
+ // This looks nasty if we attempt to render it to the GUI, thus this (further)
+ // workaround
+ if ((item.size() == 1 && item[0] == 0x01) || (item.size() > 1 && ((unsigned char) item[1]) == 0xff))
+ return StringUtils::Empty;
+
+ return item;
+}
+
+int
+PopulateTagFromObject(CMusicInfoTag& tag,
+ PLT_MediaObject& object,
+ PLT_MediaItemResource* resource /* = NULL */,
+ UPnPService service /* = UPnPServiceNone */)
+{
+ tag.SetTitle((const char*)object.m_Title);
+ tag.SetArtist((const char*)object.m_Creator);
+ for(PLT_PersonRoles::Iterator it = object.m_People.artists.GetFirstItem(); it; it++) {
+ if (it->role == "") tag.SetArtist((const char*)it->name);
+ else if(it->role == "Performer") tag.SetArtist((const char*)it->name);
+ else if(it->role == "AlbumArtist") tag.SetAlbumArtist((const char*)it->name);
+ }
+ tag.SetTrackNumber(object.m_MiscInfo.original_track_number);
+
+ for (NPT_List<NPT_String>::Iterator it = object.m_Affiliation.genres.GetFirstItem(); it; it++) {
+ // ignore single "Unknown" genre inserted by Platinum
+ if (it == object.m_Affiliation.genres.GetFirstItem() && object.m_Affiliation.genres.GetItemCount() == 1 &&
+ *it == "Unknown")
+ break;
+
+ tag.SetGenre((const char*) *it);
+ }
+
+ tag.SetAlbum((const char*)object.m_Affiliation.album);
+ CDateTime last;
+ last.SetFromW3CDateTime((const char*)object.m_MiscInfo.last_time);
+ tag.SetLastPlayed(last);
+ tag.SetPlayCount(object.m_MiscInfo.play_count);
+ if(resource)
+ tag.SetDuration(resource->m_Duration);
+ tag.SetLoaded();
+ return NPT_SUCCESS;
+}
+
+int
+PopulateTagFromObject(CVideoInfoTag& tag,
+ PLT_MediaObject& object,
+ PLT_MediaItemResource* resource /* = NULL */,
+ UPnPService service /* = UPnPServiceNone */)
+{
+ CDateTime date;
+ date.SetFromW3CDate((const char*)object.m_Date);
+
+ if(!object.m_Recorded.program_title.IsEmpty() || object.m_ObjectClass.type == "object.item.videoItem.videoBroadcast")
+ {
+ tag.m_type = MediaTypeEpisode;
+ tag.m_strShowTitle = object.m_Recorded.series_title;
+ if (date.IsValid())
+ tag.m_firstAired = date;
+
+ int title = object.m_Recorded.program_title.Find(" : ");
+ if (title >= 0)
+ tag.m_strTitle = object.m_Recorded.program_title.SubString(title + 3);
+ else
+ tag.m_strTitle = object.m_Recorded.program_title;
+
+ int episode;
+ int season;
+ if (object.m_Recorded.episode_number > 0 && object.m_Recorded.episode_season < (NPT_UInt32)-1) {
+ tag.m_iEpisode = object.m_Recorded.episode_number;
+ tag.m_iSeason = object.m_Recorded.episode_season;
+ } else if(sscanf(object.m_Recorded.program_title, "S%2dE%2d", &season, &episode) == 2 && title >= 0) {
+ tag.m_iEpisode = episode;
+ tag.m_iSeason = season;
+ } else {
+ tag.m_iSeason = object.m_Recorded.episode_number / 100;
+ tag.m_iEpisode = object.m_Recorded.episode_number % 100;
+ }
+ }
+ else {
+ tag.m_strTitle = object.m_Title;
+ if (date.IsValid())
+ tag.m_premiered = date;
+
+ if (!object.m_Recorded.series_title.IsEmpty()) {
+ if (object.m_ObjectClass.type == "object.container.album.videoAlbum.videoBroadcastSeason") {
+ tag.m_type = MediaTypeSeason;
+ tag.m_iSeason = object.m_Recorded.episode_season;
+ tag.m_strShowTitle = object.m_Recorded.series_title;
+ }
+ else {
+ tag.m_type = MediaTypeTvShow;
+ tag.m_strShowTitle = object.m_Title;
+ }
+
+ if (object.m_Recorded.episode_count > 0)
+ tag.m_iEpisode = object.m_Recorded.episode_count;
+ else
+ tag.m_iEpisode = object.m_Recorded.episode_number;
+ }
+ else if(object.m_ObjectClass.type == "object.item.videoItem.musicVideoClip") {
+ tag.m_type = MediaTypeMusicVideo;
+
+ if (object.m_People.artists.GetItemCount() > 0) {
+ for (unsigned int index = 0; index < object.m_People.artists.GetItemCount(); index++)
+ tag.m_artist.emplace_back(object.m_People.artists.GetItem(index)->name.GetChars());
+ }
+ else if (!object.m_Creator.IsEmpty() && object.m_Creator != "Unknown")
+ tag.m_artist = StringUtils::Split(object.m_Creator.GetChars(), CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+ tag.m_strAlbum = object.m_Affiliation.album;
+ }
+ else
+ tag.m_type = MediaTypeMovie;
+
+ tag.m_strTitle = object.m_Title;
+ if (date.IsValid())
+ tag.SetPremiered(date);
+ }
+
+ for (unsigned int index = 0; index < object.m_People.publisher.GetItemCount(); index++)
+ tag.m_studio.emplace_back(object.m_People.publisher.GetItem(index)->GetChars());
+
+ tag.m_dateAdded.SetFromW3CDate((const char*)object.m_XbmcInfo.date_added);
+ tag.SetRating(object.m_XbmcInfo.rating, object.m_XbmcInfo.votes);
+ tag.SetUniqueID(object.m_XbmcInfo.unique_identifier.GetChars());
+ for (unsigned int index = 0; index < object.m_XbmcInfo.countries.GetItemCount(); index++)
+ tag.m_country.emplace_back(object.m_XbmcInfo.countries.GetItem(index)->GetChars());
+ tag.m_iUserRating = object.m_XbmcInfo.user_rating;
+
+ for (unsigned int index = 0; index < object.m_Affiliation.genres.GetItemCount(); index++)
+ {
+ // ignore single "Unknown" genre inserted by Platinum
+ if (index == 0 && object.m_Affiliation.genres.GetItemCount() == 1 &&
+ *object.m_Affiliation.genres.GetItem(index) == "Unknown")
+ break;
+
+ tag.m_genre.emplace_back(object.m_Affiliation.genres.GetItem(index)->GetChars());
+ }
+ for (unsigned int index = 0; index < object.m_People.directors.GetItemCount(); index++)
+ tag.m_director.emplace_back(object.m_People.directors.GetItem(index)->name.GetChars());
+ for (unsigned int index = 0; index < object.m_People.authors.GetItemCount(); index++)
+ tag.m_writingCredits.emplace_back(object.m_People.authors.GetItem(index)->name.GetChars());
+ for (unsigned int index = 0; index < object.m_People.actors.GetItemCount(); index++)
+ {
+ SActorInfo info;
+ info.strName = object.m_People.actors.GetItem(index)->name;
+ info.strRole = object.m_People.actors.GetItem(index)->role;
+ tag.m_cast.push_back(info);
+ }
+ tag.m_strTagLine = object.m_Description.description;
+ tag.m_strPlot = object.m_Description.long_description;
+ tag.m_strMPAARating = object.m_Description.rating;
+ tag.m_strShowTitle = object.m_Recorded.series_title;
+ tag.m_lastPlayed.SetFromW3CDateTime((const char*)object.m_MiscInfo.last_time);
+ tag.SetPlayCount(object.m_MiscInfo.play_count);
+
+ if(resource)
+ {
+ if (resource->m_Duration)
+ tag.SetDuration(resource->m_Duration);
+ if (object.m_MiscInfo.last_position > 0 )
+ {
+ tag.SetResumePoint(object.m_MiscInfo.last_position,
+ resource->m_Duration,
+ object.m_XbmcInfo.last_playerstate.GetChars());
+ }
+ if (!resource->m_Resolution.IsEmpty())
+ {
+ int width, height;
+ if (sscanf(resource->m_Resolution, "%dx%d", &width, &height) == 2)
+ {
+ CStreamDetailVideo* detail = new CStreamDetailVideo;
+ detail->m_iWidth = width;
+ detail->m_iHeight = height;
+ detail->m_iDuration = tag.GetDuration();
+ tag.m_streamDetails.AddStream(detail);
+ }
+ }
+ if (resource->m_NbAudioChannels > 0)
+ {
+ CStreamDetailAudio* detail = new CStreamDetailAudio;
+ detail->m_iChannels = resource->m_NbAudioChannels;
+ tag.m_streamDetails.AddStream(detail);
+ }
+ }
+ return NPT_SUCCESS;
+}
+
+std::shared_ptr<CFileItem> BuildObject(PLT_MediaObject* entry,
+ UPnPService upnp_service /* = UPnPServiceNone */)
+{
+ NPT_String ObjectClass = entry->m_ObjectClass.type.ToLowercase();
+
+ CFileItemPtr pItem(new CFileItem((const char*)entry->m_Title));
+ pItem->SetLabelPreformatted(true);
+ pItem->m_strTitle = (const char*)entry->m_Title;
+ pItem->m_bIsFolder = entry->IsContainer();
+
+ // if it's a container, format a string as upnp://uuid/object_id
+ if (pItem->m_bIsFolder) {
+
+ // look for metadata
+ if( ObjectClass.StartsWith("object.container.album.videoalbum") ) {
+ pItem->SetLabelPreformatted(false);
+ UPNP::PopulateTagFromObject(*pItem->GetVideoInfoTag(), *entry, NULL, upnp_service);
+
+ } else if( ObjectClass.StartsWith("object.container.album.photoalbum")) {
+ //CPictureInfoTag* tag = pItem->GetPictureInfoTag();
+
+ } else if( ObjectClass.StartsWith("object.container.album") ) {
+ pItem->SetLabelPreformatted(false);
+ UPNP::PopulateTagFromObject(*pItem->GetMusicInfoTag(), *entry, NULL, upnp_service);
+ }
+
+ } else {
+ bool audio = false
+ , image = false
+ , video = false;
+ // set a general content type
+ const char* content = NULL;
+ if (ObjectClass.StartsWith("object.item.videoitem")) {
+ pItem->SetMimeType("video/octet-stream");
+ content = "video";
+ video = true;
+ }
+ else if(ObjectClass.StartsWith("object.item.audioitem")) {
+ pItem->SetMimeType("audio/octet-stream");
+ content = "audio";
+ audio = true;
+ }
+ else if(ObjectClass.StartsWith("object.item.imageitem")) {
+ pItem->SetMimeType("image/octet-stream");
+ content = "image";
+ image = true;
+ }
+
+ // attempt to find a valid resource (may be multiple)
+ PLT_MediaItemResource resource, *res = NULL;
+ if(NPT_SUCCEEDED(NPT_ContainerFind(entry->m_Resources,
+ CResourceFinder("http-get", content), resource))) {
+
+ // set metadata
+ if (resource.m_Size != (NPT_LargeSize)-1) {
+ pItem->m_dwSize = resource.m_Size;
+ }
+ res = &resource;
+ }
+ // look for metadata
+ if(video) {
+ pItem->SetLabelPreformatted(false);
+ UPNP::PopulateTagFromObject(*pItem->GetVideoInfoTag(), *entry, res, upnp_service);
+
+ } else if(audio) {
+ pItem->SetLabelPreformatted(false);
+ UPNP::PopulateTagFromObject(*pItem->GetMusicInfoTag(), *entry, res, upnp_service);
+
+ } else if(image) {
+ //! @todo fill pictureinfotag?
+ GetResource(entry, *pItem);
+ }
+ }
+
+ // look for date?
+ if(entry->m_Description.date.GetLength()) {
+ KODI::TIME::SystemTime time = {};
+ sscanf(entry->m_Description.date, "%hu-%hu-%huT%hu:%hu:%hu", &time.year, &time.month, &time.day,
+ &time.hour, &time.minute, &time.second);
+ pItem->m_dateTime = time;
+ }
+
+ // if there is a thumbnail available set it here
+ if(entry->m_ExtraInfo.album_arts.GetItem(0))
+ // only considers first album art
+ pItem->SetArt("thumb", (const char*) entry->m_ExtraInfo.album_arts.GetItem(0)->uri);
+ else if(entry->m_Description.icon_uri.GetLength())
+ pItem->SetArt("thumb", (const char*) entry->m_Description.icon_uri);
+
+ for (unsigned int index = 0; index < entry->m_XbmcInfo.artwork.GetItemCount(); index++)
+ pItem->SetArt(entry->m_XbmcInfo.artwork.GetItem(index)->type.GetChars(),
+ entry->m_XbmcInfo.artwork.GetItem(index)->url.GetChars());
+
+ // set the watched overlay, as this will not be set later due to
+ // content set on file item list
+ if (pItem->HasVideoInfoTag()) {
+ int episodes = pItem->GetVideoInfoTag()->m_iEpisode;
+ int played = pItem->GetVideoInfoTag()->GetPlayCount();
+ const std::string& type = pItem->GetVideoInfoTag()->m_type;
+ bool watched(false);
+ if (type == MediaTypeTvShow || type == MediaTypeSeason) {
+ pItem->SetProperty("totalepisodes", episodes);
+ pItem->SetProperty("numepisodes", episodes);
+ pItem->SetProperty("watchedepisodes", played);
+ pItem->SetProperty("unwatchedepisodes", episodes - played);
+ pItem->SetProperty("watchedepisodepercent", episodes > 0 ? played * 100 / episodes : 0);
+ watched = (episodes && played >= episodes);
+ pItem->GetVideoInfoTag()->SetPlayCount(watched ? 1 : 0);
+ }
+ else if (type == MediaTypeEpisode || type == MediaTypeMovie)
+ watched = (played > 0);
+ pItem->SetOverlayImage(CGUIListItem::ICON_OVERLAY_UNWATCHED, watched);
+ }
+ return pItem;
+}
+
+struct ResourcePrioritySort
+{
+ explicit ResourcePrioritySort(const PLT_MediaObject* entry)
+ {
+ if (entry->m_ObjectClass.type.StartsWith("object.item.audioItem"))
+ m_content = "audio";
+ else if (entry->m_ObjectClass.type.StartsWith("object.item.imageItem"))
+ m_content = "image";
+ else if (entry->m_ObjectClass.type.StartsWith("object.item.videoItem"))
+ m_content = "video";
+ }
+
+ int GetPriority(const PLT_MediaItemResource& res) const
+ {
+ int prio = 0;
+
+ if (m_content != "" && res.m_ProtocolInfo.GetContentType().StartsWith(m_content))
+ prio += 400;
+
+ NPT_Url url(res.m_Uri);
+ if (URIUtils::IsHostOnLAN((const char*)url.GetHost(), false))
+ prio += 300;
+
+ if (res.m_ProtocolInfo.GetProtocol() == "xbmc-get")
+ prio += 200;
+ else if (res.m_ProtocolInfo.GetProtocol() == "http-get")
+ prio += 100;
+
+ return prio;
+ }
+
+ int operator()(const PLT_MediaItemResource& lh, const PLT_MediaItemResource& rh) const
+ {
+ if(GetPriority(lh) < GetPriority(rh))
+ return 1;
+ else
+ return 0;
+ }
+
+ NPT_String m_content;
+};
+
+bool GetResource(const PLT_MediaObject* entry, CFileItem& item)
+{
+ static Logger logger = CServiceBroker::GetLogging().GetLogger("CUPnPDirectory::GetResource");
+
+ PLT_MediaItemResource resource;
+
+ // store original path so we remember it
+ item.SetProperty("original_listitem_url", item.GetPath());
+ item.SetProperty("original_listitem_mime", item.GetMimeType());
+
+ // get a sorted list based on our preference
+ NPT_List<PLT_MediaItemResource> sorted;
+ for (NPT_Cardinal i = 0; i < entry->m_Resources.GetItemCount(); ++i) {
+ sorted.Add(entry->m_Resources[i]);
+ }
+ sorted.Sort(ResourcePrioritySort(entry));
+
+ if(sorted.GetItemCount() == 0)
+ return false;
+
+ resource = *sorted.GetFirstItem();
+
+ // if it's an item, path is the first url to the item
+ // we hope the server made the first one reachable for us
+ // (it could be a format we dont know how to play however)
+ item.SetDynPath((const char*) resource.m_Uri);
+
+ // look for content type in protocol info
+ if (resource.m_ProtocolInfo.IsValid()) {
+ logger->debug("resource protocol info '{}'", (const char*)(resource.m_ProtocolInfo.ToString()));
+
+ if (resource.m_ProtocolInfo.GetContentType().Compare("application/octet-stream") != 0) {
+ item.SetMimeType((const char*)resource.m_ProtocolInfo.GetContentType());
+ }
+
+ // if this is an image fill the thumb of the item
+ if (StringUtils::StartsWithNoCase(resource.m_ProtocolInfo.GetContentType(), "image"))
+ {
+ item.SetArt("thumb", std::string(resource.m_Uri));
+ }
+ } else {
+ logger->error("invalid protocol info '{}'", (const char*)(resource.m_ProtocolInfo.ToString()));
+ }
+
+ // look for subtitles
+ unsigned subIdx = 0;
+
+ for(unsigned r = 0; r < entry->m_Resources.GetItemCount(); r++)
+ {
+ const PLT_MediaItemResource& res = entry->m_Resources[r];
+ const PLT_ProtocolInfo& info = res.m_ProtocolInfo;
+
+ for (std::string_view type : SupportedSubFormats)
+ {
+ if (type == info.GetContentType().Split("/").GetLastItem()->GetChars())
+ {
+ ++subIdx;
+ logger->info("adding subtitle: #{}, type '{}', URI '{}'", subIdx, type,
+ res.m_Uri.GetChars());
+
+ std::string prop = StringUtils::Format("subtitle:{}", subIdx);
+ item.SetProperty(prop, (const char*)res.m_Uri);
+ }
+ }
+ }
+ return true;
+}
+
+std::shared_ptr<CFileItem> GetFileItem(const NPT_String& uri, const NPT_String& meta)
+{
+ PLT_MediaObjectListReference list;
+ PLT_MediaObject* object = NULL;
+ CFileItemPtr item;
+
+ if (NPT_SUCCEEDED(PLT_Didl::FromDidl(meta, list))) {
+ list->Get(0, object);
+ }
+
+ if (object) {
+ item = BuildObject(object);
+ }
+
+ if (item) {
+ item->SetPath((const char*)uri);
+ GetResource(object, *item);
+ } else {
+ item.reset(new CFileItem((const char*)uri, false));
+ }
+ return item;
+}
+
+} /* namespace UPNP */
+