mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-01-15 23:58:57 +00:00
Compare commits
82 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f17ed961e | ||
|
|
5fc4ad6c4e | ||
|
|
b117b364f2 | ||
|
|
3603c64fa6 | ||
|
|
d405a400aa | ||
|
|
54c6f02ebb | ||
|
|
b3f9d04501 | ||
|
|
acf52b9b55 | ||
|
|
7587fe56d8 | ||
|
|
ab34a95142 | ||
|
|
0ee40cb636 | ||
|
|
62105c249f | ||
|
|
a629f209b9 | ||
|
|
c08c0272b5 | ||
|
|
c52e8a2027 | ||
|
|
1fd8164756 | ||
|
|
3c16c34386 | ||
|
|
394d96246b | ||
|
|
084854d71d | ||
|
|
c2ab0ad641 | ||
|
|
7eb94e9674 | ||
|
|
0a5550b13d | ||
|
|
067200be83 | ||
|
|
b136f14084 | ||
|
|
d5fe82314e | ||
|
|
06834fefef | ||
|
|
2946ae1009 | ||
|
|
4b8f735cb8 | ||
|
|
c230d49d7c | ||
|
|
20e2cb2d86 | ||
|
|
b70083f3b3 | ||
|
|
74ef389879 | ||
|
|
d78a55adb4 | ||
|
|
6f99ed3955 | ||
|
|
247a5e12ab | ||
|
|
855911333a | ||
|
|
127bfc7d3b | ||
|
|
7919dd81da | ||
|
|
e1da046960 | ||
|
|
a756026962 | ||
|
|
75260a960b | ||
|
|
69ee49bee6 | ||
|
|
1bf3a26a61 | ||
|
|
c5760b3a40 | ||
|
|
4de8bf3295 | ||
|
|
87c8f19f19 | ||
|
|
0ef52c739e | ||
|
|
0a9a6b949c | ||
|
|
c22068d6b1 | ||
|
|
bbc1a86b57 | ||
|
|
cd83d80f2b | ||
|
|
12721eb7dd | ||
|
|
b8a09339cd | ||
|
|
3634d367c1 | ||
|
|
c1daea0ec7 | ||
|
|
e8196fed7c | ||
|
|
477702fbb9 | ||
|
|
2216a271bb | ||
|
|
91cd7d2f6b | ||
|
|
4e681d25d9 | ||
|
|
06cc2891de | ||
|
|
682432f55a | ||
|
|
7c4cb5ec58 | ||
|
|
1df73fdeba | ||
|
|
21ba8a0593 | ||
|
|
08ed52eb72 | ||
|
|
99700e1b95 | ||
|
|
4e0be95368 | ||
|
|
c8a59c8343 | ||
|
|
2b2a2ed708 | ||
|
|
a827a2fbcc | ||
|
|
f97f6b8061 | ||
|
|
844ea9d77e | ||
|
|
71479286e9 | ||
|
|
28c2ac528d | ||
|
|
696a36b4a5 | ||
|
|
5fb4922c6f | ||
|
|
3738f95871 | ||
|
|
6797bdec04 | ||
|
|
764c6d5461 | ||
|
|
6973182ade | ||
|
|
f62af07381 |
@@ -23,7 +23,10 @@
|
||||
- [fruhnow](https://github.com/fruhnow)
|
||||
- [Lynxy](https://github.com/Lynxy)
|
||||
- [fasheng](https://github.com/fasheng)
|
||||
- [ploughpuff](https://github.com/ploughpuff)
|
||||
- [ploughpuff](https://github.com/ploughpuff)
|
||||
- [pjeanjean](https://github.com/pjeanjean)
|
||||
- [DrPandemic](https://github.com/drpandemic)
|
||||
- [joern-h](https://github.com/joern-h)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ RUN apt-get update \
|
||||
COPY --from=ffmpeg / /
|
||||
COPY --from=builder /jellyfin /jellyfin
|
||||
|
||||
ARG JELLYFIN_WEB_VERSION=10.3.0
|
||||
ARG JELLYFIN_WEB_VERSION=10.3.6
|
||||
RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||
&& rm -rf /jellyfin/jellyfin-web \
|
||||
&& mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web
|
||||
|
||||
@@ -3,11 +3,6 @@
|
||||
ARG DOTNET_VERSION=3.0
|
||||
|
||||
|
||||
FROM multiarch/qemu-user-static:x86_64-arm as qemu
|
||||
FROM alpine as qemu_extract
|
||||
COPY --from=qemu /usr/bin qemu-arm-static.tar.gz
|
||||
RUN tar -xzvf qemu-arm-static.tar.gz
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder
|
||||
WORKDIR /repo
|
||||
COPY . .
|
||||
@@ -21,8 +16,9 @@ RUN bash -c "source deployment/common.build.sh && \
|
||||
build_jellyfin Jellyfin.Server Release linux-arm /jellyfin"
|
||||
|
||||
|
||||
FROM multiarch/qemu-user-static:x86_64-arm as qemu
|
||||
FROM mcr.microsoft.com/dotnet/core/runtime:${DOTNET_VERSION}-stretch-slim-arm32v7
|
||||
COPY --from=qemu_extract qemu-arm-static /usr/bin
|
||||
COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
|
||||
RUN apt-get update \
|
||||
&& apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
@@ -30,7 +26,7 @@ RUN apt-get update \
|
||||
&& chmod 777 /cache /config /media
|
||||
COPY --from=builder /jellyfin /jellyfin
|
||||
|
||||
ARG JELLYFIN_WEB_VERSION=10.3.0
|
||||
ARG JELLYFIN_WEB_VERSION=10.3.6
|
||||
RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||
&& rm -rf /jellyfin/jellyfin-web \
|
||||
&& mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web
|
||||
|
||||
@@ -3,12 +3,6 @@
|
||||
ARG DOTNET_VERSION=3.0
|
||||
|
||||
|
||||
FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
|
||||
FROM alpine as qemu_extract
|
||||
COPY --from=qemu /usr/bin qemu-aarch64-static.tar.gz
|
||||
RUN tar -xzvf qemu-aarch64-static.tar.gz
|
||||
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder
|
||||
WORKDIR /repo
|
||||
COPY . .
|
||||
@@ -22,8 +16,9 @@ RUN bash -c "source deployment/common.build.sh && \
|
||||
build_jellyfin Jellyfin.Server Release linux-arm64 /jellyfin"
|
||||
|
||||
|
||||
FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
|
||||
FROM mcr.microsoft.com/dotnet/core/runtime:${DOTNET_VERSION}-stretch-slim-arm64v8
|
||||
COPY --from=qemu_extract qemu-aarch64-static /usr/bin
|
||||
COPY --from=qemu /usr/bin/qemu-aarch64-static /usr/bin
|
||||
RUN apt-get update \
|
||||
&& apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
@@ -31,7 +26,7 @@ RUN apt-get update \
|
||||
&& chmod 777 /cache /config /media
|
||||
COPY --from=builder /jellyfin /jellyfin
|
||||
|
||||
ARG JELLYFIN_WEB_VERSION=10.3.0
|
||||
ARG JELLYFIN_WEB_VERSION=10.3.6
|
||||
RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||
&& rm -rf /jellyfin/jellyfin-web \
|
||||
&& mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web
|
||||
|
||||
@@ -920,8 +920,6 @@ namespace Emby.Dlna.Didl
|
||||
}
|
||||
}
|
||||
|
||||
AddImageResElement(item, writer, 160, 160, "jpg", "JPEG_TN");
|
||||
|
||||
if (!_profile.EnableSingleAlbumArtLimit || string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
AddImageResElement(item, writer, 4096, 4096, "jpg", "JPEG_LRG");
|
||||
@@ -930,6 +928,9 @@ namespace Emby.Dlna.Didl
|
||||
AddImageResElement(item, writer, 4096, 4096, "png", "PNG_LRG");
|
||||
AddImageResElement(item, writer, 160, 160, "png", "PNG_TN");
|
||||
}
|
||||
|
||||
AddImageResElement(item, writer, 160, 160, "jpg", "JPEG_TN");
|
||||
|
||||
}
|
||||
|
||||
private void AddEmbeddedImageAsCover(string name, XmlWriter writer)
|
||||
|
||||
@@ -34,16 +34,13 @@ namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
var cancellationToken = CancellationToken.None;
|
||||
|
||||
using (var response = await PostSoapDataAsync(NormalizeServiceUrl(baseUrl, service.ControlUrl), "\"" + service.ServiceType + "#" + command + "\"", postData, header, logRequest, cancellationToken)
|
||||
var url = NormalizeServiceUrl(baseUrl, service.ControlUrl);
|
||||
using (var response = await PostSoapDataAsync(url, '\"' + service.ServiceType + '#' + command + '\"', postData, header, logRequest, cancellationToken)
|
||||
.ConfigureAwait(false))
|
||||
using (var stream = response.Content)
|
||||
using (var reader = new StreamReader(stream, Encoding.UTF8))
|
||||
{
|
||||
using (var stream = response.Content)
|
||||
{
|
||||
using (var reader = new StreamReader(stream, Encoding.UTF8))
|
||||
{
|
||||
return XDocument.Parse(reader.ReadToEnd(), LoadOptions.PreserveWhitespace);
|
||||
}
|
||||
}
|
||||
return XDocument.Parse(reader.ReadToEnd(), LoadOptions.PreserveWhitespace);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,15 +118,18 @@ namespace Emby.Dlna.PlayTo
|
||||
}
|
||||
}
|
||||
|
||||
private Task<HttpResponseInfo> PostSoapDataAsync(string url,
|
||||
private Task<HttpResponseInfo> PostSoapDataAsync(
|
||||
string url,
|
||||
string soapAction,
|
||||
string postData,
|
||||
string header,
|
||||
bool logRequest,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!soapAction.StartsWith("\""))
|
||||
soapAction = "\"" + soapAction + "\"";
|
||||
if (soapAction[0] != '\"')
|
||||
{
|
||||
soapAction = '\"' + soapAction + '\"';
|
||||
}
|
||||
|
||||
var options = new HttpRequestOptions
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
@@ -429,7 +430,7 @@ namespace Emby.Server.Implementations
|
||||
/// Gets the current application user agent
|
||||
/// </summary>
|
||||
/// <value>The application user agent.</value>
|
||||
public string ApplicationUserAgent => Name.Replace(' ','-') + "/" + ApplicationVersion;
|
||||
public string ApplicationUserAgent => Name.Replace(' ','-') + '/' + ApplicationVersion;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the email address for use within a comment section of a user agent field.
|
||||
@@ -689,11 +690,6 @@ namespace Emby.Server.Implementations
|
||||
await HttpServer.RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
protected virtual IHttpClient CreateHttpClient()
|
||||
{
|
||||
return new HttpClientManager.HttpClientManager(ApplicationPaths, LoggerFactory, FileSystemManager, () => ApplicationUserAgent);
|
||||
}
|
||||
|
||||
public static IStreamHelper StreamHelper { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -719,7 +715,11 @@ namespace Emby.Server.Implementations
|
||||
serviceCollection.AddSingleton(FileSystemManager);
|
||||
serviceCollection.AddSingleton<TvDbClientManager>();
|
||||
|
||||
HttpClient = CreateHttpClient();
|
||||
HttpClient = new HttpClientManager.HttpClientManager(
|
||||
ApplicationPaths,
|
||||
LoggerFactory.CreateLogger<HttpClientManager.HttpClientManager>(),
|
||||
FileSystemManager,
|
||||
() => ApplicationUserAgent);
|
||||
serviceCollection.AddSingleton(HttpClient);
|
||||
|
||||
serviceCollection.AddSingleton(NetworkManager);
|
||||
@@ -1167,7 +1167,7 @@ namespace Emby.Server.Implementations
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error loading plugin {pluginName}", plugin.GetType().FullName);
|
||||
Logger.LogError(ex, "Error loading plugin {PluginName}", plugin.GetType().FullName);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1181,10 +1181,32 @@ namespace Emby.Server.Implementations
|
||||
{
|
||||
Logger.LogInformation("Loading assemblies");
|
||||
|
||||
AllConcreteTypes = GetComposablePartAssemblies()
|
||||
.SelectMany(x => x.ExportedTypes)
|
||||
.Where(type => type.IsClass && !type.IsAbstract && !type.IsInterface && !type.IsGenericType)
|
||||
.ToArray();
|
||||
AllConcreteTypes = GetTypes(GetComposablePartAssemblies()).ToArray();
|
||||
}
|
||||
|
||||
private IEnumerable<Type> GetTypes(IEnumerable<Assembly> assemblies)
|
||||
{
|
||||
foreach (var ass in assemblies)
|
||||
{
|
||||
Type[] exportedTypes;
|
||||
try
|
||||
{
|
||||
exportedTypes = ass.GetExportedTypes();
|
||||
}
|
||||
catch (TypeLoadException ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName);
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (Type type in exportedTypes)
|
||||
{
|
||||
if (type.IsClass && !type.IsAbstract && !type.IsInterface && !type.IsGenericType)
|
||||
{
|
||||
yield return type;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private CertificateInfo CertificateInfo { get; set; }
|
||||
@@ -1348,8 +1370,19 @@ namespace Emby.Server.Implementations
|
||||
{
|
||||
foreach (var file in Directory.EnumerateFiles(ApplicationPaths.PluginsPath, "*.dll", SearchOption.AllDirectories))
|
||||
{
|
||||
Logger.LogInformation("Loading assembly {Path}", file);
|
||||
yield return Assembly.LoadFrom(file);
|
||||
Assembly plugAss;
|
||||
try
|
||||
{
|
||||
plugAss = Assembly.LoadFrom(file);
|
||||
}
|
||||
catch (FileLoadException ex)
|
||||
{
|
||||
Logger.LogError(ex, "Failed to load assembly {Path}", file);
|
||||
continue;
|
||||
}
|
||||
|
||||
Logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugAss.FullName, file);
|
||||
yield return plugAss;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1527,12 +1560,12 @@ namespace Emby.Server.Implementations
|
||||
LogErrorResponseBody = false,
|
||||
LogErrors = false,
|
||||
LogRequest = false,
|
||||
TimeoutMs = 10000,
|
||||
BufferContent = false,
|
||||
CancellationToken = cancellationToken
|
||||
}).ConfigureAwait(false))
|
||||
{
|
||||
return GetWanApiUrl(response.ReadToEnd().Trim());
|
||||
string res = await response.ReadToEndAsync().ConfigureAwait(false);
|
||||
return GetWanApiUrl(res.Trim());
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -1684,15 +1717,15 @@ namespace Emby.Server.Implementations
|
||||
LogErrorResponseBody = false,
|
||||
LogErrors = LogPing,
|
||||
LogRequest = LogPing,
|
||||
TimeoutMs = 5000,
|
||||
BufferContent = false,
|
||||
|
||||
CancellationToken = cancellationToken
|
||||
}, "POST").ConfigureAwait(false))
|
||||
}, HttpMethod.Post).ConfigureAwait(false))
|
||||
|
||||
{
|
||||
using (var reader = new StreamReader(response.Content))
|
||||
{
|
||||
var result = reader.ReadToEnd();
|
||||
var result = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
var valid = string.Equals(Name, result, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
_validAddressResults.AddOrUpdate(apiUrl, valid, (k, v) => valid);
|
||||
|
||||
@@ -223,6 +223,8 @@ namespace Emby.Server.Implementations.Data
|
||||
"pragma temp_store = file"
|
||||
});
|
||||
}
|
||||
// Configuration and pragmas can affect VACUUM so it needs to be last.
|
||||
queries.Add("VACUUM");
|
||||
|
||||
db.ExecuteAll(string.Join(";", queries));
|
||||
Logger.LogInformation("PRAGMA synchronous=" + db.Query("PRAGMA synchronous").SelectScalarString().First());
|
||||
|
||||
@@ -81,7 +81,7 @@ namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
// If the user password is the sha1 hash of the empty string, remove it
|
||||
if (!string.Equals(user.Password, "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal)
|
||||
|| !string.Equals(user.Password, "$SHA1$DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal))
|
||||
&& !string.Equals(user.Password, "$SHA1$DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Emby.Server.Implementations.HttpClientManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Class HttpClientInfo
|
||||
/// </summary>
|
||||
public class HttpClientInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the last timeout.
|
||||
/// </summary>
|
||||
/// <value>The last timeout.</value>
|
||||
public DateTime LastTimeout { get; set; }
|
||||
public HttpClient HttpClient { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Cache;
|
||||
using System.Text;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
@@ -24,30 +21,24 @@ namespace Emby.Server.Implementations.HttpClientManager
|
||||
/// </summary>
|
||||
public class HttpClientManager : IHttpClient
|
||||
{
|
||||
/// <summary>
|
||||
/// When one request to a host times out, we'll ban all other requests for this period of time, to prevent scans from stalling
|
||||
/// </summary>
|
||||
private const int TimeoutSeconds = 30;
|
||||
|
||||
/// <summary>
|
||||
/// The _logger
|
||||
/// </summary>
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// The _app paths
|
||||
/// </summary>
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly Func<string> _defaultUserAgentFn;
|
||||
|
||||
/// <summary>
|
||||
/// Holds a dictionary of http clients by host. Use GetHttpClient(host) to retrieve or create a client for web requests.
|
||||
/// DON'T dispose it after use.
|
||||
/// </summary>
|
||||
/// <value>The HTTP clients.</value>
|
||||
private readonly ConcurrentDictionary<string, HttpClient> _httpClients = new ConcurrentDictionary<string, HttpClient>();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HttpClientManager" /> class.
|
||||
/// </summary>
|
||||
public HttpClientManager(
|
||||
IApplicationPaths appPaths,
|
||||
ILoggerFactory loggerFactory,
|
||||
ILogger<HttpClientManager> logger,
|
||||
IFileSystem fileSystem,
|
||||
Func<string> defaultUserAgentFn)
|
||||
{
|
||||
@@ -55,46 +46,33 @@ namespace Emby.Server.Implementations.HttpClientManager
|
||||
{
|
||||
throw new ArgumentNullException(nameof(appPaths));
|
||||
}
|
||||
if (loggerFactory == null)
|
||||
|
||||
if (logger == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(loggerFactory));
|
||||
throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
_logger = loggerFactory.CreateLogger("HttpClient");
|
||||
_logger = logger;
|
||||
_fileSystem = fileSystem;
|
||||
_appPaths = appPaths;
|
||||
_defaultUserAgentFn = defaultUserAgentFn;
|
||||
|
||||
// http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c
|
||||
ServicePointManager.Expect100Continue = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds a dictionary of http clients by host. Use GetHttpClient(host) to retrieve or create a client for web requests.
|
||||
/// DON'T dispose it after use.
|
||||
/// Gets the correct http client for the given url.
|
||||
/// </summary>
|
||||
/// <value>The HTTP clients.</value>
|
||||
private readonly ConcurrentDictionary<string, HttpClientInfo> _httpClients = new ConcurrentDictionary<string, HttpClientInfo>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets
|
||||
/// </summary>
|
||||
/// <param name="host">The host.</param>
|
||||
/// <param name="enableHttpCompression">if set to <c>true</c> [enable HTTP compression].</param>
|
||||
/// <param name="url">The url.</param>
|
||||
/// <returns>HttpClient.</returns>
|
||||
/// <exception cref="ArgumentNullException">host</exception>
|
||||
private HttpClientInfo GetHttpClient(string host, bool enableHttpCompression)
|
||||
private HttpClient GetHttpClient(string url)
|
||||
{
|
||||
if (string.IsNullOrEmpty(host))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(host));
|
||||
}
|
||||
|
||||
var key = host + enableHttpCompression;
|
||||
var key = GetHostFromUrl(url);
|
||||
|
||||
if (!_httpClients.TryGetValue(key, out var client))
|
||||
{
|
||||
client = new HttpClientInfo();
|
||||
client = new HttpClient()
|
||||
{
|
||||
BaseAddress = new Uri(url)
|
||||
};
|
||||
|
||||
_httpClients.TryAdd(key, client);
|
||||
}
|
||||
@@ -102,119 +80,84 @@ namespace Emby.Server.Implementations.HttpClientManager
|
||||
return client;
|
||||
}
|
||||
|
||||
private WebRequest GetRequest(HttpRequestOptions options, string method)
|
||||
private HttpRequestMessage GetRequestMessage(HttpRequestOptions options, HttpMethod method)
|
||||
{
|
||||
string url = options.Url;
|
||||
|
||||
var uriAddress = new Uri(url);
|
||||
string userInfo = uriAddress.UserInfo;
|
||||
if (!string.IsNullOrWhiteSpace(userInfo))
|
||||
{
|
||||
_logger.LogInformation("Found userInfo in url: {0} ... url: {1}", userInfo, url);
|
||||
url = url.Replace(userInfo + "@", string.Empty);
|
||||
_logger.LogWarning("Found userInfo in url: {0} ... url: {1}", userInfo, url);
|
||||
url = url.Replace(userInfo + '@', string.Empty);
|
||||
}
|
||||
|
||||
var request = WebRequest.Create(url);
|
||||
var request = new HttpRequestMessage(method, url);
|
||||
|
||||
if (request is HttpWebRequest httpWebRequest)
|
||||
AddRequestHeaders(request, options);
|
||||
|
||||
switch (options.DecompressionMethod)
|
||||
{
|
||||
AddRequestHeaders(httpWebRequest, options);
|
||||
|
||||
if (options.EnableHttpCompression)
|
||||
{
|
||||
httpWebRequest.AutomaticDecompression = DecompressionMethods.Deflate;
|
||||
if (options.DecompressionMethod.HasValue
|
||||
&& options.DecompressionMethod.Value == CompressionMethod.Gzip)
|
||||
{
|
||||
httpWebRequest.AutomaticDecompression = DecompressionMethods.GZip;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
httpWebRequest.AutomaticDecompression = DecompressionMethods.None;
|
||||
}
|
||||
|
||||
httpWebRequest.KeepAlive = options.EnableKeepAlive;
|
||||
|
||||
if (!string.IsNullOrEmpty(options.Host))
|
||||
{
|
||||
httpWebRequest.Host = options.Host;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(options.Referer))
|
||||
{
|
||||
httpWebRequest.Referer = options.Referer;
|
||||
}
|
||||
case CompressionMethod.Deflate | CompressionMethod.Gzip:
|
||||
request.Headers.Add(HeaderNames.AcceptEncoding, new[] { "gzip", "deflate" });
|
||||
break;
|
||||
case CompressionMethod.Deflate:
|
||||
request.Headers.Add(HeaderNames.AcceptEncoding, "deflate");
|
||||
break;
|
||||
case CompressionMethod.Gzip:
|
||||
request.Headers.Add(HeaderNames.AcceptEncoding, "gzip");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
request.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache);
|
||||
if (options.EnableKeepAlive)
|
||||
{
|
||||
request.Headers.Add(HeaderNames.Connection, "Keep-Alive");
|
||||
}
|
||||
|
||||
request.Method = method;
|
||||
request.Timeout = options.TimeoutMs;
|
||||
//request.Headers.Add(HeaderNames.CacheControl, "no-cache");
|
||||
|
||||
/*
|
||||
if (!string.IsNullOrWhiteSpace(userInfo))
|
||||
{
|
||||
var parts = userInfo.Split(':');
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
request.Credentials = GetCredential(url, parts[0], parts[1]);
|
||||
// TODO: .net core ??
|
||||
request.PreAuthenticate = true;
|
||||
request.Headers.Add(HeaderNames., GetCredential(url, parts[0], parts[1]);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
private static CredentialCache GetCredential(string url, string username, string password)
|
||||
{
|
||||
//ServicePointManager.SecurityProtocol = SecurityProtocolType.Ssl3;
|
||||
var credentialCache = new CredentialCache();
|
||||
credentialCache.Add(new Uri(url), "Basic", new NetworkCredential(username, password));
|
||||
return credentialCache;
|
||||
}
|
||||
|
||||
private void AddRequestHeaders(HttpWebRequest request, HttpRequestOptions options)
|
||||
private void AddRequestHeaders(HttpRequestMessage request, HttpRequestOptions options)
|
||||
{
|
||||
var hasUserAgent = false;
|
||||
|
||||
foreach (var header in options.RequestHeaders)
|
||||
{
|
||||
if (string.Equals(header.Key, HeaderNames.Accept, StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(header.Key, HeaderNames.UserAgent, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
request.Accept = header.Value;
|
||||
}
|
||||
else if (string.Equals(header.Key, HeaderNames.UserAgent, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
SetUserAgent(request, header.Value);
|
||||
hasUserAgent = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
request.Headers.Set(header.Key, header.Value);
|
||||
}
|
||||
|
||||
request.Headers.Add(header.Key, header.Value);
|
||||
}
|
||||
|
||||
if (!hasUserAgent && options.EnableDefaultUserAgent)
|
||||
{
|
||||
SetUserAgent(request, _defaultUserAgentFn());
|
||||
request.Headers.Add(HeaderNames.UserAgent, _defaultUserAgentFn());
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetUserAgent(HttpWebRequest request, string userAgent)
|
||||
{
|
||||
request.UserAgent = userAgent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the response internal.
|
||||
/// </summary>
|
||||
/// <param name="options">The options.</param>
|
||||
/// <returns>Task{HttpResponseInfo}.</returns>
|
||||
public Task<HttpResponseInfo> GetResponse(HttpRequestOptions options)
|
||||
{
|
||||
return SendAsync(options, "GET");
|
||||
}
|
||||
=> SendAsync(options, HttpMethod.Get);
|
||||
|
||||
/// <summary>
|
||||
/// Performs a GET request and returns the resulting stream
|
||||
@@ -233,9 +176,19 @@ namespace Emby.Server.Implementations.HttpClientManager
|
||||
/// <param name="options">The options.</param>
|
||||
/// <param name="httpMethod">The HTTP method.</param>
|
||||
/// <returns>Task{HttpResponseInfo}.</returns>
|
||||
/// <exception cref="HttpException">
|
||||
/// </exception>
|
||||
public async Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, string httpMethod)
|
||||
public Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, string httpMethod)
|
||||
{
|
||||
var httpMethod2 = GetHttpMethod(httpMethod);
|
||||
return SendAsync(options, httpMethod2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// send as an asynchronous operation.
|
||||
/// </summary>
|
||||
/// <param name="options">The options.</param>
|
||||
/// <param name="httpMethod">The HTTP method.</param>
|
||||
/// <returns>Task{HttpResponseInfo}.</returns>
|
||||
public async Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, HttpMethod httpMethod)
|
||||
{
|
||||
if (options.CacheMode == CacheMode.None)
|
||||
{
|
||||
@@ -263,6 +216,40 @@ namespace Emby.Server.Implementations.HttpClientManager
|
||||
return response;
|
||||
}
|
||||
|
||||
private HttpMethod GetHttpMethod(string httpMethod)
|
||||
{
|
||||
if (httpMethod.Equals("DELETE", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return HttpMethod.Delete;
|
||||
}
|
||||
else if (httpMethod.Equals("GET", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return HttpMethod.Get;
|
||||
}
|
||||
else if (httpMethod.Equals("HEAD", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return HttpMethod.Head;
|
||||
}
|
||||
else if (httpMethod.Equals("OPTIONS", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return HttpMethod.Options;
|
||||
}
|
||||
else if (httpMethod.Equals("POST", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return HttpMethod.Post;
|
||||
}
|
||||
else if (httpMethod.Equals("PUT", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return HttpMethod.Put;
|
||||
}
|
||||
else if (httpMethod.Equals("TRACE", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return HttpMethod.Trace;
|
||||
}
|
||||
|
||||
throw new ArgumentException("Invalid HTTP method", nameof(httpMethod));
|
||||
}
|
||||
|
||||
private HttpResponseInfo GetCachedResponse(string responseCachePath, TimeSpan cacheLength, string url)
|
||||
{
|
||||
if (File.Exists(responseCachePath)
|
||||
@@ -294,186 +281,97 @@ namespace Emby.Server.Implementations.HttpClientManager
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<HttpResponseInfo> SendAsyncInternal(HttpRequestOptions options, string httpMethod)
|
||||
private async Task<HttpResponseInfo> SendAsyncInternal(HttpRequestOptions options, HttpMethod httpMethod)
|
||||
{
|
||||
ValidateParams(options);
|
||||
|
||||
options.CancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var client = GetHttpClient(GetHostFromUrl(options.Url), options.EnableHttpCompression);
|
||||
var client = GetHttpClient(options.Url);
|
||||
|
||||
if ((DateTime.UtcNow - client.LastTimeout).TotalSeconds < TimeoutSeconds)
|
||||
var httpWebRequest = GetRequestMessage(options, httpMethod);
|
||||
|
||||
if (options.RequestContentBytes != null
|
||||
|| !string.IsNullOrEmpty(options.RequestContent)
|
||||
|| httpMethod == HttpMethod.Post)
|
||||
{
|
||||
throw new HttpException(string.Format("Cancelling connection to {0} due to a previous timeout.", options.Url))
|
||||
if (options.RequestContentBytes != null)
|
||||
{
|
||||
IsTimedOut = true
|
||||
};
|
||||
}
|
||||
|
||||
var httpWebRequest = GetRequest(options, httpMethod);
|
||||
|
||||
if (options.RequestContentBytes != null ||
|
||||
!string.IsNullOrEmpty(options.RequestContent) ||
|
||||
string.Equals(httpMethod, "post", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
try
|
||||
{
|
||||
var bytes = options.RequestContentBytes ?? Encoding.UTF8.GetBytes(options.RequestContent ?? string.Empty);
|
||||
|
||||
var contentType = options.RequestContentType ?? "application/x-www-form-urlencoded";
|
||||
|
||||
if (options.AppendCharsetToMimeType)
|
||||
{
|
||||
contentType = contentType.TrimEnd(';') + "; charset=\"utf-8\"";
|
||||
}
|
||||
|
||||
httpWebRequest.ContentType = contentType;
|
||||
(await httpWebRequest.GetRequestStreamAsync().ConfigureAwait(false)).Write(bytes, 0, bytes.Length);
|
||||
httpWebRequest.Content = new ByteArrayContent(options.RequestContentBytes);
|
||||
}
|
||||
catch (Exception ex)
|
||||
else if (options.RequestContent != null)
|
||||
{
|
||||
throw new HttpException(ex.Message) { IsTimedOut = true };
|
||||
httpWebRequest.Content = new StringContent(options.RequestContent);
|
||||
}
|
||||
else
|
||||
{
|
||||
httpWebRequest.Content = new ByteArrayContent(Array.Empty<byte>());
|
||||
}
|
||||
}
|
||||
|
||||
if (options.ResourcePool != null)
|
||||
{
|
||||
await options.ResourcePool.WaitAsync(options.CancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
// TODO: add correct content type
|
||||
/*
|
||||
var contentType = options.RequestContentType ?? "application/x-www-form-urlencoded";
|
||||
|
||||
if ((DateTime.UtcNow - client.LastTimeout).TotalSeconds < TimeoutSeconds)
|
||||
{
|
||||
options.ResourcePool?.Release();
|
||||
if (options.AppendCharsetToMimeType)
|
||||
{
|
||||
contentType = contentType.TrimEnd(';') + "; charset=\"utf-8\"";
|
||||
}
|
||||
|
||||
throw new HttpException($"Connection to {options.Url} timed out") { IsTimedOut = true };
|
||||
httpWebRequest.Headers.Add(HeaderNames.ContentType, contentType);*/
|
||||
}
|
||||
|
||||
if (options.LogRequest)
|
||||
{
|
||||
if (options.LogRequestAsDebug)
|
||||
{
|
||||
_logger.LogDebug("HttpClientManager {0}: {1}", httpMethod.ToUpper(CultureInfo.CurrentCulture), options.Url);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("HttpClientManager {0}: {1}", httpMethod.ToUpper(CultureInfo.CurrentCulture), options.Url);
|
||||
}
|
||||
_logger.LogDebug("HttpClientManager {0}: {1}", httpMethod.ToString(), options.Url);
|
||||
}
|
||||
|
||||
try
|
||||
options.CancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (!options.BufferContent)
|
||||
{
|
||||
var response = await client.SendAsync(httpWebRequest, options.CancellationToken).ConfigureAwait(false);
|
||||
|
||||
await EnsureSuccessStatusCode(response, options).ConfigureAwait(false);
|
||||
|
||||
options.CancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (!options.BufferContent)
|
||||
var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
|
||||
return new HttpResponseInfo(response.Headers)
|
||||
{
|
||||
var response = await GetResponseAsync(httpWebRequest, TimeSpan.FromMilliseconds(options.TimeoutMs)).ConfigureAwait(false);
|
||||
Content = stream,
|
||||
StatusCode = response.StatusCode,
|
||||
ContentType = response.Content.Headers.ContentType?.MediaType,
|
||||
ContentLength = stream.Length,
|
||||
ResponseUrl = response.Content.Headers.ContentLocation?.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
var httpResponse = (HttpWebResponse)response;
|
||||
using (var response = await client.SendAsync(httpWebRequest, options.CancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
await EnsureSuccessStatusCode(response, options).ConfigureAwait(false);
|
||||
|
||||
EnsureSuccessStatusCode(client, httpResponse, options);
|
||||
options.CancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
options.CancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
return GetResponseInfo(httpResponse, httpResponse.GetResponseStream(), GetContentLength(httpResponse), httpResponse);
|
||||
}
|
||||
|
||||
using (var response = await GetResponseAsync(httpWebRequest, TimeSpan.FromMilliseconds(options.TimeoutMs)).ConfigureAwait(false))
|
||||
using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
var httpResponse = (HttpWebResponse)response;
|
||||
var memoryStream = new MemoryStream();
|
||||
await stream.CopyToAsync(memoryStream, StreamDefaults.DefaultCopyToBufferSize, options.CancellationToken).ConfigureAwait(false);
|
||||
memoryStream.Position = 0;
|
||||
|
||||
EnsureSuccessStatusCode(client, httpResponse, options);
|
||||
|
||||
options.CancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
using (var stream = httpResponse.GetResponseStream())
|
||||
return new HttpResponseInfo(response.Headers)
|
||||
{
|
||||
var memoryStream = new MemoryStream();
|
||||
await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
|
||||
|
||||
memoryStream.Position = 0;
|
||||
|
||||
return GetResponseInfo(httpResponse, memoryStream, memoryStream.Length, null);
|
||||
}
|
||||
Content = memoryStream,
|
||||
StatusCode = response.StatusCode,
|
||||
ContentType = response.Content.Headers.ContentType?.MediaType,
|
||||
ContentLength = memoryStream.Length,
|
||||
ResponseUrl = response.Content.Headers.ContentLocation?.ToString()
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
throw GetCancellationException(options, client, options.CancellationToken, ex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw GetException(ex, options, client);
|
||||
}
|
||||
finally
|
||||
{
|
||||
options.ResourcePool?.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private HttpResponseInfo GetResponseInfo(HttpWebResponse httpResponse, Stream content, long? contentLength, IDisposable disposable)
|
||||
{
|
||||
var responseInfo = new HttpResponseInfo(disposable)
|
||||
{
|
||||
Content = content,
|
||||
StatusCode = httpResponse.StatusCode,
|
||||
ContentType = httpResponse.ContentType,
|
||||
ContentLength = contentLength,
|
||||
ResponseUrl = httpResponse.ResponseUri.ToString()
|
||||
};
|
||||
|
||||
if (httpResponse.Headers != null)
|
||||
{
|
||||
SetHeaders(httpResponse.Headers, responseInfo);
|
||||
}
|
||||
|
||||
return responseInfo;
|
||||
}
|
||||
|
||||
private HttpResponseInfo GetResponseInfo(HttpWebResponse httpResponse, string tempFile, long? contentLength)
|
||||
{
|
||||
var responseInfo = new HttpResponseInfo
|
||||
{
|
||||
TempFilePath = tempFile,
|
||||
StatusCode = httpResponse.StatusCode,
|
||||
ContentType = httpResponse.ContentType,
|
||||
ContentLength = contentLength
|
||||
};
|
||||
|
||||
if (httpResponse.Headers != null)
|
||||
{
|
||||
SetHeaders(httpResponse.Headers, responseInfo);
|
||||
}
|
||||
|
||||
return responseInfo;
|
||||
}
|
||||
|
||||
private static void SetHeaders(WebHeaderCollection headers, HttpResponseInfo responseInfo)
|
||||
{
|
||||
foreach (var key in headers.AllKeys)
|
||||
{
|
||||
responseInfo.Headers[key] = headers[key];
|
||||
}
|
||||
}
|
||||
|
||||
public Task<HttpResponseInfo> Post(HttpRequestOptions options)
|
||||
{
|
||||
return SendAsync(options, "POST");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs a POST request
|
||||
/// </summary>
|
||||
/// <param name="options">The options.</param>
|
||||
/// <param name="postData">Params to add to the POST data.</param>
|
||||
/// <returns>stream on success, null on failure</returns>
|
||||
public async Task<Stream> Post(HttpRequestOptions options, Dictionary<string, string> postData)
|
||||
{
|
||||
options.SetPostData(postData);
|
||||
|
||||
var response = await Post(options).ConfigureAwait(false);
|
||||
|
||||
return response.Content;
|
||||
}
|
||||
=> SendAsync(options, HttpMethod.Post);
|
||||
|
||||
/// <summary>
|
||||
/// Downloads the contents of a given url into a temporary location
|
||||
@@ -483,7 +381,6 @@ namespace Emby.Server.Implementations.HttpClientManager
|
||||
public async Task<string> GetTempFile(HttpRequestOptions options)
|
||||
{
|
||||
var response = await GetTempFileResponse(options).ConfigureAwait(false);
|
||||
|
||||
return response.TempFilePath;
|
||||
}
|
||||
|
||||
@@ -502,44 +399,28 @@ namespace Emby.Server.Implementations.HttpClientManager
|
||||
|
||||
options.CancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var httpWebRequest = GetRequest(options, "GET");
|
||||
|
||||
if (options.ResourcePool != null)
|
||||
{
|
||||
await options.ResourcePool.WaitAsync(options.CancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
var httpWebRequest = GetRequestMessage(options, HttpMethod.Get);
|
||||
|
||||
options.Progress.Report(0);
|
||||
|
||||
if (options.LogRequest)
|
||||
{
|
||||
if (options.LogRequestAsDebug)
|
||||
{
|
||||
_logger.LogDebug("HttpClientManager.GetTempFileResponse url: {0}", options.Url);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("HttpClientManager.GetTempFileResponse url: {0}", options.Url);
|
||||
}
|
||||
_logger.LogDebug("HttpClientManager.GetTempFileResponse url: {0}", options.Url);
|
||||
}
|
||||
|
||||
var client = GetHttpClient(GetHostFromUrl(options.Url), options.EnableHttpCompression);
|
||||
var client = GetHttpClient(options.Url);
|
||||
|
||||
try
|
||||
{
|
||||
options.CancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
using (var response = await httpWebRequest.GetResponseAsync().ConfigureAwait(false))
|
||||
using (var response = (await client.SendAsync(httpWebRequest, options.CancellationToken).ConfigureAwait(false)))
|
||||
{
|
||||
var httpResponse = (HttpWebResponse)response;
|
||||
|
||||
EnsureSuccessStatusCode(client, httpResponse, options);
|
||||
await EnsureSuccessStatusCode(response, options).ConfigureAwait(false);
|
||||
|
||||
options.CancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var contentLength = GetContentLength(httpResponse);
|
||||
|
||||
using (var stream = httpResponse.GetResponseStream())
|
||||
using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
|
||||
using (var fs = _fileSystem.GetFileStream(tempFile, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true))
|
||||
{
|
||||
await stream.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, options.CancellationToken).ConfigureAwait(false);
|
||||
@@ -547,35 +428,29 @@ namespace Emby.Server.Implementations.HttpClientManager
|
||||
|
||||
options.Progress.Report(100);
|
||||
|
||||
return GetResponseInfo(httpResponse, tempFile, contentLength);
|
||||
var responseInfo = new HttpResponseInfo(response.Headers)
|
||||
{
|
||||
TempFilePath = tempFile,
|
||||
StatusCode = response.StatusCode,
|
||||
ContentType = response.Content.Headers.ContentType?.MediaType,
|
||||
ContentLength = response.Content.Headers.ContentLength
|
||||
};
|
||||
|
||||
return responseInfo;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DeleteTempFile(tempFile);
|
||||
throw GetException(ex, options, client);
|
||||
}
|
||||
finally
|
||||
{
|
||||
options.ResourcePool?.Release();
|
||||
if (File.Exists(tempFile))
|
||||
{
|
||||
File.Delete(tempFile);
|
||||
}
|
||||
|
||||
throw GetException(ex, options);
|
||||
}
|
||||
}
|
||||
|
||||
private static long? GetContentLength(HttpWebResponse response)
|
||||
{
|
||||
var length = response.ContentLength;
|
||||
|
||||
if (length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
protected static readonly CultureInfo UsCulture = new CultureInfo("en-US");
|
||||
|
||||
private Exception GetException(Exception ex, HttpRequestOptions options, HttpClientInfo client)
|
||||
private Exception GetException(Exception ex, HttpRequestOptions options)
|
||||
{
|
||||
if (ex is HttpException)
|
||||
{
|
||||
@@ -589,7 +464,7 @@ namespace Emby.Server.Implementations.HttpClientManager
|
||||
{
|
||||
if (options.LogErrors)
|
||||
{
|
||||
_logger.LogError(webException, "Error {status} getting response from {url}", webException.Status, options.Url);
|
||||
_logger.LogError(webException, "Error {Status} getting response from {Url}", webException.Status, options.Url);
|
||||
}
|
||||
|
||||
var exception = new HttpException(webException.Message, webException);
|
||||
@@ -599,11 +474,6 @@ namespace Emby.Server.Implementations.HttpClientManager
|
||||
if (response != null)
|
||||
{
|
||||
exception.StatusCode = response.StatusCode;
|
||||
|
||||
if ((int)response.StatusCode == 429)
|
||||
{
|
||||
client.LastTimeout = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -624,29 +494,17 @@ namespace Emby.Server.Implementations.HttpClientManager
|
||||
|
||||
if (operationCanceledException != null)
|
||||
{
|
||||
return GetCancellationException(options, client, options.CancellationToken, operationCanceledException);
|
||||
return GetCancellationException(options, options.CancellationToken, operationCanceledException);
|
||||
}
|
||||
|
||||
if (options.LogErrors)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting response from {url}", options.Url);
|
||||
_logger.LogError(ex, "Error getting response from {Url}", options.Url);
|
||||
}
|
||||
|
||||
return ex;
|
||||
}
|
||||
|
||||
private void DeleteTempFile(string file)
|
||||
{
|
||||
try
|
||||
{
|
||||
_fileSystem.DeleteFile(file);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// Might not have been created at all. No need to worry.
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateParams(HttpRequestOptions options)
|
||||
{
|
||||
if (string.IsNullOrEmpty(options.Url))
|
||||
@@ -682,11 +540,10 @@ namespace Emby.Server.Implementations.HttpClientManager
|
||||
/// Throws the cancellation exception.
|
||||
/// </summary>
|
||||
/// <param name="options">The options.</param>
|
||||
/// <param name="client">The client.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="exception">The exception.</param>
|
||||
/// <returns>Exception.</returns>
|
||||
private Exception GetCancellationException(HttpRequestOptions options, HttpClientInfo client, CancellationToken cancellationToken, OperationCanceledException exception)
|
||||
private Exception GetCancellationException(HttpRequestOptions options, CancellationToken cancellationToken, OperationCanceledException exception)
|
||||
{
|
||||
// If the HttpClient's timeout is reached, it will cancel the Task internally
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
@@ -698,8 +555,6 @@ namespace Emby.Server.Implementations.HttpClientManager
|
||||
_logger.LogError(msg);
|
||||
}
|
||||
|
||||
client.LastTimeout = DateTime.UtcNow;
|
||||
|
||||
// Throw an HttpException so that the caller doesn't think it was cancelled by user code
|
||||
return new HttpException(msg, exception)
|
||||
{
|
||||
@@ -710,91 +565,20 @@ namespace Emby.Server.Implementations.HttpClientManager
|
||||
return exception;
|
||||
}
|
||||
|
||||
private void EnsureSuccessStatusCode(HttpClientInfo client, HttpWebResponse response, HttpRequestOptions options)
|
||||
private async Task EnsureSuccessStatusCode(HttpResponseMessage response, HttpRequestOptions options)
|
||||
{
|
||||
var statusCode = response.StatusCode;
|
||||
|
||||
var isSuccessful = statusCode >= HttpStatusCode.OK && statusCode <= (HttpStatusCode)299;
|
||||
|
||||
if (isSuccessful)
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.LogErrorResponseBody)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var stream = response.GetResponseStream())
|
||||
{
|
||||
if (stream != null)
|
||||
{
|
||||
using (var reader = new StreamReader(stream))
|
||||
{
|
||||
var msg = reader.ReadToEnd();
|
||||
var msg = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
_logger.LogError("HTTP request failed with message: {Message}", msg);
|
||||
|
||||
_logger.LogError(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
throw new HttpException(response.StatusDescription)
|
||||
throw new HttpException(response.ReasonPhrase)
|
||||
{
|
||||
StatusCode = response.StatusCode
|
||||
};
|
||||
}
|
||||
|
||||
private static Task<WebResponse> GetResponseAsync(WebRequest request, TimeSpan timeout)
|
||||
{
|
||||
var taskCompletion = new TaskCompletionSource<WebResponse>();
|
||||
|
||||
var asyncTask = Task.Factory.FromAsync(request.BeginGetResponse, request.EndGetResponse, null);
|
||||
|
||||
ThreadPool.RegisterWaitForSingleObject((asyncTask as IAsyncResult).AsyncWaitHandle, TimeoutCallback, request, timeout, true);
|
||||
var callback = new TaskCallback { taskCompletion = taskCompletion };
|
||||
asyncTask.ContinueWith(callback.OnSuccess, TaskContinuationOptions.NotOnFaulted);
|
||||
|
||||
// Handle errors
|
||||
asyncTask.ContinueWith(callback.OnError, TaskContinuationOptions.OnlyOnFaulted);
|
||||
|
||||
return taskCompletion.Task;
|
||||
}
|
||||
|
||||
private static void TimeoutCallback(object state, bool timedOut)
|
||||
{
|
||||
if (timedOut && state != null)
|
||||
{
|
||||
var request = (WebRequest)state;
|
||||
request.Abort();
|
||||
}
|
||||
}
|
||||
|
||||
private class TaskCallback
|
||||
{
|
||||
public TaskCompletionSource<WebResponse> taskCompletion;
|
||||
|
||||
public void OnSuccess(Task<WebResponse> task)
|
||||
{
|
||||
taskCompletion.TrySetResult(task.Result);
|
||||
}
|
||||
|
||||
public void OnError(Task<WebResponse> task)
|
||||
{
|
||||
if (task.Exception == null)
|
||||
{
|
||||
taskCompletion.TrySetException(Enumerable.Empty<Exception>());
|
||||
}
|
||||
else
|
||||
{
|
||||
taskCompletion.TrySetException(task.Exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
|
||||
if (string.IsNullOrWhiteSpace(rangeHeader))
|
||||
{
|
||||
Headers[HeaderNames.ContentLength] = TotalContentLength.ToString(CultureInfo.InvariantCulture);
|
||||
StatusCode = HttpStatusCode.OK;
|
||||
}
|
||||
else
|
||||
@@ -99,10 +100,13 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
RangeStart = requestedRange.Key;
|
||||
RangeLength = 1 + RangeEnd - RangeStart;
|
||||
|
||||
// Content-Length is the length of what we're serving, not the original content
|
||||
var lengthString = RangeLength.ToString(CultureInfo.InvariantCulture);
|
||||
Headers[HeaderNames.ContentLength] = lengthString;
|
||||
var rangeString = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}";
|
||||
Headers[HeaderNames.ContentRange] = rangeString;
|
||||
|
||||
Logger.LogInformation("Setting range response values for {0}. RangeRequest: {1} Content-Range: {2}", Path, RangeHeader, rangeString);
|
||||
Logger.LogInformation("Setting range response values for {0}. RangeRequest: {1} Content-Length: {2}, Content-Range: {3}", Path, RangeHeader, lengthString, rangeString);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -638,6 +638,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
private static Task Write(IResponse response, string text)
|
||||
{
|
||||
var bOutput = Encoding.UTF8.GetBytes(text);
|
||||
response.OriginalResponse.ContentLength = bOutput.Length;
|
||||
return response.OutputStream.WriteAsync(bOutput, 0, bOutput.Length);
|
||||
}
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
content = Array.Empty<byte>();
|
||||
}
|
||||
|
||||
result = new StreamWriter(content, contentType);
|
||||
result = new StreamWriter(content, contentType, contentLength);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -176,7 +176,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
bytes = Array.Empty<byte>();
|
||||
}
|
||||
|
||||
result = new StreamWriter(bytes, contentType);
|
||||
result = new StreamWriter(bytes, contentType, contentLength);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -335,13 +335,13 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
|
||||
if (isHeadRequest)
|
||||
{
|
||||
var result = new StreamWriter(Array.Empty<byte>(), contentType);
|
||||
var result = new StreamWriter(Array.Empty<byte>(), contentType, contentLength);
|
||||
AddResponseHeaders(result, responseHeaders);
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = new StreamWriter(content, contentType);
|
||||
var result = new StreamWriter(content, contentType, contentLength);
|
||||
AddResponseHeaders(result, responseHeaders);
|
||||
return result;
|
||||
}
|
||||
@@ -581,6 +581,11 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
}
|
||||
else
|
||||
{
|
||||
if (totalContentLength.HasValue)
|
||||
{
|
||||
responseHeaders["Content-Length"] = totalContentLength.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (isHeadRequest)
|
||||
{
|
||||
using (stream)
|
||||
@@ -624,7 +629,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
|
||||
if (lastModifiedDate.HasValue)
|
||||
{
|
||||
responseHeaders[HeaderNames.LastModified] = lastModifiedDate.ToString();
|
||||
responseHeaders[HeaderNames.LastModified] = lastModifiedDate.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -96,6 +96,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
RangeStart = requestedRange.Key;
|
||||
RangeLength = 1 + RangeEnd - RangeStart;
|
||||
|
||||
Headers[HeaderNames.ContentLength] = RangeLength.ToString(CultureInfo.InvariantCulture);
|
||||
Headers[HeaderNames.ContentRange] = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}";
|
||||
|
||||
if (RangeStart > 0 && SourceStream.CanSeek)
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
public void FilterResponse(IRequest req, IResponse res, object dto)
|
||||
{
|
||||
// Try to prevent compatibility view
|
||||
res.AddHeader("Access-Control-Allow-Headers", "Accept, Accept-Language, Authorization, Cache-Control, Content-Disposition, Content-Encoding, Content-Language, Content-MD5, Content-Range, Content-Type, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, X-Emby-Authorization");
|
||||
res.AddHeader("Access-Control-Allow-Headers", "Accept, Accept-Language, Authorization, Cache-Control, Content-Disposition, Content-Encoding, Content-Language, Content-Length, Content-MD5, Content-Range, Content-Type, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, X-Emby-Authorization");
|
||||
res.AddHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
|
||||
res.AddHeader("Access-Control-Allow-Origin", "*");
|
||||
|
||||
@@ -58,6 +58,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
|
||||
if (length > 0)
|
||||
{
|
||||
res.OriginalResponse.ContentLength = length;
|
||||
res.SendChunked = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -49,6 +50,13 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
|
||||
SourceStream = source;
|
||||
|
||||
Headers["Content-Type"] = contentType;
|
||||
|
||||
if (source.CanSeek)
|
||||
{
|
||||
Headers[HeaderNames.ContentLength] = source.Length.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
Headers[HeaderNames.ContentType] = contentType;
|
||||
}
|
||||
|
||||
@@ -57,7 +65,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// </summary>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="contentType">Type of the content.</param>
|
||||
public StreamWriter(byte[] source, string contentType)
|
||||
public StreamWriter(byte[] source, string contentType, int contentLength)
|
||||
{
|
||||
if (string.IsNullOrEmpty(contentType))
|
||||
{
|
||||
@@ -66,6 +74,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
|
||||
SourceBytes = source;
|
||||
|
||||
Headers[HeaderNames.ContentLength] = contentLength.ToString(CultureInfo.InvariantCulture);
|
||||
Headers[HeaderNames.ContentType] = contentType;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Emby.Server.Implementations.Library
|
||||
public string Name => "Default";
|
||||
|
||||
public bool IsEnabled => true;
|
||||
|
||||
|
||||
// This is dumb and an artifact of the backwards way auth providers were designed.
|
||||
// This version of authenticate was never meant to be called, but needs to be here for interface compat
|
||||
// Only the providers that don't provide local user support use this
|
||||
@@ -27,7 +27,7 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
|
||||
// This is the verson that we need to use for local users. Because reasons.
|
||||
public Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser)
|
||||
{
|
||||
@@ -103,7 +103,7 @@ namespace Emby.Server.Implementations.Library
|
||||
string hash = user.Password;
|
||||
user.Password = string.Format("$SHA1${0}", hash);
|
||||
}
|
||||
|
||||
|
||||
if (user.EasyPassword != null && !user.EasyPassword.Contains("$"))
|
||||
{
|
||||
string hash = user.EasyPassword;
|
||||
@@ -165,6 +165,34 @@ namespace Emby.Server.Implementations.Library
|
||||
return user.Password;
|
||||
}
|
||||
|
||||
public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash)
|
||||
{
|
||||
ConvertPasswordFormat(user);
|
||||
|
||||
if (newPassword != null)
|
||||
{
|
||||
newPasswordHash = string.Format("$SHA1${0}", GetHashedString(user, newPassword));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(newPasswordHash))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(newPasswordHash));
|
||||
}
|
||||
|
||||
user.EasyPassword = newPasswordHash;
|
||||
}
|
||||
|
||||
public string GetEasyPasswordHash(User user)
|
||||
{
|
||||
// This should be removed in the future. This was added to let user login after
|
||||
// Jellyfin 10.3.3 failed to save a well formatted PIN.
|
||||
ConvertPasswordFormat(user);
|
||||
|
||||
return string.IsNullOrEmpty(user.EasyPassword)
|
||||
? null
|
||||
: (new PasswordHash(user.EasyPassword)).Hash;
|
||||
}
|
||||
|
||||
public string GetHashedStringChangeAuth(string newPassword, PasswordHash passwordHash)
|
||||
{
|
||||
passwordHash.HashBytes = Encoding.UTF8.GetBytes(newPassword);
|
||||
|
||||
47
Emby.Server.Implementations/Library/InvalidAuthProvider.cs
Normal file
47
Emby.Server.Implementations/Library/InvalidAuthProvider.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Authentication;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Net;
|
||||
|
||||
namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
public class InvalidAuthProvider : IAuthenticationProvider
|
||||
{
|
||||
public string Name => "InvalidOrMissingAuthenticationProvider";
|
||||
|
||||
public bool IsEnabled => true;
|
||||
|
||||
public Task<ProviderAuthenticationResult> Authenticate(string username, string password)
|
||||
{
|
||||
throw new SecurityException("User Account cannot login with this provider. The Normal provider for this user cannot be found");
|
||||
}
|
||||
|
||||
public Task<bool> HasPassword(User user)
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public Task ChangePassword(User user, string newPassword)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash)
|
||||
{
|
||||
// Nothing here
|
||||
}
|
||||
|
||||
public string GetPasswordHash(User user)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public string GetEasyPasswordHash(User user)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,6 +79,8 @@ namespace Emby.Server.Implementations.Library
|
||||
private IAuthenticationProvider[] _authenticationProviders;
|
||||
private DefaultAuthenticationProvider _defaultAuthenticationProvider;
|
||||
|
||||
private InvalidAuthProvider _invalidAuthProvider;
|
||||
|
||||
private IPasswordResetProvider[] _passwordResetProviders;
|
||||
private DefaultPasswordResetProvider _defaultPasswordResetProvider;
|
||||
|
||||
@@ -141,6 +143,8 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
_defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
|
||||
|
||||
_invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
|
||||
|
||||
_passwordResetProviders = passwordResetProviders.ToArray();
|
||||
|
||||
_defaultPasswordResetProvider = passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
|
||||
@@ -307,8 +311,7 @@ namespace Emby.Server.Implementations.Library
|
||||
user = Users
|
||||
.FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
var hasNewUserPolicy = authenticationProvider as IHasNewUserPolicy;
|
||||
if (hasNewUserPolicy != null)
|
||||
if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy)
|
||||
{
|
||||
var policy = hasNewUserPolicy.GetNewUserPolicy();
|
||||
UpdateUserPolicy(user, policy, true);
|
||||
@@ -400,7 +403,9 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
if (providers.Length == 0)
|
||||
{
|
||||
providers = new IAuthenticationProvider[] { _defaultAuthenticationProvider };
|
||||
// Assign the user to the InvalidAuthProvider since no configured auth provider was valid/found
|
||||
_logger.LogWarning("User {UserName} was found with invalid/missing Authentication Provider {AuthenticationProviderId}. Assigning user to InvalidAuthProvider until this is corrected", user.Name, user.Policy.AuthenticationProviderId);
|
||||
providers = new IAuthenticationProvider[] { _invalidAuthProvider };
|
||||
}
|
||||
|
||||
return providers;
|
||||
@@ -471,7 +476,7 @@ namespace Emby.Server.Implementations.Library
|
||||
if (password == null)
|
||||
{
|
||||
// legacy
|
||||
success = string.Equals(_defaultAuthenticationProvider.GetPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase);
|
||||
success = string.Equals(GetAuthenticationProvider(user).GetPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -497,11 +502,11 @@ namespace Emby.Server.Implementations.Library
|
||||
if (password == null)
|
||||
{
|
||||
// legacy
|
||||
success = string.Equals(GetLocalPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase);
|
||||
success = string.Equals(GetAuthenticationProvider(user).GetEasyPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
else
|
||||
{
|
||||
success = string.Equals(GetLocalPasswordHash(user), _defaultAuthenticationProvider.GetHashedString(user, password), StringComparison.OrdinalIgnoreCase);
|
||||
success = string.Equals(GetAuthenticationProvider(user).GetEasyPasswordHash(user), _defaultAuthenticationProvider.GetHashedString(user, password), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -546,13 +551,6 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
private string GetLocalPasswordHash(User user)
|
||||
{
|
||||
return string.IsNullOrEmpty(user.EasyPassword)
|
||||
? null
|
||||
: user.EasyPassword;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the users from the repository
|
||||
/// </summary>
|
||||
@@ -596,7 +594,7 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
|
||||
bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result;
|
||||
bool hasConfiguredEasyPassword = string.IsNullOrEmpty(GetLocalPasswordHash(user));
|
||||
bool hasConfiguredEasyPassword = !string.IsNullOrEmpty(GetAuthenticationProvider(user).GetEasyPasswordHash(user));
|
||||
|
||||
bool hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ?
|
||||
hasConfiguredEasyPassword :
|
||||
@@ -884,17 +882,7 @@ namespace Emby.Server.Implementations.Library
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
if (newPassword != null)
|
||||
{
|
||||
newPasswordHash = _defaultAuthenticationProvider.GetHashedString(user, newPassword);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(newPasswordHash))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(newPasswordHash));
|
||||
}
|
||||
|
||||
user.EasyPassword = newPasswordHash;
|
||||
GetAuthenticationProvider(user).ChangeEasyPassword(user, newPassword, newPasswordHash);
|
||||
|
||||
UpdateUser(user);
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
UserAgent = "Emby/3.0",
|
||||
|
||||
// Shouldn't matter but may cause issues
|
||||
EnableHttpCompression = false
|
||||
DecompressionMethod = CompressionMethod.None
|
||||
};
|
||||
|
||||
using (var response = await _httpClient.SendAsync(httpRequestOptions, "GET").ConfigureAwait(false))
|
||||
|
||||
@@ -96,8 +96,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
Url = ApiUrl + "/schedules",
|
||||
UserAgent = UserAgent,
|
||||
CancellationToken = cancellationToken,
|
||||
// The data can be large so give it some extra time
|
||||
TimeoutMs = 60000,
|
||||
LogErrorResponseBody = true,
|
||||
RequestContent = requestString
|
||||
};
|
||||
@@ -115,9 +113,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
Url = ApiUrl + "/programs",
|
||||
UserAgent = UserAgent,
|
||||
CancellationToken = cancellationToken,
|
||||
LogErrorResponseBody = true,
|
||||
// The data can be large so give it some extra time
|
||||
TimeoutMs = 60000
|
||||
LogErrorResponseBody = true
|
||||
};
|
||||
|
||||
httpOptions.RequestHeaders["token"] = token;
|
||||
@@ -483,8 +479,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
CancellationToken = cancellationToken,
|
||||
RequestContent = imageIdString,
|
||||
LogErrorResponseBody = true,
|
||||
// The data can be large so give it some extra time
|
||||
TimeoutMs = 60000
|
||||
};
|
||||
|
||||
try
|
||||
@@ -633,15 +627,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
ListingsProviderInfo providerInfo)
|
||||
{
|
||||
// Schedules direct requires that the client support compression and will return a 400 response without it
|
||||
options.EnableHttpCompression = true;
|
||||
|
||||
// On windows 7 under .net core, this header is not getting added
|
||||
#if NETSTANDARD2_0
|
||||
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
|
||||
{
|
||||
options.RequestHeaders[HeaderNames.AcceptEncoding] = "deflate";
|
||||
}
|
||||
#endif
|
||||
options.DecompressionMethod = CompressionMethod.Deflate;
|
||||
|
||||
try
|
||||
{
|
||||
@@ -671,15 +657,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
ListingsProviderInfo providerInfo)
|
||||
{
|
||||
// Schedules direct requires that the client support compression and will return a 400 response without it
|
||||
options.EnableHttpCompression = true;
|
||||
|
||||
// On windows 7 under .net core, this header is not getting added
|
||||
#if NETSTANDARD2_0
|
||||
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
|
||||
{
|
||||
options.RequestHeaders[HeaderNames.AcceptEncoding] = "deflate";
|
||||
}
|
||||
#endif
|
||||
options.DecompressionMethod = CompressionMethod.Deflate;
|
||||
|
||||
try
|
||||
{
|
||||
@@ -871,8 +849,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
UserAgent = UserAgent,
|
||||
CancellationToken = cancellationToken,
|
||||
LogErrorResponseBody = true,
|
||||
// The data can be large so give it some extra time
|
||||
TimeoutMs = 60000
|
||||
};
|
||||
|
||||
httpOptions.RequestHeaders["token"] = token;
|
||||
|
||||
@@ -73,11 +73,9 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
CancellationToken = cancellationToken,
|
||||
Url = path,
|
||||
Progress = new SimpleProgress<double>(),
|
||||
DecompressionMethod = CompressionMethod.Gzip,
|
||||
|
||||
// It's going to come back gzipped regardless of this value
|
||||
// So we need to make sure the decompression method is set to gzip
|
||||
EnableHttpCompression = true,
|
||||
DecompressionMethod = CompressionMethod.Gzip,
|
||||
|
||||
UserAgent = "Emby/3.0"
|
||||
|
||||
|
||||
@@ -138,7 +138,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
{
|
||||
Url = string.Format("{0}/discover.json", GetApiUrl(info)),
|
||||
CancellationToken = cancellationToken,
|
||||
TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(10).TotalMilliseconds),
|
||||
BufferContent = false
|
||||
|
||||
}, "GET").ConfigureAwait(false))
|
||||
@@ -191,7 +190,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
{
|
||||
Url = string.Format("{0}/tuners.html", GetApiUrl(info)),
|
||||
CancellationToken = cancellationToken,
|
||||
TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds),
|
||||
BufferContent = false
|
||||
}))
|
||||
{
|
||||
|
||||
@@ -47,13 +47,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
CancellationToken = CancellationToken.None,
|
||||
BufferContent = false,
|
||||
|
||||
// Increase a little bit
|
||||
TimeoutMs = 30000,
|
||||
|
||||
EnableHttpCompression = false,
|
||||
|
||||
LogResponse = true,
|
||||
LogResponseHeaders = true
|
||||
DecompressionMethod = CompressionMethod.None,
|
||||
};
|
||||
|
||||
foreach (var header in mediaSource.RequiredHttpHeaders)
|
||||
|
||||
@@ -43,6 +43,11 @@ namespace Emby.Server.Implementations.Services
|
||||
{
|
||||
var contentLength = bytesResponse.Length;
|
||||
|
||||
if (response != null)
|
||||
{
|
||||
response.OriginalResponse.ContentLength = contentLength;
|
||||
}
|
||||
|
||||
if (contentLength > 0)
|
||||
{
|
||||
await responseStream.WriteAsync(bytesResponse, 0, contentLength, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
@@ -20,6 +21,8 @@ namespace Emby.Server.Implementations.Services
|
||||
{
|
||||
response.StatusCode = (int)HttpStatusCode.NoContent;
|
||||
}
|
||||
|
||||
response.OriginalResponse.ContentLength = 0;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -39,11 +42,6 @@ namespace Emby.Server.Implementations.Services
|
||||
|
||||
response.StatusCode = httpResult.Status;
|
||||
response.StatusDescription = httpResult.StatusCode.ToString();
|
||||
//if (string.IsNullOrEmpty(httpResult.ContentType))
|
||||
//{
|
||||
// httpResult.ContentType = defaultContentType;
|
||||
//}
|
||||
//response.ContentType = httpResult.ContentType;
|
||||
}
|
||||
|
||||
var responseOptions = result as IHasHeaders;
|
||||
@@ -53,6 +51,7 @@ namespace Emby.Server.Implementations.Services
|
||||
{
|
||||
if (string.Equals(responseHeaders.Key, "Content-Length", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
response.OriginalResponse.ContentLength = long.Parse(responseHeaders.Value, CultureInfo.InvariantCulture);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -72,52 +71,37 @@ namespace Emby.Server.Implementations.Services
|
||||
response.ContentType += "; charset=utf-8";
|
||||
}
|
||||
|
||||
var asyncStreamWriter = result as IAsyncStreamWriter;
|
||||
if (asyncStreamWriter != null)
|
||||
switch (result)
|
||||
{
|
||||
return asyncStreamWriter.WriteToAsync(response.OutputStream, cancellationToken);
|
||||
}
|
||||
case IAsyncStreamWriter asyncStreamWriter:
|
||||
return asyncStreamWriter.WriteToAsync(response.OutputStream, cancellationToken);
|
||||
case IStreamWriter streamWriter:
|
||||
streamWriter.WriteTo(response.OutputStream);
|
||||
return Task.CompletedTask;
|
||||
case FileWriter fileWriter:
|
||||
return fileWriter.WriteToAsync(response, cancellationToken);
|
||||
case Stream stream:
|
||||
return CopyStream(stream, response.OutputStream);
|
||||
case byte[] bytes:
|
||||
response.ContentType = "application/octet-stream";
|
||||
response.OriginalResponse.ContentLength = bytes.Length;
|
||||
|
||||
var streamWriter = result as IStreamWriter;
|
||||
if (streamWriter != null)
|
||||
{
|
||||
streamWriter.WriteTo(response.OutputStream);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
if (bytes.Length > 0)
|
||||
{
|
||||
return response.OutputStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken);
|
||||
}
|
||||
|
||||
var fileWriter = result as FileWriter;
|
||||
if (fileWriter != null)
|
||||
{
|
||||
return fileWriter.WriteToAsync(response, cancellationToken);
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
case string responseText:
|
||||
var responseTextAsBytes = Encoding.UTF8.GetBytes(responseText);
|
||||
response.OriginalResponse.ContentLength = responseTextAsBytes.Length;
|
||||
|
||||
var stream = result as Stream;
|
||||
if (stream != null)
|
||||
{
|
||||
return CopyStream(stream, response.OutputStream);
|
||||
}
|
||||
if (responseTextAsBytes.Length > 0)
|
||||
{
|
||||
return response.OutputStream.WriteAsync(responseTextAsBytes, 0, responseTextAsBytes.Length, cancellationToken);
|
||||
}
|
||||
|
||||
var bytes = result as byte[];
|
||||
if (bytes != null)
|
||||
{
|
||||
response.ContentType = "application/octet-stream";
|
||||
|
||||
if (bytes.Length > 0)
|
||||
{
|
||||
return response.OutputStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken);
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var responseText = result as string;
|
||||
if (responseText != null)
|
||||
{
|
||||
bytes = Encoding.UTF8.GetBytes(responseText);
|
||||
if (bytes.Length > 0)
|
||||
{
|
||||
return response.OutputStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken);
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
return WriteObject(request, result, response);
|
||||
@@ -143,14 +127,13 @@ namespace Emby.Server.Implementations.Services
|
||||
ms.Position = 0;
|
||||
|
||||
var contentLength = ms.Length;
|
||||
response.OriginalResponse.ContentLength = contentLength;
|
||||
|
||||
if (contentLength > 0)
|
||||
{
|
||||
await ms.CopyToAsync(response.OutputStream).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
//serializer(result, outputStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ namespace Emby.Server.Implementations.Services
|
||||
string propertyName = pair.Key;
|
||||
string propertyTextValue = pair.Value;
|
||||
|
||||
if (string.IsNullOrEmpty(propertyTextValue)
|
||||
if (propertyTextValue == null
|
||||
|| !propertySetterMap.TryGetValue(propertyName, out propertySerializerEntry)
|
||||
|| propertySerializerEntry.PropertySetFn == null)
|
||||
{
|
||||
|
||||
@@ -185,6 +185,11 @@ namespace Jellyfin.Drawing.Skia
|
||||
|
||||
public ImageDimensions GetImageSize(string path)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
throw new FileNotFoundException("File not found", path);
|
||||
}
|
||||
|
||||
using (var s = new SKFileStream(path))
|
||||
using (var codec = SKCodec.Create(s))
|
||||
{
|
||||
|
||||
@@ -118,8 +118,20 @@ namespace Jellyfin.Server
|
||||
|
||||
SQLitePCL.Batteries_V2.Init();
|
||||
|
||||
// Increase the max http request limit
|
||||
// The default connection limit is 10 for ASP.NET hosted applications and 2 for all others.
|
||||
ServicePointManager.DefaultConnectionLimit = Math.Max(96, ServicePointManager.DefaultConnectionLimit);
|
||||
|
||||
// Disable the "Expect: 100-Continue" header by default
|
||||
// http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c
|
||||
ServicePointManager.Expect100Continue = false;
|
||||
|
||||
// CA5359: Do Not Disable Certificate Validation
|
||||
#pragma warning disable CA5359
|
||||
|
||||
// Allow all https requests
|
||||
ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(delegate { return true; });
|
||||
#pragma warning restore CA5359
|
||||
|
||||
var fileSystem = new ManagedFileSystem(_loggerFactory, appPaths);
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Api.Movies;
|
||||
@@ -828,7 +830,16 @@ namespace MediaBrowser.Api.Library
|
||||
var filename = (Path.GetFileName(path) ?? string.Empty).Replace("\"", string.Empty);
|
||||
if (!string.IsNullOrWhiteSpace(filename))
|
||||
{
|
||||
headers[HeaderNames.ContentDisposition] = "attachment; filename=\"" + filename + "\"";
|
||||
// Kestrel doesn't support non-ASCII characters in headers
|
||||
if (Regex.IsMatch(filename, "[^[:ascii:]]"))
|
||||
{
|
||||
// Manually encoding non-ASCII characters, following https://tools.ietf.org/html/rfc5987#section-3.2.2
|
||||
headers[HeaderNames.ContentDisposition] = "attachment; filename*=UTF-8''" + WebUtility.UrlEncode(filename);
|
||||
}
|
||||
else
|
||||
{
|
||||
headers[HeaderNames.ContentDisposition] = "attachment; filename=\"" + filename + "\"";
|
||||
}
|
||||
}
|
||||
|
||||
return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -13,6 +14,7 @@ using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Model.Services;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace MediaBrowser.Api.Playback.Progressive
|
||||
@@ -279,10 +281,7 @@ namespace MediaBrowser.Api.Playback.Progressive
|
||||
/// <returns>Task{System.Object}.</returns>
|
||||
private async Task<object> GetStaticRemoteStreamResult(StreamState state, Dictionary<string, string> responseHeaders, bool isHeadRequest, CancellationTokenSource cancellationTokenSource)
|
||||
{
|
||||
string useragent = null;
|
||||
state.RemoteHttpHeaders.TryGetValue("User-Agent", out useragent);
|
||||
|
||||
var trySupportSeek = false;
|
||||
state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent);
|
||||
|
||||
var options = new HttpRequestOptions
|
||||
{
|
||||
@@ -292,29 +291,14 @@ namespace MediaBrowser.Api.Playback.Progressive
|
||||
CancellationToken = cancellationTokenSource.Token
|
||||
};
|
||||
|
||||
if (trySupportSeek)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(Request.QueryString[HeaderNames.Range]))
|
||||
{
|
||||
options.RequestHeaders[HeaderNames.Range] = Request.QueryString[HeaderNames.Range];
|
||||
}
|
||||
}
|
||||
var response = await HttpClient.GetResponse(options).ConfigureAwait(false);
|
||||
|
||||
if (trySupportSeek)
|
||||
responseHeaders[HeaderNames.AcceptRanges] = "none";
|
||||
|
||||
// Seeing cases of -1 here
|
||||
if (response.ContentLength.HasValue && response.ContentLength.Value >= 0)
|
||||
{
|
||||
foreach (var name in new[] { HeaderNames.ContentRange, HeaderNames.AcceptRanges })
|
||||
{
|
||||
var val = response.Headers[name];
|
||||
if (!string.IsNullOrWhiteSpace(val))
|
||||
{
|
||||
responseHeaders[name] = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
responseHeaders[HeaderNames.AcceptRanges] = "none";
|
||||
responseHeaders[HeaderNames.ContentLength] = response.ContentLength.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (isHeadRequest)
|
||||
@@ -356,10 +340,31 @@ namespace MediaBrowser.Api.Playback.Progressive
|
||||
var contentType = state.GetMimeType(outputPath);
|
||||
|
||||
// TODO: The isHeadRequest is only here because ServiceStack will add Content-Length=0 to the response
|
||||
var contentLength = state.EstimateContentLength || isHeadRequest ? GetEstimatedContentLength(state) : null;
|
||||
|
||||
if (contentLength.HasValue)
|
||||
{
|
||||
responseHeaders[HeaderNames.ContentLength] = contentLength.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
// Headers only
|
||||
if (isHeadRequest)
|
||||
{
|
||||
return ResultFactory.GetResult(null, Array.Empty<byte>(), contentType, responseHeaders);
|
||||
var streamResult = ResultFactory.GetResult(null, Array.Empty<byte>(), contentType, responseHeaders);
|
||||
|
||||
if (streamResult is IHasHeaders hasHeaders)
|
||||
{
|
||||
if (contentLength.HasValue)
|
||||
{
|
||||
hasHeaders.Headers[HeaderNames.ContentLength] = contentLength.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
else
|
||||
{
|
||||
hasHeaders.Headers.Remove(HeaderNames.ContentLength);
|
||||
}
|
||||
}
|
||||
|
||||
return streamResult;
|
||||
}
|
||||
|
||||
var transcodingLock = ApiEntryPoint.Instance.GetTranscodingLock(outputPath);
|
||||
@@ -397,5 +402,22 @@ namespace MediaBrowser.Api.Playback.Progressive
|
||||
transcodingLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the length of the estimated content.
|
||||
/// </summary>
|
||||
/// <param name="state">The state.</param>
|
||||
/// <returns>System.Nullable{System.Int64}.</returns>
|
||||
private long? GetEstimatedContentLength(StreamState state)
|
||||
{
|
||||
var totalBitrate = state.TotalOutputBitrate ?? 0;
|
||||
|
||||
if (totalBitrate > 0 && state.RunTimeTicks.HasValue)
|
||||
{
|
||||
return Convert.ToInt64(totalBitrate * TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds / 8);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,7 +224,17 @@ namespace MediaBrowser.Api.UserLibrary
|
||||
request.IncludeItemTypes = "Playlist";
|
||||
}
|
||||
|
||||
if (!user.Policy.EnableAllFolders && !user.Policy.EnabledFolders.Any(i => new Guid(i) == item.Id))
|
||||
bool isInEnabledFolder = user.Policy.EnabledFolders.Any(i => new Guid(i) == item.Id);
|
||||
var collectionFolders = _libraryManager.GetCollectionFolders(item);
|
||||
foreach (var collectionFolder in collectionFolders)
|
||||
{
|
||||
if (user.Policy.EnabledFolders.Contains(collectionFolder.Id.ToString("N"), StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
isInEnabledFolder = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!(item is UserRootFolder) && !user.Policy.EnableAllFolders && !isInEnabledFolder)
|
||||
{
|
||||
Logger.LogWarning("{UserName} is not permitted to access Library {ItemName}.", user.Name, item.Name);
|
||||
return new QueryResult<BaseItem>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
@@ -17,7 +16,7 @@ namespace MediaBrowser.Common.Net
|
||||
/// <value>The URL.</value>
|
||||
public string Url { get; set; }
|
||||
|
||||
public CompressionMethod? DecompressionMethod { get; set; }
|
||||
public CompressionMethod DecompressionMethod { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the accept header.
|
||||
@@ -28,18 +27,13 @@ namespace MediaBrowser.Common.Net
|
||||
get => GetHeaderValue(HeaderNames.Accept);
|
||||
set => RequestHeaders[HeaderNames.Accept] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the cancellation token.
|
||||
/// </summary>
|
||||
/// <value>The cancellation token.</value>
|
||||
public CancellationToken CancellationToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the resource pool.
|
||||
/// </summary>
|
||||
/// <value>The resource pool.</value>
|
||||
public SemaphoreSlim ResourcePool { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the user agent.
|
||||
/// </summary>
|
||||
@@ -54,13 +48,21 @@ namespace MediaBrowser.Common.Net
|
||||
/// Gets or sets the referrer.
|
||||
/// </summary>
|
||||
/// <value>The referrer.</value>
|
||||
public string Referer { get; set; }
|
||||
public string Referer
|
||||
{
|
||||
get => GetHeaderValue(HeaderNames.Referer);
|
||||
set => RequestHeaders[HeaderNames.Referer] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the host.
|
||||
/// </summary>
|
||||
/// <value>The host.</value>
|
||||
public string Host { get; set; }
|
||||
public string Host
|
||||
{
|
||||
get => GetHeaderValue(HeaderNames.Host);
|
||||
set => RequestHeaders[HeaderNames.Host] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the progress.
|
||||
@@ -68,12 +70,6 @@ namespace MediaBrowser.Common.Net
|
||||
/// <value>The progress.</value>
|
||||
public IProgress<double> Progress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether [enable HTTP compression].
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [enable HTTP compression]; otherwise, <c>false</c>.</value>
|
||||
public bool EnableHttpCompression { get; set; }
|
||||
|
||||
public Dictionary<string, string> RequestHeaders { get; private set; }
|
||||
|
||||
public string RequestContentType { get; set; }
|
||||
@@ -86,8 +82,6 @@ namespace MediaBrowser.Common.Net
|
||||
public bool LogRequest { get; set; }
|
||||
public bool LogRequestAsDebug { get; set; }
|
||||
public bool LogErrors { get; set; }
|
||||
public bool LogResponse { get; set; }
|
||||
public bool LogResponseHeaders { get; set; }
|
||||
|
||||
public bool LogErrorResponseBody { get; set; }
|
||||
public bool EnableKeepAlive { get; set; }
|
||||
@@ -95,11 +89,9 @@ namespace MediaBrowser.Common.Net
|
||||
public CacheMode CacheMode { get; set; }
|
||||
public TimeSpan CacheLength { get; set; }
|
||||
|
||||
public int TimeoutMs { get; set; }
|
||||
public bool EnableDefaultUserAgent { get; set; }
|
||||
|
||||
public bool AppendCharsetToMimeType { get; set; }
|
||||
public string DownloadFilePath { get; set; }
|
||||
|
||||
private string GetHeaderValue(string name)
|
||||
{
|
||||
@@ -113,24 +105,12 @@ namespace MediaBrowser.Common.Net
|
||||
/// </summary>
|
||||
public HttpRequestOptions()
|
||||
{
|
||||
EnableHttpCompression = true;
|
||||
|
||||
RequestHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
LogRequest = true;
|
||||
LogErrors = true;
|
||||
CacheMode = CacheMode.None;
|
||||
|
||||
TimeoutMs = 20000;
|
||||
}
|
||||
|
||||
public void SetPostData(IDictionary<string, string> values)
|
||||
{
|
||||
var strings = values.Keys.Select(key => string.Format("{0}={1}", key, values[key]));
|
||||
var postContent = string.Join("&", strings.ToArray());
|
||||
|
||||
RequestContent = postContent;
|
||||
RequestContentType = "application/x-www-form-urlencoded";
|
||||
DecompressionMethod = CompressionMethod.Deflate;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,9 +120,11 @@ namespace MediaBrowser.Common.Net
|
||||
Unconditional = 1
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum CompressionMethod
|
||||
{
|
||||
Deflate,
|
||||
Gzip
|
||||
None = 0b00000001,
|
||||
Deflate = 0b00000010,
|
||||
Gzip = 0b00000100
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace MediaBrowser.Common.Net
|
||||
{
|
||||
@@ -50,26 +50,21 @@ namespace MediaBrowser.Common.Net
|
||||
/// Gets or sets the headers.
|
||||
/// </summary>
|
||||
/// <value>The headers.</value>
|
||||
public Dictionary<string, string> Headers { get; set; }
|
||||
public HttpResponseHeaders Headers { get; set; }
|
||||
|
||||
private readonly IDisposable _disposable;
|
||||
|
||||
public HttpResponseInfo(IDisposable disposable)
|
||||
{
|
||||
_disposable = disposable;
|
||||
Headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
public HttpResponseInfo()
|
||||
{
|
||||
Headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
}
|
||||
|
||||
public HttpResponseInfo(HttpResponseHeaders headers)
|
||||
{
|
||||
Headers = headers;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposable != null)
|
||||
{
|
||||
_disposable.Dispose();
|
||||
}
|
||||
// Only IDisposable for backwards compatibility
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace MediaBrowser.Common.Net
|
||||
{
|
||||
@@ -23,6 +24,8 @@ namespace MediaBrowser.Common.Net
|
||||
Task<Stream> Get(HttpRequestOptions options);
|
||||
|
||||
/// <summary>
|
||||
/// Warning: Deprecated function,
|
||||
/// use 'Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, HttpMethod httpMethod);' instead
|
||||
/// Sends the asynchronous.
|
||||
/// </summary>
|
||||
/// <param name="options">The options.</param>
|
||||
@@ -30,6 +33,14 @@ namespace MediaBrowser.Common.Net
|
||||
/// <returns>Task{HttpResponseInfo}.</returns>
|
||||
Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, string httpMethod);
|
||||
|
||||
/// <summary>
|
||||
/// Sends the asynchronous.
|
||||
/// </summary>
|
||||
/// <param name="options">The options.</param>
|
||||
/// <param name="httpMethod">The HTTP method.</param>
|
||||
/// <returns>Task{HttpResponseInfo}.</returns>
|
||||
Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, HttpMethod httpMethod);
|
||||
|
||||
/// <summary>
|
||||
/// Posts the specified options.
|
||||
/// </summary>
|
||||
|
||||
@@ -11,6 +11,9 @@ namespace MediaBrowser.Controller.Authentication
|
||||
Task<ProviderAuthenticationResult> Authenticate(string username, string password);
|
||||
Task<bool> HasPassword(User user);
|
||||
Task ChangePassword(User user, string newPassword);
|
||||
void ChangeEasyPassword(User user, string newPassword, string newPasswordHash);
|
||||
string GetPasswordHash(User user);
|
||||
string GetEasyPasswordHash(User user);
|
||||
}
|
||||
|
||||
public interface IRequiresResolvedUser
|
||||
|
||||
@@ -78,10 +78,25 @@ namespace MediaBrowser.Controller.Entities
|
||||
/// <summary>
|
||||
/// The trailer folder name
|
||||
/// </summary>
|
||||
public static string TrailerFolderName = "trailers";
|
||||
public static string ThemeSongsFolderName = "theme-music";
|
||||
public static string ThemeSongFilename = "theme";
|
||||
public static string ThemeVideosFolderName = "backdrops";
|
||||
public const string TrailerFolderName = "trailers";
|
||||
public const string ThemeSongsFolderName = "theme-music";
|
||||
public const string ThemeSongFilename = "theme";
|
||||
public const string ThemeVideosFolderName = "backdrops";
|
||||
public const string ExtrasFolderName = "extras";
|
||||
public const string BehindTheScenesFolderName = "behind the scenes";
|
||||
public const string DeletedScenesFolderName = "deleted scenes";
|
||||
public const string InterviewFolderName = "interviews";
|
||||
public const string SceneFolderName = "scenes";
|
||||
public const string SampleFolderName = "samples";
|
||||
|
||||
public static readonly string[] AllExtrasTypesFolderNames = {
|
||||
ExtrasFolderName,
|
||||
BehindTheScenesFolderName,
|
||||
DeletedScenesFolderName,
|
||||
InterviewFolderName,
|
||||
SceneFolderName,
|
||||
SampleFolderName
|
||||
};
|
||||
|
||||
[IgnoreDataMember]
|
||||
public Guid[] ThemeSongIds { get; set; }
|
||||
@@ -1276,16 +1291,15 @@ namespace MediaBrowser.Controller.Entities
|
||||
.Select(item =>
|
||||
{
|
||||
// Try to retrieve it from the db. If we don't find it, use the resolved version
|
||||
var dbItem = LibraryManager.GetItemById(item.Id) as Video;
|
||||
|
||||
if (dbItem != null)
|
||||
if (LibraryManager.GetItemById(item.Id) is Video dbItem)
|
||||
{
|
||||
item = dbItem;
|
||||
}
|
||||
else
|
||||
{
|
||||
// item is new
|
||||
item.ExtraType = MediaBrowser.Model.Entities.ExtraType.ThemeVideo;
|
||||
item.ExtraType = Model.Entities.ExtraType.ThemeVideo;
|
||||
}
|
||||
|
||||
return item;
|
||||
@@ -1296,33 +1310,38 @@ namespace MediaBrowser.Controller.Entities
|
||||
|
||||
protected virtual BaseItem[] LoadExtras(List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
|
||||
{
|
||||
var files = fileSystemChildren.Where(i => i.IsDirectory)
|
||||
.SelectMany(i => FileSystem.GetFiles(i.FullName));
|
||||
var extras = new List<Video>();
|
||||
|
||||
return LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions())
|
||||
.OfType<Video>()
|
||||
.Select(item =>
|
||||
{
|
||||
// Try to retrieve it from the db. If we don't find it, use the resolved version
|
||||
var dbItem = LibraryManager.GetItemById(item.Id) as Video;
|
||||
var folders = fileSystemChildren.Where(i => i.IsDirectory).ToArray();
|
||||
foreach (var extraFolderName in AllExtrasTypesFolderNames)
|
||||
{
|
||||
var files = folders
|
||||
.Where(i => string.Equals(i.Name, extraFolderName, StringComparison.OrdinalIgnoreCase))
|
||||
.SelectMany(i => FileSystem.GetFiles(i.FullName));
|
||||
|
||||
if (dbItem != null)
|
||||
extras.AddRange(LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions())
|
||||
.OfType<Video>()
|
||||
.Select(item =>
|
||||
{
|
||||
item = dbItem;
|
||||
}
|
||||
else
|
||||
{
|
||||
// item is new
|
||||
item.ExtraType = MediaBrowser.Model.Entities.ExtraType.Clip;
|
||||
}
|
||||
// Try to retrieve it from the db. If we don't find it, use the resolved version
|
||||
if (LibraryManager.GetItemById(item.Id) is Video dbItem)
|
||||
{
|
||||
item = dbItem;
|
||||
}
|
||||
|
||||
return item;
|
||||
// Use some hackery to get the extra type based on foldername
|
||||
Enum.TryParse(extraFolderName.Replace(" ", ""), true, out ExtraType extraType);
|
||||
item.ExtraType = extraType;
|
||||
|
||||
// Sort them so that the list can be easily compared for changes
|
||||
}).OrderBy(i => i.Path).ToArray();
|
||||
return item;
|
||||
|
||||
// Sort them so that the list can be easily compared for changes
|
||||
}).OrderBy(i => i.Path));
|
||||
}
|
||||
|
||||
return extras.ToArray();
|
||||
}
|
||||
|
||||
|
||||
public Task RefreshMetadata(CancellationToken cancellationToken)
|
||||
{
|
||||
return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem)), cancellationToken);
|
||||
@@ -1481,7 +1500,13 @@ namespace MediaBrowser.Controller.Entities
|
||||
|
||||
private async Task<bool> RefreshExtras(BaseItem item, MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
|
||||
{
|
||||
var newExtras = LoadExtras(fileSystemChildren, options.DirectoryService).Concat(LoadThemeVideos(fileSystemChildren, options.DirectoryService)).Concat(LoadThemeSongs(fileSystemChildren, options.DirectoryService));
|
||||
var extras = LoadExtras(fileSystemChildren, options.DirectoryService);
|
||||
var themeVideos = LoadThemeVideos(fileSystemChildren, options.DirectoryService);
|
||||
var themeSongs = LoadThemeSongs(fileSystemChildren, options.DirectoryService);
|
||||
var newExtras = new BaseItem[extras.Length + themeVideos.Length + themeSongs.Length];
|
||||
extras.CopyTo(newExtras, 0);
|
||||
themeVideos.CopyTo(newExtras, extras.Length);
|
||||
themeSongs.CopyTo(newExtras, extras.Length + themeVideos.Length);
|
||||
|
||||
var newExtraIds = newExtras.Select(i => i.Id).ToArray();
|
||||
|
||||
@@ -1493,7 +1518,15 @@ namespace MediaBrowser.Controller.Entities
|
||||
|
||||
var tasks = newExtras.Select(i =>
|
||||
{
|
||||
return RefreshMetadataForOwnedItem(i, true, new MetadataRefreshOptions(options), cancellationToken);
|
||||
var subOptions = new MetadataRefreshOptions(options);
|
||||
if (i.OwnerId != ownerId || i.ParentId != Guid.Empty)
|
||||
{
|
||||
i.OwnerId = ownerId;
|
||||
i.ParentId = Guid.Empty;
|
||||
subOptions.ForceSave = true;
|
||||
}
|
||||
|
||||
return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken);
|
||||
});
|
||||
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
private readonly int DefaultImageExtractionTimeoutMs;
|
||||
private readonly string StartupOptionFFmpegPath;
|
||||
|
||||
private readonly SemaphoreSlim _thumbnailResourcePool = new SemaphoreSlim(1, 1);
|
||||
private readonly SemaphoreSlim _thumbnailResourcePool = new SemaphoreSlim(2, 2);
|
||||
private readonly List<ProcessWrapper> _runningProcesses = new List<ProcessWrapper>();
|
||||
private readonly ILocalizationManager _localization;
|
||||
|
||||
@@ -582,19 +582,27 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
{
|
||||
bool ranToCompletion;
|
||||
|
||||
StartProcess(processWrapper);
|
||||
|
||||
var timeoutMs = ConfigurationManager.Configuration.ImageExtractionTimeoutMs;
|
||||
if (timeoutMs <= 0)
|
||||
await _thumbnailResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
timeoutMs = DefaultImageExtractionTimeoutMs;
|
||||
StartProcess(processWrapper);
|
||||
|
||||
var timeoutMs = ConfigurationManager.Configuration.ImageExtractionTimeoutMs;
|
||||
if (timeoutMs <= 0)
|
||||
{
|
||||
timeoutMs = DefaultImageExtractionTimeoutMs;
|
||||
}
|
||||
|
||||
ranToCompletion = await process.WaitForExitAsync(timeoutMs).ConfigureAwait(false);
|
||||
|
||||
if (!ranToCompletion)
|
||||
{
|
||||
StopProcess(processWrapper, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
ranToCompletion = await process.WaitForExitAsync(timeoutMs).ConfigureAwait(false);
|
||||
|
||||
if (!ranToCompletion)
|
||||
finally
|
||||
{
|
||||
StopProcess(processWrapper, 1000);
|
||||
_thumbnailResourcePool.Release();
|
||||
}
|
||||
|
||||
var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
|
||||
@@ -625,7 +633,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
return time.ToString(@"hh\:mm\:ss\.fff", UsCulture);
|
||||
}
|
||||
|
||||
public async Task ExtractVideoImagesOnInterval(string[] inputFiles,
|
||||
public async Task ExtractVideoImagesOnInterval(
|
||||
string[] inputFiles,
|
||||
string container,
|
||||
MediaStream videoStream,
|
||||
MediaProtocol protocol,
|
||||
@@ -636,8 +645,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
int? maxWidth,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var resourcePool = _thumbnailResourcePool;
|
||||
|
||||
var inputArgument = GetInputArgument(inputFiles, protocol);
|
||||
|
||||
var vf = "fps=fps=1/" + interval.TotalSeconds.ToString(UsCulture);
|
||||
@@ -701,7 +708,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
_logger.LogInformation(process.StartInfo.FileName + " " + process.StartInfo.Arguments);
|
||||
|
||||
await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
await _thumbnailResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
bool ranToCompletion = false;
|
||||
|
||||
@@ -742,7 +749,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
}
|
||||
finally
|
||||
{
|
||||
resourcePool.Release();
|
||||
_thumbnailResourcePool.Release();
|
||||
}
|
||||
|
||||
var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
|
||||
|
||||
@@ -336,7 +336,7 @@ namespace MediaBrowser.Providers.Music
|
||||
}
|
||||
using (var subReader = reader.ReadSubtree())
|
||||
{
|
||||
return ParseReleaseList(subReader);
|
||||
return ParseReleaseList(subReader).ToList();
|
||||
}
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -110,7 +110,7 @@ namespace MediaBrowser.Providers.Music
|
||||
}
|
||||
using (var subReader = reader.ReadSubtree())
|
||||
{
|
||||
return ParseArtistList(subReader);
|
||||
return ParseArtistList(subReader).ToList();
|
||||
}
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -24,24 +24,28 @@ namespace MediaBrowser.Providers.TV.TheTVDB
|
||||
{
|
||||
_cache = memoryCache;
|
||||
_tvDbClient = new TvDbClient();
|
||||
_tvDbClient.Authentication.AuthenticateAsync(TvdbUtils.TvdbApiKey);
|
||||
_tokenCreatedAt = DateTime.Now;
|
||||
}
|
||||
|
||||
public TvDbClient TvDbClient
|
||||
private TvDbClient TvDbClient
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(_tvDbClient.Authentication.Token))
|
||||
{
|
||||
_tvDbClient.Authentication.AuthenticateAsync(TvdbUtils.TvdbApiKey).GetAwaiter().GetResult();
|
||||
_tokenCreatedAt = DateTime.Now;
|
||||
}
|
||||
|
||||
// Refresh if necessary
|
||||
if (_tokenCreatedAt > DateTime.Now.Subtract(TimeSpan.FromHours(20)))
|
||||
if (_tokenCreatedAt < DateTime.Now.Subtract(TimeSpan.FromHours(20)))
|
||||
{
|
||||
try
|
||||
{
|
||||
_tvDbClient.Authentication.RefreshTokenAsync();
|
||||
_tvDbClient.Authentication.RefreshTokenAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
catch
|
||||
{
|
||||
_tvDbClient.Authentication.AuthenticateAsync(TvdbUtils.TvdbApiKey);
|
||||
_tvDbClient.Authentication.AuthenticateAsync(TvdbUtils.TvdbApiKey).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
_tokenCreatedAt = DateTime.Now;
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB
|
||||
/// <summary>
|
||||
/// Class RemoteEpisodeProvider
|
||||
/// </summary>
|
||||
class TvdbEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, IHasOrder
|
||||
public class TvdbEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, IHasOrder
|
||||
{
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
Submodule MediaBrowser.WebDashboard/jellyfin-web updated: 874b51234e...c9e70d9564
@@ -1,4 +1,4 @@
|
||||
using System.Reflection;
|
||||
|
||||
[assembly: AssemblyVersion("10.3.0")]
|
||||
[assembly: AssemblyFileVersion("10.3.0")]
|
||||
[assembly: AssemblyVersion("10.3.6")]
|
||||
[assembly: AssemblyFileVersion("10.3.6")]
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
---
|
||||
# We just wrap `build` so this is really it
|
||||
name: "jellyfin"
|
||||
version: "10.3.0"
|
||||
version: "10.3.6"
|
||||
packages:
|
||||
- debian-package-x64
|
||||
- debian-package-armhf
|
||||
- debian-package-arm64
|
||||
- ubuntu-package-x64
|
||||
- ubuntu-package-armhf
|
||||
- ubuntu-package-arm64
|
||||
- fedora-package-x64
|
||||
- centos-package-x64
|
||||
- linux-x64
|
||||
|
||||
@@ -18,3 +18,4 @@ rpmbuild -bb SPECS/jellyfin.spec --define "_sourcedir ${SOURCE_DIR}/SOURCES/pkg-
|
||||
# Move the artifacts out
|
||||
mkdir -p ${ARTIFACT_DIR}/rpm
|
||||
mv /root/rpmbuild/RPMS/x86_64/jellyfin-*.rpm /root/rpmbuild/SRPMS/jellyfin-*.src.rpm ${ARTIFACT_DIR}/rpm/
|
||||
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
|
||||
|
||||
@@ -72,9 +72,6 @@ fi
|
||||
${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile
|
||||
# Build the RPMs and copy out to ${package_temporary_dir}
|
||||
${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}"
|
||||
# Correct ownership on the RPMs (as current user, then as root if that fails)
|
||||
chown -R "${current_user}" "${package_temporary_dir}" \
|
||||
|| sudo chown -R "${current_user}" "${package_temporary_dir}"
|
||||
# Move the RPMs to the output directory
|
||||
mkdir -p "${output_dir}"
|
||||
mv "${package_temporary_dir}"/rpm/* "${output_dir}"
|
||||
|
||||
43
deployment/debian-package-arm64/Dockerfile.amd64
Normal file
43
deployment/debian-package-arm64/Dockerfile.amd64
Normal file
@@ -0,0 +1,43 @@
|
||||
FROM debian:9
|
||||
# Docker build arguments
|
||||
ARG SOURCE_DIR=/jellyfin
|
||||
ARG PLATFORM_DIR=/jellyfin/deployment/debian-package-arm64
|
||||
ARG ARTIFACT_DIR=/dist
|
||||
ARG SDK_VERSION=2.2
|
||||
# Docker run environment
|
||||
ENV SOURCE_DIR=/jellyfin
|
||||
ENV ARTIFACT_DIR=/dist
|
||||
ENV DEB_BUILD_OPTIONS=noddebs
|
||||
ENV ARCH=amd64
|
||||
|
||||
# Prepare Debian build environment
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv
|
||||
|
||||
# Install dotnet repository
|
||||
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
|
||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/69937b49-a877-4ced-81e6-286620b390ab/8ab938cf6f5e83b2221630354160ef21/dotnet-sdk-2.2.104-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
&& mkdir -p dotnet-sdk \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
||||
# Prepare the cross-toolchain
|
||||
RUN dpkg --add-architecture arm64 \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y cross-gcc-dev \
|
||||
&& TARGET_LIST="arm64" cross-gcc-gensource 6 \
|
||||
&& cd cross-gcc-packages-amd64/cross-gcc-6-arm64 \
|
||||
&& apt-get install -y gcc-6-source libstdc++6-arm64-cross binutils-aarch64-linux-gnu bison flex libtool gdb sharutils netbase libcloog-isl-dev libmpc-dev libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip libc6-dev:arm64 linux-libc-dev:arm64 libgcc1:arm64 libcurl4-openssl-dev:arm64 libfontconfig1-dev:arm64 libfreetype6-dev:arm64 liblttng-ust0:arm64 libstdc++6:arm64
|
||||
|
||||
# Link to docker-build script
|
||||
RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh
|
||||
|
||||
# Link to Debian source dir; mkdir needed or it fails, can't force dest
|
||||
RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian
|
||||
|
||||
VOLUME ${ARTIFACT_DIR}/
|
||||
|
||||
COPY . ${SOURCE_DIR}/
|
||||
|
||||
ENTRYPOINT ["/docker-build.sh"]
|
||||
#ENTRYPOINT ["/bin/bash"]
|
||||
34
deployment/debian-package-arm64/Dockerfile.arm64
Normal file
34
deployment/debian-package-arm64/Dockerfile.arm64
Normal file
@@ -0,0 +1,34 @@
|
||||
FROM debian:9
|
||||
# Docker build arguments
|
||||
ARG SOURCE_DIR=/jellyfin
|
||||
ARG PLATFORM_DIR=/jellyfin/deployment/debian-package-arm64
|
||||
ARG ARTIFACT_DIR=/dist
|
||||
ARG SDK_VERSION=2.2
|
||||
# Docker run environment
|
||||
ENV SOURCE_DIR=/jellyfin
|
||||
ENV ARTIFACT_DIR=/dist
|
||||
ENV DEB_BUILD_OPTIONS=noddebs
|
||||
ENV ARCH=arm64
|
||||
|
||||
# Prepare Debian build environment
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev liblttng-ust0
|
||||
|
||||
# Install dotnet repository
|
||||
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
|
||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/d9f37b73-df8d-4dfa-a905-b7648d3401d0/6312573ac13d7a8ddc16e4058f7d7dc5/dotnet-sdk-2.2.104-linux-arm.tar.gz -O dotnet-sdk.tar.gz \
|
||||
&& mkdir -p dotnet-sdk \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
||||
# Link to docker-build script
|
||||
RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh
|
||||
|
||||
# Link to Debian source dir; mkdir needed or it fails, can't force dest
|
||||
RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian
|
||||
|
||||
VOLUME ${ARTIFACT_DIR}/
|
||||
|
||||
COPY . ${SOURCE_DIR}/
|
||||
|
||||
ENTRYPOINT ["/docker-build.sh"]
|
||||
29
deployment/debian-package-arm64/clean.sh
Executable file
29
deployment/debian-package-arm64/clean.sh
Executable file
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
source ../common.build.sh
|
||||
|
||||
keep_artifacts="${1}"
|
||||
|
||||
WORKDIR="$( pwd )"
|
||||
|
||||
package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
|
||||
output_dir="${WORKDIR}/pkg-dist"
|
||||
current_user="$( whoami )"
|
||||
image_name="jellyfin-debian_arm64-build"
|
||||
|
||||
rm -rf "${package_temporary_dir}" &>/dev/null \
|
||||
|| sudo rm -rf "${package_temporary_dir}" &>/dev/null
|
||||
|
||||
rm -rf "${output_dir}" &>/dev/null \
|
||||
|| sudo rm -rf "${output_dir}" &>/dev/null
|
||||
|
||||
if [[ ${keep_artifacts} == 'n' ]]; then
|
||||
docker_sudo=""
|
||||
if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
|
||||
&& [[ ! ${EUID:-1000} -eq 0 ]] \
|
||||
&& [[ ! ${USER} == "root" ]] \
|
||||
&& [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
|
||||
docker_sudo=sudo
|
||||
fi
|
||||
${docker_sudo} docker image rm ${image_name} --force
|
||||
fi
|
||||
1
deployment/debian-package-arm64/dependencies.txt
Normal file
1
deployment/debian-package-arm64/dependencies.txt
Normal file
@@ -0,0 +1 @@
|
||||
docker
|
||||
21
deployment/debian-package-arm64/docker-build.sh
Executable file
21
deployment/debian-package-arm64/docker-build.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Builds the DEB inside the Docker container
|
||||
|
||||
set -o errexit
|
||||
set -o xtrace
|
||||
|
||||
# Move to source directory
|
||||
pushd ${SOURCE_DIR}
|
||||
|
||||
# Remove build-dep for dotnet-sdk-2.2, since it's not a package in this image
|
||||
sed -i '/dotnet-sdk-2.2,/d' debian/control
|
||||
|
||||
# Build DEB
|
||||
export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH}
|
||||
dpkg-buildpackage -us -uc -aarm64
|
||||
|
||||
# Move the artifacts out
|
||||
mkdir -p ${ARTIFACT_DIR}/deb
|
||||
mv /jellyfin_* ${ARTIFACT_DIR}/deb/
|
||||
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
|
||||
41
deployment/debian-package-arm64/package.sh
Executable file
41
deployment/debian-package-arm64/package.sh
Executable file
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
source ../common.build.sh
|
||||
|
||||
ARCH="$( arch )"
|
||||
WORKDIR="$( pwd )"
|
||||
|
||||
package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
|
||||
output_dir="${WORKDIR}/pkg-dist"
|
||||
current_user="$( whoami )"
|
||||
image_name="jellyfin-debian_arm64-build"
|
||||
|
||||
# Determine if sudo should be used for Docker
|
||||
if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
|
||||
&& [[ ! ${EUID:-1000} -eq 0 ]] \
|
||||
&& [[ ! ${USER} == "root" ]] \
|
||||
&& [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
|
||||
docker_sudo="sudo"
|
||||
else
|
||||
docker_sudo=""
|
||||
fi
|
||||
|
||||
# Determine which Dockerfile to use
|
||||
case $ARCH in
|
||||
'x86_64')
|
||||
DOCKERFILE="Dockerfile.amd64"
|
||||
;;
|
||||
'armv7l')
|
||||
DOCKERFILE="Dockerfile.arm64"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Prepare temporary package dir
|
||||
mkdir -p "${package_temporary_dir}"
|
||||
# Set up the build environment Docker image
|
||||
${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE}
|
||||
# Build the DEBs and copy out to ${package_temporary_dir}
|
||||
${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}"
|
||||
# Move the DEBs to the output directory
|
||||
mkdir -p "${output_dir}"
|
||||
mv "${package_temporary_dir}"/deb/* "${output_dir}"
|
||||
1
deployment/debian-package-arm64/pkg-src
Symbolic link
1
deployment/debian-package-arm64/pkg-src
Symbolic link
@@ -0,0 +1 @@
|
||||
../debian-package-x64/pkg-src
|
||||
@@ -18,3 +18,4 @@ dpkg-buildpackage -us -uc -aarmhf
|
||||
# Move the artifacts out
|
||||
mkdir -p ${ARTIFACT_DIR}/deb
|
||||
mv /jellyfin_* ${ARTIFACT_DIR}/deb/
|
||||
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
|
||||
|
||||
@@ -30,13 +30,12 @@ case $ARCH in
|
||||
;;
|
||||
esac
|
||||
|
||||
# Prepare temporary package dir
|
||||
mkdir -p "${package_temporary_dir}"
|
||||
# Set up the build environment Docker image
|
||||
${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE}
|
||||
# Build the DEBs and copy out to ${package_temporary_dir}
|
||||
${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}"
|
||||
# Correct ownership on the DEBs (as current user, then as root if that fails)
|
||||
chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null \
|
||||
|| sudo chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null
|
||||
# Move the DEBs to the output directory
|
||||
mkdir -p "${output_dir}"
|
||||
mv "${package_temporary_dir}"/deb/* "${output_dir}"
|
||||
|
||||
@@ -17,3 +17,4 @@ dpkg-buildpackage -us -uc
|
||||
# Move the artifacts out
|
||||
mkdir -p ${ARTIFACT_DIR}/deb
|
||||
mv /jellyfin_* ${ARTIFACT_DIR}/deb/
|
||||
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
|
||||
|
||||
@@ -19,13 +19,12 @@ else
|
||||
docker_sudo=""
|
||||
fi
|
||||
|
||||
# Prepare temporary package dir
|
||||
mkdir -p "${package_temporary_dir}"
|
||||
# Set up the build environment Docker image
|
||||
${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile
|
||||
# Build the DEBs and copy out to ${package_temporary_dir}
|
||||
${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}"
|
||||
# Correct ownership on the DEBs (as current user, then as root if that fails)
|
||||
chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null \
|
||||
|| sudo chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null
|
||||
# Move the DEBs to the output directory
|
||||
mkdir -p "${output_dir}"
|
||||
mv "${package_temporary_dir}"/deb/* "${output_dir}"
|
||||
|
||||
@@ -1,20 +1,36 @@
|
||||
#!/bin/bash
|
||||
|
||||
NAME=jellyfin
|
||||
# restart.sh - Jellyfin server restart script
|
||||
# Part of the Jellyfin project (https://github.com/jellyfin)
|
||||
#
|
||||
# This script restarts the Jellyfin daemon on Linux when using
|
||||
# the Restart button on the admin dashboard. It supports the
|
||||
# systemctl, service, and traditional /etc/init.d (sysv) restart
|
||||
# methods, chosen automatically by which one is found first (in
|
||||
# that order).
|
||||
#
|
||||
# This script is used by the Debian/Ubuntu/Fedora/CentOS packages.
|
||||
|
||||
restart_cmds=(
|
||||
"systemctl restart ${NAME}"
|
||||
"service ${NAME} restart"
|
||||
"/etc/init.d/${NAME} restart"
|
||||
"s6-svc -t /var/run/s6/services/${NAME}"
|
||||
)
|
||||
get_service_command() {
|
||||
for command in systemctl service; do
|
||||
if which $command &>/dev/null; then
|
||||
echo $command && return
|
||||
fi
|
||||
done
|
||||
echo "sysv"
|
||||
}
|
||||
|
||||
for restart_cmd in "${restart_cmds[@]}"; do
|
||||
cmd=$(echo "$restart_cmd" | awk '{print $1}')
|
||||
cmd_loc=$(command -v ${cmd})
|
||||
if [[ -n "$cmd_loc" ]]; then
|
||||
restart_cmd=$(echo "$restart_cmd" | sed -e "s%${cmd}%${cmd_loc}%")
|
||||
echo "sleep 2; sudo $restart_cmd > /dev/null 2>&1" | at now > /dev/null 2>&1
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
cmd="$( get_service_command )"
|
||||
echo "Detected service control platform '$cmd'; using it to restart Jellyfin..."
|
||||
case $cmd in
|
||||
'systemctl')
|
||||
echo "sleep 2; /usr/bin/sudo $( which systemctl ) restart jellyfin" | at now
|
||||
;;
|
||||
'service')
|
||||
echo "sleep 2; /usr/bin/sudo $( which service ) jellyfin restart" | at now
|
||||
;;
|
||||
'sysv')
|
||||
echo "sleep 2; /usr/bin/sudo /etc/init.d/jellyfin restart" | at now
|
||||
;;
|
||||
esac
|
||||
exit 0
|
||||
|
||||
@@ -1,3 +1,39 @@
|
||||
jellyfin (10.3.6-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.3.6; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.6
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sat, 06 Jul 2019 13:34:19 -0400
|
||||
|
||||
jellyfin (10.3.5-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.3.5; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.5
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sun, 09 Jun 2019 21:47:35 -0400
|
||||
|
||||
jellyfin (10.3.4-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.3.4; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.4
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Thu, 06 Jun 2019 22:45:31 -0400
|
||||
|
||||
jellyfin (10.3.3-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.3.3; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.3
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Fri, 17 May 2019 23:12:08 -0400
|
||||
|
||||
jellyfin (10.3.2-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.3.2; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.2
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Tue, 30 Apr 2019 20:18:44 -0400
|
||||
|
||||
jellyfin (10.3.1-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.3.1; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.1
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sat, 20 Apr 2019 14:24:07 -0400
|
||||
|
||||
jellyfin (10.3.0-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.3.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.0
|
||||
|
||||
@@ -23,6 +23,6 @@ Depends: at,
|
||||
jellyfin-ffmpeg,
|
||||
libfontconfig1,
|
||||
libfreetype6,
|
||||
libssl1.0.0 | libssl1.0.2
|
||||
libssl1.0.0 | libssl1.0.2 | libssl1.1
|
||||
Description: Jellyfin is a home media server.
|
||||
It is built on top of other popular open source technologies such as Service Stack, jQuery, jQuery mobile, and Mono. It features a REST-based api with built-in documentation to facilitate client development. We also have client libraries for our api to enable rapid development.
|
||||
|
||||
@@ -6,18 +6,25 @@ SHELL := /bin/bash
|
||||
HOST_ARCH := $(shell arch)
|
||||
BUILD_ARCH := ${DEB_HOST_MULTIARCH}
|
||||
ifeq ($(HOST_ARCH),x86_64)
|
||||
# Building AMD64
|
||||
DOTNETRUNTIME := debian-x64
|
||||
ifeq ($(BUILD_ARCH),arm-linux-gnueabihf)
|
||||
# Cross-building ARM on AMD64
|
||||
DOTNETRUNTIME := debian-arm
|
||||
else
|
||||
# Building AMD64
|
||||
DOTNETRUNTIME := debian-x64
|
||||
endif
|
||||
ifeq ($(BUILD_ARCH),aarch64-linux-gnu)
|
||||
# Cross-building ARM on AMD64
|
||||
DOTNETRUNTIME := debian-arm64
|
||||
endif
|
||||
endif
|
||||
ifeq ($(HOST_ARCH),armv7l)
|
||||
# Building ARM
|
||||
DOTNETRUNTIME := debian-arm
|
||||
endif
|
||||
ifeq ($(HOST_ARCH),arm64)
|
||||
# Building ARM
|
||||
DOTNETRUNTIME := debian-arm64
|
||||
endif
|
||||
|
||||
export DH_VERBOSE=1
|
||||
export DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
|
||||
@@ -18,3 +18,4 @@ rpmbuild -bb SPECS/jellyfin.spec --define "_sourcedir ${SOURCE_DIR}/SOURCES/pkg-
|
||||
# Move the artifacts out
|
||||
mkdir -p ${ARTIFACT_DIR}/rpm
|
||||
mv /root/rpmbuild/RPMS/x86_64/jellyfin-*.rpm /root/rpmbuild/SRPMS/jellyfin-*.src.rpm ${ARTIFACT_DIR}/rpm/
|
||||
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
|
||||
|
||||
@@ -23,13 +23,12 @@ fi
|
||||
|
||||
./create_tarball.sh
|
||||
|
||||
# Prepare temporary package dir
|
||||
mkdir -p "${package_temporary_dir}"
|
||||
# Set up the build environment Docker image
|
||||
${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile
|
||||
# Build the RPMs and copy out to ${package_temporary_dir}
|
||||
${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}"
|
||||
# Correct ownership on the RPMs (as current user, then as root if that fails)
|
||||
chown -R "${current_user}" "${package_temporary_dir}" \
|
||||
|| sudo chown -R "${current_user}" "${package_temporary_dir}"
|
||||
# Move the RPMs to the output directory
|
||||
mkdir -p "${output_dir}"
|
||||
mv "${package_temporary_dir}"/rpm/* "${output_dir}"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
%endif
|
||||
|
||||
Name: jellyfin
|
||||
Version: 10.3.0
|
||||
Version: 10.3.6
|
||||
Release: 1%{?dist}
|
||||
Summary: The Free Software Media Browser
|
||||
License: GPLv2
|
||||
@@ -140,6 +140,18 @@ fi
|
||||
%systemd_postun_with_restart jellyfin.service
|
||||
|
||||
%changelog
|
||||
* Sat Jul 06 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
- New upstream version 10.3.6; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.6
|
||||
* Sun Jun 09 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
- New upstream version 10.3.5; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.5
|
||||
* Thu Jun 06 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
- New upstream version 10.3.4; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.4
|
||||
* Fri May 17 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
- New upstream version 10.3.3; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.3
|
||||
* Tue Apr 30 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
- New upstream version 10.3.2; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.2
|
||||
* Sat Apr 20 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
- New upstream version 10.3.1; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.1
|
||||
* Fri Apr 19 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
- New upstream version 10.3.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.0
|
||||
* Thu Feb 28 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
|
||||
@@ -1,6 +1,36 @@
|
||||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
|
||||
NAME=jellyfin
|
||||
restart_cmd="/usr/bin/systemctl restart ${NAME}"
|
||||
echo "sleep 2; sudo $restart_cmd > /dev/null 2>&1" | at now > /dev/null 2>&1
|
||||
exit 0
|
||||
# restart.sh - Jellyfin server restart script
|
||||
# Part of the Jellyfin project (https://github.com/jellyfin)
|
||||
#
|
||||
# This script restarts the Jellyfin daemon on Linux when using
|
||||
# the Restart button on the admin dashboard. It supports the
|
||||
# systemctl, service, and traditional /etc/init.d (sysv) restart
|
||||
# methods, chosen automatically by which one is found first (in
|
||||
# that order).
|
||||
#
|
||||
# This script is used by the Debian/Ubuntu/Fedora/CentOS packages.
|
||||
|
||||
get_service_command() {
|
||||
for command in systemctl service; do
|
||||
if which $command &>/dev/null; then
|
||||
echo $command && return
|
||||
fi
|
||||
done
|
||||
echo "sysv"
|
||||
}
|
||||
|
||||
cmd="$( get_service_command )"
|
||||
echo "Detected service control platform '$cmd'; using it to restart Jellyfin..."
|
||||
case $cmd in
|
||||
'systemctl')
|
||||
echo "sleep 2; /usr/bin/sudo $( which systemctl ) restart jellyfin" | at now
|
||||
;;
|
||||
'service')
|
||||
echo "sleep 2; /usr/bin/sudo $( which service ) jellyfin restart" | at now
|
||||
;;
|
||||
'sysv')
|
||||
echo "sleep 2; /usr/bin/sudo /etc/init.d/jellyfin restart" | at now
|
||||
;;
|
||||
esac
|
||||
exit 0
|
||||
|
||||
53
deployment/ubuntu-package-arm64/Dockerfile.amd64
Normal file
53
deployment/ubuntu-package-arm64/Dockerfile.amd64
Normal file
@@ -0,0 +1,53 @@
|
||||
FROM ubuntu:bionic
|
||||
# Docker build arguments
|
||||
ARG SOURCE_DIR=/jellyfin
|
||||
ARG PLATFORM_DIR=/jellyfin/deployment/ubuntu-package-arm64
|
||||
ARG ARTIFACT_DIR=/dist
|
||||
ARG SDK_VERSION=2.2
|
||||
# Docker run environment
|
||||
ENV SOURCE_DIR=/jellyfin
|
||||
ENV ARTIFACT_DIR=/dist
|
||||
ENV DEB_BUILD_OPTIONS=noddebs
|
||||
ENV ARCH=amd64
|
||||
|
||||
# Prepare Debian build environment
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv
|
||||
|
||||
# Install dotnet repository
|
||||
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
|
||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/69937b49-a877-4ced-81e6-286620b390ab/8ab938cf6f5e83b2221630354160ef21/dotnet-sdk-2.2.104-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
&& mkdir -p dotnet-sdk \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
||||
# Prepare the cross-toolchain
|
||||
RUN rm /etc/apt/sources.list \
|
||||
&& export CODENAME="$( lsb_release -c -s )" \
|
||||
&& echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME} main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \
|
||||
&& echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME}-updates main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \
|
||||
&& echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME}-backports main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \
|
||||
&& echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME}-security main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \
|
||||
&& echo "deb [arch=arm64] http://ports.ubuntu.com/ ${CODENAME} main restricted universe multiverse" >>/etc/apt/sources.list.d/arm64.list \
|
||||
&& echo "deb [arch=arm64] http://ports.ubuntu.com/ ${CODENAME}-updates main restricted universe multiverse" >>/etc/apt/sources.list.d/arm64.list \
|
||||
&& echo "deb [arch=arm64] http://ports.ubuntu.com/ ${CODENAME}-backports main restricted universe multiverse" >>/etc/apt/sources.list.d/arm64.list \
|
||||
&& echo "deb [arch=arm64] http://ports.ubuntu.com/ ${CODENAME}-security main restricted universe multiverse" >>/etc/apt/sources.list.d/arm64.list \
|
||||
&& dpkg --add-architecture arm64 \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y cross-gcc-dev \
|
||||
&& TARGET_LIST="arm64" cross-gcc-gensource 6 \
|
||||
&& cd cross-gcc-packages-amd64/cross-gcc-6-arm64 \
|
||||
&& ln -fs /usr/share/zoneinfo/America/Toronto /etc/localtime \
|
||||
&& apt-get install -y gcc-6-source libstdc++6-arm64-cross binutils-aarch64-linux-gnu bison flex libtool gdb sharutils netbase libcloog-isl-dev libmpc-dev libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip libc6-dev:arm64 linux-libc-dev:arm64 libgcc1:arm64 libcurl4-openssl-dev:arm64 libfontconfig1-dev:arm64 libfreetype6-dev:arm64 liblttng-ust0:arm64 libstdc++6:arm64
|
||||
|
||||
# Link to docker-build script
|
||||
RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh
|
||||
|
||||
# Link to Debian source dir; mkdir needed or it fails, can't force dest
|
||||
RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian
|
||||
|
||||
VOLUME ${ARTIFACT_DIR}/
|
||||
|
||||
COPY . ${SOURCE_DIR}/
|
||||
|
||||
ENTRYPOINT ["/docker-build.sh"]
|
||||
34
deployment/ubuntu-package-arm64/Dockerfile.arm64
Normal file
34
deployment/ubuntu-package-arm64/Dockerfile.arm64
Normal file
@@ -0,0 +1,34 @@
|
||||
FROM ubuntu:bionic
|
||||
# Docker build arguments
|
||||
ARG SOURCE_DIR=/jellyfin
|
||||
ARG PLATFORM_DIR=/jellyfin/deployment/ubuntu-package-arm64
|
||||
ARG ARTIFACT_DIR=/dist
|
||||
ARG SDK_VERSION=2.2
|
||||
# Docker run environment
|
||||
ENV SOURCE_DIR=/jellyfin
|
||||
ENV ARTIFACT_DIR=/dist
|
||||
ENV DEB_BUILD_OPTIONS=noddebs
|
||||
ENV ARCH=arm64
|
||||
|
||||
# Prepare Debian build environment
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev liblttng-ust0
|
||||
|
||||
# Install dotnet repository
|
||||
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
|
||||
RUN wget https://download.visualstudio.microsoft.com/download/pr/d9f37b73-df8d-4dfa-a905-b7648d3401d0/6312573ac13d7a8ddc16e4058f7d7dc5/dotnet-sdk-2.2.104-linux-arm.tar.gz -O dotnet-sdk.tar.gz \
|
||||
&& mkdir -p dotnet-sdk \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
||||
# Link to docker-build script
|
||||
RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh
|
||||
|
||||
# Link to Debian source dir; mkdir needed or it fails, can't force dest
|
||||
RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian
|
||||
|
||||
VOLUME ${ARTIFACT_DIR}/
|
||||
|
||||
COPY . ${SOURCE_DIR}/
|
||||
|
||||
ENTRYPOINT ["/docker-build.sh"]
|
||||
29
deployment/ubuntu-package-arm64/clean.sh
Executable file
29
deployment/ubuntu-package-arm64/clean.sh
Executable file
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
source ../common.build.sh
|
||||
|
||||
keep_artifacts="${1}"
|
||||
|
||||
WORKDIR="$( pwd )"
|
||||
|
||||
package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
|
||||
output_dir="${WORKDIR}/pkg-dist"
|
||||
current_user="$( whoami )"
|
||||
image_name="jellyfin-ubuntu-build"
|
||||
|
||||
rm -rf "${package_temporary_dir}" &>/dev/null \
|
||||
|| sudo rm -rf "${package_temporary_dir}" &>/dev/null
|
||||
|
||||
rm -rf "${output_dir}" &>/dev/null \
|
||||
|| sudo rm -rf "${output_dir}" &>/dev/null
|
||||
|
||||
if [[ ${keep_artifacts} == 'n' ]]; then
|
||||
docker_sudo=""
|
||||
if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
|
||||
&& [[ ! ${EUID:-1000} -eq 0 ]] \
|
||||
&& [[ ! ${USER} == "root" ]] \
|
||||
&& [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
|
||||
docker_sudo=sudo
|
||||
fi
|
||||
${docker_sudo} docker image rm ${image_name} --force
|
||||
fi
|
||||
1
deployment/ubuntu-package-arm64/dependencies.txt
Normal file
1
deployment/ubuntu-package-arm64/dependencies.txt
Normal file
@@ -0,0 +1 @@
|
||||
docker
|
||||
21
deployment/ubuntu-package-arm64/docker-build.sh
Executable file
21
deployment/ubuntu-package-arm64/docker-build.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Builds the DEB inside the Docker container
|
||||
|
||||
set -o errexit
|
||||
set -o xtrace
|
||||
|
||||
# Move to source directory
|
||||
pushd ${SOURCE_DIR}
|
||||
|
||||
# Remove build-dep for dotnet-sdk-2.2, since it's not a package in this image
|
||||
sed -i '/dotnet-sdk-2.2,/d' debian/control
|
||||
|
||||
# Build DEB
|
||||
export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH}
|
||||
dpkg-buildpackage -us -uc -aarm64
|
||||
|
||||
# Move the artifacts out
|
||||
mkdir -p ${ARTIFACT_DIR}/deb
|
||||
mv /jellyfin_* ${ARTIFACT_DIR}/deb/
|
||||
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
|
||||
41
deployment/ubuntu-package-arm64/package.sh
Executable file
41
deployment/ubuntu-package-arm64/package.sh
Executable file
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
source ../common.build.sh
|
||||
|
||||
ARCH="$( arch )"
|
||||
WORKDIR="$( pwd )"
|
||||
|
||||
package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
|
||||
output_dir="${WORKDIR}/pkg-dist"
|
||||
current_user="$( whoami )"
|
||||
image_name="jellyfin-ubuntu_arm64-build"
|
||||
|
||||
# Determine if sudo should be used for Docker
|
||||
if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
|
||||
&& [[ ! ${EUID:-1000} -eq 0 ]] \
|
||||
&& [[ ! ${USER} == "root" ]] \
|
||||
&& [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
|
||||
docker_sudo="sudo"
|
||||
else
|
||||
docker_sudo=""
|
||||
fi
|
||||
|
||||
# Determine which Dockerfile to use
|
||||
case $ARCH in
|
||||
'x86_64')
|
||||
DOCKERFILE="Dockerfile.amd64"
|
||||
;;
|
||||
'armv7l')
|
||||
DOCKERFILE="Dockerfile.arm64"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Prepare temporary package dir
|
||||
mkdir -p "${package_temporary_dir}"
|
||||
# Set up the build environment Docker image
|
||||
${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE}
|
||||
# Build the DEBs and copy out to ${package_temporary_dir}
|
||||
${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}"
|
||||
# Move the DEBs to the output directory
|
||||
mkdir -p "${output_dir}"
|
||||
mv "${package_temporary_dir}"/deb/* "${output_dir}"
|
||||
1
deployment/ubuntu-package-arm64/pkg-src
Symbolic link
1
deployment/ubuntu-package-arm64/pkg-src
Symbolic link
@@ -0,0 +1 @@
|
||||
../debian-package-x64/pkg-src
|
||||
@@ -18,3 +18,4 @@ dpkg-buildpackage -us -uc -aarmhf
|
||||
# Move the artifacts out
|
||||
mkdir -p ${ARTIFACT_DIR}/deb
|
||||
mv /jellyfin_* ${ARTIFACT_DIR}/deb/
|
||||
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
|
||||
|
||||
@@ -30,13 +30,12 @@ case $ARCH in
|
||||
;;
|
||||
esac
|
||||
|
||||
# Prepare temporary package dir
|
||||
mkdir -p "${package_temporary_dir}"
|
||||
# Set up the build environment Docker image
|
||||
${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE}
|
||||
# Build the DEBs and copy out to ${package_temporary_dir}
|
||||
${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}"
|
||||
# Correct ownership on the DEBs (as current user, then as root if that fails)
|
||||
chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null \
|
||||
|| sudo chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null
|
||||
# Move the DEBs to the output directory
|
||||
mkdir -p "${output_dir}"
|
||||
mv "${package_temporary_dir}"/deb/* "${output_dir}"
|
||||
|
||||
@@ -17,3 +17,4 @@ dpkg-buildpackage -us -uc
|
||||
# Move the artifacts out
|
||||
mkdir -p ${ARTIFACT_DIR}/deb
|
||||
mv /jellyfin_* ${ARTIFACT_DIR}/deb/
|
||||
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
|
||||
|
||||
@@ -19,13 +19,12 @@ else
|
||||
docker_sudo=""
|
||||
fi
|
||||
|
||||
# Prepare temporary package dir
|
||||
mkdir -p "${package_temporary_dir}"
|
||||
# Set up the build environment Docker image
|
||||
${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile
|
||||
# Build the DEBs and copy out to ${package_temporary_dir}
|
||||
${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}"
|
||||
# Correct ownership on the DEBs (as current user, then as root if that fails)
|
||||
chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null \
|
||||
|| sudo chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null
|
||||
# Move the DEBs to the output directory
|
||||
mkdir -p "${output_dir}"
|
||||
mv "${package_temporary_dir}"/deb/* "${output_dir}"
|
||||
|
||||
Reference in New Issue
Block a user