|
@@ -6,14 +6,13 @@ using System.IO;
|
|
using System.Linq;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Text;
|
|
using System.Xml;
|
|
using System.Xml;
|
|
-using Emby.Dlna.Configuration;
|
|
|
|
using Emby.Dlna.ContentDirectory;
|
|
using Emby.Dlna.ContentDirectory;
|
|
|
|
+using Jellyfin.Data.Entities;
|
|
using MediaBrowser.Controller.Channels;
|
|
using MediaBrowser.Controller.Channels;
|
|
using MediaBrowser.Controller.Drawing;
|
|
using MediaBrowser.Controller.Drawing;
|
|
using MediaBrowser.Controller.Entities;
|
|
using MediaBrowser.Controller.Entities;
|
|
using MediaBrowser.Controller.Entities.Audio;
|
|
using MediaBrowser.Controller.Entities.Audio;
|
|
using MediaBrowser.Controller.Entities.Movies;
|
|
using MediaBrowser.Controller.Entities.Movies;
|
|
-using MediaBrowser.Controller.Entities.TV;
|
|
|
|
using MediaBrowser.Controller.Library;
|
|
using MediaBrowser.Controller.Library;
|
|
using MediaBrowser.Controller.MediaEncoding;
|
|
using MediaBrowser.Controller.MediaEncoding;
|
|
using MediaBrowser.Controller.Playlists;
|
|
using MediaBrowser.Controller.Playlists;
|
|
@@ -23,6 +22,13 @@ using MediaBrowser.Model.Entities;
|
|
using MediaBrowser.Model.Globalization;
|
|
using MediaBrowser.Model.Globalization;
|
|
using MediaBrowser.Model.Net;
|
|
using MediaBrowser.Model.Net;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
+using Episode = MediaBrowser.Controller.Entities.TV.Episode;
|
|
|
|
+using Genre = MediaBrowser.Controller.Entities.Genre;
|
|
|
|
+using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
|
|
|
|
+using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
|
|
|
|
+using Season = MediaBrowser.Controller.Entities.TV.Season;
|
|
|
|
+using Series = MediaBrowser.Controller.Entities.TV.Series;
|
|
|
|
+using XmlAttribute = MediaBrowser.Model.Dlna.XmlAttribute;
|
|
|
|
|
|
namespace Emby.Dlna.Didl
|
|
namespace Emby.Dlna.Didl
|
|
{
|
|
{
|
|
@@ -92,21 +98,21 @@ namespace Emby.Dlna.Didl
|
|
{
|
|
{
|
|
using (var writer = XmlWriter.Create(builder, settings))
|
|
using (var writer = XmlWriter.Create(builder, settings))
|
|
{
|
|
{
|
|
- //writer.WriteStartDocument();
|
|
|
|
|
|
+ // writer.WriteStartDocument();
|
|
|
|
|
|
writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL);
|
|
writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL);
|
|
|
|
|
|
writer.WriteAttributeString("xmlns", "dc", null, NS_DC);
|
|
writer.WriteAttributeString("xmlns", "dc", null, NS_DC);
|
|
writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA);
|
|
writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA);
|
|
writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP);
|
|
writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP);
|
|
- //didl.SetAttribute("xmlns:sec", NS_SEC);
|
|
|
|
|
|
+ // didl.SetAttribute("xmlns:sec", NS_SEC);
|
|
|
|
|
|
WriteXmlRootAttributes(_profile, writer);
|
|
WriteXmlRootAttributes(_profile, writer);
|
|
|
|
|
|
WriteItemElement(writer, item, user, context, null, deviceId, filter, streamInfo);
|
|
WriteItemElement(writer, item, user, context, null, deviceId, filter, streamInfo);
|
|
|
|
|
|
writer.WriteFullEndElement();
|
|
writer.WriteFullEndElement();
|
|
- //writer.WriteEndDocument();
|
|
|
|
|
|
+ // writer.WriteEndDocument();
|
|
}
|
|
}
|
|
|
|
|
|
return builder.ToString();
|
|
return builder.ToString();
|
|
@@ -421,61 +427,102 @@ namespace Emby.Dlna.Didl
|
|
case StubType.FavoriteSeries: return _localization.GetLocalizedString("HeaderFavoriteShows");
|
|
case StubType.FavoriteSeries: return _localization.GetLocalizedString("HeaderFavoriteShows");
|
|
case StubType.FavoriteEpisodes: return _localization.GetLocalizedString("HeaderFavoriteEpisodes");
|
|
case StubType.FavoriteEpisodes: return _localization.GetLocalizedString("HeaderFavoriteEpisodes");
|
|
case StubType.Series: return _localization.GetLocalizedString("Shows");
|
|
case StubType.Series: return _localization.GetLocalizedString("Shows");
|
|
- default: break;
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
- if (item is Episode episode && context is Season season)
|
|
|
|
|
|
+ return item is Episode episode
|
|
|
|
+ ? GetEpisodeDisplayName(episode, context)
|
|
|
|
+ : item.Name;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /// <summary>
|
|
|
|
+ /// Gets episode display name appropriate for the given context.
|
|
|
|
+ /// </summary>
|
|
|
|
+ /// <remarks>
|
|
|
|
+ /// If context is a season, this will return a string containing just episode number and name.
|
|
|
|
+ /// Otherwise the result will include series nams and season number.
|
|
|
|
+ /// </remarks>
|
|
|
|
+ /// <param name="episode">The episode.</param>
|
|
|
|
+ /// <param name="context">Current context.</param>
|
|
|
|
+ /// <returns>Formatted name of the episode.</returns>
|
|
|
|
+ private string GetEpisodeDisplayName(Episode episode, BaseItem context)
|
|
|
|
+ {
|
|
|
|
+ string[] components;
|
|
|
|
+
|
|
|
|
+ if (context is Season season)
|
|
{
|
|
{
|
|
// This is a special embedded within a season
|
|
// This is a special embedded within a season
|
|
- if (item.ParentIndexNumber.HasValue && item.ParentIndexNumber.Value == 0
|
|
|
|
|
|
+ if (episode.ParentIndexNumber.HasValue && episode.ParentIndexNumber.Value == 0
|
|
&& season.IndexNumber.HasValue && season.IndexNumber.Value != 0)
|
|
&& season.IndexNumber.HasValue && season.IndexNumber.Value != 0)
|
|
{
|
|
{
|
|
return string.Format(
|
|
return string.Format(
|
|
CultureInfo.InvariantCulture,
|
|
CultureInfo.InvariantCulture,
|
|
_localization.GetLocalizedString("ValueSpecialEpisodeName"),
|
|
_localization.GetLocalizedString("ValueSpecialEpisodeName"),
|
|
- item.Name);
|
|
|
|
|
|
+ episode.Name);
|
|
}
|
|
}
|
|
|
|
|
|
- if (item.IndexNumber.HasValue)
|
|
|
|
- {
|
|
|
|
- var number = item.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
|
|
|
|
|
|
+ // inside a season use simple format (ex. '12 - Episode Name')
|
|
|
|
+ var epNumberName = GetEpisodeIndexFullName(episode);
|
|
|
|
+ components = new[] { epNumberName, episode.Name };
|
|
|
|
+ }
|
|
|
|
+ else
|
|
|
|
+ {
|
|
|
|
+ // outside a season include series and season details (ex. 'TV Show - S05E11 - Episode Name')
|
|
|
|
+ var epNumberName = GetEpisodeNumberDisplayName(episode);
|
|
|
|
+ components = new[] { episode.SeriesName, epNumberName, episode.Name };
|
|
|
|
+ }
|
|
|
|
|
|
- if (episode.IndexNumberEnd.HasValue)
|
|
|
|
- {
|
|
|
|
- number += "-" + episode.IndexNumberEnd.Value.ToString("00", CultureInfo.InvariantCulture);
|
|
|
|
- }
|
|
|
|
|
|
+ return string.Join(" - ", components.Where(NotNullOrWhiteSpace));
|
|
|
|
+ }
|
|
|
|
|
|
- return number + " - " + item.Name;
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
- else if (item is Episode ep)
|
|
|
|
|
|
+ /// <summary>
|
|
|
|
+ /// Gets complete episode number.
|
|
|
|
+ /// </summary>
|
|
|
|
+ /// <param name="episode">The episode.</param>
|
|
|
|
+ /// <returns>For single episodes returns just the number. For double episodes - current and ending numbers.</returns>
|
|
|
|
+ private string GetEpisodeIndexFullName(Episode episode)
|
|
|
|
+ {
|
|
|
|
+ var name = string.Empty;
|
|
|
|
+ if (episode.IndexNumber.HasValue)
|
|
{
|
|
{
|
|
- var parent = ep.GetParent();
|
|
|
|
- var name = parent.Name + " - ";
|
|
|
|
|
|
+ name += episode.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
|
|
|
|
|
|
- if (ep.ParentIndexNumber.HasValue)
|
|
|
|
- {
|
|
|
|
- name += "S" + ep.ParentIndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
|
|
|
|
- }
|
|
|
|
- else if (!item.IndexNumber.HasValue)
|
|
|
|
|
|
+ if (episode.IndexNumberEnd.HasValue)
|
|
{
|
|
{
|
|
- return name + " - " + item.Name;
|
|
|
|
|
|
+ name += "-" + episode.IndexNumberEnd.Value.ToString("00", CultureInfo.InvariantCulture);
|
|
}
|
|
}
|
|
|
|
+ }
|
|
|
|
|
|
- name += "E" + ep.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
|
|
|
|
- if (ep.IndexNumberEnd.HasValue)
|
|
|
|
- {
|
|
|
|
- name += "-" + ep.IndexNumberEnd.Value.ToString("00", CultureInfo.InvariantCulture);
|
|
|
|
- }
|
|
|
|
|
|
+ return name;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /// <summary>
|
|
|
|
+ /// Gets episode number formatted as 'S##E##'.
|
|
|
|
+ /// </summary>
|
|
|
|
+ /// <param name="episode">The episode.</param>
|
|
|
|
+ /// <returns>Formatted episode number.</returns>
|
|
|
|
+ private string GetEpisodeNumberDisplayName(Episode episode)
|
|
|
|
+ {
|
|
|
|
+ var name = string.Empty;
|
|
|
|
+ var seasonNumber = episode.Season?.IndexNumber;
|
|
|
|
|
|
- name += " - " + item.Name;
|
|
|
|
- return name;
|
|
|
|
|
|
+ if (seasonNumber.HasValue)
|
|
|
|
+ {
|
|
|
|
+ name = "S" + seasonNumber.Value.ToString("00", CultureInfo.InvariantCulture);
|
|
}
|
|
}
|
|
|
|
|
|
- return item.Name;
|
|
|
|
|
|
+ var indexName = GetEpisodeIndexFullName(episode);
|
|
|
|
+
|
|
|
|
+ if (!string.IsNullOrWhiteSpace(indexName))
|
|
|
|
+ {
|
|
|
|
+ name += "E" + indexName;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return name;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ private bool NotNullOrWhiteSpace(string s) => !string.IsNullOrWhiteSpace(s);
|
|
|
|
+
|
|
private void AddAudioResource(XmlWriter writer, BaseItem audio, string deviceId, Filter filter, StreamInfo streamInfo = null)
|
|
private void AddAudioResource(XmlWriter writer, BaseItem audio, string deviceId, Filter filter, StreamInfo streamInfo = null)
|
|
{
|
|
{
|
|
writer.WriteStartElement(string.Empty, "res", NS_DIDL);
|
|
writer.WriteStartElement(string.Empty, "res", NS_DIDL);
|
|
@@ -628,7 +675,7 @@ namespace Emby.Dlna.Didl
|
|
return;
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
- MediaBrowser.Model.Dlna.XmlAttribute secAttribute = null;
|
|
|
|
|
|
+ XmlAttribute secAttribute = null;
|
|
foreach (var attribute in _profile.XmlRootAttributes)
|
|
foreach (var attribute in _profile.XmlRootAttributes)
|
|
{
|
|
{
|
|
if (string.Equals(attribute.Name, "xmlns:sec", StringComparison.OrdinalIgnoreCase))
|
|
if (string.Equals(attribute.Name, "xmlns:sec", StringComparison.OrdinalIgnoreCase))
|
|
@@ -658,13 +705,13 @@ namespace Emby.Dlna.Didl
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
- /// Adds fields used by both items and folders
|
|
|
|
|
|
+ /// Adds fields used by both items and folders.
|
|
/// </summary>
|
|
/// </summary>
|
|
private void AddCommonFields(BaseItem item, StubType? itemStubType, BaseItem context, XmlWriter writer, Filter filter)
|
|
private void AddCommonFields(BaseItem item, StubType? itemStubType, BaseItem context, XmlWriter writer, Filter filter)
|
|
{
|
|
{
|
|
// Don't filter on dc:title because not all devices will include it in the filter
|
|
// Don't filter on dc:title because not all devices will include it in the filter
|
|
// MediaMonkey for example won't display content without a title
|
|
// MediaMonkey for example won't display content without a title
|
|
- //if (filter.Contains("dc:title"))
|
|
|
|
|
|
+ // if (filter.Contains("dc:title"))
|
|
{
|
|
{
|
|
AddValue(writer, "dc", "title", GetDisplayName(item, itemStubType, context), NS_DC);
|
|
AddValue(writer, "dc", "title", GetDisplayName(item, itemStubType, context), NS_DC);
|
|
}
|
|
}
|
|
@@ -703,7 +750,7 @@ namespace Emby.Dlna.Didl
|
|
AddValue(writer, "dc", "description", desc, NS_DC);
|
|
AddValue(writer, "dc", "description", desc, NS_DC);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
- //if (filter.Contains("upnp:longDescription"))
|
|
|
|
|
|
+ // if (filter.Contains("upnp:longDescription"))
|
|
//{
|
|
//{
|
|
// if (!string.IsNullOrWhiteSpace(item.Overview))
|
|
// if (!string.IsNullOrWhiteSpace(item.Overview))
|
|
// {
|
|
// {
|
|
@@ -718,6 +765,7 @@ namespace Emby.Dlna.Didl
|
|
{
|
|
{
|
|
AddValue(writer, "dc", "rating", item.OfficialRating, NS_DC);
|
|
AddValue(writer, "dc", "rating", item.OfficialRating, NS_DC);
|
|
}
|
|
}
|
|
|
|
+
|
|
if (filter.Contains("upnp:rating"))
|
|
if (filter.Contains("upnp:rating"))
|
|
{
|
|
{
|
|
AddValue(writer, "upnp", "rating", item.OfficialRating, NS_UPNP);
|
|
AddValue(writer, "upnp", "rating", item.OfficialRating, NS_UPNP);
|
|
@@ -953,7 +1001,6 @@ namespace Emby.Dlna.Didl
|
|
}
|
|
}
|
|
|
|
|
|
AddImageResElement(item, writer, 160, 160, "jpg", "JPEG_TN");
|
|
AddImageResElement(item, writer, 160, 160, "jpg", "JPEG_TN");
|
|
-
|
|
|
|
}
|
|
}
|
|
|
|
|
|
private void AddImageResElement(
|
|
private void AddImageResElement(
|
|
@@ -1006,10 +1053,12 @@ namespace Emby.Dlna.Didl
|
|
{
|
|
{
|
|
return GetImageInfo(item, ImageType.Primary);
|
|
return GetImageInfo(item, ImageType.Primary);
|
|
}
|
|
}
|
|
|
|
+
|
|
if (item.HasImage(ImageType.Thumb))
|
|
if (item.HasImage(ImageType.Thumb))
|
|
{
|
|
{
|
|
return GetImageInfo(item, ImageType.Thumb);
|
|
return GetImageInfo(item, ImageType.Thumb);
|
|
}
|
|
}
|
|
|
|
+
|
|
if (item.HasImage(ImageType.Backdrop))
|
|
if (item.HasImage(ImageType.Backdrop))
|
|
{
|
|
{
|
|
if (item is Channel)
|
|
if (item is Channel)
|
|
@@ -1018,19 +1067,58 @@ namespace Emby.Dlna.Didl
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
- item = item.GetParents().FirstOrDefault(i => i.HasImage(ImageType.Primary));
|
|
|
|
|
|
+ // For audio tracks without art use album art if available.
|
|
|
|
+ if (item is Audio audioItem)
|
|
|
|
+ {
|
|
|
|
+ var album = audioItem.AlbumEntity;
|
|
|
|
+ return album != null && album.HasImage(ImageType.Primary)
|
|
|
|
+ ? GetImageInfo(album, ImageType.Primary)
|
|
|
|
+ : null;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Don't look beyond album/playlist level. Metadata service may assign an image from a different album/show to the parent folder.
|
|
|
|
+ if (item is MusicAlbum || item is Playlist)
|
|
|
|
+ {
|
|
|
|
+ return null;
|
|
|
|
+ }
|
|
|
|
|
|
- if (item != null)
|
|
|
|
|
|
+ // For other item types check parents, but be aware that image retrieved from a parent may be not suitable for this media item.
|
|
|
|
+ var parentWithImage = GetFirstParentWithImageBelowUserRoot(item);
|
|
|
|
+ if (parentWithImage != null)
|
|
{
|
|
{
|
|
- if (item.HasImage(ImageType.Primary))
|
|
|
|
- {
|
|
|
|
- return GetImageInfo(item, ImageType.Primary);
|
|
|
|
- }
|
|
|
|
|
|
+ return GetImageInfo(parentWithImage, ImageType.Primary);
|
|
}
|
|
}
|
|
|
|
|
|
return null;
|
|
return null;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ private BaseItem GetFirstParentWithImageBelowUserRoot(BaseItem item)
|
|
|
|
+ {
|
|
|
|
+ if (item == null)
|
|
|
|
+ {
|
|
|
|
+ return null;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (item.HasImage(ImageType.Primary))
|
|
|
|
+ {
|
|
|
|
+ return item;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ var parent = item.GetParent();
|
|
|
|
+ if (parent is UserRootFolder)
|
|
|
|
+ {
|
|
|
|
+ return null;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // terminate in case we went past user root folder (unlikely?)
|
|
|
|
+ if (parent is Folder folder && folder.IsRoot)
|
|
|
|
+ {
|
|
|
|
+ return null;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return GetFirstParentWithImageBelowUserRoot(parent);
|
|
|
|
+ }
|
|
|
|
+
|
|
private ImageDownloadInfo GetImageInfo(BaseItem item, ImageType type)
|
|
private ImageDownloadInfo GetImageInfo(BaseItem item, ImageType type)
|
|
{
|
|
{
|
|
var imageInfo = item.GetImageInfo(type, 0);
|
|
var imageInfo = item.GetImageInfo(type, 0);
|
|
@@ -1050,25 +1138,24 @@ namespace Emby.Dlna.Didl
|
|
|
|
|
|
if (width == 0 || height == 0)
|
|
if (width == 0 || height == 0)
|
|
{
|
|
{
|
|
- //_imageProcessor.GetImageSize(item, imageInfo);
|
|
|
|
|
|
+ // _imageProcessor.GetImageSize(item, imageInfo);
|
|
width = null;
|
|
width = null;
|
|
height = null;
|
|
height = null;
|
|
}
|
|
}
|
|
-
|
|
|
|
else if (width == -1 || height == -1)
|
|
else if (width == -1 || height == -1)
|
|
{
|
|
{
|
|
width = null;
|
|
width = null;
|
|
height = null;
|
|
height = null;
|
|
}
|
|
}
|
|
|
|
|
|
- //try
|
|
|
|
|
|
+ // try
|
|
//{
|
|
//{
|
|
// var size = _imageProcessor.GetImageSize(imageInfo);
|
|
// var size = _imageProcessor.GetImageSize(imageInfo);
|
|
|
|
|
|
// width = size.Width;
|
|
// width = size.Width;
|
|
// height = size.Height;
|
|
// height = size.Height;
|
|
//}
|
|
//}
|
|
- //catch
|
|
|
|
|
|
+ // catch
|
|
//{
|
|
//{
|
|
|
|
|
|
//}
|
|
//}
|