Merge remote-tracking branch 'upstream/master' into 3.1.7

This commit is contained in:
crobibero 2020-08-24 07:39:16 -06:00
commit 9626101c9f
465 changed files with 5143 additions and 4017 deletions

View File

@ -13,7 +13,7 @@ namespace Emby.Dlna.Common
public string Name { get; set; }
public List<Argument> ArgumentList { get; set; }
public List<Argument> ArgumentList { get; }
/// <inheritdoc />
public override string ToString()

View File

@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
namespace Emby.Dlna.Common
{
@ -17,7 +18,7 @@ namespace Emby.Dlna.Common
public bool SendsEvents { get; set; }
public string[] AllowedValues { get; set; }
public IReadOnlyList<string> AllowedValues { get; set; }
/// <inheritdoc />
public override string ToString()

View File

@ -1,7 +1,6 @@
#nullable enable
#pragma warning disable CS1591
using System.Collections.Generic;
using Emby.Dlna.Configuration;
using MediaBrowser.Common.Configuration;
@ -14,19 +13,4 @@ namespace Emby.Dlna
return manager.GetConfiguration<DlnaOptions>("dlna");
}
}
public class DlnaConfigurationFactory : IConfigurationFactory
{
public IEnumerable<ConfigurationStore> GetConfigurations()
{
return new ConfigurationStore[]
{
new ConfigurationStore
{
Key = "dlna",
ConfigurationType = typeof (DlnaOptions)
}
};
}
}
}

View File

@ -9,22 +9,20 @@ using Microsoft.Extensions.Logging;
namespace Emby.Dlna.ConnectionManager
{
public class ConnectionManager : BaseService, IConnectionManager
public class ConnectionManagerService : BaseService, IConnectionManager
{
private readonly IDlnaManager _dlna;
private readonly ILogger _logger;
private readonly IServerConfigurationManager _config;
public ConnectionManager(
public ConnectionManagerService(
IDlnaManager dlna,
IServerConfigurationManager config,
ILogger<ConnectionManager> logger,
ILogger<ConnectionManagerService> logger,
IHttpClient httpClient)
: base(logger, httpClient)
{
_dlna = dlna;
_config = config;
_logger = logger;
}
/// <inheritdoc />
@ -39,7 +37,7 @@ namespace Emby.Dlna.ConnectionManager
var profile = _dlna.GetProfile(request.Headers) ??
_dlna.GetDefaultProfile();
return new ControlHandler(_config, _logger, profile).ProcessControlRequestAsync(request);
return new ControlHandler(_config, Logger, profile).ProcessControlRequestAsync(request);
}
}
}

View File

@ -44,7 +44,7 @@ namespace Emby.Dlna.ConnectionManager
DataType = "string",
SendsEvents = false,
AllowedValues = new string[]
AllowedValues = new[]
{
"OK",
"ContentFormatMismatch",
@ -67,7 +67,7 @@ namespace Emby.Dlna.ConnectionManager
DataType = "string",
SendsEvents = false,
AllowedValues = new string[]
AllowedValues = new[]
{
"Output",
"Input"

View File

@ -19,7 +19,7 @@ using Microsoft.Extensions.Logging;
namespace Emby.Dlna.ContentDirectory
{
public class ContentDirectory : BaseService, IContentDirectory
public class ContentDirectoryService : BaseService, IContentDirectory
{
private readonly ILibraryManager _libraryManager;
private readonly IImageProcessor _imageProcessor;
@ -33,14 +33,14 @@ namespace Emby.Dlna.ContentDirectory
private readonly IMediaEncoder _mediaEncoder;
private readonly ITVSeriesManager _tvSeriesManager;
public ContentDirectory(
public ContentDirectoryService(
IDlnaManager dlna,
IUserDataManager userDataManager,
IImageProcessor imageProcessor,
ILibraryManager libraryManager,
IServerConfigurationManager config,
IUserManager userManager,
ILogger<ContentDirectory> logger,
ILogger<ContentDirectoryService> logger,
IHttpClient httpClient,
ILocalizationManager localization,
IMediaSourceManager mediaSourceManager,

View File

@ -10,7 +10,8 @@ namespace Emby.Dlna.ContentDirectory
{
public string GetXml()
{
return new ServiceXmlBuilder().GetXml(new ServiceActionListBuilder().GetActions(),
return new ServiceXmlBuilder().GetXml(
new ServiceActionListBuilder().GetActions(),
GetStateVariables());
}
@ -101,7 +102,7 @@ namespace Emby.Dlna.ContentDirectory
DataType = "string",
SendsEvents = false,
AllowedValues = new string[]
AllowedValues = new[]
{
"BrowseMetadata",
"BrowseDirectChildren"

View File

@ -40,6 +40,11 @@ namespace Emby.Dlna.ContentDirectory
{
public class ControlHandler : BaseControlHandler
{
private const string NsDc = "http://purl.org/dc/elements/1.1/";
private const string NsDidl = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
private const string NsDlna = "urn:schemas-dlna-org:metadata-1-0/";
private const string NsUpnp = "urn:schemas-upnp-org:metadata-1-0/upnp/";
private readonly ILibraryManager _libraryManager;
private readonly IUserDataManager _userDataManager;
private readonly IServerConfigurationManager _config;
@ -47,11 +52,6 @@ namespace Emby.Dlna.ContentDirectory
private readonly IUserViewManager _userViewManager;
private readonly ITVSeriesManager _tvSeriesManager;
private const string NS_DC = "http://purl.org/dc/elements/1.1/";
private const string NS_DIDL = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
private const string NS_DLNA = "urn:schemas-dlna-org:metadata-1-0/";
private const string NS_UPNP = "urn:schemas-upnp-org:metadata-1-0/upnp/";
private readonly int _systemUpdateId;
private readonly DidlBuilder _didlBuilder;
@ -181,7 +181,11 @@ namespace Emby.Dlna.ContentDirectory
userdata.PlaybackPositionTicks = TimeSpan.FromSeconds(newbookmark).Ticks;
_userDataManager.SaveUserData(_user, item, userdata, UserDataSaveReason.TogglePlayed,
_userDataManager.SaveUserData(
_user,
item,
userdata,
UserDataSaveReason.TogglePlayed,
CancellationToken.None);
}
@ -253,7 +257,7 @@ namespace Emby.Dlna.ContentDirectory
var id = sparams["ObjectID"];
var flag = sparams["BrowseFlag"];
var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*"));
var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", ""));
var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", string.Empty));
var provided = 0;
@ -286,18 +290,17 @@ namespace Emby.Dlna.ContentDirectory
using (var writer = XmlWriter.Create(builder, settings))
{
writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL);
writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl);
writer.WriteAttributeString("xmlns", "dc", null, NS_DC);
writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA);
writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP);
writer.WriteAttributeString("xmlns", "dc", null, NsDc);
writer.WriteAttributeString("xmlns", "dlna", null, NsDlna);
writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp);
DidlBuilder.WriteXmlRootAttributes(_profile, writer);
var serverItem = GetItemFromObjectId(id);
var item = serverItem.Item;
if (string.Equals(flag, "BrowseMetadata", StringComparison.Ordinal))
{
totalCount = 1;
@ -362,8 +365,8 @@ namespace Emby.Dlna.ContentDirectory
private void HandleSearch(XmlWriter xmlWriter, IDictionary<string, string> sparams, string deviceId)
{
var searchCriteria = new SearchCriteria(GetValueOrDefault(sparams, "SearchCriteria", ""));
var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", ""));
var searchCriteria = new SearchCriteria(GetValueOrDefault(sparams, "SearchCriteria", string.Empty));
var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", string.Empty));
var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*"));
// sort example: dc:title, dc:date
@ -397,11 +400,11 @@ namespace Emby.Dlna.ContentDirectory
using (var writer = XmlWriter.Create(builder, settings))
{
writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL);
writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl);
writer.WriteAttributeString("xmlns", "dc", null, NS_DC);
writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA);
writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP);
writer.WriteAttributeString("xmlns", "dc", null, NsDc);
writer.WriteAttributeString("xmlns", "dlna", null, NsDlna);
writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp);
DidlBuilder.WriteXmlRootAttributes(_profile, writer);
@ -783,11 +786,14 @@ namespace Emby.Dlna.ContentDirectory
})
.ToArray();
return ApplyPaging(new QueryResult<ServerItem>
return ApplyPaging(
new QueryResult<ServerItem>
{
Items = folders,
TotalRecordCount = folders.Length
}, startIndex, limit);
},
startIndex,
limit);
}
private QueryResult<ServerItem> GetTvFolders(BaseItem item, User user, StubType? stubType, SortCriteria sort, int? startIndex, int? limit)
@ -1135,14 +1141,16 @@ namespace Emby.Dlna.ContentDirectory
{
query.OrderBy = Array.Empty<(string, SortOrder)>();
var items = _userViewManager.GetLatestItems(new LatestItemsQuery
var items = _userViewManager.GetLatestItems(
new LatestItemsQuery
{
UserId = user.Id,
Limit = 50,
IncludeItemTypes = new[] { nameof(Audio) },
ParentId = parent?.Id ?? Guid.Empty,
GroupItems = true
}, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
},
query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
return ToResult(items);
}
@ -1151,12 +1159,15 @@ namespace Emby.Dlna.ContentDirectory
{
query.OrderBy = Array.Empty<(string, SortOrder)>();
var result = _tvSeriesManager.GetNextUp(new NextUpQuery
var result = _tvSeriesManager.GetNextUp(
new NextUpQuery
{
Limit = query.Limit,
StartIndex = query.StartIndex,
UserId = query.User.Id
}, new[] { parent }, query.DtoOptions);
},
new[] { parent },
query.DtoOptions);
return ToResult(result);
}
@ -1165,14 +1176,16 @@ namespace Emby.Dlna.ContentDirectory
{
query.OrderBy = Array.Empty<(string, SortOrder)>();
var items = _userViewManager.GetLatestItems(new LatestItemsQuery
var items = _userViewManager.GetLatestItems(
new LatestItemsQuery
{
UserId = user.Id,
Limit = 50,
IncludeItemTypes = new[] { typeof(Episode).Name },
ParentId = parent == null ? Guid.Empty : parent.Id,
GroupItems = false
}, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
},
query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
return ToResult(items);
}
@ -1189,7 +1202,8 @@ namespace Emby.Dlna.ContentDirectory
IncludeItemTypes = new[] { nameof(Movie) },
ParentId = parent?.Id ?? Guid.Empty,
GroupItems = true
}, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
},
query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
return ToResult(items);
}
@ -1354,44 +1368,4 @@ namespace Emby.Dlna.ContentDirectory
return new ServerItem(_libraryManager.GetUserRootFolder());
}
}
internal class ServerItem
{
public BaseItem Item { get; set; }
public StubType? StubType { get; set; }
public ServerItem(BaseItem item)
{
Item = item;
if (item is IItemByName && !(item is Folder))
{
StubType = Dlna.ContentDirectory.StubType.Folder;
}
}
}
public enum StubType
{
Folder = 0,
Latest = 2,
Playlists = 3,
Albums = 4,
AlbumArtists = 5,
Artists = 6,
Songs = 7,
Genres = 8,
FavoriteSongs = 9,
FavoriteArtists = 10,
FavoriteAlbums = 11,
ContinueWatching = 12,
Movies = 13,
Collections = 14,
Favorites = 15,
NextUp = 16,
Series = 17,
FavoriteSeries = 18,
FavoriteEpisodes = 19
}
}

View File

@ -0,0 +1,23 @@
#pragma warning disable CS1591
using MediaBrowser.Controller.Entities;
namespace Emby.Dlna.ContentDirectory
{
internal class ServerItem
{
public ServerItem(BaseItem item)
{
Item = item;
if (item is IItemByName && !(item is Folder))
{
StubType = Dlna.ContentDirectory.StubType.Folder;
}
}
public BaseItem Item { get; set; }
public StubType? StubType { get; set; }
}
}

View File

@ -0,0 +1,28 @@
#pragma warning disable CS1591
#pragma warning disable SA1602
namespace Emby.Dlna.ContentDirectory
{
public enum StubType
{
Folder = 0,
Latest = 2,
Playlists = 3,
Albums = 4,
AlbumArtists = 5,
Artists = 6,
Songs = 7,
Genres = 8,
FavoriteSongs = 9,
FavoriteArtists = 10,
FavoriteAlbums = 11,
ContinueWatching = 12,
Movies = 13,
Collections = 14,
Favorites = 15,
NextUp = 16,
Series = 17,
FavoriteSeries = 18,
FavoriteEpisodes = 19
}
}

View File

@ -7,17 +7,17 @@ namespace Emby.Dlna
{
public class ControlRequest
{
public IHeaderDictionary Headers { get; set; }
public ControlRequest(IHeaderDictionary headers)
{
Headers = headers;
}
public IHeaderDictionary Headers { get; }
public Stream InputXml { get; set; }
public string TargetServerUuId { get; set; }
public string RequestedUrl { get; set; }
public ControlRequest()
{
Headers = new HeaderDictionary();
}
}
}

View File

@ -11,10 +11,16 @@ namespace Emby.Dlna
Headers = new Dictionary<string, string>();
}
public IDictionary<string, string> Headers { get; set; }
public IDictionary<string, string> Headers { get; }
public string Xml { get; set; }
public bool IsSuccessful { get; set; }
/// <inheritdoc />
public override string ToString()
{
return Xml;
}
}
}

View File

@ -34,12 +34,12 @@ namespace Emby.Dlna.Didl
{
public class DidlBuilder
{
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private const string NsDidl = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
private const string NsDc = "http://purl.org/dc/elements/1.1/";
private const string NsUpnp = "urn:schemas-upnp-org:metadata-1-0/upnp/";
private const string NsDlna = "urn:schemas-dlna-org:metadata-1-0/";
private const string NS_DIDL = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
private const string NS_DC = "http://purl.org/dc/elements/1.1/";
private const string NS_UPNP = "urn:schemas-upnp-org:metadata-1-0/upnp/";
private const string NS_DLNA = "urn:schemas-dlna-org:metadata-1-0/";
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private readonly DeviceProfile _profile;
private readonly IImageProcessor _imageProcessor;
@ -100,11 +100,11 @@ namespace Emby.Dlna.Didl
{
// writer.WriteStartDocument();
writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL);
writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl);
writer.WriteAttributeString("xmlns", "dc", null, NS_DC);
writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA);
writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP);
writer.WriteAttributeString("xmlns", "dc", null, NsDc);
writer.WriteAttributeString("xmlns", "dlna", null, NsDlna);
writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp);
// didl.SetAttribute("xmlns:sec", NS_SEC);
WriteXmlRootAttributes(_profile, writer);
@ -147,7 +147,7 @@ namespace Emby.Dlna.Didl
{
var clientId = GetClientId(item, null);
writer.WriteStartElement(string.Empty, "item", NS_DIDL);
writer.WriteStartElement(string.Empty, "item", NsDidl);
writer.WriteAttributeString("restricted", "1");
writer.WriteAttributeString("id", clientId);
@ -207,7 +207,8 @@ namespace Emby.Dlna.Didl
var targetWidth = streamInfo.TargetWidth;
var targetHeight = streamInfo.TargetHeight;
var contentFeatureList = new ContentFeatureBuilder(_profile).BuildVideoHeader(streamInfo.Container,
var contentFeatureList = new ContentFeatureBuilder(_profile).BuildVideoHeader(
streamInfo.Container,
streamInfo.TargetVideoCodec.FirstOrDefault(),
streamInfo.TargetAudioCodec.FirstOrDefault(),
targetWidth,
@ -279,7 +280,7 @@ namespace Emby.Dlna.Didl
}
else if (string.Equals(subtitleMode, "smi", StringComparison.OrdinalIgnoreCase))
{
writer.WriteStartElement(string.Empty, "res", NS_DIDL);
writer.WriteStartElement(string.Empty, "res", NsDidl);
writer.WriteAttributeString("protocolInfo", "http-get:*:smi/caption:*");
@ -288,7 +289,7 @@ namespace Emby.Dlna.Didl
}
else
{
writer.WriteStartElement(string.Empty, "res", NS_DIDL);
writer.WriteStartElement(string.Empty, "res", NsDidl);
var protocolInfo = string.Format(
CultureInfo.InvariantCulture,
"http-get:*:text/{0}:*",
@ -304,7 +305,7 @@ namespace Emby.Dlna.Didl
private void AddVideoResource(XmlWriter writer, Filter filter, string contentFeatures, StreamInfo streamInfo)
{
writer.WriteStartElement(string.Empty, "res", NS_DIDL);
writer.WriteStartElement(string.Empty, "res", NsDidl);
var url = NormalizeDlnaMediaUrl(streamInfo.ToUrl(_serverAddress, _accessToken));
@ -526,7 +527,7 @@ namespace Emby.Dlna.Didl
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", NsDidl);
if (streamInfo == null)
{
@ -583,7 +584,8 @@ namespace Emby.Dlna.Didl
writer.WriteAttributeString("bitrate", targetAudioBitrate.Value.ToString(_usCulture));
}
var mediaProfile = _profile.GetAudioMediaProfile(streamInfo.Container,
var mediaProfile = _profile.GetAudioMediaProfile(
streamInfo.Container,
streamInfo.TargetAudioCodec.FirstOrDefault(),
targetChannels,
targetAudioBitrate,
@ -596,7 +598,8 @@ namespace Emby.Dlna.Didl
? MimeTypes.GetMimeType(filename)
: mediaProfile.MimeType;
var contentFeatures = new ContentFeatureBuilder(_profile).BuildAudioHeader(streamInfo.Container,
var contentFeatures = new ContentFeatureBuilder(_profile).BuildAudioHeader(
streamInfo.Container,
streamInfo.TargetAudioCodec.FirstOrDefault(),
targetAudioBitrate,
targetSampleRate,
@ -627,7 +630,7 @@ namespace Emby.Dlna.Didl
public void WriteFolderElement(XmlWriter writer, BaseItem folder, StubType? stubType, BaseItem context, int childCount, Filter filter, string requestedId = null)
{
writer.WriteStartElement(string.Empty, "container", NS_DIDL);
writer.WriteStartElement(string.Empty, "container", NsDidl);
writer.WriteAttributeString("restricted", "1");
writer.WriteAttributeString("searchable", "1");
@ -714,7 +717,7 @@ namespace Emby.Dlna.Didl
// MediaMonkey for example won't display content without a title
// if (filter.Contains("dc:title"))
{
AddValue(writer, "dc", "title", GetDisplayName(item, itemStubType, context), NS_DC);
AddValue(writer, "dc", "title", GetDisplayName(item, itemStubType, context), NsDc);
}
WriteObjectClass(writer, item, itemStubType);
@ -723,7 +726,7 @@ namespace Emby.Dlna.Didl
{
if (item.PremiereDate.HasValue)
{
AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("o", CultureInfo.InvariantCulture), NS_DC);
AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("o", CultureInfo.InvariantCulture), NsDc);
}
}
@ -731,13 +734,13 @@ namespace Emby.Dlna.Didl
{
foreach (var genre in item.Genres)
{
AddValue(writer, "upnp", "genre", genre, NS_UPNP);
AddValue(writer, "upnp", "genre", genre, NsUpnp);
}
}
foreach (var studio in item.Studios)
{
AddValue(writer, "upnp", "publisher", studio, NS_UPNP);
AddValue(writer, "upnp", "publisher", studio, NsUpnp);
}
if (!(item is Folder))
@ -748,14 +751,15 @@ namespace Emby.Dlna.Didl
if (!string.IsNullOrWhiteSpace(desc))
{
AddValue(writer, "dc", "description", desc, NS_DC);
AddValue(writer, "dc", "description", desc, NsDc);
}
}
// if (filter.Contains("upnp:longDescription"))
// {
// if (!string.IsNullOrWhiteSpace(item.Overview))
// {
// AddValue(writer, "upnp", "longDescription", item.Overview, NS_UPNP);
// AddValue(writer, "upnp", "longDescription", item.Overview, NsUpnp);
// }
// }
}
@ -764,12 +768,12 @@ namespace Emby.Dlna.Didl
{
if (filter.Contains("dc:rating"))
{
AddValue(writer, "dc", "rating", item.OfficialRating, NS_DC);
AddValue(writer, "dc", "rating", item.OfficialRating, NsDc);
}
if (filter.Contains("upnp:rating"))
{
AddValue(writer, "upnp", "rating", item.OfficialRating, NS_UPNP);
AddValue(writer, "upnp", "rating", item.OfficialRating, NsUpnp);
}
}
@ -781,7 +785,7 @@ namespace Emby.Dlna.Didl
// More types here
// http://oss.linn.co.uk/repos/Public/LibUpnpCil/DidlLite/UpnpAv/Test/TestDidlLite.cs
writer.WriteStartElement("upnp", "class", NS_UPNP);
writer.WriteStartElement("upnp", "class", NsUpnp);
if (item.IsDisplayedAsFolder || stubType.HasValue)
{
@ -882,7 +886,7 @@ namespace Emby.Dlna.Didl
var type = types.FirstOrDefault(i => string.Equals(i, actor.Type, StringComparison.OrdinalIgnoreCase) || string.Equals(i, actor.Role, StringComparison.OrdinalIgnoreCase))
?? PersonType.Actor;
AddValue(writer, "upnp", type.ToLowerInvariant(), actor.Name, NS_UPNP);
AddValue(writer, "upnp", type.ToLowerInvariant(), actor.Name, NsUpnp);
}
}
@ -896,8 +900,8 @@ namespace Emby.Dlna.Didl
{
foreach (var artist in hasArtists.Artists)
{
AddValue(writer, "upnp", "artist", artist, NS_UPNP);
AddValue(writer, "dc", "creator", artist, NS_DC);
AddValue(writer, "upnp", "artist", artist, NsUpnp);
AddValue(writer, "dc", "creator", artist, NsDc);
// If it doesn't support album artists (musicvideo), then tag as both
if (hasAlbumArtists == null)
@ -917,16 +921,16 @@ namespace Emby.Dlna.Didl
if (!string.IsNullOrWhiteSpace(item.Album))
{
AddValue(writer, "upnp", "album", item.Album, NS_UPNP);
AddValue(writer, "upnp", "album", item.Album, NsUpnp);
}
if (item.IndexNumber.HasValue)
{
AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(_usCulture), NS_UPNP);
AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(_usCulture), NsUpnp);
if (item is Episode)
{
AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(_usCulture), NS_UPNP);
AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(_usCulture), NsUpnp);
}
}
}
@ -935,7 +939,7 @@ namespace Emby.Dlna.Didl
{
try
{
writer.WriteStartElement("upnp", "artist", NS_UPNP);
writer.WriteStartElement("upnp", "artist", NsUpnp);
writer.WriteAttributeString("role", "AlbumArtist");
writer.WriteString(name);
@ -971,14 +975,14 @@ namespace Emby.Dlna.Didl
var albumartUrlInfo = GetImageUrl(imageInfo, _profile.MaxAlbumArtWidth, _profile.MaxAlbumArtHeight, "jpg");
writer.WriteStartElement("upnp", "albumArtURI", NS_UPNP);
writer.WriteAttributeString("dlna", "profileID", NS_DLNA, _profile.AlbumArtPn);
writer.WriteString(albumartUrlInfo.Url);
writer.WriteStartElement("upnp", "albumArtURI", NsUpnp);
writer.WriteAttributeString("dlna", "profileID", NsDlna, _profile.AlbumArtPn);
writer.WriteString(albumartUrlInfo.url);
writer.WriteFullEndElement();
// TOOD: Remove these default values
var iconUrlInfo = GetImageUrl(imageInfo, _profile.MaxIconWidth ?? 48, _profile.MaxIconHeight ?? 48, "jpg");
writer.WriteElementString("upnp", "icon", NS_UPNP, iconUrlInfo.Url);
writer.WriteElementString("upnp", "icon", NsUpnp, iconUrlInfo.url);
if (!_profile.EnableAlbumArtInDidl)
{
@ -1021,12 +1025,12 @@ namespace Emby.Dlna.Didl
var albumartUrlInfo = GetImageUrl(imageInfo, maxWidth, maxHeight, format);
writer.WriteStartElement(string.Empty, "res", NS_DIDL);
writer.WriteStartElement(string.Empty, "res", NsDidl);
// Images must have a reported size or many clients (Bubble upnp), will only use the first thumbnail
// rather than using a larger one when available
var width = albumartUrlInfo.Width ?? maxWidth;
var height = albumartUrlInfo.Height ?? maxHeight;
var width = albumartUrlInfo.width ?? maxWidth;
var height = albumartUrlInfo.height ?? maxHeight;
var contentFeatures = new ContentFeatureBuilder(_profile)
.BuildImageHeader(format, width, height, imageInfo.IsDirectStream, org_Pn);
@ -1043,7 +1047,7 @@ namespace Emby.Dlna.Didl
"resolution",
string.Format(CultureInfo.InvariantCulture, "{0}x{1}", width, height));
writer.WriteString(albumartUrlInfo.Url);
writer.WriteString(albumartUrlInfo.url);
writer.WriteFullEndElement();
}
@ -1139,7 +1143,6 @@ namespace Emby.Dlna.Didl
if (width == 0 || height == 0)
{
// _imageProcessor.GetImageSize(item, imageInfo);
width = null;
height = null;
}
@ -1149,18 +1152,6 @@ namespace Emby.Dlna.Didl
height = null;
}
// try
//{
// var size = _imageProcessor.GetImageSize(imageInfo);
// width = size.Width;
// height = size.Height;
//}
// catch
//{
//}
var inputFormat = (Path.GetExtension(imageInfo.Path) ?? string.Empty)
.TrimStart('.')
.Replace("jpeg", "jpg", StringComparison.OrdinalIgnoreCase);
@ -1177,30 +1168,6 @@ namespace Emby.Dlna.Didl
};
}
private class ImageDownloadInfo
{
internal Guid ItemId;
internal string ImageTag;
internal ImageType Type;
internal int? Width;
internal int? Height;
internal bool IsDirectStream;
internal string Format;
internal ItemImageInfo ItemImageInfo;
}
private class ImageUrlInfo
{
internal string Url;
internal int? Width;
internal int? Height;
}
public static string GetClientId(BaseItem item, StubType? stubType)
{
return GetClientId(item.Id, stubType);
@ -1218,7 +1185,7 @@ namespace Emby.Dlna.Didl
return id;
}
private ImageUrlInfo GetImageUrl(ImageDownloadInfo info, int maxWidth, int maxHeight, string format)
private (string url, int? width, int? height) GetImageUrl(ImageDownloadInfo info, int maxWidth, int maxHeight, string format)
{
var url = string.Format(
CultureInfo.InvariantCulture,
@ -1256,12 +1223,26 @@ namespace Emby.Dlna.Didl
// just lie
info.IsDirectStream = true;
return new ImageUrlInfo
return (url, width, height);
}
private class ImageDownloadInfo
{
Url = url,
Width = width,
Height = height
};
internal Guid ItemId { get; set; }
internal string ImageTag { get; set; }
internal ImageType Type { get; set; }
internal int? Width { get; set; }
internal int? Height { get; set; }
internal bool IsDirectStream { get; set; }
internal string Format { get; set; }
internal ItemImageInfo ItemImageInfo { get; set; }
}
}
}

View File

@ -23,9 +23,7 @@ namespace Emby.Dlna.Didl
public bool Contains(string field)
{
// Don't bother with this. Some clients (media monkey) use the filter and then don't display very well when very little data comes back.
return true;
// return _all || ListHelper.ContainsIgnoreCase(_fields, field);
return _all || Array.Exists(_fields, x => x.Equals(field, StringComparison.OrdinalIgnoreCase));
}
}
}

View File

