mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-06-03 21:54:26 -04:00
Merge branch 'master' into tonemap
This commit is contained in:
commit
df6b303da7
@ -2776,82 +2776,82 @@ namespace Emby.Server.Implementations.Data
|
|||||||
|
|
||||||
private string FixUnicodeChars(string buffer)
|
private string FixUnicodeChars(string buffer)
|
||||||
{
|
{
|
||||||
if (buffer.IndexOf('\u2013') > -1)
|
if (buffer.IndexOf('\u2013', StringComparison.Ordinal) > -1)
|
||||||
{
|
{
|
||||||
buffer = buffer.Replace('\u2013', '-'); // en dash
|
buffer = buffer.Replace('\u2013', '-'); // en dash
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buffer.IndexOf('\u2014') > -1)
|
if (buffer.IndexOf('\u2014', StringComparison.Ordinal) > -1)
|
||||||
{
|
{
|
||||||
buffer = buffer.Replace('\u2014', '-'); // em dash
|
buffer = buffer.Replace('\u2014', '-'); // em dash
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buffer.IndexOf('\u2015') > -1)
|
if (buffer.IndexOf('\u2015', StringComparison.Ordinal) > -1)
|
||||||
{
|
{
|
||||||
buffer = buffer.Replace('\u2015', '-'); // horizontal bar
|
buffer = buffer.Replace('\u2015', '-'); // horizontal bar
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buffer.IndexOf('\u2017') > -1)
|
if (buffer.IndexOf('\u2017', StringComparison.Ordinal) > -1)
|
||||||
{
|
{
|
||||||
buffer = buffer.Replace('\u2017', '_'); // double low line
|
buffer = buffer.Replace('\u2017', '_'); // double low line
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buffer.IndexOf('\u2018') > -1)
|
if (buffer.IndexOf('\u2018', StringComparison.Ordinal) > -1)
|
||||||
{
|
{
|
||||||
buffer = buffer.Replace('\u2018', '\''); // left single quotation mark
|
buffer = buffer.Replace('\u2018', '\''); // left single quotation mark
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buffer.IndexOf('\u2019') > -1)
|
if (buffer.IndexOf('\u2019', StringComparison.Ordinal) > -1)
|
||||||
{
|
{
|
||||||
buffer = buffer.Replace('\u2019', '\''); // right single quotation mark
|
buffer = buffer.Replace('\u2019', '\''); // right single quotation mark
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buffer.IndexOf('\u201a') > -1)
|
if (buffer.IndexOf('\u201a', StringComparison.Ordinal) > -1)
|
||||||
{
|
{
|
||||||
buffer = buffer.Replace('\u201a', ','); // single low-9 quotation mark
|
buffer = buffer.Replace('\u201a', ','); // single low-9 quotation mark
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buffer.IndexOf('\u201b') > -1)
|
if (buffer.IndexOf('\u201b', StringComparison.Ordinal) > -1)
|
||||||
{
|
{
|
||||||
buffer = buffer.Replace('\u201b', '\''); // single high-reversed-9 quotation mark
|
buffer = buffer.Replace('\u201b', '\''); // single high-reversed-9 quotation mark
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buffer.IndexOf('\u201c') > -1)
|
if (buffer.IndexOf('\u201c', StringComparison.Ordinal) > -1)
|
||||||
{
|
{
|
||||||
buffer = buffer.Replace('\u201c', '\"'); // left double quotation mark
|
buffer = buffer.Replace('\u201c', '\"'); // left double quotation mark
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buffer.IndexOf('\u201d') > -1)
|
if (buffer.IndexOf('\u201d', StringComparison.Ordinal) > -1)
|
||||||
{
|
{
|
||||||
buffer = buffer.Replace('\u201d', '\"'); // right double quotation mark
|
buffer = buffer.Replace('\u201d', '\"'); // right double quotation mark
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buffer.IndexOf('\u201e') > -1)
|
if (buffer.IndexOf('\u201e', StringComparison.Ordinal) > -1)
|
||||||
{
|
{
|
||||||
buffer = buffer.Replace('\u201e', '\"'); // double low-9 quotation mark
|
buffer = buffer.Replace('\u201e', '\"'); // double low-9 quotation mark
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buffer.IndexOf('\u2026') > -1)
|
if (buffer.IndexOf('\u2026', StringComparison.Ordinal) > -1)
|
||||||
{
|
{
|
||||||
buffer = buffer.Replace("\u2026", "..."); // horizontal ellipsis
|
buffer = buffer.Replace("\u2026", "...", StringComparison.Ordinal); // horizontal ellipsis
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buffer.IndexOf('\u2032') > -1)
|
if (buffer.IndexOf('\u2032', StringComparison.Ordinal) > -1)
|
||||||
{
|
{
|
||||||
buffer = buffer.Replace('\u2032', '\''); // prime
|
buffer = buffer.Replace('\u2032', '\''); // prime
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buffer.IndexOf('\u2033') > -1)
|
if (buffer.IndexOf('\u2033', StringComparison.Ordinal) > -1)
|
||||||
{
|
{
|
||||||
buffer = buffer.Replace('\u2033', '\"'); // double prime
|
buffer = buffer.Replace('\u2033', '\"'); // double prime
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buffer.IndexOf('\u0060') > -1)
|
if (buffer.IndexOf('\u0060', StringComparison.Ordinal) > -1)
|
||||||
{
|
{
|
||||||
buffer = buffer.Replace('\u0060', '\''); // grave accent
|
buffer = buffer.Replace('\u0060', '\''); // grave accent
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buffer.IndexOf('\u00B4') > -1)
|
if (buffer.IndexOf('\u00B4', StringComparison.Ordinal) > -1)
|
||||||
{
|
{
|
||||||
buffer = buffer.Replace('\u00B4', '\''); // acute accent
|
buffer = buffer.Replace('\u00B4', '\''); // acute accent
|
||||||
}
|
}
|
||||||
@ -3000,7 +3000,6 @@ namespace Emby.Server.Implementations.Data
|
|||||||
{
|
{
|
||||||
connection.RunInTransaction(db =>
|
connection.RunInTransaction(db =>
|
||||||
{
|
{
|
||||||
|
|
||||||
var statements = PrepareAll(db, statementTexts).ToList();
|
var statements = PrepareAll(db, statementTexts).ToList();
|
||||||
|
|
||||||
if (!isReturningZeroItems)
|
if (!isReturningZeroItems)
|
||||||
@ -4670,8 +4669,12 @@ namespace Emby.Server.Implementations.Data
|
|||||||
|
|
||||||
if (query.BlockUnratedItems.Length > 1)
|
if (query.BlockUnratedItems.Length > 1)
|
||||||
{
|
{
|
||||||
var inClause = string.Join(",", query.BlockUnratedItems.Select(i => "'" + i.ToString() + "'"));
|
var inClause = string.Join(',', query.BlockUnratedItems.Select(i => "'" + i.ToString() + "'"));
|
||||||
whereClauses.Add(string.Format("(InheritedParentalRatingValue > 0 or UnratedType not in ({0}))", inClause));
|
whereClauses.Add(
|
||||||
|
string.Format(
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
"(InheritedParentalRatingValue > 0 or UnratedType not in ({0}))",
|
||||||
|
inClause));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.ExcludeInheritedTags.Length > 0)
|
if (query.ExcludeInheritedTags.Length > 0)
|
||||||
@ -4680,7 +4683,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
if (statement == null)
|
if (statement == null)
|
||||||
{
|
{
|
||||||
int index = 0;
|
int index = 0;
|
||||||
string excludedTags = string.Join(",", query.ExcludeInheritedTags.Select(t => paramName + index++));
|
string excludedTags = string.Join(',', query.ExcludeInheritedTags.Select(t => paramName + index++));
|
||||||
whereClauses.Add("((select CleanValue from itemvalues where ItemId=Guid and Type=6 and cleanvalue in (" + excludedTags + ")) is null)");
|
whereClauses.Add("((select CleanValue from itemvalues where ItemId=Guid and Type=6 and cleanvalue in (" + excludedTags + ")) is null)");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -449,7 +449,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||||||
if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
httpRes.StatusCode = 200;
|
httpRes.StatusCode = 200;
|
||||||
foreach(var (key, value) in GetDefaultCorsHeaders(httpReq))
|
foreach (var (key, value) in GetDefaultCorsHeaders(httpReq))
|
||||||
{
|
{
|
||||||
httpRes.Headers.Add(key, value);
|
httpRes.Headers.Add(key, value);
|
||||||
}
|
}
|
||||||
@ -486,7 +486,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||||||
var handler = GetServiceHandler(httpReq);
|
var handler = GetServiceHandler(httpReq);
|
||||||
if (handler != null)
|
if (handler != null)
|
||||||
{
|
{
|
||||||
await handler.ProcessRequestAsync(this, httpReq, httpRes, _logger, cancellationToken).ConfigureAwait(false);
|
await handler.ProcessRequestAsync(this, httpReq, httpRes, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -21,6 +21,7 @@ namespace Emby.Server.Implementations.IO
|
|||||||
private readonly List<string> _affectedPaths = new List<string>();
|
private readonly List<string> _affectedPaths = new List<string>();
|
||||||
private readonly object _timerLock = new object();
|
private readonly object _timerLock = new object();
|
||||||
private Timer _timer;
|
private Timer _timer;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger)
|
public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger)
|
||||||
{
|
{
|
||||||
@ -213,11 +214,11 @@ namespace Emby.Server.Implementations.IO
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool _disposed;
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
DisposeTimer();
|
DisposeTimer();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,21 @@ namespace Emby.Server.Implementations.Library
|
|||||||
{
|
{
|
||||||
"**/small.jpg",
|
"**/small.jpg",
|
||||||
"**/albumart.jpg",
|
"**/albumart.jpg",
|
||||||
"**/*sample*",
|
|
||||||
|
// We have neither non-greedy matching or character group repetitions, working around that here.
|
||||||
|
// https://github.com/dazinator/DotNet.Glob#patterns
|
||||||
|
// .*/sample\..{1,5}
|
||||||
|
"**/sample.?",
|
||||||
|
"**/sample.??",
|
||||||
|
"**/sample.???", // Matches sample.mkv
|
||||||
|
"**/sample.????", // Matches sample.webm
|
||||||
|
"**/sample.?????",
|
||||||
|
"**/*.sample.?",
|
||||||
|
"**/*.sample.??",
|
||||||
|
"**/*.sample.???",
|
||||||
|
"**/*.sample.????",
|
||||||
|
"**/*.sample.?????",
|
||||||
|
"**/sample/*",
|
||||||
|
|
||||||
// Directories
|
// Directories
|
||||||
"**/metadata/**",
|
"**/metadata/**",
|
||||||
@ -64,10 +78,13 @@ namespace Emby.Server.Implementations.Library
|
|||||||
"**/.grab/**",
|
"**/.grab/**",
|
||||||
"**/.grab",
|
"**/.grab",
|
||||||
|
|
||||||
// Unix hidden files and directories
|
// Unix hidden files
|
||||||
"**/.*/**",
|
|
||||||
"**/.*",
|
"**/.*",
|
||||||
|
|
||||||
|
// Mac - if you ever remove the above.
|
||||||
|
// "**/._*",
|
||||||
|
// "**/.DS_Store",
|
||||||
|
|
||||||
// thumbs.db
|
// thumbs.db
|
||||||
"**/thumbs.db",
|
"**/thumbs.db",
|
||||||
|
|
||||||
|
@ -46,8 +46,6 @@ namespace Emby.Server.Implementations.Library
|
|||||||
private readonly Dictionary<string, ILiveStream> _openStreams = new Dictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
|
private readonly Dictionary<string, ILiveStream> _openStreams = new Dictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
|
||||||
private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
|
private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
|
||||||
|
|
||||||
private readonly object _disposeLock = new object();
|
|
||||||
|
|
||||||
private IMediaSourceProvider[] _providers;
|
private IMediaSourceProvider[] _providers;
|
||||||
|
|
||||||
public MediaSourceManager(
|
public MediaSourceManager(
|
||||||
@ -623,12 +621,14 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
if (liveStreamInfo is IDirectStreamProvider)
|
if (liveStreamInfo is IDirectStreamProvider)
|
||||||
{
|
{
|
||||||
var info = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest
|
var info = await _mediaEncoder.GetMediaInfo(
|
||||||
{
|
new MediaInfoRequest
|
||||||
MediaSource = mediaSource,
|
{
|
||||||
ExtractChapters = false,
|
MediaSource = mediaSource,
|
||||||
MediaType = DlnaProfileType.Video
|
ExtractChapters = false,
|
||||||
}, cancellationToken).ConfigureAwait(false);
|
MediaType = DlnaProfileType.Video
|
||||||
|
},
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
mediaSource.MediaStreams = info.MediaStreams;
|
mediaSource.MediaStreams = info.MediaStreams;
|
||||||
mediaSource.Container = info.Container;
|
mediaSource.Container = info.Container;
|
||||||
@ -859,11 +859,11 @@ namespace Emby.Server.Implementations.Library
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Tuple<IMediaSourceProvider, string> GetProvider(string key)
|
private (IMediaSourceProvider, string) GetProvider(string key)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(key))
|
if (string.IsNullOrEmpty(key))
|
||||||
{
|
{
|
||||||
throw new ArgumentException("key");
|
throw new ArgumentException("Key can't be empty.", nameof(key));
|
||||||
}
|
}
|
||||||
|
|
||||||
var keys = key.Split(new[] { LiveStreamIdDelimeter }, 2);
|
var keys = key.Split(new[] { LiveStreamIdDelimeter }, 2);
|
||||||
@ -873,7 +873,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
var splitIndex = key.IndexOf(LiveStreamIdDelimeter, StringComparison.Ordinal);
|
var splitIndex = key.IndexOf(LiveStreamIdDelimeter, StringComparison.Ordinal);
|
||||||
var keyId = key.Substring(splitIndex + 1);
|
var keyId = key.Substring(splitIndex + 1);
|
||||||
|
|
||||||
return new Tuple<IMediaSourceProvider, string>(provider, keyId);
|
return (provider, keyId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -893,15 +893,12 @@ namespace Emby.Server.Implementations.Library
|
|||||||
{
|
{
|
||||||
if (dispose)
|
if (dispose)
|
||||||
{
|
{
|
||||||
lock (_disposeLock)
|
foreach (var key in _openStreams.Keys.ToList())
|
||||||
{
|
{
|
||||||
foreach (var key in _openStreams.Keys.ToList())
|
CloseLiveStream(key).GetAwaiter().GetResult();
|
||||||
{
|
|
||||||
var task = CloseLiveStream(key);
|
|
||||||
|
|
||||||
Task.WaitAll(task);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_liveStreamSemaphore.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class HdHomerunManager : IDisposable
|
public sealed class HdHomerunManager : IDisposable
|
||||||
{
|
{
|
||||||
public const int HdHomeRunPort = 65001;
|
public const int HdHomeRunPort = 65001;
|
||||||
|
|
||||||
@ -105,6 +105,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||||||
StopStreaming(socket).GetAwaiter().GetResult();
|
StopStreaming(socket).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken)
|
public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken)
|
||||||
@ -162,7 +164,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||||||
}
|
}
|
||||||
|
|
||||||
_activeTuner = i;
|
_activeTuner = i;
|
||||||
var lockKeyString = string.Format("{0:d}", lockKeyValue);
|
var lockKeyString = string.Format(CultureInfo.InvariantCulture, "{0:d}", lockKeyValue);
|
||||||
var lockkeyMsg = CreateSetMessage(i, "lockkey", lockKeyString, null);
|
var lockkeyMsg = CreateSetMessage(i, "lockkey", lockKeyString, null);
|
||||||
await stream.WriteAsync(lockkeyMsg, 0, lockkeyMsg.Length, cancellationToken).ConfigureAwait(false);
|
await stream.WriteAsync(lockkeyMsg, 0, lockkeyMsg.Length, cancellationToken).ConfigureAwait(false);
|
||||||
int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
|
int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
|
||||||
@ -173,8 +175,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var commandList = commands.GetCommands();
|
foreach (var command in commands.GetCommands())
|
||||||
foreach (var command in commandList)
|
|
||||||
{
|
{
|
||||||
var channelMsg = CreateSetMessage(i, command.Item1, command.Item2, lockKeyValue);
|
var channelMsg = CreateSetMessage(i, command.Item1, command.Item2, lockKeyValue);
|
||||||
await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false);
|
await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false);
|
||||||
@ -188,7 +189,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var targetValue = string.Format("rtp://{0}:{1}", localIp, localPort);
|
var targetValue = string.Format(CultureInfo.InvariantCulture, "rtp://{0}:{1}", localIp, localPort);
|
||||||
var targetMsg = CreateSetMessage(i, "target", targetValue, lockKeyValue);
|
var targetMsg = CreateSetMessage(i, "target", targetValue, lockKeyValue);
|
||||||
|
|
||||||
await stream.WriteAsync(targetMsg, 0, targetMsg.Length, cancellationToken).ConfigureAwait(false);
|
await stream.WriteAsync(targetMsg, 0, targetMsg.Length, cancellationToken).ConfigureAwait(false);
|
||||||
|
@ -92,7 +92,7 @@
|
|||||||
"HeaderRecordingGroups": "錄製組",
|
"HeaderRecordingGroups": "錄製組",
|
||||||
"Inherit": "繼承",
|
"Inherit": "繼承",
|
||||||
"SubtitleDownloadFailureFromForItem": "無法為 {1} 從 {0} 下載字幕",
|
"SubtitleDownloadFailureFromForItem": "無法為 {1} 從 {0} 下載字幕",
|
||||||
"TaskDownloadMissingSubtitlesDescription": "在網路上透過描述資料搜尋遺失的字幕。",
|
"TaskDownloadMissingSubtitlesDescription": "在網路上透過中繼資料搜尋遺失的字幕。",
|
||||||
"TaskDownloadMissingSubtitles": "下載遺失的字幕",
|
"TaskDownloadMissingSubtitles": "下載遺失的字幕",
|
||||||
"TaskRefreshChannels": "重新整理頻道",
|
"TaskRefreshChannels": "重新整理頻道",
|
||||||
"TaskUpdatePlugins": "更新插件",
|
"TaskUpdatePlugins": "更新插件",
|
||||||
|
@ -15,13 +15,11 @@ namespace Emby.Server.Implementations.Net
|
|||||||
public sealed class UdpSocket : ISocket, IDisposable
|
public sealed class UdpSocket : ISocket, IDisposable
|
||||||
{
|
{
|
||||||
private Socket _socket;
|
private Socket _socket;
|
||||||
private int _localPort;
|
private readonly int _localPort;
|
||||||
private bool _disposed = false;
|
private bool _disposed = false;
|
||||||
|
|
||||||
public Socket Socket => _socket;
|
public Socket Socket => _socket;
|
||||||
|
|
||||||
public IPAddress LocalIPAddress { get; }
|
|
||||||
|
|
||||||
private readonly SocketAsyncEventArgs _receiveSocketAsyncEventArgs = new SocketAsyncEventArgs()
|
private readonly SocketAsyncEventArgs _receiveSocketAsyncEventArgs = new SocketAsyncEventArgs()
|
||||||
{
|
{
|
||||||
SocketFlags = SocketFlags.None
|
SocketFlags = SocketFlags.None
|
||||||
@ -51,18 +49,33 @@ namespace Emby.Server.Implementations.Net
|
|||||||
InitReceiveSocketAsyncEventArgs();
|
InitReceiveSocketAsyncEventArgs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public UdpSocket(Socket socket, IPEndPoint endPoint)
|
||||||
|
{
|
||||||
|
if (socket == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(socket));
|
||||||
|
}
|
||||||
|
|
||||||
|
_socket = socket;
|
||||||
|
_socket.Connect(endPoint);
|
||||||
|
|
||||||
|
InitReceiveSocketAsyncEventArgs();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IPAddress LocalIPAddress { get; }
|
||||||
|
|
||||||
private void InitReceiveSocketAsyncEventArgs()
|
private void InitReceiveSocketAsyncEventArgs()
|
||||||
{
|
{
|
||||||
var receiveBuffer = new byte[8192];
|
var receiveBuffer = new byte[8192];
|
||||||
_receiveSocketAsyncEventArgs.SetBuffer(receiveBuffer, 0, receiveBuffer.Length);
|
_receiveSocketAsyncEventArgs.SetBuffer(receiveBuffer, 0, receiveBuffer.Length);
|
||||||
_receiveSocketAsyncEventArgs.Completed += _receiveSocketAsyncEventArgs_Completed;
|
_receiveSocketAsyncEventArgs.Completed += OnReceiveSocketAsyncEventArgsCompleted;
|
||||||
|
|
||||||
var sendBuffer = new byte[8192];
|
var sendBuffer = new byte[8192];
|
||||||
_sendSocketAsyncEventArgs.SetBuffer(sendBuffer, 0, sendBuffer.Length);
|
_sendSocketAsyncEventArgs.SetBuffer(sendBuffer, 0, sendBuffer.Length);
|
||||||
_sendSocketAsyncEventArgs.Completed += _sendSocketAsyncEventArgs_Completed;
|
_sendSocketAsyncEventArgs.Completed += OnSendSocketAsyncEventArgsCompleted;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void _receiveSocketAsyncEventArgs_Completed(object sender, SocketAsyncEventArgs e)
|
private void OnReceiveSocketAsyncEventArgsCompleted(object sender, SocketAsyncEventArgs e)
|
||||||
{
|
{
|
||||||
var tcs = _currentReceiveTaskCompletionSource;
|
var tcs = _currentReceiveTaskCompletionSource;
|
||||||
if (tcs != null)
|
if (tcs != null)
|
||||||
@ -86,7 +99,7 @@ namespace Emby.Server.Implementations.Net
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void _sendSocketAsyncEventArgs_Completed(object sender, SocketAsyncEventArgs e)
|
private void OnSendSocketAsyncEventArgsCompleted(object sender, SocketAsyncEventArgs e)
|
||||||
{
|
{
|
||||||
var tcs = _currentSendTaskCompletionSource;
|
var tcs = _currentSendTaskCompletionSource;
|
||||||
if (tcs != null)
|
if (tcs != null)
|
||||||
@ -104,19 +117,6 @@ namespace Emby.Server.Implementations.Net
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public UdpSocket(Socket socket, IPEndPoint endPoint)
|
|
||||||
{
|
|
||||||
if (socket == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(socket));
|
|
||||||
}
|
|
||||||
|
|
||||||
_socket = socket;
|
|
||||||
_socket.Connect(endPoint);
|
|
||||||
|
|
||||||
InitReceiveSocketAsyncEventArgs();
|
|
||||||
}
|
|
||||||
|
|
||||||
public IAsyncResult BeginReceive(byte[] buffer, int offset, int count, AsyncCallback callback)
|
public IAsyncResult BeginReceive(byte[] buffer, int offset, int count, AsyncCallback callback)
|
||||||
{
|
{
|
||||||
ThrowIfDisposed();
|
ThrowIfDisposed();
|
||||||
@ -247,6 +247,7 @@ namespace Emby.Server.Implementations.Net
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
@ -255,6 +256,8 @@ namespace Emby.Server.Implementations.Net
|
|||||||
}
|
}
|
||||||
|
|
||||||
_socket?.Dispose();
|
_socket?.Dispose();
|
||||||
|
_receiveSocketAsyncEventArgs.Dispose();
|
||||||
|
_sendSocketAsyncEventArgs.Dispose();
|
||||||
_currentReceiveTaskCompletionSource?.TrySetCanceled();
|
_currentReceiveTaskCompletionSource?.TrySetCanceled();
|
||||||
_currentSendTaskCompletionSource?.TrySetCanceled();
|
_currentSendTaskCompletionSource?.TrySetCanceled();
|
||||||
|
|
||||||
|
@ -349,16 +349,14 @@ namespace Emby.Server.Implementations.Playlists
|
|||||||
AlbumTitle = child.Album
|
AlbumTitle = child.Album
|
||||||
};
|
};
|
||||||
|
|
||||||
var hasAlbumArtist = child as IHasAlbumArtist;
|
if (child is IHasAlbumArtist hasAlbumArtist)
|
||||||
if (hasAlbumArtist != null)
|
|
||||||
{
|
{
|
||||||
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
|
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasArtist = child as IHasArtist;
|
if (child is IHasArtist hasArtist)
|
||||||
if (hasArtist != null)
|
|
||||||
{
|
{
|
||||||
entry.TrackArtist = hasArtist.Artists.FirstOrDefault();
|
entry.TrackArtist = hasArtist.Artists.Count > 0 ? hasArtist.Artists[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (child.RunTimeTicks.HasValue)
|
if (child.RunTimeTicks.HasValue)
|
||||||
@ -385,16 +383,14 @@ namespace Emby.Server.Implementations.Playlists
|
|||||||
AlbumTitle = child.Album
|
AlbumTitle = child.Album
|
||||||
};
|
};
|
||||||
|
|
||||||
var hasAlbumArtist = child as IHasAlbumArtist;
|
if (child is IHasAlbumArtist hasAlbumArtist)
|
||||||
if (hasAlbumArtist != null)
|
|
||||||
{
|
{
|
||||||
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
|
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasArtist = child as IHasArtist;
|
if (child is IHasArtist hasArtist)
|
||||||
if (hasArtist != null)
|
|
||||||
{
|
{
|
||||||
entry.TrackArtist = hasArtist.Artists.FirstOrDefault();
|
entry.TrackArtist = hasArtist.Artists.Count > 0 ? hasArtist.Artists[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (child.RunTimeTicks.HasValue)
|
if (child.RunTimeTicks.HasValue)
|
||||||
@ -411,8 +407,10 @@ namespace Emby.Server.Implementations.Playlists
|
|||||||
|
|
||||||
if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var playlist = new M3uPlaylist();
|
var playlist = new M3uPlaylist
|
||||||
playlist.IsExtended = true;
|
{
|
||||||
|
IsExtended = true
|
||||||
|
};
|
||||||
foreach (var child in item.GetLinkedChildren())
|
foreach (var child in item.GetLinkedChildren())
|
||||||
{
|
{
|
||||||
var entry = new M3uPlaylistEntry()
|
var entry = new M3uPlaylistEntry()
|
||||||
@ -422,10 +420,9 @@ namespace Emby.Server.Implementations.Playlists
|
|||||||
Album = child.Album
|
Album = child.Album
|
||||||
};
|
};
|
||||||
|
|
||||||
var hasAlbumArtist = child as IHasAlbumArtist;
|
if (child is IHasAlbumArtist hasAlbumArtist)
|
||||||
if (hasAlbumArtist != null)
|
|
||||||
{
|
{
|
||||||
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
|
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (child.RunTimeTicks.HasValue)
|
if (child.RunTimeTicks.HasValue)
|
||||||
@ -453,10 +450,9 @@ namespace Emby.Server.Implementations.Playlists
|
|||||||
Album = child.Album
|
Album = child.Album
|
||||||
};
|
};
|
||||||
|
|
||||||
var hasAlbumArtist = child as IHasAlbumArtist;
|
if (child is IHasAlbumArtist hasAlbumArtist)
|
||||||
if (hasAlbumArtist != null)
|
|
||||||
{
|
{
|
||||||
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
|
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (child.RunTimeTicks.HasValue)
|
if (child.RunTimeTicks.HasValue)
|
||||||
@ -514,7 +510,7 @@ namespace Emby.Server.Implementations.Playlists
|
|||||||
|
|
||||||
if (!folderPath.EndsWith(Path.DirectorySeparatorChar))
|
if (!folderPath.EndsWith(Path.DirectorySeparatorChar))
|
||||||
{
|
{
|
||||||
folderPath = folderPath + Path.DirectorySeparatorChar;
|
folderPath += Path.DirectorySeparatorChar;
|
||||||
}
|
}
|
||||||
|
|
||||||
var folderUri = new Uri(folderPath);
|
var folderUri = new Uri(folderPath);
|
||||||
@ -537,32 +533,12 @@ namespace Emby.Server.Implementations.Playlists
|
|||||||
return relativePath;
|
return relativePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string UnEscape(string content)
|
|
||||||
{
|
|
||||||
if (content == null)
|
|
||||||
{
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
return content.Replace("&", "&").Replace("'", "'").Replace(""", "\"").Replace(">", ">").Replace("<", "<");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string Escape(string content)
|
|
||||||
{
|
|
||||||
if (content == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return content.Replace("&", "&").Replace("'", "'").Replace("\"", """).Replace(">", ">").Replace("<", "<");
|
|
||||||
}
|
|
||||||
|
|
||||||
public Folder GetPlaylistsFolder(Guid userId)
|
public Folder GetPlaylistsFolder(Guid userId)
|
||||||
{
|
{
|
||||||
var typeName = "PlaylistsFolder";
|
const string TypeName = "PlaylistsFolder";
|
||||||
|
|
||||||
return _libraryManager.RootFolder.Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, typeName, StringComparison.Ordinal)) ??
|
return _libraryManager.RootFolder.Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, TypeName, StringComparison.Ordinal)) ??
|
||||||
_libraryManager.GetUserRootFolder().Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, typeName, StringComparison.Ordinal));
|
_libraryManager.GetUserRootFolder().Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, TypeName, StringComparison.Ordinal));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Emby.Server.Implementations.HttpServer;
|
using Emby.Server.Implementations.HttpServer;
|
||||||
using MediaBrowser.Model.Services;
|
using MediaBrowser.Model.Services;
|
||||||
@ -91,12 +92,22 @@ namespace Emby.Server.Implementations.Services
|
|||||||
{
|
{
|
||||||
if (restPath.Path[0] != '/')
|
if (restPath.Path[0] != '/')
|
||||||
{
|
{
|
||||||
throw new ArgumentException(string.Format("Route '{0}' on '{1}' must start with a '/'", restPath.Path, restPath.RequestType.GetMethodName()));
|
throw new ArgumentException(
|
||||||
|
string.Format(
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
"Route '{0}' on '{1}' must start with a '/'",
|
||||||
|
restPath.Path,
|
||||||
|
restPath.RequestType.GetMethodName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (restPath.Path.IndexOfAny(InvalidRouteChars) != -1)
|
if (restPath.Path.IndexOfAny(InvalidRouteChars) != -1)
|
||||||
{
|
{
|
||||||
throw new ArgumentException(string.Format("Route '{0}' on '{1}' contains invalid chars. ", restPath.Path, restPath.RequestType.GetMethodName()));
|
throw new ArgumentException(
|
||||||
|
string.Format(
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
"Route '{0}' on '{1}' contains invalid chars. ",
|
||||||
|
restPath.Path,
|
||||||
|
restPath.RequestType.GetMethodName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (RestPathMap.TryGetValue(restPath.FirstMatchHashKey, out List<RestPath> pathsAtFirstMatch))
|
if (RestPathMap.TryGetValue(restPath.FirstMatchHashKey, out List<RestPath> pathsAtFirstMatch))
|
||||||
@ -179,8 +190,7 @@ namespace Emby.Server.Implementations.Services
|
|||||||
|
|
||||||
var service = httpHost.CreateInstance(serviceType);
|
var service = httpHost.CreateInstance(serviceType);
|
||||||
|
|
||||||
var serviceRequiresContext = service as IRequiresRequest;
|
if (service is IRequiresRequest serviceRequiresContext)
|
||||||
if (serviceRequiresContext != null)
|
|
||||||
{
|
{
|
||||||
serviceRequiresContext.Request = req;
|
serviceRequiresContext.Request = req;
|
||||||
}
|
}
|
||||||
|
@ -71,7 +71,7 @@ namespace Emby.Server.Implementations.Services
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ProcessRequestAsync(HttpListenerHost httpHost, IRequest httpReq, HttpResponse httpRes, ILogger logger, CancellationToken cancellationToken)
|
public async Task ProcessRequestAsync(HttpListenerHost httpHost, IRequest httpReq, HttpResponse httpRes, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
httpReq.Items["__route"] = _restPath;
|
httpReq.Items["__route"] = _restPath;
|
||||||
|
|
||||||
@ -80,10 +80,10 @@ namespace Emby.Server.Implementations.Services
|
|||||||
httpReq.ResponseContentType = _responseContentType;
|
httpReq.ResponseContentType = _responseContentType;
|
||||||
}
|
}
|
||||||
|
|
||||||
var request = await CreateRequest(httpHost, httpReq, _restPath, logger).ConfigureAwait(false);
|
var request = await CreateRequest(httpHost, httpReq, _restPath).ConfigureAwait(false);
|
||||||
|
|
||||||
httpHost.ApplyRequestFilters(httpReq, httpRes, request);
|
httpHost.ApplyRequestFilters(httpReq, httpRes, request);
|
||||||
|
|
||||||
httpRes.HttpContext.SetServiceStackRequest(httpReq);
|
httpRes.HttpContext.SetServiceStackRequest(httpReq);
|
||||||
var response = await httpHost.ServiceController.Execute(httpHost, request, httpReq).ConfigureAwait(false);
|
var response = await httpHost.ServiceController.Execute(httpHost, request, httpReq).ConfigureAwait(false);
|
||||||
|
|
||||||
@ -96,7 +96,7 @@ namespace Emby.Server.Implementations.Services
|
|||||||
await ResponseHelper.WriteToResponse(httpRes, httpReq, response, cancellationToken).ConfigureAwait(false);
|
await ResponseHelper.WriteToResponse(httpRes, httpReq, response, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<object> CreateRequest(HttpListenerHost host, IRequest httpReq, RestPath restPath, ILogger logger)
|
public static async Task<object> CreateRequest(HttpListenerHost host, IRequest httpReq, RestPath restPath)
|
||||||
{
|
{
|
||||||
var requestType = restPath.RequestType;
|
var requestType = restPath.RequestType;
|
||||||
|
|
||||||
|
@ -848,8 +848,8 @@ namespace Emby.Server.Implementations.Session
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="info">The info.</param>
|
/// <param name="info">The info.</param>
|
||||||
/// <returns>Task.</returns>
|
/// <returns>Task.</returns>
|
||||||
/// <exception cref="ArgumentNullException">info</exception>
|
/// <exception cref="ArgumentNullException"><c>info</c> is <c>null</c>.</exception>
|
||||||
/// <exception cref="ArgumentOutOfRangeException">positionTicks</exception>
|
/// <exception cref="ArgumentOutOfRangeException"><c>info.PositionTicks</c> is <c>null</c> or negative.</exception>
|
||||||
public async Task OnPlaybackStopped(PlaybackStopInfo info)
|
public async Task OnPlaybackStopped(PlaybackStopInfo info)
|
||||||
{
|
{
|
||||||
CheckDisposed();
|
CheckDisposed();
|
||||||
|
@ -93,7 +93,7 @@ namespace Emby.Server.Implementations.Session
|
|||||||
if (session != null)
|
if (session != null)
|
||||||
{
|
{
|
||||||
EnsureController(session, e.Argument);
|
EnsureController(session, e.Argument);
|
||||||
await KeepAliveWebSocket(e.Argument);
|
await KeepAliveWebSocket(e.Argument).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -177,7 +177,7 @@ namespace Emby.Server.Implementations.Session
|
|||||||
// Notify WebSocket about timeout
|
// Notify WebSocket about timeout
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await SendForceKeepAlive(webSocket);
|
await SendForceKeepAlive(webSocket).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (WebSocketException exception)
|
catch (WebSocketException exception)
|
||||||
{
|
{
|
||||||
@ -233,6 +233,7 @@ namespace Emby.Server.Implementations.Session
|
|||||||
if (_keepAliveCancellationToken != null)
|
if (_keepAliveCancellationToken != null)
|
||||||
{
|
{
|
||||||
_keepAliveCancellationToken.Cancel();
|
_keepAliveCancellationToken.Cancel();
|
||||||
|
_keepAliveCancellationToken.Dispose();
|
||||||
_keepAliveCancellationToken = null;
|
_keepAliveCancellationToken = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -268,7 +269,7 @@ namespace Emby.Server.Implementations.Session
|
|||||||
lost = _webSockets.Where(i => (DateTime.UtcNow - i.LastKeepAliveDate).TotalSeconds >= WebSocketLostTimeout).ToList();
|
lost = _webSockets.Where(i => (DateTime.UtcNow - i.LastKeepAliveDate).TotalSeconds >= WebSocketLostTimeout).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inactive.Any())
|
if (inactive.Count > 0)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Sending ForceKeepAlive message to {0} inactive WebSockets.", inactive.Count);
|
_logger.LogInformation("Sending ForceKeepAlive message to {0} inactive WebSockets.", inactive.Count);
|
||||||
}
|
}
|
||||||
@ -277,7 +278,7 @@ namespace Emby.Server.Implementations.Session
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await SendForceKeepAlive(webSocket);
|
await SendForceKeepAlive(webSocket).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (WebSocketException exception)
|
catch (WebSocketException exception)
|
||||||
{
|
{
|
||||||
@ -288,7 +289,7 @@ namespace Emby.Server.Implementations.Session
|
|||||||
|
|
||||||
lock (_webSocketsLock)
|
lock (_webSocketsLock)
|
||||||
{
|
{
|
||||||
if (lost.Any())
|
if (lost.Count > 0)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Lost {0} WebSockets.", lost.Count);
|
_logger.LogInformation("Lost {0} WebSockets.", lost.Count);
|
||||||
foreach (var webSocket in lost)
|
foreach (var webSocket in lost)
|
||||||
@ -298,7 +299,7 @@ namespace Emby.Server.Implementations.Session
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_webSockets.Any())
|
if (_webSockets.Count == 0)
|
||||||
{
|
{
|
||||||
StopKeepAlive();
|
StopKeepAlive();
|
||||||
}
|
}
|
||||||
@ -312,11 +313,13 @@ namespace Emby.Server.Implementations.Session
|
|||||||
/// <returns>Task.</returns>
|
/// <returns>Task.</returns>
|
||||||
private Task SendForceKeepAlive(IWebSocketConnection webSocket)
|
private Task SendForceKeepAlive(IWebSocketConnection webSocket)
|
||||||
{
|
{
|
||||||
return webSocket.SendAsync(new WebSocketMessage<int>
|
return webSocket.SendAsync(
|
||||||
{
|
new WebSocketMessage<int>
|
||||||
MessageType = "ForceKeepAlive",
|
{
|
||||||
Data = WebSocketLostTimeout
|
MessageType = "ForceKeepAlive",
|
||||||
}, CancellationToken.None);
|
Data = WebSocketLostTimeout
|
||||||
|
},
|
||||||
|
CancellationToken.None);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -330,12 +333,11 @@ namespace Emby.Server.Implementations.Session
|
|||||||
{
|
{
|
||||||
while (!cancellationToken.IsCancellationRequested)
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
await callback();
|
await callback().ConfigureAwait(false);
|
||||||
Task task = Task.Delay(interval, cancellationToken);
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await task;
|
await Task.Delay(interval, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (TaskCanceledException)
|
catch (TaskCanceledException)
|
||||||
{
|
{
|
||||||
|
@ -154,8 +154,8 @@ namespace Emby.Server.Implementations.Sorting
|
|||||||
|
|
||||||
private static int CompareEpisodes(Episode x, Episode y)
|
private static int CompareEpisodes(Episode x, Episode y)
|
||||||
{
|
{
|
||||||
var xValue = (x.ParentIndexNumber ?? -1) * 1000 + (x.IndexNumber ?? -1);
|
var xValue = ((x.ParentIndexNumber ?? -1) * 1000) + (x.IndexNumber ?? -1);
|
||||||
var yValue = (y.ParentIndexNumber ?? -1) * 1000 + (y.IndexNumber ?? -1);
|
var yValue = ((y.ParentIndexNumber ?? -1) * 1000) + (y.IndexNumber ?? -1);
|
||||||
|
|
||||||
return xValue.CompareTo(yValue);
|
return xValue.CompareTo(yValue);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -27,14 +28,17 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
/// All sessions will receive the message.
|
/// All sessions will receive the message.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
AllGroup = 0,
|
AllGroup = 0,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Only the specified session will receive the message.
|
/// Only the specified session will receive the message.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
CurrentSession = 1,
|
CurrentSession = 1,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// All sessions, except the current one, will receive the message.
|
/// All sessions, except the current one, will receive the message.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
AllExceptCurrentSession = 2,
|
AllExceptCurrentSession = 2,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Only sessions that are not buffering will receive the message.
|
/// Only sessions that are not buffering will receive the message.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -56,15 +60,6 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly GroupInfo _group = new GroupInfo();
|
private readonly GroupInfo _group = new GroupInfo();
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Guid GetGroupId() => _group.GroupId;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Guid GetPlayingItemId() => _group.PlayingItem.Id;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public bool IsGroupEmpty() => _group.IsEmpty();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="SyncPlayController" /> class.
|
/// Initializes a new instance of the <see cref="SyncPlayController" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -78,6 +73,15 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
_syncPlayManager = syncPlayManager;
|
_syncPlayManager = syncPlayManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Guid GetGroupId() => _group.GroupId;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Guid GetPlayingItemId() => _group.PlayingItem.Id;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool IsGroupEmpty() => _group.IsEmpty();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts DateTime to UTC string.
|
/// Converts DateTime to UTC string.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -85,7 +89,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
/// <value>The UTC string.</value>
|
/// <value>The UTC string.</value>
|
||||||
private string DateToUTCString(DateTime date)
|
private string DateToUTCString(DateTime date)
|
||||||
{
|
{
|
||||||
return date.ToUniversalTime().ToString("o");
|
return date.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -94,23 +98,23 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
/// <param name="from">The current session.</param>
|
/// <param name="from">The current session.</param>
|
||||||
/// <param name="type">The filtering type.</param>
|
/// <param name="type">The filtering type.</param>
|
||||||
/// <value>The array of sessions matching the filter.</value>
|
/// <value>The array of sessions matching the filter.</value>
|
||||||
private SessionInfo[] FilterSessions(SessionInfo from, BroadcastType type)
|
private IEnumerable<SessionInfo> FilterSessions(SessionInfo from, BroadcastType type)
|
||||||
{
|
{
|
||||||
switch (type)
|
switch (type)
|
||||||
{
|
{
|
||||||
case BroadcastType.CurrentSession:
|
case BroadcastType.CurrentSession:
|
||||||
return new SessionInfo[] { from };
|
return new SessionInfo[] { from };
|
||||||
case BroadcastType.AllGroup:
|
case BroadcastType.AllGroup:
|
||||||
return _group.Participants.Values.Select(
|
return _group.Participants.Values
|
||||||
session => session.Session).ToArray();
|
.Select(session => session.Session);
|
||||||
case BroadcastType.AllExceptCurrentSession:
|
case BroadcastType.AllExceptCurrentSession:
|
||||||
return _group.Participants.Values.Select(
|
return _group.Participants.Values
|
||||||
session => session.Session).Where(
|
.Select(session => session.Session)
|
||||||
session => !session.Id.Equals(from.Id)).ToArray();
|
.Where(session => !session.Id.Equals(from.Id, StringComparison.Ordinal));
|
||||||
case BroadcastType.AllReady:
|
case BroadcastType.AllReady:
|
||||||
return _group.Participants.Values.Where(
|
return _group.Participants.Values
|
||||||
session => !session.IsBuffering).Select(
|
.Where(session => !session.IsBuffering)
|
||||||
session => session.Session).ToArray();
|
.Select(session => session.Session);
|
||||||
default:
|
default:
|
||||||
return Array.Empty<SessionInfo>();
|
return Array.Empty<SessionInfo>();
|
||||||
}
|
}
|
||||||
@ -128,10 +132,9 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
{
|
{
|
||||||
IEnumerable<Task> GetTasks()
|
IEnumerable<Task> GetTasks()
|
||||||
{
|
{
|
||||||
SessionInfo[] sessions = FilterSessions(from, type);
|
foreach (var session in FilterSessions(from, type))
|
||||||
foreach (var session in sessions)
|
|
||||||
{
|
{
|
||||||
yield return _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), message, cancellationToken);
|
yield return _sessionManager.SendSyncPlayGroupUpdate(session.Id, message, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,10 +153,9 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
{
|
{
|
||||||
IEnumerable<Task> GetTasks()
|
IEnumerable<Task> GetTasks()
|
||||||
{
|
{
|
||||||
SessionInfo[] sessions = FilterSessions(from, type);
|
foreach (var session in FilterSessions(from, type))
|
||||||
foreach (var session in sessions)
|
|
||||||
{
|
{
|
||||||
yield return _sessionManager.SendSyncPlayCommand(session.Id.ToString(), message, cancellationToken);
|
yield return _sessionManager.SendSyncPlayCommand(session.Id, message, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,9 +238,11 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var playRequest = new PlayRequest();
|
var playRequest = new PlayRequest
|
||||||
playRequest.ItemIds = new Guid[] { _group.PlayingItem.Id };
|
{
|
||||||
playRequest.StartPositionTicks = _group.PositionTicks;
|
ItemIds = new Guid[] { _group.PlayingItem.Id },
|
||||||
|
StartPositionTicks = _group.PositionTicks
|
||||||
|
};
|
||||||
var update = NewSyncPlayGroupUpdate(GroupUpdateType.PrepareSession, playRequest);
|
var update = NewSyncPlayGroupUpdate(GroupUpdateType.PrepareSession, playRequest);
|
||||||
SendGroupUpdate(session, BroadcastType.CurrentSession, update, cancellationToken);
|
SendGroupUpdate(session, BroadcastType.CurrentSession, update, cancellationToken);
|
||||||
}
|
}
|
||||||
|
@ -19,22 +19,18 @@ namespace Jellyfin.Drawing.Skia
|
|||||||
/// <param name="percent">The percentage played to display with the indicator.</param>
|
/// <param name="percent">The percentage played to display with the indicator.</param>
|
||||||
public static void Process(SKCanvas canvas, ImageDimensions imageSize, double percent)
|
public static void Process(SKCanvas canvas, ImageDimensions imageSize, double percent)
|
||||||
{
|
{
|
||||||
using (var paint = new SKPaint())
|
using var paint = new SKPaint();
|
||||||
{
|
var endX = imageSize.Width - 1;
|
||||||
var endX = imageSize.Width - 1;
|
var endY = imageSize.Height - 1;
|
||||||
var endY = imageSize.Height - 1;
|
|
||||||
|
|
||||||
paint.Color = SKColor.Parse("#99000000");
|
paint.Color = SKColor.Parse("#99000000");
|
||||||
paint.Style = SKPaintStyle.Fill;
|
paint.Style = SKPaintStyle.Fill;
|
||||||
canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, (float)endX, (float)endY), paint);
|
canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, endX, endY), paint);
|
||||||
|
|
||||||
double foregroundWidth = endX;
|
double foregroundWidth = (endX * percent) / 100;
|
||||||
foregroundWidth *= percent;
|
|
||||||
foregroundWidth /= 100;
|
|
||||||
|
|
||||||
paint.Color = SKColor.Parse("#FF00A4DC");
|
paint.Color = SKColor.Parse("#FF00A4DC");
|
||||||
canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, Convert.ToInt32(foregroundWidth), (float)endY), paint);
|
canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, Convert.ToInt32(foregroundWidth), endY), paint);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,31 +22,27 @@ namespace Jellyfin.Drawing.Skia
|
|||||||
{
|
{
|
||||||
var x = imageSize.Width - OffsetFromTopRightCorner;
|
var x = imageSize.Width - OffsetFromTopRightCorner;
|
||||||
|
|
||||||
using (var paint = new SKPaint())
|
using var paint = new SKPaint
|
||||||
{
|
{
|
||||||
paint.Color = SKColor.Parse("#CC00A4DC");
|
Color = SKColor.Parse("#CC00A4DC"),
|
||||||
paint.Style = SKPaintStyle.Fill;
|
Style = SKPaintStyle.Fill
|
||||||
canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint);
|
};
|
||||||
}
|
|
||||||
|
|
||||||
using (var paint = new SKPaint())
|
canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint);
|
||||||
{
|
|
||||||
paint.Color = new SKColor(255, 255, 255, 255);
|
|
||||||
paint.Style = SKPaintStyle.Fill;
|
|
||||||
|
|
||||||
paint.TextSize = 30;
|
paint.Color = new SKColor(255, 255, 255, 255);
|
||||||
paint.IsAntialias = true;
|
paint.TextSize = 30;
|
||||||
|
paint.IsAntialias = true;
|
||||||
|
|
||||||
// or:
|
// or:
|
||||||
// var emojiChar = 0x1F680;
|
// var emojiChar = 0x1F680;
|
||||||
const string Text = "✔️";
|
const string Text = "✔️";
|
||||||
var emojiChar = StringUtilities.GetUnicodeCharacterCode(Text, SKTextEncoding.Utf32);
|
var emojiChar = StringUtilities.GetUnicodeCharacterCode(Text, SKTextEncoding.Utf32);
|
||||||
|
|
||||||
// ask the font manager for a font with that character
|
// ask the font manager for a font with that character
|
||||||
paint.Typeface = SKFontManager.Default.MatchCharacter(emojiChar);
|
paint.Typeface = SKFontManager.Default.MatchCharacter(emojiChar);
|
||||||
|
|
||||||
canvas.DrawText(Text, (float)x - 20, OffsetFromTopRightCorner + 12, paint);
|
canvas.DrawText(Text, (float)x - 20, OffsetFromTopRightCorner + 12, paint);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ namespace Jellyfin.Drawing.Skia
|
|||||||
/// Initializes a new instance of the <see cref="SkiaCodecException" /> class.
|
/// Initializes a new instance of the <see cref="SkiaCodecException" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="result">The non-successful codec result returned by Skia.</param>
|
/// <param name="result">The non-successful codec result returned by Skia.</param>
|
||||||
public SkiaCodecException(SKCodecResult result) : base()
|
public SkiaCodecException(SKCodecResult result)
|
||||||
{
|
{
|
||||||
CodecResult = result;
|
CodecResult = result;
|
||||||
}
|
}
|
||||||
|
@ -29,9 +29,7 @@ namespace Jellyfin.Drawing.Skia
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="logger">The application logger.</param>
|
/// <param name="logger">The application logger.</param>
|
||||||
/// <param name="appPaths">The application paths.</param>
|
/// <param name="appPaths">The application paths.</param>
|
||||||
public SkiaEncoder(
|
public SkiaEncoder(ILogger<SkiaEncoder> logger, IApplicationPaths appPaths)
|
||||||
ILogger<SkiaEncoder> logger,
|
|
||||||
IApplicationPaths appPaths)
|
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_appPaths = appPaths;
|
_appPaths = appPaths;
|
||||||
@ -102,19 +100,14 @@ namespace Jellyfin.Drawing.Skia
|
|||||||
/// <returns>The converted format.</returns>
|
/// <returns>The converted format.</returns>
|
||||||
public static SKEncodedImageFormat GetImageFormat(ImageFormat selectedFormat)
|
public static SKEncodedImageFormat GetImageFormat(ImageFormat selectedFormat)
|
||||||
{
|
{
|
||||||
switch (selectedFormat)
|
return selectedFormat switch
|
||||||
{
|
{
|
||||||
case ImageFormat.Bmp:
|
ImageFormat.Bmp => SKEncodedImageFormat.Bmp,
|
||||||
return SKEncodedImageFormat.Bmp;
|
ImageFormat.Jpg => SKEncodedImageFormat.Jpeg,
|
||||||
case ImageFormat.Jpg:
|
ImageFormat.Gif => SKEncodedImageFormat.Gif,
|
||||||
return SKEncodedImageFormat.Jpeg;
|
ImageFormat.Webp => SKEncodedImageFormat.Webp,
|
||||||
case ImageFormat.Gif:
|
_ => SKEncodedImageFormat.Png
|
||||||
return SKEncodedImageFormat.Gif;
|
};
|
||||||
case ImageFormat.Webp:
|
|
||||||
return SKEncodedImageFormat.Webp;
|
|
||||||
default:
|
|
||||||
return SKEncodedImageFormat.Png;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsTransparentRow(SKBitmap bmp, int row)
|
private static bool IsTransparentRow(SKBitmap bmp, int row)
|
||||||
@ -146,63 +139,34 @@ namespace Jellyfin.Drawing.Skia
|
|||||||
private SKBitmap CropWhiteSpace(SKBitmap bitmap)
|
private SKBitmap CropWhiteSpace(SKBitmap bitmap)
|
||||||
{
|
{
|
||||||
var topmost = 0;
|
var topmost = 0;
|
||||||
for (int row = 0; row < bitmap.Height; ++row)
|
while (topmost < bitmap.Height && IsTransparentRow(bitmap, topmost))
|
||||||
{
|
{
|
||||||
if (IsTransparentRow(bitmap, row))
|
topmost++;
|
||||||
{
|
|
||||||
topmost = row + 1;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int bottommost = bitmap.Height;
|
int bottommost = bitmap.Height;
|
||||||
for (int row = bitmap.Height - 1; row >= 0; --row)
|
while (bottommost >= 0 && IsTransparentRow(bitmap, bottommost - 1))
|
||||||
{
|
{
|
||||||
if (IsTransparentRow(bitmap, row))
|
bottommost--;
|
||||||
{
|
|
||||||
bottommost = row;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int leftmost = 0, rightmost = bitmap.Width;
|
var leftmost = 0;
|
||||||
for (int col = 0; col < bitmap.Width; ++col)
|
while (leftmost < bitmap.Width && IsTransparentColumn(bitmap, leftmost))
|
||||||
{
|
{
|
||||||
if (IsTransparentColumn(bitmap, col))
|
leftmost++;
|
||||||
{
|
|
||||||
leftmost = col + 1;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int col = bitmap.Width - 1; col >= 0; --col)
|
var rightmost = bitmap.Width;
|
||||||
|
while (rightmost >= 0 && IsTransparentColumn(bitmap, rightmost - 1))
|
||||||
{
|
{
|
||||||
if (IsTransparentColumn(bitmap, col))
|
rightmost--;
|
||||||
{
|
|
||||||
rightmost = col;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var newRect = SKRectI.Create(leftmost, topmost, rightmost - leftmost, bottommost - topmost);
|
var newRect = SKRectI.Create(leftmost, topmost, rightmost - leftmost, bottommost - topmost);
|
||||||
|
|
||||||
using (var image = SKImage.FromBitmap(bitmap))
|
using var image = SKImage.FromBitmap(bitmap);
|
||||||
using (var subset = image.Subset(newRect))
|
using var subset = image.Subset(newRect);
|
||||||
{
|
return SKBitmap.FromImage(subset);
|
||||||
return SKBitmap.FromImage(subset);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -216,14 +180,12 @@ namespace Jellyfin.Drawing.Skia
|
|||||||
throw new FileNotFoundException("File not found", path);
|
throw new FileNotFoundException("File not found", path);
|
||||||
}
|
}
|
||||||
|
|
||||||
using (var codec = SKCodec.Create(path, out SKCodecResult result))
|
using var codec = SKCodec.Create(path, out SKCodecResult result);
|
||||||
{
|
EnsureSuccess(result);
|
||||||
EnsureSuccess(result);
|
|
||||||
|
|
||||||
var info = codec.Info;
|
var info = codec.Info;
|
||||||
|
|
||||||
return new ImageDimensions(info.Width, info.Height);
|
return new ImageDimensions(info.Width, info.Height);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -253,12 +215,7 @@ namespace Jellyfin.Drawing.Skia
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (HasDiacritics(path))
|
return HasDiacritics(path);
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string NormalizePath(string path)
|
private string NormalizePath(string path)
|
||||||
@ -283,25 +240,17 @@ namespace Jellyfin.Drawing.Skia
|
|||||||
return SKEncodedOrigin.TopLeft;
|
return SKEncodedOrigin.TopLeft;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (orientation.Value)
|
return orientation.Value switch
|
||||||
{
|
{
|
||||||
case ImageOrientation.TopRight:
|
ImageOrientation.TopRight => SKEncodedOrigin.TopRight,
|
||||||
return SKEncodedOrigin.TopRight;
|
ImageOrientation.RightTop => SKEncodedOrigin.RightTop,
|
||||||
case ImageOrientation.RightTop:
|
ImageOrientation.RightBottom => SKEncodedOrigin.RightBottom,
|
||||||
return SKEncodedOrigin.RightTop;
|
ImageOrientation.LeftTop => SKEncodedOrigin.LeftTop,
|
||||||
case ImageOrientation.RightBottom:
|
ImageOrientation.LeftBottom => SKEncodedOrigin.LeftBottom,
|
||||||
return SKEncodedOrigin.RightBottom;
|
ImageOrientation.BottomRight => SKEncodedOrigin.BottomRight,
|
||||||
case ImageOrientation.LeftTop:
|
ImageOrientation.BottomLeft => SKEncodedOrigin.BottomLeft,
|
||||||
return SKEncodedOrigin.LeftTop;
|
_ => SKEncodedOrigin.TopLeft
|
||||||
case ImageOrientation.LeftBottom:
|
};
|
||||||
return SKEncodedOrigin.LeftBottom;
|
|
||||||
case ImageOrientation.BottomRight:
|
|
||||||
return SKEncodedOrigin.BottomRight;
|
|
||||||
case ImageOrientation.BottomLeft:
|
|
||||||
return SKEncodedOrigin.BottomLeft;
|
|
||||||
default:
|
|
||||||
return SKEncodedOrigin.TopLeft;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -323,24 +272,22 @@ namespace Jellyfin.Drawing.Skia
|
|||||||
|
|
||||||
if (requiresTransparencyHack || forceCleanBitmap)
|
if (requiresTransparencyHack || forceCleanBitmap)
|
||||||
{
|
{
|
||||||
using (var codec = SKCodec.Create(NormalizePath(path)))
|
using var codec = SKCodec.Create(NormalizePath(path));
|
||||||
|
if (codec == null)
|
||||||
{
|
{
|
||||||
if (codec == null)
|
origin = GetSKEncodedOrigin(orientation);
|
||||||
{
|
return null;
|
||||||
origin = GetSKEncodedOrigin(orientation);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// create the bitmap
|
|
||||||
var bitmap = new SKBitmap(codec.Info.Width, codec.Info.Height, !requiresTransparencyHack);
|
|
||||||
|
|
||||||
// decode
|
|
||||||
_ = codec.GetPixels(bitmap.Info, bitmap.GetPixels());
|
|
||||||
|
|
||||||
origin = codec.EncodedOrigin;
|
|
||||||
|
|
||||||
return bitmap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// create the bitmap
|
||||||
|
var bitmap = new SKBitmap(codec.Info.Width, codec.Info.Height, !requiresTransparencyHack);
|
||||||
|
|
||||||
|
// decode
|
||||||
|
_ = codec.GetPixels(bitmap.Info, bitmap.GetPixels());
|
||||||
|
|
||||||
|
origin = codec.EncodedOrigin;
|
||||||
|
|
||||||
|
return bitmap;
|
||||||
}
|
}
|
||||||
|
|
||||||
var resultBitmap = SKBitmap.Decode(NormalizePath(path));
|
var resultBitmap = SKBitmap.Decode(NormalizePath(path));
|
||||||
@ -367,15 +314,8 @@ namespace Jellyfin.Drawing.Skia
|
|||||||
{
|
{
|
||||||
if (cropWhitespace)
|
if (cropWhitespace)
|
||||||
{
|
{
|
||||||
using (var bitmap = Decode(path, forceAnalyzeBitmap, orientation, out origin))
|
using var bitmap = Decode(path, forceAnalyzeBitmap, orientation, out origin);
|
||||||
{
|
return bitmap == null ? null : CropWhiteSpace(bitmap);
|
||||||
if (bitmap == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return CropWhiteSpace(bitmap);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Decode(path, forceAnalyzeBitmap, orientation, out origin);
|
return Decode(path, forceAnalyzeBitmap, orientation, out origin);
|
||||||
@ -403,133 +343,55 @@ namespace Jellyfin.Drawing.Skia
|
|||||||
|
|
||||||
private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin)
|
private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin)
|
||||||
{
|
{
|
||||||
|
if (origin == SKEncodedOrigin.Default)
|
||||||
|
{
|
||||||
|
return bitmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
var needsFlip = origin == SKEncodedOrigin.LeftBottom
|
||||||
|
|| origin == SKEncodedOrigin.LeftTop
|
||||||
|
|| origin == SKEncodedOrigin.RightBottom
|
||||||
|
|| origin == SKEncodedOrigin.RightTop;
|
||||||
|
var rotated = needsFlip
|
||||||
|
? new SKBitmap(bitmap.Height, bitmap.Width)
|
||||||
|
: new SKBitmap(bitmap.Width, bitmap.Height);
|
||||||
|
using var surface = new SKCanvas(rotated);
|
||||||
|
var midX = (float)rotated.Width / 2;
|
||||||
|
var midY = (float)rotated.Height / 2;
|
||||||
|
|
||||||
switch (origin)
|
switch (origin)
|
||||||
{
|
{
|
||||||
case SKEncodedOrigin.TopRight:
|
case SKEncodedOrigin.TopRight:
|
||||||
{
|
surface.Scale(-1, 1, midX, midY);
|
||||||
var rotated = new SKBitmap(bitmap.Width, bitmap.Height);
|
break;
|
||||||
using (var surface = new SKCanvas(rotated))
|
|
||||||
{
|
|
||||||
surface.Translate(rotated.Width, 0);
|
|
||||||
surface.Scale(-1, 1);
|
|
||||||
surface.DrawBitmap(bitmap, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rotated;
|
|
||||||
}
|
|
||||||
|
|
||||||
case SKEncodedOrigin.BottomRight:
|
case SKEncodedOrigin.BottomRight:
|
||||||
{
|
surface.RotateDegrees(180, midX, midY);
|
||||||
var rotated = new SKBitmap(bitmap.Width, bitmap.Height);
|
break;
|
||||||
using (var surface = new SKCanvas(rotated))
|
|
||||||
{
|
|
||||||
float px = (float)bitmap.Width / 2;
|
|
||||||
float py = (float)bitmap.Height / 2;
|
|
||||||
|
|
||||||
surface.RotateDegrees(180, px, py);
|
|
||||||
surface.DrawBitmap(bitmap, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rotated;
|
|
||||||
}
|
|
||||||
|
|
||||||
case SKEncodedOrigin.BottomLeft:
|
case SKEncodedOrigin.BottomLeft:
|
||||||
{
|
surface.Scale(1, -1, midX, midY);
|
||||||
var rotated = new SKBitmap(bitmap.Width, bitmap.Height);
|
break;
|
||||||
using (var surface = new SKCanvas(rotated))
|
|
||||||
{
|
|
||||||
float px = (float)bitmap.Width / 2;
|
|
||||||
|
|
||||||
float py = (float)bitmap.Height / 2;
|
|
||||||
|
|
||||||
surface.Translate(rotated.Width, 0);
|
|
||||||
surface.Scale(-1, 1);
|
|
||||||
|
|
||||||
surface.RotateDegrees(180, px, py);
|
|
||||||
surface.DrawBitmap(bitmap, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rotated;
|
|
||||||
}
|
|
||||||
|
|
||||||
case SKEncodedOrigin.LeftTop:
|
case SKEncodedOrigin.LeftTop:
|
||||||
{
|
surface.Translate(0, -rotated.Height);
|
||||||
// TODO: Remove dual canvases, had trouble with flipping
|
surface.Scale(1, -1, midX, midY);
|
||||||
using (var rotated = new SKBitmap(bitmap.Height, bitmap.Width))
|
surface.RotateDegrees(-90);
|
||||||
{
|
break;
|
||||||
using (var surface = new SKCanvas(rotated))
|
|
||||||
{
|
|
||||||
surface.Translate(rotated.Width, 0);
|
|
||||||
|
|
||||||
surface.RotateDegrees(90);
|
|
||||||
|
|
||||||
surface.DrawBitmap(bitmap, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
var flippedBitmap = new SKBitmap(rotated.Width, rotated.Height);
|
|
||||||
using (var flippedCanvas = new SKCanvas(flippedBitmap))
|
|
||||||
{
|
|
||||||
flippedCanvas.Translate(flippedBitmap.Width, 0);
|
|
||||||
flippedCanvas.Scale(-1, 1);
|
|
||||||
flippedCanvas.DrawBitmap(rotated, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return flippedBitmap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case SKEncodedOrigin.RightTop:
|
case SKEncodedOrigin.RightTop:
|
||||||
{
|
surface.Translate(rotated.Width, 0);
|
||||||
var rotated = new SKBitmap(bitmap.Height, bitmap.Width);
|
surface.RotateDegrees(90);
|
||||||
using (var surface = new SKCanvas(rotated))
|
break;
|
||||||
{
|
|
||||||
surface.Translate(rotated.Width, 0);
|
|
||||||
surface.RotateDegrees(90);
|
|
||||||
surface.DrawBitmap(bitmap, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rotated;
|
|
||||||
}
|
|
||||||
|
|
||||||
case SKEncodedOrigin.RightBottom:
|
case SKEncodedOrigin.RightBottom:
|
||||||
{
|
surface.Translate(rotated.Width, 0);
|
||||||
// TODO: Remove dual canvases, had trouble with flipping
|
surface.Scale(1, -1, midX, midY);
|
||||||
using (var rotated = new SKBitmap(bitmap.Height, bitmap.Width))
|
surface.RotateDegrees(90);
|
||||||
{
|
break;
|
||||||
using (var surface = new SKCanvas(rotated))
|
|
||||||
{
|
|
||||||
surface.Translate(0, rotated.Height);
|
|
||||||
surface.RotateDegrees(270);
|
|
||||||
surface.DrawBitmap(bitmap, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
var flippedBitmap = new SKBitmap(rotated.Width, rotated.Height);
|
|
||||||
using (var flippedCanvas = new SKCanvas(flippedBitmap))
|
|
||||||
{
|
|
||||||
flippedCanvas.Translate(flippedBitmap.Width, 0);
|
|
||||||
flippedCanvas.Scale(-1, 1);
|
|
||||||
flippedCanvas.DrawBitmap(rotated, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return flippedBitmap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case SKEncodedOrigin.LeftBottom:
|
case SKEncodedOrigin.LeftBottom:
|
||||||
{
|
surface.Translate(0, rotated.Height);
|
||||||
var rotated = new SKBitmap(bitmap.Height, bitmap.Width);
|
surface.RotateDegrees(-90);
|
||||||
using (var surface = new SKCanvas(rotated))
|
break;
|
||||||
{
|
|
||||||
surface.Translate(0, rotated.Height);
|
|
||||||
surface.RotateDegrees(270);
|
|
||||||
surface.DrawBitmap(bitmap, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rotated;
|
|
||||||
}
|
|
||||||
|
|
||||||
default: return bitmap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
surface.DrawBitmap(bitmap, 0, 0);
|
||||||
|
return rotated;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
@ -552,97 +414,87 @@ namespace Jellyfin.Drawing.Skia
|
|||||||
var blur = options.Blur ?? 0;
|
var blur = options.Blur ?? 0;
|
||||||
var hasIndicator = options.AddPlayedIndicator || options.UnplayedCount.HasValue || !options.PercentPlayed.Equals(0);
|
var hasIndicator = options.AddPlayedIndicator || options.UnplayedCount.HasValue || !options.PercentPlayed.Equals(0);
|
||||||
|
|
||||||
using (var bitmap = GetBitmap(inputPath, options.CropWhiteSpace, autoOrient, orientation))
|
using var bitmap = GetBitmap(inputPath, options.CropWhiteSpace, autoOrient, orientation);
|
||||||
|
if (bitmap == null)
|
||||||
{
|
{
|
||||||
if (bitmap == null)
|
throw new InvalidDataException($"Skia unable to read image {inputPath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var originalImageSize = new ImageDimensions(bitmap.Width, bitmap.Height);
|
||||||
|
|
||||||
|
if (!options.CropWhiteSpace
|
||||||
|
&& options.HasDefaultOptions(inputPath, originalImageSize)
|
||||||
|
&& !autoOrient)
|
||||||
|
{
|
||||||
|
// Just spit out the original file if all the options are default
|
||||||
|
return inputPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
var newImageSize = ImageHelper.GetNewImageSize(options, originalImageSize);
|
||||||
|
|
||||||
|
var width = newImageSize.Width;
|
||||||
|
var height = newImageSize.Height;
|
||||||
|
|
||||||
|
using var resizedBitmap = new SKBitmap(width, height, bitmap.ColorType, bitmap.AlphaType);
|
||||||
|
// scale image
|
||||||
|
bitmap.ScalePixels(resizedBitmap, SKFilterQuality.High);
|
||||||
|
|
||||||
|
// If all we're doing is resizing then we can stop now
|
||||||
|
if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
|
||||||
|
using var outputStream = new SKFileWStream(outputPath);
|
||||||
|
using var pixmap = new SKPixmap(new SKImageInfo(width, height), resizedBitmap.GetPixels());
|
||||||
|
pixmap.Encode(outputStream, skiaOutputFormat, quality);
|
||||||
|
return outputPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create bitmap to use for canvas drawing used to draw into bitmap
|
||||||
|
using var saveBitmap = new SKBitmap(width, height);
|
||||||
|
using var canvas = new SKCanvas(saveBitmap);
|
||||||
|
// set background color if present
|
||||||
|
if (hasBackgroundColor)
|
||||||
|
{
|
||||||
|
canvas.Clear(SKColor.Parse(options.BackgroundColor));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add blur if option is present
|
||||||
|
if (blur > 0)
|
||||||
|
{
|
||||||
|
// create image from resized bitmap to apply blur
|
||||||
|
using var paint = new SKPaint();
|
||||||
|
using var filter = SKImageFilter.CreateBlur(blur, blur);
|
||||||
|
paint.ImageFilter = filter;
|
||||||
|
canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), paint);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// draw resized bitmap onto canvas
|
||||||
|
canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height));
|
||||||
|
}
|
||||||
|
|
||||||
|
// If foreground layer present then draw
|
||||||
|
if (hasForegroundColor)
|
||||||
|
{
|
||||||
|
if (!double.TryParse(options.ForegroundLayer, out double opacity))
|
||||||
{
|
{
|
||||||
throw new InvalidDataException($"Skia unable to read image {inputPath}");
|
opacity = .4;
|
||||||
}
|
}
|
||||||
|
|
||||||
var originalImageSize = new ImageDimensions(bitmap.Width, bitmap.Height);
|
canvas.DrawColor(new SKColor(0, 0, 0, (byte)((1 - opacity) * 0xFF)), SKBlendMode.SrcOver);
|
||||||
|
}
|
||||||
|
|
||||||
if (!options.CropWhiteSpace
|
if (hasIndicator)
|
||||||
&& options.HasDefaultOptions(inputPath, originalImageSize)
|
{
|
||||||
&& !autoOrient)
|
DrawIndicator(canvas, width, height, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
|
||||||
|
using (var outputStream = new SKFileWStream(outputPath))
|
||||||
|
{
|
||||||
|
using (var pixmap = new SKPixmap(new SKImageInfo(width, height), saveBitmap.GetPixels()))
|
||||||
{
|
{
|
||||||
// Just spit out the original file if all the options are default
|
pixmap.Encode(outputStream, skiaOutputFormat, quality);
|
||||||
return inputPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
var newImageSize = ImageHelper.GetNewImageSize(options, originalImageSize);
|
|
||||||
|
|
||||||
var width = newImageSize.Width;
|
|
||||||
var height = newImageSize.Height;
|
|
||||||
|
|
||||||
using (var resizedBitmap = new SKBitmap(width, height, bitmap.ColorType, bitmap.AlphaType))
|
|
||||||
{
|
|
||||||
// scale image
|
|
||||||
bitmap.ScalePixels(resizedBitmap, SKFilterQuality.High);
|
|
||||||
|
|
||||||
// If all we're doing is resizing then we can stop now
|
|
||||||
if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator)
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
|
|
||||||
using (var outputStream = new SKFileWStream(outputPath))
|
|
||||||
using (var pixmap = new SKPixmap(new SKImageInfo(width, height), resizedBitmap.GetPixels()))
|
|
||||||
{
|
|
||||||
pixmap.Encode(outputStream, skiaOutputFormat, quality);
|
|
||||||
return outputPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// create bitmap to use for canvas drawing used to draw into bitmap
|
|
||||||
using (var saveBitmap = new SKBitmap(width, height)) // , bitmap.ColorType, bitmap.AlphaType))
|
|
||||||
using (var canvas = new SKCanvas(saveBitmap))
|
|
||||||
{
|
|
||||||
// set background color if present
|
|
||||||
if (hasBackgroundColor)
|
|
||||||
{
|
|
||||||
canvas.Clear(SKColor.Parse(options.BackgroundColor));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add blur if option is present
|
|
||||||
if (blur > 0)
|
|
||||||
{
|
|
||||||
// create image from resized bitmap to apply blur
|
|
||||||
using (var paint = new SKPaint())
|
|
||||||
using (var filter = SKImageFilter.CreateBlur(blur, blur))
|
|
||||||
{
|
|
||||||
paint.ImageFilter = filter;
|
|
||||||
canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), paint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// draw resized bitmap onto canvas
|
|
||||||
canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height));
|
|
||||||
}
|
|
||||||
|
|
||||||
// If foreground layer present then draw
|
|
||||||
if (hasForegroundColor)
|
|
||||||
{
|
|
||||||
if (!double.TryParse(options.ForegroundLayer, out double opacity))
|
|
||||||
{
|
|
||||||
opacity = .4;
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas.DrawColor(new SKColor(0, 0, 0, (byte)((1 - opacity) * 0xFF)), SKBlendMode.SrcOver);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasIndicator)
|
|
||||||
{
|
|
||||||
DrawIndicator(canvas, width, height, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
|
|
||||||
using (var outputStream = new SKFileWStream(outputPath))
|
|
||||||
{
|
|
||||||
using (var pixmap = new SKPixmap(new SKImageInfo(width, height), saveBitmap.GetPixels()))
|
|
||||||
{
|
|
||||||
pixmap.Encode(outputStream, skiaOutputFormat, quality);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ namespace Jellyfin.Drawing.Skia
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="SkiaException"/> class.
|
/// Initializes a new instance of the <see cref="SkiaException"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SkiaException() : base()
|
public SkiaException()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,12 +69,10 @@ namespace Jellyfin.Drawing.Skia
|
|||||||
/// <param name="height">The desired height of the collage.</param>
|
/// <param name="height">The desired height of the collage.</param>
|
||||||
public void BuildSquareCollage(string[] paths, string outputPath, int width, int height)
|
public void BuildSquareCollage(string[] paths, string outputPath, int width, int height)
|
||||||
{
|
{
|
||||||
using (var bitmap = BuildSquareCollageBitmap(paths, width, height))
|
using var bitmap = BuildSquareCollageBitmap(paths, width, height);
|
||||||
using (var outputStream = new SKFileWStream(outputPath))
|
using var outputStream = new SKFileWStream(outputPath);
|
||||||
using (var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels()))
|
using var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels());
|
||||||
{
|
pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
|
||||||
pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -86,56 +84,46 @@ namespace Jellyfin.Drawing.Skia
|
|||||||
/// <param name="height">The desired height of the collage.</param>
|
/// <param name="height">The desired height of the collage.</param>
|
||||||
public void BuildThumbCollage(string[] paths, string outputPath, int width, int height)
|
public void BuildThumbCollage(string[] paths, string outputPath, int width, int height)
|
||||||
{
|
{
|
||||||
using (var bitmap = BuildThumbCollageBitmap(paths, width, height))
|
using var bitmap = BuildThumbCollageBitmap(paths, width, height);
|
||||||
using (var outputStream = new SKFileWStream(outputPath))
|
using var outputStream = new SKFileWStream(outputPath);
|
||||||
using (var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels()))
|
using var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels());
|
||||||
{
|
pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
|
||||||
pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private SKBitmap BuildThumbCollageBitmap(string[] paths, int width, int height)
|
private SKBitmap BuildThumbCollageBitmap(string[] paths, int width, int height)
|
||||||
{
|
{
|
||||||
var bitmap = new SKBitmap(width, height);
|
var bitmap = new SKBitmap(width, height);
|
||||||
|
|
||||||
using (var canvas = new SKCanvas(bitmap))
|
using var canvas = new SKCanvas(bitmap);
|
||||||
|
canvas.Clear(SKColors.Black);
|
||||||
|
|
||||||
|
// number of images used in the thumbnail
|
||||||
|
var iCount = 3;
|
||||||
|
|
||||||
|
// determine sizes for each image that will composited into the final image
|
||||||
|
var iSlice = Convert.ToInt32(width / iCount);
|
||||||
|
int iHeight = Convert.ToInt32(height * 1.00);
|
||||||
|
int imageIndex = 0;
|
||||||
|
for (int i = 0; i < iCount; i++)
|
||||||
{
|
{
|
||||||
canvas.Clear(SKColors.Black);
|
using var currentBitmap = GetNextValidImage(paths, imageIndex, out int newIndex);
|
||||||
|
imageIndex = newIndex;
|
||||||
// number of images used in the thumbnail
|
if (currentBitmap == null)
|
||||||
var iCount = 3;
|
|
||||||
|
|
||||||
// determine sizes for each image that will composited into the final image
|
|
||||||
var iSlice = Convert.ToInt32(width / iCount);
|
|
||||||
int iHeight = Convert.ToInt32(height * 1.00);
|
|
||||||
int imageIndex = 0;
|
|
||||||
for (int i = 0; i < iCount; i++)
|
|
||||||
{
|
{
|
||||||
using (var currentBitmap = GetNextValidImage(paths, imageIndex, out int newIndex))
|
continue;
|
||||||
{
|
|
||||||
imageIndex = newIndex;
|
|
||||||
if (currentBitmap == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// resize to the same aspect as the original
|
|
||||||
int iWidth = Math.Abs(iHeight * currentBitmap.Width / currentBitmap.Height);
|
|
||||||
using (var resizeBitmap = new SKBitmap(iWidth, iHeight, currentBitmap.ColorType, currentBitmap.AlphaType))
|
|
||||||
{
|
|
||||||
currentBitmap.ScalePixels(resizeBitmap, SKFilterQuality.High);
|
|
||||||
|
|
||||||
// crop image
|
|
||||||
int ix = Math.Abs((iWidth - iSlice) / 2);
|
|
||||||
using (var image = SKImage.FromBitmap(resizeBitmap))
|
|
||||||
using (var subset = image.Subset(SKRectI.Create(ix, 0, iSlice, iHeight)))
|
|
||||||
{
|
|
||||||
// draw image onto canvas
|
|
||||||
canvas.DrawImage(subset ?? image, iSlice * i, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resize to the same aspect as the original
|
||||||
|
int iWidth = Math.Abs(iHeight * currentBitmap.Width / currentBitmap.Height);
|
||||||
|
using var resizeBitmap = new SKBitmap(iWidth, iHeight, currentBitmap.ColorType, currentBitmap.AlphaType);
|
||||||
|
currentBitmap.ScalePixels(resizeBitmap, SKFilterQuality.High);
|
||||||
|
|
||||||
|
// crop image
|
||||||
|
int ix = Math.Abs((iWidth - iSlice) / 2);
|
||||||
|
using var image = SKImage.FromBitmap(resizeBitmap);
|
||||||
|
using var subset = image.Subset(SKRectI.Create(ix, 0, iSlice, iHeight));
|
||||||
|
// draw image onto canvas
|
||||||
|
canvas.DrawImage(subset ?? image, iSlice * i, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
return bitmap;
|
return bitmap;
|
||||||
@ -176,33 +164,27 @@ namespace Jellyfin.Drawing.Skia
|
|||||||
var cellWidth = width / 2;
|
var cellWidth = width / 2;
|
||||||
var cellHeight = height / 2;
|
var cellHeight = height / 2;
|
||||||
|
|
||||||
using (var canvas = new SKCanvas(bitmap))
|
using var canvas = new SKCanvas(bitmap);
|
||||||
|
for (var x = 0; x < 2; x++)
|
||||||
{
|
{
|
||||||
for (var x = 0; x < 2; x++)
|
for (var y = 0; y < 2; y++)
|
||||||
{
|
{
|
||||||
for (var y = 0; y < 2; y++)
|
using var currentBitmap = GetNextValidImage(paths, imageIndex, out int newIndex);
|
||||||
|
imageIndex = newIndex;
|
||||||
|
|
||||||
|
if (currentBitmap == null)
|
||||||
{
|
{
|
||||||
using (var currentBitmap = GetNextValidImage(paths, imageIndex, out int newIndex))
|
continue;
|
||||||
{
|
|
||||||
imageIndex = newIndex;
|
|
||||||
|
|
||||||
if (currentBitmap == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
using (var resizedBitmap = new SKBitmap(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType))
|
|
||||||
{
|
|
||||||
// scale image
|
|
||||||
currentBitmap.ScalePixels(resizedBitmap, SKFilterQuality.High);
|
|
||||||
|
|
||||||
// draw this image into the strip at the next position
|
|
||||||
var xPos = x * cellWidth;
|
|
||||||
var yPos = y * cellHeight;
|
|
||||||
canvas.DrawBitmap(resizedBitmap, xPos, yPos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
using var resizedBitmap = new SKBitmap(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType);
|
||||||
|
// scale image
|
||||||
|
currentBitmap.ScalePixels(resizedBitmap, SKFilterQuality.High);
|
||||||
|
|
||||||
|
// draw this image into the strip at the next position
|
||||||
|
var xPos = x * cellWidth;
|
||||||
|
var yPos = y * cellHeight;
|
||||||
|
canvas.DrawBitmap(resizedBitmap, xPos, yPos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,41 +28,37 @@ namespace Jellyfin.Drawing.Skia
|
|||||||
var x = imageSize.Width - OffsetFromTopRightCorner;
|
var x = imageSize.Width - OffsetFromTopRightCorner;
|
||||||
var text = count.ToString(CultureInfo.InvariantCulture);
|
var text = count.ToString(CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
using (var paint = new SKPaint())
|
using var paint = new SKPaint
|
||||||
{
|
{
|
||||||
paint.Color = SKColor.Parse("#CC00A4DC");
|
Color = SKColor.Parse("#CC00A4DC"),
|
||||||
paint.Style = SKPaintStyle.Fill;
|
Style = SKPaintStyle.Fill
|
||||||
canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint);
|
};
|
||||||
|
|
||||||
|
canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint);
|
||||||
|
|
||||||
|
paint.Color = new SKColor(255, 255, 255, 255);
|
||||||
|
paint.TextSize = 24;
|
||||||
|
paint.IsAntialias = true;
|
||||||
|
|
||||||
|
var y = OffsetFromTopRightCorner + 9;
|
||||||
|
|
||||||
|
if (text.Length == 1)
|
||||||
|
{
|
||||||
|
x -= 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
using (var paint = new SKPaint())
|
if (text.Length == 2)
|
||||||
{
|
{
|
||||||
paint.Color = new SKColor(255, 255, 255, 255);
|
x -= 13;
|
||||||
paint.Style = SKPaintStyle.Fill;
|
|
||||||
|
|
||||||
paint.TextSize = 24;
|
|
||||||
paint.IsAntialias = true;
|
|
||||||
|
|
||||||
var y = OffsetFromTopRightCorner + 9;
|
|
||||||
|
|
||||||
if (text.Length == 1)
|
|
||||||
{
|
|
||||||
x -= 7;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (text.Length == 2)
|
|
||||||
{
|
|
||||||
x -= 13;
|
|
||||||
}
|
|
||||||
else if (text.Length >= 3)
|
|
||||||
{
|
|
||||||
x -= 15;
|
|
||||||
y -= 2;
|
|
||||||
paint.TextSize = 18;
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas.DrawText(text, x, y, paint);
|
|
||||||
}
|
}
|
||||||
|
else if (text.Length >= 3)
|
||||||
|
{
|
||||||
|
x -= 15;
|
||||||
|
y -= 2;
|
||||||
|
paint.TextSize = 18;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.DrawText(text, x, y, paint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -766,8 +766,8 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
{
|
{
|
||||||
// This is some regex that matches only on unicode "word" characters, as well as -, _ and @
|
// This is some regex that matches only on unicode "word" characters, as well as -, _ and @
|
||||||
// In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
|
// In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
|
||||||
// Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), and periods (.)
|
// Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), periods (.) and spaces ( )
|
||||||
return Regex.IsMatch(name, @"^[\w\-'._@]*$");
|
return Regex.IsMatch(name, @"^[\w\ \-'._@]*$");
|
||||||
}
|
}
|
||||||
|
|
||||||
private IAuthenticationProvider GetAuthenticationProvider(User user)
|
private IAuthenticationProvider GetAuthenticationProvider(User user)
|
||||||
|
@ -457,6 +457,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
var isNvencHevcDecoder = videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1;
|
var isNvencHevcDecoder = videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1;
|
||||||
var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
||||||
var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
|
var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
|
||||||
|
var isMacOS = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
|
||||||
|
|
||||||
if (!IsCopyCodec(outputVideoCodec))
|
if (!IsCopyCodec(outputVideoCodec))
|
||||||
{
|
{
|
||||||
@ -529,6 +530,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
.Append(' ')
|
.Append(' ')
|
||||||
.Append("-filter_hw_device ocl ");
|
.Append("-filter_hw_device ocl ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.IsVideoRequest
|
||||||
|
&& string.Equals(encodingOptions.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
arg.Append("-hwaccel videotoolbox ");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,7 +174,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
inputFiles = new[] { mediaSource.Path };
|
inputFiles = new[] { mediaSource.Path };
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileInfo = await GetReadableFile(mediaSource.Path, inputFiles, _mediaSourceManager.GetPathProtocol(subtitleStream.Path), subtitleStream, cancellationToken).ConfigureAwait(false);
|
var protocol = mediaSource.Protocol;
|
||||||
|
if (subtitleStream.IsExternal)
|
||||||
|
{
|
||||||
|
protocol = _mediaSourceManager.GetPathProtocol(subtitleStream.Path);
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileInfo = await GetReadableFile(mediaSource.Path, inputFiles, protocol, subtitleStream, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var stream = await GetSubtitleStream(fileInfo.Path, fileInfo.Protocol, fileInfo.IsExternal, cancellationToken).ConfigureAwait(false);
|
var stream = await GetSubtitleStream(fileInfo.Path, fileInfo.Protocol, fileInfo.IsExternal, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
@ -19,6 +19,9 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
|
|||||||
|
|
||||||
public override string Description => "Get artist and album metadata or images from AudioDB.";
|
public override string Description => "Get artist and album metadata or images from AudioDB.";
|
||||||
|
|
||||||
|
// TODO remove when plugin removed from server.
|
||||||
|
public override string ConfigurationFileName => "Jellyfin.Plugin.AudioDb.xml";
|
||||||
|
|
||||||
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
|
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
|
||||||
: base(applicationPaths, xmlSerializer)
|
: base(applicationPaths, xmlSerializer)
|
||||||
{
|
{
|
||||||
|
@ -23,6 +23,9 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz
|
|||||||
|
|
||||||
public const long DefaultRateLimit = 2000u;
|
public const long DefaultRateLimit = 2000u;
|
||||||
|
|
||||||
|
// TODO remove when plugin removed from server.
|
||||||
|
public override string ConfigurationFileName => "Jellyfin.Plugin.MusicBrainz.xml";
|
||||||
|
|
||||||
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
|
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
|
||||||
: base(applicationPaths, xmlSerializer)
|
: base(applicationPaths, xmlSerializer)
|
||||||
{
|
{
|
||||||
|
@ -19,6 +19,9 @@ namespace MediaBrowser.Providers.Plugins.Omdb
|
|||||||
|
|
||||||
public override string Description => "Get metadata for movies and other video content from OMDb.";
|
public override string Description => "Get metadata for movies and other video content from OMDb.";
|
||||||
|
|
||||||
|
// TODO remove when plugin removed from server.
|
||||||
|
public override string ConfigurationFileName => "Jellyfin.Plugin.Omdb.xml";
|
||||||
|
|
||||||
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
|
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
|
||||||
: base(applicationPaths, xmlSerializer)
|
: base(applicationPaths, xmlSerializer)
|
||||||
{
|
{
|
||||||
|
@ -17,6 +17,9 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
|
|||||||
|
|
||||||
public override string Description => "Get metadata for movies and other video content from TheTVDB.";
|
public override string Description => "Get metadata for movies and other video content from TheTVDB.";
|
||||||
|
|
||||||
|
// TODO remove when plugin removed from server.
|
||||||
|
public override string ConfigurationFileName => "Jellyfin.Plugin.TheTvdb.xml";
|
||||||
|
|
||||||
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
|
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
|
||||||
: base(applicationPaths, xmlSerializer)
|
: base(applicationPaths, xmlSerializer)
|
||||||
{
|
{
|
||||||
|
@ -9,17 +9,29 @@ namespace Jellyfin.Server.Implementations.Tests.Library
|
|||||||
[InlineData("/media/small.jpg", true)]
|
[InlineData("/media/small.jpg", true)]
|
||||||
[InlineData("/media/albumart.jpg", true)]
|
[InlineData("/media/albumart.jpg", true)]
|
||||||
[InlineData("/media/movie.sample.mp4", true)]
|
[InlineData("/media/movie.sample.mp4", true)]
|
||||||
|
[InlineData("/media/movie/sample.mp4", true)]
|
||||||
|
[InlineData("/media/movie/sample/movie.mp4", true)]
|
||||||
|
[InlineData("/foo/sample/bar/baz.mkv", false)]
|
||||||
|
[InlineData("/media/movies/the sample/the sample.mkv", false)]
|
||||||
|
[InlineData("/media/movies/sampler.mkv", false)]
|
||||||
[InlineData("/media/movies/#Recycle/test.txt", true)]
|
[InlineData("/media/movies/#Recycle/test.txt", true)]
|
||||||
[InlineData("/media/movies/#recycle/", true)]
|
[InlineData("/media/movies/#recycle/", true)]
|
||||||
[InlineData("/media/movies/#recycle", true)]
|
[InlineData("/media/movies/#recycle", true)]
|
||||||
[InlineData("thumbs.db", true)]
|
[InlineData("thumbs.db", true)]
|
||||||
[InlineData(@"C:\media\movies\movie.avi", false)]
|
[InlineData(@"C:\media\movies\movie.avi", false)]
|
||||||
[InlineData("/media/.hiddendir/file.mp4", true)]
|
[InlineData("/media/.hiddendir/file.mp4", false)]
|
||||||
[InlineData("/media/dir/.hiddenfile.mp4", true)]
|
[InlineData("/media/dir/.hiddenfile.mp4", true)]
|
||||||
|
[InlineData("/media/dir/._macjunk.mp4", true)]
|
||||||
[InlineData("/volume1/video/Series/@eaDir", true)]
|
[InlineData("/volume1/video/Series/@eaDir", true)]
|
||||||
[InlineData("/volume1/video/Series/@eaDir/file.txt", true)]
|
[InlineData("/volume1/video/Series/@eaDir/file.txt", true)]
|
||||||
[InlineData("/directory/@Recycle", true)]
|
[InlineData("/directory/@Recycle", true)]
|
||||||
[InlineData("/directory/@Recycle/file.mp3", true)]
|
[InlineData("/directory/@Recycle/file.mp3", true)]
|
||||||
|
[InlineData("/media/movies/.@__thumb", true)]
|
||||||
|
[InlineData("/media/movies/.@__thumb/foo-bar-thumbnail.png", true)]
|
||||||
|
[InlineData("/media/music/Foo B.A.R./epic.flac", false)]
|
||||||
|
[InlineData("/media/music/Foo B.A.R", false)]
|
||||||
|
// This test is pending an upstream fix: https://github.com/dazinator/DotNet.Glob/issues/78
|
||||||
|
// [InlineData("/media/music/Foo B.A.R.", false)]
|
||||||
public void PathIgnored(string path, bool expected)
|
public void PathIgnored(string path, bool expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, IgnorePatterns.ShouldIgnore(path));
|
Assert.Equal(expected, IgnorePatterns.ShouldIgnore(path));
|
||||||
|
Loading…
x
Reference in New Issue
Block a user