@ -1,4 +1,5 @@
#pragma warning disable CS1591
#pragma warning disable CA1305
using System;
using System.IO;
@ -29,7 +30,6 @@ namespace Emby.Dlna.Didl
{
}
public StringWriterWithEncoding(Encoding encoding)
{
_encoding = encoding;

View File

@ -0,0 +1,24 @@
#nullable enable
#pragma warning disable CS1591
using System.Collections.Generic;
using Emby.Dlna.Configuration;
using MediaBrowser.Common.Configuration;
namespace Emby.Dlna
{
public class DlnaConfigurationFactory : IConfigurationFactory
{
public IEnumerable<ConfigurationStore> GetConfigurations()
{
return new[]
{
new ConfigurationStore
{
Key = "dlna",
ConfigurationType = typeof(DlnaOptions)
}
};
}
}
}

View File

@ -54,11 +54,15 @@ namespace Emby.Dlna
_appHost = appHost;
}
private string UserProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "user");
private string SystemProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "system");
public async Task InitProfilesAsync()
{
try
{
await ExtractSystemProfilesAsync();
await ExtractSystemProfilesAsync().ConfigureAwait(false);
LoadProfiles();
}
catch (Exception ex)
@ -240,7 +244,7 @@ namespace Emby.Dlna
}
else
{
var headerString = string.Join(", ", headers.Select(i => string.Format("{0}={1}", i.Key, i.Value)).ToArray());
var headerString = string.Join(", ", headers.Select(i => string.Format(CultureInfo.InvariantCulture, "{0}={1}", i.Key, i.Value)));
_logger.LogDebug("No matching device profile found. {0}", headerString);
}
@ -280,10 +284,6 @@ namespace Emby.Dlna
return false;
}
private string UserProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "user");
private string SystemProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "system");
private IEnumerable<DeviceProfile> GetProfiles(string path, DeviceProfileType type)
{
try
@ -495,8 +495,8 @@ namespace Emby.Dlna
/// Recreates the object using serialization, to ensure it's not a subclass.
/// If it's a subclass it may not serlialize properly to xml (different root element tag name).
/// </summary>
/// <param name="profile"></param>
/// <returns></returns>
/// <param name="profile">The device profile.</param>
/// <returns>The reserialized device profile.</returns>
private DeviceProfile ReserializeProfile(DeviceProfile profile)
{
if (profile.GetType() == typeof(DeviceProfile))
@ -509,13 +509,6 @@ namespace Emby.Dlna
return _jsonSerializer.DeserializeFromString<DeviceProfile>(json);
}
private class InternalProfileInfo
{
internal DeviceProfileInfo Info { get; set; }
internal string Path { get; set; }
}
public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress)
{
var profile = GetProfile(headers) ??
@ -540,7 +533,15 @@ namespace Emby.Dlna
Stream = _assembly.GetManifestResourceStream(resource)
};
}
private class InternalProfileInfo
{
internal DeviceProfileInfo Info { get; set; }
internal string Path { get; set; }
}
}
/*
class DlnaProfileEntryPoint : IServerEntryPoint
{

View File

@ -20,7 +20,7 @@
<TargetFramework>netstandard2.1</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'" >true</TreatWarningsAsErrors>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<!-- Code Analyzers-->

View File

@ -15,6 +15,6 @@ namespace Emby.Dlna
public string ContentType { get; set; }
public Dictionary<string, string> Headers { get; set; }
public Dictionary<string, string> Headers { get; }
}
}

View File

@ -22,6 +22,8 @@ namespace Emby.Dlna.Eventing
private readonly ILogger _logger;
private readonly IHttpClient _httpClient;
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
public EventManager(ILogger logger, IHttpClient httpClient)
{
_httpClient = httpClient;
@ -58,7 +60,8 @@ namespace Emby.Dlna.Eventing
var timeout = ParseTimeout(requestedTimeoutString) ?? 300;
var id = "uuid:" + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
_logger.LogDebug("Creating event subscription for {0} with timeout of {1} to {2}",
_logger.LogDebug(
"Creating event subscription for {0} with timeout of {1} to {2}",
notificationType,
timeout,
callbackUrl);
@ -94,7 +97,7 @@ namespace Emby.Dlna.Eventing
{
_logger.LogDebug("Cancelling event subscription {0}", subscriptionId);
_subscriptions.TryRemove(subscriptionId, out EventSubscription sub);
_subscriptions.TryRemove(subscriptionId, out _);
return new EventSubscriptionResponse
{
@ -103,7 +106,6 @@ namespace Emby.Dlna.Eventing
};
}
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private EventSubscriptionResponse GetEventSubscriptionResponse(string subscriptionId, string requestedTimeoutString, int timeoutSeconds)
{
var response = new EventSubscriptionResponse

View File

@ -8,16 +8,26 @@ namespace Emby.Dlna
/// Cancels the event subscription.
/// </summary>
/// <param name="subscriptionId">The subscription identifier.</param>
/// <returns>The response.</returns>
EventSubscriptionResponse CancelEventSubscription(string subscriptionId);
/// <summary>
/// Renews the event subscription.
/// </summary>
/// <param name="subscriptionId">The subscription identifier.</param>
/// <param name="notificationType">The notification type.</param>
/// <param name="requestedTimeoutString">The requested timeout as a sting.</param>
/// <param name="callbackUrl">The callback url.</param>
/// <returns>The response.</returns>
EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl);
/// <summary>
/// Creates the event subscription.
/// </summary>
/// <param name="notificationType">The notification type.</param>
/// <param name="requestedTimeoutString">The requested timeout as a sting.</param>
/// <param name="callbackUrl">The callback url.</param>
/// <returns>The response.</returns>
EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl);
}
}

View File

@ -30,7 +30,7 @@ using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
namespace Emby.Dlna.Main
{
public class DlnaEntryPoint : IServerEntryPoint, IRunBeforeStartup
public sealed class DlnaEntryPoint : IServerEntryPoint, IRunBeforeStartup
{
private readonly IServerConfigurationManager _config;
private readonly ILogger<DlnaEntryPoint> _logger;
@ -54,13 +54,7 @@ namespace Emby.Dlna.Main
private SsdpDevicePublisher _publisher;
private ISsdpCommunicationsServer _communicationsServer;
public IContentDirectory ContentDirectory { get; private set; }
public IConnectionManager ConnectionManager { get; private set; }
public IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; }
public static DlnaEntryPoint Current;
private bool _disposed;
public DlnaEntryPoint(
IServerConfigurationManager config,
@ -99,14 +93,14 @@ namespace Emby.Dlna.Main
_networkManager = networkManager;
_logger = loggerFactory.CreateLogger<DlnaEntryPoint>();
ContentDirectory = new ContentDirectory.ContentDirectory(
ContentDirectory = new ContentDirectory.ContentDirectoryService(
dlnaManager,
userDataManager,
imageProcessor,
libraryManager,
config,
userManager,
loggerFactory.CreateLogger<ContentDirectory.ContentDirectory>(),
loggerFactory.CreateLogger<ContentDirectory.ContentDirectoryService>(),
httpClient,
localizationManager,
mediaSourceManager,
@ -114,19 +108,27 @@ namespace Emby.Dlna.Main
mediaEncoder,
tvSeriesManager);
ConnectionManager = new ConnectionManager.ConnectionManager(
ConnectionManager = new ConnectionManager.ConnectionManagerService(
dlnaManager,
config,
loggerFactory.CreateLogger<ConnectionManager.ConnectionManager>(),
loggerFactory.CreateLogger<ConnectionManager.ConnectionManagerService>(),
httpClient);
MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrar(
loggerFactory.CreateLogger<MediaReceiverRegistrar.MediaReceiverRegistrar>(),
MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrarService(
loggerFactory.CreateLogger<MediaReceiverRegistrar.MediaReceiverRegistrarService>(),
httpClient,
config);
Current = this;
}
public static DlnaEntryPoint Current { get; private set; }
public IContentDirectory ContentDirectory { get; private set; }
public IConnectionManager ConnectionManager { get; private set; }
public IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; }
public async Task RunAsync()
{
await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
@ -399,8 +401,24 @@ namespace Emby.Dlna.Main
}
}
public void DisposeDevicePublisher()
{
if (_publisher != null)
{
_logger.LogInformation("Disposing SsdpDevicePublisher");
_publisher.Dispose();
_publisher = null;
}
}
/// <inheritdoc />
public void Dispose()
{
if (_disposed)
{
return;
}
DisposeDevicePublisher();
DisposePlayToManager();
DisposeDeviceDiscovery();
@ -416,16 +434,8 @@ namespace Emby.Dlna.Main
ConnectionManager = null;
MediaReceiverRegistrar = null;
Current = null;
}
public void DisposeDevicePublisher()
{
if (_publisher != null)
{
_logger.LogInformation("Disposing SsdpDevicePublisher");
_publisher.Dispose();
_publisher = null;
}
_disposed = true;
}
}
}

View File

@ -8,12 +8,12 @@ using Microsoft.Extensions.Logging;
namespace Emby.Dlna.MediaReceiverRegistrar
{
public class MediaReceiverRegistrar : BaseService, IMediaReceiverRegistrar
public class MediaReceiverRegistrarService : BaseService, IMediaReceiverRegistrar
{
private readonly IServerConfigurationManager _config;
public MediaReceiverRegistrar(
ILogger<MediaReceiverRegistrar> logger,
public MediaReceiverRegistrarService(
ILogger<MediaReceiverRegistrarService> logger,
IHttpClient httpClient,
IServerConfigurationManager config)
: base(logger, httpClient)

View File

@ -10,7 +10,8 @@ namespace Emby.Dlna.MediaReceiverRegistrar
{
public string GetXml()
{
return new ServiceXmlBuilder().GetXml(new ServiceActionListBuilder().GetActions(),
return new ServiceXmlBuilder().GetXml(
new ServiceActionListBuilder().GetActions(),
GetStateVariables());
}

View File

@ -19,15 +19,40 @@ namespace Emby.Dlna.PlayTo
{
public class Device : IDisposable
{
private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
private readonly IHttpClient _httpClient;
private readonly ILogger _logger;
private readonly object _timerLock = new object();
private Timer _timer;
private int _muteVol;
private int _volume;
private DateTime _lastVolumeRefresh;
private bool _volumeRefreshActive;
private int _connectFailureCount;
private bool _disposed;
public Device(DeviceInfo deviceProperties, IHttpClient httpClient, ILogger logger)
{
Properties = deviceProperties;
_httpClient = httpClient;
_logger = logger;
}
public event EventHandler<PlaybackStartEventArgs> PlaybackStart;
public event EventHandler<PlaybackProgressEventArgs> PlaybackProgress;
public event EventHandler<PlaybackStoppedEventArgs> PlaybackStopped;
public event EventHandler<MediaChangedEventArgs> MediaChanged;
public DeviceInfo Properties { get; set; }
private int _muteVol;
public bool IsMuted { get; set; }
private int _volume;
public int Volume
{
get
@ -43,29 +68,21 @@ namespace Emby.Dlna.PlayTo
public TimeSpan Position { get; set; } = TimeSpan.FromSeconds(0);
public TRANSPORTSTATE TransportState { get; private set; }
public TransportState TransportState { get; private set; }
public bool IsPlaying => TransportState == TRANSPORTSTATE.PLAYING;
public bool IsPlaying => TransportState == TransportState.Playing;
public bool IsPaused => TransportState == TRANSPORTSTATE.PAUSED || TransportState == TRANSPORTSTATE.PAUSED_PLAYBACK;
public bool IsPaused => TransportState == TransportState.Paused || TransportState == TransportState.PausedPlayback;
public bool IsStopped => TransportState == TRANSPORTSTATE.STOPPED;
private readonly IHttpClient _httpClient;
private readonly ILogger _logger;
private readonly IServerConfigurationManager _config;
public bool IsStopped => TransportState == TransportState.Stopped;
public Action OnDeviceUnavailable { get; set; }
public Device(DeviceInfo deviceProperties, IHttpClient httpClient, ILogger logger, IServerConfigurationManager config)
{
Properties = deviceProperties;
_httpClient = httpClient;
_logger = logger;
_config = config;
}
private TransportCommands AvCommands { get; set; }
private TransportCommands RendererCommands { get; set; }
public UBaseObject CurrentMediaInfo { get; private set; }
public void Start()
{
@ -73,8 +90,6 @@ namespace Emby.Dlna.PlayTo
_timer = new Timer(TimerCallback, null, 1000, Timeout.Infinite);
}
private DateTime _lastVolumeRefresh;
private bool _volumeRefreshActive;
private Task RefreshVolumeIfNeeded()
{
if (_volumeRefreshActive
@ -105,7 +120,6 @@ namespace Emby.Dlna.PlayTo
}
}
private readonly object _timerLock = new object();
private void RestartTimer(bool immediate = false)
{
lock (_timerLock)
@ -233,6 +247,9 @@ namespace Emby.Dlna.PlayTo
/// <summary>
/// Sets volume on a scale of 0-100.
/// </summary>
/// <param name="value">The volume on a scale of 0-100.</param>
/// <param name="cancellationToken">The cancellation token to cancel operation.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task SetVolume(int value, CancellationToken cancellationToken)
{
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
@ -275,7 +292,7 @@ namespace Emby.Dlna.PlayTo
throw new InvalidOperationException("Unable to find service");
}
await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, string.Format("{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"))
await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"))
.ConfigureAwait(false);
RestartTimer(true);
@ -285,7 +302,7 @@ namespace Emby.Dlna.PlayTo
{
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
url = url.Replace("&", "&amp;");
url = url.Replace("&", "&amp;", StringComparison.Ordinal);
_logger.LogDebug("{0} - SetAvTransport Uri: {1} DlnaHeaders: {2}", Properties.Name, url, header);
@ -401,13 +418,11 @@ namespace Emby.Dlna.PlayTo
await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
.ConfigureAwait(false);
TransportState = TRANSPORTSTATE.PAUSED;
TransportState = TransportState.Paused;
RestartTimer(true);
}
private int _connectFailureCount;
private async void TimerCallback(object sender)
{
if (_disposed)
@ -436,7 +451,7 @@ namespace Emby.Dlna.PlayTo
if (transportState.HasValue)
{
// If we're not playing anything no need to get additional data
if (transportState.Value == TRANSPORTSTATE.STOPPED)
if (transportState.Value == TransportState.Stopped)
{
UpdateMediaInfo(null, transportState.Value);
}
@ -465,7 +480,7 @@ namespace Emby.Dlna.PlayTo
}
// If we're not playing anything make sure we don't get data more often than neccessry to keep the Session alive
if (transportState.Value == TRANSPORTSTATE.STOPPED)
if (transportState.Value == TransportState.Stopped)
{
RestartTimerInactive();
}
@ -539,7 +554,7 @@ namespace Emby.Dlna.PlayTo
return;
}
var volume = result.Document.Descendants(uPnpNamespaces.RenderingControl + "GetVolumeResponse").Select(i => i.Element("CurrentVolume")).FirstOrDefault(i => i != null);
var volume = result.Document.Descendants(UPnpNamespaces.RenderingControl + "GetVolumeResponse").Select(i => i.Element("CurrentVolume")).FirstOrDefault(i => i != null);
var volumeValue = volume?.Value;
if (string.IsNullOrWhiteSpace(volumeValue))
@ -589,14 +604,14 @@ namespace Emby.Dlna.PlayTo
return;
}
var valueNode = result.Document.Descendants(uPnpNamespaces.RenderingControl + "GetMuteResponse")
var valueNode = result.Document.Descendants(UPnpNamespaces.RenderingControl + "GetMuteResponse")
.Select(i => i.Element("CurrentMute"))
.FirstOrDefault(i => i != null);
IsMuted = string.Equals(valueNode?.Value, "1", StringComparison.OrdinalIgnoreCase);
}
private async Task<TRANSPORTSTATE?> GetTransportInfo(TransportCommands avCommands, CancellationToken cancellationToken)
private async Task<TransportState?> GetTransportInfo(TransportCommands avCommands, CancellationToken cancellationToken)
{
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetTransportInfo");
if (command == null)
@ -623,12 +638,12 @@ namespace Emby.Dlna.PlayTo
}
var transportState =
result.Document.Descendants(uPnpNamespaces.AvTransport + "GetTransportInfoResponse").Select(i => i.Element("CurrentTransportState")).FirstOrDefault(i => i != null);
result.Document.Descendants(UPnpNamespaces.AvTransport + "GetTransportInfoResponse").Select(i => i.Element("CurrentTransportState")).FirstOrDefault(i => i != null);
var transportStateValue = transportState?.Value;
if (transportStateValue != null
&& Enum.TryParse(transportStateValue, true, out TRANSPORTSTATE state))
&& Enum.TryParse(transportStateValue, true, out TransportState state))
{
return state;
}
@ -636,7 +651,7 @@ namespace Emby.Dlna.PlayTo
return null;
}
private async Task<uBaseObject> GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken)
private async Task<UBaseObject> GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken)
{
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMediaInfo");
if (command == null)
@ -671,7 +686,7 @@ namespace Emby.Dlna.PlayTo
return null;
}
var e = track.Element(uPnpNamespaces.items) ?? track;
var e = track.Element(UPnpNamespaces.Items) ?? track;
var elementString = (string)e;
@ -687,13 +702,13 @@ namespace Emby.Dlna.PlayTo
return null;
}
e = track.Element(uPnpNamespaces.items) ?? track;
e = track.Element(UPnpNamespaces.Items) ?? track;
elementString = (string)e;
if (!string.IsNullOrWhiteSpace(elementString))
{
return new uBaseObject
return new UBaseObject
{
Url = elementString
};
@ -702,7 +717,7 @@ namespace Emby.Dlna.PlayTo
return null;
}
private async Task<(bool, uBaseObject)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken)
private async Task<(bool, UBaseObject)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken)
{
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetPositionInfo");
if (command == null)
@ -731,11 +746,11 @@ namespace Emby.Dlna.PlayTo
return (false, null);
}
var trackUriElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackURI")).FirstOrDefault(i => i != null);
var trackUri = trackUriElem == null ? null : trackUriElem.Value;
var trackUriElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackURI")).FirstOrDefault(i => i != null);
var trackUri = trackUriElem?.Value;
var durationElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackDuration")).FirstOrDefault(i => i != null);
var duration = durationElem == null ? null : durationElem.Value;
var durationElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackDuration")).FirstOrDefault(i => i != null);
var duration = durationElem?.Value;
if (!string.IsNullOrWhiteSpace(duration)
&& !string.Equals(duration, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
@ -747,8 +762,8 @@ namespace Emby.Dlna.PlayTo
Duration = null;
}
var positionElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("RelTime")).FirstOrDefault(i => i != null);
var position = positionElem == null ? null : positionElem.Value;
var positionElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("RelTime")).FirstOrDefault(i => i != null);
var position = positionElem?.Value;
if (!string.IsNullOrWhiteSpace(position) && !string.Equals(position, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
{
@ -787,7 +802,7 @@ namespace Emby.Dlna.PlayTo
return (true, null);
}
var e = uPnpResponse.Element(uPnpNamespaces.items);
var e = uPnpResponse.Element(UPnpNamespaces.Items);
var uTrack = CreateUBaseObject(e, trackUri);
@ -819,7 +834,7 @@ namespace Emby.Dlna.PlayTo
// some devices send back invalid xml
try
{
return XElement.Parse(xml.Replace("&", "&amp;"));
return XElement.Parse(xml.Replace("&", "&amp;", StringComparison.Ordinal));
}
catch (XmlException)
{
@ -828,27 +843,27 @@ namespace Emby.Dlna.PlayTo
return null;
}
private static uBaseObject CreateUBaseObject(XElement container, string trackUri)
private static UBaseObject CreateUBaseObject(XElement container, string trackUri)
{
if (container == null)
{
throw new ArgumentNullException(nameof(container));
}
var url = container.GetValue(uPnpNamespaces.Res);
var url = container.GetValue(UPnpNamespaces.Res);
if (string.IsNullOrWhiteSpace(url))
{
url = trackUri;
}
return new uBaseObject
return new UBaseObject
{
Id = container.GetAttributeValue(uPnpNamespaces.Id),
ParentId = container.GetAttributeValue(uPnpNamespaces.ParentId),
Title = container.GetValue(uPnpNamespaces.title),
IconUrl = container.GetValue(uPnpNamespaces.Artwork),
SecondText = "",
Id = container.GetAttributeValue(UPnpNamespaces.Id),
ParentId = container.GetAttributeValue(UPnpNamespaces.ParentId),
Title = container.GetValue(UPnpNamespaces.Title),
IconUrl = container.GetValue(UPnpNamespaces.Artwork),
SecondText = string.Empty,
Url = url,
ProtocolInfo = GetProtocolInfo(container),
MetaData = container.ToString()
@ -862,11 +877,11 @@ namespace Emby.Dlna.PlayTo
throw new ArgumentNullException(nameof(container));
}
var resElement = container.Element(uPnpNamespaces.Res);
var resElement = container.Element(UPnpNamespaces.Res);
if (resElement != null)
{
var info = resElement.Attribute(uPnpNamespaces.ProtocolInfo);
var info = resElement.Attribute(UPnpNamespaces.ProtocolInfo);
if (info != null && !string.IsNullOrWhiteSpace(info.Value))
{
@ -941,12 +956,12 @@ namespace Emby.Dlna.PlayTo
return url;
}
if (!url.Contains("/"))
if (!url.Contains('/', StringComparison.Ordinal))
{
url = "/dmr/" + url;
}
if (!url.StartsWith("/"))
if (!url.StartsWith("/", StringComparison.Ordinal))
{
url = "/" + url;
}
@ -954,11 +969,7 @@ namespace Emby.Dlna.PlayTo
return baseUrl + url;
}
private TransportCommands AvCommands { get; set; }
private TransportCommands RendererCommands { get; set; }
public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClient httpClient, IServerConfigurationManager config, ILogger logger, CancellationToken cancellationToken)
public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClient httpClient, ILogger logger, CancellationToken cancellationToken)
{
var ssdpHttpClient = new SsdpHttpClient(httpClient);
@ -966,13 +977,13 @@ namespace Emby.Dlna.PlayTo
var friendlyNames = new List<string>();
var name = document.Descendants(uPnpNamespaces.ud.GetName("friendlyName")).FirstOrDefault();
var name = document.Descendants(UPnpNamespaces.Ud.GetName("friendlyName")).FirstOrDefault();
if (name != null && !string.IsNullOrWhiteSpace(name.Value))
{
friendlyNames.Add(name.Value);
}
var room = document.Descendants(uPnpNamespaces.ud.GetName("roomName")).FirstOrDefault();
var room = document.Descendants(UPnpNamespaces.Ud.GetName("roomName")).FirstOrDefault();
if (room != null && !string.IsNullOrWhiteSpace(room.Value))
{
friendlyNames.Add(room.Value);
@ -981,77 +992,77 @@ namespace Emby.Dlna.PlayTo
var deviceProperties = new DeviceInfo()
{
Name = string.Join(" ", friendlyNames),
BaseUrl = string.Format("http://{0}:{1}", url.Host, url.Port)
BaseUrl = string.Format(CultureInfo.InvariantCulture, "http://{0}:{1}", url.Host, url.Port)
};
var model = document.Descendants(uPnpNamespaces.ud.GetName("modelName")).FirstOrDefault();
var model = document.Descendants(UPnpNamespaces.Ud.GetName("modelName")).FirstOrDefault();
if (model != null)
{
deviceProperties.ModelName = model.Value;
}
var modelNumber = document.Descendants(uPnpNamespaces.ud.GetName("modelNumber")).FirstOrDefault();
var modelNumber = document.Descendants(UPnpNamespaces.Ud.GetName("modelNumber")).FirstOrDefault();
if (modelNumber != null)
{
deviceProperties.ModelNumber = modelNumber.Value;
}
var uuid = document.Descendants(uPnpNamespaces.ud.GetName("UDN")).FirstOrDefault();
var uuid = document.Descendants(UPnpNamespaces.Ud.GetName("UDN")).FirstOrDefault();
if (uuid != null)
{
deviceProperties.UUID = uuid.Value;
}
var manufacturer = document.Descendants(uPnpNamespaces.ud.GetName("manufacturer")).FirstOrDefault();
var manufacturer = document.Descendants(UPnpNamespaces.Ud.GetName("manufacturer")).FirstOrDefault();
if (manufacturer != null)
{
deviceProperties.Manufacturer = manufacturer.Value;
}
var manufacturerUrl = document.Descendants(uPnpNamespaces.ud.GetName("manufacturerURL")).FirstOrDefault();
var manufacturerUrl = document.Descendants(UPnpNamespaces.Ud.GetName("manufacturerURL")).FirstOrDefault();
if (manufacturerUrl != null)
{
deviceProperties.ManufacturerUrl = manufacturerUrl.Value;
}
var presentationUrl = document.Descendants(uPnpNamespaces.ud.GetName("presentationURL")).FirstOrDefault();
var presentationUrl = document.Descendants(UPnpNamespaces.Ud.GetName("presentationURL")).FirstOrDefault();
if (presentationUrl != null)
{
deviceProperties.PresentationUrl = presentationUrl.Value;
}
var modelUrl = document.Descendants(uPnpNamespaces.ud.GetName("modelURL")).FirstOrDefault();
var modelUrl = document.Descendants(UPnpNamespaces.Ud.GetName("modelURL")).FirstOrDefault();
if (modelUrl != null)
{
deviceProperties.ModelUrl = modelUrl.Value;
}
var serialNumber = document.Descendants(uPnpNamespaces.ud.GetName("serialNumber")).FirstOrDefault();
var serialNumber = document.Descendants(UPnpNamespaces.Ud.GetName("serialNumber")).FirstOrDefault();
if (serialNumber != null)
{
deviceProperties.SerialNumber = serialNumber.Value;
}
var modelDescription = document.Descendants(uPnpNamespaces.ud.GetName("modelDescription")).FirstOrDefault();
var modelDescription = document.Descendants(UPnpNamespaces.Ud.GetName("modelDescription")).FirstOrDefault();
if (modelDescription != null)
{
deviceProperties.ModelDescription = modelDescription.Value;
}
var icon = document.Descendants(uPnpNamespaces.ud.GetName("icon")).FirstOrDefault();
var icon = document.Descendants(UPnpNamespaces.Ud.GetName("icon")).FirstOrDefault();
if (icon != null)
{
deviceProperties.Icon = CreateIcon(icon);
}
foreach (var services in document.Descendants(uPnpNamespaces.ud.GetName("serviceList")))
foreach (var services in document.Descendants(UPnpNamespaces.Ud.GetName("serviceList")))
{
if (services == null)
{
continue;
}
var servicesList = services.Descendants(uPnpNamespaces.ud.GetName("service"));
var servicesList = services.Descendants(UPnpNamespaces.Ud.GetName("service"));
if (servicesList == null)
{
continue;
@ -1068,10 +1079,9 @@ namespace Emby.Dlna.PlayTo
}
}
return new Device(deviceProperties, httpClient, logger, config);
return new Device(deviceProperties, httpClient, logger);
}
private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
private static DeviceIcon CreateIcon(XElement element)
{
if (element == null)
@ -1079,11 +1089,11 @@ namespace Emby.Dlna.PlayTo
throw new ArgumentNullException(nameof(element));
}
var mimeType = element.GetDescendantValue(uPnpNamespaces.ud.GetName("mimetype"));
var width = element.GetDescendantValue(uPnpNamespaces.ud.GetName("width"));
var height = element.GetDescendantValue(uPnpNamespaces.ud.GetName("height"));
var depth = element.GetDescendantValue(uPnpNamespaces.ud.GetName("depth"));
var url = element.GetDescendantValue(uPnpNamespaces.ud.GetName("url"));
var mimeType = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("mimetype"));
var width = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("width"));
var height = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("height"));
var depth = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("depth"));
var url = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("url"));
var widthValue = int.Parse(width, NumberStyles.Integer, UsCulture);
var heightValue = int.Parse(height, NumberStyles.Integer, UsCulture);
@ -1100,11 +1110,11 @@ namespace Emby.Dlna.PlayTo
private static DeviceService Create(XElement element)
{
var type = element.GetDescendantValue(uPnpNamespaces.ud.GetName("serviceType"));
var id = element.GetDescendantValue(uPnpNamespaces.ud.GetName("serviceId"));
var scpdUrl = element.GetDescendantValue(uPnpNamespaces.ud.GetName("SCPDURL"));
var controlURL = element.GetDescendantValue(uPnpNamespaces.ud.GetName("controlURL"));
var eventSubURL = element.GetDescendantValue(uPnpNamespaces.ud.GetName("eventSubURL"));
var type = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceType"));
var id = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceId"));
var scpdUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("SCPDURL"));
var controlURL = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("controlURL"));
var eventSubURL = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("eventSubURL"));
return new DeviceService
{
@ -1116,14 +1126,7 @@ namespace Emby.Dlna.PlayTo
};
}
public event EventHandler<PlaybackStartEventArgs> PlaybackStart;
public event EventHandler<PlaybackProgressEventArgs> PlaybackProgress;
public event EventHandler<PlaybackStoppedEventArgs> PlaybackStopped;
public event EventHandler<MediaChangedEventArgs> MediaChanged;
public uBaseObject CurrentMediaInfo { get; private set; }
private void UpdateMediaInfo(uBaseObject mediaInfo, TRANSPORTSTATE state)
private void UpdateMediaInfo(UBaseObject mediaInfo, TransportState state)
{
TransportState = state;
@ -1132,7 +1135,7 @@ namespace Emby.Dlna.PlayTo
if (previousMediaInfo == null && mediaInfo != null)
{
if (state != TRANSPORTSTATE.STOPPED)
if (state != TransportState.Stopped)
{
OnPlaybackStart(mediaInfo);
}
@ -1151,7 +1154,7 @@ namespace Emby.Dlna.PlayTo
}
}
private void OnPlaybackStart(uBaseObject mediaInfo)
private void OnPlaybackStart(UBaseObject mediaInfo)
{
if (string.IsNullOrWhiteSpace(mediaInfo.Url))
{
@ -1164,7 +1167,7 @@ namespace Emby.Dlna.PlayTo
});
}
private void OnPlaybackProgress(uBaseObject mediaInfo)
private void OnPlaybackProgress(UBaseObject mediaInfo)
{
if (string.IsNullOrWhiteSpace(mediaInfo.Url))
{
@ -1177,7 +1180,7 @@ namespace Emby.Dlna.PlayTo
});
}
private void OnPlaybackStop(uBaseObject mediaInfo)
private void OnPlaybackStop(UBaseObject mediaInfo)
{
PlaybackStopped?.Invoke(this, new PlaybackStoppedEventArgs
{
@ -1185,7 +1188,7 @@ namespace Emby.Dlna.PlayTo
});
}
private void OnMediaChanged(uBaseObject old, uBaseObject newMedia)
private void OnMediaChanged(UBaseObject old, UBaseObject newMedia)
{
MediaChanged?.Invoke(this, new MediaChangedEventArgs
{
@ -1194,14 +1197,17 @@ namespace Emby.Dlna.PlayTo
});
}
bool _disposed;
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases unmanaged and optionally managed resources.
/// </summary>
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose(bool disposing)
{
if (_disposed)
@ -1220,9 +1226,10 @@ namespace Emby.Dlna.PlayTo
_disposed = true;
}
/// <inheritdoc />
public override string ToString()
{
return string.Format("{0} - {1}", Properties.Name, Properties.BaseUrl);
return string.Format(CultureInfo.InvariantCulture, "{0} - {1}", Properties.Name, Properties.BaseUrl);
}
}
}

View File

@ -8,6 +8,9 @@ namespace Emby.Dlna.PlayTo
{
public class DeviceInfo
{
private readonly List<DeviceService> _services = new List<DeviceService>();
private string _baseUrl = string.Empty;
public DeviceInfo()
{
Name = "Generic Device";
@ -33,7 +36,6 @@ namespace Emby.Dlna.PlayTo
public string PresentationUrl { get; set; }
private string _baseUrl = string.Empty;
public string BaseUrl
{
get => _baseUrl;
@ -42,7 +44,6 @@ namespace Emby.Dlna.PlayTo
public DeviceIcon Icon { get; set; }
private readonly List<DeviceService> _services = new List<DeviceService>();
public List<DeviceService> Services => _services;
public DeviceIdentification ToDeviceIdentification()

View File

@ -0,0 +1,13 @@
#pragma warning disable CS1591
using System;
namespace Emby.Dlna.PlayTo
{
public class MediaChangedEventArgs : EventArgs
{
public UBaseObject OldMediaInfo { get; set; }
public UBaseObject NewMediaInfo { get; set; }
}
}

View File

@ -31,7 +31,6 @@ namespace Emby.Dlna.PlayTo
{
private static readonly CultureInfo _usCulture = CultureInfo.ReadOnly(new CultureInfo("en-US"));
private Device _device;
private readonly SessionInfo _session;
private readonly ISessionManager _sessionManager;
private readonly ILibraryManager _libraryManager;
@ -50,6 +49,7 @@ namespace Emby.Dlna.PlayTo
private readonly string _accessToken;
private readonly List<PlaylistItem> _playlist = new List<PlaylistItem>();
private Device _device;
private int _currentPlaylistIndex;
private bool _disposed;
@ -372,8 +372,13 @@ namespace Emby.Dlna.PlayTo
if (!command.ControllingUserId.Equals(Guid.Empty))
{
_sessionManager.LogSessionActivity(_session.Client, _session.ApplicationVersion, _session.DeviceId,
_session.DeviceName, _session.RemoteEndPoint, user);
_sessionManager.LogSessionActivity(
_session.Client,
_session.ApplicationVersion,
_session.DeviceId,
_session.DeviceName,
_session.RemoteEndPoint,
user);
}
return PlayItems(playlist, cancellationToken);
@ -498,7 +503,8 @@ namespace Emby.Dlna.PlayTo
if (streamInfo.MediaType == DlnaProfileType.Audio)
{
return new ContentFeatureBuilder(profile)
.BuildAudioHeader(streamInfo.Container,
.BuildAudioHeader(
streamInfo.Container,
streamInfo.TargetAudioCodec.FirstOrDefault(),
streamInfo.TargetAudioBitrate,
streamInfo.TargetAudioSampleRate,
@ -512,7 +518,8 @@ namespace Emby.Dlna.PlayTo
if (streamInfo.MediaType == DlnaProfileType.Video)
{
var list = new ContentFeatureBuilder(profile)
.BuildVideoHeader(streamInfo.Container,
.BuildVideoHeader(
streamInfo.Container,
streamInfo.TargetVideoCodec.FirstOrDefault(),
streamInfo.TargetAudioCodec.FirstOrDefault(),
streamInfo.TargetWidth,
@ -633,6 +640,10 @@ namespace Emby.Dlna.PlayTo
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases unmanaged and optionally managed resources.
/// </summary>
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose(bool disposing)
{
if (_disposed)
@ -673,10 +684,9 @@ namespace Emby.Dlna.PlayTo
case GeneralCommandType.ToggleMute:
return _device.ToggleMute(cancellationToken);
case GeneralCommandType.SetAudioStreamIndex:
if (command.Arguments.TryGetValue("Index", out string index))
{
if (command.Arguments.TryGetValue("Index", out string arg))
{
if (int.TryParse(arg, NumberStyles.Integer, _usCulture, out var val))
if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
{
return SetAudioStreamIndex(val);
}
@ -685,12 +695,10 @@ namespace Emby.Dlna.PlayTo
}
throw new ArgumentException("SetAudioStreamIndex argument cannot be null");
}
case GeneralCommandType.SetSubtitleStreamIndex:
if (command.Arguments.TryGetValue("Index", out index))
{
if (command.Arguments.TryGetValue("Index", out string arg))
{
if (int.TryParse(arg, NumberStyles.Integer, _usCulture, out var val))
if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
{
return SetSubtitleStreamIndex(val);
}
@ -699,12 +707,10 @@ namespace Emby.Dlna.PlayTo
}
throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null");
}
case GeneralCommandType.SetVolume:
if (command.Arguments.TryGetValue("Volume", out string vol))
{
if (command.Arguments.TryGetValue("Volume", out string arg))
{
if (int.TryParse(arg, NumberStyles.Integer, _usCulture, out var volume))
if (int.TryParse(vol, NumberStyles.Integer, _usCulture, out var volume))
{
return _device.SetVolume(volume, cancellationToken);
}
@ -713,8 +719,6 @@ namespace Emby.Dlna.PlayTo
}
throw new ArgumentException("Volume argument cannot be null");
}
default:
return Task.CompletedTask;
}
@ -778,7 +782,7 @@ namespace Emby.Dlna.PlayTo
const int maxWait = 15000000;
const int interval = 500;
var currentWait = 0;
while (_device.TransportState != TRANSPORTSTATE.PLAYING && currentWait < maxWait)
while (_device.TransportState != TransportState.Playing && currentWait < maxWait)
{
await Task.Delay(interval).ConfigureAwait(false);
currentWait += interval;
@ -787,8 +791,67 @@ namespace Emby.Dlna.PlayTo
await _device.Seek(TimeSpan.FromTicks(positionTicks), cancellationToken).ConfigureAwait(false);
}
private static int? GetIntValue(IReadOnlyDictionary<string, string> values, string name)
{
var value = values.GetValueOrDefault(name);
if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
{
return result;
}
return null;
}
private static long GetLongValue(IReadOnlyDictionary<string, string> values, string name)
{
var value = values.GetValueOrDefault(name);
if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
{
return result;
}
return 0;
}
/// <inheritdoc />
public Task SendMessage<T>(string name, Guid messageId, T data, CancellationToken cancellationToken)
{
if (_disposed)
{
throw new ObjectDisposedException(GetType().Name);
}
if (_device == null)
{
return Task.CompletedTask;
}
if (string.Equals(name, "Play", StringComparison.OrdinalIgnoreCase))
{
return SendPlayCommand(data as PlayRequest, cancellationToken);
}
if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase))
{
return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
}
if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase))
{
return SendGeneralCommand(data as GeneralCommand, cancellationToken);
}
// Not supported or needed right now
return Task.CompletedTask;
}
private class StreamParams
{
private MediaSourceInfo mediaSource;
private IMediaSourceManager _mediaSourceManager;
public Guid ItemId { get; set; }
public bool IsDirectStream { get; set; }
@ -809,15 +872,11 @@ namespace Emby.Dlna.PlayTo
public BaseItem Item { get; set; }
private MediaSourceInfo MediaSource;
private IMediaSourceManager _mediaSourceManager;
public async Task<MediaSourceInfo> GetMediaSource(CancellationToken cancellationToken)
{
if (MediaSource != null)
if (mediaSource != null)
{
return MediaSource;
return mediaSource;
}
var hasMediaSources = Item as IHasMediaSources;
@ -827,9 +886,9 @@ namespace Emby.Dlna.PlayTo
return null;
}
MediaSource = await _mediaSourceManager.GetMediaSource(Item, MediaSourceId, LiveStreamId, false, cancellationToken).ConfigureAwait(false);
mediaSource = await _mediaSourceManager.GetMediaSource(Item, MediaSourceId, LiveStreamId, false, cancellationToken).ConfigureAwait(false);
return MediaSource;
return mediaSource;
}
private static Guid GetItemId(string url)
@ -901,61 +960,5 @@ namespace Emby.Dlna.PlayTo
return request;
}
}
private static int? GetIntValue(IReadOnlyDictionary<string, string> values, string name)
{
var value = values.GetValueOrDefault(name);
if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
{
return result;
}
return null;
}
private static long GetLongValue(IReadOnlyDictionary<string, string> values, string name)
{
var value = values.GetValueOrDefault(name);
if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
{
return result;
}
return 0;
}
/// <inheritdoc />
public Task SendMessage<T>(string name, Guid messageId, T data, CancellationToken cancellationToken)
{
if (_disposed)
{
throw new ObjectDisposedException(GetType().Name);
}
if (_device == null)
{
return Task.CompletedTask;
}
if (string.Equals(name, "Play", StringComparison.OrdinalIgnoreCase))
{
return SendPlayCommand(data as PlayRequest, cancellationToken);
}
if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase))
{
return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
}
if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase))
{
return SendGeneralCommand(data as GeneralCommand, cancellationToken);
}
// Not supported or needed right now
return Task.CompletedTask;
}
}
}

View File

@ -174,7 +174,7 @@ namespace Emby.Dlna.PlayTo
if (controller == null)
{
var device = await Device.CreateuPnpDeviceAsync(uri, _httpClient, _config, _logger, cancellationToken).ConfigureAwait(false);
var device = await Device.CreateuPnpDeviceAsync(uri, _httpClient, _logger, cancellationToken).ConfigureAwait(false);
string deviceName = device.Properties.Name;
@ -218,7 +218,7 @@ namespace Emby.Dlna.PlayTo
{
PlayableMediaTypes = profile.GetSupportedMediaTypes(),
SupportedCommands = new string[]
SupportedCommands = new[]
{
GeneralCommandType.VolumeDown.ToString(),
GeneralCommandType.VolumeUp.ToString(),
@ -247,8 +247,9 @@ namespace Emby.Dlna.PlayTo
{
_disposeCancellationTokenSource.Cancel();
}
catch
catch (Exception ex)
{
_logger.LogDebug(ex, "Error while disposing PlayToManager");
}
_sessionLock.Dispose();

View File

@ -6,6 +6,6 @@ namespace Emby.Dlna.PlayTo
{
public class PlaybackProgressEventArgs : EventArgs
{
public uBaseObject MediaInfo { get; set; }
public UBaseObject MediaInfo { get; set; }
}
}

View File

@ -6,6 +6,6 @@ namespace Emby.Dlna.PlayTo
{
public class PlaybackStartEventArgs : EventArgs
{
public uBaseObject MediaInfo { get; set; }
public UBaseObject MediaInfo { get; set; }
}
}

View File

@ -6,13 +6,6 @@ namespace Emby.Dlna.PlayTo
{
public class PlaybackStoppedEventArgs : EventArgs
{
public uBaseObject MediaInfo { get; set; }
}
public class MediaChangedEventArgs : EventArgs
{
public uBaseObject OldMediaInfo { get; set; }
public uBaseObject NewMediaInfo { get; set; }
public UBaseObject MediaInfo { get; set; }
}
}

View File

@ -1,13 +0,0 @@
#pragma warning disable CS1591
namespace Emby.Dlna.PlayTo
{
public enum TRANSPORTSTATE
{
STOPPED,
PLAYING,
TRANSITIONING,
PAUSED_PLAYBACK,
PAUSED
}
}

View File

@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
using Emby.Dlna.Common;
@ -11,36 +12,30 @@ namespace Emby.Dlna.PlayTo
{
public class TransportCommands
{
private const string CommandBase = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + "<SOAP-ENV:Body>" + "<m:{0} xmlns:m=\"{1}\">" + "{2}" + "</m:{0}>" + "</SOAP-ENV:Body></SOAP-ENV:Envelope>";
private List<StateVariable> _stateVariables = new List<StateVariable>();
public List<StateVariable> StateVariables
{
get => _stateVariables;
set => _stateVariables = value;
}
private List<ServiceAction> _serviceActions = new List<ServiceAction>();
public List<ServiceAction> ServiceActions
{
get => _serviceActions;
set => _serviceActions = value;
}
public List<StateVariable> StateVariables => _stateVariables;
public List<ServiceAction> ServiceActions => _serviceActions;
public static TransportCommands Create(XDocument document)
{
var command = new TransportCommands();
var actionList = document.Descendants(uPnpNamespaces.svc + "actionList");
var actionList = document.Descendants(UPnpNamespaces.Svc + "actionList");
foreach (var container in actionList.Descendants(uPnpNamespaces.svc + "action"))
foreach (var container in actionList.Descendants(UPnpNamespaces.Svc + "action"))
{
command.ServiceActions.Add(ServiceActionFromXml(container));
}
var stateValues = document.Descendants(uPnpNamespaces.ServiceStateTable).FirstOrDefault();
var stateValues = document.Descendants(UPnpNamespaces.ServiceStateTable).FirstOrDefault();
if (stateValues != null)
{
foreach (var container in stateValues.Elements(uPnpNamespaces.svc + "stateVariable"))
foreach (var container in stateValues.Elements(UPnpNamespaces.Svc + "stateVariable"))
{
command.StateVariables.Add(FromXml(container));
}
@ -51,19 +46,19 @@ namespace Emby.Dlna.PlayTo
private static ServiceAction ServiceActionFromXml(XElement container)
{
var argumentList = new List<Argument>();
var serviceAction = new ServiceAction
{
Name = container.GetValue(UPnpNamespaces.Svc + "name"),
};
foreach (var arg in container.Descendants(uPnpNamespaces.svc + "argument"))
var argumentList = serviceAction.ArgumentList;
foreach (var arg in container.Descendants(UPnpNamespaces.Svc + "argument"))
{
argumentList.Add(ArgumentFromXml(arg));
}
return new ServiceAction
{
Name = container.GetValue(uPnpNamespaces.svc + "name"),
ArgumentList = argumentList
};
return serviceAction;
}
private static Argument ArgumentFromXml(XElement container)
@ -75,29 +70,29 @@ namespace Emby.Dlna.PlayTo
return new Argument
{
Name = container.GetValue(uPnpNamespaces.svc + "name"),
Direction = container.GetValue(uPnpNamespaces.svc + "direction"),
RelatedStateVariable = container.GetValue(uPnpNamespaces.svc + "relatedStateVariable")
Name = container.GetValue(UPnpNamespaces.Svc + "name"),
Direction = container.GetValue(UPnpNamespaces.Svc + "direction"),
RelatedStateVariable = container.GetValue(UPnpNamespaces.Svc + "relatedStateVariable")
};
}
private static StateVariable FromXml(XElement container)
{
var allowedValues = new List<string>();
var element = container.Descendants(uPnpNamespaces.svc + "allowedValueList")
var element = container.Descendants(UPnpNamespaces.Svc + "allowedValueList")
.FirstOrDefault();
if (element != null)
{
var values = element.Descendants(uPnpNamespaces.svc + "allowedValue");
var values = element.Descendants(UPnpNamespaces.Svc + "allowedValue");
allowedValues.AddRange(values.Select(child => child.Value));
}
return new StateVariable
{
Name = container.GetValue(uPnpNamespaces.svc + "name"),
DataType = container.GetValue(uPnpNamespaces.svc + "dataType"),
Name = container.GetValue(UPnpNamespaces.Svc + "name"),
DataType = container.GetValue(UPnpNamespaces.Svc + "dataType"),
AllowedValues = allowedValues.ToArray()
};
}
@ -123,7 +118,7 @@ namespace Emby.Dlna.PlayTo
}
}
return string.Format(CommandBase, action.Name, xmlNamespace, stateString);
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString);
}
public string BuildPost(ServiceAction action, string xmlNamesapce, object value, string commandParameter = "")
@ -147,7 +142,7 @@ namespace Emby.Dlna.PlayTo
}
}
return string.Format(CommandBase, action.Name, xmlNamesapce, stateString);
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString);
}
public string BuildPost(ServiceAction action, string xmlNamesapce, object value, Dictionary<string, string> dictionary)
@ -170,7 +165,7 @@ namespace Emby.Dlna.PlayTo
}
}
return string.Format(CommandBase, action.Name, xmlNamesapce, stateString);
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString);
}
private string BuildArgumentXml(Argument argument, string value, string commandParameter = "")
@ -180,15 +175,12 @@ namespace Emby.Dlna.PlayTo
if (state != null)
{
var sendValue = state.AllowedValues.FirstOrDefault(a => string.Equals(a, commandParameter, StringComparison.OrdinalIgnoreCase)) ??
state.AllowedValues.FirstOrDefault() ??
value;
(state.AllowedValues.Count > 0 ? state.AllowedValues[0] : value);
return string.Format("<{0} xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"{1}\">{2}</{0}>", argument.Name, state.DataType ?? "string", sendValue);
return string.Format(CultureInfo.InvariantCulture, "<{0} xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"{1}\">{2}</{0}>", argument.Name, state.DataType ?? "string", sendValue);
}
return string.Format("<{0}>{1}</{0}>", argument.Name, value);
}
private const string CommandBase = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + "<SOAP-ENV:Body>" + "<m:{0} xmlns:m=\"{1}\">" + "{2}" + "</m:{0}>" + "</SOAP-ENV:Body></SOAP-ENV:Envelope>";
return string.Format(CultureInfo.InvariantCulture, "<{0}>{1}</{0}>", argument.Name, value);
}
}
}

View File

@ -0,0 +1,14 @@
#pragma warning disable CS1591
#pragma warning disable SA1602
namespace Emby.Dlna.PlayTo
{
public enum TransportState
{
Stopped,
Playing,
Transitioning,
PausedPlayback,
Paused
}
}

View File

@ -6,22 +6,22 @@ using Emby.Dlna.Ssdp;
namespace Emby.Dlna.PlayTo
{
public class UpnpContainer : uBaseObject
public class UpnpContainer : UBaseObject
{
public static uBaseObject Create(XElement container)
public static UBaseObject Create(XElement container)
{
if (container == null)
{
throw new ArgumentNullException(nameof(container));
}
return new uBaseObject
return new UBaseObject
{
Id = container.GetAttributeValue(uPnpNamespaces.Id),
ParentId = container.GetAttributeValue(uPnpNamespaces.ParentId),
Title = container.GetValue(uPnpNamespaces.title),
IconUrl = container.GetValue(uPnpNamespaces.Artwork),
UpnpClass = container.GetValue(uPnpNamespaces.uClass)
Id = container.GetAttributeValue(UPnpNamespaces.Id),
ParentId = container.GetAttributeValue(UPnpNamespaces.ParentId),
Title = container.GetValue(UPnpNamespaces.Title),
IconUrl = container.GetValue(UPnpNamespaces.Artwork),
UpnpClass = container.GetValue(UPnpNamespaces.Class)
};
}
}

View File

@ -1,10 +1,11 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
namespace Emby.Dlna.PlayTo
{
public class uBaseObject
public class UBaseObject
{
public string Id { get; set; }
@ -20,20 +21,10 @@ namespace Emby.Dlna.PlayTo
public string Url { get; set; }
public string[] ProtocolInfo { get; set; }
public IReadOnlyList<string> ProtocolInfo { get; set; }
public string UpnpClass { get; set; }
public bool Equals(uBaseObject obj)
{
if (obj == null)
{
throw new ArgumentNullException(nameof(obj));
}
return string.Equals(Id, obj.Id);
}
public string MediaType
{
get
@ -58,5 +49,15 @@ namespace Emby.Dlna.PlayTo
return null;
}
}
public bool Equals(UBaseObject obj)
{
if (obj == null)
{
throw new ArgumentNullException(nameof(obj));
}
return string.Equals(Id, obj.Id, StringComparison.Ordinal);
}
}
}

View File

@ -4,38 +4,64 @@ using System.Xml.Linq;
namespace Emby.Dlna.PlayTo
{
public class uPnpNamespaces
public static class UPnpNamespaces
{
public static XNamespace dc = "http://purl.org/dc/elements/1.1/";
public static XNamespace ns = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
public static XNamespace svc = "urn:schemas-upnp-org:service-1-0";
public static XNamespace ud = "urn:schemas-upnp-org:device-1-0";
public static XNamespace upnp = "urn:schemas-upnp-org:metadata-1-0/upnp/";
public static XNamespace RenderingControl = "urn:schemas-upnp-org:service:RenderingControl:1";
public static XNamespace AvTransport = "urn:schemas-upnp-org:service:AVTransport:1";
public static XNamespace ContentDirectory = "urn:schemas-upnp-org:service:ContentDirectory:1";
public static XNamespace Dc { get; } = "http://purl.org/dc/elements/1.1/";
public static XName containers = ns + "container";
public static XName items = ns + "item";
public static XName title = dc + "title";
public static XName creator = dc + "creator";
public static XName artist = upnp + "artist";
public static XName Id = "id";
public static XName ParentId = "parentID";
public static XName uClass = upnp + "class";
public static XName Artwork = upnp + "albumArtURI";
public static XName Description = dc + "description";
public static XName LongDescription = upnp + "longDescription";
public static XName Album = upnp + "album";
public static XName Author = upnp + "author";
public static XName Director = upnp + "director";
public static XName PlayCount = upnp + "playbackCount";
public static XName Tracknumber = upnp + "originalTrackNumber";
public static XName Res = ns + "res";
public static XName Duration = "duration";
public static XName ProtocolInfo = "protocolInfo";
public static XNamespace Ns { get; } = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
public static XName ServiceStateTable = svc + "serviceStateTable";
public static XName StateVariable = svc + "stateVariable";
public static XNamespace Svc { get; } = "urn:schemas-upnp-org:service-1-0";
public static XNamespace Ud { get; } = "urn:schemas-upnp-org:device-1-0";
public static XNamespace UPnp { get; } = "urn:schemas-upnp-org:metadata-1-0/upnp/";
public static XNamespace RenderingControl { get; } = "urn:schemas-upnp-org:service:RenderingControl:1";
public static XNamespace AvTransport { get; } = "urn:schemas-upnp-org:service:AVTransport:1";
public static XNamespace ContentDirectory { get; } = "urn:schemas-upnp-org:service:ContentDirectory:1";
public static XName Containers { get; } = Ns + "container";
public static XName Items { get; } = Ns + "item";
public static XName Title { get; } = Dc + "title";
public static XName Creator { get; } = Dc + "creator";
public static XName Artist { get; } = UPnp + "artist";
public static XName Id { get; } = "id";
public static XName ParentId { get; } = "parentID";
public static XName Class { get; } = UPnp + "class";
public static XName Artwork { get; } = UPnp + "albumArtURI";
public static XName Description { get; } = Dc + "description";
public static XName LongDescription { get; } = UPnp + "longDescription";
public static XName Album { get; } = UPnp + "album";
public static XName Author { get; } = UPnp + "author";
public static XName Director { get; } = UPnp + "director";
public static XName PlayCount { get; } = UPnp + "playbackCount";
public static XName Tracknumber { get; } = UPnp + "originalTrackNumber";
public static XName Res { get; } = Ns + "res";
public static XName Duration { get; } = "duration";
public static XName ProtocolInfo { get; } = "protocolInfo";
public static XName ServiceStateTable { get; } = Svc + "serviceStateTable";
public static XName StateVariable { get; } = Svc + "stateVariable";
}
}

View File

@ -64,14 +64,14 @@ namespace Emby.Dlna.Profiles
new DirectPlayProfile
{
// play all
Container = "",
Container = string.Empty,
Type = DlnaProfileType.Video
},
new DirectPlayProfile
{
// play all
Container = "",
Container = string.Empty,
Type = DlnaProfileType.Audio
}
};

View File

@ -191,7 +191,7 @@ namespace Emby.Dlna.Profiles
}
};
ResponseProfiles = new ResponseProfile[]
ResponseProfiles = new[]
{
new ResponseProfile
{

View File

@ -32,7 +32,7 @@ namespace Emby.Dlna.Profiles
}
};
ResponseProfiles = new ResponseProfile[]
ResponseProfiles = new[]
{
new ResponseProfile
{

View File

@ -202,7 +202,7 @@ namespace Emby.Dlna.Profiles
}
};
ResponseProfiles = new ResponseProfile[]
ResponseProfiles = new[]
{
new ResponseProfile
{

View File

@ -265,7 +265,6 @@ namespace Emby.Dlna.Profiles
}
};
CodecProfiles = new[]
{
new CodecProfile

View File

@ -265,7 +265,6 @@ namespace Emby.Dlna.Profiles
}
};
CodecProfiles = new[]
{
new CodecProfile

View File

@ -15,11 +15,7 @@ namespace Emby.Dlna.Service
{
public abstract class BaseControlHandler
{
private const string NS_SOAPENV = "http://schemas.xmlsoap.org/soap/envelope/";
protected IServerConfigurationManager Config { get; }
protected ILogger Logger { get; }
private const string NsSoapEnv = "http://schemas.xmlsoap.org/soap/envelope/";
protected BaseControlHandler(IServerConfigurationManager config, ILogger logger)
{
@ -27,6 +23,10 @@ namespace Emby.Dlna.Service
Logger = logger;
}
protected IServerConfigurationManager Config { get; }
protected ILogger Logger { get; }
public async Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
{
try
@ -80,10 +80,10 @@ namespace Emby.Dlna.Service
{
writer.WriteStartDocument(true);
writer.WriteStartElement("SOAP-ENV", "Envelope", NS_SOAPENV);
writer.WriteAttributeString(string.Empty, "encodingStyle", NS_SOAPENV, "http://schemas.xmlsoap.org/soap/encoding/");
writer.WriteStartElement("SOAP-ENV", "Envelope", NsSoapEnv);
writer.WriteAttributeString(string.Empty, "encodingStyle", NsSoapEnv, "http://schemas.xmlsoap.org/soap/encoding/");
writer.WriteStartElement("SOAP-ENV", "Body", NS_SOAPENV);
writer.WriteStartElement("SOAP-ENV", "Body", NsSoapEnv);
writer.WriteStartElement("u", requestInfo.LocalName + "Response", requestInfo.NamespaceURI);
WriteResult(requestInfo.LocalName, requestInfo.Headers, writer);
@ -210,15 +210,6 @@ namespace Emby.Dlna.Service
}
}
private class ControlRequestInfo
{
public string LocalName { get; set; }
public string NamespaceURI { get; set; }
public Dictionary<string, string> Headers { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
protected abstract void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter);
private void LogRequest(ControlRequest request)
@ -240,5 +231,14 @@ namespace Emby.Dlna.Service
Logger.LogDebug("Control response. Headers: {@Headers}\n{Xml}", response.Headers, response.Xml);
}
private class ControlRequestInfo
{
public string LocalName { get; set; }
public string NamespaceURI { get; set; }
public Dictionary<string, string> Headers { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
}
}

View File

@ -8,10 +8,6 @@ namespace Emby.Dlna.Service
{
public class BaseService : IEventManager
{
protected IEventManager EventManager;
protected IHttpClient HttpClient;
protected ILogger Logger;
protected BaseService(ILogger<BaseService> logger, IHttpClient httpClient)
{
Logger = logger;
@ -20,6 +16,12 @@ namespace Emby.Dlna.Service
EventManager = new EventManager(logger, HttpClient);
}
protected IEventManager EventManager { get; }
protected IHttpClient HttpClient { get; }
protected ILogger Logger { get; }
public EventSubscriptionResponse CancelEventSubscription(string subscriptionId)
{
return EventManager.CancelEventSubscription(subscriptionId);

View File

@ -10,7 +10,7 @@ namespace Emby.Dlna.Service
{
public static class ControlErrorHandler
{
private const string NS_SOAPENV = "http://schemas.xmlsoap.org/soap/envelope/";
private const string NsSoapEnv = "http://schemas.xmlsoap.org/soap/envelope/";
public static ControlResponse GetResponse(Exception ex)
{
@ -26,11 +26,11 @@ namespace Emby.Dlna.Service
{
writer.WriteStartDocument(true);
writer.WriteStartElement("SOAP-ENV", "Envelope", NS_SOAPENV);
writer.WriteAttributeString(string.Empty, "encodingStyle", NS_SOAPENV, "http://schemas.xmlsoap.org/soap/encoding/");
writer.WriteStartElement("SOAP-ENV", "Envelope", NsSoapEnv);
writer.WriteAttributeString(string.Empty, "encodingStyle", NsSoapEnv, "http://schemas.xmlsoap.org/soap/encoding/");
writer.WriteStartElement("SOAP-ENV", "Body", NS_SOAPENV);
writer.WriteStartElement("SOAP-ENV", "Fault", NS_SOAPENV);
writer.WriteStartElement("SOAP-ENV", "Body", NsSoapEnv);
writer.WriteStartElement("SOAP-ENV", "Fault", NsSoapEnv);
writer.WriteElementString("faultcode", "500");
writer.WriteElementString("faultstring", ex.Message);

View File

@ -87,7 +87,7 @@ namespace Emby.Dlna.Service
.Append(SecurityElement.Escape(item.DataType ?? string.Empty))
.Append("</dataType>");
if (item.AllowedValues.Length > 0)
if (item.AllowedValues.Count > 0)
{
builder.Append("<allowedValueList>");
foreach (var allowedValue in item.AllowedValues)

View File

@ -17,9 +17,17 @@ namespace Emby.Dlna.Ssdp
private readonly IServerConfigurationManager _config;
private SsdpDeviceLocator _deviceLocator;
private ISsdpCommunicationsServer _commsServer;
private int _listenerCount;
private bool _disposed;
public DeviceDiscovery(IServerConfigurationManager config)
{
_config = config;
}
private event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceDiscoveredInternal;
/// <inheritdoc />
@ -49,15 +57,6 @@ namespace Emby.Dlna.Ssdp
/// <inheritdoc />
public event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceLeft;
private SsdpDeviceLocator _deviceLocator;
private ISsdpCommunicationsServer _commsServer;
public DeviceDiscovery(IServerConfigurationManager config)
{
_config = config;
}
// Call this method from somewhere in your code to start the search.
public void Start(ISsdpCommunicationsServer communicationsServer)
{

View File

@ -5,7 +5,7 @@ using System.Xml.Linq;
namespace Emby.Dlna.Ssdp
{
public static class Extensions
public static class SsdpExtensions
{
public static string GetValue(this XElement container, XName name)
{

View File

@ -1,3 +1,5 @@
#nullable enable
using System;
using System.IO;
using System.Linq;
@ -22,7 +24,7 @@ namespace Emby.Server.Implementations.AppBase
{
object configuration;
byte[] buffer = null;
byte[]? buffer = null;
// Use try/catch to avoid the extra file system lookup using File.Exists
try
@ -36,19 +38,23 @@ namespace Emby.Server.Implementations.AppBase
configuration = Activator.CreateInstance(type);
}
using var stream = new MemoryStream();
using var stream = new MemoryStream(buffer?.Length ?? 0);
xmlSerializer.SerializeToStream(configuration, stream);
// Take the object we just got and serialize it back to bytes
var newBytes = stream.ToArray();
byte[] newBytes = stream.GetBuffer();
int newBytesLen = (int)stream.Length;
// If the file didn't exist before, or if something has changed, re-save
if (buffer == null || !buffer.SequenceEqual(newBytes))
if (buffer == null || !newBytes.AsSpan(0, newBytesLen).SequenceEqual(buffer))
{
Directory.CreateDirectory(Path.GetDirectoryName(path));
// Save it after load in case we got new items
File.WriteAllBytes(path, newBytes);
using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
{
fs.Write(newBytes, 0, newBytesLen);
}
}
return configuration;

View File

@ -632,6 +632,9 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
serviceCollection.AddSingleton<TranscodingJobHelper>();
serviceCollection.AddScoped<MediaInfoHelper>();
serviceCollection.AddScoped<AudioHelper>();
serviceCollection.AddScoped<DynamicHlsHelper>();
}
/// <summary>

View File

@ -746,12 +746,21 @@ namespace Emby.Server.Implementations.Channels
// null if came from cache
if (itemsResult != null)
{
var internalItems = itemsResult.Items
.Select(i => GetChannelItemEntity(i, channelProvider, channel.Id, parentItem, cancellationToken))
.ToArray();
var items = itemsResult.Items;
var itemsLen = items.Count;
var internalItems = new Guid[itemsLen];
for (int i = 0; i < itemsLen; i++)
{
internalItems[i] = (await GetChannelItemEntityAsync(
items[i],
channelProvider,
channel.Id,
parentItem,
cancellationToken).ConfigureAwait(false)).Id;
}
var existingIds = _libraryManager.GetItemIds(query);
var deadIds = existingIds.Except(internalItems.Select(i => i.Id))
var deadIds = existingIds.Except(internalItems)
.ToArray();
foreach (var deadId in deadIds)
@ -963,7 +972,7 @@ namespace Emby.Server.Implementations.Channels
return item;
}
private BaseItem GetChannelItemEntity(ChannelItemInfo info, IChannel channelProvider, Guid internalChannelId, BaseItem parentFolder, CancellationToken cancellationToken)
private async Task<BaseItem> GetChannelItemEntityAsync(ChannelItemInfo info, IChannel channelProvider, Guid internalChannelId, BaseItem parentFolder, CancellationToken cancellationToken)
{
var parentFolderId = parentFolder.Id;
@ -1165,7 +1174,7 @@ namespace Emby.Server.Implementations.Channels
}
else if (forceUpdate)
{
item.UpdateToRepository(ItemUpdateType.None, cancellationToken);
await item.UpdateToRepositoryAsync(ItemUpdateType.None, cancellationToken).ConfigureAwait(false);
}
if ((isNew || forceUpdate) && info.Type == ChannelItemType.Media)

View File

@ -132,7 +132,7 @@ namespace Emby.Server.Implementations.Collections
}
/// <inheritdoc />
public BoxSet CreateCollection(CollectionCreationOptions options)
public async Task<BoxSet> CreateCollectionAsync(CollectionCreationOptions options)
{
var name = options.Name;
@ -141,7 +141,7 @@ namespace Emby.Server.Implementations.Collections
// This could cause it to get re-resolved as a plain folder
var folderName = _fileSystem.GetValidFilename(name) + " [boxset]";
var parentFolder = GetCollectionsFolder(true).GetAwaiter().GetResult();
var parentFolder = await GetCollectionsFolder(true).ConfigureAwait(false);
if (parentFolder == null)
{
@ -169,12 +169,16 @@ namespace Emby.Server.Implementations.Collections
if (options.ItemIdList.Length > 0)
{
AddToCollection(collection.Id, options.ItemIdList, false, new MetadataRefreshOptions(new DirectoryService(_fileSystem))
await AddToCollectionAsync(
collection.Id,
options.ItemIdList.Select(x => new Guid(x)),
false,
new MetadataRefreshOptions(new DirectoryService(_fileSystem))
{
// The initial adding of items is going to create a local metadata file
// This will cause internet metadata to be skipped as a result
MetadataRefreshMode = MetadataRefreshMode.FullRefresh
});
}).ConfigureAwait(false);
}
else
{
@ -197,18 +201,10 @@ namespace Emby.Server.Implementations.Collections
}
/// <inheritdoc />
public void AddToCollection(Guid collectionId, IEnumerable<string> ids)
{
AddToCollection(collectionId, ids, true, new MetadataRefreshOptions(new DirectoryService(_fileSystem)));
}
public Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> ids)
=> AddToCollectionAsync(collectionId, ids, true, new MetadataRefreshOptions(new DirectoryService(_fileSystem)));
/// <inheritdoc />
public void AddToCollection(Guid collectionId, IEnumerable<Guid> ids)
{
AddToCollection(collectionId, ids.Select(i => i.ToString("N", CultureInfo.InvariantCulture)), true, new MetadataRefreshOptions(new DirectoryService(_fileSystem)));
}
private void AddToCollection(Guid collectionId, IEnumerable<string> ids, bool fireEvent, MetadataRefreshOptions refreshOptions)
private async Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> ids, bool fireEvent, MetadataRefreshOptions refreshOptions)
{
var collection = _libraryManager.GetItemById(collectionId) as BoxSet;
if (collection == null)
@ -224,15 +220,14 @@ namespace Emby.Server.Implementations.Collections
foreach (var id in ids)
{
var guidId = new Guid(id);
var item = _libraryManager.GetItemById(guidId);
var item = _libraryManager.GetItemById(id);
if (item == null)
{
throw new ArgumentException("No item exists with the supplied Id");
}
if (!currentLinkedChildrenIds.Contains(guidId))
if (!currentLinkedChildrenIds.Contains(id))
{
itemList.Add(item);
@ -249,7 +244,7 @@ namespace Emby.Server.Implementations.Collections
collection.UpdateRatingToItems(linkedChildrenList);
collection.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
await collection.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
refreshOptions.ForceSave = true;
_providerManager.QueueRefresh(collection.Id, refreshOptions, RefreshPriority.High);
@ -266,13 +261,7 @@ namespace Emby.Server.Implementations.Collections
}
/// <inheritdoc />
public void RemoveFromCollection(Guid collectionId, IEnumerable<string> itemIds)
{
RemoveFromCollection(collectionId, itemIds.Select(i => new Guid(i)));
}
/// <inheritdoc />
public void RemoveFromCollection(Guid collectionId, IEnumerable<Guid> itemIds)
public async Task RemoveFromCollectionAsync(Guid collectionId, IEnumerable<Guid> itemIds)
{
var collection = _libraryManager.GetItemById(collectionId) as BoxSet;
@ -309,7 +298,7 @@ namespace Emby.Server.Implementations.Collections
collection.LinkedChildren = collection.LinkedChildren.Except(list).ToArray();
}
collection.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
await collection.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
_providerManager.QueueRefresh(
collection.Id,
new MetadataRefreshOptions(new DirectoryService(_fileSystem))

View File

@ -1,6 +1,5 @@
using System.Collections.Generic;
using Emby.Server.Implementations.HttpServer;
using Emby.Server.Implementations.Updates;
using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
namespace Emby.Server.Implementations
@ -19,7 +18,8 @@ namespace Emby.Server.Implementations
{ HttpListenerHost.DefaultRedirectKey, "web/index.html" },
{ FfmpegProbeSizeKey, "1G" },
{ FfmpegAnalyzeDurationKey, "200M" },
{ PlaylistsAllowDuplicatesKey, bool.TrueString }
{ PlaylistsAllowDuplicatesKey, bool.TrueString },
{ BindToUnixSocketKey, bool.FalseString }
};
}
}

View File

@ -90,6 +90,9 @@ namespace Emby.Server.Implementations.Data
_typeMapper = new TypeMapper();
_jsonOptions = JsonDefaults.GetOptions();
// GetItem throws NotSupportedException with this enabled, so hardcode false.
_jsonOptions.IgnoreNullValues = false;
DbFilePath = Path.Combine(_config.ApplicationPaths.DataPath, "library.db");
}
@ -4308,7 +4311,7 @@ namespace Emby.Server.Implementations.Data
whereClauses.Add("ProductionYear=@Years");
if (statement != null)
{
statement.TryBind("@Years", query.Years[0].ToString());
statement.TryBind("@Years", query.Years[0].ToString(CultureInfo.InvariantCulture));
}
}
else if (query.Years.Length > 1)
@ -4560,13 +4563,13 @@ namespace Emby.Server.Implementations.Data
if (query.AncestorIds.Length > 1)
{
var inClause = string.Join(",", query.AncestorIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
whereClauses.Add(string.Format("Guid in (select itemId from AncestorIds where AncestorIdText in ({0}))", inClause));
whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorIdText in ({0}))", inClause));
}
if (!string.IsNullOrWhiteSpace(query.AncestorWithPresentationUniqueKey))
{
var inClause = "select guid from TypedBaseItems where PresentationUniqueKey=@AncestorWithPresentationUniqueKey";
whereClauses.Add(string.Format("Guid in (select itemId from AncestorIds where AncestorId in ({0}))", inClause));
whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorId in ({0}))", inClause));
if (statement != null)
{
statement.TryBind("@AncestorWithPresentationUniqueKey", query.AncestorWithPresentationUniqueKey);
@ -5170,7 +5173,10 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
insertText.Append(',');
}
insertText.AppendFormat("(@ItemId, @AncestorId{0}, @AncestorIdText{0})", i.ToString(CultureInfo.InvariantCulture));
insertText.AppendFormat(
CultureInfo.InvariantCulture,
"(@ItemId, @AncestorId{0}, @AncestorIdText{0})",
i.ToString(CultureInfo.InvariantCulture));
}
using (var statement = PrepareStatement(db, insertText.ToString()))

View File

@ -73,25 +73,6 @@ namespace Emby.Server.Implementations.Dto
_livetvManagerFactory = livetvManagerFactory;
}
/// <summary>
/// Converts a BaseItem to a DTOBaseItem.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="fields">The fields.</param>
/// <param name="user">The user.</param>
/// <param name="owner">The owner.</param>
/// <returns>Task{DtoBaseItem}.</returns>
/// <exception cref="ArgumentNullException">item</exception>
public BaseItemDto GetBaseItemDto(BaseItem item, ItemFields[] fields, User user = null, BaseItem owner = null)
{
var options = new DtoOptions
{
Fields = fields
};
return GetBaseItemDto(item, options, user, owner);
}
/// <inheritdoc />
public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null)
{
@ -443,17 +424,6 @@ namespace Emby.Server.Implementations.Dto
return folder.GetChildCount(user);
}
/// <summary>
/// Gets client-side Id of a server-side BaseItem.
/// </summary>
/// <param name="item">The item.</param>
/// <returns>System.String.</returns>
/// <exception cref="ArgumentNullException">item</exception>
public string GetDtoId(BaseItem item)
{
return item.Id.ToString("N", CultureInfo.InvariantCulture);
}
private static void SetBookProperties(BaseItemDto dto, Book item)
{
dto.SeriesName = item.SeriesName;
@ -484,6 +454,11 @@ namespace Emby.Server.Implementations.Dto
}
}
private string GetDtoId(BaseItem item)
{
return item.Id.ToString("N", CultureInfo.InvariantCulture);
}
private void SetMusicVideoProperties(BaseItemDto dto, MusicVideo item)
{
if (!string.IsNullOrEmpty(item.Album))
@ -513,19 +488,6 @@ namespace Emby.Server.Implementations.Dto
.ToArray();
}
private string GetImageCacheTag(BaseItem item, ImageType type)
{
try
{
return _imageProcessor.GetImageCacheTag(item, type);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting {type} image info", type);
return null;
}
}
private string GetImageCacheTag(BaseItem item, ItemImageInfo image)
{
try

View File

@ -41,7 +41,7 @@
<PackageReference Include="ServiceStack.Text.Core" Version="5.9.2" />
<PackageReference Include="sharpcompress" Version="0.26.0" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
<PackageReference Include="DotNet.Glob" Version="3.0.9" />
<PackageReference Include="DotNet.Glob" Version="3.1.0" />
</ItemGroup>
<ItemGroup>

View File

@ -105,7 +105,7 @@ namespace Emby.Server.Implementations.HttpServer
responseHeaders = new Dictionary<string, string>();
}
if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string expires))
if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out _))
{
responseHeaders[HeaderNames.Expires] = "0";
}
@ -326,7 +326,8 @@ namespace Emby.Server.Implementations.HttpServer
return GetHttpResult(request, ms, contentType, true, responseHeaders);
}
private IHasHeaders GetCompressedResult(byte[] content,
private IHasHeaders GetCompressedResult(
byte[] content,
string requestedCompressionType,
IDictionary<string, string> responseHeaders,
bool isHeadRequest,

View File

@ -95,13 +95,13 @@ namespace Emby.Server.Implementations.HttpServer
if (bytes != null)
{
await responseStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
await responseStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken).ConfigureAwait(false);
}
else
{
using (var src = SourceStream)
{
await src.CopyToAsync(responseStream).ConfigureAwait(false);
await src.CopyToAsync(responseStream, cancellationToken).ConfigureAwait(false);
}
}
}

View File

@ -6,12 +6,11 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Emby.Server.Implementations.Library;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Model.IO;
using Emby.Server.Implementations.Library;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.IO
@ -38,6 +37,8 @@ namespace Emby.Server.Implementations.IO
/// </summary>
private readonly ConcurrentDictionary<string, string> _tempIgnoredPaths = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
private bool _disposed = false;
/// <summary>
/// Add the path to our temporary ignore list. Use when writing to a path within our listening scope.
/// </summary>
@ -492,8 +493,6 @@ namespace Emby.Server.Implementations.IO
}
}
private bool _disposed = false;
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
@ -522,24 +521,4 @@ namespace Emby.Server.Implementations.IO
_disposed = true;
}
}
public class LibraryMonitorStartup : IServerEntryPoint
{
private readonly ILibraryMonitor _monitor;
public LibraryMonitorStartup(ILibraryMonitor monitor)
{
_monitor = monitor;
}
public Task RunAsync()
{
_monitor.Start();
return Task.CompletedTask;
}
public void Dispose()
{
}
}
}

View File

@ -0,0 +1,35 @@
using System.Threading.Tasks;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Plugins;
namespace Emby.Server.Implementations.IO
{
/// <summary>
/// <see cref="IServerEntryPoint" /> which is responsible for starting the library monitor.
/// </summary>
public sealed class LibraryMonitorStartup : IServerEntryPoint
{
private readonly ILibraryMonitor _monitor;
/// <summary>
/// Initializes a new instance of the <see cref="LibraryMonitorStartup"/> class.
/// </summary>
/// <param name="monitor">The library monitor.</param>
public LibraryMonitorStartup(ILibraryMonitor monitor)
{
_monitor = monitor;
}
/// <inheritdoc />
public Task RunAsync()
{
_monitor.Start();
return Task.CompletedTask;
}
/// <inheritdoc />
public void Dispose()
{
}
}
}

View File

@ -771,7 +771,7 @@ namespace Emby.Server.Implementations.Library
if (folder.ParentId != rootFolder.Id)
{
folder.ParentId = rootFolder.Id;
folder.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None);
folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetResult();
}
rootFolder.AddVirtualChild(folder);
@ -1868,7 +1868,8 @@ namespace Emby.Server.Implementations.Library
return image.Path != null && !image.IsLocalFile;
}
public void UpdateImages(BaseItem item, bool forceUpdate = false)
/// <inheritdoc />
public async Task UpdateImagesAsync(BaseItem item, bool forceUpdate = false)
{
if (item == null)
{
@ -1891,7 +1892,7 @@ namespace Emby.Server.Implementations.Library
try
{
var index = item.GetImageIndex(img);
image = ConvertImageToLocal(item, img, index).ConfigureAwait(false).GetAwaiter().GetResult();
image = await ConvertImageToLocal(item, img, index).ConfigureAwait(false);
}
catch (ArgumentException)
{
@ -1913,7 +1914,7 @@ namespace Emby.Server.Implementations.Library
}
catch (Exception ex)
{
_logger.LogError(ex, "Cannnot get image dimensions for {0}", image.Path);
_logger.LogError(ex, "Cannot get image dimensions for {0}", image.Path);
image.Width = 0;
image.Height = 0;
continue;
@ -1943,10 +1944,8 @@ namespace Emby.Server.Implementations.Library
RegisterItem(item);
}
/// <summary>
/// Updates the item.
/// </summary>
public void UpdateItems(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
/// <inheritdoc />
public async Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
{
foreach (var item in items)
{
@ -1957,7 +1956,7 @@ namespace Emby.Server.Implementations.Library
item.DateLastSaved = DateTime.UtcNow;
UpdateImages(item, updateReason >= ItemUpdateType.ImageUpdate);
await UpdateImagesAsync(item, updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false);
}
_itemRepository.SaveItems(items, cancellationToken);
@ -1991,17 +1990,9 @@ namespace Emby.Server.Implementations.Library
}
}
/// <summary>
/// Updates the item.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="parent">The parent item.</param>
/// <param name="updateReason">The update reason.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public void UpdateItem(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
{
UpdateItems(new[] { item }, parent, updateReason, cancellationToken);
}
/// <inheritdoc />
public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
=> UpdateItemsAsync(new[] { item }, parent, updateReason, cancellationToken);
/// <summary>
/// Reports the item removed.
@ -2233,7 +2224,7 @@ namespace Emby.Server.Implementations.Library
if (refresh)
{
item.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None);
item.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetResult();
ProviderManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.Normal);
}
@ -2420,7 +2411,7 @@ namespace Emby.Server.Implementations.Library
if (!string.Equals(viewType, item.ViewType, StringComparison.OrdinalIgnoreCase))
{
item.ViewType = viewType;
item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
}
var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval;
@ -2902,7 +2893,7 @@ namespace Emby.Server.Implementations.Library
await ProviderManager.SaveImage(item, url, image.Type, imageIndex, CancellationToken.None).ConfigureAwait(false);
item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
return item.GetImageInfo(image.Type, imageIndex);
}
@ -2920,7 +2911,7 @@ namespace Emby.Server.Implementations.Library
// Remove this image to prevent it from retrying over and over
item.RemoveImage(image);
item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
throw new InvalidOperationException();
}

View File

@ -230,7 +230,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
if (filters.Count > 0)
{
output += string.Format(" -vf \"{0}\"", string.Join(",", filters.ToArray()));
output += string.Format(CultureInfo.InvariantCulture, " -vf \"{0}\"", string.Join(",", filters.ToArray()));
}
return output;

View File

@ -5,7 +5,7 @@ using MediaBrowser.Controller.Plugins;
namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
public class EntryPoint : IServerEntryPoint
public sealed class EntryPoint : IServerEntryPoint
{
/// <inheritdoc />
public Task RunAsync()

View File

@ -929,7 +929,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private static string NormalizeName(string value)
{
return value.Replace(" ", string.Empty).Replace("-", string.Empty);
return value.Replace(" ", string.Empty, StringComparison.Ordinal).Replace("-", string.Empty, StringComparison.Ordinal);
}
public class ScheduleDirect

View File

@ -237,7 +237,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
&& !programInfo.IsRepeat
&& (programInfo.EpisodeNumber ?? 0) == 0)
{
programInfo.ShowId = programInfo.ShowId + programInfo.StartDate.Ticks.ToString(CultureInfo.InvariantCulture);
programInfo.ShowId += programInfo.StartDate.Ticks.ToString(CultureInfo.InvariantCulture);
}
}
else
@ -246,7 +246,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
}
// Construct an id from the channel and start date
programInfo.Id = string.Format("{0}_{1:O}", program.ChannelId, program.StartDate);
programInfo.Id = string.Format(CultureInfo.InvariantCulture, "{0}_{1:O}", program.ChannelId, program.StartDate);
if (programInfo.IsMovie)
{
@ -296,7 +296,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
Name = c.DisplayName,
ImageUrl = c.Icon != null && !string.IsNullOrEmpty(c.Icon.Source) ? c.Icon.Source : null,
Number = string.IsNullOrWhiteSpace(c.Number) ? c.Id : c.Number
}).ToList();
}
}

View File

@ -41,6 +41,7 @@ namespace Emby.Server.Implementations.LiveTv
/// </summary>
public class LiveTvManager : ILiveTvManager, IDisposable
{
private const int MaxGuideDays = 14;
private const string ExternalServiceTag = "ExternalServiceId";
private const string EtagKey = "ProgramEtag";
@ -421,7 +422,7 @@ namespace Emby.Server.Implementations.LiveTv
}
}
private LiveTvChannel GetChannel(ChannelInfo channelInfo, string serviceName, BaseItem parentFolder, CancellationToken cancellationToken)
private async Task<LiveTvChannel> GetChannelAsync(ChannelInfo channelInfo, string serviceName, BaseItem parentFolder, CancellationToken cancellationToken)
{
var parentFolderId = parentFolder.Id;
var isNew = false;
@ -511,7 +512,7 @@ namespace Emby.Server.Implementations.LiveTv
}
else if (forceUpdate)
{
_libraryManager.UpdateItem(item, parentFolder, ItemUpdateType.MetadataImport, cancellationToken);
await _libraryManager.UpdateItemAsync(item, parentFolder, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
}
return item;
@ -560,7 +561,7 @@ namespace Emby.Server.Implementations.LiveTv
item.Audio = info.Audio;
item.ChannelId = channel.Id;
item.CommunityRating = item.CommunityRating ?? info.CommunityRating;
item.CommunityRating ??= info.CommunityRating;
if ((item.CommunityRating ?? 0).Equals(0))
{
item.CommunityRating = null;
@ -645,8 +646,8 @@ namespace Emby.Server.Implementations.LiveTv
item.IsSeries = isSeries;
item.Name = info.Name;
item.OfficialRating = item.OfficialRating ?? info.OfficialRating;
item.Overview = item.Overview ?? info.Overview;
item.OfficialRating ??= info.OfficialRating;
item.Overview ??= info.Overview;
item.RunTimeTicks = (info.EndDate - info.StartDate).Ticks;
item.ProviderIds = info.ProviderIds;
@ -683,19 +684,23 @@ namespace Emby.Server.Implementations.LiveTv
{
if (!string.IsNullOrWhiteSpace(info.ImagePath))
{
item.SetImage(new ItemImageInfo
item.SetImage(
new ItemImageInfo
{
Path = info.ImagePath,
Type = ImageType.Primary
}, 0);
},
0);
}
else if (!string.IsNullOrWhiteSpace(info.ImageUrl))
{
item.SetImage(new ItemImageInfo
item.SetImage(
new ItemImageInfo
{
Path = info.ImageUrl,
Type = ImageType.Primary
}, 0);
},
0);
}
}
@ -703,11 +708,13 @@ namespace Emby.Server.Implementations.LiveTv
{
if (!string.IsNullOrWhiteSpace(info.ThumbImageUrl))
{
item.SetImage(new ItemImageInfo
item.SetImage(
new ItemImageInfo
{
Path = info.ThumbImageUrl,
Type = ImageType.Thumb
}, 0);
},
0);
}
}
@ -715,11 +722,13 @@ namespace Emby.Server.Implementations.LiveTv
{
if (!string.IsNullOrWhiteSpace(info.LogoImageUrl))
{
item.SetImage(new ItemImageInfo
item.SetImage(
new ItemImageInfo
{
Path = info.LogoImageUrl,
Type = ImageType.Logo
}, 0);
},
0);
}
}
@ -727,11 +736,13 @@ namespace Emby.Server.Implementations.LiveTv
{
if (!string.IsNullOrWhiteSpace(info.BackdropImageUrl))
{
item.SetImage(new ItemImageInfo
item.SetImage(
new ItemImageInfo
{
Path = info.BackdropImageUrl,
Type = ImageType.Backdrop
}, 0);
},
0);
}
}
@ -786,7 +797,6 @@ namespace Emby.Server.Implementations.LiveTv
if (query.OrderBy.Count == 0)
{
// Unless something else was specified, order by start date to take advantage of a specialized index
query.OrderBy = new[]
{
@ -824,7 +834,7 @@ namespace Emby.Server.Implementations.LiveTv
if (!string.IsNullOrWhiteSpace(query.SeriesTimerId))
{
var seriesTimers = await GetSeriesTimersInternal(new SeriesTimerQuery { }, cancellationToken).ConfigureAwait(false);
var seriesTimers = await GetSeriesTimersInternal(new SeriesTimerQuery(), cancellationToken).ConfigureAwait(false);
var seriesTimer = seriesTimers.Items.FirstOrDefault(i => string.Equals(_tvDtoService.GetInternalSeriesTimerId(i.Id).ToString("N", CultureInfo.InvariantCulture), query.SeriesTimerId, StringComparison.OrdinalIgnoreCase));
if (seriesTimer != null)
{
@ -847,13 +857,11 @@ namespace Emby.Server.Implementations.LiveTv
var returnArray = _dtoService.GetBaseItemDtos(queryResult.Items, options, user);
var result = new QueryResult<BaseItemDto>
return new QueryResult<BaseItemDto>
{
Items = returnArray,
TotalRecordCount = queryResult.TotalRecordCount
};
return result;
}
public QueryResult<BaseItem> GetRecommendedProgramsInternal(InternalItemsQuery query, DtoOptions options, CancellationToken cancellationToken)
@ -1121,7 +1129,7 @@ namespace Emby.Server.Implementations.LiveTv
try
{
var item = GetChannel(channelInfo.Item2, channelInfo.Item1, parentFolder, cancellationToken);
var item = await GetChannelAsync(channelInfo.Item2, channelInfo.Item1, parentFolder, cancellationToken).ConfigureAwait(false);
list.Add(item);
}
@ -1138,7 +1146,7 @@ namespace Emby.Server.Implementations.LiveTv
double percent = numComplete;
percent /= allChannelsList.Count;
progress.Report(5 * percent + 10);
progress.Report((5 * percent) + 10);
}
progress.Report(15);
@ -1173,7 +1181,6 @@ namespace Emby.Server.Implementations.LiveTv
var existingPrograms = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
ChannelIds = new Guid[] { currentChannel.Id },
DtoOptions = new DtoOptions(true)
@ -1214,7 +1221,11 @@ namespace Emby.Server.Implementations.LiveTv
if (updatedPrograms.Count > 0)
{
_libraryManager.UpdateItems(updatedPrograms, currentChannel, ItemUpdateType.MetadataImport, cancellationToken);
await _libraryManager.UpdateItemsAsync(
updatedPrograms,
currentChannel,
ItemUpdateType.MetadataImport,
cancellationToken).ConfigureAwait(false);
}
currentChannel.IsMovie = isMovie;
@ -1227,7 +1238,7 @@ namespace Emby.Server.Implementations.LiveTv
currentChannel.AddTag("Kids");
}
currentChannel.UpdateToRepository(ItemUpdateType.MetadataImport, cancellationToken);
await currentChannel.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
await currentChannel.RefreshMetadata(
new MetadataRefreshOptions(new DirectoryService(_fileSystem))
{
@ -1298,8 +1309,6 @@ namespace Emby.Server.Implementations.LiveTv
}
}
private const int MaxGuideDays = 14;
private double GetGuideDays()
{
var config = GetConfiguration();
@ -1712,7 +1721,7 @@ namespace Emby.Server.Implementations.LiveTv
if (timer == null)
{
throw new ResourceNotFoundException(string.Format("Timer with Id {0} not found", id));
throw new ResourceNotFoundException(string.Format(CultureInfo.InvariantCulture, "Timer with Id {0} not found", id));
}
var service = GetService(timer.ServiceName);
@ -1731,7 +1740,7 @@ namespace Emby.Server.Implementations.LiveTv
if (timer == null)
{
throw new ResourceNotFoundException(string.Format("SeriesTimer with Id {0} not found", id));
throw new ResourceNotFoundException(string.Format(CultureInfo.InvariantCulture, "SeriesTimer with Id {0} not found", id));
}
var service = GetService(timer.ServiceName);
@ -1743,10 +1752,12 @@ namespace Emby.Server.Implementations.LiveTv
public async Task<TimerInfoDto> GetTimer(string id, CancellationToken cancellationToken)
{
var results = await GetTimers(new TimerQuery
var results = await GetTimers(
new TimerQuery
{
Id = id
}, cancellationToken).ConfigureAwait(false);
},
cancellationToken).ConfigureAwait(false);
return results.Items.FirstOrDefault(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase));
}
@ -1794,10 +1805,7 @@ namespace Emby.Server.Implementations.LiveTv
}
var returnArray = timers
.Select(i =>
{
return i.Item1;
})
.Select(i => i.Item1)
.ToArray();
return new QueryResult<SeriesTimerInfo>
@ -1968,7 +1976,7 @@ namespace Emby.Server.Implementations.LiveTv
if (service == null)
{
service = _services.First();
service = _services[0];
}
var info = await service.GetNewTimerDefaultsAsync(cancellationToken, programInfo).ConfigureAwait(false);
@ -1994,9 +2002,7 @@ namespace Emby.Server.Implementations.LiveTv
{
var info = await GetNewTimerDefaultsInternal(cancellationToken).ConfigureAwait(false);
var obj = _tvDtoService.GetSeriesTimerInfoDto(info.Item1, info.Item2, null);
return obj;
return _tvDtoService.GetSeriesTimerInfoDto(info.Item1, info.Item2, null);
}
public async Task<SeriesTimerInfoDto> GetNewTimerDefaults(string programId, CancellationToken cancellationToken)
@ -2125,6 +2131,7 @@ namespace Emby.Server.Implementations.LiveTv
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private bool _disposed = false;
@ -2447,8 +2454,7 @@ namespace Emby.Server.Implementations.LiveTv
.SelectMany(i => i.Locations)
.Distinct(StringComparer.OrdinalIgnoreCase)
.Select(i => _libraryManager.FindByPath(i, true))
.Where(i => i != null)
.Where(i => i.IsVisibleStandalone(user))
.Where(i => i != null && i.IsVisibleStandalone(user))
.SelectMany(i => _libraryManager.GetCollectionFolders(i))
.GroupBy(x => x.Id)
.Select(x => x.First())

View File

@ -19,8 +19,7 @@ namespace Emby.Server.Implementations.LiveTv
public class LiveTvMediaSourceProvider : IMediaSourceProvider
{
// Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
private const char StreamIdDelimeter = '_';
private const string StreamIdDelimeterString = "_";
private const char StreamIdDelimiter = '_';
private readonly ILiveTvManager _liveTvManager;
private readonly ILogger<LiveTvMediaSourceProvider> _logger;
@ -47,7 +46,7 @@ namespace Emby.Server.Implementations.LiveTv
}
}
return Task.FromResult<IEnumerable<MediaSourceInfo>>(Array.Empty<MediaSourceInfo>());
return Task.FromResult(Enumerable.Empty<MediaSourceInfo>());
}
private async Task<IEnumerable<MediaSourceInfo>> GetMediaSourcesInternal(BaseItem item, ActiveRecordingInfo activeRecordingInfo, CancellationToken cancellationToken)
@ -98,7 +97,7 @@ namespace Emby.Server.Implementations.LiveTv
source.Id ?? string.Empty
};
source.OpenToken = string.Join(StreamIdDelimeterString, openKeys);
source.OpenToken = string.Join(StreamIdDelimiter, openKeys);
}
// Dummy this up so that direct play checks can still run
@ -116,7 +115,7 @@ namespace Emby.Server.Implementations.LiveTv
/// <inheritdoc />
public async Task<ILiveStream> OpenMediaSource(string openToken, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
{
var keys = openToken.Split(new[] { StreamIdDelimeter }, 3);
var keys = openToken.Split(StreamIdDelimiter, 3);
var mediaSourceId = keys.Length >= 3 ? keys[2] : null;
var info = await _liveTvManager.GetChannelStream(keys[1], mediaSourceId, currentLiveStreams, cancellationToken).ConfigureAwait(false);

View File

@ -1,10 +1,10 @@
#pragma warning disable CS1591
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
@ -14,7 +14,7 @@ using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.TunerHosts
@ -23,17 +23,15 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
protected readonly IServerConfigurationManager Config;
protected readonly ILogger<BaseTunerHost> Logger;
protected IJsonSerializer JsonSerializer;
protected readonly IFileSystem FileSystem;
private readonly ConcurrentDictionary<string, ChannelCache> _channelCache =
new ConcurrentDictionary<string, ChannelCache>(StringComparer.OrdinalIgnoreCase);
private readonly IMemoryCache _memoryCache;
protected BaseTunerHost(IServerConfigurationManager config, ILogger<BaseTunerHost> logger, IJsonSerializer jsonSerializer, IFileSystem fileSystem)
protected BaseTunerHost(IServerConfigurationManager config, ILogger<BaseTunerHost> logger, IFileSystem fileSystem, IMemoryCache memoryCache)
{
Config = config;
Logger = logger;
JsonSerializer = jsonSerializer;
_memoryCache = memoryCache;
FileSystem = fileSystem;
}
@ -44,23 +42,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
public async Task<List<ChannelInfo>> GetChannels(TunerHostInfo tuner, bool enableCache, CancellationToken cancellationToken)
{
ChannelCache cache = null;
var key = tuner.Id;
if (enableCache && !string.IsNullOrEmpty(key) && _channelCache.TryGetValue(key, out cache))
if (enableCache && !string.IsNullOrEmpty(key) && _memoryCache.TryGetValue(key, out List<ChannelInfo> cache))
{
return cache.Channels.ToList();
return cache;
}
var result = await GetChannelsInternal(tuner, cancellationToken).ConfigureAwait(false);
var list = result.ToList();
var list = await GetChannelsInternal(tuner, cancellationToken).ConfigureAwait(false);
// logger.LogInformation("Channels from {0}: {1}", tuner.Url, JsonSerializer.SerializeToString(list));
if (!string.IsNullOrEmpty(key) && list.Count > 0)
{
cache = cache ?? new ChannelCache();
cache.Channels = list;
_channelCache.AddOrUpdate(key, cache, (k, v) => cache);
_memoryCache.Set(key, list);
}
return list;
@ -95,7 +89,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
try
{
Directory.CreateDirectory(Path.GetDirectoryName(channelCacheFile));
JsonSerializer.SerializeToFile(channels, channelCacheFile);
await using var writeStream = File.OpenWrite(channelCacheFile);
await JsonSerializer.SerializeAsync(writeStream, channels, cancellationToken: cancellationToken).ConfigureAwait(false);
}
catch (IOException)
{
@ -110,7 +105,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
try
{
var channels = JsonSerializer.DeserializeFromFile<List<ChannelInfo>>(channelCacheFile);
await using var readStream = File.OpenRead(channelCacheFile);
var channels = await JsonSerializer.DeserializeAsync<List<ChannelInfo>>(readStream, cancellationToken: cancellationToken)
.ConfigureAwait(false);
list.AddRange(channels);
}
catch (IOException)
@ -233,10 +230,5 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
return Config.GetConfiguration<LiveTvOptions>("livetv");
}
private class ChannelCache
{
public List<ChannelInfo> Channels;
}
}
}

View File

@ -7,6 +7,7 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
@ -23,7 +24,7 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
@ -36,17 +37,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
private readonly INetworkManager _networkManager;
private readonly IStreamHelper _streamHelper;
private readonly Dictionary<string, DiscoverResponse> _modelCache = new Dictionary<string, DiscoverResponse>();
public HdHomerunHost(
IServerConfigurationManager config,
ILogger<HdHomerunHost> logger,
IJsonSerializer jsonSerializer,
IFileSystem fileSystem,
IHttpClient httpClient,
IServerApplicationHost appHost,
ISocketFactory socketFactory,
INetworkManager networkManager,
IStreamHelper streamHelper)
: base(config, logger, jsonSerializer, fileSystem)
IStreamHelper streamHelper,
IMemoryCache memoryCache)
: base(config, logger, fileSystem, memoryCache)
{
_httpClient = httpClient;
_appHost = appHost;
@ -75,10 +78,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
BufferContent = false
};
using (var response = await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false))
using (var stream = response.Content)
{
var lineup = await JsonSerializer.DeserializeFromStreamAsync<List<Channels>>(stream).ConfigureAwait(false) ?? new List<Channels>();
using var response = await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false);
await using var stream = response.Content;
var lineup = await JsonSerializer.DeserializeAsync<List<Channels>>(stream, cancellationToken: cancellationToken)
.ConfigureAwait(false) ?? new List<Channels>();
if (info.ImportFavoritesOnly)
{
@ -87,7 +90,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return lineup.Where(i => !i.DRM).ToList();
}
}
private class HdHomerunChannelInfo : ChannelInfo
{
@ -114,7 +116,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}).Cast<ChannelInfo>().ToList();
}
private readonly Dictionary<string, DiscoverResponse> _modelCache = new Dictionary<string, DiscoverResponse>();
private async Task<DiscoverResponse> GetModelInfo(TunerHostInfo info, bool throwAllExceptions, CancellationToken cancellationToken)
{
var cacheKey = info.Id;
@ -132,15 +133,16 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
try
{
using (var response = await _httpClient.SendAsync(new HttpRequestOptions()
using var response = await _httpClient.SendAsync(
new HttpRequestOptions
{
Url = string.Format("{0}/discover.json", GetApiUrl(info)),
Url = string.Format(CultureInfo.InvariantCulture, "{0}/discover.json", GetApiUrl(info)),
CancellationToken = cancellationToken,
BufferContent = false
}, HttpMethod.Get).ConfigureAwait(false))
using (var stream = response.Content)
{
var discoverResponse = await JsonSerializer.DeserializeFromStreamAsync<DiscoverResponse>(stream).ConfigureAwait(false);
}, HttpMethod.Get).ConfigureAwait(false);
await using var stream = response.Content;
var discoverResponse = await JsonSerializer.DeserializeAsync<DiscoverResponse>(stream, cancellationToken: cancellationToken)
.ConfigureAwait(false);
if (!string.IsNullOrEmpty(cacheKey))
{
@ -152,15 +154,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return discoverResponse;
}
}
catch (HttpException ex)
{
if (!throwAllExceptions && ex.StatusCode.HasValue && ex.StatusCode.Value == System.Net.HttpStatusCode.NotFound)
if (!throwAllExceptions && ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound)
{
var defaultValue = "HDHR";
const string DefaultValue = "HDHR";
var response = new DiscoverResponse
{
ModelNumber = defaultValue
ModelNumber = DefaultValue
};
if (!string.IsNullOrEmpty(cacheKey))
{
@ -182,12 +183,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
using (var response = await _httpClient.SendAsync(new HttpRequestOptions()
using (var response = await _httpClient.SendAsync(
new HttpRequestOptions()
{
Url = string.Format("{0}/tuners.html", GetApiUrl(info)),
Url = string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)),
CancellationToken = cancellationToken,
BufferContent = false
}, HttpMethod.Get).ConfigureAwait(false))
},
HttpMethod.Get).ConfigureAwait(false))
using (var stream = response.Content)
using (var sr = new StreamReader(stream, System.Text.Encoding.UTF8))
{
@ -730,7 +733,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
// Need a way to set the Receive timeout on the socket otherwise this might never timeout?
try
{
await udpClient.SendToAsync(discBytes, 0, discBytes.Length, new IPEndPoint(IPAddress.Parse("255.255.255.255"), 65001), cancellationToken);
await udpClient.SendToAsync(discBytes, 0, discBytes.Length, new IPEndPoint(IPAddress.Parse("255.255.255.255"), 65001), cancellationToken).ConfigureAwait(false);
var receiveBuffer = new byte[8192];
while (!cancellationToken.IsCancellationRequested)

View File

@ -18,7 +18,7 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
@ -36,13 +36,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
IServerConfigurationManager config,
IMediaSourceManager mediaSourceManager,
ILogger<M3UTunerHost> logger,
IJsonSerializer jsonSerializer,
IFileSystem fileSystem,
IHttpClient httpClient,
IServerApplicationHost appHost,
INetworkManager networkManager,
IStreamHelper streamHelper)
: base(config, logger, jsonSerializer, fileSystem)
IStreamHelper streamHelper,
IMemoryCache memoryCache)
: base(config, logger, fileSystem, memoryCache)
{
_httpClient = httpClient;
_appHost = appHost;

View File

@ -1,12 +1,12 @@
{
"DeviceOnlineWithName": "{0}-এর সাথে সংযুক্ত হয়েছে",
"DeviceOfflineWithName": "{0}-এর সাথে সংযোগ বিচ্ছিন্ন হয়েছে",
"Collections": "সংকলন",
"Collections": "কলেক্শন",
"ChapterNameValue": "অধ্যায় {0}",
"Channels": "চ্যানেল",
"CameraImageUploadedFrom": "একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে {0} থেকে",
"CameraImageUploadedFrom": "{0} থেকে একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে",
"Books": "বই",
"AuthenticationSucceededWithUserName": "{0} যাচাই সফল",
"AuthenticationSucceededWithUserName": "{0} অনুমোদন সফল",
"Artists": "শিল্পীরা",
"Application": "অ্যাপ্লিকেশন",
"Albums": "অ্যালবামগুলো",
@ -14,13 +14,13 @@
"HeaderFavoriteArtists": "প্রিয় শিল্পীরা",
"HeaderFavoriteAlbums": "প্রিয় এলবামগুলো",
"HeaderContinueWatching": "দেখতে থাকুন",
"HeaderCameraUploads": "ক্যামেরার আপলোডগুলো",
"HeaderAlbumArtists": "এলবামের শিল্পী",
"Genres": "ঘরানা",
"HeaderCameraUploads": "ক্যামেরার আপলোড সমূহ",
"HeaderAlbumArtists": "এলবাম শিল্পী",
"Genres": "জেনার",
"Folders": "ফোল্ডারগুলো",
"Favorites": "ফেভারিটগুলো",
"Favorites": "পছন্দসমূহ",
"FailedLoginAttemptWithUserName": "{0} লগিন করতে ব্যর্থ হয়েছে",
"AppDeviceValues": "প: {0}, ডিভাইস: {0}",
"AppDeviceValues": "অ্যাপ: {0}, ডিভাইস: {0}",
"VersionNumber": "সংস্করণ {0}",
"ValueSpecialEpisodeName": "বিশেষ - {0}",
"ValueHasBeenAddedToLibrary": "আপনার লাইব্রেরিতে {0} যোগ করা হয়েছে",
@ -74,20 +74,20 @@
"NameInstallFailed": "{0} ইন্সটল ব্যর্থ",
"MusicVideos": "গানের ভিডিও",
"Music": "গান",
"Movies": "সিনেমা",
"Movies": "চলচ্চিত্র",
"MixedContent": "মিশ্র কন্টেন্ট",
"MessageServerConfigurationUpdated": "সার্ভারের কনফিগারেশন হালনাগাদ করা হয়েছে",
"HeaderRecordingGroups": "রেকর্ডিং গ্রুপ",
"MessageNamedServerConfigurationUpdatedWithValue": "সার্ভারের {0} কনফিগারেসন অংশ আপডেট করা হয়েছে",
"MessageApplicationUpdatedTo": "জেলিফিন সার্ভার {0} তে হালনাগাদ করা হয়েছে",
"MessageApplicationUpdated": "জেলিফিন সার্ভার হালনাগাদ করা হয়েছে",
"Latest": "একদম নতুন",
"MessageServerConfigurationUpdated": "সার্ভারের কনফিগারেশন আপডেট করা হয়েছে",
"HeaderRecordingGroups": "রেকর্ডিং দল",
"MessageNamedServerConfigurationUpdatedWithValue": "সার্ভারের {0} কনফিগারেসনের অংশ আপডেট করা হয়েছে",
"MessageApplicationUpdatedTo": "জেলিফিন সার্ভার {0} তে আপডেট করা হয়েছে",
"MessageApplicationUpdated": "জেলিফিন সার্ভার আপডেট করা হয়েছে",
"Latest": "সর্বশেষ",
"LabelRunningTimeValue": "চলার সময়: {0}",
"LabelIpAddressValue": "আইপি ঠিকানা: {0}",
"LabelIpAddressValue": "আইপি এড্রেস: {0}",
"ItemRemovedWithName": "{0} লাইব্রেরি থেকে বাদ দেয়া হয়েছে",
"ItemAddedWithName": "{0} লাইব্রেরিতে যোগ করা হয়েছে",
"Inherit": "থেকে পাওয়া",
"HomeVideos": "বাসার ভিডিও",
"HomeVideos": "হোম ভিডিও",
"HeaderNextUp": "এরপরে আসছে",
"HeaderLiveTV": "লাইভ টিভি",
"HeaderFavoriteSongs": "প্রিয় গানগুলো",

View File

@ -7,8 +7,8 @@
"MessageApplicationUpdated": "Jellyfin Server sudah diperbarui",
"Latest": "Terbaru",
"LabelIpAddressValue": "Alamat IP: {0}",
"ItemRemovedWithName": "{0} sudah dikeluarkan dari perpustakaan",
"ItemAddedWithName": "{0} sudah dimasukkan ke dalam perpustakaan",
"ItemRemovedWithName": "{0} sudah dikeluarkan dari pustaka",
"ItemAddedWithName": "{0} telah dimasukkan ke dalam pustaka",
"Inherit": "Warisan",
"HomeVideos": "Video Rumah",
"HeaderRecordingGroups": "Grup Rekaman",
@ -19,8 +19,8 @@
"HeaderFavoriteEpisodes": "Episode Favorit",
"HeaderFavoriteArtists": "Artis Favorit",
"HeaderFavoriteAlbums": "Album Favorit",
"HeaderContinueWatching": "Masih Melihat",
"HeaderCameraUploads": "Uplod Kamera",
"HeaderContinueWatching": "Lanjutkan Menonton",
"HeaderCameraUploads": "Unggahan Kamera",
"HeaderAlbumArtists": "Album Artis",
"Genres": "Genre",
"Folders": "Folder",
@ -32,11 +32,11 @@
"ChapterNameValue": "Bagian {0}",
"Channels": "Saluran",
"TvShows": "Seri TV",
"SubtitleDownloadFailureFromForItem": "Talop gagal diunduh dari {0} untuk {1}",
"StartupEmbyServerIsLoading": "Peladen Jellyfin sedang dimuat. Silakan coba kembali beberapa saat lagi.",
"SubtitleDownloadFailureFromForItem": "Subtitel gagal diunduh dari {0} untuk {1}",
"StartupEmbyServerIsLoading": "Server Jellyfin sedang dimuat. Silakan coba lagi nanti.",
"Songs": "Lagu",
"Playlists": "Daftar putar",
"NotificationOptionPluginUninstalled": "Plugin dilepas",
"NotificationOptionPluginUninstalled": "Plugin dihapus",
"MusicVideos": "Video musik",
"VersionNumber": "Versi {0}",
"ValueSpecialEpisodeName": "Spesial - {0}",
@ -65,7 +65,7 @@
"Photos": "Foto",
"NotificationOptionUserLockedOut": "Pengguna terkunci",
"NotificationOptionTaskFailed": "Kegagalan tugas terjadwal",
"NotificationOptionServerRestartRequired": "Restart peladen dibutuhkan",
"NotificationOptionServerRestartRequired": "Muat ulang server dibutuhkan",
"NotificationOptionPluginUpdateInstalled": "Pembaruan plugin terpasang",
"NotificationOptionPluginInstalled": "Plugin terpasang",
"NotificationOptionPluginError": "Kegagalan plugin",
@ -74,14 +74,14 @@
"NotificationOptionCameraImageUploaded": "Gambar kamera terunggah",
"NotificationOptionApplicationUpdateInstalled": "Pembaruan aplikasi terpasang",
"NotificationOptionApplicationUpdateAvailable": "Pembaruan aplikasi tersedia",
"NewVersionIsAvailable": "Sebuah versi baru dari Peladen Jellyfin tersedia untuk diunduh.",
"NewVersionIsAvailable": "Versi baru dari Jellyfin Server tersedia untuk diunduh.",
"NameSeasonUnknown": "Musim tak diketahui",
"NameSeasonNumber": "Musim {0}",
"NameInstallFailed": "{0} instalasi gagal",
"NameInstallFailed": "{0} penginstalan gagal",
"Music": "Musik",
"Movies": "Film",
"MessageServerConfigurationUpdated": "Konfigurasi peladen telah diperbarui",
"MessageNamedServerConfigurationUpdatedWithValue": "Konfigurasi peladen bagian {0} telah diperbarui",
"MessageServerConfigurationUpdated": "Konfigurasi server telah diperbarui",
"MessageNamedServerConfigurationUpdatedWithValue": "Bagian konfigurasi server {0} telah diperbarui",
"FailedLoginAttemptWithUserName": "Percobaan login gagal dari {0}",
"CameraImageUploadedFrom": "Sebuah gambar baru telah diunggah dari {0}",
"DeviceOfflineWithName": "{0} telah terputus",
@ -90,6 +90,28 @@
"NotificationOptionVideoPlayback": "Pemutaran video dimulai",
"NotificationOptionAudioPlaybackStopped": "Pemutaran audio berhenti",
"NotificationOptionAudioPlayback": "Pemutaran audio dimulai",
"MixedContent": "Konten campur",
"PluginUninstalledWithName": "{0} telah dihapus"
"MixedContent": "Konten campuran",
"PluginUninstalledWithName": "{0} telah dihapus",
"TaskRefreshChapterImagesDescription": "Membuat gambar mini untuk video yang memiliki bagian.",
"TaskRefreshChapterImages": "Ekstrak Gambar Bagian",
"TaskCleanCacheDescription": "Menghapus file cache yang tidak lagi dibutuhkan oleh sistem.",
"TaskCleanCache": "Bersihkan Cache Direktori",
"TasksLibraryCategory": "Pustaka",
"TasksMaintenanceCategory": "Perbaikan",
"TasksApplicationCategory": "Aplikasi",
"TaskRefreshPeopleDescription": "Memperbarui metadata untuk aktor dan sutradara di pustaka media Anda.",
"TaskRefreshLibraryDescription": "Memindai Pustaka media Anda untuk mencari file baru dan memperbarui metadata.",
"TasksChannelsCategory": "Saluran Online",
"TaskDownloadMissingSubtitlesDescription": "Mencari di internet untuk subtitle yang hilang berdasarkan konfigurasi metadata.",
"TaskDownloadMissingSubtitles": "Unduh subtitle yang hilang",
"TaskRefreshChannelsDescription": "Segarkan informasi saluran internet.",
"TaskRefreshChannels": "Segarkan Saluran",
"TaskCleanTranscodeDescription": "Menghapus file transcode yang berumur lebih dari satu hari.",
"TaskCleanTranscode": "Bersihkan Direktori Transcode",
"TaskUpdatePluginsDescription": "Unduh dan instal pembaruan untuk plugin yang dikonfigurasi untuk memperbarui secara otomatis.",
"TaskUpdatePlugins": "Perbarui Plugin",
"TaskRefreshPeople": "Muat ulang Orang",
"TaskCleanLogsDescription": "Menghapus file log yang lebih dari {0} hari.",
"TaskCleanLogs": "Bersihkan Log Direktori",
"TaskRefreshLibrary": "Pindai Pustaka Media"
}

View File

@ -102,11 +102,11 @@
"TaskUpdatePluginsDescription": "Scarica e installa gli aggiornamenti per i plugin che sono stati configurati per essere aggiornati contemporaneamente.",
"TaskUpdatePlugins": "Aggiorna i Plugin",
"TaskRefreshPeopleDescription": "Aggiorna i metadati per gli attori e registi nella tua libreria multimediale.",
"TaskRefreshPeople": "Aggiorna persone",
"TaskRefreshPeople": "Aggiornamento Persone",
"TaskCleanLogsDescription": "Rimuovi i file di log più vecchi di {0} giorni.",
"TaskCleanLogs": "Pulisci la cartella dei log",
"TaskRefreshLibraryDescription": "Analizza la tua libreria multimediale per nuovi file e rinnova i metadati.",
"TaskRefreshLibrary": "Analizza la libreria dei contenuti multimediali",
"TaskRefreshLibrary": "Scan Librerie",
"TaskRefreshChapterImagesDescription": "Crea le thumbnail per i video che hanno capitoli.",
"TaskRefreshChapterImages": "Estrai immagini capitolo",
"TaskCleanCacheDescription": "Cancella i file di cache non più necessari al sistema.",

View File

@ -21,7 +21,7 @@
"HeaderFavoriteAlbums": "Избранные альбомы",
"HeaderFavoriteArtists": "Избранные исполнители",
"HeaderFavoriteEpisodes": "Избранные эпизоды",
"HeaderFavoriteShows": "Избранные передачи",
"HeaderFavoriteShows": "Избранные сериалы",
"HeaderFavoriteSongs": "Избранные композиции",
"HeaderLiveTV": "Эфир",
"HeaderNextUp": "Очередное",

View File

@ -45,7 +45,7 @@
"TvShows": "தொலைக்காட்சித் தொடர்கள்",
"Sync": "ஒத்திசைவு",
"StartupEmbyServerIsLoading": "ஜெல்லிஃபின் சேவையகம் துவங்குகிறது. சிறிது நேரம் கழித்து முயற்சிக்கவும்.",
"Songs": "பாடடுகள்",
"Songs": "பாட்கள்",
"Shows": "தொடர்கள்",
"ServerNameNeedsToBeRestarted": "{0} மறுதொடக்கம் செய்யப்பட வேண்டும்",
"ScheduledTaskStartedWithName": "{0} துவங்கியது",
@ -93,7 +93,25 @@
"Channels": "சேனல்கள்",
"Books": "புத்தகங்கள்",
"AuthenticationSucceededWithUserName": "{0} வெற்றிகரமாக அங்கீகரிக்கப்பட்டது",
"Artists": "கலைஞர்கள்",
"Artists": "கலைஞர்",
"Application": "செயலி",
"Albums": "ஆல்பங்கள்"
"Albums": "ஆல்பங்கள்",
"NewVersionIsAvailable": "ஜெல்லிஃபின் சேவையகத்தின் புதிய பதிப்பு பதிவிறக்கத்திற்கு கிடைக்கிறது.",
"MessageNamedServerConfigurationUpdatedWithValue": "சேவையக உள்ளமைவு பிரிவு {0 புதுப்பிக்கப்பட்டது",
"TaskCleanCacheDescription": "கணினிக்கு இனி தேவைப்படாத தற்காலிக கோப்புகளை நீக்கு.",
"UserOfflineFromDevice": "{0} இலிருந்து {1} துண்டிக்கப்பட்டுள்ளது",
"SubtitleDownloadFailureFromForItem": "வசன வரிகள் {0 } இலிருந்து {1} க்கு பதிவிறக்கத் தவறிவிட்டன",
"TaskDownloadMissingSubtitlesDescription": "மெட்டாடேட்டா உள்ளமைவின் அடிப்படையில் வசன வரிகள் காணாமல் போனதற்கு இணையத்தைத் தேடுகிறது.",
"TaskCleanTranscodeDescription": "டிரான்ஸ்கோட் கோப்புகளை ஒரு நாளுக்கு மேல் பழையதாக நீக்குகிறது.",
"TaskUpdatePluginsDescription": "தானாகவே புதுப்பிக்க கட்டமைக்கப்பட்ட செருகுநிரல்களுக்கான புதுப்பிப்புகளை பதிவிறக்குகிறது மற்றும் நிறுவுகிறது.",
"TaskRefreshPeopleDescription": "உங்கள் மீடியா நூலகத்தில் உள்ள நடிகர்கள் மற்றும் இயக்குனர்களுக்கான மெட்டாடேட்டாவை புதுப்பிக்கும்.",
"TaskCleanLogsDescription": "{0} நாட்களுக்கு மேல் இருக்கும் பதிவு கோப்புகளை நீக்கும்.",
"TaskCleanLogs": "பதிவு அடைவு சுத்தம் செய்யுங்கள்",
"TaskRefreshLibraryDescription": "புதிய கோப்புகளுக்காக உங்கள் மீடியா நூலகத்தை ஸ்கேன் செய்து மீத்தரவை புதுப்பிக்கும்.",
"TaskRefreshChapterImagesDescription": "அத்தியாயங்களைக் கொண்ட வீடியோக்களுக்கான சிறு உருவங்களை உருவாக்குகிறது.",
"ValueHasBeenAddedToLibrary": "உங்கள் மீடியா நூலகத்தில் {0} சேர்க்கப்பட்டது",
"UserOnlineFromDevice": "{1} இருந்து {0} ஆன்லைன்",
"HomeVideos": "முகப்பு வீடியோக்கள்",
"UserStoppedPlayingItemWithValues": "{2} இல் {1} முடித்துவிட்டது",
"UserStartedPlayingItemWithValues": "{0} {2}இல் {1} ஐ இயக்குகிறது"
}

View File

@ -152,10 +152,10 @@ namespace Emby.Server.Implementations.Playlists
if (options.ItemIdList.Length > 0)
{
AddToPlaylistInternal(playlist.Id.ToString("N", CultureInfo.InvariantCulture), options.ItemIdList, user, new DtoOptions(false)
await AddToPlaylistInternal(playlist.Id, options.ItemIdList, user, new DtoOptions(false)
{
EnableImages = true
});
}).ConfigureAwait(false);
}
return new PlaylistCreationResult(playlist.Id.ToString("N", CultureInfo.InvariantCulture));
@ -184,17 +184,17 @@ namespace Emby.Server.Implementations.Playlists
return Playlist.GetPlaylistItems(playlistMediaType, items, user, options);
}
public void AddToPlaylist(string playlistId, ICollection<Guid> itemIds, Guid userId)
public Task AddToPlaylistAsync(Guid playlistId, ICollection<Guid> itemIds, Guid userId)
{
var user = userId.Equals(Guid.Empty) ? null : _userManager.GetUserById(userId);
AddToPlaylistInternal(playlistId, itemIds, user, new DtoOptions(false)
return AddToPlaylistInternal(playlistId, itemIds, user, new DtoOptions(false)
{
EnableImages = true
});
}
private void AddToPlaylistInternal(string playlistId, ICollection<Guid> newItemIds, User user, DtoOptions options)
private async Task AddToPlaylistInternal(Guid playlistId, ICollection<Guid> newItemIds, User user, DtoOptions options)
{
// Retrieve the existing playlist
var playlist = _libraryManager.GetItemById(playlistId) as Playlist
@ -238,7 +238,7 @@ namespace Emby.Server.Implementations.Playlists
// Update the playlist in the repository
playlist.LinkedChildren = newLinkedChildren;
playlist.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
// Update the playlist on disk
if (playlist.IsFile)
@ -256,7 +256,7 @@ namespace Emby.Server.Implementations.Playlists
RefreshPriority.High);
}
public void RemoveFromPlaylist(string playlistId, IEnumerable<string> entryIds)
public async Task RemoveFromPlaylistAsync(string playlistId, IEnumerable<string> entryIds)
{
if (!(_libraryManager.GetItemById(playlistId) is Playlist playlist))
{
@ -273,7 +273,7 @@ namespace Emby.Server.Implementations.Playlists
.Select(i => i.Item1)
.ToArray();
playlist.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
if (playlist.IsFile)
{
@ -289,7 +289,7 @@ namespace Emby.Server.Implementations.Playlists
RefreshPriority.High);
}
public void MoveItem(string playlistId, string entryId, int newIndex)
public async Task MoveItemAsync(string playlistId, string entryId, int newIndex)
{
if (!(_libraryManager.GetItemById(playlistId) is Playlist playlist))
{
@ -322,7 +322,7 @@ namespace Emby.Server.Implementations.Playlists
playlist.LinkedChildren = newList.ToArray();
playlist.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
if (playlist.IsFile)
{

View File

@ -10,7 +10,6 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Progress;
using MediaBrowser.Model.Events;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
@ -22,37 +21,53 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// </summary>
public class ScheduledTaskWorker : IScheduledTaskWorker
{
public event EventHandler<GenericEventArgs<double>> TaskProgress;
/// <summary>
/// Gets the scheduled task.
/// </summary>
/// <value>The scheduled task.</value>
public IScheduledTask ScheduledTask { get; private set; }
/// <summary>
/// Gets or sets the json serializer.
/// </summary>
/// <value>The json serializer.</value>
private IJsonSerializer JsonSerializer { get; set; }
private readonly IJsonSerializer _jsonSerializer;
/// <summary>
/// Gets or sets the application paths.
/// </summary>
/// <value>The application paths.</value>
private IApplicationPaths ApplicationPaths { get; set; }
private readonly IApplicationPaths _applicationPaths;
/// <summary>
/// Gets the logger.
/// Gets or sets the logger.
/// </summary>
/// <value>The logger.</value>
private ILogger Logger { get; set; }
private readonly ILogger _logger;
/// <summary>
/// Gets the task manager.
/// Gets or sets the task manager.
/// </summary>
/// <value>The task manager.</value>
private ITaskManager TaskManager { get; set; }
private readonly ITaskManager _taskManager;
/// <summary>
/// The _last execution result sync lock.
/// </summary>
private readonly object _lastExecutionResultSyncLock = new object();
private bool _readFromFile = false;
/// <summary>
/// The _last execution result.
/// </summary>
private TaskResult _lastExecutionResult;
private Task _currentTask;
/// <summary>
/// The _triggers.
/// </summary>
private Tuple<TaskTriggerInfo, ITaskTrigger>[] _triggers;
/// <summary>
/// The _id.
/// </summary>
private string _id;
/// <summary>
/// Initializes a new instance of the <see cref="ScheduledTaskWorker" /> class.
@ -71,7 +86,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// or
/// jsonSerializer
/// or
/// logger
/// logger.
/// </exception>
public ScheduledTaskWorker(IScheduledTask scheduledTask, IApplicationPaths applicationPaths, ITaskManager taskManager, IJsonSerializer jsonSerializer, ILogger logger)
{
@ -101,23 +116,22 @@ namespace Emby.Server.Implementations.ScheduledTasks
}
ScheduledTask = scheduledTask;
ApplicationPaths = applicationPaths;
TaskManager = taskManager;
JsonSerializer = jsonSerializer;
Logger = logger;
_applicationPaths = applicationPaths;
_taskManager = taskManager;
_jsonSerializer = jsonSerializer;
_logger = logger;
InitTriggerEvents();
}
private bool _readFromFile = false;
public event EventHandler<GenericEventArgs<double>> TaskProgress;
/// <summary>
/// The _last execution result.
/// Gets the scheduled task.
/// </summary>
private TaskResult _lastExecutionResult;
/// <summary>
/// The _last execution result sync lock.
/// </summary>
private readonly object _lastExecutionResultSyncLock = new object();
/// <value>The scheduled task.</value>
public IScheduledTask ScheduledTask { get; private set; }
/// <summary>
/// Gets the last execution result.
/// </summary>
@ -136,11 +150,11 @@ namespace Emby.Server.Implementations.ScheduledTasks
{
try
{
_lastExecutionResult = JsonSerializer.DeserializeFromFile<TaskResult>(path);
_lastExecutionResult = _jsonSerializer.DeserializeFromFile<TaskResult>(path);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error deserializing {File}", path);
_logger.LogError(ex, "Error deserializing {File}", path);
}
}
@ -160,7 +174,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
lock (_lastExecutionResultSyncLock)
{
JsonSerializer.SerializeToFile(value, path);
_jsonSerializer.SerializeToFile(value, path);
}
}
}
@ -184,7 +198,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
public string Category => ScheduledTask.Category;
/// <summary>
/// Gets the current cancellation token.
/// Gets or sets the current cancellation token.
/// </summary>
/// <value>The current cancellation token source.</value>
private CancellationTokenSource CurrentCancellationTokenSource { get; set; }
@ -221,12 +235,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
public double? CurrentProgress { get; private set; }
/// <summary>
/// The _triggers.
/// </summary>
private Tuple<TaskTriggerInfo, ITaskTrigger>[] _triggers;
/// <summary>
/// Gets the triggers that define when the task will run.
/// Gets or sets the triggers that define when the task will run.
/// </summary>
/// <value>The triggers.</value>
private Tuple<TaskTriggerInfo, ITaskTrigger>[] InternalTriggers
@ -255,7 +264,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// Gets the triggers that define when the task will run.
/// </summary>
/// <value>The triggers.</value>
/// <exception cref="ArgumentNullException">value</exception>
/// <exception cref="ArgumentNullException"><c>value</c> is <c>null</c>.</exception>
public TaskTriggerInfo[] Triggers
{
get
@ -280,11 +289,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
}
}
/// <summary>
/// The _id.
/// </summary>
private string _id;
/// <summary>
/// Gets the unique id.
/// </summary>
@ -325,9 +329,9 @@ namespace Emby.Server.Implementations.ScheduledTasks
trigger.Stop();
trigger.Triggered -= trigger_Triggered;
trigger.Triggered += trigger_Triggered;
trigger.Start(LastExecutionResult, Logger, Name, isApplicationStartup);
trigger.Triggered -= OnTriggerTriggered;
trigger.Triggered += OnTriggerTriggered;
trigger.Start(LastExecutionResult, _logger, Name, isApplicationStartup);
}
}
@ -336,7 +340,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
async void trigger_Triggered(object sender, EventArgs e)
private async void OnTriggerTriggered(object sender, EventArgs e)
{
var trigger = (ITaskTrigger)sender;
@ -347,19 +351,17 @@ namespace Emby.Server.Implementations.ScheduledTasks
return;
}
Logger.LogInformation("{0} fired for task: {1}", trigger.GetType().Name, Name);
_logger.LogInformation("{0} fired for task: {1}", trigger.GetType().Name, Name);
trigger.Stop();
TaskManager.QueueScheduledTask(ScheduledTask, trigger.TaskOptions);
_taskManager.QueueScheduledTask(ScheduledTask, trigger.TaskOptions);
await Task.Delay(1000).ConfigureAwait(false);
trigger.Start(LastExecutionResult, Logger, Name, false);
trigger.Start(LastExecutionResult, _logger, Name, false);
}
private Task _currentTask;
/// <summary>
/// Executes the task.
/// </summary>
@ -395,9 +397,9 @@ namespace Emby.Server.Implementations.ScheduledTasks
CurrentCancellationTokenSource = new CancellationTokenSource();
Logger.LogInformation("Executing {0}", Name);
_logger.LogInformation("Executing {0}", Name);
((TaskManager)TaskManager).OnTaskExecuting(this);
((TaskManager)_taskManager).OnTaskExecuting(this);
progress.ProgressChanged += OnProgressChanged;
@ -423,7 +425,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
}
catch (Exception ex)
{
Logger.LogError(ex, "Error");
_logger.LogError(ex, "Error");
failureException = ex;
@ -476,7 +478,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
{
if (State == TaskState.Running)
{
Logger.LogInformation("Attempting to cancel Scheduled Task {0}", Name);
_logger.LogInformation("Attempting to cancel Scheduled Task {0}", Name);
CurrentCancellationTokenSource.Cancel();
}
}
@ -487,7 +489,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// <returns>System.String.</returns>
private string GetScheduledTasksConfigurationDirectory()
{
return Path.Combine(ApplicationPaths.ConfigurationDirectoryPath, "ScheduledTasks");
return Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "ScheduledTasks");
}
/// <summary>
@ -496,7 +498,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// <returns>System.String.</returns>
private string GetScheduledTasksDataDirectory()
{
return Path.Combine(ApplicationPaths.DataPath, "ScheduledTasks");
return Path.Combine(_applicationPaths.DataPath, "ScheduledTasks");
}
/// <summary>
@ -535,7 +537,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
TaskTriggerInfo[] list = null;
if (File.Exists(path))
{
list = JsonSerializer.DeserializeFromFile<TaskTriggerInfo[]>(path);
list = _jsonSerializer.DeserializeFromFile<TaskTriggerInfo[]>(path);
}
// Return defaults if file doesn't exist.
@ -571,7 +573,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
Directory.CreateDirectory(Path.GetDirectoryName(path));
JsonSerializer.SerializeToFile(triggers, path);
_jsonSerializer.SerializeToFile(triggers, path);
}
/// <summary>
@ -585,7 +587,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
{
var elapsedTime = endTime - startTime;
Logger.LogInformation("{0} {1} after {2} minute(s) and {3} seconds", Name, status, Math.Truncate(elapsedTime.TotalMinutes), elapsedTime.Seconds);
_logger.LogInformation("{0} {1} after {2} minute(s) and {3} seconds", Name, status, Math.Truncate(elapsedTime.TotalMinutes), elapsedTime.Seconds);
var result = new TaskResult
{
@ -606,7 +608,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
LastExecutionResult = result;
((TaskManager)TaskManager).OnTaskCompleted(this, result);
((TaskManager)_taskManager).OnTaskCompleted(this, result);
}
/// <summary>
@ -615,6 +617,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
@ -635,12 +638,12 @@ namespace Emby.Server.Implementations.ScheduledTasks
{
try
{
Logger.LogInformation(Name + ": Cancelling");
_logger.LogInformation(Name + ": Cancelling");
token.Cancel();
}
catch (Exception ex)
{
Logger.LogError(ex, "Error calling CancellationToken.Cancel();");
_logger.LogError(ex, "Error calling CancellationToken.Cancel();");
}
}
@ -649,21 +652,21 @@ namespace Emby.Server.Implementations.ScheduledTasks
{
try
{
Logger.LogInformation(Name + ": Waiting on Task");
_logger.LogInformation(Name + ": Waiting on Task");
var exited = Task.WaitAll(new[] { task }, 2000);
if (exited)
{
Logger.LogInformation(Name + ": Task exited");
_logger.LogInformation(Name + ": Task exited");
}
else
{
Logger.LogInformation(Name + ": Timed out waiting for task to stop");
_logger.LogInformation(Name + ": Timed out waiting for task to stop");
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Error calling Task.WaitAll();");
_logger.LogError(ex, "Error calling Task.WaitAll();");
}
}
@ -671,12 +674,12 @@ namespace Emby.Server.Implementations.ScheduledTasks
{
try
{
Logger.LogDebug(Name + ": Disposing CancellationToken");
_logger.LogDebug(Name + ": Disposing CancellationToken");
token.Dispose();
}
catch (Exception ex)
{
Logger.LogError(ex, "Error calling CancellationToken.Dispose();");
_logger.LogError(ex, "Error calling CancellationToken.Dispose();");
}
}
@ -692,8 +695,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// </summary>
/// <param name="info">The info.</param>
/// <returns>BaseTaskTrigger.</returns>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="ArgumentException">Invalid trigger type: + info.Type</exception>
/// <exception cref="ArgumentException">Invalid trigger type: + info.Type.</exception>
private ITaskTrigger GetTrigger(TaskTriggerInfo info)
{
var options = new TaskOptions
@ -765,7 +767,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
foreach (var triggerInfo in InternalTriggers)
{
var trigger = triggerInfo.Item2;
trigger.Triggered -= trigger_Triggered;
trigger.Triggered -= OnTriggerTriggered;
trigger.Stop();
}
}

View File

@ -207,6 +207,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>

View File

@ -1,12 +1,13 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Tasks;
using MediaBrowser.Model.Globalization;
namespace Emby.Server.Implementations.ScheduledTasks.Tasks
{
@ -15,12 +16,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
/// </summary>
public class DeleteLogFileTask : IScheduledTask, IConfigurableScheduledTask
{
/// <summary>
/// Gets or sets the configuration manager.
/// </summary>
/// <value>The configuration manager.</value>
private IConfigurationManager ConfigurationManager { get; set; }
private readonly IConfigurationManager _configurationManager;
private readonly IFileSystem _fileSystem;
private readonly ILocalizationManager _localization;
@ -32,18 +28,43 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
/// <param name="localization">The localization manager.</param>
public DeleteLogFileTask(IConfigurationManager configurationManager, IFileSystem fileSystem, ILocalizationManager localization)
{
ConfigurationManager = configurationManager;
_configurationManager = configurationManager;
_fileSystem = fileSystem;
_localization = localization;
}
/// <inheritdoc />
public string Name => _localization.GetLocalizedString("TaskCleanLogs");
/// <inheritdoc />
public string Description => string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("TaskCleanLogsDescription"),
_configurationManager.CommonConfiguration.LogFileRetentionDays);
/// <inheritdoc />
public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
/// <inheritdoc />
public string Key => "CleanLogFiles";
/// <inheritdoc />
public bool IsHidden => false;
/// <inheritdoc />
public bool IsEnabled => true;
/// <inheritdoc />
public bool IsLogged => true;
/// <summary>
/// Creates the triggers that define when the task will run.
/// </summary>
/// <returns>IEnumerable{BaseTaskTrigger}.</returns>
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
{
return new[] {
return new[]
{
new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks}
};
}
@ -57,10 +78,10 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
public Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
{
// Delete log files more than n days old
var minDateModified = DateTime.UtcNow.AddDays(-ConfigurationManager.CommonConfiguration.LogFileRetentionDays);
var minDateModified = DateTime.UtcNow.AddDays(-_configurationManager.CommonConfiguration.LogFileRetentionDays);
// Only delete the .txt log files, the *.log files created by serilog get managed by itself
var filesToDelete = _fileSystem.GetFiles(ConfigurationManager.CommonApplicationPaths.LogDirectoryPath, new[] { ".txt" }, true, true)
var filesToDelete = _fileSystem.GetFiles(_configurationManager.CommonApplicationPaths.LogDirectoryPath, new[] { ".txt" }, true, true)
.Where(f => _fileSystem.GetLastWriteTimeUtc(f) < minDateModified)
.ToList();
@ -83,26 +104,5 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
return Task.CompletedTask;
}
/// <inheritdoc />
public string Name => _localization.GetLocalizedString("TaskCleanLogs");
/// <inheritdoc />
public string Description => string.Format(_localization.GetLocalizedString("TaskCleanLogsDescription"), ConfigurationManager.CommonConfiguration.LogFileRetentionDays);
/// <inheritdoc />
public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
/// <inheritdoc />
public string Key => "CleanLogFiles";
/// <inheritdoc />
public bool IsHidden => false;
/// <inheritdoc />
public bool IsEnabled => true;
/// <inheritdoc />
public bool IsLogged => true;
}
}

View File

@ -3,6 +3,7 @@ using System.Collections.Concurrent;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
namespace Emby.Server.Implementations.Serialization
@ -53,10 +54,11 @@ namespace Emby.Server.Implementations.Serialization
/// <param name="stream">The stream.</param>
public void SerializeToStream(object obj, Stream stream)
{
using (var writer = new XmlTextWriter(stream, null))
using (var writer = new StreamWriter(stream, null, IODefaults.StreamWriterBufferSize, true))
using (var textWriter = new XmlTextWriter(writer))
{
writer.Formatting = Formatting.Indented;
SerializeToWriter(obj, writer);
textWriter.Formatting = Formatting.Indented;
SerializeToWriter(obj, textWriter);
}
}
@ -95,7 +97,7 @@ namespace Emby.Server.Implementations.Serialization
/// <returns>System.Object.</returns>
public object DeserializeFromBytes(Type type, byte[] buffer)
{
using (var stream = new MemoryStream(buffer))
using (var stream = new MemoryStream(buffer, 0, buffer.Length, false, true))
{
return DeserializeFromStream(type, stream);
}

View File

@ -80,8 +80,8 @@ namespace Emby.Server.Implementations.Services
public static List<string> GetFirstMatchWildCardHashKeys(string[] pathPartsForMatching)
{
const string hashPrefix = WildCard + PathSeperator;
return GetPotentialMatchesWithPrefix(hashPrefix, pathPartsForMatching);
const string HashPrefix = WildCard + PathSeperator;
return GetPotentialMatchesWithPrefix(HashPrefix, pathPartsForMatching);
}
private static List<string> GetPotentialMatchesWithPrefix(string hashPrefix, string[] pathPartsForMatching)
@ -92,7 +92,7 @@ namespace Emby.Server.Implementations.Services
{
list.Add(hashPrefix + part);
if (part.IndexOf(ComponentSeperator) == -1)
if (part.IndexOf(ComponentSeperator, StringComparison.Ordinal) == -1)
{
continue;
}
@ -130,7 +130,7 @@ namespace Emby.Server.Implementations.Services
}
if (component.IndexOf(VariablePrefix, StringComparison.OrdinalIgnoreCase) != -1
&& component.IndexOf(ComponentSeperator) != -1)
&& component.IndexOf(ComponentSeperator, StringComparison.Ordinal) != -1)
{
hasSeparators.Add(true);
componentsList.AddRange(component.Split(ComponentSeperator));

View File

@ -1,93 +1,32 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
namespace Jellyfin.Api.Controllers
{
/// <summary>
/// The audio controller.
/// </summary>
// TODO: In order to autheneticate this in the future, Dlna playback will require updating
// TODO: In order to authenticate this in the future, Dlna playback will require updating
public class AudioController : BaseJellyfinApiController
{
private readonly IDlnaManager _dlnaManager;
private readonly IAuthorizationContext _authContext;
private readonly IUserManager _userManager;
private readonly ILibraryManager _libraryManager;
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly IFileSystem _fileSystem;
private readonly ISubtitleEncoder _subtitleEncoder;
private readonly IConfiguration _configuration;
private readonly IDeviceManager _deviceManager;
private readonly TranscodingJobHelper _transcodingJobHelper;
private readonly IHttpClientFactory _httpClientFactory;
private readonly AudioHelper _audioHelper;
private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive;
/// <summary>
/// Initializes a new instance of the <see cref="AudioController"/> class.
/// </summary>
/// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
/// <param name="userManger">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
/// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
/// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
/// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper"/> singleton.</param>
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
public AudioController(
IDlnaManager dlnaManager,
IUserManager userManger,
IAuthorizationContext authorizationContext,
ILibraryManager libraryManager,
IMediaSourceManager mediaSourceManager,
IServerConfigurationManager serverConfigurationManager,
IMediaEncoder mediaEncoder,
IFileSystem fileSystem,
ISubtitleEncoder subtitleEncoder,
IConfiguration configuration,
IDeviceManager deviceManager,
TranscodingJobHelper transcodingJobHelper,
IHttpClientFactory httpClientFactory)
/// <param name="audioHelper">Instance of <see cref="AudioHelper"/>.</param>
public AudioController(AudioHelper audioHelper)
{
_dlnaManager = dlnaManager;
_authContext = authorizationContext;
_userManager = userManger;
_libraryManager = libraryManager;
_mediaSourceManager = mediaSourceManager;
_serverConfigurationManager = serverConfigurationManager;
_mediaEncoder = mediaEncoder;
_fileSystem = fileSystem;
_subtitleEncoder = subtitleEncoder;
_configuration = configuration;
_deviceManager = deviceManager;
_transcodingJobHelper = transcodingJobHelper;
_httpClientFactory = httpClientFactory;
_audioHelper = audioHelper;
}
/// <summary>
@ -144,9 +83,9 @@ namespace Jellyfin.Api.Controllers
/// <param name="streamOptions">Optional. The streaming options.</param>
/// <response code="200">Audio stream returned.</response>
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
[HttpGet("{itemId}/{stream=stream}.{container?}", Name = "GetAudioStreamByContainer")]
[HttpGet("{itemId}/stream.{container}", Name = "GetAudioStreamByContainer")]
[HttpGet("{itemId}/stream", Name = "GetAudioStream")]
[HttpHead("{itemId}/{stream=stream}.{container?}", Name = "HeadAudioStreamByContainer")]
[HttpHead("{itemId}/stream.{container}", Name = "HeadAudioStreamByContainer")]
[HttpHead("{itemId}/stream", Name = "HeadAudioStream")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult> GetAudioStream(
@ -200,10 +139,6 @@ namespace Jellyfin.Api.Controllers
[FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string>? streamOptions)
{
bool isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
var cancellationTokenSource = new CancellationTokenSource();
StreamingRequestDto streamingRequest = new StreamingRequestDto
{
Id = itemId,
@ -257,97 +192,7 @@ namespace Jellyfin.Api.Controllers
StreamOptions = streamOptions
};
using var state = await StreamingHelpers.GetStreamingState(
streamingRequest,
Request,
_authContext,
_mediaSourceManager,
_userManager,
_libraryManager,
_serverConfigurationManager,
_mediaEncoder,
_fileSystem,
_subtitleEncoder,
_configuration,
_dlnaManager,
_deviceManager,
_transcodingJobHelper,
_transcodingJobType,
cancellationTokenSource.Token)
.ConfigureAwait(false);
if (@static.HasValue && @static.Value && state.DirectStreamProvider != null)
{
StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
await new ProgressiveFileCopier(state.DirectStreamProvider, null, _transcodingJobHelper, CancellationToken.None)
{
AllowEndOfFile = false
}.WriteToAsync(Response.Body, CancellationToken.None)
.ConfigureAwait(false);
// TODO (moved from MediaBrowser.Api): Don't hardcode contentType
return File(Response.Body, MimeTypes.GetMimeType("file.ts")!);
}
// Static remote stream
if (@static.HasValue && @static.Value && state.InputProtocol == MediaProtocol.Http)
{
StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
using var httpClient = _httpClientFactory.CreateClient();
return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this, httpClient).ConfigureAwait(false);
}
if (@static.HasValue && @static.Value && state.InputProtocol != MediaProtocol.File)
{
return BadRequest($"Input protocol {state.InputProtocol} cannot be streamed statically");
}
var outputPath = state.OutputFilePath;
var outputPathExists = System.IO.File.Exists(outputPath);
var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
var isTranscodeCached = outputPathExists && transcodingJob != null;
StreamingHelpers.AddDlnaHeaders(state, Response.Headers, (@static.HasValue && @static.Value) || isTranscodeCached, startTimeTicks, Request, _dlnaManager);
// Static stream
if (@static.HasValue && @static.Value)
{
var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath);
if (state.MediaSource.IsInfiniteStream)
{
await new ProgressiveFileCopier(state.MediaPath, null, _transcodingJobHelper, CancellationToken.None)
{
AllowEndOfFile = false
}.WriteToAsync(Response.Body, CancellationToken.None)
.ConfigureAwait(false);
return File(Response.Body, contentType);
}
return FileStreamResponseHelpers.GetStaticFileResult(
state.MediaPath,
contentType,
isHeadRequest,
this);
}
// Need to start ffmpeg (because media can't be returned directly)
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
var encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
var ffmpegCommandLineArguments = encodingHelper.GetProgressiveAudioFullCommandLine(state, encodingOptions, outputPath);
return await FileStreamResponseHelpers.GetTranscodedFile(
state,
isHeadRequest,
this,
_transcodingJobHelper,
ffmpegCommandLineArguments,
Request,
_transcodingJobType,
cancellationTokenSource).ConfigureAwait(false);
return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false);
}
}
}

View File

@ -1,5 +1,6 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
@ -51,7 +52,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="CollectionCreationOptions"/> with information about the new collection.</returns>
[HttpPost]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<CollectionCreationResult> CreateCollection(
public async Task<ActionResult<CollectionCreationResult>> CreateCollection(
[FromQuery] string? name,
[FromQuery] string? ids,
[FromQuery] Guid? parentId,
@ -59,14 +60,14 @@ namespace Jellyfin.Api.Controllers
{
var userId = _authContext.GetAuthorizationInfo(Request).UserId;
var item = _collectionManager.CreateCollection(new CollectionCreationOptions
var item = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions
{
IsLocked = isLocked,
Name = name,
ParentId = parentId,
ItemIdList = RequestHelpers.Split(ids, ',', true),
UserIds = new[] { userId }
});
}).ConfigureAwait(false);
var dtoOptions = new DtoOptions().AddClientFields(Request);
@ -87,9 +88,9 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("{collectionId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult AddToCollection([FromRoute] Guid collectionId, [FromQuery, Required] string? itemIds)
public async Task<ActionResult> AddToCollection([FromRoute] Guid collectionId, [FromQuery, Required] string? itemIds)
{
_collectionManager.AddToCollection(collectionId, RequestHelpers.Split(itemIds, ',', true));
await _collectionManager.AddToCollectionAsync(collectionId, RequestHelpers.GetGuids(itemIds)).ConfigureAwait(true);
return NoContent();
}
@ -102,9 +103,9 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpDelete("{collectionId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult RemoveFromCollection([FromRoute] Guid collectionId, [FromQuery, Required] string? itemIds)
public async Task<ActionResult> RemoveFromCollection([FromRoute] Guid collectionId, [FromQuery, Required] string? itemIds)
{
_collectionManager.RemoveFromCollection(collectionId, RequestHelpers.Split(itemIds, ',', true));
await _collectionManager.RemoveFromCollectionAsync(collectionId, RequestHelpers.GetGuids(itemIds)).ConfigureAwait(false);
return NoContent();
}
}

View File

@ -117,7 +117,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("MediaEncoder/Path")]
[Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult UpdateMediaEncoderPath([FromForm, Required] MediaEncoderPathDto mediaEncoderPath)
public ActionResult UpdateMediaEncoderPath([FromBody, Required] MediaEncoderPathDto mediaEncoderPath)
{
_mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType);
return NoContent();

View File

@ -48,7 +48,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<DeviceInfo>> GetDevices([FromQuery] bool? supportsSync, [FromQuery, Required] Guid? userId)
public ActionResult<QueryResult<DeviceInfo>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId)
{
var deviceQuery = new DeviceQuery { SupportsSync = supportsSync, UserId = userId ?? Guid.Empty };
return _deviceManager.GetDevices(deviceQuery);

View File

@ -60,8 +60,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="serverId">Server UUID.</param>
/// <response code="200">Dlna content directory returned.</response>
/// <returns>An <see cref="OkResult"/> containing the dlna content directory xml.</returns>
[HttpGet("{serverId}/ContentDirectory/ContentDirectory")]
[HttpGet("{serverId}/ContentDirectory/ContentDirectory.xml", Name = "GetContentDirectory_2")]
[HttpGet("{serverId}/ContentDirectory")]
[HttpGet("{serverId}/ContentDirectory.xml", Name = "GetContentDirectory_2")]
[Produces(XMLContentType)]
[ProducesResponseType(StatusCodes.Status200OK)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
@ -75,8 +75,8 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="serverId">Server UUID.</param>
/// <returns>Dlna media receiver registrar xml.</returns>
[HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar")]
[HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_2")]
[HttpGet("{serverId}/MediaReceiverRegistrar")]
[HttpGet("{serverId}/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_2")]
[Produces(XMLContentType)]
[ProducesResponseType(StatusCodes.Status200OK)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
@ -90,8 +90,8 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="serverId">Server UUID.</param>
/// <returns>Dlna media receiver registrar xml.</returns>
[HttpGet("{serverId}/ConnectionManager/ConnectionManager")]
[HttpGet("{serverId}/ConnectionManager/ConnectionManager.xml", Name = "GetConnectionManager_2")]
[HttpGet("{serverId}/ConnectionManager")]
[HttpGet("{serverId}/ConnectionManager.xml", Name = "GetConnectionManager_2")]
[Produces(XMLContentType)]
[ProducesResponseType(StatusCodes.Status200OK)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
@ -221,9 +221,8 @@ namespace Jellyfin.Api.Controllers
private Task<ControlResponse> ProcessControlRequestInternalAsync(string id, Stream requestStream, IUpnpService service)
{
return service.ProcessControlRequestAsync(new ControlRequest
return service.ProcessControlRequestAsync(new ControlRequest(Request.Headers)
{
Headers = Request.Headers,
InputXml = requestStream,
TargetServerUuId = id,
RequestedUrl = GetAbsoluteUri()

View File

@ -13,7 +13,6 @@ using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.PlaybackDtos;
using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna;
@ -22,7 +21,6 @@ using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using Microsoft.AspNetCore.Authorization;
@ -53,9 +51,9 @@ namespace Jellyfin.Api.Controllers
private readonly IConfiguration _configuration;
private readonly IDeviceManager _deviceManager;
private readonly TranscodingJobHelper _transcodingJobHelper;
private readonly INetworkManager _networkManager;
private readonly ILogger<DynamicHlsController> _logger;
private readonly EncodingHelper _encodingHelper;
private readonly DynamicHlsHelper _dynamicHlsHelper;
private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Hls;
@ -74,8 +72,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
/// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
/// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param>
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsController}"/> interface.</param>
/// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param>
public DynamicHlsController(
ILibraryManager libraryManager,
IUserManager userManager,
@ -89,8 +87,8 @@ namespace Jellyfin.Api.Controllers
IConfiguration configuration,
IDeviceManager deviceManager,
TranscodingJobHelper transcodingJobHelper,
INetworkManager networkManager,
ILogger<DynamicHlsController> logger)
ILogger<DynamicHlsController> logger,
DynamicHlsHelper dynamicHlsHelper)
{
_libraryManager = libraryManager;
_userManager = userManager;
@ -104,8 +102,8 @@ namespace Jellyfin.Api.Controllers
_configuration = configuration;
_deviceManager = deviceManager;
_transcodingJobHelper = transcodingJobHelper;
_networkManager = networkManager;
_logger = logger;
_dynamicHlsHelper = dynamicHlsHelper;
_encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
}
@ -220,8 +218,6 @@ namespace Jellyfin.Api.Controllers
[FromQuery] Dictionary<string, string> streamOptions,
[FromQuery] bool enableAdaptiveBitrateStreaming = true)
{
var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
var cancellationTokenSource = new CancellationTokenSource();
var streamingRequest = new HlsVideoRequestDto
{
Id = itemId,
@ -276,8 +272,7 @@ namespace Jellyfin.Api.Controllers
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
};
return await GetMasterPlaylistInternal(streamingRequest, isHeadRequest, enableAdaptiveBitrateStreaming, cancellationTokenSource)
.ConfigureAwait(false);
return await _dynamicHlsHelper.GetMasterHlsPlaylist(_transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
}
/// <summary>
@ -390,8 +385,6 @@ namespace Jellyfin.Api.Controllers
[FromQuery] Dictionary<string, string> streamOptions,
[FromQuery] bool enableAdaptiveBitrateStreaming = true)
{
var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
var cancellationTokenSource = new CancellationTokenSource();
var streamingRequest = new HlsAudioRequestDto
{
Id = itemId,
@ -446,8 +439,7 @@ namespace Jellyfin.Api.Controllers
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
};
return await GetMasterPlaylistInternal(streamingRequest, isHeadRequest, enableAdaptiveBitrateStreaming, cancellationTokenSource)
.ConfigureAwait(false);
return await _dynamicHlsHelper.GetMasterHlsPlaylist(_transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
}
/// <summary>
@ -1118,106 +1110,6 @@ namespace Jellyfin.Api.Controllers
.ConfigureAwait(false);
}
private async Task<ActionResult> GetMasterPlaylistInternal(
StreamingRequestDto streamingRequest,
bool isHeadRequest,
bool enableAdaptiveBitrateStreaming,
CancellationTokenSource cancellationTokenSource)
{
using var state = await StreamingHelpers.GetStreamingState(
streamingRequest,
Request,
_authContext,
_mediaSourceManager,
_userManager,
_libraryManager,
_serverConfigurationManager,
_mediaEncoder,
_fileSystem,
_subtitleEncoder,
_configuration,
_dlnaManager,
_deviceManager,
_transcodingJobHelper,
_transcodingJobType,
cancellationTokenSource.Token)
.ConfigureAwait(false);
Response.Headers.Add(HeaderNames.Expires, "0");
if (isHeadRequest)
{
return new FileContentResult(Array.Empty<byte>(), MimeTypes.GetMimeType("playlist.m3u8"));
}
var totalBitrate = state.OutputAudioBitrate ?? 0 + state.OutputVideoBitrate ?? 0;
var builder = new StringBuilder();
builder.AppendLine("#EXTM3U");
var isLiveStream = state.IsSegmentedLiveStream;
var queryString = Request.QueryString.ToString();
// from universal audio service
if (queryString.IndexOf("SegmentContainer", StringComparison.OrdinalIgnoreCase) == -1 && !string.IsNullOrWhiteSpace(state.Request.SegmentContainer))
{
queryString += "&SegmentContainer=" + state.Request.SegmentContainer;
}
// from universal audio service
if (!string.IsNullOrWhiteSpace(state.Request.TranscodeReasons) && queryString.IndexOf("TranscodeReasons=", StringComparison.OrdinalIgnoreCase) == -1)
{
queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons;
}
// Main stream
var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8";
playlistUrl += queryString;
var subtitleStreams = state.MediaSource
.MediaStreams
.Where(i => i.IsTextSubtitleStream)
.ToList();
var subtitleGroup = subtitleStreams.Count > 0 && (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Hls || state.VideoRequest!.EnableSubtitlesInManifest)
? "subs"
: null;
// If we're burning in subtitles then don't add additional subs to the manifest
if (state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
{
subtitleGroup = null;
}
if (!string.IsNullOrWhiteSpace(subtitleGroup))
{
AddSubtitles(state, subtitleStreams, builder);
}
AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming))
{
var requestedVideoBitrate = state.VideoRequest == null ? 0 : state.VideoRequest.VideoBitRate ?? 0;
// By default, vary by just 200k
var variation = GetBitrateVariation(totalBitrate);
var newBitrate = totalBitrate - variation;
var variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
variation *= 2;
newBitrate = totalBitrate - variation;
variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
}
return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
}
private async Task<ActionResult> GetVariantPlaylistInternal(StreamingRequestDto streamingRequest, string name, CancellationTokenSource cancellationTokenSource)
{
using var state = await StreamingHelpers.GetStreamingState(
@ -1411,330 +1303,6 @@ namespace Jellyfin.Api.Controllers
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
}
private void AddSubtitles(StreamState state, IEnumerable<MediaStream> subtitles, StringBuilder builder)
{
var selectedIndex = state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Hls ? (int?)null : state.SubtitleStream.Index;
const string Format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},AUTOSELECT=YES,URI=\"{3}\",LANGUAGE=\"{4}\"";
foreach (var stream in subtitles)
{
var name = stream.DisplayTitle;
var isDefault = selectedIndex.HasValue && selectedIndex.Value == stream.Index;
var isForced = stream.IsForced;
var url = string.Format(
CultureInfo.InvariantCulture,
"{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}&api_key={3}",
state.Request.MediaSourceId,
stream.Index.ToString(CultureInfo.InvariantCulture),
30.ToString(CultureInfo.InvariantCulture),
ClaimHelpers.GetToken(Request.HttpContext.User));
var line = string.Format(
CultureInfo.InvariantCulture,
Format,
name,
isDefault ? "YES" : "NO",
isForced ? "YES" : "NO",
url,
stream.Language ?? "Unknown");
builder.AppendLine(line);
}
}
private void AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subtitleGroup)
{
builder.Append("#EXT-X-STREAM-INF:BANDWIDTH=")
.Append(bitrate.ToString(CultureInfo.InvariantCulture))
.Append(",AVERAGE-BANDWIDTH=")
.Append(bitrate.ToString(CultureInfo.InvariantCulture));
AppendPlaylistCodecsField(builder, state);
AppendPlaylistResolutionField(builder, state);
AppendPlaylistFramerateField(builder, state);
if (!string.IsNullOrWhiteSpace(subtitleGroup))
{
builder.Append(",SUBTITLES=\"")
.Append(subtitleGroup)
.Append('"');
}
builder.Append(Environment.NewLine);
builder.AppendLine(url);
}
/// <summary>
/// Appends a CODECS field containing formatted strings of
/// the active streams output video and audio codecs.
/// </summary>
/// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
/// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
/// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
/// <param name="builder">StringBuilder to append the field to.</param>
/// <param name="state">StreamState of the current stream.</param>
private void AppendPlaylistCodecsField(StringBuilder builder, StreamState state)
{
// Video
string videoCodecs = string.Empty;
int? videoCodecLevel = GetOutputVideoCodecLevel(state);
if (!string.IsNullOrEmpty(state.ActualOutputVideoCodec) && videoCodecLevel.HasValue)
{
videoCodecs = GetPlaylistVideoCodecs(state, state.ActualOutputVideoCodec, videoCodecLevel.Value);
}
// Audio
string audioCodecs = string.Empty;
if (!string.IsNullOrEmpty(state.ActualOutputAudioCodec))
{
audioCodecs = GetPlaylistAudioCodecs(state);
}
StringBuilder codecs = new StringBuilder();
codecs.Append(videoCodecs);
if (!string.IsNullOrEmpty(videoCodecs) && !string.IsNullOrEmpty(audioCodecs))
{
codecs.Append(',');
}
codecs.Append(audioCodecs);
if (codecs.Length > 1)
{
builder.Append(",CODECS=\"")
.Append(codecs)
.Append('"');
}
}
/// <summary>
/// Appends a RESOLUTION field containing the resolution of the output stream.
/// </summary>
/// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
/// <param name="builder">StringBuilder to append the field to.</param>
/// <param name="state">StreamState of the current stream.</param>
private void AppendPlaylistResolutionField(StringBuilder builder, StreamState state)
{
if (state.OutputWidth.HasValue && state.OutputHeight.HasValue)
{
builder.Append(",RESOLUTION=")
.Append(state.OutputWidth.GetValueOrDefault())
.Append('x')
.Append(state.OutputHeight.GetValueOrDefault());
}
}
/// <summary>
/// Appends a FRAME-RATE field containing the framerate of the output stream.
/// </summary>
/// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
/// <param name="builder">StringBuilder to append the field to.</param>
/// <param name="state">StreamState of the current stream.</param>
private void AppendPlaylistFramerateField(StringBuilder builder, StreamState state)
{
double? framerate = null;
if (state.TargetFramerate.HasValue)
{
framerate = Math.Round(state.TargetFramerate.GetValueOrDefault(), 3);
}
else if (state.VideoStream?.RealFrameRate != null)
{
framerate = Math.Round(state.VideoStream.RealFrameRate.GetValueOrDefault(), 3);
}
if (framerate.HasValue)
{
builder.Append(",FRAME-RATE=")
.Append(framerate.Value);
}
}
private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream, bool enableAdaptiveBitrateStreaming)
{
// Within the local network this will likely do more harm than good.
var ip = RequestHelpers.NormalizeIp(Request.HttpContext.Connection.RemoteIpAddress).ToString();
if (_networkManager.IsInLocalNetwork(ip))
{
return false;
}
if (!enableAdaptiveBitrateStreaming)
{
return false;
}
if (isLiveStream || string.IsNullOrWhiteSpace(state.MediaPath))
{
// Opening live streams is so slow it's not even worth it
return false;
}
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
{
return false;
}
if (EncodingHelper.IsCopyCodec(state.OutputAudioCodec))
{
return false;
}
if (!state.IsOutputVideo)
{
return false;
}
// Having problems in android
return false;
// return state.VideoRequest.VideoBitRate.HasValue;
}
/// <summary>
/// Get the H.26X level of the output video stream.
/// </summary>
/// <param name="state">StreamState of the current stream.</param>
/// <returns>H.26X level of the output video stream.</returns>
private int? GetOutputVideoCodecLevel(StreamState state)
{
string? levelString;
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
&& state.VideoStream.Level.HasValue)
{
levelString = state.VideoStream?.Level.ToString();
}
else
{
levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec);
}
if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel))
{
return parsedLevel;
}
return null;
}
/// <summary>
/// Gets a formatted string of the output audio codec, for use in the CODECS field.
/// </summary>
/// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
/// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
/// <param name="state">StreamState of the current stream.</param>
/// <returns>Formatted audio codec string.</returns>
private string GetPlaylistAudioCodecs(StreamState state)
{
if (string.Equals(state.ActualOutputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase))
{
string? profile = state.GetRequestedProfiles("aac").FirstOrDefault();
return HlsCodecStringHelpers.GetAACString(profile);
}
if (string.Equals(state.ActualOutputAudioCodec, "mp3", StringComparison.OrdinalIgnoreCase))
{
return HlsCodecStringHelpers.GetMP3String();
}
if (string.Equals(state.ActualOutputAudioCodec, "ac3", StringComparison.OrdinalIgnoreCase))
{
return HlsCodecStringHelpers.GetAC3String();
}
if (string.Equals(state.ActualOutputAudioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
{
return HlsCodecStringHelpers.GetEAC3String();
}
return string.Empty;
}
/// <summary>
/// Gets a formatted string of the output video codec, for use in the CODECS field.
/// </summary>
/// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
/// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
/// <param name="state">StreamState of the current stream.</param>
/// <param name="codec">Video codec.</param>
/// <param name="level">Video level.</param>
/// <returns>Formatted video codec string.</returns>
private string GetPlaylistVideoCodecs(StreamState state, string codec, int level)
{
if (level == 0)
{
// This is 0 when there's no requested H.26X level in the device profile
// and the source is not encoded in H.26X
_logger.LogError("Got invalid H.26X level when building CODECS field for HLS master playlist");
return string.Empty;
}
if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase))
{
string profile = state.GetRequestedProfiles("h264").FirstOrDefault();
return HlsCodecStringHelpers.GetH264String(profile, level);
}
if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
{
string profile = state.GetRequestedProfiles("h265").FirstOrDefault();
return HlsCodecStringHelpers.GetH265String(profile, level);
}
return string.Empty;
}
private int GetBitrateVariation(int bitrate)
{
// By default, vary by just 50k
var variation = 50000;
if (bitrate >= 10000000)
{
variation = 2000000;
}
else if (bitrate >= 5000000)
{
variation = 1500000;
}
else if (bitrate >= 3000000)
{
variation = 1000000;
}
else if (bitrate >= 2000000)
{
variation = 500000;
}
else if (bitrate >= 1000000)
{
variation = 300000;
}
else if (bitrate >= 600000)
{
variation = 200000;
}
else if (bitrate >= 400000)
{
variation = 100000;
}
return variation;
}
private string ReplaceBitrate(string url, int oldValue, int newValue)
{
return url.Replace(
"videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture),
"videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture),
StringComparison.OrdinalIgnoreCase);
}
private double[] GetSegmentLengths(StreamState state)
{
var result = new List<double>();
@ -2089,7 +1657,7 @@ namespace Jellyfin.Api.Controllers
return Task.CompletedTask;
});
return FileStreamResponseHelpers.GetStaticFileResult(segmentPath, MimeTypes.GetMimeType(segmentPath)!, false, this);
return FileStreamResponseHelpers.GetStaticFileResult(segmentPath, MimeTypes.GetMimeType(segmentPath)!, false, HttpContext);
}
private long GetEndPositionTicks(StreamState state, int requestedIndex)

View File

@ -61,7 +61,7 @@ namespace Jellyfin.Api.Controllers
var file = segmentId + Path.GetExtension(Request.Path);
file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file);
return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)!, false, this);
return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)!, false, HttpContext);
}
/// <summary>
@ -148,7 +148,7 @@ namespace Jellyfin.Api.Controllers
return Task.CompletedTask;
});
return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path)!, false, this);
return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path)!, false, HttpContext);
}
}
}

View File

@ -113,7 +113,7 @@ namespace Jellyfin.Api.Controllers
user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType)));
await _providerManager
.SaveImage(user, memoryStream, mimeType, user.ProfileImage.Path)
.SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
.ConfigureAwait(false);
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
@ -174,7 +174,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult DeleteItemImage(
public async Task<ActionResult> DeleteItemImage(
[FromRoute] Guid itemId,
[FromRoute] ImageType imageType,
[FromRoute] int? imageIndex = null)
@ -185,7 +185,7 @@ namespace Jellyfin.Api.Controllers
return NotFound();
}
item.DeleteImage(imageType, imageIndex ?? 0);
await item.DeleteImageAsync(imageType, imageIndex ?? 0).ConfigureAwait(false);
return NoContent();
}
@ -218,7 +218,7 @@ namespace Jellyfin.Api.Controllers
// Handle image/png; charset=utf-8
var mimeType = Request.ContentType.Split(';').FirstOrDefault();
await _providerManager.SaveImage(item, Request.Body, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
return NoContent();
}
@ -237,7 +237,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UpdateItemImageIndex(
public async Task<ActionResult> UpdateItemImageIndex(
[FromRoute] Guid itemId,
[FromRoute] ImageType imageType,
[FromRoute] int imageIndex,
@ -249,7 +249,7 @@ namespace Jellyfin.Api.Controllers
return NotFound();
}
item.SwapImages(imageType, imageIndex, newIndex);
await item.SwapImagesAsync(imageType, imageIndex, newIndex).ConfigureAwait(false);
return NoContent();
}
@ -264,7 +264,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<IEnumerable<ImageInfo>> GetItemImageInfos([FromRoute] Guid itemId)
public async Task<ActionResult<IEnumerable<ImageInfo>>> GetItemImageInfos([FromRoute] Guid itemId)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
@ -281,7 +281,7 @@ namespace Jellyfin.Api.Controllers
return list;
}
_libraryManager.UpdateImages(item); // this makes sure dimensions and hashes are correct
await _libraryManager.UpdateImagesAsync(item).ConfigureAwait(false); // this makes sure dimensions and hashes are correct
foreach (var image in itemImages)
{
@ -969,7 +969,7 @@ namespace Jellyfin.Api.Controllers
var text = await reader.ReadToEndAsync().ConfigureAwait(false);
var bytes = Convert.FromBase64String(text);
return new MemoryStream(bytes) { Position = 0 };
return new MemoryStream(bytes, 0, bytes.Length, false, true);
}
private ImageInfo? GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex)

View File

@ -331,12 +331,12 @@ namespace Jellyfin.Api.Controllers
/// <returns>Task.</returns>
private async Task DownloadImage(string providerName, string url, Guid urlHash, string pointerCachePath)
{
var result = await _providerManager.GetSearchImage(providerName, url, CancellationToken.None).ConfigureAwait(false);
var ext = result.ContentType.Split('/').Last();
using var result = await _providerManager.GetSearchImage(providerName, url, CancellationToken.None).ConfigureAwait(false);
var ext = result.Content.Headers.ContentType.MediaType.Split('/')[^1];
var fullCachePath = GetFullCachePath(urlHash + "." + ext);
Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath));
await using (var stream = result.Content)
using (var stream = result.Content)
{
await using var fileStream = new FileStream(
fullCachePath,

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@ -67,7 +68,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Items/{itemId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UpdateItem([FromRoute] Guid itemId, [FromBody, Required] BaseItemDto request)
public async Task<ActionResult> UpdateItem([FromRoute] Guid itemId, [FromBody, Required] BaseItemDto request)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
@ -101,7 +102,7 @@ namespace Jellyfin.Api.Controllers
item.OnMetadataChanged();
item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
if (isLockedChanged && item.IsFolder)
{
@ -110,7 +111,7 @@ namespace Jellyfin.Api.Controllers
foreach (var child in folder.GetRecursiveChildren())
{
child.IsLocked = newLockData;
child.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
await child.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
}
}

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Mime;
using System.Security.Cryptography;
using System.Text;
@ -15,7 +16,6 @@ using Jellyfin.Api.Models.LiveTvDtos;
using Jellyfin.Data.Enums;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
@ -39,7 +39,7 @@ namespace Jellyfin.Api.Controllers
{
private readonly ILiveTvManager _liveTvManager;
private readonly IUserManager _userManager;
private readonly IHttpClient _httpClient;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILibraryManager _libraryManager;
private readonly IDtoService _dtoService;
private readonly ISessionContext _sessionContext;
@ -52,7 +52,7 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="liveTvManager">Instance of the <see cref="ILiveTvManager"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="httpClient">Instance of the <see cref="IHttpClient"/> interface.</param>
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
/// <param name="sessionContext">Instance of the <see cref="ISessionContext"/> interface.</param>
@ -62,7 +62,7 @@ namespace Jellyfin.Api.Controllers
public LiveTvController(
ILiveTvManager liveTvManager,
IUserManager userManager,
IHttpClient httpClient,
IHttpClientFactory httpClientFactory,
ILibraryManager libraryManager,
IDtoService dtoService,
ISessionContext sessionContext,
@ -72,7 +72,7 @@ namespace Jellyfin.Api.Controllers
{
_liveTvManager = liveTvManager;
_userManager = userManager;
_httpClient = httpClient;
_httpClientFactory = httpClientFactory;
_libraryManager = libraryManager;
_dtoService = dtoService;
_sessionContext = sessionContext;
@ -592,11 +592,11 @@ namespace Jellyfin.Api.Controllers
GenreIds = RequestHelpers.GetGuids(genreIds)
};
if (!librarySeriesId.Equals(Guid.Empty))
if (librarySeriesId != null && !librarySeriesId.Equals(Guid.Empty))
{
query.IsSeries = true;
if (_libraryManager.GetItemById(librarySeriesId ?? Guid.Empty) is Series series)
if (_libraryManager.GetItemById(librarySeriesId.Value) is Series series)
{
query.Name = series.Name;
}
@ -1004,7 +1004,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="validateLogin">Validate login.</param>
/// <response code="200">Created listings provider returned.</response>
/// <returns>A <see cref="OkResult"/> containing the created listings provider.</returns>
[HttpGet("ListingProviders")]
[HttpPost("ListingProviders")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
[SuppressMessage("Microsoft.Performance", "CA5350:RemoveSha1", MessageId = "AddListingProvider", Justification = "Imported from ServiceStack")]
@ -1069,13 +1069,13 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult> GetSchedulesDirectCountries()
{
var client = _httpClientFactory.CreateClient();
// https://json.schedulesdirect.org/20141201/available/countries
var response = await _httpClient.Get(new HttpRequestOptions
{
Url = "https://json.schedulesdirect.org/20141201/available/countries",
BufferContent = false
}).ConfigureAwait(false);
return File(response, MediaTypeNames.Application.Json);
// Can't dispose the response as it's required up the call chain.
var response = await client.GetAsync("https://json.schedulesdirect.org/20141201/available/countries")
.ConfigureAwait(false);
return File(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), MediaTypeNames.Application.Json);
}
/// <summary>

View File

@ -1,30 +1,18 @@
using System;
using System.Buffers;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using System.Net.Mime;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.MediaInfoDtos;
using Jellyfin.Api.Models.VideoDtos;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@ -42,12 +30,9 @@ namespace Jellyfin.Api.Controllers
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IDeviceManager _deviceManager;
private readonly ILibraryManager _libraryManager;
private readonly INetworkManager _networkManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly IUserManager _userManager;
private readonly IAuthorizationContext _authContext;
private readonly ILogger<MediaInfoController> _logger;
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly MediaInfoHelper _mediaInfoHelper;
/// <summary>
/// Initializes a new instance of the <see cref="MediaInfoController"/> class.
@ -55,32 +40,23 @@ namespace Jellyfin.Api.Controllers
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
/// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{MediaInfoController}"/> interface.</param>
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="mediaInfoHelper">Instance of the <see cref="MediaInfoHelper"/>.</param>
public MediaInfoController(
IMediaSourceManager mediaSourceManager,
IDeviceManager deviceManager,
ILibraryManager libraryManager,
INetworkManager networkManager,
IMediaEncoder mediaEncoder,
IUserManager userManager,
IAuthorizationContext authContext,
ILogger<MediaInfoController> logger,
IServerConfigurationManager serverConfigurationManager)
MediaInfoHelper mediaInfoHelper)
{
_mediaSourceManager = mediaSourceManager;
_deviceManager = deviceManager;
_libraryManager = libraryManager;
_networkManager = networkManager;
_mediaEncoder = mediaEncoder;
_userManager = userManager;
_authContext = authContext;
_logger = logger;
_serverConfigurationManager = serverConfigurationManager;
_mediaInfoHelper = mediaInfoHelper;
}
/// <summary>
@ -94,7 +70,10 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<PlaybackInfoResponse>> GetPlaybackInfo([FromRoute] Guid itemId, [FromQuery, Required] Guid? userId)
{
return await GetPlaybackInfoInternal(itemId, userId).ConfigureAwait(false);
return await _mediaInfoHelper.GetPlaybackInfo(
itemId,
userId)
.ConfigureAwait(false);
}
/// <summary>
@ -153,7 +132,12 @@ namespace Jellyfin.Api.Controllers
}
}
var info = await GetPlaybackInfoInternal(itemId, userId, mediaSourceId, liveStreamId).ConfigureAwait(false);
var info = await _mediaInfoHelper.GetPlaybackInfo(
itemId,
userId,
mediaSourceId,
liveStreamId)
.ConfigureAwait(false);
if (profile != null)
{
@ -162,7 +146,7 @@ namespace Jellyfin.Api.Controllers
foreach (var mediaSource in info.MediaSources)
{
SetDeviceSpecificData(
_mediaInfoHelper.SetDeviceSpecificData(
item,
mediaSource,
profile,
@ -179,10 +163,11 @@ namespace Jellyfin.Api.Controllers
enableDirectStream,
enableTranscoding,
allowVideoStreamCopy,
allowAudioStreamCopy);
allowAudioStreamCopy,
Request.HttpContext.Connection.RemoteIpAddress.ToString());
}
SortMediaSources(info, maxStreamingBitrate);
_mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
}
if (autoOpenLiveStream)
@ -191,7 +176,9 @@ namespace Jellyfin.Api.Controllers
if (mediaSource != null && mediaSource.RequiresOpening && string.IsNullOrWhiteSpace(mediaSource.LiveStreamId))
{
var openStreamResult = await OpenMediaSource(new LiveStreamRequest
var openStreamResult = await _mediaInfoHelper.OpenMediaSource(
Request,
new LiveStreamRequest
{
AudioStreamIndex = audioStreamIndex,
DeviceProfile = deviceProfile?.DeviceProfile,
@ -215,7 +202,7 @@ namespace Jellyfin.Api.Controllers
{
foreach (var mediaSource in info.MediaSources)
{
NormalizeMediaSourceContainer(mediaSource, profile!, DlnaProfileType.Video);
_mediaInfoHelper.NormalizeMediaSourceContainer(mediaSource, profile!, DlnaProfileType.Video);
}
}
@ -271,7 +258,7 @@ namespace Jellyfin.Api.Controllers
EnableDirectStream = enableDirectStream,
DirectPlayProtocols = openLiveStreamDto?.DirectPlayProtocols ?? new[] { MediaProtocol.Http }
};
return await OpenMediaSource(request).ConfigureAwait(false);
return await _mediaInfoHelper.OpenMediaSource(Request, request).ConfigureAwait(false);
}
/// <summary>
@ -282,9 +269,9 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("LiveStreams/Close")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult CloseLiveStream([FromQuery, Required] string? liveStreamId)
public async Task<ActionResult> CloseLiveStream([FromQuery, Required] string? liveStreamId)
{
_mediaSourceManager.CloseLiveStream(liveStreamId).GetAwaiter().GetResult();
await _mediaSourceManager.CloseLiveStream(liveStreamId).ConfigureAwait(false);
return NoContent();
}
@ -324,454 +311,5 @@ namespace Jellyfin.Api.Controllers
ArrayPool<byte>.Shared.Return(buffer);
}
}
private async Task<PlaybackInfoResponse> GetPlaybackInfoInternal(
Guid id,
Guid? userId,
string? mediaSourceId = null,
string? liveStreamId = null)
{
var user = userId.HasValue && !userId.Equals(Guid.Empty)
? _userManager.GetUserById(userId.Value)
: null;
var item = _libraryManager.GetItemById(id);
var result = new PlaybackInfoResponse();
MediaSourceInfo[] mediaSources;
if (string.IsNullOrWhiteSpace(liveStreamId))
{
// TODO (moved from MediaBrowser.Api) handle supportedLiveMediaTypes?
var mediaSourcesList = await _mediaSourceManager.GetPlaybackMediaSources(item, user, true, true, CancellationToken.None).ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(mediaSourceId))
{
mediaSources = mediaSourcesList.ToArray();
}
else
{
mediaSources = mediaSourcesList
.Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
.ToArray();
}
}
else
{
var mediaSource = await _mediaSourceManager.GetLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false);
mediaSources = new[] { mediaSource };
}
if (mediaSources.Length == 0)
{
result.MediaSources = Array.Empty<MediaSourceInfo>();
result.ErrorCode ??= PlaybackErrorCode.NoCompatibleStream;
}
else
{
// Since we're going to be setting properties on MediaSourceInfos that come out of _mediaSourceManager, we should clone it
// Should we move this directly into MediaSourceManager?
result.MediaSources = JsonSerializer.Deserialize<MediaSourceInfo[]>(JsonSerializer.SerializeToUtf8Bytes(mediaSources));
result.PlaySessionId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
}
return result;
}
private void NormalizeMediaSourceContainer(MediaSourceInfo mediaSource, DeviceProfile profile, DlnaProfileType type)
{
mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, mediaSource.Path, profile, type);
}
private void SetDeviceSpecificData(
BaseItem item,
MediaSourceInfo mediaSource,
DeviceProfile profile,
AuthorizationInfo auth,
long? maxBitrate,
long startTimeTicks,
string mediaSourceId,
int? audioStreamIndex,
int? subtitleStreamIndex,
int? maxAudioChannels,
string playSessionId,
Guid userId,
bool enableDirectPlay,
bool enableDirectStream,
bool enableTranscoding,
bool allowVideoStreamCopy,
bool allowAudioStreamCopy)
{
var streamBuilder = new StreamBuilder(_mediaEncoder, _logger);
var options = new VideoOptions
{
MediaSources = new[] { mediaSource },
Context = EncodingContext.Streaming,
DeviceId = auth.DeviceId,
ItemId = item.Id,
Profile = profile,
MaxAudioChannels = maxAudioChannels
};
if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase))
{
options.MediaSourceId = mediaSourceId;
options.AudioStreamIndex = audioStreamIndex;
options.SubtitleStreamIndex = subtitleStreamIndex;
}
var user = _userManager.GetUserById(userId);
if (!enableDirectPlay)
{
mediaSource.SupportsDirectPlay = false;
}
if (!enableDirectStream)
{
mediaSource.SupportsDirectStream = false;
}
if (!enableTranscoding)
{
mediaSource.SupportsTranscoding = false;
}
if (item is Audio)
{
_logger.LogInformation(
"User policy for {0}. EnableAudioPlaybackTranscoding: {1}",
user.Username,
user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
}
else
{
_logger.LogInformation(
"User policy for {0}. EnablePlaybackRemuxing: {1} EnableVideoPlaybackTranscoding: {2} EnableAudioPlaybackTranscoding: {3}",
user.Username,
user.HasPermission(PermissionKind.EnablePlaybackRemuxing),
user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding),
user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
}
// Beginning of Playback Determination: Attempt DirectPlay first
if (mediaSource.SupportsDirectPlay)
{
if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
{
mediaSource.SupportsDirectPlay = false;
}
else
{
var supportsDirectStream = mediaSource.SupportsDirectStream;
// Dummy this up to fool StreamBuilder
mediaSource.SupportsDirectStream = true;
options.MaxBitrate = maxBitrate;
if (item is Audio)
{
if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
{
options.ForceDirectPlay = true;
}
}
else if (item is Video)
{
if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
&& !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
&& !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
{
options.ForceDirectPlay = true;
}
}
// The MediaSource supports direct stream, now test to see if the client supports it
var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
? streamBuilder.BuildAudioItem(options)
: streamBuilder.BuildVideoItem(options);
if (streamInfo == null || !streamInfo.IsDirectStream)
{
mediaSource.SupportsDirectPlay = false;
}
// Set this back to what it was
mediaSource.SupportsDirectStream = supportsDirectStream;
if (streamInfo != null)
{
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
}
}
}
if (mediaSource.SupportsDirectStream)
{
if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
{
mediaSource.SupportsDirectStream = false;
}
else
{
options.MaxBitrate = GetMaxBitrate(maxBitrate, user);
if (item is Audio)
{
if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
{
options.ForceDirectStream = true;
}
}
else if (item is Video)
{
if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
&& !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
&& !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
{
options.ForceDirectStream = true;
}
}
// The MediaSource supports direct stream, now test to see if the client supports it
var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
? streamBuilder.BuildAudioItem(options)
: streamBuilder.BuildVideoItem(options);
if (streamInfo == null || !streamInfo.IsDirectStream)
{
mediaSource.SupportsDirectStream = false;
}
if (streamInfo != null)
{
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
}
}
}
if (mediaSource.SupportsTranscoding)
{
options.MaxBitrate = GetMaxBitrate(maxBitrate, user);
// The MediaSource supports direct stream, now test to see if the client supports it
var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
? streamBuilder.BuildAudioItem(options)
: streamBuilder.BuildVideoItem(options);
if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
{
if (streamInfo != null)
{
streamInfo.PlaySessionId = playSessionId;
streamInfo.StartPositionTicks = startTimeTicks;
mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-');
mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
mediaSource.TranscodingContainer = streamInfo.Container;
mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
// Do this after the above so that StartPositionTicks is set
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
}
}
else
{
if (streamInfo != null)
{
streamInfo.PlaySessionId = playSessionId;
if (streamInfo.PlayMethod == PlayMethod.Transcode)
{
streamInfo.StartPositionTicks = startTimeTicks;
mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-');
if (!allowVideoStreamCopy)
{
mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
}
if (!allowAudioStreamCopy)
{
mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
}
mediaSource.TranscodingContainer = streamInfo.Container;
mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
}
if (!allowAudioStreamCopy)
{
mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
}
mediaSource.TranscodingContainer = streamInfo.Container;
mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
// Do this after the above so that StartPositionTicks is set
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
}
}
}
foreach (var attachment in mediaSource.MediaAttachments)
{
attachment.DeliveryUrl = string.Format(
CultureInfo.InvariantCulture,
"/Videos/{0}/{1}/Attachments/{2}",
item.Id,
mediaSource.Id,
attachment.Index);
}
}
private async Task<LiveStreamResponse> OpenMediaSource(LiveStreamRequest request)
{
var authInfo = _authContext.GetAuthorizationInfo(Request);
var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false);
var profile = request.DeviceProfile;
if (profile == null)
{
var caps = _deviceManager.GetCapabilities(authInfo.DeviceId);
if (caps != null)
{
profile = caps.DeviceProfile;
}
}
if (profile != null)
{
var item = _libraryManager.GetItemById(request.ItemId);
SetDeviceSpecificData(
item,
result.MediaSource,
profile,
authInfo,
request.MaxStreamingBitrate,
request.StartTimeTicks ?? 0,
result.MediaSource.Id,
request.AudioStreamIndex,
request.SubtitleStreamIndex,
request.MaxAudioChannels,
request.PlaySessionId,
request.UserId,
request.EnableDirectPlay,
request.EnableDirectStream,
true,
true,
true);
}
else
{
if (!string.IsNullOrWhiteSpace(result.MediaSource.TranscodingUrl))
{
result.MediaSource.TranscodingUrl += "&LiveStreamId=" + result.MediaSource.LiveStreamId;
}
}
// here was a check if (result.MediaSource != null) but Rider said it will never be null
NormalizeMediaSourceContainer(result.MediaSource, profile!, DlnaProfileType.Video);
return result;
}
private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken)
{
var profiles = info.GetSubtitleProfiles(_mediaEncoder, false, "-", accessToken);
mediaSource.DefaultSubtitleStreamIndex = info.SubtitleStreamIndex;
mediaSource.TranscodeReasons = info.TranscodeReasons;
foreach (var profile in profiles)
{
foreach (var stream in mediaSource.MediaStreams)
{
if (stream.Type == MediaStreamType.Subtitle && stream.Index == profile.Index)
{
stream.DeliveryMethod = profile.DeliveryMethod;
if (profile.DeliveryMethod == SubtitleDeliveryMethod.External)
{
stream.DeliveryUrl = profile.Url.TrimStart('-');
stream.IsExternalUrl = profile.IsExternalUrl;
}
}
}
}
}
private long? GetMaxBitrate(long? clientMaxBitrate, User user)
{
var maxBitrate = clientMaxBitrate;
var remoteClientMaxBitrate = user?.RemoteClientBitrateLimit ?? 0;
if (remoteClientMaxBitrate <= 0)
{
remoteClientMaxBitrate = _serverConfigurationManager.Configuration.RemoteClientBitrateLimit;
}
if (remoteClientMaxBitrate > 0)
{
var isInLocalNetwork = _networkManager.IsInLocalNetwork(Request.HttpContext.Connection.RemoteIpAddress.ToString());
_logger.LogInformation("RemoteClientBitrateLimit: {0}, RemoteIp: {1}, IsInLocalNetwork: {2}", remoteClientMaxBitrate, Request.HttpContext.Connection.RemoteIpAddress.ToString(), isInLocalNetwork);
if (!isInLocalNetwork)
{
maxBitrate = Math.Min(maxBitrate ?? remoteClientMaxBitrate, remoteClientMaxBitrate);
}
}
return maxBitrate;
}
private void SortMediaSources(PlaybackInfoResponse result, long? maxBitrate)
{
var originalList = result.MediaSources.ToList();
result.MediaSources = result.MediaSources.OrderBy(i =>
{
// Nothing beats direct playing a file
if (i.SupportsDirectPlay && i.Protocol == MediaProtocol.File)
{
return 0;
}
return 1;
})
.ThenBy(i =>
{
// Let's assume direct streaming a file is just as desirable as direct playing a remote url
if (i.SupportsDirectPlay || i.SupportsDirectStream)
{
return 0;
}
return 1;
})
.ThenBy(i =>
{
return i.Protocol switch
{
MediaProtocol.File => 0,
_ => 1,
};
})
.ThenBy(i =>
{
if (maxBitrate.HasValue && i.Bitrate.HasValue)
{
return i.Bitrate.Value <= maxBitrate.Value ? 0 : 2;
}
return 1;
})
.ThenBy(originalList.IndexOf)
.ToArray();
}
}
}

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading;
using Jellyfin.Api.Constants;

View File

@ -83,12 +83,12 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="NoContentResult"/> on success.</returns>
[HttpPost("{playlistId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult AddToPlaylist(
[FromRoute] string? playlistId,
public async Task<ActionResult> AddToPlaylist(
[FromRoute] Guid playlistId,
[FromQuery] string? ids,
[FromQuery] Guid? userId)
{
_playlistManager.AddToPlaylist(playlistId, RequestHelpers.GetGuids(ids), userId ?? Guid.Empty);
await _playlistManager.AddToPlaylistAsync(playlistId, RequestHelpers.GetGuids(ids), userId ?? Guid.Empty).ConfigureAwait(false);
return NoContent();
}
@ -102,12 +102,12 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="NoContentResult"/> on success.</returns>
[HttpPost("{playlistId}/Items/{itemId}/Move/{newIndex}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult MoveItem(
public async Task<ActionResult> MoveItem(
[FromRoute] string? playlistId,
[FromRoute] string? itemId,
[FromRoute] int newIndex)
{
_playlistManager.MoveItem(playlistId, itemId, newIndex);
await _playlistManager.MoveItemAsync(playlistId, itemId, newIndex).ConfigureAwait(false);
return NoContent();
}
@ -120,9 +120,9 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="NoContentResult"/> on success.</returns>
[HttpDelete("{playlistId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult RemoveFromPlaylist([FromRoute] string? playlistId, [FromQuery] string? entryIds)
public async Task<ActionResult> RemoveFromPlaylist([FromRoute] string? playlistId, [FromQuery] string? entryIds)
{
_playlistManager.RemoveFromPlaylist(playlistId, RequestHelpers.Split(entryIds, ',', true));
await _playlistManager.RemoveFromPlaylistAsync(playlistId, RequestHelpers.Split(entryIds, ',', true)).ConfigureAwait(false);
return NoContent();
}

View File

@ -94,7 +94,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="itemId">Item id.</param>
/// <response code="200">Item marked as unplayed.</response>
/// <returns>A <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpDelete("Users/{userId}/PlayedItem/{itemId}")]
[HttpDelete("Users/{userId}/PlayedItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<UserItemDataDto> MarkUnplayedItem([FromRoute] Guid userId, [FromRoute] Guid itemId)
{

View File

@ -3,12 +3,12 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
@ -30,7 +30,7 @@ namespace Jellyfin.Api.Controllers
{
private readonly IProviderManager _providerManager;
private readonly IServerApplicationPaths _applicationPaths;
private readonly IHttpClient _httpClient;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILibraryManager _libraryManager;
/// <summary>
@ -38,17 +38,17 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
/// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
/// <param name="httpClient">Instance of the <see cref="IHttpClient"/> interface.</param>
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
public RemoteImageController(
IProviderManager providerManager,
IServerApplicationPaths applicationPaths,
IHttpClient httpClient,
IHttpClientFactory httpClientFactory,
ILibraryManager libraryManager)
{
_providerManager = providerManager;
_applicationPaths = applicationPaths;
_httpClient = httpClient;
_httpClientFactory = httpClientFactory;
_libraryManager = libraryManager;
}
@ -221,7 +221,7 @@ namespace Jellyfin.Api.Controllers
await _providerManager.SaveImage(item, imageUrl, type, null, CancellationToken.None)
.ConfigureAwait(false);
item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
return NoContent();
}
@ -244,22 +244,14 @@ namespace Jellyfin.Api.Controllers
/// <returns>Task.</returns>
private async Task DownloadImage(string url, Guid urlHash, string pointerCachePath)
{
using var result = await _httpClient.GetResponse(new HttpRequestOptions
{
Url = url,
BufferContent = false
}).ConfigureAwait(false);
var ext = result.ContentType.Split('/').Last();
var httpClient = _httpClientFactory.CreateClient();
using var response = await httpClient.GetAsync(url).ConfigureAwait(false);
var ext = response.Content.Headers.ContentType.MediaType.Split('/').Last();
var fullCachePath = GetFullCachePath(urlHash + "." + ext);
Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath));
await using (var stream = result.Content)
{
await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
}
await response.Content.CopyToAsync(fileStream).ConfigureAwait(false);
Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath));
await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath, CancellationToken.None)
.ConfigureAwait(false);

View File

@ -153,7 +153,6 @@ namespace Jellyfin.Api.Controllers
/// <param name="itemIds">The ids of the items to play, comma delimited.</param>
/// <param name="startPositionTicks">The starting position of the first item.</param>
/// <param name="playCommand">The type of play command to issue (PlayNow, PlayNext, PlayLast). Clients who have not yet implemented play next and play last may play now.</param>
/// <param name="playRequest">The <see cref="PlayRequest"/>.</param>
/// <response code="204">Instruction sent to session.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Sessions/{sessionId}/Playing")]
@ -163,17 +162,14 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] string? sessionId,
[FromQuery] Guid[] itemIds,
[FromQuery] long? startPositionTicks,
[FromQuery] PlayCommand playCommand,
[FromBody, Required] PlayRequest playRequest)
[FromQuery] PlayCommand playCommand)
{
if (playRequest == null)
var playRequest = new PlayRequest
{
throw new ArgumentException("Request Body may not be null");
}
playRequest.ItemIds = itemIds;
playRequest.StartPositionTicks = startPositionTicks;
playRequest.PlayCommand = playCommand;
ItemIds = itemIds,
StartPositionTicks = startPositionTicks,
PlayCommand = playCommand
};
_sessionManager.SendPlayCommand(
RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id,
@ -413,7 +409,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult PostFullCapabilities(
[FromQuery, Required] string? id,
[FromQuery] string? id,
[FromBody, Required] ClientCapabilities capabilities)
{
if (string.IsNullOrWhiteSpace(id))
@ -480,7 +476,7 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <response code="200">Password reset providers retrieved.</response>
/// <returns>An <see cref="IEnumerable{NameIdPair}"/> with the password reset providers.</returns>
[HttpGet("Auto/PasswordResetProviders")]
[HttpGet("Auth/PasswordResetProviders")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.RequiresElevation)]
public ActionResult<IEnumerable<NameIdPair>> GetPasswordResetProviders()

Some files were not shown because too many files have changed in this diff Show More