Merge branch 'master' into tasks
This commit is contained in:
commit
ce1fa42f9d
|
@ -99,7 +99,7 @@ jobs:
|
||||||
pool:
|
pool:
|
||||||
vmImage: ubuntu-16.04
|
vmImage: ubuntu-16.04
|
||||||
dependsOn: main_build
|
dependsOn: main_build
|
||||||
condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber']) # Only execute if the pullrequest numer is defined. (So not for normal CI builds)
|
condition: false #and(succeeded(), variables['System.PullRequest.PullRequestNumber']) # Only execute if the pullrequest numer is defined. (So not for normal CI builds)
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
Naming:
|
Naming:
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
ARG DOTNET_VERSION=2
|
ARG DOTNET_VERSION=2.2
|
||||||
|
|
||||||
FROM microsoft/dotnet:${DOTNET_VERSION}-sdk as builder
|
FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder
|
||||||
WORKDIR /repo
|
WORKDIR /repo
|
||||||
COPY . .
|
COPY . .
|
||||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||||
|
@ -8,7 +8,7 @@ RUN bash -c "source deployment/common.build.sh && \
|
||||||
build_jellyfin Jellyfin.Server Release linux-x64 /jellyfin"
|
build_jellyfin Jellyfin.Server Release linux-x64 /jellyfin"
|
||||||
|
|
||||||
FROM jellyfin/ffmpeg as ffmpeg
|
FROM jellyfin/ffmpeg as ffmpeg
|
||||||
FROM microsoft/dotnet:${DOTNET_VERSION}-runtime
|
FROM mcr.microsoft.com/dotnet/core/runtime:${DOTNET_VERSION}
|
||||||
# libfontconfig1 is required for Skia
|
# libfontconfig1 is required for Skia
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install --no-install-recommends --no-install-suggests -y \
|
&& apt-get install --no-install-recommends --no-install-suggests -y \
|
||||||
|
@ -21,7 +21,7 @@ RUN apt-get update \
|
||||||
COPY --from=ffmpeg / /
|
COPY --from=ffmpeg / /
|
||||||
COPY --from=builder /jellyfin /jellyfin
|
COPY --from=builder /jellyfin /jellyfin
|
||||||
|
|
||||||
ARG JELLYFIN_WEB_VERSION=10.2.2
|
ARG JELLYFIN_WEB_VERSION=10.3.3
|
||||||
RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||||
&& rm -rf /jellyfin/jellyfin-web \
|
&& rm -rf /jellyfin/jellyfin-web \
|
||||||
&& mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web
|
&& mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web
|
||||||
|
|
|
@ -8,7 +8,7 @@ FROM alpine as qemu_extract
|
||||||
COPY --from=qemu /usr/bin qemu-arm-static.tar.gz
|
COPY --from=qemu /usr/bin qemu-arm-static.tar.gz
|
||||||
RUN tar -xzvf qemu-arm-static.tar.gz
|
RUN tar -xzvf qemu-arm-static.tar.gz
|
||||||
|
|
||||||
FROM microsoft/dotnet:${DOTNET_VERSION}-sdk-stretch as builder
|
FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder
|
||||||
WORKDIR /repo
|
WORKDIR /repo
|
||||||
COPY . .
|
COPY . .
|
||||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||||
|
@ -21,7 +21,7 @@ RUN bash -c "source deployment/common.build.sh && \
|
||||||
build_jellyfin Jellyfin.Server Release linux-arm /jellyfin"
|
build_jellyfin Jellyfin.Server Release linux-arm /jellyfin"
|
||||||
|
|
||||||
|
|
||||||
FROM microsoft/dotnet:${DOTNET_VERSION}-runtime-stretch-slim-arm32v7
|
FROM mcr.microsoft.com/dotnet/core/runtime:${DOTNET_VERSION}-stretch-slim-arm32v7
|
||||||
COPY --from=qemu_extract qemu-arm-static /usr/bin
|
COPY --from=qemu_extract qemu-arm-static /usr/bin
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \
|
&& apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \
|
||||||
|
@ -30,7 +30,7 @@ RUN apt-get update \
|
||||||
&& chmod 777 /cache /config /media
|
&& chmod 777 /cache /config /media
|
||||||
COPY --from=builder /jellyfin /jellyfin
|
COPY --from=builder /jellyfin /jellyfin
|
||||||
|
|
||||||
ARG JELLYFIN_WEB_VERSION=10.2.2
|
ARG JELLYFIN_WEB_VERSION=10.3.3
|
||||||
RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||||
&& rm -rf /jellyfin/jellyfin-web \
|
&& rm -rf /jellyfin/jellyfin-web \
|
||||||
&& mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web
|
&& mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web
|
||||||
|
|
|
@ -9,7 +9,7 @@ COPY --from=qemu /usr/bin qemu-aarch64-static.tar.gz
|
||||||
RUN tar -xzvf qemu-aarch64-static.tar.gz
|
RUN tar -xzvf qemu-aarch64-static.tar.gz
|
||||||
|
|
||||||
|
|
||||||
FROM microsoft/dotnet:${DOTNET_VERSION}-sdk-stretch as builder
|
FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder
|
||||||
WORKDIR /repo
|
WORKDIR /repo
|
||||||
COPY . .
|
COPY . .
|
||||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||||
|
@ -22,7 +22,7 @@ RUN bash -c "source deployment/common.build.sh && \
|
||||||
build_jellyfin Jellyfin.Server Release linux-arm64 /jellyfin"
|
build_jellyfin Jellyfin.Server Release linux-arm64 /jellyfin"
|
||||||
|
|
||||||
|
|
||||||
FROM microsoft/dotnet:${DOTNET_VERSION}-runtime-stretch-slim-arm64v8
|
FROM mcr.microsoft.com/dotnet/core/runtime:${DOTNET_VERSION}-stretch-slim-arm64v8
|
||||||
COPY --from=qemu_extract qemu-aarch64-static /usr/bin
|
COPY --from=qemu_extract qemu-aarch64-static /usr/bin
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \
|
&& apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \
|
||||||
|
@ -31,7 +31,7 @@ RUN apt-get update \
|
||||||
&& chmod 777 /cache /config /media
|
&& chmod 777 /cache /config /media
|
||||||
COPY --from=builder /jellyfin /jellyfin
|
COPY --from=builder /jellyfin /jellyfin
|
||||||
|
|
||||||
ARG JELLYFIN_WEB_VERSION=10.2.2
|
ARG JELLYFIN_WEB_VERSION=10.3.3
|
||||||
RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||||
&& rm -rf /jellyfin/jellyfin-web \
|
&& rm -rf /jellyfin/jellyfin-web \
|
||||||
&& mv jellyfin-web-${JELLYFIN_WEB_VERSION} /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))
|
if (!_profile.EnableSingleAlbumArtLimit || string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
AddImageResElement(item, writer, 4096, 4096, "jpg", "JPEG_LRG");
|
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, 4096, 4096, "png", "PNG_LRG");
|
||||||
AddImageResElement(item, writer, 160, 160, "png", "PNG_TN");
|
AddImageResElement(item, writer, 160, 160, "png", "PNG_TN");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AddImageResElement(item, writer, 160, 160, "jpg", "JPEG_TN");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddEmbeddedImageAsCover(string name, XmlWriter writer)
|
private void AddEmbeddedImageAsCover(string name, XmlWriter writer)
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 6.1 KiB |
Binary file not shown.
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 13 KiB |
Binary file not shown.
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.2 KiB |
|
@ -102,9 +102,10 @@ namespace Emby.Dlna.PlayTo
|
||||||
{
|
{
|
||||||
_sessionManager.ReportSessionEnded(_session.Id);
|
_sessionManager.ReportSessionEnded(_session.Id);
|
||||||
}
|
}
|
||||||
catch
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Could throw if the session is already gone
|
// Could throw if the session is already gone
|
||||||
|
_logger.LogError(ex, "Error reporting the end of session {Id}", _session.Id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,20 +113,14 @@ namespace Emby.Dlna.PlayTo
|
||||||
{
|
{
|
||||||
var info = e.Argument;
|
var info = e.Argument;
|
||||||
|
|
||||||
info.Headers.TryGetValue("NTS", out string nts);
|
if (!_disposed
|
||||||
|
&& info.Headers.TryGetValue("USN", out string usn)
|
||||||
if (!info.Headers.TryGetValue("USN", out string usn)) usn = string.Empty;
|
&& usn.IndexOf(_device.Properties.UUID, StringComparison.OrdinalIgnoreCase) != -1
|
||||||
|
&& (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1
|
||||||
if (!info.Headers.TryGetValue("NT", out string nt)) nt = string.Empty;
|
|| (info.Headers.TryGetValue("NT", out string nt)
|
||||||
|
&& nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1)))
|
||||||
if (usn.IndexOf(_device.Properties.UUID, StringComparison.OrdinalIgnoreCase) != -1 &&
|
|
||||||
!_disposed)
|
|
||||||
{
|
{
|
||||||
if (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1 ||
|
OnDeviceUnavailable();
|
||||||
nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1)
|
|
||||||
{
|
|
||||||
OnDeviceUnavailable();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -612,22 +607,34 @@ namespace Emby.Dlna.PlayTo
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (!_disposed)
|
Dispose(true);
|
||||||
{
|
GC.SuppressFinalize(this);
|
||||||
_disposed = true;
|
|
||||||
|
|
||||||
_device.PlaybackStart -= _device_PlaybackStart;
|
|
||||||
_device.PlaybackProgress -= _device_PlaybackProgress;
|
|
||||||
_device.PlaybackStopped -= _device_PlaybackStopped;
|
|
||||||
_device.MediaChanged -= _device_MediaChanged;
|
|
||||||
//_deviceDiscovery.DeviceLeft -= _deviceDiscovery_DeviceLeft;
|
|
||||||
_device.OnDeviceUnavailable = null;
|
|
||||||
|
|
||||||
_device.Dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
_device.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_device.PlaybackStart -= _device_PlaybackStart;
|
||||||
|
_device.PlaybackProgress -= _device_PlaybackProgress;
|
||||||
|
_device.PlaybackStopped -= _device_PlaybackStopped;
|
||||||
|
_device.MediaChanged -= _device_MediaChanged;
|
||||||
|
_deviceDiscovery.DeviceLeft -= _deviceDiscovery_DeviceLeft;
|
||||||
|
_device.OnDeviceUnavailable = null;
|
||||||
|
_device = null;
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly CultureInfo _usCulture = CultureInfo.ReadOnly(new CultureInfo("en-US"));
|
||||||
|
|
||||||
private Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken)
|
private Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
|
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
|
|
@ -20,7 +20,10 @@ namespace Emby.Photos
|
||||||
public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IHasItemChangeMonitor
|
public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IHasItemChangeMonitor
|
||||||
{
|
{
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private IImageProcessor _imageProcessor;
|
private readonly IImageProcessor _imageProcessor;
|
||||||
|
|
||||||
|
// These are causing taglib to hang
|
||||||
|
private string[] _includextensions = new string[] { ".jpg", ".jpeg", ".png", ".tiff", ".cr2" };
|
||||||
|
|
||||||
public PhotoProvider(ILogger logger, IImageProcessor imageProcessor)
|
public PhotoProvider(ILogger logger, IImageProcessor imageProcessor)
|
||||||
{
|
{
|
||||||
|
@ -28,75 +31,55 @@ namespace Emby.Photos
|
||||||
_imageProcessor = imageProcessor;
|
_imageProcessor = imageProcessor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string Name => "Embedded Information";
|
||||||
|
|
||||||
public bool HasChanged(BaseItem item, IDirectoryService directoryService)
|
public bool HasChanged(BaseItem item, IDirectoryService directoryService)
|
||||||
{
|
{
|
||||||
if (item.IsFileProtocol)
|
if (item.IsFileProtocol)
|
||||||
{
|
{
|
||||||
var file = directoryService.GetFile(item.Path);
|
var file = directoryService.GetFile(item.Path);
|
||||||
if (file != null && file.LastWriteTimeUtc != item.DateModified)
|
return (file != null && file.LastWriteTimeUtc != item.DateModified);
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// These are causing taglib to hang
|
|
||||||
private string[] _includextensions = new string[] { ".jpg", ".jpeg", ".png", ".tiff", ".cr2" };
|
|
||||||
|
|
||||||
public Task<ItemUpdateType> FetchAsync(Photo item, MetadataRefreshOptions options, CancellationToken cancellationToken)
|
public Task<ItemUpdateType> FetchAsync(Photo item, MetadataRefreshOptions options, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
item.SetImagePath(ImageType.Primary, item.Path);
|
item.SetImagePath(ImageType.Primary, item.Path);
|
||||||
|
|
||||||
// Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
|
// Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
|
||||||
if (_includextensions.Contains(Path.GetExtension(item.Path) ?? string.Empty, StringComparer.OrdinalIgnoreCase))
|
if (_includextensions.Contains(Path.GetExtension(item.Path), StringComparer.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using (var file = TagLib.File.Create(item.Path))
|
using (var file = TagLib.File.Create(item.Path))
|
||||||
{
|
{
|
||||||
var image = file as TagLib.Image.File;
|
if (file.GetTag(TagTypes.TiffIFD) is IFDTag tag)
|
||||||
|
|
||||||
var tag = file.GetTag(TagTypes.TiffIFD) as IFDTag;
|
|
||||||
|
|
||||||
if (tag != null)
|
|
||||||
{
|
{
|
||||||
var structure = tag.Structure;
|
var structure = tag.Structure;
|
||||||
|
if (structure != null
|
||||||
if (structure != null)
|
&& structure.GetEntry(0, (ushort)IFDEntryTag.ExifIFD) is SubIFDEntry exif)
|
||||||
{
|
{
|
||||||
var exif = structure.GetEntry(0, (ushort)IFDEntryTag.ExifIFD) as SubIFDEntry;
|
var exifStructure = exif.Structure;
|
||||||
|
if (exifStructure != null)
|
||||||
if (exif != null)
|
|
||||||
{
|
{
|
||||||
var exifStructure = exif.Structure;
|
var entry = exifStructure.GetEntry(0, (ushort)ExifEntryTag.ApertureValue) as RationalIFDEntry;
|
||||||
|
if (entry != null)
|
||||||
if (exifStructure != null)
|
|
||||||
{
|
{
|
||||||
var entry = exifStructure.GetEntry(0, (ushort)ExifEntryTag.ApertureValue) as RationalIFDEntry;
|
item.Aperture = (double)entry.Value.Numerator / entry.Value.Denominator;
|
||||||
|
}
|
||||||
|
|
||||||
if (entry != null)
|
entry = exifStructure.GetEntry(0, (ushort)ExifEntryTag.ShutterSpeedValue) as RationalIFDEntry;
|
||||||
{
|
if (entry != null)
|
||||||
double val = entry.Value.Numerator;
|
{
|
||||||
val /= entry.Value.Denominator;
|
item.ShutterSpeed = (double)entry.Value.Numerator / entry.Value.Denominator;
|
||||||
item.Aperture = val;
|
|
||||||
}
|
|
||||||
|
|
||||||
entry = exifStructure.GetEntry(0, (ushort)ExifEntryTag.ShutterSpeedValue) as RationalIFDEntry;
|
|
||||||
|
|
||||||
if (entry != null)
|
|
||||||
{
|
|
||||||
double val = entry.Value.Numerator;
|
|
||||||
val /= entry.Value.Denominator;
|
|
||||||
item.ShutterSpeed = val;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (image != null)
|
if (file is TagLib.Image.File image)
|
||||||
{
|
{
|
||||||
item.CameraMake = image.ImageTag.Make;
|
item.CameraMake = image.ImageTag.Make;
|
||||||
item.CameraModel = image.ImageTag.Model;
|
item.CameraModel = image.ImageTag.Model;
|
||||||
|
@ -116,12 +99,10 @@ namespace Emby.Photos
|
||||||
|
|
||||||
item.Overview = image.ImageTag.Comment;
|
item.Overview = image.ImageTag.Comment;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(image.ImageTag.Title))
|
if (!string.IsNullOrWhiteSpace(image.ImageTag.Title)
|
||||||
|
&& !item.LockedFields.Contains(MetadataFields.Name))
|
||||||
{
|
{
|
||||||
if (!item.LockedFields.Contains(MetadataFields.Name))
|
item.Name = image.ImageTag.Title;
|
||||||
{
|
|
||||||
item.Name = image.ImageTag.Title;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var dateTaken = image.ImageTag.DateTime;
|
var dateTaken = image.ImageTag.DateTime;
|
||||||
|
@ -140,12 +121,9 @@ namespace Emby.Photos
|
||||||
{
|
{
|
||||||
item.Orientation = null;
|
item.Orientation = null;
|
||||||
}
|
}
|
||||||
else
|
else if (Enum.TryParse(image.ImageTag.Orientation.ToString(), true, out ImageOrientation orientation))
|
||||||
{
|
{
|
||||||
if (Enum.TryParse(image.ImageTag.Orientation.ToString(), true, out ImageOrientation orientation))
|
item.Orientation = orientation;
|
||||||
{
|
|
||||||
item.Orientation = orientation;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
item.ExposureTime = image.ImageTag.ExposureTime;
|
item.ExposureTime = image.ImageTag.ExposureTime;
|
||||||
|
@ -195,7 +173,5 @@ namespace Emby.Photos
|
||||||
const ItemUpdateType result = ItemUpdateType.ImageUpdate | ItemUpdateType.MetadataImport;
|
const ItemUpdateType result = ItemUpdateType.ImageUpdate | ItemUpdateType.MetadataImport;
|
||||||
return Task.FromResult(result);
|
return Task.FromResult(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Name => "Embedded Information";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,8 +83,6 @@ namespace Emby.Server.Implementations.Activity
|
||||||
|
|
||||||
_deviceManager.CameraImageUploaded += OnCameraImageUploaded;
|
_deviceManager.CameraImageUploaded += OnCameraImageUploaded;
|
||||||
|
|
||||||
_appHost.ApplicationUpdated += OnApplicationUpdated;
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -275,16 +273,6 @@ namespace Emby.Server.Implementations.Activity
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnApplicationUpdated(object sender, GenericEventArgs<PackageVersionInfo> e)
|
|
||||||
{
|
|
||||||
CreateLogEntry(new ActivityLogEntry
|
|
||||||
{
|
|
||||||
Name = string.Format(_localization.GetLocalizedString("MessageApplicationUpdatedTo"), e.Argument.versionStr),
|
|
||||||
Type = NotificationType.ApplicationUpdateInstalled.ToString(),
|
|
||||||
Overview = e.Argument.description
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnUserPolicyUpdated(object sender, GenericEventArgs<User> e)
|
private void OnUserPolicyUpdated(object sender, GenericEventArgs<User> e)
|
||||||
{
|
{
|
||||||
CreateLogEntry(new ActivityLogEntry
|
CreateLogEntry(new ActivityLogEntry
|
||||||
|
@ -460,8 +448,6 @@ namespace Emby.Server.Implementations.Activity
|
||||||
_userManager.UserLockedOut -= OnUserLockedOut;
|
_userManager.UserLockedOut -= OnUserLockedOut;
|
||||||
|
|
||||||
_deviceManager.CameraImageUploaded -= OnCameraImageUploaded;
|
_deviceManager.CameraImageUploaded -= OnCameraImageUploaded;
|
||||||
|
|
||||||
_appHost.ApplicationUpdated -= OnApplicationUpdated;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -154,11 +154,6 @@ namespace Emby.Server.Implementations
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public event EventHandler HasPendingRestartChanged;
|
public event EventHandler HasPendingRestartChanged;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Occurs when [application updated].
|
|
||||||
/// </summary>
|
|
||||||
public event EventHandler<GenericEventArgs<PackageVersionInfo>> ApplicationUpdated;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a value indicating whether this instance has changes that require the entire application to restart.
|
/// Gets a value indicating whether this instance has changes that require the entire application to restart.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -173,11 +168,17 @@ namespace Emby.Server.Implementations
|
||||||
/// <value>The logger.</value>
|
/// <value>The logger.</value>
|
||||||
protected ILogger Logger { get; set; }
|
protected ILogger Logger { get; set; }
|
||||||
|
|
||||||
|
private IPlugin[] _plugins;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the plugins.
|
/// Gets the plugins.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The plugins.</value>
|
/// <value>The plugins.</value>
|
||||||
public IPlugin[] Plugins { get; protected set; }
|
public IPlugin[] Plugins
|
||||||
|
{
|
||||||
|
get => _plugins;
|
||||||
|
protected set => _plugins = value;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the logger factory.
|
/// Gets or sets the logger factory.
|
||||||
|
@ -200,7 +201,7 @@ namespace Emby.Server.Implementations
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The disposable parts
|
/// The disposable parts
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected readonly List<IDisposable> _disposableParts = new List<IDisposable>();
|
private readonly List<IDisposable> _disposableParts = new List<IDisposable>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the configuration manager.
|
/// Gets the configuration manager.
|
||||||
|
@ -216,8 +217,9 @@ namespace Emby.Server.Implementations
|
||||||
{
|
{
|
||||||
#if BETA
|
#if BETA
|
||||||
return PackageVersionClass.Beta;
|
return PackageVersionClass.Beta;
|
||||||
#endif
|
#else
|
||||||
return PackageVersionClass.Release;
|
return PackageVersionClass.Release;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -340,7 +342,6 @@ namespace Emby.Server.Implementations
|
||||||
|
|
||||||
protected IProcessFactory ProcessFactory { get; private set; }
|
protected IProcessFactory ProcessFactory { get; private set; }
|
||||||
|
|
||||||
protected ICryptoProvider CryptographyProvider = new CryptographyProvider();
|
|
||||||
protected readonly IXmlSerializer XmlSerializer;
|
protected readonly IXmlSerializer XmlSerializer;
|
||||||
|
|
||||||
protected ISocketFactory SocketFactory { get; private set; }
|
protected ISocketFactory SocketFactory { get; private set; }
|
||||||
|
@ -369,9 +370,6 @@ namespace Emby.Server.Implementations
|
||||||
{
|
{
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
|
|
||||||
// hack alert, until common can target .net core
|
|
||||||
BaseExtensions.CryptographyProvider = CryptographyProvider;
|
|
||||||
|
|
||||||
XmlSerializer = new MyXmlSerializer(fileSystem, loggerFactory);
|
XmlSerializer = new MyXmlSerializer(fileSystem, loggerFactory);
|
||||||
|
|
||||||
NetworkManager = networkManager;
|
NetworkManager = networkManager;
|
||||||
|
@ -617,8 +615,6 @@ namespace Emby.Server.Implementations
|
||||||
|
|
||||||
DiscoverTypes();
|
DiscoverTypes();
|
||||||
|
|
||||||
SetHttpLimit();
|
|
||||||
|
|
||||||
await RegisterResources(serviceCollection).ConfigureAwait(false);
|
await RegisterResources(serviceCollection).ConfigureAwait(false);
|
||||||
|
|
||||||
FindParts();
|
FindParts();
|
||||||
|
@ -735,13 +731,12 @@ namespace Emby.Server.Implementations
|
||||||
ApplicationHost.StreamHelper = new StreamHelper();
|
ApplicationHost.StreamHelper = new StreamHelper();
|
||||||
serviceCollection.AddSingleton(StreamHelper);
|
serviceCollection.AddSingleton(StreamHelper);
|
||||||
|
|
||||||
serviceCollection.AddSingleton(CryptographyProvider);
|
serviceCollection.AddSingleton(typeof(ICryptoProvider), typeof(CryptographyProvider));
|
||||||
|
|
||||||
SocketFactory = new SocketFactory();
|
SocketFactory = new SocketFactory();
|
||||||
serviceCollection.AddSingleton(SocketFactory);
|
serviceCollection.AddSingleton(SocketFactory);
|
||||||
|
|
||||||
InstallationManager = new InstallationManager(LoggerFactory, this, ApplicationPaths, HttpClient, JsonSerializer, ServerConfigurationManager, FileSystemManager, CryptographyProvider, ZipClient, PackageRuntime);
|
serviceCollection.AddSingleton(typeof(IInstallationManager), typeof(InstallationManager));
|
||||||
serviceCollection.AddSingleton(InstallationManager);
|
|
||||||
|
|
||||||
ZipClient = new ZipClient();
|
ZipClient = new ZipClient();
|
||||||
serviceCollection.AddSingleton(ZipClient);
|
serviceCollection.AddSingleton(ZipClient);
|
||||||
|
@ -908,8 +903,6 @@ namespace Emby.Server.Implementations
|
||||||
_serviceProvider = serviceCollection.BuildServiceProvider();
|
_serviceProvider = serviceCollection.BuildServiceProvider();
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual string PackageRuntime => "netcore";
|
|
||||||
|
|
||||||
public static void LogEnvironmentInfo(ILogger logger, IApplicationPaths appPaths)
|
public static void LogEnvironmentInfo(ILogger logger, IApplicationPaths appPaths)
|
||||||
{
|
{
|
||||||
// Distinct these to prevent users from reporting problems that aren't actually problems
|
// Distinct these to prevent users from reporting problems that aren't actually problems
|
||||||
|
@ -918,8 +911,7 @@ namespace Emby.Server.Implementations
|
||||||
.Distinct();
|
.Distinct();
|
||||||
|
|
||||||
logger.LogInformation("Arguments: {Args}", commandLineArgs);
|
logger.LogInformation("Arguments: {Args}", commandLineArgs);
|
||||||
// FIXME: @bond this logs the kernel version, not the OS version
|
logger.LogInformation("Operating system: {OS}", OperatingSystem.Name);
|
||||||
logger.LogInformation("Operating system: {OS} {OSVersion}", OperatingSystem.Name, Environment.OSVersion.Version);
|
|
||||||
logger.LogInformation("Architecture: {Architecture}", RuntimeInformation.OSArchitecture);
|
logger.LogInformation("Architecture: {Architecture}", RuntimeInformation.OSArchitecture);
|
||||||
logger.LogInformation("64-Bit Process: {Is64Bit}", Environment.Is64BitProcess);
|
logger.LogInformation("64-Bit Process: {Is64Bit}", Environment.Is64BitProcess);
|
||||||
logger.LogInformation("User Interactive: {IsUserInteractive}", Environment.UserInteractive);
|
logger.LogInformation("User Interactive: {IsUserInteractive}", Environment.UserInteractive);
|
||||||
|
@ -929,19 +921,6 @@ namespace Emby.Server.Implementations
|
||||||
logger.LogInformation("Application directory: {ApplicationPath}", appPaths.ProgramSystemPath);
|
logger.LogInformation("Application directory: {ApplicationPath}", appPaths.ProgramSystemPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetHttpLimit()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Increase the max http request limit
|
|
||||||
ServicePointManager.DefaultConnectionLimit = Math.Max(96, ServicePointManager.DefaultConnectionLimit);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogError(ex, "Error setting http limit");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private X509Certificate2 GetCertificate(CertificateInfo info)
|
private X509Certificate2 GetCertificate(CertificateInfo info)
|
||||||
{
|
{
|
||||||
var certificateLocation = info?.Path;
|
var certificateLocation = info?.Path;
|
||||||
|
@ -1044,11 +1023,47 @@ namespace Emby.Server.Implementations
|
||||||
AuthenticatedAttribute.AuthService = AuthService;
|
AuthenticatedAttribute.AuthService = AuthService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async void PluginInstalled(object sender, GenericEventArgs<PackageVersionInfo> args)
|
||||||
|
{
|
||||||
|
string dir = Path.Combine(ApplicationPaths.PluginsPath, args.Argument.name);
|
||||||
|
var types = Directory.EnumerateFiles(dir, "*.dll", SearchOption.AllDirectories)
|
||||||
|
.Select(x => Assembly.LoadFrom(x))
|
||||||
|
.SelectMany(x => x.ExportedTypes)
|
||||||
|
.Where(x => x.IsClass && !x.IsAbstract && !x.IsInterface && !x.IsGenericType)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
types.AddRange(types);
|
||||||
|
|
||||||
|
var plugins = types.Where(x => x.IsAssignableFrom(typeof(IPlugin)))
|
||||||
|
.Select(CreateInstanceSafe)
|
||||||
|
.Where(x => x != null)
|
||||||
|
.Cast<IPlugin>()
|
||||||
|
.Select(LoadPlugin)
|
||||||
|
.Where(x => x != null)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
int oldLen = _plugins.Length;
|
||||||
|
Array.Resize<IPlugin>(ref _plugins, _plugins.Length + plugins.Length);
|
||||||
|
plugins.CopyTo(_plugins, oldLen);
|
||||||
|
|
||||||
|
var entries = types.Where(x => x.IsAssignableFrom(typeof(IServerEntryPoint)))
|
||||||
|
.Select(CreateInstanceSafe)
|
||||||
|
.Where(x => x != null)
|
||||||
|
.Cast<IServerEntryPoint>()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
await Task.WhenAll(StartEntryPoints(entries, true));
|
||||||
|
await Task.WhenAll(StartEntryPoints(entries, false));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Finds the parts.
|
/// Finds the parts.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected void FindParts()
|
protected void FindParts()
|
||||||
{
|
{
|
||||||
|
InstallationManager = _serviceProvider.GetService<IInstallationManager>();
|
||||||
|
InstallationManager.PluginInstalled += PluginInstalled;
|
||||||
|
|
||||||
if (!ServerConfigurationManager.Configuration.IsPortAuthorized)
|
if (!ServerConfigurationManager.Configuration.IsPortAuthorized)
|
||||||
{
|
{
|
||||||
ServerConfigurationManager.Configuration.IsPortAuthorized = true;
|
ServerConfigurationManager.Configuration.IsPortAuthorized = true;
|
||||||
|
@ -1088,7 +1103,7 @@ namespace Emby.Server.Implementations
|
||||||
MediaSourceManager.AddParts(GetExports<IMediaSourceProvider>());
|
MediaSourceManager.AddParts(GetExports<IMediaSourceProvider>());
|
||||||
|
|
||||||
NotificationManager.AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>());
|
NotificationManager.AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>());
|
||||||
UserManager.AddParts(GetExports<IAuthenticationProvider>());
|
UserManager.AddParts(GetExports<IAuthenticationProvider>(), GetExports<IPasswordResetProvider>());
|
||||||
|
|
||||||
IsoManager.AddParts(GetExports<IIsoMounter>());
|
IsoManager.AddParts(GetExports<IIsoMounter>());
|
||||||
}
|
}
|
||||||
|
@ -1131,7 +1146,7 @@ namespace Emby.Server.Implementations
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1145,10 +1160,32 @@ namespace Emby.Server.Implementations
|
||||||
{
|
{
|
||||||
Logger.LogInformation("Loading assemblies");
|
Logger.LogInformation("Loading assemblies");
|
||||||
|
|
||||||
AllConcreteTypes = GetComposablePartAssemblies()
|
AllConcreteTypes = GetTypes(GetComposablePartAssemblies()).ToArray();
|
||||||
.SelectMany(x => x.ExportedTypes)
|
}
|
||||||
.Where(type => type.IsClass && !type.IsAbstract && !type.IsInterface && !type.IsGenericType)
|
|
||||||
.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; }
|
private CertificateInfo CertificateInfo { get; set; }
|
||||||
|
@ -1310,10 +1347,21 @@ namespace Emby.Server.Implementations
|
||||||
{
|
{
|
||||||
if (Directory.Exists(ApplicationPaths.PluginsPath))
|
if (Directory.Exists(ApplicationPaths.PluginsPath))
|
||||||
{
|
{
|
||||||
foreach (var file in Directory.EnumerateFiles(ApplicationPaths.PluginsPath, "*.dll", SearchOption.TopDirectoryOnly))
|
foreach (var file in Directory.EnumerateFiles(ApplicationPaths.PluginsPath, "*.dll", SearchOption.AllDirectories))
|
||||||
{
|
{
|
||||||
Logger.LogInformation("Loading assembly {Path}", file);
|
Assembly plugAss;
|
||||||
yield return Assembly.LoadFrom(file);
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1372,14 +1420,23 @@ namespace Emby.Server.Implementations
|
||||||
public async Task<SystemInfo> GetSystemInfo(CancellationToken cancellationToken)
|
public async Task<SystemInfo> GetSystemInfo(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
|
var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
|
||||||
var wanAddress = await GetWanApiUrl(cancellationToken).ConfigureAwait(false);
|
|
||||||
|
string wanAddress;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(ServerConfigurationManager.Configuration.WanDdns))
|
||||||
|
{
|
||||||
|
wanAddress = await GetWanApiUrlFromExternal(cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
wanAddress = GetWanApiUrl(ServerConfigurationManager.Configuration.WanDdns);
|
||||||
|
}
|
||||||
|
|
||||||
return new SystemInfo
|
return new SystemInfo
|
||||||
{
|
{
|
||||||
HasPendingRestart = HasPendingRestart,
|
HasPendingRestart = HasPendingRestart,
|
||||||
IsShuttingDown = IsShuttingDown,
|
IsShuttingDown = IsShuttingDown,
|
||||||
Version = ApplicationVersion,
|
Version = ApplicationVersion,
|
||||||
ProductName = ApplicationProductName,
|
|
||||||
WebSocketPortNumber = HttpPort,
|
WebSocketPortNumber = HttpPort,
|
||||||
CompletedInstallations = InstallationManager.CompletedInstallations.ToArray(),
|
CompletedInstallations = InstallationManager.CompletedInstallations.ToArray(),
|
||||||
Id = SystemId,
|
Id = SystemId,
|
||||||
|
@ -1422,10 +1479,21 @@ namespace Emby.Server.Implementations
|
||||||
public async Task<PublicSystemInfo> GetPublicSystemInfo(CancellationToken cancellationToken)
|
public async Task<PublicSystemInfo> GetPublicSystemInfo(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
|
var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
|
||||||
var wanAddress = await GetWanApiUrl(cancellationToken).ConfigureAwait(false);
|
|
||||||
|
string wanAddress;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(ServerConfigurationManager.Configuration.WanDdns))
|
||||||
|
{
|
||||||
|
wanAddress = await GetWanApiUrlFromExternal(cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
wanAddress = GetWanApiUrl(ServerConfigurationManager.Configuration.WanDdns);
|
||||||
|
}
|
||||||
return new PublicSystemInfo
|
return new PublicSystemInfo
|
||||||
{
|
{
|
||||||
Version = ApplicationVersion,
|
Version = ApplicationVersion,
|
||||||
|
ProductName = ApplicationProductName,
|
||||||
Id = SystemId,
|
Id = SystemId,
|
||||||
OperatingSystem = OperatingSystem.Id.ToString(),
|
OperatingSystem = OperatingSystem.Id.ToString(),
|
||||||
WanAddress = wanAddress,
|
WanAddress = wanAddress,
|
||||||
|
@ -1460,7 +1528,7 @@ namespace Emby.Server.Implementations
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> GetWanApiUrl(CancellationToken cancellationToken)
|
public async Task<string> GetWanApiUrlFromExternal(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
const string Url = "http://ipv4.icanhazip.com";
|
const string Url = "http://ipv4.icanhazip.com";
|
||||||
try
|
try
|
||||||
|
@ -1476,13 +1544,14 @@ namespace Emby.Server.Implementations
|
||||||
CancellationToken = cancellationToken
|
CancellationToken = cancellationToken
|
||||||
}).ConfigureAwait(false))
|
}).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
return GetLocalApiUrl(response.ReadToEnd().Trim());
|
return GetWanApiUrl(response.ReadToEnd().Trim());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogError(ex, "Error getting WAN Ip address information");
|
Logger.LogError(ex, "Error getting WAN Ip address information");
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1498,9 +1567,38 @@ namespace Emby.Server.Implementations
|
||||||
|
|
||||||
public string GetLocalApiUrl(string host)
|
public string GetLocalApiUrl(string host)
|
||||||
{
|
{
|
||||||
|
if (EnableHttps)
|
||||||
|
{
|
||||||
|
return string.Format("https://{0}:{1}",
|
||||||
|
host,
|
||||||
|
HttpsPort.ToString(CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
return string.Format("http://{0}:{1}",
|
return string.Format("http://{0}:{1}",
|
||||||
host,
|
host,
|
||||||
HttpPort.ToString(CultureInfo.InvariantCulture));
|
HttpPort.ToString(CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetWanApiUrl(IpAddressInfo ipAddress)
|
||||||
|
{
|
||||||
|
if (ipAddress.AddressFamily == IpAddressFamily.InterNetworkV6)
|
||||||
|
{
|
||||||
|
return GetWanApiUrl("[" + ipAddress.Address + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetWanApiUrl(ipAddress.Address);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetWanApiUrl(string host)
|
||||||
|
{
|
||||||
|
if (EnableHttps)
|
||||||
|
{
|
||||||
|
return string.Format("https://{0}:{1}",
|
||||||
|
host,
|
||||||
|
ServerConfigurationManager.Configuration.PublicHttpsPort.ToString(CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
return string.Format("http://{0}:{1}",
|
||||||
|
host,
|
||||||
|
ServerConfigurationManager.Configuration.PublicPort.ToString(CultureInfo.InvariantCulture));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<List<IpAddressInfo>> GetLocalIpAddresses(CancellationToken cancellationToken)
|
public Task<List<IpAddressInfo>> GetLocalIpAddresses(CancellationToken cancellationToken)
|
||||||
|
@ -1756,24 +1854,6 @@ namespace Emby.Server.Implementations
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called when [application updated].
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="package">The package.</param>
|
|
||||||
protected void OnApplicationUpdated(PackageVersionInfo package)
|
|
||||||
{
|
|
||||||
Logger.LogInformation("Application has been updated to version {0}", package.versionStr);
|
|
||||||
|
|
||||||
ApplicationUpdated?.Invoke(
|
|
||||||
this,
|
|
||||||
new GenericEventArgs<PackageVersionInfo>()
|
|
||||||
{
|
|
||||||
Argument = package
|
|
||||||
});
|
|
||||||
|
|
||||||
NotifyPendingRestart();
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool _disposed = false;
|
private bool _disposed = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -74,23 +74,14 @@ namespace Emby.Server.Implementations.Configuration
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void UpdateMetadataPath()
|
private void UpdateMetadataPath()
|
||||||
{
|
{
|
||||||
string metadataPath;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(Configuration.MetadataPath))
|
if (string.IsNullOrWhiteSpace(Configuration.MetadataPath))
|
||||||
{
|
{
|
||||||
metadataPath = GetInternalMetadataPath();
|
((ServerApplicationPaths)ApplicationPaths).InternalMetadataPath = Path.Combine(ApplicationPaths.ProgramDataPath, "metadata");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
metadataPath = Path.Combine(Configuration.MetadataPath, "metadata");
|
((ServerApplicationPaths)ApplicationPaths).InternalMetadataPath = Configuration.MetadataPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
((ServerApplicationPaths)ApplicationPaths).InternalMetadataPath = metadataPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetInternalMetadataPath()
|
|
||||||
{
|
|
||||||
return Path.Combine(ApplicationPaths.ProgramDataPath, "metadata");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -4,7 +4,6 @@ using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Linq;
|
|
||||||
using MediaBrowser.Model.Cryptography;
|
using MediaBrowser.Model.Cryptography;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Cryptography
|
namespace Emby.Server.Implementations.Cryptography
|
||||||
|
|
|
@ -90,9 +90,10 @@ namespace Emby.Server.Implementations.Data
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(displayPreferences));
|
throw new ArgumentNullException(nameof(displayPreferences));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(displayPreferences.Id))
|
if (string.IsNullOrEmpty(displayPreferences.Id))
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(displayPreferences.Id));
|
throw new ArgumentException("Display preferences has an invalid Id", nameof(displayPreferences));
|
||||||
}
|
}
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
|
@ -2741,15 +2741,16 @@ namespace Emby.Server.Implementations.Data
|
||||||
{
|
{
|
||||||
var elapsed = (DateTime.UtcNow - startDate).TotalMilliseconds;
|
var elapsed = (DateTime.UtcNow - startDate).TotalMilliseconds;
|
||||||
|
|
||||||
int slowThreshold = 100;
|
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
slowThreshold = 10;
|
const int SlowThreshold = 100;
|
||||||
|
#else
|
||||||
|
const int SlowThreshold = 10;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (elapsed >= slowThreshold)
|
if (elapsed >= SlowThreshold)
|
||||||
{
|
{
|
||||||
Logger.LogWarning("{0} query time (slow): {1:g}. Query: {2}",
|
Logger.LogWarning(
|
||||||
|
"{Method} query time (slow): {ElapsedMs}ms. Query: {Query}",
|
||||||
methodName,
|
methodName,
|
||||||
elapsed,
|
elapsed,
|
||||||
commandText);
|
commandText);
|
||||||
|
|
|
@ -81,7 +81,7 @@ namespace Emby.Server.Implementations.Data
|
||||||
{
|
{
|
||||||
// If the user password is the sha1 hash of the empty string, remove it
|
// If the user password is the sha1 hash of the empty string, remove it
|
||||||
if (!string.Equals(user.Password, "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal)
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
@ -388,7 +388,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||||
|
|
||||||
FoldersRemovedFrom = foldersRemovedFrom.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)).Select(i => i.Id.ToString("N")).Distinct().ToArray(),
|
FoldersRemovedFrom = foldersRemovedFrom.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)).Select(i => i.Id.ToString("N")).Distinct().ToArray(),
|
||||||
|
|
||||||
CollectionFolders = GetTopParentIds(newAndRemoved, user, allUserRootChildren).ToArray()
|
CollectionFolders = GetTopParentIds(newAndRemoved, allUserRootChildren).ToArray()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -407,7 +407,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||||
return item.SourceType == SourceType.Library;
|
return item.SourceType == SourceType.Library;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<string> GetTopParentIds(List<BaseItem> items, User user, List<Folder> allUserRootChildren)
|
private IEnumerable<string> GetTopParentIds(List<BaseItem> items, List<Folder> allUserRootChildren)
|
||||||
{
|
{
|
||||||
var list = new List<string>();
|
var list = new List<string>();
|
||||||
|
|
||||||
|
|
|
@ -67,6 +67,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(rangeHeader))
|
if (string.IsNullOrWhiteSpace(rangeHeader))
|
||||||
{
|
{
|
||||||
|
Headers[HeaderNames.ContentLength] = TotalContentLength.ToString(CultureInfo.InvariantCulture);
|
||||||
StatusCode = HttpStatusCode.OK;
|
StatusCode = HttpStatusCode.OK;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -99,10 +100,13 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
RangeStart = requestedRange.Key;
|
RangeStart = requestedRange.Key;
|
||||||
RangeLength = 1 + RangeEnd - RangeStart;
|
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}";
|
var rangeString = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}";
|
||||||
Headers[HeaderNames.ContentRange] = rangeString;
|
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>
|
/// <summary>
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
|
@ -126,12 +125,12 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
|
|
||||||
private List<IHasRequestFilter> GetRequestFilterAttributes(Type requestDtoType)
|
private List<IHasRequestFilter> GetRequestFilterAttributes(Type requestDtoType)
|
||||||
{
|
{
|
||||||
var attributes = requestDtoType.GetTypeInfo().GetCustomAttributes(true).OfType<IHasRequestFilter>().ToList();
|
var attributes = requestDtoType.GetCustomAttributes(true).OfType<IHasRequestFilter>().ToList();
|
||||||
|
|
||||||
var serviceType = GetServiceTypeByRequest(requestDtoType);
|
var serviceType = GetServiceTypeByRequest(requestDtoType);
|
||||||
if (serviceType != null)
|
if (serviceType != null)
|
||||||
{
|
{
|
||||||
attributes.AddRange(serviceType.GetTypeInfo().GetCustomAttributes(true).OfType<IHasRequestFilter>());
|
attributes.AddRange(serviceType.GetCustomAttributes(true).OfType<IHasRequestFilter>());
|
||||||
}
|
}
|
||||||
|
|
||||||
attributes.Sort((x, y) => x.Priority - y.Priority);
|
attributes.Sort((x, y) => x.Priority - y.Priority);
|
||||||
|
@ -153,7 +152,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
QueryString = e.QueryString ?? new QueryCollection()
|
QueryString = e.QueryString ?? new QueryCollection()
|
||||||
};
|
};
|
||||||
|
|
||||||
connection.Closed += Connection_Closed;
|
connection.Closed += OnConnectionClosed;
|
||||||
|
|
||||||
lock (_webSocketConnections)
|
lock (_webSocketConnections)
|
||||||
{
|
{
|
||||||
|
@ -163,7 +162,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection));
|
WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Connection_Closed(object sender, EventArgs e)
|
private void OnConnectionClosed(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
lock (_webSocketConnections)
|
lock (_webSocketConnections)
|
||||||
{
|
{
|
||||||
|
@ -202,6 +201,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
case DirectoryNotFoundException _:
|
case DirectoryNotFoundException _:
|
||||||
case FileNotFoundException _:
|
case FileNotFoundException _:
|
||||||
case ResourceNotFoundException _: return 404;
|
case ResourceNotFoundException _: return 404;
|
||||||
|
case MethodNotAllowedException _: return 405;
|
||||||
case RemoteServiceUnavailableException _: return 502;
|
case RemoteServiceUnavailableException _: return 502;
|
||||||
default: return 500;
|
default: return 500;
|
||||||
}
|
}
|
||||||
|
@ -321,14 +321,14 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
|
|
||||||
private static string NormalizeConfiguredLocalAddress(string address)
|
private static string NormalizeConfiguredLocalAddress(string address)
|
||||||
{
|
{
|
||||||
var index = address.Trim('/').IndexOf('/');
|
var add = address.AsSpan().Trim('/');
|
||||||
|
int index = add.IndexOf('/');
|
||||||
if (index != -1)
|
if (index != -1)
|
||||||
{
|
{
|
||||||
address = address.Substring(index + 1);
|
add = add.Slice(index + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return address.Trim('/');
|
return add.TrimStart('/').ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool ValidateHost(string host)
|
private bool ValidateHost(string host)
|
||||||
|
@ -398,8 +398,8 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
if (urlString.IndexOf("https://", StringComparison.OrdinalIgnoreCase) == -1)
|
if (urlString.IndexOf("https://", StringComparison.OrdinalIgnoreCase) == -1)
|
||||||
{
|
{
|
||||||
// These are hacks, but if these ever occur on ipv6 in the local network they could be incorrectly redirected
|
// These are hacks, but if these ever occur on ipv6 in the local network they could be incorrectly redirected
|
||||||
if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1 ||
|
if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1
|
||||||
urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1)
|
|| urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -571,7 +571,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
|
|
||||||
if (handler != null)
|
if (handler != null)
|
||||||
{
|
{
|
||||||
await handler.ProcessRequestAsync(this, httpReq, httpRes, Logger, httpReq.OperationName, cancellationToken).ConfigureAwait(false);
|
await handler.ProcessRequestAsync(this, httpReq, httpRes, Logger, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -612,21 +612,11 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
{
|
{
|
||||||
var pathInfo = httpReq.PathInfo;
|
var pathInfo = httpReq.PathInfo;
|
||||||
|
|
||||||
var pathParts = pathInfo.TrimStart('/').Split('/');
|
pathInfo = ServiceHandler.GetSanitizedPathInfo(pathInfo, out string contentType);
|
||||||
if (pathParts.Length == 0)
|
var restPath = ServiceController.GetRestPathForRequest(httpReq.HttpMethod, pathInfo);
|
||||||
{
|
|
||||||
Logger.LogError("Path parts empty for PathInfo: {PathInfo}, Url: {RawUrl}", pathInfo, httpReq.RawUrl);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var restPath = ServiceHandler.FindMatchingRestPath(httpReq.HttpMethod, pathInfo, out string contentType);
|
|
||||||
if (restPath != null)
|
if (restPath != null)
|
||||||
{
|
{
|
||||||
return new ServiceHandler
|
return new ServiceHandler(restPath, contentType);
|
||||||
{
|
|
||||||
RestPath = restPath,
|
|
||||||
ResponseContentType = contentType
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.LogError("Could not find handler for {PathInfo}", pathInfo);
|
Logger.LogError("Could not find handler for {PathInfo}", pathInfo);
|
||||||
|
@ -636,6 +626,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
private static Task Write(IResponse response, string text)
|
private static Task Write(IResponse response, string text)
|
||||||
{
|
{
|
||||||
var bOutput = Encoding.UTF8.GetBytes(text);
|
var bOutput = Encoding.UTF8.GetBytes(text);
|
||||||
|
response.OriginalResponse.ContentLength = bOutput.Length;
|
||||||
return response.OutputStream.WriteAsync(bOutput, 0, bOutput.Length);
|
return response.OutputStream.WriteAsync(bOutput, 0, bOutput.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -654,11 +645,6 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// TODO what is this?
|
|
||||||
var httpsUrl = url
|
|
||||||
.Replace("http://", "https://", StringComparison.OrdinalIgnoreCase)
|
|
||||||
.Replace(":" + _config.Configuration.PublicPort.ToString(CultureInfo.InvariantCulture), ":" + _config.Configuration.PublicHttpsPort.ToString(CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
RedirectToUrl(httpRes, url);
|
RedirectToUrl(httpRes, url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -683,10 +669,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
UrlPrefixes = urlPrefixes.ToArray();
|
UrlPrefixes = urlPrefixes.ToArray();
|
||||||
ServiceController = new ServiceController();
|
ServiceController = new ServiceController();
|
||||||
|
|
||||||
Logger.LogInformation("Calling ServiceStack AppHost.Init");
|
var types = services.Select(r => r.GetType());
|
||||||
|
|
||||||
var types = services.Select(r => r.GetType()).ToArray();
|
|
||||||
|
|
||||||
ServiceController.Init(this, types);
|
ServiceController.Init(this, types);
|
||||||
|
|
||||||
ResponseFilters = new Action<IRequest, IResponse, object>[]
|
ResponseFilters = new Action<IRequest, IResponse, object>[]
|
||||||
|
|
|
@ -132,7 +132,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
content = Array.Empty<byte>();
|
content = Array.Empty<byte>();
|
||||||
}
|
}
|
||||||
|
|
||||||
result = new StreamWriter(content, contentType);
|
result = new StreamWriter(content, contentType, contentLength);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -176,7 +176,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
bytes = Array.Empty<byte>();
|
bytes = Array.Empty<byte>();
|
||||||
}
|
}
|
||||||
|
|
||||||
result = new StreamWriter(bytes, contentType);
|
result = new StreamWriter(bytes, contentType, contentLength);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -335,13 +335,13 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
|
|
||||||
if (isHeadRequest)
|
if (isHeadRequest)
|
||||||
{
|
{
|
||||||
var result = new StreamWriter(Array.Empty<byte>(), contentType);
|
var result = new StreamWriter(Array.Empty<byte>(), contentType, contentLength);
|
||||||
AddResponseHeaders(result, responseHeaders);
|
AddResponseHeaders(result, responseHeaders);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var result = new StreamWriter(content, contentType);
|
var result = new StreamWriter(content, contentType, contentLength);
|
||||||
AddResponseHeaders(result, responseHeaders);
|
AddResponseHeaders(result, responseHeaders);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -581,6 +581,11 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
if (totalContentLength.HasValue)
|
||||||
|
{
|
||||||
|
responseHeaders["Content-Length"] = totalContentLength.Value.ToString(CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
if (isHeadRequest)
|
if (isHeadRequest)
|
||||||
{
|
{
|
||||||
using (stream)
|
using (stream)
|
||||||
|
@ -624,7 +629,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
|
|
||||||
if (lastModifiedDate.HasValue)
|
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;
|
RangeStart = requestedRange.Key;
|
||||||
RangeLength = 1 + RangeEnd - RangeStart;
|
RangeLength = 1 + RangeEnd - RangeStart;
|
||||||
|
|
||||||
|
Headers[HeaderNames.ContentLength] = RangeLength.ToString(CultureInfo.InvariantCulture);
|
||||||
Headers[HeaderNames.ContentRange] = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}";
|
Headers[HeaderNames.ContentRange] = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}";
|
||||||
|
|
||||||
if (RangeStart > 0 && SourceStream.CanSeek)
|
if (RangeStart > 0 && SourceStream.CanSeek)
|
||||||
|
|
|
@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
public void FilterResponse(IRequest req, IResponse res, object dto)
|
public void FilterResponse(IRequest req, IResponse res, object dto)
|
||||||
{
|
{
|
||||||
// Try to prevent compatibility view
|
// 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-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
|
||||||
res.AddHeader("Access-Control-Allow-Origin", "*");
|
res.AddHeader("Access-Control-Allow-Origin", "*");
|
||||||
|
|
||||||
|
@ -58,6 +58,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
|
|
||||||
if (length > 0)
|
if (length > 0)
|
||||||
{
|
{
|
||||||
|
res.OriginalResponse.ContentLength = length;
|
||||||
res.SendChunked = false;
|
res.SendChunked = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||||
// This code is executed before the service
|
// This code is executed before the service
|
||||||
var auth = AuthorizationContext.GetAuthorizationInfo(request);
|
var auth = AuthorizationContext.GetAuthorizationInfo(request);
|
||||||
|
|
||||||
if (!IsExemptFromAuthenticationToken(auth, authAttribtues, request))
|
if (!IsExemptFromAuthenticationToken(authAttribtues, request))
|
||||||
{
|
{
|
||||||
ValidateSecurityToken(request, auth.Token);
|
ValidateSecurityToken(request, auth.Token);
|
||||||
}
|
}
|
||||||
|
@ -122,7 +122,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsExemptFromAuthenticationToken(AuthorizationInfo auth, IAuthenticationAttributes authAttribtues, IRequest request)
|
private bool IsExemptFromAuthenticationToken(IAuthenticationAttributes authAttribtues, IRequest request)
|
||||||
{
|
{
|
||||||
if (!_config.Configuration.IsStartupWizardCompleted && authAttribtues.AllowBeforeStartupWizard)
|
if (!_config.Configuration.IsStartupWizardCompleted && authAttribtues.AllowBeforeStartupWizard)
|
||||||
{
|
{
|
||||||
|
|
|
@ -14,8 +14,6 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class StreamWriter : IAsyncStreamWriter, IHasHeaders
|
public class StreamWriter : IAsyncStreamWriter, IHasHeaders
|
||||||
{
|
{
|
||||||
private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the source stream.
|
/// Gets or sets the source stream.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -52,6 +50,13 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
|
|
||||||
SourceStream = source;
|
SourceStream = source;
|
||||||
|
|
||||||
|
Headers["Content-Type"] = contentType;
|
||||||
|
|
||||||
|
if (source.CanSeek)
|
||||||
|
{
|
||||||
|
Headers[HeaderNames.ContentLength] = source.Length.ToString(CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
Headers[HeaderNames.ContentType] = contentType;
|
Headers[HeaderNames.ContentType] = contentType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,7 +65,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="source">The source.</param>
|
/// <param name="source">The source.</param>
|
||||||
/// <param name="contentType">Type of the content.</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))
|
if (string.IsNullOrEmpty(contentType))
|
||||||
{
|
{
|
||||||
|
@ -69,6 +74,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
|
|
||||||
SourceBytes = source;
|
SourceBytes = source;
|
||||||
|
|
||||||
|
Headers[HeaderNames.ContentLength] = contentLength.ToString(CultureInfo.InvariantCulture);
|
||||||
Headers[HeaderNames.ContentType] = contentType;
|
Headers[HeaderNames.ContentType] = contentType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,10 +6,6 @@ using System.Threading;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Model.Extensions;
|
|
||||||
using MediaBrowser.Model.IO;
|
|
||||||
using MediaBrowser.Model.System;
|
|
||||||
using MediaBrowser.Model.Tasks;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.IO
|
namespace Emby.Server.Implementations.IO
|
||||||
|
@ -61,6 +57,7 @@ namespace Emby.Server.Implementations.IO
|
||||||
{
|
{
|
||||||
AddAffectedPath(path);
|
AddAffectedPath(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
RestartTimer();
|
RestartTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,6 +100,7 @@ namespace Emby.Server.Implementations.IO
|
||||||
AddAffectedPath(affectedFile);
|
AddAffectedPath(affectedFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RestartTimer();
|
RestartTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,9 +9,7 @@ using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Plugins;
|
using MediaBrowser.Controller.Plugins;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Model.System;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.IO
|
namespace Emby.Server.Implementations.IO
|
||||||
{
|
{
|
||||||
|
@ -21,6 +19,7 @@ namespace Emby.Server.Implementations.IO
|
||||||
/// The file system watchers
|
/// The file system watchers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly ConcurrentDictionary<string, FileSystemWatcher> _fileSystemWatchers = new ConcurrentDictionary<string, FileSystemWatcher>(StringComparer.OrdinalIgnoreCase);
|
private readonly ConcurrentDictionary<string, FileSystemWatcher> _fileSystemWatchers = new ConcurrentDictionary<string, FileSystemWatcher>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The affected paths
|
/// The affected paths
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -97,7 +96,7 @@ namespace Emby.Server.Implementations.IO
|
||||||
throw new ArgumentNullException(nameof(path));
|
throw new ArgumentNullException(nameof(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is an arbitraty amount of time, but delay it because file system writes often trigger events long after the file was actually written to.
|
// This is an arbitrary amount of time, but delay it because file system writes often trigger events long after the file was actually written to.
|
||||||
// Seeing long delays in some situations, especially over the network, sometimes up to 45 seconds
|
// Seeing long delays in some situations, especially over the network, sometimes up to 45 seconds
|
||||||
// But if we make this delay too high, we risk missing legitimate changes, such as user adding a new file, or hand-editing metadata
|
// But if we make this delay too high, we risk missing legitimate changes, such as user adding a new file, or hand-editing metadata
|
||||||
await Task.Delay(45000).ConfigureAwait(false);
|
await Task.Delay(45000).ConfigureAwait(false);
|
||||||
|
@ -162,10 +161,10 @@ namespace Emby.Server.Implementations.IO
|
||||||
|
|
||||||
public void Start()
|
public void Start()
|
||||||
{
|
{
|
||||||
LibraryManager.ItemAdded += LibraryManager_ItemAdded;
|
LibraryManager.ItemAdded += OnLibraryManagerItemAdded;
|
||||||
LibraryManager.ItemRemoved += LibraryManager_ItemRemoved;
|
LibraryManager.ItemRemoved += OnLibraryManagerItemRemoved;
|
||||||
|
|
||||||
var pathsToWatch = new List<string> { };
|
var pathsToWatch = new List<string>();
|
||||||
|
|
||||||
var paths = LibraryManager
|
var paths = LibraryManager
|
||||||
.RootFolder
|
.RootFolder
|
||||||
|
@ -204,7 +203,7 @@ namespace Emby.Server.Implementations.IO
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="sender">The source of the event.</param>
|
/// <param name="sender">The source of the event.</param>
|
||||||
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
|
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
|
||||||
void LibraryManager_ItemRemoved(object sender, ItemChangeEventArgs e)
|
private void OnLibraryManagerItemRemoved(object sender, ItemChangeEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Parent is AggregateFolder)
|
if (e.Parent is AggregateFolder)
|
||||||
{
|
{
|
||||||
|
@ -217,7 +216,7 @@ namespace Emby.Server.Implementations.IO
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="sender">The source of the event.</param>
|
/// <param name="sender">The source of the event.</param>
|
||||||
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
|
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
|
||||||
void LibraryManager_ItemAdded(object sender, ItemChangeEventArgs e)
|
private void OnLibraryManagerItemAdded(object sender, ItemChangeEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Parent is AggregateFolder)
|
if (e.Parent is AggregateFolder)
|
||||||
{
|
{
|
||||||
|
@ -244,7 +243,7 @@ namespace Emby.Server.Implementations.IO
|
||||||
|
|
||||||
return lst.Any(str =>
|
return lst.Any(str =>
|
||||||
{
|
{
|
||||||
//this should be a little quicker than examining each actual parent folder...
|
// this should be a little quicker than examining each actual parent folder...
|
||||||
var compare = str.TrimEnd(Path.DirectorySeparatorChar);
|
var compare = str.TrimEnd(Path.DirectorySeparatorChar);
|
||||||
|
|
||||||
return path.Equals(compare, StringComparison.OrdinalIgnoreCase) || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.DirectorySeparatorChar);
|
return path.Equals(compare, StringComparison.OrdinalIgnoreCase) || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.DirectorySeparatorChar);
|
||||||
|
@ -260,19 +259,10 @@ namespace Emby.Server.Implementations.IO
|
||||||
if (!Directory.Exists(path))
|
if (!Directory.Exists(path))
|
||||||
{
|
{
|
||||||
// Seeing a crash in the mono runtime due to an exception being thrown on a different thread
|
// Seeing a crash in the mono runtime due to an exception being thrown on a different thread
|
||||||
Logger.LogInformation("Skipping realtime monitor for {0} because the path does not exist", path);
|
Logger.LogInformation("Skipping realtime monitor for {Path} because the path does not exist", path);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (OperatingSystem.Id != OperatingSystemId.Windows)
|
|
||||||
{
|
|
||||||
if (path.StartsWith("\\\\", StringComparison.OrdinalIgnoreCase) || path.StartsWith("smb://", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
// not supported
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Already being watched
|
// Already being watched
|
||||||
if (_fileSystemWatchers.ContainsKey(path))
|
if (_fileSystemWatchers.ContainsKey(path))
|
||||||
{
|
{
|
||||||
|
@ -286,23 +276,21 @@ namespace Emby.Server.Implementations.IO
|
||||||
{
|
{
|
||||||
var newWatcher = new FileSystemWatcher(path, "*")
|
var newWatcher = new FileSystemWatcher(path, "*")
|
||||||
{
|
{
|
||||||
IncludeSubdirectories = true
|
IncludeSubdirectories = true,
|
||||||
|
InternalBufferSize = 65536,
|
||||||
|
NotifyFilter = NotifyFilters.CreationTime |
|
||||||
|
NotifyFilters.DirectoryName |
|
||||||
|
NotifyFilters.FileName |
|
||||||
|
NotifyFilters.LastWrite |
|
||||||
|
NotifyFilters.Size |
|
||||||
|
NotifyFilters.Attributes
|
||||||
};
|
};
|
||||||
|
|
||||||
newWatcher.InternalBufferSize = 65536;
|
newWatcher.Created += OnWatcherChanged;
|
||||||
|
newWatcher.Deleted += OnWatcherChanged;
|
||||||
newWatcher.NotifyFilter = NotifyFilters.CreationTime |
|
newWatcher.Renamed += OnWatcherChanged;
|
||||||
NotifyFilters.DirectoryName |
|
newWatcher.Changed += OnWatcherChanged;
|
||||||
NotifyFilters.FileName |
|
newWatcher.Error += OnWatcherError;
|
||||||
NotifyFilters.LastWrite |
|
|
||||||
NotifyFilters.Size |
|
|
||||||
NotifyFilters.Attributes;
|
|
||||||
|
|
||||||
newWatcher.Created += watcher_Changed;
|
|
||||||
newWatcher.Deleted += watcher_Changed;
|
|
||||||
newWatcher.Renamed += watcher_Changed;
|
|
||||||
newWatcher.Changed += watcher_Changed;
|
|
||||||
newWatcher.Error += watcher_Error;
|
|
||||||
|
|
||||||
if (_fileSystemWatchers.TryAdd(path, newWatcher))
|
if (_fileSystemWatchers.TryAdd(path, newWatcher))
|
||||||
{
|
{
|
||||||
|
@ -343,32 +331,16 @@ namespace Emby.Server.Implementations.IO
|
||||||
{
|
{
|
||||||
using (watcher)
|
using (watcher)
|
||||||
{
|
{
|
||||||
Logger.LogInformation("Stopping directory watching for path {path}", watcher.Path);
|
Logger.LogInformation("Stopping directory watching for path {Path}", watcher.Path);
|
||||||
|
|
||||||
watcher.Created -= watcher_Changed;
|
watcher.Created -= OnWatcherChanged;
|
||||||
watcher.Deleted -= watcher_Changed;
|
watcher.Deleted -= OnWatcherChanged;
|
||||||
watcher.Renamed -= watcher_Changed;
|
watcher.Renamed -= OnWatcherChanged;
|
||||||
watcher.Changed -= watcher_Changed;
|
watcher.Changed -= OnWatcherChanged;
|
||||||
watcher.Error -= watcher_Error;
|
watcher.Error -= OnWatcherError;
|
||||||
|
|
||||||
try
|
watcher.EnableRaisingEvents = false;
|
||||||
{
|
|
||||||
watcher.EnableRaisingEvents = false;
|
|
||||||
}
|
|
||||||
catch (InvalidOperationException)
|
|
||||||
{
|
|
||||||
// Seeing this under mono on linux sometimes
|
|
||||||
// Collection was modified; enumeration operation may not execute.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
catch (NotImplementedException)
|
|
||||||
{
|
|
||||||
// the dispose method on FileSystemWatcher is sometimes throwing NotImplementedException on Xamarin Android
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
@ -385,7 +357,7 @@ namespace Emby.Server.Implementations.IO
|
||||||
/// <param name="watcher">The watcher.</param>
|
/// <param name="watcher">The watcher.</param>
|
||||||
private void RemoveWatcherFromList(FileSystemWatcher watcher)
|
private void RemoveWatcherFromList(FileSystemWatcher watcher)
|
||||||
{
|
{
|
||||||
_fileSystemWatchers.TryRemove(watcher.Path, out var removed);
|
_fileSystemWatchers.TryRemove(watcher.Path, out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -393,12 +365,12 @@ namespace Emby.Server.Implementations.IO
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="sender">The source of the event.</param>
|
/// <param name="sender">The source of the event.</param>
|
||||||
/// <param name="e">The <see cref="ErrorEventArgs" /> instance containing the event data.</param>
|
/// <param name="e">The <see cref="ErrorEventArgs" /> instance containing the event data.</param>
|
||||||
void watcher_Error(object sender, ErrorEventArgs e)
|
private void OnWatcherError(object sender, ErrorEventArgs e)
|
||||||
{
|
{
|
||||||
var ex = e.GetException();
|
var ex = e.GetException();
|
||||||
var dw = (FileSystemWatcher)sender;
|
var dw = (FileSystemWatcher)sender;
|
||||||
|
|
||||||
Logger.LogError(ex, "Error in Directory watcher for: {path}", dw.Path);
|
Logger.LogError(ex, "Error in Directory watcher for: {Path}", dw.Path);
|
||||||
|
|
||||||
DisposeWatcher(dw, true);
|
DisposeWatcher(dw, true);
|
||||||
}
|
}
|
||||||
|
@ -408,15 +380,11 @@ namespace Emby.Server.Implementations.IO
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="sender">The source of the event.</param>
|
/// <param name="sender">The source of the event.</param>
|
||||||
/// <param name="e">The <see cref="FileSystemEventArgs" /> instance containing the event data.</param>
|
/// <param name="e">The <see cref="FileSystemEventArgs" /> instance containing the event data.</param>
|
||||||
void watcher_Changed(object sender, FileSystemEventArgs e)
|
private void OnWatcherChanged(object sender, FileSystemEventArgs e)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
//logger.LogDebug("Changed detected of type " + e.ChangeType + " to " + e.FullPath);
|
ReportFileSystemChanged(e.FullPath);
|
||||||
|
|
||||||
var path = e.FullPath;
|
|
||||||
|
|
||||||
ReportFileSystemChanged(path);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
@ -446,25 +414,22 @@ namespace Emby.Server.Implementations.IO
|
||||||
{
|
{
|
||||||
if (_fileSystem.AreEqual(i, path))
|
if (_fileSystem.AreEqual(i, path))
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Ignoring change to {path}", path);
|
Logger.LogDebug("Ignoring change to {Path}", path);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_fileSystem.ContainsSubPath(i, path))
|
if (_fileSystem.ContainsSubPath(i, path))
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Ignoring change to {path}", path);
|
Logger.LogDebug("Ignoring change to {Path}", path);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Go up a level
|
// Go up a level
|
||||||
var parent = Path.GetDirectoryName(i);
|
var parent = Path.GetDirectoryName(i);
|
||||||
if (!string.IsNullOrEmpty(parent))
|
if (!string.IsNullOrEmpty(parent) && _fileSystem.AreEqual(parent, path))
|
||||||
{
|
{
|
||||||
if (_fileSystem.AreEqual(parent, path))
|
Logger.LogDebug("Ignoring change to {Path}", path);
|
||||||
{
|
return true;
|
||||||
Logger.LogDebug("Ignoring change to {path}", path);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
@ -487,8 +452,7 @@ namespace Emby.Server.Implementations.IO
|
||||||
|
|
||||||
lock (_activeRefreshers)
|
lock (_activeRefreshers)
|
||||||
{
|
{
|
||||||
var refreshers = _activeRefreshers.ToList();
|
foreach (var refresher in _activeRefreshers)
|
||||||
foreach (var refresher in refreshers)
|
|
||||||
{
|
{
|
||||||
// Path is already being refreshed
|
// Path is already being refreshed
|
||||||
if (_fileSystem.AreEqual(path, refresher.Path))
|
if (_fileSystem.AreEqual(path, refresher.Path))
|
||||||
|
@ -536,8 +500,8 @@ namespace Emby.Server.Implementations.IO
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Stop()
|
public void Stop()
|
||||||
{
|
{
|
||||||
LibraryManager.ItemAdded -= LibraryManager_ItemAdded;
|
LibraryManager.ItemAdded -= OnLibraryManagerItemAdded;
|
||||||
LibraryManager.ItemRemoved -= LibraryManager_ItemRemoved;
|
LibraryManager.ItemRemoved -= OnLibraryManagerItemRemoved;
|
||||||
|
|
||||||
foreach (var watcher in _fileSystemWatchers.Values.ToList())
|
foreach (var watcher in _fileSystemWatchers.Values.ToList())
|
||||||
{
|
{
|
||||||
|
@ -565,17 +529,20 @@ namespace Emby.Server.Implementations.IO
|
||||||
{
|
{
|
||||||
refresher.Dispose();
|
refresher.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
_activeRefreshers.Clear();
|
_activeRefreshers.Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool _disposed;
|
private bool _disposed = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Dispose(true);
|
Dispose(true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -19,8 +19,6 @@ namespace Emby.Server.Implementations.IO
|
||||||
{
|
{
|
||||||
protected ILogger Logger;
|
protected ILogger Logger;
|
||||||
|
|
||||||
private readonly bool _supportsAsyncFileStreams;
|
|
||||||
private char[] _invalidFileNameChars;
|
|
||||||
private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>();
|
private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>();
|
||||||
|
|
||||||
private readonly string _tempPath;
|
private readonly string _tempPath;
|
||||||
|
@ -32,11 +30,8 @@ namespace Emby.Server.Implementations.IO
|
||||||
IApplicationPaths applicationPaths)
|
IApplicationPaths applicationPaths)
|
||||||
{
|
{
|
||||||
Logger = loggerFactory.CreateLogger("FileSystem");
|
Logger = loggerFactory.CreateLogger("FileSystem");
|
||||||
_supportsAsyncFileStreams = true;
|
|
||||||
_tempPath = applicationPaths.TempDirectory;
|
_tempPath = applicationPaths.TempDirectory;
|
||||||
|
|
||||||
SetInvalidFileNameChars(OperatingSystem.Id == OperatingSystemId.Windows);
|
|
||||||
|
|
||||||
_isEnvironmentCaseInsensitive = OperatingSystem.Id == OperatingSystemId.Windows;
|
_isEnvironmentCaseInsensitive = OperatingSystem.Id == OperatingSystemId.Windows;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,20 +40,6 @@ namespace Emby.Server.Implementations.IO
|
||||||
_shortcutHandlers.Add(handler);
|
_shortcutHandlers.Add(handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void SetInvalidFileNameChars(bool enableManagedInvalidFileNameChars)
|
|
||||||
{
|
|
||||||
if (enableManagedInvalidFileNameChars)
|
|
||||||
{
|
|
||||||
_invalidFileNameChars = Path.GetInvalidFileNameChars();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Be consistent across platforms because the windows server will fail to query network shares that don't follow windows conventions
|
|
||||||
// https://referencesource.microsoft.com/#mscorlib/system/io/path.cs
|
|
||||||
_invalidFileNameChars = new char[] { '\"', '<', '>', '|', '\0', (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10, (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20, (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30, (char)31, ':', '*', '?', '\\', '/' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determines whether the specified filename is shortcut.
|
/// Determines whether the specified filename is shortcut.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -92,20 +73,22 @@ namespace Emby.Server.Implementations.IO
|
||||||
var extension = Path.GetExtension(filename);
|
var extension = Path.GetExtension(filename);
|
||||||
var handler = _shortcutHandlers.FirstOrDefault(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase));
|
var handler = _shortcutHandlers.FirstOrDefault(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
if (handler != null)
|
return handler?.Resolve(filename);
|
||||||
{
|
|
||||||
return handler.Resolve(filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual string MakeAbsolutePath(string folderPath, string filePath)
|
public virtual string MakeAbsolutePath(string folderPath, string filePath)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(filePath)) return filePath;
|
if (string.IsNullOrWhiteSpace(filePath)
|
||||||
|
// stream
|
||||||
|
|| filePath.Contains("://"))
|
||||||
|
{
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
if (filePath.Contains(@"://")) return filePath; //stream
|
if (filePath.Length > 3 && filePath[1] == ':' && filePath[2] == '/')
|
||||||
if (filePath.Length > 3 && filePath[1] == ':' && filePath[2] == '/') return filePath; //absolute local path
|
{
|
||||||
|
return filePath; // absolute local path
|
||||||
|
}
|
||||||
|
|
||||||
// unc path
|
// unc path
|
||||||
if (filePath.StartsWith("\\\\"))
|
if (filePath.StartsWith("\\\\"))
|
||||||
|
@ -125,9 +108,7 @@ namespace Emby.Server.Implementations.IO
|
||||||
}
|
}
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string path = System.IO.Path.Combine(folderPath, filePath);
|
return Path.Combine(Path.GetFullPath(folderPath), filePath);
|
||||||
path = System.IO.Path.GetFullPath(path);
|
|
||||||
return path;
|
|
||||||
}
|
}
|
||||||
catch (ArgumentException)
|
catch (ArgumentException)
|
||||||
{
|
{
|
||||||
|
@ -166,7 +147,7 @@ namespace Emby.Server.Implementations.IO
|
||||||
}
|
}
|
||||||
|
|
||||||
var extension = Path.GetExtension(shortcutPath);
|
var extension = Path.GetExtension(shortcutPath);
|
||||||
var handler = _shortcutHandlers.FirstOrDefault(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase));
|
var handler = _shortcutHandlers.Find(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
if (handler != null)
|
if (handler != null)
|
||||||
{
|
{
|
||||||
|
@ -244,12 +225,13 @@ namespace Emby.Server.Implementations.IO
|
||||||
|
|
||||||
private FileSystemMetadata GetFileSystemMetadata(FileSystemInfo info)
|
private FileSystemMetadata GetFileSystemMetadata(FileSystemInfo info)
|
||||||
{
|
{
|
||||||
var result = new FileSystemMetadata();
|
var result = new FileSystemMetadata
|
||||||
|
{
|
||||||
result.Exists = info.Exists;
|
Exists = info.Exists,
|
||||||
result.FullName = info.FullName;
|
FullName = info.FullName,
|
||||||
result.Extension = info.Extension;
|
Extension = info.Extension,
|
||||||
result.Name = info.Name;
|
Name = info.Name
|
||||||
|
};
|
||||||
|
|
||||||
if (result.Exists)
|
if (result.Exists)
|
||||||
{
|
{
|
||||||
|
@ -260,8 +242,7 @@ namespace Emby.Server.Implementations.IO
|
||||||
// result.IsHidden = (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden;
|
// result.IsHidden = (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden;
|
||||||
//}
|
//}
|
||||||
|
|
||||||
var fileInfo = info as FileInfo;
|
if (info is FileInfo fileInfo)
|
||||||
if (fileInfo != null)
|
|
||||||
{
|
{
|
||||||
result.Length = fileInfo.Length;
|
result.Length = fileInfo.Length;
|
||||||
result.DirectoryName = fileInfo.DirectoryName;
|
result.DirectoryName = fileInfo.DirectoryName;
|
||||||
|
@ -307,7 +288,7 @@ namespace Emby.Server.Implementations.IO
|
||||||
{
|
{
|
||||||
var builder = new StringBuilder(filename);
|
var builder = new StringBuilder(filename);
|
||||||
|
|
||||||
foreach (var c in _invalidFileNameChars)
|
foreach (var c in Path.GetInvalidFileNameChars())
|
||||||
{
|
{
|
||||||
builder = builder.Replace(c, ' ');
|
builder = builder.Replace(c, ' ');
|
||||||
}
|
}
|
||||||
|
@ -394,7 +375,7 @@ namespace Emby.Server.Implementations.IO
|
||||||
/// <returns>FileStream.</returns>
|
/// <returns>FileStream.</returns>
|
||||||
public virtual Stream GetFileStream(string path, FileOpenMode mode, FileAccessMode access, FileShareMode share, bool isAsync = false)
|
public virtual Stream GetFileStream(string path, FileOpenMode mode, FileAccessMode access, FileShareMode share, bool isAsync = false)
|
||||||
{
|
{
|
||||||
if (_supportsAsyncFileStreams && isAsync)
|
if (isAsync)
|
||||||
{
|
{
|
||||||
return GetFileStream(path, mode, access, share, FileOpenOptions.Asynchronous);
|
return GetFileStream(path, mode, access, share, FileOpenOptions.Asynchronous);
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,11 +17,11 @@ namespace Emby.Server.Implementations.IO
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
int read;
|
int read;
|
||||||
while ((read = await source.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) != 0)
|
while ((read = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
await destination.WriteAsync(buffer, 0, read).ConfigureAwait(false);
|
await destination.WriteAsync(buffer, 0, read, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
if (onStarted != null)
|
if (onStarted != null)
|
||||||
{
|
{
|
||||||
|
@ -44,11 +44,11 @@ namespace Emby.Server.Implementations.IO
|
||||||
if (emptyReadLimit <= 0)
|
if (emptyReadLimit <= 0)
|
||||||
{
|
{
|
||||||
int read;
|
int read;
|
||||||
while ((read = await source.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) != 0)
|
while ((read = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
await destination.WriteAsync(buffer, 0, read).ConfigureAwait(false);
|
await destination.WriteAsync(buffer, 0, read, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
@ -60,7 +60,7 @@ namespace Emby.Server.Implementations.IO
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var bytesRead = await source.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
|
var bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
if (bytesRead == 0)
|
if (bytesRead == 0)
|
||||||
{
|
{
|
||||||
|
@ -71,7 +71,7 @@ namespace Emby.Server.Implementations.IO
|
||||||
{
|
{
|
||||||
eofCount = 0;
|
eofCount = 0;
|
||||||
|
|
||||||
await destination.WriteAsync(buffer, 0, bytesRead).ConfigureAwait(false);
|
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -109,64 +109,6 @@ namespace Emby.Server.Implementations.IO
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<int> CopyToAsyncWithSyncRead(Stream source, Stream destination, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
byte[] buffer = ArrayPool<byte>.Shared.Rent(StreamCopyToBufferSize);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
int bytesRead;
|
|
||||||
int totalBytesRead = 0;
|
|
||||||
|
|
||||||
while ((bytesRead = source.Read(buffer, 0, buffer.Length)) != 0)
|
|
||||||
{
|
|
||||||
var bytesToWrite = bytesRead;
|
|
||||||
|
|
||||||
if (bytesToWrite > 0)
|
|
||||||
{
|
|
||||||
await destination.WriteAsync(buffer, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
totalBytesRead += bytesRead;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return totalBytesRead;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
ArrayPool<byte>.Shared.Return(buffer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task CopyToAsyncWithSyncRead(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
byte[] buffer = ArrayPool<byte>.Shared.Rent(StreamCopyToBufferSize);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
int bytesRead;
|
|
||||||
|
|
||||||
while ((bytesRead = source.Read(buffer, 0, buffer.Length)) != 0)
|
|
||||||
{
|
|
||||||
var bytesToWrite = Math.Min(bytesRead, copyLength);
|
|
||||||
|
|
||||||
if (bytesToWrite > 0)
|
|
||||||
{
|
|
||||||
await destination.WriteAsync(buffer, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
copyLength -= bytesToWrite;
|
|
||||||
|
|
||||||
if (copyLength <= 0)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
ArrayPool<byte>.Shared.Return(buffer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task CopyToAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken)
|
public async Task CopyToAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
byte[] buffer = ArrayPool<byte>.Shared.Rent(StreamCopyToBufferSize);
|
byte[] buffer = ArrayPool<byte>.Shared.Rent(StreamCopyToBufferSize);
|
||||||
|
@ -208,7 +150,7 @@ namespace Emby.Server.Implementations.IO
|
||||||
|
|
||||||
if (bytesRead == 0)
|
if (bytesRead == 0)
|
||||||
{
|
{
|
||||||
await Task.Delay(100).ConfigureAwait(false);
|
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -225,7 +167,7 @@ namespace Emby.Server.Implementations.IO
|
||||||
|
|
||||||
while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
|
while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
|
||||||
{
|
{
|
||||||
await destination.WriteAsync(buffer, 0, bytesRead).ConfigureAwait(false);
|
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
totalBytesRead += bytesRead;
|
totalBytesRead += bytesRead;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,355 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.IO
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Class for streaming data with throttling support.
|
|
||||||
/// </summary>
|
|
||||||
public class ThrottledStream : Stream
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A constant used to specify an infinite number of bytes that can be transferred per second.
|
|
||||||
/// </summary>
|
|
||||||
public const long Infinite = 0;
|
|
||||||
|
|
||||||
#region Private members
|
|
||||||
/// <summary>
|
|
||||||
/// The base stream.
|
|
||||||
/// </summary>
|
|
||||||
private readonly Stream _baseStream;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The maximum bytes per second that can be transferred through the base stream.
|
|
||||||
/// </summary>
|
|
||||||
private long _maximumBytesPerSecond;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The number of bytes that has been transferred since the last throttle.
|
|
||||||
/// </summary>
|
|
||||||
private long _byteCount;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The start time in milliseconds of the last throttle.
|
|
||||||
/// </summary>
|
|
||||||
private long _start;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Properties
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the current milliseconds.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The current milliseconds.</value>
|
|
||||||
protected long CurrentMilliseconds => Environment.TickCount;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the maximum bytes per second that can be transferred through the base stream.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The maximum bytes per second.</value>
|
|
||||||
public long MaximumBytesPerSecond
|
|
||||||
{
|
|
||||||
get => _maximumBytesPerSecond;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (MaximumBytesPerSecond != value)
|
|
||||||
{
|
|
||||||
_maximumBytesPerSecond = value;
|
|
||||||
Reset();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a value indicating whether the current stream supports reading.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>true if the stream supports reading; otherwise, false.</returns>
|
|
||||||
public override bool CanRead => _baseStream.CanRead;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a value indicating whether the current stream supports seeking.
|
|
||||||
/// </summary>
|
|
||||||
/// <value></value>
|
|
||||||
/// <returns>true if the stream supports seeking; otherwise, false.</returns>
|
|
||||||
public override bool CanSeek => _baseStream.CanSeek;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a value indicating whether the current stream supports writing.
|
|
||||||
/// </summary>
|
|
||||||
/// <value></value>
|
|
||||||
/// <returns>true if the stream supports writing; otherwise, false.</returns>
|
|
||||||
public override bool CanWrite => _baseStream.CanWrite;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the length in bytes of the stream.
|
|
||||||
/// </summary>
|
|
||||||
/// <value></value>
|
|
||||||
/// <returns>A long value representing the length of the stream in bytes.</returns>
|
|
||||||
/// <exception cref="T:System.NotSupportedException">The base stream does not support seeking. </exception>
|
|
||||||
/// <exception cref="T:System.ObjectDisposedException">Methods were called after the stream was closed. </exception>
|
|
||||||
public override long Length => _baseStream.Length;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the position within the current stream.
|
|
||||||
/// </summary>
|
|
||||||
/// <value></value>
|
|
||||||
/// <returns>The current position within the stream.</returns>
|
|
||||||
/// <exception cref="T:System.IO.IOException">An I/O error occurs. </exception>
|
|
||||||
/// <exception cref="T:System.NotSupportedException">The base stream does not support seeking. </exception>
|
|
||||||
/// <exception cref="T:System.ObjectDisposedException">Methods were called after the stream was closed. </exception>
|
|
||||||
public override long Position
|
|
||||||
{
|
|
||||||
get => _baseStream.Position;
|
|
||||||
set => _baseStream.Position = value;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
public long MinThrottlePosition;
|
|
||||||
|
|
||||||
#region Ctor
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="T:ThrottledStream"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="baseStream">The base stream.</param>
|
|
||||||
/// <param name="maximumBytesPerSecond">The maximum bytes per second that can be transferred through the base stream.</param>
|
|
||||||
/// <exception cref="ArgumentNullException">Thrown when <see cref="baseStream"/> is a null reference.</exception>
|
|
||||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when <see cref="maximumBytesPerSecond"/> is a negative value.</exception>
|
|
||||||
public ThrottledStream(Stream baseStream, long maximumBytesPerSecond)
|
|
||||||
{
|
|
||||||
if (baseStream == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(baseStream));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maximumBytesPerSecond < 0)
|
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(maximumBytesPerSecond),
|
|
||||||
maximumBytesPerSecond, "The maximum number of bytes per second can't be negative.");
|
|
||||||
}
|
|
||||||
|
|
||||||
_baseStream = baseStream;
|
|
||||||
_maximumBytesPerSecond = maximumBytesPerSecond;
|
|
||||||
_start = CurrentMilliseconds;
|
|
||||||
_byteCount = 0;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Public methods
|
|
||||||
/// <summary>
|
|
||||||
/// Clears all buffers for this stream and causes any buffered data to be written to the underlying device.
|
|
||||||
/// </summary>
|
|
||||||
/// <exception cref="T:System.IO.IOException">An I/O error occurs.</exception>
|
|
||||||
public override void Flush()
|
|
||||||
{
|
|
||||||
_baseStream.Flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="buffer">An array of bytes. When this method returns, the buffer contains the specified byte array with the values between offset and (offset + count - 1) replaced by the bytes read from the current source.</param>
|
|
||||||
/// <param name="offset">The zero-based byte offset in buffer at which to begin storing the data read from the current stream.</param>
|
|
||||||
/// <param name="count">The maximum number of bytes to be read from the current stream.</param>
|
|
||||||
/// <returns>
|
|
||||||
/// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many bytes are not currently available, or zero (0) if the end of the stream has been reached.
|
|
||||||
/// </returns>
|
|
||||||
/// <exception cref="T:System.ArgumentException">The sum of offset and count is larger than the buffer length. </exception>
|
|
||||||
/// <exception cref="T:System.ObjectDisposedException">Methods were called after the stream was closed. </exception>
|
|
||||||
/// <exception cref="T:System.NotSupportedException">The base stream does not support reading. </exception>
|
|
||||||
/// <exception cref="T:System.ArgumentNullException">buffer is null. </exception>
|
|
||||||
/// <exception cref="T:System.IO.IOException">An I/O error occurs. </exception>
|
|
||||||
/// <exception cref="T:System.ArgumentOutOfRangeException">offset or count is negative. </exception>
|
|
||||||
public override int Read(byte[] buffer, int offset, int count)
|
|
||||||
{
|
|
||||||
Throttle(count);
|
|
||||||
|
|
||||||
return _baseStream.Read(buffer, offset, count);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sets the position within the current stream.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="offset">A byte offset relative to the origin parameter.</param>
|
|
||||||
/// <param name="origin">A value of type <see cref="T:System.IO.SeekOrigin"></see> indicating the reference point used to obtain the new position.</param>
|
|
||||||
/// <returns>
|
|
||||||
/// The new position within the current stream.
|
|
||||||
/// </returns>
|
|
||||||
/// <exception cref="T:System.IO.IOException">An I/O error occurs. </exception>
|
|
||||||
/// <exception cref="T:System.NotSupportedException">The base stream does not support seeking, such as if the stream is constructed from a pipe or console output. </exception>
|
|
||||||
/// <exception cref="T:System.ObjectDisposedException">Methods were called after the stream was closed. </exception>
|
|
||||||
public override long Seek(long offset, SeekOrigin origin)
|
|
||||||
{
|
|
||||||
return _baseStream.Seek(offset, origin);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sets the length of the current stream.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">The desired length of the current stream in bytes.</param>
|
|
||||||
/// <exception cref="T:System.NotSupportedException">The base stream does not support both writing and seeking, such as if the stream is constructed from a pipe or console output. </exception>
|
|
||||||
/// <exception cref="T:System.IO.IOException">An I/O error occurs. </exception>
|
|
||||||
/// <exception cref="T:System.ObjectDisposedException">Methods were called after the stream was closed. </exception>
|
|
||||||
public override void SetLength(long value)
|
|
||||||
{
|
|
||||||
_baseStream.SetLength(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private long _bytesWritten;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="buffer">An array of bytes. This method copies count bytes from buffer to the current stream.</param>
|
|
||||||
/// <param name="offset">The zero-based byte offset in buffer at which to begin copying bytes to the current stream.</param>
|
|
||||||
/// <param name="count">The number of bytes to be written to the current stream.</param>
|
|
||||||
/// <exception cref="T:System.IO.IOException">An I/O error occurs. </exception>
|
|
||||||
/// <exception cref="T:System.NotSupportedException">The base stream does not support writing. </exception>
|
|
||||||
/// <exception cref="T:System.ObjectDisposedException">Methods were called after the stream was closed. </exception>
|
|
||||||
/// <exception cref="T:System.ArgumentNullException">buffer is null. </exception>
|
|
||||||
/// <exception cref="T:System.ArgumentException">The sum of offset and count is greater than the buffer length. </exception>
|
|
||||||
/// <exception cref="T:System.ArgumentOutOfRangeException">offset or count is negative. </exception>
|
|
||||||
public override void Write(byte[] buffer, int offset, int count)
|
|
||||||
{
|
|
||||||
Throttle(count);
|
|
||||||
|
|
||||||
_baseStream.Write(buffer, offset, count);
|
|
||||||
|
|
||||||
_bytesWritten += count;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
await ThrottleAsync(count, cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
await _baseStream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
_bytesWritten += count;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a <see cref="T:System.String"></see> that represents the current <see cref="T:System.Object"></see>.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>
|
|
||||||
/// A <see cref="T:System.String"></see> that represents the current <see cref="T:System.Object"></see>.
|
|
||||||
/// </returns>
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return _baseStream.ToString();
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
private bool ThrottleCheck(int bufferSizeInBytes)
|
|
||||||
{
|
|
||||||
if (_bytesWritten < MinThrottlePosition)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the buffer isn't empty.
|
|
||||||
if (_maximumBytesPerSecond <= 0 || bufferSizeInBytes <= 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Protected methods
|
|
||||||
/// <summary>
|
|
||||||
/// Throttles for the specified buffer size in bytes.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="bufferSizeInBytes">The buffer size in bytes.</param>
|
|
||||||
protected void Throttle(int bufferSizeInBytes)
|
|
||||||
{
|
|
||||||
if (!ThrottleCheck(bufferSizeInBytes))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_byteCount += bufferSizeInBytes;
|
|
||||||
long elapsedMilliseconds = CurrentMilliseconds - _start;
|
|
||||||
|
|
||||||
if (elapsedMilliseconds > 0)
|
|
||||||
{
|
|
||||||
// Calculate the current bps.
|
|
||||||
long bps = _byteCount * 1000L / elapsedMilliseconds;
|
|
||||||
|
|
||||||
// If the bps are more then the maximum bps, try to throttle.
|
|
||||||
if (bps > _maximumBytesPerSecond)
|
|
||||||
{
|
|
||||||
// Calculate the time to sleep.
|
|
||||||
long wakeElapsed = _byteCount * 1000L / _maximumBytesPerSecond;
|
|
||||||
int toSleep = (int)(wakeElapsed - elapsedMilliseconds);
|
|
||||||
|
|
||||||
if (toSleep > 1)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// The time to sleep is more then a millisecond, so sleep.
|
|
||||||
var task = Task.Delay(toSleep);
|
|
||||||
Task.WaitAll(task);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// Eatup ThreadAbortException.
|
|
||||||
}
|
|
||||||
|
|
||||||
// A sleep has been done, reset.
|
|
||||||
Reset();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async Task ThrottleAsync(int bufferSizeInBytes, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (!ThrottleCheck(bufferSizeInBytes))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_byteCount += bufferSizeInBytes;
|
|
||||||
long elapsedMilliseconds = CurrentMilliseconds - _start;
|
|
||||||
|
|
||||||
if (elapsedMilliseconds > 0)
|
|
||||||
{
|
|
||||||
// Calculate the current bps.
|
|
||||||
long bps = _byteCount * 1000L / elapsedMilliseconds;
|
|
||||||
|
|
||||||
// If the bps are more then the maximum bps, try to throttle.
|
|
||||||
if (bps > _maximumBytesPerSecond)
|
|
||||||
{
|
|
||||||
// Calculate the time to sleep.
|
|
||||||
long wakeElapsed = _byteCount * 1000L / _maximumBytesPerSecond;
|
|
||||||
int toSleep = (int)(wakeElapsed - elapsedMilliseconds);
|
|
||||||
|
|
||||||
if (toSleep > 1)
|
|
||||||
{
|
|
||||||
// The time to sleep is more then a millisecond, so sleep.
|
|
||||||
await Task.Delay(toSleep, cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
// A sleep has been done, reset.
|
|
||||||
Reset();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Will reset the bytecount to 0 and reset the start time to the current time.
|
|
||||||
/// </summary>
|
|
||||||
protected void Reset()
|
|
||||||
{
|
|
||||||
long difference = CurrentMilliseconds - _start;
|
|
||||||
|
|
||||||
// Only reset counters when a known history is available of more then 1 second.
|
|
||||||
if (difference > 1000)
|
|
||||||
{
|
|
||||||
_byteCount = 0;
|
|
||||||
_start = CurrentMilliseconds;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,132 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MediaBrowser.Common.Extensions;
|
||||||
|
using MediaBrowser.Controller.Authentication;
|
||||||
|
using MediaBrowser.Controller.Configuration;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
|
using MediaBrowser.Model.Cryptography;
|
||||||
|
using MediaBrowser.Model.Serialization;
|
||||||
|
using MediaBrowser.Model.Users;
|
||||||
|
|
||||||
|
namespace Emby.Server.Implementations.Library
|
||||||
|
{
|
||||||
|
public class DefaultPasswordResetProvider : IPasswordResetProvider
|
||||||
|
{
|
||||||
|
public string Name => "Default Password Reset Provider";
|
||||||
|
|
||||||
|
public bool IsEnabled => true;
|
||||||
|
|
||||||
|
private readonly string _passwordResetFileBase;
|
||||||
|
private readonly string _passwordResetFileBaseDir;
|
||||||
|
private readonly string _passwordResetFileBaseName = "passwordreset";
|
||||||
|
|
||||||
|
private readonly IJsonSerializer _jsonSerializer;
|
||||||
|
private readonly IUserManager _userManager;
|
||||||
|
private readonly ICryptoProvider _crypto;
|
||||||
|
|
||||||
|
public DefaultPasswordResetProvider(IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IUserManager userManager, ICryptoProvider cryptoProvider)
|
||||||
|
{
|
||||||
|
_passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath;
|
||||||
|
_passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, _passwordResetFileBaseName);
|
||||||
|
_jsonSerializer = jsonSerializer;
|
||||||
|
_userManager = userManager;
|
||||||
|
_crypto = cryptoProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
|
||||||
|
{
|
||||||
|
SerializablePasswordReset spr;
|
||||||
|
HashSet<string> usersreset = new HashSet<string>();
|
||||||
|
foreach (var resetfile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{_passwordResetFileBaseName}*"))
|
||||||
|
{
|
||||||
|
using (var str = File.OpenRead(resetfile))
|
||||||
|
{
|
||||||
|
spr = await _jsonSerializer.DeserializeFromStreamAsync<SerializablePasswordReset>(str).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spr.ExpirationDate < DateTime.Now)
|
||||||
|
{
|
||||||
|
File.Delete(resetfile);
|
||||||
|
}
|
||||||
|
else if (spr.Pin.Replace("-", "").Equals(pin.Replace("-", ""), StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
var resetUser = _userManager.GetUserByName(spr.UserName);
|
||||||
|
if (resetUser == null)
|
||||||
|
{
|
||||||
|
throw new Exception($"User with a username of {spr.UserName} not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false);
|
||||||
|
usersreset.Add(resetUser.Name);
|
||||||
|
File.Delete(resetfile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (usersreset.Count < 1)
|
||||||
|
{
|
||||||
|
throw new ResourceNotFoundException($"No Users found with a password reset request matching pin {pin}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new PinRedeemResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
UsersReset = usersreset.ToArray()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ForgotPasswordResult> StartForgotPasswordProcess(MediaBrowser.Controller.Entities.User user, bool isInNetwork)
|
||||||
|
{
|
||||||
|
string pin = string.Empty;
|
||||||
|
using (var cryptoRandom = System.Security.Cryptography.RandomNumberGenerator.Create())
|
||||||
|
{
|
||||||
|
byte[] bytes = new byte[4];
|
||||||
|
cryptoRandom.GetBytes(bytes);
|
||||||
|
pin = BitConverter.ToString(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTime expireTime = DateTime.Now.AddMinutes(30);
|
||||||
|
string filePath = _passwordResetFileBase + user.InternalId + ".json";
|
||||||
|
SerializablePasswordReset spr = new SerializablePasswordReset
|
||||||
|
{
|
||||||
|
ExpirationDate = expireTime,
|
||||||
|
Pin = pin,
|
||||||
|
PinFile = filePath,
|
||||||
|
UserName = user.Name
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (FileStream fileStream = File.OpenWrite(filePath))
|
||||||
|
{
|
||||||
|
_jsonSerializer.SerializeToStream(spr, fileStream);
|
||||||
|
await fileStream.FlushAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
throw new Exception($"Error serializing or writing password reset for {user.Name} to location: {filePath}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ForgotPasswordResult
|
||||||
|
{
|
||||||
|
Action = ForgotPasswordAction.PinCode,
|
||||||
|
PinExpirationDate = expireTime,
|
||||||
|
PinFile = filePath
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SerializablePasswordReset : PasswordPinCreationResult
|
||||||
|
{
|
||||||
|
public string Pin { get; set; }
|
||||||
|
|
||||||
|
public string UserName { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,6 +14,18 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||||
{
|
{
|
||||||
private readonly IImageProcessor _imageProcessor;
|
private readonly IImageProcessor _imageProcessor;
|
||||||
private readonly ILibraryManager _libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
private static readonly HashSet<string> _ignoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
"folder",
|
||||||
|
"thumb",
|
||||||
|
"landscape",
|
||||||
|
"fanart",
|
||||||
|
"backdrop",
|
||||||
|
"poster",
|
||||||
|
"cover",
|
||||||
|
"logo",
|
||||||
|
"default"
|
||||||
|
};
|
||||||
|
|
||||||
public PhotoResolver(IImageProcessor imageProcessor, ILibraryManager libraryManager)
|
public PhotoResolver(IImageProcessor imageProcessor, ILibraryManager libraryManager)
|
||||||
{
|
{
|
||||||
|
@ -31,10 +43,10 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||||
if (!args.IsDirectory)
|
if (!args.IsDirectory)
|
||||||
{
|
{
|
||||||
// Must be an image file within a photo collection
|
// Must be an image file within a photo collection
|
||||||
var collectionType = args.GetCollectionType();
|
var collectionType = args.CollectionType;
|
||||||
|
|
||||||
if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase) ||
|
if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase)
|
||||||
(string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.GetLibraryOptions().EnablePhotos))
|
|| (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.GetLibraryOptions().EnablePhotos))
|
||||||
{
|
{
|
||||||
if (IsImageFile(args.Path, _imageProcessor))
|
if (IsImageFile(args.Path, _imageProcessor))
|
||||||
{
|
{
|
||||||
|
@ -74,43 +86,29 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static bool IsOwnedByResolvedMedia(ILibraryManager libraryManager, LibraryOptions libraryOptions, string file, string imageFilename)
|
internal static bool IsOwnedByResolvedMedia(ILibraryManager libraryManager, LibraryOptions libraryOptions, string file, string imageFilename)
|
||||||
{
|
=> imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase);
|
||||||
if (imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static readonly HashSet<string> IgnoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
|
||||||
{
|
|
||||||
"folder",
|
|
||||||
"thumb",
|
|
||||||
"landscape",
|
|
||||||
"fanart",
|
|
||||||
"backdrop",
|
|
||||||
"poster",
|
|
||||||
"cover",
|
|
||||||
"logo",
|
|
||||||
"default"
|
|
||||||
};
|
|
||||||
|
|
||||||
internal static bool IsImageFile(string path, IImageProcessor imageProcessor)
|
internal static bool IsImageFile(string path, IImageProcessor imageProcessor)
|
||||||
{
|
{
|
||||||
var filename = Path.GetFileNameWithoutExtension(path) ?? string.Empty;
|
if (path == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(path));
|
||||||
|
}
|
||||||
|
|
||||||
if (IgnoreFiles.Contains(filename))
|
var filename = Path.GetFileNameWithoutExtension(path);
|
||||||
|
|
||||||
|
if (_ignoreFiles.Contains(filename))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IgnoreFiles.Any(i => filename.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1))
|
if (_ignoreFiles.Any(i => filename.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return imageProcessor.SupportedInputFormats.Contains(Path.GetExtension(path).TrimStart('.'), StringComparer.Ordinal);
|
string extension = Path.GetExtension(path).TrimStart('.');
|
||||||
|
return imageProcessor.SupportedInputFormats.Contains(extension, StringComparer.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,6 +79,9 @@ namespace Emby.Server.Implementations.Library
|
||||||
private IAuthenticationProvider[] _authenticationProviders;
|
private IAuthenticationProvider[] _authenticationProviders;
|
||||||
private DefaultAuthenticationProvider _defaultAuthenticationProvider;
|
private DefaultAuthenticationProvider _defaultAuthenticationProvider;
|
||||||
|
|
||||||
|
private IPasswordResetProvider[] _passwordResetProviders;
|
||||||
|
private DefaultPasswordResetProvider _defaultPasswordResetProvider;
|
||||||
|
|
||||||
public UserManager(
|
public UserManager(
|
||||||
ILoggerFactory loggerFactory,
|
ILoggerFactory loggerFactory,
|
||||||
IServerConfigurationManager configurationManager,
|
IServerConfigurationManager configurationManager,
|
||||||
|
@ -102,8 +105,6 @@ namespace Emby.Server.Implementations.Library
|
||||||
_fileSystem = fileSystem;
|
_fileSystem = fileSystem;
|
||||||
ConfigurationManager = configurationManager;
|
ConfigurationManager = configurationManager;
|
||||||
_users = Array.Empty<User>();
|
_users = Array.Empty<User>();
|
||||||
|
|
||||||
DeletePinFile();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public NameIdPair[] GetAuthenticationProviders()
|
public NameIdPair[] GetAuthenticationProviders()
|
||||||
|
@ -120,11 +121,29 @@ namespace Emby.Server.Implementations.Library
|
||||||
.ToArray();
|
.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders)
|
public NameIdPair[] GetPasswordResetProviders()
|
||||||
|
{
|
||||||
|
return _passwordResetProviders
|
||||||
|
.Where(i => i.IsEnabled)
|
||||||
|
.OrderBy(i => i is DefaultPasswordResetProvider ? 0 : 1)
|
||||||
|
.ThenBy(i => i.Name)
|
||||||
|
.Select(i => new NameIdPair
|
||||||
|
{
|
||||||
|
Name = i.Name,
|
||||||
|
Id = GetPasswordResetProviderId(i)
|
||||||
|
})
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders,IEnumerable<IPasswordResetProvider> passwordResetProviders)
|
||||||
{
|
{
|
||||||
_authenticationProviders = authenticationProviders.ToArray();
|
_authenticationProviders = authenticationProviders.ToArray();
|
||||||
|
|
||||||
_defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
|
_defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
|
||||||
|
|
||||||
|
_passwordResetProviders = passwordResetProviders.ToArray();
|
||||||
|
|
||||||
|
_defaultPasswordResetProvider = passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
|
||||||
}
|
}
|
||||||
|
|
||||||
#region UserUpdated Event
|
#region UserUpdated Event
|
||||||
|
@ -258,24 +277,35 @@ namespace Emby.Server.Implementations.Library
|
||||||
.FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase));
|
.FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
var success = false;
|
var success = false;
|
||||||
|
string updatedUsername = null;
|
||||||
IAuthenticationProvider authenticationProvider = null;
|
IAuthenticationProvider authenticationProvider = null;
|
||||||
|
|
||||||
if (user != null)
|
if (user != null)
|
||||||
{
|
{
|
||||||
var authResult = await AuthenticateLocalUser(username, password, hashedPassword, user, remoteEndPoint).ConfigureAwait(false);
|
var authResult = await AuthenticateLocalUser(username, password, hashedPassword, user, remoteEndPoint).ConfigureAwait(false);
|
||||||
authenticationProvider = authResult.Item1;
|
authenticationProvider = authResult.Item1;
|
||||||
success = authResult.Item2;
|
updatedUsername = authResult.Item2;
|
||||||
|
success = authResult.Item3;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// user is null
|
// user is null
|
||||||
var authResult = await AuthenticateLocalUser(username, password, hashedPassword, null, remoteEndPoint).ConfigureAwait(false);
|
var authResult = await AuthenticateLocalUser(username, password, hashedPassword, null, remoteEndPoint).ConfigureAwait(false);
|
||||||
authenticationProvider = authResult.Item1;
|
authenticationProvider = authResult.Item1;
|
||||||
success = authResult.Item2;
|
updatedUsername = authResult.Item2;
|
||||||
|
success = authResult.Item3;
|
||||||
|
|
||||||
if (success && authenticationProvider != null && !(authenticationProvider is DefaultAuthenticationProvider))
|
if (success && authenticationProvider != null && !(authenticationProvider is DefaultAuthenticationProvider))
|
||||||
{
|
{
|
||||||
user = await CreateUser(username).ConfigureAwait(false);
|
// We should trust the user that the authprovider says, not what was typed
|
||||||
|
if (updatedUsername != username)
|
||||||
|
{
|
||||||
|
username = updatedUsername;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search the database for the user again; the authprovider might have created it
|
||||||
|
user = Users
|
||||||
|
.FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
var hasNewUserPolicy = authenticationProvider as IHasNewUserPolicy;
|
var hasNewUserPolicy = authenticationProvider as IHasNewUserPolicy;
|
||||||
if (hasNewUserPolicy != null)
|
if (hasNewUserPolicy != null)
|
||||||
|
@ -342,11 +372,21 @@ namespace Emby.Server.Implementations.Library
|
||||||
return provider.GetType().FullName;
|
return provider.GetType().FullName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string GetPasswordResetProviderId(IPasswordResetProvider provider)
|
||||||
|
{
|
||||||
|
return provider.GetType().FullName;
|
||||||
|
}
|
||||||
|
|
||||||
private IAuthenticationProvider GetAuthenticationProvider(User user)
|
private IAuthenticationProvider GetAuthenticationProvider(User user)
|
||||||
{
|
{
|
||||||
return GetAuthenticationProviders(user).First();
|
return GetAuthenticationProviders(user).First();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IPasswordResetProvider GetPasswordResetProvider(User user)
|
||||||
|
{
|
||||||
|
return GetPasswordResetProviders(user)[0];
|
||||||
|
}
|
||||||
|
|
||||||
private IAuthenticationProvider[] GetAuthenticationProviders(User user)
|
private IAuthenticationProvider[] GetAuthenticationProviders(User user)
|
||||||
{
|
{
|
||||||
var authenticationProviderId = user == null ? null : user.Policy.AuthenticationProviderId;
|
var authenticationProviderId = user == null ? null : user.Policy.AuthenticationProviderId;
|
||||||
|
@ -366,32 +406,59 @@ namespace Emby.Server.Implementations.Library
|
||||||
return providers;
|
return providers;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> AuthenticateWithProvider(IAuthenticationProvider provider, string username, string password, User resolvedUser)
|
private IPasswordResetProvider[] GetPasswordResetProviders(User user)
|
||||||
|
{
|
||||||
|
var passwordResetProviderId = user?.Policy.PasswordResetProviderId;
|
||||||
|
|
||||||
|
var providers = _passwordResetProviders.Where(i => i.IsEnabled).ToArray();
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(passwordResetProviderId))
|
||||||
|
{
|
||||||
|
providers = providers.Where(i => string.Equals(passwordResetProviderId, GetPasswordResetProviderId(i), StringComparison.OrdinalIgnoreCase)).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (providers.Length == 0)
|
||||||
|
{
|
||||||
|
providers = new IPasswordResetProvider[] { _defaultPasswordResetProvider };
|
||||||
|
}
|
||||||
|
|
||||||
|
return providers;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Tuple<string, bool>> AuthenticateWithProvider(IAuthenticationProvider provider, string username, string password, User resolvedUser)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var requiresResolvedUser = provider as IRequiresResolvedUser;
|
var requiresResolvedUser = provider as IRequiresResolvedUser;
|
||||||
|
ProviderAuthenticationResult authenticationResult = null;
|
||||||
if (requiresResolvedUser != null)
|
if (requiresResolvedUser != null)
|
||||||
{
|
{
|
||||||
await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false);
|
authenticationResult = await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await provider.Authenticate(username, password).ConfigureAwait(false);
|
authenticationResult = await provider.Authenticate(username, password).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
if(authenticationResult.Username != username)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Authentication provider provided updated username {1}", authenticationResult.Username);
|
||||||
|
username = authenticationResult.Username;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Tuple<string, bool>(username, true);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error authenticating with provider {provider}", provider.Name);
|
_logger.LogError(ex, "Error authenticating with provider {provider}", provider.Name);
|
||||||
|
|
||||||
return false;
|
return new Tuple<string, bool>(username, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Tuple<IAuthenticationProvider, bool>> AuthenticateLocalUser(string username, string password, string hashedPassword, User user, string remoteEndPoint)
|
private async Task<Tuple<IAuthenticationProvider, string, bool>> AuthenticateLocalUser(string username, string password, string hashedPassword, User user, string remoteEndPoint)
|
||||||
{
|
{
|
||||||
|
string updatedUsername = null;
|
||||||
bool success = false;
|
bool success = false;
|
||||||
IAuthenticationProvider authenticationProvider = null;
|
IAuthenticationProvider authenticationProvider = null;
|
||||||
|
|
||||||
|
@ -410,11 +477,14 @@ namespace Emby.Server.Implementations.Library
|
||||||
{
|
{
|
||||||
foreach (var provider in GetAuthenticationProviders(user))
|
foreach (var provider in GetAuthenticationProviders(user))
|
||||||
{
|
{
|
||||||
success = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false);
|
var providerAuthResult = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false);
|
||||||
|
updatedUsername = providerAuthResult.Item1;
|
||||||
|
success = providerAuthResult.Item2;
|
||||||
|
|
||||||
if (success)
|
if (success)
|
||||||
{
|
{
|
||||||
authenticationProvider = provider;
|
authenticationProvider = provider;
|
||||||
|
username = updatedUsername;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -436,7 +506,7 @@ namespace Emby.Server.Implementations.Library
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Tuple<IAuthenticationProvider, bool>(authenticationProvider, success);
|
return new Tuple<IAuthenticationProvider, string, bool>(authenticationProvider, username, success);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateInvalidLoginAttemptCount(User user, int newValue)
|
private void UpdateInvalidLoginAttemptCount(User user, int newValue)
|
||||||
|
@ -480,7 +550,7 @@ namespace Emby.Server.Implementations.Library
|
||||||
{
|
{
|
||||||
return string.IsNullOrEmpty(user.EasyPassword)
|
return string.IsNullOrEmpty(user.EasyPassword)
|
||||||
? null
|
? null
|
||||||
: user.EasyPassword;
|
: (new PasswordHash(user.EasyPassword)).Hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -526,7 +596,7 @@ namespace Emby.Server.Implementations.Library
|
||||||
}
|
}
|
||||||
|
|
||||||
bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result;
|
bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result;
|
||||||
bool hasConfiguredEasyPassword = string.IsNullOrEmpty(GetLocalPasswordHash(user));
|
bool hasConfiguredEasyPassword = !string.IsNullOrEmpty(GetLocalPasswordHash(user));
|
||||||
|
|
||||||
bool hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ?
|
bool hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ?
|
||||||
hasConfiguredEasyPassword :
|
hasConfiguredEasyPassword :
|
||||||
|
@ -844,159 +914,51 @@ namespace Emby.Server.Implementations.Library
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
DateCreated = DateTime.UtcNow,
|
DateCreated = DateTime.UtcNow,
|
||||||
DateModified = DateTime.UtcNow,
|
DateModified = DateTime.UtcNow,
|
||||||
UsesIdForConfigurationPath = true,
|
UsesIdForConfigurationPath = true
|
||||||
//Salt = BCrypt.GenerateSalt()
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private string PasswordResetFile => Path.Combine(ConfigurationManager.ApplicationPaths.ProgramDataPath, "passwordreset.txt");
|
|
||||||
|
|
||||||
private string _lastPin;
|
|
||||||
private PasswordPinCreationResult _lastPasswordPinCreationResult;
|
|
||||||
private int _pinAttempts;
|
|
||||||
|
|
||||||
private async Task<PasswordPinCreationResult> CreatePasswordResetPin()
|
|
||||||
{
|
|
||||||
var num = new Random().Next(1, 9999);
|
|
||||||
|
|
||||||
var path = PasswordResetFile;
|
|
||||||
|
|
||||||
var pin = num.ToString("0000", CultureInfo.InvariantCulture);
|
|
||||||
_lastPin = pin;
|
|
||||||
|
|
||||||
var time = TimeSpan.FromMinutes(5);
|
|
||||||
var expiration = DateTime.UtcNow.Add(time);
|
|
||||||
|
|
||||||
var text = new StringBuilder();
|
|
||||||
|
|
||||||
var localAddress = (await _appHost.GetLocalApiUrl(CancellationToken.None).ConfigureAwait(false)) ?? string.Empty;
|
|
||||||
|
|
||||||
text.AppendLine("Use your web browser to visit:");
|
|
||||||
text.AppendLine(string.Empty);
|
|
||||||
text.AppendLine(localAddress + "/web/index.html#!/forgotpasswordpin.html");
|
|
||||||
text.AppendLine(string.Empty);
|
|
||||||
text.AppendLine("Enter the following pin code:");
|
|
||||||
text.AppendLine(string.Empty);
|
|
||||||
text.AppendLine(pin);
|
|
||||||
text.AppendLine(string.Empty);
|
|
||||||
|
|
||||||
var localExpirationTime = expiration.ToLocalTime();
|
|
||||||
// Tuesday, 22 August 2006 06:30 AM
|
|
||||||
text.AppendLine("The pin code will expire at " + localExpirationTime.ToString("f1", CultureInfo.CurrentCulture));
|
|
||||||
|
|
||||||
File.WriteAllText(path, text.ToString(), Encoding.UTF8);
|
|
||||||
|
|
||||||
var result = new PasswordPinCreationResult
|
|
||||||
{
|
|
||||||
PinFile = path,
|
|
||||||
ExpirationDate = expiration
|
|
||||||
};
|
|
||||||
|
|
||||||
_lastPasswordPinCreationResult = result;
|
|
||||||
_pinAttempts = 0;
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ForgotPasswordResult> StartForgotPasswordProcess(string enteredUsername, bool isInNetwork)
|
public async Task<ForgotPasswordResult> StartForgotPasswordProcess(string enteredUsername, bool isInNetwork)
|
||||||
{
|
{
|
||||||
DeletePinFile();
|
|
||||||
|
|
||||||
var user = string.IsNullOrWhiteSpace(enteredUsername) ?
|
var user = string.IsNullOrWhiteSpace(enteredUsername) ?
|
||||||
null :
|
null :
|
||||||
GetUserByName(enteredUsername);
|
GetUserByName(enteredUsername);
|
||||||
|
|
||||||
var action = ForgotPasswordAction.InNetworkRequired;
|
var action = ForgotPasswordAction.InNetworkRequired;
|
||||||
string pinFile = null;
|
|
||||||
DateTime? expirationDate = null;
|
|
||||||
|
|
||||||
if (user != null && !user.Policy.IsAdministrator)
|
if (user != null && isInNetwork)
|
||||||
{
|
{
|
||||||
action = ForgotPasswordAction.ContactAdmin;
|
var passwordResetProvider = GetPasswordResetProvider(user);
|
||||||
|
return await passwordResetProvider.StartForgotPasswordProcess(user, isInNetwork).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (isInNetwork)
|
return new ForgotPasswordResult
|
||||||
{
|
{
|
||||||
action = ForgotPasswordAction.PinCode;
|
Action = action,
|
||||||
}
|
PinFile = string.Empty
|
||||||
|
};
|
||||||
var result = await CreatePasswordResetPin().ConfigureAwait(false);
|
|
||||||
pinFile = result.PinFile;
|
|
||||||
expirationDate = result.ExpirationDate;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ForgotPasswordResult
|
|
||||||
{
|
|
||||||
Action = action,
|
|
||||||
PinFile = pinFile,
|
|
||||||
PinExpirationDate = expirationDate
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
|
public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
|
||||||
{
|
{
|
||||||
DeletePinFile();
|
foreach (var provider in _passwordResetProviders)
|
||||||
|
|
||||||
var usersReset = new List<string>();
|
|
||||||
|
|
||||||
var valid = !string.IsNullOrWhiteSpace(_lastPin) &&
|
|
||||||
string.Equals(_lastPin, pin, StringComparison.OrdinalIgnoreCase) &&
|
|
||||||
_lastPasswordPinCreationResult != null &&
|
|
||||||
_lastPasswordPinCreationResult.ExpirationDate > DateTime.UtcNow;
|
|
||||||
|
|
||||||
if (valid)
|
|
||||||
{
|
{
|
||||||
_lastPin = null;
|
var result = await provider.RedeemPasswordResetPin(pin).ConfigureAwait(false);
|
||||||
_lastPasswordPinCreationResult = null;
|
if (result.Success)
|
||||||
|
|
||||||
foreach (var user in Users)
|
|
||||||
{
|
{
|
||||||
await ResetPassword(user).ConfigureAwait(false);
|
return result;
|
||||||
|
|
||||||
if (user.Policy.IsDisabled)
|
|
||||||
{
|
|
||||||
user.Policy.IsDisabled = false;
|
|
||||||
UpdateUserPolicy(user, user.Policy, true);
|
|
||||||
}
|
|
||||||
usersReset.Add(user.Name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_pinAttempts++;
|
|
||||||
if (_pinAttempts >= 3)
|
|
||||||
{
|
|
||||||
_lastPin = null;
|
|
||||||
_lastPasswordPinCreationResult = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new PinRedeemResult
|
return new PinRedeemResult
|
||||||
{
|
{
|
||||||
Success = valid,
|
Success = false,
|
||||||
UsersReset = usersReset.ToArray()
|
UsersReset = Array.Empty<string>()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DeletePinFile()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_fileSystem.DeleteFile(PasswordResetFile);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class PasswordPinCreationResult
|
|
||||||
{
|
|
||||||
public string PinFile { get; set; }
|
|
||||||
public DateTime ExpirationDate { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public UserPolicy GetUserPolicy(User user)
|
public UserPolicy GetUserPolicy(User user)
|
||||||
{
|
{
|
||||||
var path = GetPolicyFilePath(user);
|
var path = GetPolicyFilePath(user);
|
||||||
|
|
|
@ -261,7 +261,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||||
|
|
||||||
public string HomePageUrl => "https://github.com/jellyfin/jellyfin";
|
public string HomePageUrl => "https://github.com/jellyfin/jellyfin";
|
||||||
|
|
||||||
public async Task RefreshSeriesTimers(CancellationToken cancellationToken, IProgress<double> progress)
|
public async Task RefreshSeriesTimers(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var seriesTimers = await GetSeriesTimersAsync(cancellationToken).ConfigureAwait(false);
|
var seriesTimers = await GetSeriesTimersAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
@ -271,7 +271,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RefreshTimers(CancellationToken cancellationToken, IProgress<double> progress)
|
public async Task RefreshTimers(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var timers = await GetTimersAsync(cancellationToken).ConfigureAwait(false);
|
var timers = await GetTimersAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
|
|
@ -1087,8 +1087,8 @@ namespace Emby.Server.Implementations.LiveTv
|
||||||
|
|
||||||
if (coreService != null)
|
if (coreService != null)
|
||||||
{
|
{
|
||||||
await coreService.RefreshSeriesTimers(cancellationToken, new SimpleProgress<double>()).ConfigureAwait(false);
|
await coreService.RefreshSeriesTimers(cancellationToken).ConfigureAwait(false);
|
||||||
await coreService.RefreshTimers(cancellationToken, new SimpleProgress<double>()).ConfigureAwait(false);
|
await coreService.RefreshTimers(cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load these now which will prefetch metadata
|
// Load these now which will prefetch metadata
|
||||||
|
|
|
@ -10,14 +10,12 @@ using MediaBrowser.Controller;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.LiveTv;
|
using MediaBrowser.Controller.LiveTv;
|
||||||
using MediaBrowser.Controller.MediaEncoding;
|
|
||||||
using MediaBrowser.Model.Dto;
|
using MediaBrowser.Model.Dto;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Model.LiveTv;
|
using MediaBrowser.Model.LiveTv;
|
||||||
using MediaBrowser.Model.MediaInfo;
|
using MediaBrowser.Model.MediaInfo;
|
||||||
using MediaBrowser.Model.Serialization;
|
using MediaBrowser.Model.Serialization;
|
||||||
using MediaBrowser.Model.System;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Net.Http.Headers;
|
using Microsoft.Net.Http.Headers;
|
||||||
|
|
||||||
|
@ -52,9 +50,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||||
{
|
{
|
||||||
var channelIdPrefix = GetFullChannelIdPrefix(info);
|
var channelIdPrefix = GetFullChannelIdPrefix(info);
|
||||||
|
|
||||||
var result = await new M3uParser(Logger, _httpClient, _appHost).Parse(info.Url, channelIdPrefix, info.Id, cancellationToken).ConfigureAwait(false);
|
return await new M3uParser(Logger, _httpClient, _appHost).Parse(info.Url, channelIdPrefix, info.Id, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
return result.Cast<ChannelInfo>().ToList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken)
|
public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken)
|
||||||
|
@ -73,7 +69,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||||
return Task.FromResult(list);
|
return Task.FromResult(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string[] _disallowedSharedStreamExtensions = new string[]
|
private static readonly string[] _disallowedSharedStreamExtensions = new string[]
|
||||||
{
|
{
|
||||||
".mkv",
|
".mkv",
|
||||||
".mp4",
|
".mp4",
|
||||||
|
@ -88,9 +84,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||||
if (tunerCount > 0)
|
if (tunerCount > 0)
|
||||||
{
|
{
|
||||||
var tunerHostId = info.Id;
|
var tunerHostId = info.Id;
|
||||||
var liveStreams = currentLiveStreams.Where(i => string.Equals(i.TunerHostId, tunerHostId, StringComparison.OrdinalIgnoreCase)).ToList();
|
var liveStreams = currentLiveStreams.Where(i => string.Equals(i.TunerHostId, tunerHostId, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
if (liveStreams.Count >= tunerCount)
|
if (liveStreams.Count() >= tunerCount)
|
||||||
{
|
{
|
||||||
throw new LiveTvConflictException("M3U simultaneous stream limit has been reached.");
|
throw new LiveTvConflictException("M3U simultaneous stream limit has been reached.");
|
||||||
}
|
}
|
||||||
|
@ -98,7 +94,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||||
|
|
||||||
var sources = await GetChannelStreamMediaSources(info, channelInfo, cancellationToken).ConfigureAwait(false);
|
var sources = await GetChannelStreamMediaSources(info, channelInfo, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var mediaSource = sources.First();
|
var mediaSource = sources[0];
|
||||||
|
|
||||||
if (mediaSource.Protocol == MediaProtocol.Http && !mediaSource.RequiresLooping)
|
if (mediaSource.Protocol == MediaProtocol.Http && !mediaSource.RequiresLooping)
|
||||||
{
|
{
|
||||||
|
|
|
@ -11,7 +11,6 @@ using MediaBrowser.Common.Net;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
using MediaBrowser.Controller.LiveTv;
|
using MediaBrowser.Controller.LiveTv;
|
||||||
using MediaBrowser.Model.Extensions;
|
using MediaBrowser.Model.Extensions;
|
||||||
using MediaBrowser.Model.IO;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||||
|
@ -62,12 +61,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||||
return Task.FromResult((Stream)File.OpenRead(url));
|
return Task.FromResult((Stream)File.OpenRead(url));
|
||||||
}
|
}
|
||||||
|
|
||||||
const string ExtInfPrefix = "#EXTINF:";
|
private const string ExtInfPrefix = "#EXTINF:";
|
||||||
|
|
||||||
private List<ChannelInfo> GetChannels(TextReader reader, string channelIdPrefix, string tunerHostId)
|
private List<ChannelInfo> GetChannels(TextReader reader, string channelIdPrefix, string tunerHostId)
|
||||||
{
|
{
|
||||||
var channels = new List<ChannelInfo>();
|
var channels = new List<ChannelInfo>();
|
||||||
string line;
|
string line;
|
||||||
string extInf = "";
|
string extInf = string.Empty;
|
||||||
|
|
||||||
while ((line = reader.ReadLine()) != null)
|
while ((line = reader.ReadLine()) != null)
|
||||||
{
|
{
|
||||||
|
@ -101,7 +101,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||||
|
|
||||||
channel.Path = line;
|
channel.Path = line;
|
||||||
channels.Add(channel);
|
channels.Add(channel);
|
||||||
extInf = "";
|
extInf = string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,8 +110,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||||
|
|
||||||
private ChannelInfo GetChannelnfo(string extInf, string tunerHostId, string mediaUrl)
|
private ChannelInfo GetChannelnfo(string extInf, string tunerHostId, string mediaUrl)
|
||||||
{
|
{
|
||||||
var channel = new ChannelInfo();
|
var channel = new ChannelInfo()
|
||||||
channel.TunerHostId = tunerHostId;
|
{
|
||||||
|
TunerHostId = tunerHostId
|
||||||
|
};
|
||||||
|
|
||||||
extInf = extInf.Trim();
|
extInf = extInf.Trim();
|
||||||
|
|
||||||
|
@ -137,13 +139,15 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||||
{
|
{
|
||||||
channelIdValues.Add(channelId);
|
channelIdValues.Add(channelId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(tvgId))
|
if (!string.IsNullOrWhiteSpace(tvgId))
|
||||||
{
|
{
|
||||||
channelIdValues.Add(tvgId);
|
channelIdValues.Add(tvgId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (channelIdValues.Count > 0)
|
if (channelIdValues.Count > 0)
|
||||||
{
|
{
|
||||||
channel.Id = string.Join("_", channelIdValues.ToArray());
|
channel.Id = string.Join("_", channelIdValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
return channel;
|
return channel;
|
||||||
|
@ -152,7 +156,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||||
private string GetChannelNumber(string extInf, Dictionary<string, string> attributes, string mediaUrl)
|
private string GetChannelNumber(string extInf, Dictionary<string, string> attributes, string mediaUrl)
|
||||||
{
|
{
|
||||||
var nameParts = extInf.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
var nameParts = extInf.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
var nameInExtInf = nameParts.Length > 1 ? nameParts.Last().Trim() : null;
|
var nameInExtInf = nameParts.Length > 1 ? nameParts[nameParts.Length - 1].Trim() : null;
|
||||||
|
|
||||||
string numberString = null;
|
string numberString = null;
|
||||||
string attributeValue;
|
string attributeValue;
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
"Artists": "Umělci",
|
"Artists": "Umělci",
|
||||||
"AuthenticationSucceededWithUserName": "{0} úspěšně ověřen",
|
"AuthenticationSucceededWithUserName": "{0} úspěšně ověřen",
|
||||||
"Books": "Knihy",
|
"Books": "Knihy",
|
||||||
"CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
|
"CameraImageUploadedFrom": "Z {0} byla nahrána nová fotografie",
|
||||||
"Channels": "Kanály",
|
"Channels": "Kanály",
|
||||||
"ChapterNameValue": "Kapitola {0}",
|
"ChapterNameValue": "Kapitola {0}",
|
||||||
"Collections": "Kolekce",
|
"Collections": "Kolekce",
|
||||||
|
@ -16,14 +16,14 @@
|
||||||
"Folders": "Složky",
|
"Folders": "Složky",
|
||||||
"Genres": "Žánry",
|
"Genres": "Žánry",
|
||||||
"HeaderAlbumArtists": "Umělci alba",
|
"HeaderAlbumArtists": "Umělci alba",
|
||||||
"HeaderCameraUploads": "Camera Uploads",
|
"HeaderCameraUploads": "Nahrané fotografie",
|
||||||
"HeaderContinueWatching": "Pokračovat ve sledování",
|
"HeaderContinueWatching": "Pokračovat ve sledování",
|
||||||
"HeaderFavoriteAlbums": "Oblíbená alba",
|
"HeaderFavoriteAlbums": "Oblíbená alba",
|
||||||
"HeaderFavoriteArtists": "Oblíbení umělci",
|
"HeaderFavoriteArtists": "Oblíbení interpreti",
|
||||||
"HeaderFavoriteEpisodes": "Oblíbené epizody",
|
"HeaderFavoriteEpisodes": "Oblíbené epizody",
|
||||||
"HeaderFavoriteShows": "Oblíbené seriály",
|
"HeaderFavoriteShows": "Oblíbené seriály",
|
||||||
"HeaderFavoriteSongs": "Oblíbené písně",
|
"HeaderFavoriteSongs": "Oblíbená hudba",
|
||||||
"HeaderLiveTV": "Živá TV",
|
"HeaderLiveTV": "Live TV",
|
||||||
"HeaderNextUp": "Nadcházející",
|
"HeaderNextUp": "Nadcházející",
|
||||||
"HeaderRecordingGroups": "Skupiny nahrávek",
|
"HeaderRecordingGroups": "Skupiny nahrávek",
|
||||||
"HomeVideos": "Domáci videa",
|
"HomeVideos": "Domáci videa",
|
||||||
|
@ -34,17 +34,17 @@
|
||||||
"LabelRunningTimeValue": "Délka média: {0}",
|
"LabelRunningTimeValue": "Délka média: {0}",
|
||||||
"Latest": "Nejnovější",
|
"Latest": "Nejnovější",
|
||||||
"MessageApplicationUpdated": "Jellyfin Server byl aktualizován",
|
"MessageApplicationUpdated": "Jellyfin Server byl aktualizován",
|
||||||
"MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}",
|
"MessageApplicationUpdatedTo": "Jellyfin server byl aktualizován na verzi {0}",
|
||||||
"MessageNamedServerConfigurationUpdatedWithValue": "Konfigurace sekce {0} na serveru byla aktualizována",
|
"MessageNamedServerConfigurationUpdatedWithValue": "Konfigurace sekce {0} na serveru byla aktualizována",
|
||||||
"MessageServerConfigurationUpdated": "Konfigurace serveru aktualizována",
|
"MessageServerConfigurationUpdated": "Konfigurace serveru aktualizována",
|
||||||
"MixedContent": "Smíšený obsah",
|
"MixedContent": "Smíšený obsah",
|
||||||
"Movies": "Filmy",
|
"Movies": "Filmy",
|
||||||
"Music": "Hudba",
|
"Music": "Hudba",
|
||||||
"MusicVideos": "Hudební klipy",
|
"MusicVideos": "Hudební klipy",
|
||||||
"NameInstallFailed": "{0} installation failed",
|
"NameInstallFailed": "Instalace {0} selhala",
|
||||||
"NameSeasonNumber": "Sezóna {0}",
|
"NameSeasonNumber": "Sezóna {0}",
|
||||||
"NameSeasonUnknown": "Neznámá sezóna",
|
"NameSeasonUnknown": "Neznámá sezóna",
|
||||||
"NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.",
|
"NewVersionIsAvailable": "Nová verze Jellyfin serveru je k dispozici ke stažení.",
|
||||||
"NotificationOptionApplicationUpdateAvailable": "Dostupná aktualizace aplikace",
|
"NotificationOptionApplicationUpdateAvailable": "Dostupná aktualizace aplikace",
|
||||||
"NotificationOptionApplicationUpdateInstalled": "Aktualizace aplikace instalována",
|
"NotificationOptionApplicationUpdateInstalled": "Aktualizace aplikace instalována",
|
||||||
"NotificationOptionAudioPlayback": "Přehrávání audia zahájeno",
|
"NotificationOptionAudioPlayback": "Přehrávání audia zahájeno",
|
||||||
|
@ -70,12 +70,12 @@
|
||||||
"ProviderValue": "Poskytl: {0}",
|
"ProviderValue": "Poskytl: {0}",
|
||||||
"ScheduledTaskFailedWithName": "{0} selhalo",
|
"ScheduledTaskFailedWithName": "{0} selhalo",
|
||||||
"ScheduledTaskStartedWithName": "{0} zahájeno",
|
"ScheduledTaskStartedWithName": "{0} zahájeno",
|
||||||
"ServerNameNeedsToBeRestarted": "{0} needs to be restarted",
|
"ServerNameNeedsToBeRestarted": "{0} vyžaduje restart",
|
||||||
"Shows": "Seriály",
|
"Shows": "Seriály",
|
||||||
"Songs": "Skladby",
|
"Songs": "Skladby",
|
||||||
"StartupEmbyServerIsLoading": "Jellyfin Server je spouštěn. Zkuste to prosím v brzké době znovu.",
|
"StartupEmbyServerIsLoading": "Jellyfin Server je spouštěn. Zkuste to prosím v brzké době znovu.",
|
||||||
"SubtitleDownloadFailureForItem": "Stahování titulků selhalo pro {0}",
|
"SubtitleDownloadFailureForItem": "Stahování titulků selhalo pro {0}",
|
||||||
"SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
|
"SubtitleDownloadFailureFromForItem": "Stažení titulků pro {1} z {0} selhalo",
|
||||||
"SubtitlesDownloadedForItem": "Staženy titulky pro {0}",
|
"SubtitlesDownloadedForItem": "Staženy titulky pro {0}",
|
||||||
"Sync": "Synchronizace",
|
"Sync": "Synchronizace",
|
||||||
"System": "Systém",
|
"System": "Systém",
|
||||||
|
@ -88,10 +88,10 @@
|
||||||
"UserOfflineFromDevice": "{0} se odpojil od {1}",
|
"UserOfflineFromDevice": "{0} se odpojil od {1}",
|
||||||
"UserOnlineFromDevice": "{0} se připojil z {1}",
|
"UserOnlineFromDevice": "{0} se připojil z {1}",
|
||||||
"UserPasswordChangedWithName": "Provedena změna hesla pro uživatele {0}",
|
"UserPasswordChangedWithName": "Provedena změna hesla pro uživatele {0}",
|
||||||
"UserPolicyUpdatedWithName": "User policy has been updated for {0}",
|
"UserPolicyUpdatedWithName": "Zásady uživatele pro {0} byly aktualizovány",
|
||||||
"UserStartedPlayingItemWithValues": "{0} spustil přehrávání {1}",
|
"UserStartedPlayingItemWithValues": "{0} spustil přehrávání {1}",
|
||||||
"UserStoppedPlayingItemWithValues": "{0} zastavil přehrávání {1}",
|
"UserStoppedPlayingItemWithValues": "{0} zastavil přehrávání {1}",
|
||||||
"ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
|
"ValueHasBeenAddedToLibrary": "{0} byl přidán do vaší knihovny médií",
|
||||||
"ValueSpecialEpisodeName": "Speciál - {0}",
|
"ValueSpecialEpisodeName": "Speciál - {0}",
|
||||||
"VersionNumber": "Verze {0}"
|
"VersionNumber": "Verze {0}"
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,8 +61,8 @@
|
||||||
"NotificationOptionUserLockedOut": "Bruger låst ude",
|
"NotificationOptionUserLockedOut": "Bruger låst ude",
|
||||||
"NotificationOptionVideoPlayback": "Videoafspilning påbegyndt",
|
"NotificationOptionVideoPlayback": "Videoafspilning påbegyndt",
|
||||||
"NotificationOptionVideoPlaybackStopped": "Videoafspilning stoppet",
|
"NotificationOptionVideoPlaybackStopped": "Videoafspilning stoppet",
|
||||||
"Photos": "Fotos",
|
"Photos": "Fotoer",
|
||||||
"Playlists": "Spillelister",
|
"Playlists": "Afspilningslister",
|
||||||
"Plugin": "Plugin",
|
"Plugin": "Plugin",
|
||||||
"PluginInstalledWithName": "{0} blev installeret",
|
"PluginInstalledWithName": "{0} blev installeret",
|
||||||
"PluginUninstalledWithName": "{0} blev afinstalleret",
|
"PluginUninstalledWithName": "{0} blev afinstalleret",
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
"Folders": "Φάκελοι",
|
"Folders": "Φάκελοι",
|
||||||
"Genres": "Είδη",
|
"Genres": "Είδη",
|
||||||
"HeaderAlbumArtists": "Άλμπουμ Καλλιτεχνών",
|
"HeaderAlbumArtists": "Άλμπουμ Καλλιτεχνών",
|
||||||
"HeaderCameraUploads": "Camera Uploads",
|
"HeaderCameraUploads": "Μεταφορτώσεις Κάμερας",
|
||||||
"HeaderContinueWatching": "Συνεχίστε να παρακολουθείτε",
|
"HeaderContinueWatching": "Συνεχίστε να παρακολουθείτε",
|
||||||
"HeaderFavoriteAlbums": "Αγαπημένα Άλμπουμ",
|
"HeaderFavoriteAlbums": "Αγαπημένα Άλμπουμ",
|
||||||
"HeaderFavoriteArtists": "Αγαπημένοι Καλλιτέχνες",
|
"HeaderFavoriteArtists": "Αγαπημένοι Καλλιτέχνες",
|
||||||
|
@ -34,7 +34,7 @@
|
||||||
"LabelRunningTimeValue": "Διάρκεια: {0}",
|
"LabelRunningTimeValue": "Διάρκεια: {0}",
|
||||||
"Latest": "Πρόσφατα",
|
"Latest": "Πρόσφατα",
|
||||||
"MessageApplicationUpdated": "Ο Jellyfin Server έχει ενημερωθεί",
|
"MessageApplicationUpdated": "Ο Jellyfin Server έχει ενημερωθεί",
|
||||||
"MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}",
|
"MessageApplicationUpdatedTo": "Ο server Jellyfin αναβαθμίστηκε σε έκδοση {0}",
|
||||||
"MessageNamedServerConfigurationUpdatedWithValue": "Η ενότητα {0} ρύθμισης παραμέτρων του server έχει ενημερωθεί",
|
"MessageNamedServerConfigurationUpdatedWithValue": "Η ενότητα {0} ρύθμισης παραμέτρων του server έχει ενημερωθεί",
|
||||||
"MessageServerConfigurationUpdated": "Η ρύθμιση παραμέτρων του server έχει ενημερωθεί",
|
"MessageServerConfigurationUpdated": "Η ρύθμιση παραμέτρων του server έχει ενημερωθεί",
|
||||||
"MixedContent": "Ανάμεικτο Περιεχόμενο",
|
"MixedContent": "Ανάμεικτο Περιεχόμενο",
|
||||||
|
@ -49,7 +49,7 @@
|
||||||
"NotificationOptionApplicationUpdateInstalled": "Η ενημέρωση εφαρμογής εγκαταστάθηκε",
|
"NotificationOptionApplicationUpdateInstalled": "Η ενημέρωση εφαρμογής εγκαταστάθηκε",
|
||||||
"NotificationOptionAudioPlayback": "Η αναπαραγωγή ήχου ξεκίνησε",
|
"NotificationOptionAudioPlayback": "Η αναπαραγωγή ήχου ξεκίνησε",
|
||||||
"NotificationOptionAudioPlaybackStopped": "Η αναπαραγωγή ήχου σταμάτησε",
|
"NotificationOptionAudioPlaybackStopped": "Η αναπαραγωγή ήχου σταμάτησε",
|
||||||
"NotificationOptionCameraImageUploaded": "Camera image uploaded",
|
"NotificationOptionCameraImageUploaded": "Μεταφορτώθηκε φωτογραφία απο κάμερα",
|
||||||
"NotificationOptionInstallationFailed": "Αποτυχία εγκατάστασης",
|
"NotificationOptionInstallationFailed": "Αποτυχία εγκατάστασης",
|
||||||
"NotificationOptionNewLibraryContent": "Προστέθηκε νέο περιεχόμενο",
|
"NotificationOptionNewLibraryContent": "Προστέθηκε νέο περιεχόμενο",
|
||||||
"NotificationOptionPluginError": "Αποτυχία του plugin",
|
"NotificationOptionPluginError": "Αποτυχία του plugin",
|
||||||
|
@ -75,7 +75,7 @@
|
||||||
"Songs": "Τραγούδια",
|
"Songs": "Τραγούδια",
|
||||||
"StartupEmbyServerIsLoading": "Ο Jellyfin Server φορτώνει. Παρακαλώ δοκιμάστε σε λίγο.",
|
"StartupEmbyServerIsLoading": "Ο Jellyfin Server φορτώνει. Παρακαλώ δοκιμάστε σε λίγο.",
|
||||||
"SubtitleDownloadFailureForItem": "Οι υπότιτλοι απέτυχαν να κατέβουν για {0}",
|
"SubtitleDownloadFailureForItem": "Οι υπότιτλοι απέτυχαν να κατέβουν για {0}",
|
||||||
"SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
|
"SubtitleDownloadFailureFromForItem": "Αποτυχίες μεταφόρτωσης υποτίτλων από {0} για {1}",
|
||||||
"SubtitlesDownloadedForItem": "Οι υπότιτλοι κατέβηκαν για {0}",
|
"SubtitlesDownloadedForItem": "Οι υπότιτλοι κατέβηκαν για {0}",
|
||||||
"Sync": "Συγχρονισμός",
|
"Sync": "Συγχρονισμός",
|
||||||
"System": "Σύστημα",
|
"System": "Σύστημα",
|
||||||
|
|
|
@ -1,97 +1,97 @@
|
||||||
{
|
{
|
||||||
"Albums": "Albums",
|
"Albums": "Albom",
|
||||||
"AppDeviceValues": "App: {0}, Device: {1}",
|
"AppDeviceValues": "App: {0}, Grät: {1}",
|
||||||
"Application": "Application",
|
"Application": "Aawändig",
|
||||||
"Artists": "Artists",
|
"Artists": "Könstler",
|
||||||
"AuthenticationSucceededWithUserName": "{0} successfully authenticated",
|
"AuthenticationSucceededWithUserName": "{0} het sech aagmäudet",
|
||||||
"Books": "Büecher",
|
"Books": "Büecher",
|
||||||
"CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
|
"CameraImageUploadedFrom": "Es nöis Foti esch ufeglade worde vo {0}",
|
||||||
"Channels": "Channels",
|
"Channels": "Kanäu",
|
||||||
"ChapterNameValue": "Chapter {0}",
|
"ChapterNameValue": "Kapitu {0}",
|
||||||
"Collections": "Collections",
|
"Collections": "Sammlige",
|
||||||
"DeviceOfflineWithName": "{0} has disconnected",
|
"DeviceOfflineWithName": "{0} esch offline gange",
|
||||||
"DeviceOnlineWithName": "{0} is connected",
|
"DeviceOnlineWithName": "{0} esch online cho",
|
||||||
"FailedLoginAttemptWithUserName": "Failed login attempt from {0}",
|
"FailedLoginAttemptWithUserName": "Fäugschlagne Aamäudeversuech vo {0}",
|
||||||
"Favorites": "Favorites",
|
"Favorites": "Favorite",
|
||||||
"Folders": "Folders",
|
"Folders": "Ordner",
|
||||||
"Genres": "Genres",
|
"Genres": "Genres",
|
||||||
"HeaderAlbumArtists": "Albuminterprete",
|
"HeaderAlbumArtists": "Albom-Könstler",
|
||||||
"HeaderCameraUploads": "Camera Uploads",
|
"HeaderCameraUploads": "Kamera-Uploads",
|
||||||
"HeaderContinueWatching": "Wiiterluege",
|
"HeaderContinueWatching": "Wiiterluege",
|
||||||
"HeaderFavoriteAlbums": "Favorite Albums",
|
"HeaderFavoriteAlbums": "Lieblingsalbe",
|
||||||
"HeaderFavoriteArtists": "Besti Interpret",
|
"HeaderFavoriteArtists": "Lieblings-Interprete",
|
||||||
"HeaderFavoriteEpisodes": "Favorite Episodes",
|
"HeaderFavoriteEpisodes": "Lieblingsepisode",
|
||||||
"HeaderFavoriteShows": "Favorite Shows",
|
"HeaderFavoriteShows": "Lieblingsserie",
|
||||||
"HeaderFavoriteSongs": "Besti Lieder",
|
"HeaderFavoriteSongs": "Lieblingslieder",
|
||||||
"HeaderLiveTV": "Live TV",
|
"HeaderLiveTV": "Live-Färnseh",
|
||||||
"HeaderNextUp": "Next Up",
|
"HeaderNextUp": "Als nächts",
|
||||||
"HeaderRecordingGroups": "Ufnahmegruppe",
|
"HeaderRecordingGroups": "Ufnahmegruppe",
|
||||||
"HomeVideos": "Heimfilmli",
|
"HomeVideos": "Heimfilmli",
|
||||||
"Inherit": "Hinzuefüege",
|
"Inherit": "Hinzuefüege",
|
||||||
"ItemAddedWithName": "{0} was added to the library",
|
"ItemAddedWithName": "{0} esch de Bibliothek dezuegfüegt worde",
|
||||||
"ItemRemovedWithName": "{0} was removed from the library",
|
"ItemRemovedWithName": "{0} esch vo de Bibliothek entfärnt worde",
|
||||||
"LabelIpAddressValue": "Ip address: {0}",
|
"LabelIpAddressValue": "IP-Adrässe: {0}",
|
||||||
"LabelRunningTimeValue": "Running time: {0}",
|
"LabelRunningTimeValue": "Loufziit: {0}",
|
||||||
"Latest": "Letschte",
|
"Latest": "Nöischti",
|
||||||
"MessageApplicationUpdated": "Jellyfin Server has been updated",
|
"MessageApplicationUpdated": "Jellyfin Server esch aktualisiert worde",
|
||||||
"MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}",
|
"MessageApplicationUpdatedTo": "Jellyfin Server esch of Version {0} aktualisiert worde",
|
||||||
"MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated",
|
"MessageNamedServerConfigurationUpdatedWithValue": "De Serveriistöuigsberiich {0} esch aktualisiert worde",
|
||||||
"MessageServerConfigurationUpdated": "Server configuration has been updated",
|
"MessageServerConfigurationUpdated": "Serveriistöuige send aktualisiert worde",
|
||||||
"MixedContent": "Gmischte Inhalt",
|
"MixedContent": "Gmeschti Inhäut",
|
||||||
"Movies": "Movies",
|
"Movies": "Film",
|
||||||
"Music": "Musig",
|
"Music": "Musig",
|
||||||
"MusicVideos": "Musigfilm",
|
"MusicVideos": "Musigvideos",
|
||||||
"NameInstallFailed": "{0} installation failed",
|
"NameInstallFailed": "Installation vo {0} fäugschlage",
|
||||||
"NameSeasonNumber": "Season {0}",
|
"NameSeasonNumber": "Staffle {0}",
|
||||||
"NameSeasonUnknown": "Season Unknown",
|
"NameSeasonUnknown": "Staffle unbekannt",
|
||||||
"NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.",
|
"NewVersionIsAvailable": "E nöi Version vo Jellyfin Server esch zom Download parat.",
|
||||||
"NotificationOptionApplicationUpdateAvailable": "Application update available",
|
"NotificationOptionApplicationUpdateAvailable": "Aawändigsupdate verfüegbar",
|
||||||
"NotificationOptionApplicationUpdateInstalled": "Application update installed",
|
"NotificationOptionApplicationUpdateInstalled": "Aawändigsupdate installiert",
|
||||||
"NotificationOptionAudioPlayback": "Audio playback started",
|
"NotificationOptionAudioPlayback": "Audiowedergab gstartet",
|
||||||
"NotificationOptionAudioPlaybackStopped": "Audio playback stopped",
|
"NotificationOptionAudioPlaybackStopped": "Audiwedergab gstoppt",
|
||||||
"NotificationOptionCameraImageUploaded": "Camera image uploaded",
|
"NotificationOptionCameraImageUploaded": "Foti ueglade",
|
||||||
"NotificationOptionInstallationFailed": "Installation failure",
|
"NotificationOptionInstallationFailed": "Installationsfäuer",
|
||||||
"NotificationOptionNewLibraryContent": "New content added",
|
"NotificationOptionNewLibraryContent": "Nöie Inhaut hinzuegfüegt",
|
||||||
"NotificationOptionPluginError": "Plugin failure",
|
"NotificationOptionPluginError": "Plugin-Fäuer",
|
||||||
"NotificationOptionPluginInstalled": "Plugin installed",
|
"NotificationOptionPluginInstalled": "Plugin installiert",
|
||||||
"NotificationOptionPluginUninstalled": "Plugin uninstalled",
|
"NotificationOptionPluginUninstalled": "Plugin deinstalliert",
|
||||||
"NotificationOptionPluginUpdateInstalled": "Plugin update installed",
|
"NotificationOptionPluginUpdateInstalled": "Pluginupdate installiert",
|
||||||
"NotificationOptionServerRestartRequired": "Server restart required",
|
"NotificationOptionServerRestartRequired": "Serverneustart notwändig",
|
||||||
"NotificationOptionTaskFailed": "Scheduled task failure",
|
"NotificationOptionTaskFailed": "Planti Uufgab fäugschlage",
|
||||||
"NotificationOptionUserLockedOut": "User locked out",
|
"NotificationOptionUserLockedOut": "Benotzer usgschlosse",
|
||||||
"NotificationOptionVideoPlayback": "Video playback started",
|
"NotificationOptionVideoPlayback": "Videowedergab gstartet",
|
||||||
"NotificationOptionVideoPlaybackStopped": "Video playback stopped",
|
"NotificationOptionVideoPlaybackStopped": "Videowedergab gstoppt",
|
||||||
"Photos": "Fotis",
|
"Photos": "Fotis",
|
||||||
"Playlists": "Abspielliste",
|
"Playlists": "Wedergabeliste",
|
||||||
"Plugin": "Plugin",
|
"Plugin": "Plugin",
|
||||||
"PluginInstalledWithName": "{0} was installed",
|
"PluginInstalledWithName": "{0} esch installiert worde",
|
||||||
"PluginUninstalledWithName": "{0} was uninstalled",
|
"PluginUninstalledWithName": "{0} esch deinstalliert worde",
|
||||||
"PluginUpdatedWithName": "{0} was updated",
|
"PluginUpdatedWithName": "{0} esch updated worde",
|
||||||
"ProviderValue": "Provider: {0}",
|
"ProviderValue": "Aabieter: {0}",
|
||||||
"ScheduledTaskFailedWithName": "{0} failed",
|
"ScheduledTaskFailedWithName": "{0} esch fäugschlage",
|
||||||
"ScheduledTaskStartedWithName": "{0} started",
|
"ScheduledTaskStartedWithName": "{0} het gstartet",
|
||||||
"ServerNameNeedsToBeRestarted": "{0} needs to be restarted",
|
"ServerNameNeedsToBeRestarted": "{0} mues nöi gstartet wärde",
|
||||||
"Shows": "Shows",
|
"Shows": "Serie",
|
||||||
"Songs": "Songs",
|
"Songs": "Lieder",
|
||||||
"StartupEmbyServerIsLoading": "Jellyfin Server is loading. Please try again shortly.",
|
"StartupEmbyServerIsLoading": "Jellyfin Server ladt. Bitte grad noeinisch probiere.",
|
||||||
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
||||||
"SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
|
"SubtitleDownloadFailureFromForItem": "Ondertetle vo {0} för {1} hend ned chönne abeglade wärde",
|
||||||
"SubtitlesDownloadedForItem": "Subtitles downloaded for {0}",
|
"SubtitlesDownloadedForItem": "Ondertetle abeglade för {0}",
|
||||||
"Sync": "Sync",
|
"Sync": "Synchronisation",
|
||||||
"System": "System",
|
"System": "System",
|
||||||
"TvShows": "TV Shows",
|
"TvShows": "Färnsehserie",
|
||||||
"User": "User",
|
"User": "Benotzer",
|
||||||
"UserCreatedWithName": "User {0} has been created",
|
"UserCreatedWithName": "Benotzer {0} esch erstöut worde",
|
||||||
"UserDeletedWithName": "User {0} has been deleted",
|
"UserDeletedWithName": "Benotzer {0} esch glösche worde",
|
||||||
"UserDownloadingItemWithValues": "{0} is downloading {1}",
|
"UserDownloadingItemWithValues": "{0} ladt {1} abe",
|
||||||
"UserLockedOutWithName": "User {0} has been locked out",
|
"UserLockedOutWithName": "Benotzer {0} esch usgschlosse worde",
|
||||||
"UserOfflineFromDevice": "{0} has disconnected from {1}",
|
"UserOfflineFromDevice": "{0} esch vo {1} trennt worde",
|
||||||
"UserOnlineFromDevice": "{0} is online from {1}",
|
"UserOnlineFromDevice": "{0} esch online vo {1}",
|
||||||
"UserPasswordChangedWithName": "Password has been changed for user {0}",
|
"UserPasswordChangedWithName": "S'Passwort för Benotzer {0} esch gänderet worde",
|
||||||
"UserPolicyUpdatedWithName": "User policy has been updated for {0}",
|
"UserPolicyUpdatedWithName": "Benotzerrechtlinie för {0} esch aktualisiert worde",
|
||||||
"UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}",
|
"UserStartedPlayingItemWithValues": "{0} hed d'Wedergab vo {1} of {2} gstartet",
|
||||||
"UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}",
|
"UserStoppedPlayingItemWithValues": "{0} het d'Wedergab vo {1} of {2} gstoppt",
|
||||||
"ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
|
"ValueHasBeenAddedToLibrary": "{0} esch dinnere Biblithek hinzuegfüegt worde",
|
||||||
"ValueSpecialEpisodeName": "Spezial - {0}",
|
"ValueSpecialEpisodeName": "Extra - {0}",
|
||||||
"VersionNumber": "Version {0}"
|
"VersionNumber": "Version {0}"
|
||||||
}
|
}
|
||||||
|
|
1
Emby.Server.Implementations/Localization/Core/ja.json
Normal file
1
Emby.Server.Implementations/Localization/Core/ja.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{}
|
|
@ -5,7 +5,7 @@
|
||||||
"Artists": "Oryndaýshylar",
|
"Artists": "Oryndaýshylar",
|
||||||
"AuthenticationSucceededWithUserName": "{0} túpnusqalyq rastalýy sátti aıaqtaldy",
|
"AuthenticationSucceededWithUserName": "{0} túpnusqalyq rastalýy sátti aıaqtaldy",
|
||||||
"Books": "Kitaptar",
|
"Books": "Kitaptar",
|
||||||
"CameraImageUploadedFrom": "{0} kamerasynan jańa sýret júktep alyndy",
|
"CameraImageUploadedFrom": "{0} kamerasynan jańa sýret júktep salyndy",
|
||||||
"Channels": "Arnalar",
|
"Channels": "Arnalar",
|
||||||
"ChapterNameValue": "{0}-sahna",
|
"ChapterNameValue": "{0}-sahna",
|
||||||
"Collections": "Jıyntyqtar",
|
"Collections": "Jıyntyqtar",
|
||||||
|
@ -35,8 +35,8 @@
|
||||||
"Latest": "Eń keıingi",
|
"Latest": "Eń keıingi",
|
||||||
"MessageApplicationUpdated": "Jellyfin Serveri jańartyldy",
|
"MessageApplicationUpdated": "Jellyfin Serveri jańartyldy",
|
||||||
"MessageApplicationUpdatedTo": "Jellyfin Serveri {0} nusqasyna jańartyldy",
|
"MessageApplicationUpdatedTo": "Jellyfin Serveri {0} nusqasyna jańartyldy",
|
||||||
"MessageNamedServerConfigurationUpdatedWithValue": "Server teńsheliminiń {0} bólimi jańartyldy",
|
"MessageNamedServerConfigurationUpdatedWithValue": "Server konfıgýrasýasynyń {0} bólimi jańartyldy",
|
||||||
"MessageServerConfigurationUpdated": "Server teńshelimi jańartyldy",
|
"MessageServerConfigurationUpdated": "Server konfıgýrasıasy jańartyldy",
|
||||||
"MixedContent": "Aralas mazmun",
|
"MixedContent": "Aralas mazmun",
|
||||||
"Movies": "Fılmder",
|
"Movies": "Fılmder",
|
||||||
"Music": "Mýzyka",
|
"Music": "Mýzyka",
|
||||||
|
@ -49,7 +49,7 @@
|
||||||
"NotificationOptionApplicationUpdateInstalled": "Qoldanba jańartýy ornatyldy",
|
"NotificationOptionApplicationUpdateInstalled": "Qoldanba jańartýy ornatyldy",
|
||||||
"NotificationOptionAudioPlayback": "Dybys oınatýy bastaldy",
|
"NotificationOptionAudioPlayback": "Dybys oınatýy bastaldy",
|
||||||
"NotificationOptionAudioPlaybackStopped": "Dybys oınatýy toqtatyldy",
|
"NotificationOptionAudioPlaybackStopped": "Dybys oınatýy toqtatyldy",
|
||||||
"NotificationOptionCameraImageUploaded": "Kameradan fotosýret keri qotarylǵan",
|
"NotificationOptionCameraImageUploaded": "Kameradan fotosýret júktep salynǵan",
|
||||||
"NotificationOptionInstallationFailed": "Ornatý sátsizdigi",
|
"NotificationOptionInstallationFailed": "Ornatý sátsizdigi",
|
||||||
"NotificationOptionNewLibraryContent": "Jańa mazmun ústelgen",
|
"NotificationOptionNewLibraryContent": "Jańa mazmun ústelgen",
|
||||||
"NotificationOptionPluginError": "Plagın sátsizdigi",
|
"NotificationOptionPluginError": "Plagın sátsizdigi",
|
||||||
|
|
|
@ -1,21 +1,21 @@
|
||||||
{
|
{
|
||||||
"Albums": "Albums",
|
"Albums": "Albumai",
|
||||||
"AppDeviceValues": "App: {0}, Device: {1}",
|
"AppDeviceValues": "App: {0}, Device: {1}",
|
||||||
"Application": "Application",
|
"Application": "Application",
|
||||||
"Artists": "Artists",
|
"Artists": "Atlikėjai",
|
||||||
"AuthenticationSucceededWithUserName": "{0} successfully authenticated",
|
"AuthenticationSucceededWithUserName": "{0} successfully authenticated",
|
||||||
"Books": "Books",
|
"Books": "Knygos",
|
||||||
"CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
|
"CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
|
||||||
"Channels": "Channels",
|
"Channels": "Kanalai",
|
||||||
"ChapterNameValue": "Chapter {0}",
|
"ChapterNameValue": "Chapter {0}",
|
||||||
"Collections": "Collections",
|
"Collections": "Kolekcijos",
|
||||||
"DeviceOfflineWithName": "{0} has disconnected",
|
"DeviceOfflineWithName": "{0} has disconnected",
|
||||||
"DeviceOnlineWithName": "{0} is connected",
|
"DeviceOnlineWithName": "{0} is connected",
|
||||||
"FailedLoginAttemptWithUserName": "Failed login attempt from {0}",
|
"FailedLoginAttemptWithUserName": "Failed login attempt from {0}",
|
||||||
"Favorites": "Favorites",
|
"Favorites": "Mėgstami",
|
||||||
"Folders": "Folders",
|
"Folders": "Katalogai",
|
||||||
"Genres": "Žanrai",
|
"Genres": "Žanrai",
|
||||||
"HeaderAlbumArtists": "Album Artists",
|
"HeaderAlbumArtists": "Albumo atlikėjai",
|
||||||
"HeaderCameraUploads": "Camera Uploads",
|
"HeaderCameraUploads": "Camera Uploads",
|
||||||
"HeaderContinueWatching": "Žiūrėti toliau",
|
"HeaderContinueWatching": "Žiūrėti toliau",
|
||||||
"HeaderFavoriteAlbums": "Favorite Albums",
|
"HeaderFavoriteAlbums": "Favorite Albums",
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
"Channels": "Kanali",
|
"Channels": "Kanali",
|
||||||
"ChapterNameValue": "Poglavje {0}",
|
"ChapterNameValue": "Poglavje {0}",
|
||||||
"Collections": "Zbirke",
|
"Collections": "Zbirke",
|
||||||
"DeviceOfflineWithName": "{0} has disconnected",
|
"DeviceOfflineWithName": "{0} je prekinil povezavo",
|
||||||
"DeviceOnlineWithName": "{0} je povezan",
|
"DeviceOnlineWithName": "{0} je povezan",
|
||||||
"FailedLoginAttemptWithUserName": "Neuspešen poskus prijave z {0}",
|
"FailedLoginAttemptWithUserName": "Neuspešen poskus prijave z {0}",
|
||||||
"Favorites": "Priljubljeni",
|
"Favorites": "Priljubljeni",
|
||||||
|
@ -33,9 +33,9 @@
|
||||||
"LabelIpAddressValue": "IP naslov: {0}",
|
"LabelIpAddressValue": "IP naslov: {0}",
|
||||||
"LabelRunningTimeValue": "Čas trajanja: {0}",
|
"LabelRunningTimeValue": "Čas trajanja: {0}",
|
||||||
"Latest": "Najnovejše",
|
"Latest": "Najnovejše",
|
||||||
"MessageApplicationUpdated": "Jellyfin strežnik je bil posodobljen",
|
"MessageApplicationUpdated": "Jellyfin Server je bil posodobljen",
|
||||||
"MessageApplicationUpdatedTo": "Jellyfin strežnik je bil posodobljen na {0}",
|
"MessageApplicationUpdatedTo": "Jellyfin Server je bil posodobljen na {0}",
|
||||||
"MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated",
|
"MessageNamedServerConfigurationUpdatedWithValue": "Oddelek nastavitve strežnika {0} je bil posodobljen",
|
||||||
"MessageServerConfigurationUpdated": "Nastavitve strežnika so bile posodobljene",
|
"MessageServerConfigurationUpdated": "Nastavitve strežnika so bile posodobljene",
|
||||||
"MixedContent": "Razne vsebine",
|
"MixedContent": "Razne vsebine",
|
||||||
"Movies": "Filmi",
|
"Movies": "Filmi",
|
||||||
|
@ -57,41 +57,41 @@
|
||||||
"NotificationOptionPluginUninstalled": "Dodatek odstranjen",
|
"NotificationOptionPluginUninstalled": "Dodatek odstranjen",
|
||||||
"NotificationOptionPluginUpdateInstalled": "Posodobitev dodatka nameščena",
|
"NotificationOptionPluginUpdateInstalled": "Posodobitev dodatka nameščena",
|
||||||
"NotificationOptionServerRestartRequired": "Potreben je ponovni zagon strežnika",
|
"NotificationOptionServerRestartRequired": "Potreben je ponovni zagon strežnika",
|
||||||
"NotificationOptionTaskFailed": "Scheduled task failure",
|
"NotificationOptionTaskFailed": "Razporejena naloga neuspešna",
|
||||||
"NotificationOptionUserLockedOut": "User locked out",
|
"NotificationOptionUserLockedOut": "Uporabnik zaklenjen",
|
||||||
"NotificationOptionVideoPlayback": "Video playback started",
|
"NotificationOptionVideoPlayback": "Predvajanje videa se je začelo",
|
||||||
"NotificationOptionVideoPlaybackStopped": "Video playback stopped",
|
"NotificationOptionVideoPlaybackStopped": "Predvajanje videa se je ustavilo",
|
||||||
"Photos": "Photos",
|
"Photos": "Fotografije",
|
||||||
"Playlists": "Playlists",
|
"Playlists": "Seznami predvajanja",
|
||||||
"Plugin": "Plugin",
|
"Plugin": "Plugin",
|
||||||
"PluginInstalledWithName": "{0} was installed",
|
"PluginInstalledWithName": "{0} je bil nameščen",
|
||||||
"PluginUninstalledWithName": "{0} was uninstalled",
|
"PluginUninstalledWithName": "{0} je bil odstranjen",
|
||||||
"PluginUpdatedWithName": "{0} was updated",
|
"PluginUpdatedWithName": "{0} je bil posodobljen",
|
||||||
"ProviderValue": "Provider: {0}",
|
"ProviderValue": "Provider: {0}",
|
||||||
"ScheduledTaskFailedWithName": "{0} failed",
|
"ScheduledTaskFailedWithName": "{0} ni uspelo",
|
||||||
"ScheduledTaskStartedWithName": "{0} started",
|
"ScheduledTaskStartedWithName": "{0} začeto",
|
||||||
"ServerNameNeedsToBeRestarted": "{0} needs to be restarted",
|
"ServerNameNeedsToBeRestarted": "{0} mora biti ponovno zagnan",
|
||||||
"Shows": "Serije",
|
"Shows": "Serije",
|
||||||
"Songs": "Songs",
|
"Songs": "Pesmi",
|
||||||
"StartupEmbyServerIsLoading": "Jellyfin Server is loading. Please try again shortly.",
|
"StartupEmbyServerIsLoading": "Jellyfin Server se nalaga. Poskusi ponovno kasneje.",
|
||||||
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
||||||
"SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
|
"SubtitleDownloadFailureFromForItem": "Neuspešen prenos podnapisov iz {0} za {1}",
|
||||||
"SubtitlesDownloadedForItem": "Subtitles downloaded for {0}",
|
"SubtitlesDownloadedForItem": "Podnapisi preneseni za {0}",
|
||||||
"Sync": "Sync",
|
"Sync": "Sinhroniziraj",
|
||||||
"System": "System",
|
"System": "System",
|
||||||
"TvShows": "TV Shows",
|
"TvShows": "TV serije",
|
||||||
"User": "User",
|
"User": "User",
|
||||||
"UserCreatedWithName": "User {0} has been created",
|
"UserCreatedWithName": "Uporabnik {0} je bil ustvarjen",
|
||||||
"UserDeletedWithName": "User {0} has been deleted",
|
"UserDeletedWithName": "Uporabnik {0} je bil izbrisan",
|
||||||
"UserDownloadingItemWithValues": "{0} is downloading {1}",
|
"UserDownloadingItemWithValues": "{0} prenaša {1}",
|
||||||
"UserLockedOutWithName": "User {0} has been locked out",
|
"UserLockedOutWithName": "Uporabnik {0} je bil zaklenjen",
|
||||||
"UserOfflineFromDevice": "{0} has disconnected from {1}",
|
"UserOfflineFromDevice": "{0} je prekinil povezavo z {1}",
|
||||||
"UserOnlineFromDevice": "{0} is online from {1}",
|
"UserOnlineFromDevice": "{0} je aktiven iz {1}",
|
||||||
"UserPasswordChangedWithName": "Password has been changed for user {0}",
|
"UserPasswordChangedWithName": "Geslo za uporabnika {0} je bilo spremenjeno",
|
||||||
"UserPolicyUpdatedWithName": "User policy has been updated for {0}",
|
"UserPolicyUpdatedWithName": "Pravilnik uporabe je bil posodobljen za uporabnika {0}",
|
||||||
"UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}",
|
"UserStartedPlayingItemWithValues": "{0} predvaja {1} na {2}",
|
||||||
"UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}",
|
"UserStoppedPlayingItemWithValues": "{0} je nehal predvajati {1} na {2}",
|
||||||
"ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
|
"ValueHasBeenAddedToLibrary": "{0} je bil dodan vaši knjižnici",
|
||||||
"ValueSpecialEpisodeName": "Special - {0}",
|
"ValueSpecialEpisodeName": "Special - {0}",
|
||||||
"VersionNumber": "Version {0}"
|
"VersionNumber": "Version {0}"
|
||||||
}
|
}
|
||||||
|
|
93
Emby.Server.Implementations/Localization/Core/zh-TW.json
Normal file
93
Emby.Server.Implementations/Localization/Core/zh-TW.json
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
{
|
||||||
|
"Albums": "專輯",
|
||||||
|
"AppDeviceValues": "應用: {0}, 裝置: {1}",
|
||||||
|
"Application": "應用程式",
|
||||||
|
"Artists": "演出者",
|
||||||
|
"AuthenticationSucceededWithUserName": "{0} 成功授權",
|
||||||
|
"Books": "圖書",
|
||||||
|
"CameraImageUploadedFrom": "{0} 已經成功上傳一張相片",
|
||||||
|
"Channels": "頻道",
|
||||||
|
"ChapterNameValue": "章節 {0}",
|
||||||
|
"Collections": "合輯",
|
||||||
|
"DeviceOfflineWithName": "{0} 已經斷線",
|
||||||
|
"DeviceOnlineWithName": "{0} 已經連線",
|
||||||
|
"FailedLoginAttemptWithUserName": "來自 {0} 的失敗登入嘗試",
|
||||||
|
"Favorites": "我的最愛",
|
||||||
|
"Folders": "資料夾",
|
||||||
|
"Genres": "風格",
|
||||||
|
"HeaderAlbumArtists": "專輯演出者",
|
||||||
|
"HeaderCameraUploads": "相機上傳",
|
||||||
|
"HeaderContinueWatching": "繼續觀賞",
|
||||||
|
"HeaderFavoriteAlbums": "最愛專輯",
|
||||||
|
"HeaderFavoriteArtists": "最愛演出者",
|
||||||
|
"HeaderFavoriteEpisodes": "最愛級數",
|
||||||
|
"HeaderFavoriteShows": "最愛節目",
|
||||||
|
"HeaderFavoriteSongs": "最愛歌曲",
|
||||||
|
"HeaderLiveTV": "電視直播",
|
||||||
|
"HeaderNextUp": "接下來",
|
||||||
|
"HomeVideos": "自製影片",
|
||||||
|
"ItemAddedWithName": "{0} 已新增至媒體庫",
|
||||||
|
"ItemRemovedWithName": "{0} 已從媒體庫移除",
|
||||||
|
"LabelIpAddressValue": "IP 位置: {0}",
|
||||||
|
"LabelRunningTimeValue": "運行時間: {0}",
|
||||||
|
"Latest": "最新",
|
||||||
|
"MessageApplicationUpdated": "Jellyfin Server 已經更新",
|
||||||
|
"MessageApplicationUpdatedTo": "Jellyfin Server 已經更新至 {0}",
|
||||||
|
"MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定 {0} 部分已經更新",
|
||||||
|
"MessageServerConfigurationUpdated": "伺服器設定已經更新",
|
||||||
|
"MixedContent": "混合內容",
|
||||||
|
"Movies": "電影",
|
||||||
|
"Music": "音樂",
|
||||||
|
"MusicVideos": "音樂MV",
|
||||||
|
"NameInstallFailed": "{0} 安裝失敗",
|
||||||
|
"NameSeasonNumber": "第 {0} 季",
|
||||||
|
"NameSeasonUnknown": "未知季數",
|
||||||
|
"NewVersionIsAvailable": "新版本的Jellyfin Server 軟體已經推出可供下載。",
|
||||||
|
"NotificationOptionApplicationUpdateAvailable": "有可用的應用程式更新",
|
||||||
|
"NotificationOptionApplicationUpdateInstalled": "應用程式已更新",
|
||||||
|
"NotificationOptionAudioPlayback": "音樂開始播放",
|
||||||
|
"NotificationOptionAudioPlaybackStopped": "音樂停止播放",
|
||||||
|
"NotificationOptionCameraImageUploaded": "相機相片已上傳",
|
||||||
|
"NotificationOptionInstallationFailed": "安裝失敗",
|
||||||
|
"NotificationOptionNewLibraryContent": "已新增新內容",
|
||||||
|
"NotificationOptionPluginError": "外掛失敗",
|
||||||
|
"NotificationOptionPluginInstalled": "外掛已安裝",
|
||||||
|
"NotificationOptionPluginUninstalled": "外掛已移除",
|
||||||
|
"NotificationOptionPluginUpdateInstalled": "已更新外掛",
|
||||||
|
"NotificationOptionServerRestartRequired": "伺服器需要重新啟動",
|
||||||
|
"NotificationOptionTaskFailed": "排程任務失敗",
|
||||||
|
"NotificationOptionUserLockedOut": "使用者已鎖定",
|
||||||
|
"NotificationOptionVideoPlayback": "影片開始播放",
|
||||||
|
"NotificationOptionVideoPlaybackStopped": "影片停止播放",
|
||||||
|
"Photos": "相片",
|
||||||
|
"Playlists": "播放清單",
|
||||||
|
"Plugin": "外掛",
|
||||||
|
"PluginInstalledWithName": "{0} 已安裝",
|
||||||
|
"PluginUninstalledWithName": "{0} 已移除",
|
||||||
|
"PluginUpdatedWithName": "{0} 已更新",
|
||||||
|
"ProviderValue": "提供商: {0}",
|
||||||
|
"ScheduledTaskFailedWithName": "{0} 已失敗",
|
||||||
|
"ScheduledTaskStartedWithName": "{0} 已開始",
|
||||||
|
"ServerNameNeedsToBeRestarted": "{0} 需要重新啟動",
|
||||||
|
"Shows": "節目",
|
||||||
|
"Songs": "歌曲",
|
||||||
|
"StartupEmbyServerIsLoading": "Jellyfin Server正在啟動,請稍後再試一次。",
|
||||||
|
"SubtitlesDownloadedForItem": "已為 {0} 下載字幕",
|
||||||
|
"Sync": "同步",
|
||||||
|
"System": "系統",
|
||||||
|
"TvShows": "電視節目",
|
||||||
|
"User": "使用者",
|
||||||
|
"UserCreatedWithName": "使用者 {0} 已建立",
|
||||||
|
"UserDeletedWithName": "使用者 {0} 已移除",
|
||||||
|
"UserDownloadingItemWithValues": "{0} 正在下載 {1}",
|
||||||
|
"UserLockedOutWithName": "使用者 {0} 已鎖定",
|
||||||
|
"UserOfflineFromDevice": "{0} 已從 {1} 斷線",
|
||||||
|
"UserOnlineFromDevice": "{0} 已連線,來自 {1}",
|
||||||
|
"UserPasswordChangedWithName": "使用者 {0} 的密碼已變更",
|
||||||
|
"UserPolicyUpdatedWithName": "使用者條約已更新為 {0}",
|
||||||
|
"UserStartedPlayingItemWithValues": "{0}正在使用 {2} 播放 {1}",
|
||||||
|
"UserStoppedPlayingItemWithValues": "{0} 已停止在 {2} 播放 {1}",
|
||||||
|
"ValueHasBeenAddedToLibrary": "{0} 已新增至您的媒體庫",
|
||||||
|
"ValueSpecialEpisodeName": "特典 - {0}",
|
||||||
|
"VersionNumber": "版本 {0}"
|
||||||
|
}
|
|
@ -43,6 +43,11 @@ namespace Emby.Server.Implementations.Services
|
||||||
{
|
{
|
||||||
var contentLength = bytesResponse.Length;
|
var contentLength = bytesResponse.Length;
|
||||||
|
|
||||||
|
if (response != null)
|
||||||
|
{
|
||||||
|
response.OriginalResponse.ContentLength = contentLength;
|
||||||
|
}
|
||||||
|
|
||||||
if (contentLength > 0)
|
if (contentLength > 0)
|
||||||
{
|
{
|
||||||
await responseStream.WriteAsync(bytesResponse, 0, contentLength, cancellationToken).ConfigureAwait(false);
|
await responseStream.WriteAsync(bytesResponse, 0, contentLength, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
@ -20,6 +21,8 @@ namespace Emby.Server.Implementations.Services
|
||||||
{
|
{
|
||||||
response.StatusCode = (int)HttpStatusCode.NoContent;
|
response.StatusCode = (int)HttpStatusCode.NoContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
response.OriginalResponse.ContentLength = 0;
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,11 +42,6 @@ namespace Emby.Server.Implementations.Services
|
||||||
|
|
||||||
response.StatusCode = httpResult.Status;
|
response.StatusCode = httpResult.Status;
|
||||||
response.StatusDescription = httpResult.StatusCode.ToString();
|
response.StatusDescription = httpResult.StatusCode.ToString();
|
||||||
//if (string.IsNullOrEmpty(httpResult.ContentType))
|
|
||||||
//{
|
|
||||||
// httpResult.ContentType = defaultContentType;
|
|
||||||
//}
|
|
||||||
//response.ContentType = httpResult.ContentType;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var responseOptions = result as IHasHeaders;
|
var responseOptions = result as IHasHeaders;
|
||||||
|
@ -53,6 +51,7 @@ namespace Emby.Server.Implementations.Services
|
||||||
{
|
{
|
||||||
if (string.Equals(responseHeaders.Key, "Content-Length", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(responseHeaders.Key, "Content-Length", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
response.OriginalResponse.ContentLength = long.Parse(responseHeaders.Value, CultureInfo.InvariantCulture);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,52 +71,37 @@ namespace Emby.Server.Implementations.Services
|
||||||
response.ContentType += "; charset=utf-8";
|
response.ContentType += "; charset=utf-8";
|
||||||
}
|
}
|
||||||
|
|
||||||
var asyncStreamWriter = result as IAsyncStreamWriter;
|
switch (result)
|
||||||
if (asyncStreamWriter != null)
|
|
||||||
{
|
{
|
||||||
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 (bytes.Length > 0)
|
||||||
if (streamWriter != null)
|
{
|
||||||
{
|
return response.OutputStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken);
|
||||||
streamWriter.WriteTo(response.OutputStream);
|
}
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
var fileWriter = result as FileWriter;
|
return Task.CompletedTask;
|
||||||
if (fileWriter != null)
|
case string responseText:
|
||||||
{
|
var responseTextAsBytes = Encoding.UTF8.GetBytes(responseText);
|
||||||
return fileWriter.WriteToAsync(response, cancellationToken);
|
response.OriginalResponse.ContentLength = responseTextAsBytes.Length;
|
||||||
}
|
|
||||||
|
|
||||||
var stream = result as Stream;
|
if (responseTextAsBytes.Length > 0)
|
||||||
if (stream != null)
|
{
|
||||||
{
|
return response.OutputStream.WriteAsync(responseTextAsBytes, 0, responseTextAsBytes.Length, cancellationToken);
|
||||||
return CopyStream(stream, response.OutputStream);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
var bytes = result as byte[];
|
return Task.CompletedTask;
|
||||||
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 WriteObject(request, result, response);
|
return WriteObject(request, result, response);
|
||||||
|
@ -143,14 +127,13 @@ namespace Emby.Server.Implementations.Services
|
||||||
ms.Position = 0;
|
ms.Position = 0;
|
||||||
|
|
||||||
var contentLength = ms.Length;
|
var contentLength = ms.Length;
|
||||||
|
response.OriginalResponse.ContentLength = contentLength;
|
||||||
|
|
||||||
if (contentLength > 0)
|
if (contentLength > 0)
|
||||||
{
|
{
|
||||||
await ms.CopyToAsync(response.OutputStream).ConfigureAwait(false);
|
await ms.CopyToAsync(response.OutputStream).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//serializer(result, outputStream);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +1,17 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Reflection;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Emby.Server.Implementations.HttpServer;
|
using Emby.Server.Implementations.HttpServer;
|
||||||
using MediaBrowser.Model.Services;
|
using MediaBrowser.Model.Services;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Services
|
namespace Emby.Server.Implementations.Services
|
||||||
{
|
{
|
||||||
public delegate Task<object> InstanceExecFn(IRequest requestContext, object intance, object request);
|
|
||||||
public delegate object ActionInvokerFn(object intance, object request);
|
public delegate object ActionInvokerFn(object intance, object request);
|
||||||
public delegate void VoidActionInvokerFn(object intance, object request);
|
public delegate void VoidActionInvokerFn(object intance, object request);
|
||||||
|
|
||||||
public class ServiceController
|
public class ServiceController
|
||||||
{
|
{
|
||||||
public static ServiceController Instance;
|
public void Init(HttpListenerHost appHost, IEnumerable<Type> serviceTypes)
|
||||||
|
|
||||||
public ServiceController()
|
|
||||||
{
|
|
||||||
Instance = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Init(HttpListenerHost appHost, Type[] serviceTypes)
|
|
||||||
{
|
{
|
||||||
foreach (var serviceType in serviceTypes)
|
foreach (var serviceType in serviceTypes)
|
||||||
{
|
{
|
||||||
|
@ -37,7 +28,11 @@ namespace Emby.Server.Implementations.Services
|
||||||
foreach (var mi in serviceType.GetActions())
|
foreach (var mi in serviceType.GetActions())
|
||||||
{
|
{
|
||||||
var requestType = mi.GetParameters()[0].ParameterType;
|
var requestType = mi.GetParameters()[0].ParameterType;
|
||||||
if (processedReqs.Contains(requestType)) continue;
|
if (processedReqs.Contains(requestType))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
processedReqs.Add(requestType);
|
processedReqs.Add(requestType);
|
||||||
|
|
||||||
ServiceExecGeneral.CreateServiceRunnersFor(requestType, actions);
|
ServiceExecGeneral.CreateServiceRunnersFor(requestType, actions);
|
||||||
|
@ -55,18 +50,6 @@ namespace Emby.Server.Implementations.Services
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Type FirstGenericType(Type type)
|
|
||||||
{
|
|
||||||
while (type != null)
|
|
||||||
{
|
|
||||||
if (type.GetTypeInfo().IsGenericType)
|
|
||||||
return type;
|
|
||||||
|
|
||||||
type = type.GetTypeInfo().BaseType;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public readonly RestPath.RestPathMap RestPathMap = new RestPath.RestPathMap();
|
public readonly RestPath.RestPathMap RestPathMap = new RestPath.RestPathMap();
|
||||||
|
|
||||||
public void RegisterRestPaths(HttpListenerHost appHost, Type requestType, Type serviceType)
|
public void RegisterRestPaths(HttpListenerHost appHost, Type requestType, Type serviceType)
|
||||||
|
@ -84,17 +67,24 @@ namespace Emby.Server.Implementations.Services
|
||||||
|
|
||||||
public void RegisterRestPath(RestPath restPath)
|
public void RegisterRestPath(RestPath restPath)
|
||||||
{
|
{
|
||||||
if (!restPath.Path.StartsWith("/"))
|
if (restPath.Path[0] != '/')
|
||||||
throw new ArgumentException(string.Format("Route '{0}' on '{1}' must start with a '/'", restPath.Path, restPath.RequestType.GetMethodName()));
|
|
||||||
if (restPath.Path.IndexOfAny(InvalidRouteChars) != -1)
|
|
||||||
throw new ArgumentException(string.Format("Route '{0}' on '{1}' contains invalid chars. ", restPath.Path, restPath.RequestType.GetMethodName()));
|
|
||||||
|
|
||||||
if (!RestPathMap.TryGetValue(restPath.FirstMatchHashKey, out List<RestPath> pathsAtFirstMatch))
|
|
||||||
{
|
{
|
||||||
pathsAtFirstMatch = new List<RestPath>();
|
throw new ArgumentException(string.Format("Route '{0}' on '{1}' must start with a '/'", restPath.Path, restPath.RequestType.GetMethodName()));
|
||||||
RestPathMap[restPath.FirstMatchHashKey] = pathsAtFirstMatch;
|
}
|
||||||
|
|
||||||
|
if (restPath.Path.IndexOfAny(InvalidRouteChars) != -1)
|
||||||
|
{
|
||||||
|
throw new ArgumentException(string.Format("Route '{0}' on '{1}' contains invalid chars. ", restPath.Path, restPath.RequestType.GetMethodName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RestPathMap.TryGetValue(restPath.FirstMatchHashKey, out List<RestPath> pathsAtFirstMatch))
|
||||||
|
{
|
||||||
|
pathsAtFirstMatch.Add(restPath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
RestPathMap[restPath.FirstMatchHashKey] = new List<RestPath>() { restPath };
|
||||||
}
|
}
|
||||||
pathsAtFirstMatch.Add(restPath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public RestPath GetRestPathForRequest(string httpMethod, string pathInfo)
|
public RestPath GetRestPathForRequest(string httpMethod, string pathInfo)
|
||||||
|
@ -155,17 +145,15 @@ namespace Emby.Server.Implementations.Services
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<object> Execute(HttpListenerHost appHost, object requestDto, IRequest req)
|
public Task<object> Execute(HttpListenerHost httpHost, object requestDto, IRequest req)
|
||||||
{
|
{
|
||||||
req.Dto = requestDto;
|
req.Dto = requestDto;
|
||||||
var requestType = requestDto.GetType();
|
var requestType = requestDto.GetType();
|
||||||
req.OperationName = requestType.Name;
|
req.OperationName = requestType.Name;
|
||||||
|
|
||||||
var serviceType = appHost.GetServiceTypeByRequest(requestType);
|
var serviceType = httpHost.GetServiceTypeByRequest(requestType);
|
||||||
|
|
||||||
var service = appHost.CreateInstance(serviceType);
|
var service = httpHost.CreateInstance(serviceType);
|
||||||
|
|
||||||
//var service = typeFactory.CreateInstance(serviceType);
|
|
||||||
|
|
||||||
var serviceRequiresContext = service as IRequiresRequest;
|
var serviceRequiresContext = service as IRequiresRequest;
|
||||||
if (serviceRequiresContext != null)
|
if (serviceRequiresContext != null)
|
||||||
|
|
|
@ -11,6 +11,16 @@ namespace Emby.Server.Implementations.Services
|
||||||
{
|
{
|
||||||
public class ServiceHandler
|
public class ServiceHandler
|
||||||
{
|
{
|
||||||
|
public RestPath RestPath { get; }
|
||||||
|
|
||||||
|
public string ResponseContentType { get; }
|
||||||
|
|
||||||
|
internal ServiceHandler(RestPath restPath, string responseContentType)
|
||||||
|
{
|
||||||
|
RestPath = restPath;
|
||||||
|
ResponseContentType = responseContentType;
|
||||||
|
}
|
||||||
|
|
||||||
protected static Task<object> CreateContentTypeRequest(HttpListenerHost host, IRequest httpReq, Type requestType, string contentType)
|
protected static Task<object> CreateContentTypeRequest(HttpListenerHost host, IRequest httpReq, Type requestType, string contentType)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(contentType) && httpReq.ContentLength > 0)
|
if (!string.IsNullOrEmpty(contentType) && httpReq.ContentLength > 0)
|
||||||
|
@ -18,24 +28,18 @@ namespace Emby.Server.Implementations.Services
|
||||||
var deserializer = RequestHelper.GetRequestReader(host, contentType);
|
var deserializer = RequestHelper.GetRequestReader(host, contentType);
|
||||||
if (deserializer != null)
|
if (deserializer != null)
|
||||||
{
|
{
|
||||||
return deserializer(requestType, httpReq.InputStream);
|
return deserializer.Invoke(requestType, httpReq.InputStream);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.FromResult(host.CreateInstance(requestType));
|
return Task.FromResult(host.CreateInstance(requestType));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static RestPath FindMatchingRestPath(string httpMethod, string pathInfo, out string contentType)
|
|
||||||
{
|
|
||||||
pathInfo = GetSanitizedPathInfo(pathInfo, out contentType);
|
|
||||||
|
|
||||||
return ServiceController.Instance.GetRestPathForRequest(httpMethod, pathInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string GetSanitizedPathInfo(string pathInfo, out string contentType)
|
public static string GetSanitizedPathInfo(string pathInfo, out string contentType)
|
||||||
{
|
{
|
||||||
contentType = null;
|
contentType = null;
|
||||||
var pos = pathInfo.LastIndexOf('.');
|
var pos = pathInfo.LastIndexOf('.');
|
||||||
if (pos >= 0)
|
if (pos != -1)
|
||||||
{
|
{
|
||||||
var format = pathInfo.Substring(pos + 1);
|
var format = pathInfo.Substring(pos + 1);
|
||||||
contentType = GetFormatContentType(format);
|
contentType = GetFormatContentType(format);
|
||||||
|
@ -44,58 +48,38 @@ namespace Emby.Server.Implementations.Services
|
||||||
pathInfo = pathInfo.Substring(0, pos);
|
pathInfo = pathInfo.Substring(0, pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return pathInfo;
|
return pathInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetFormatContentType(string format)
|
private static string GetFormatContentType(string format)
|
||||||
{
|
{
|
||||||
//built-in formats
|
//built-in formats
|
||||||
if (format == "json")
|
switch (format)
|
||||||
return "application/json";
|
{
|
||||||
if (format == "xml")
|
case "json": return "application/json";
|
||||||
return "application/xml";
|
case "xml": return "application/xml";
|
||||||
|
default: return null;
|
||||||
return null;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public RestPath GetRestPath(string httpMethod, string pathInfo)
|
public async Task ProcessRequestAsync(HttpListenerHost httpHost, IRequest httpReq, IResponse httpRes, ILogger logger, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (this.RestPath == null)
|
httpReq.Items["__route"] = RestPath;
|
||||||
{
|
|
||||||
this.RestPath = FindMatchingRestPath(httpMethod, pathInfo, out string contentType);
|
|
||||||
|
|
||||||
if (contentType != null)
|
|
||||||
ResponseContentType = contentType;
|
|
||||||
}
|
|
||||||
return this.RestPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public RestPath RestPath { get; set; }
|
|
||||||
|
|
||||||
// Set from SSHHF.GetHandlerForPathInfo()
|
|
||||||
public string ResponseContentType { get; set; }
|
|
||||||
|
|
||||||
public async Task ProcessRequestAsync(HttpListenerHost appHost, IRequest httpReq, IResponse httpRes, ILogger logger, string operationName, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var restPath = GetRestPath(httpReq.Verb, httpReq.PathInfo);
|
|
||||||
if (restPath == null)
|
|
||||||
{
|
|
||||||
throw new NotSupportedException("No RestPath found for: " + httpReq.Verb + " " + httpReq.PathInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
SetRoute(httpReq, restPath);
|
|
||||||
|
|
||||||
if (ResponseContentType != null)
|
if (ResponseContentType != null)
|
||||||
|
{
|
||||||
httpReq.ResponseContentType = ResponseContentType;
|
httpReq.ResponseContentType = ResponseContentType;
|
||||||
|
}
|
||||||
|
|
||||||
var request = httpReq.Dto = await CreateRequest(appHost, httpReq, restPath, logger).ConfigureAwait(false);
|
var request = httpReq.Dto = await CreateRequest(httpHost, httpReq, RestPath, logger).ConfigureAwait(false);
|
||||||
|
|
||||||
appHost.ApplyRequestFilters(httpReq, httpRes, request);
|
httpHost.ApplyRequestFilters(httpReq, httpRes, request);
|
||||||
|
|
||||||
var response = await appHost.ServiceController.Execute(appHost, request, httpReq).ConfigureAwait(false);
|
var response = await httpHost.ServiceController.Execute(httpHost, request, httpReq).ConfigureAwait(false);
|
||||||
|
|
||||||
// Apply response filters
|
// Apply response filters
|
||||||
foreach (var responseFilter in appHost.ResponseFilters)
|
foreach (var responseFilter in httpHost.ResponseFilters)
|
||||||
{
|
{
|
||||||
responseFilter(httpReq, httpRes, response);
|
responseFilter(httpReq, httpRes, response);
|
||||||
}
|
}
|
||||||
|
@ -152,7 +136,11 @@ namespace Emby.Server.Implementations.Services
|
||||||
|
|
||||||
foreach (var name in request.QueryString.Keys)
|
foreach (var name in request.QueryString.Keys)
|
||||||
{
|
{
|
||||||
if (name == null) continue; //thank you ASP.NET
|
if (name == null)
|
||||||
|
{
|
||||||
|
// thank you ASP.NET
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var values = request.QueryString[name];
|
var values = request.QueryString[name];
|
||||||
if (values.Count == 1)
|
if (values.Count == 1)
|
||||||
|
@ -175,7 +163,11 @@ namespace Emby.Server.Implementations.Services
|
||||||
{
|
{
|
||||||
foreach (var name in formData.Keys)
|
foreach (var name in formData.Keys)
|
||||||
{
|
{
|
||||||
if (name == null) continue; //thank you ASP.NET
|
if (name == null)
|
||||||
|
{
|
||||||
|
// thank you ASP.NET
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var values = formData.GetValues(name);
|
var values = formData.GetValues(name);
|
||||||
if (values.Count == 1)
|
if (values.Count == 1)
|
||||||
|
@ -210,7 +202,12 @@ namespace Emby.Server.Implementations.Services
|
||||||
|
|
||||||
foreach (var name in request.QueryString.Keys)
|
foreach (var name in request.QueryString.Keys)
|
||||||
{
|
{
|
||||||
if (name == null) continue; //thank you ASP.NET
|
if (name == null)
|
||||||
|
{
|
||||||
|
// thank you ASP.NET
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
map[name] = request.QueryString[name];
|
map[name] = request.QueryString[name];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,7 +218,12 @@ namespace Emby.Server.Implementations.Services
|
||||||
{
|
{
|
||||||
foreach (var name in formData.Keys)
|
foreach (var name in formData.Keys)
|
||||||
{
|
{
|
||||||
if (name == null) continue; //thank you ASP.NET
|
if (name == null)
|
||||||
|
{
|
||||||
|
// thank you ASP.NET
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
map[name] = formData[name];
|
map[name] = formData[name];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -229,17 +231,5 @@ namespace Emby.Server.Implementations.Services
|
||||||
|
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void SetRoute(IRequest req, RestPath route)
|
|
||||||
{
|
|
||||||
req.Items["__route"] = route;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static RestPath GetRoute(IRequest req)
|
|
||||||
{
|
|
||||||
req.Items.TryGetValue("__route", out var route);
|
|
||||||
return route as RestPath;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,7 +71,7 @@ namespace Emby.Server.Implementations.Services
|
||||||
string propertyName = pair.Key;
|
string propertyName = pair.Key;
|
||||||
string propertyTextValue = pair.Value;
|
string propertyTextValue = pair.Value;
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(propertyTextValue)
|
if (propertyTextValue == null
|
||||||
|| !propertySetterMap.TryGetValue(propertyName, out propertySerializerEntry)
|
|| !propertySetterMap.TryGetValue(propertyName, out propertySerializerEntry)
|
||||||
|| propertySerializerEntry.PropertySetFn == null)
|
|| propertySerializerEntry.PropertySetFn == null)
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using MediaBrowser.Controller.Net;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using MediaBrowser.Model.Services;
|
using MediaBrowser.Model.Services;
|
||||||
|
using Emby.Server.Implementations.HttpServer;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Services
|
namespace Emby.Server.Implementations.Services
|
||||||
{
|
{
|
||||||
|
@ -109,10 +109,16 @@ namespace Emby.Server.Implementations.Services
|
||||||
|
|
||||||
public class SwaggerService : IService, IRequiresRequest
|
public class SwaggerService : IService, IRequiresRequest
|
||||||
{
|
{
|
||||||
|
private readonly IHttpServer _httpServer;
|
||||||
private SwaggerSpec _spec;
|
private SwaggerSpec _spec;
|
||||||
|
|
||||||
public IRequest Request { get; set; }
|
public IRequest Request { get; set; }
|
||||||
|
|
||||||
|
public SwaggerService(IHttpServer httpServer)
|
||||||
|
{
|
||||||
|
_httpServer = httpServer;
|
||||||
|
}
|
||||||
|
|
||||||
public object Get(GetSwaggerSpec request)
|
public object Get(GetSwaggerSpec request)
|
||||||
{
|
{
|
||||||
return _spec ?? (_spec = GetSpec());
|
return _spec ?? (_spec = GetSpec());
|
||||||
|
@ -181,7 +187,8 @@ namespace Emby.Server.Implementations.Services
|
||||||
{
|
{
|
||||||
var paths = new SortedDictionary<string, Dictionary<string, SwaggerMethod>>();
|
var paths = new SortedDictionary<string, Dictionary<string, SwaggerMethod>>();
|
||||||
|
|
||||||
var all = ServiceController.Instance.RestPathMap.OrderBy(i => i.Key, StringComparer.OrdinalIgnoreCase).ToList();
|
// REVIEW: this can be done better
|
||||||
|
var all = ((HttpListenerHost)_httpServer).ServiceController.RestPathMap.OrderBy(i => i.Key, StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
|
|
||||||
foreach (var current in all)
|
foreach (var current in all)
|
||||||
{
|
{
|
||||||
|
@ -192,11 +199,8 @@ namespace Emby.Server.Implementations.Services
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info.Path.StartsWith("/mediabrowser", StringComparison.OrdinalIgnoreCase))
|
if (info.Path.StartsWith("/mediabrowser", StringComparison.OrdinalIgnoreCase)
|
||||||
{
|
|| info.Path.StartsWith("/jellyfin", StringComparison.OrdinalIgnoreCase))
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (info.Path.StartsWith("/jellyfin", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,8 @@ using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using MediaBrowser.Common.Net;
|
||||||
|
using MediaBrowser.Model.Services;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Http.Extensions;
|
using Microsoft.AspNetCore.Http.Extensions;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
@ -25,8 +27,6 @@ namespace Emby.Server.Implementations.SocketSharp
|
||||||
this.OperationName = operationName;
|
this.OperationName = operationName;
|
||||||
this.request = httpContext;
|
this.request = httpContext;
|
||||||
this.Response = new WebSocketSharpResponse(logger, response);
|
this.Response = new WebSocketSharpResponse(logger, response);
|
||||||
|
|
||||||
// HandlerFactoryPath = GetHandlerPathIfAny(UrlPrefixes[0]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public HttpRequest HttpRequest => request;
|
public HttpRequest HttpRequest => request;
|
||||||
|
@ -40,16 +40,9 @@ namespace Emby.Server.Implementations.SocketSharp
|
||||||
public string RawUrl => request.GetEncodedPathAndQuery();
|
public string RawUrl => request.GetEncodedPathAndQuery();
|
||||||
|
|
||||||
public string AbsoluteUri => request.GetDisplayUrl().TrimEnd('/');
|
public string AbsoluteUri => request.GetDisplayUrl().TrimEnd('/');
|
||||||
|
// Header[name] returns "" when undefined
|
||||||
|
|
||||||
public string XForwardedFor
|
private string GetHeader(string name) => request.Headers[name].ToString();
|
||||||
=> StringValues.IsNullOrEmpty(request.Headers["X-Forwarded-For"]) ? null : request.Headers["X-Forwarded-For"].ToString();
|
|
||||||
|
|
||||||
public int? XForwardedPort
|
|
||||||
=> StringValues.IsNullOrEmpty(request.Headers["X-Forwarded-Port"]) ? (int?)null : int.Parse(request.Headers["X-Forwarded-Port"], CultureInfo.InvariantCulture);
|
|
||||||
|
|
||||||
public string XForwardedProtocol => StringValues.IsNullOrEmpty(request.Headers["X-Forwarded-Proto"]) ? null : request.Headers["X-Forwarded-Proto"].ToString();
|
|
||||||
|
|
||||||
public string XRealIp => StringValues.IsNullOrEmpty(request.Headers["X-Real-IP"]) ? null : request.Headers["X-Real-IP"].ToString();
|
|
||||||
|
|
||||||
private string remoteIp;
|
private string remoteIp;
|
||||||
public string RemoteIp
|
public string RemoteIp
|
||||||
|
@ -61,107 +54,27 @@ namespace Emby.Server.Implementations.SocketSharp
|
||||||
return remoteIp;
|
return remoteIp;
|
||||||
}
|
}
|
||||||
|
|
||||||
var temp = CheckBadChars(XForwardedFor.AsSpan());
|
IPAddress ip;
|
||||||
if (temp.Length != 0)
|
|
||||||
|
// "Real" remote ip might be in X-Forwarded-For of X-Real-Ip
|
||||||
|
// (if the server is behind a reverse proxy for example)
|
||||||
|
if (!IPAddress.TryParse(GetHeader(CustomHeaderNames.XForwardedFor), out ip))
|
||||||
{
|
{
|
||||||
return remoteIp = temp.ToString();
|
if (!IPAddress.TryParse(GetHeader(CustomHeaderNames.XRealIP), out ip))
|
||||||
|
{
|
||||||
|
ip = request.HttpContext.Connection.RemoteIpAddress;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
temp = CheckBadChars(XRealIp.AsSpan());
|
return remoteIp = NormalizeIp(ip).ToString();
|
||||||
if (temp.Length != 0)
|
|
||||||
{
|
|
||||||
return remoteIp = NormalizeIp(temp).ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return remoteIp = NormalizeIp(request.HttpContext.Connection.RemoteIpAddress.ToString().AsSpan()).ToString();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly char[] HttpTrimCharacters = new char[] { (char)0x09, (char)0xA, (char)0xB, (char)0xC, (char)0xD, (char)0x20 };
|
private static IPAddress NormalizeIp(IPAddress ip)
|
||||||
|
|
||||||
// CheckBadChars - throws on invalid chars to be not found in header name/value
|
|
||||||
internal static ReadOnlySpan<char> CheckBadChars(ReadOnlySpan<char> name)
|
|
||||||
{
|
{
|
||||||
if (name.Length == 0)
|
if (ip.IsIPv4MappedToIPv6)
|
||||||
{
|
{
|
||||||
return name;
|
return ip.MapToIPv4();
|
||||||
}
|
|
||||||
|
|
||||||
// VALUE check
|
|
||||||
// Trim spaces from both ends
|
|
||||||
name = name.Trim(HttpTrimCharacters);
|
|
||||||
|
|
||||||
// First, check for correctly formed multi-line value
|
|
||||||
// Second, check for absence of CTL characters
|
|
||||||
int crlf = 0;
|
|
||||||
for (int i = 0; i < name.Length; ++i)
|
|
||||||
{
|
|
||||||
char c = (char)(0x000000ff & (uint)name[i]);
|
|
||||||
switch (crlf)
|
|
||||||
{
|
|
||||||
case 0:
|
|
||||||
{
|
|
||||||
if (c == '\r')
|
|
||||||
{
|
|
||||||
crlf = 1;
|
|
||||||
}
|
|
||||||
else if (c == '\n')
|
|
||||||
{
|
|
||||||
// Technically this is bad HTTP. But it would be a breaking change to throw here.
|
|
||||||
// Is there an exploit?
|
|
||||||
crlf = 2;
|
|
||||||
}
|
|
||||||
else if (c == 127 || (c < ' ' && c != '\t'))
|
|
||||||
{
|
|
||||||
throw new ArgumentException("net_WebHeaderInvalidControlChars", nameof(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 1:
|
|
||||||
{
|
|
||||||
if (c == '\n')
|
|
||||||
{
|
|
||||||
crlf = 2;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
case 2:
|
|
||||||
{
|
|
||||||
if (c == ' ' || c == '\t')
|
|
||||||
{
|
|
||||||
crlf = 0;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (crlf != 0)
|
|
||||||
{
|
|
||||||
throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ReadOnlySpan<char> NormalizeIp(ReadOnlySpan<char> ip)
|
|
||||||
{
|
|
||||||
if (ip.Length != 0 && !ip.IsWhiteSpace())
|
|
||||||
{
|
|
||||||
// Handle ipv4 mapped to ipv6
|
|
||||||
const string srch = "::ffff:";
|
|
||||||
var index = ip.IndexOf(srch.AsSpan(), StringComparison.OrdinalIgnoreCase);
|
|
||||||
if (index == 0)
|
|
||||||
{
|
|
||||||
ip = ip.Slice(srch.Length);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ip;
|
return ip;
|
||||||
|
@ -312,97 +225,7 @@ namespace Emby.Server.Implementations.SocketSharp
|
||||||
return pos == -1 ? strVal : strVal.Slice(0, pos);
|
return pos == -1 ? strVal : strVal.Slice(0, pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string HandlerFactoryPath;
|
public string PathInfo => this.request.Path.Value;
|
||||||
|
|
||||||
private string pathInfo;
|
|
||||||
public string PathInfo
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (this.pathInfo == null)
|
|
||||||
{
|
|
||||||
var mode = HandlerFactoryPath;
|
|
||||||
|
|
||||||
var pos = RawUrl.IndexOf("?", StringComparison.Ordinal);
|
|
||||||
if (pos != -1)
|
|
||||||
{
|
|
||||||
var path = RawUrl.Substring(0, pos);
|
|
||||||
this.pathInfo = GetPathInfo(
|
|
||||||
path,
|
|
||||||
mode,
|
|
||||||
mode ?? string.Empty);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
this.pathInfo = RawUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.pathInfo = WebUtility.UrlDecode(pathInfo);
|
|
||||||
this.pathInfo = NormalizePathInfo(pathInfo, mode).ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.pathInfo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetPathInfo(string fullPath, string mode, string appPath)
|
|
||||||
{
|
|
||||||
var pathInfo = ResolvePathInfoFromMappedPath(fullPath, mode);
|
|
||||||
if (!string.IsNullOrEmpty(pathInfo))
|
|
||||||
{
|
|
||||||
return pathInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wildcard mode relies on this to work out the handlerPath
|
|
||||||
pathInfo = ResolvePathInfoFromMappedPath(fullPath, appPath);
|
|
||||||
if (!string.IsNullOrEmpty(pathInfo))
|
|
||||||
{
|
|
||||||
return pathInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
return fullPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ResolvePathInfoFromMappedPath(string fullPath, string mappedPathRoot)
|
|
||||||
{
|
|
||||||
if (mappedPathRoot == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sbPathInfo = new StringBuilder();
|
|
||||||
var fullPathParts = fullPath.Split('/');
|
|
||||||
var mappedPathRootParts = mappedPathRoot.Split('/');
|
|
||||||
var fullPathIndexOffset = mappedPathRootParts.Length - 1;
|
|
||||||
var pathRootFound = false;
|
|
||||||
|
|
||||||
for (var fullPathIndex = 0; fullPathIndex < fullPathParts.Length; fullPathIndex++)
|
|
||||||
{
|
|
||||||
if (pathRootFound)
|
|
||||||
{
|
|
||||||
sbPathInfo.Append("/" + fullPathParts[fullPathIndex]);
|
|
||||||
}
|
|
||||||
else if (fullPathIndex - fullPathIndexOffset >= 0)
|
|
||||||
{
|
|
||||||
pathRootFound = true;
|
|
||||||
for (var mappedPathRootIndex = 0; mappedPathRootIndex < mappedPathRootParts.Length; mappedPathRootIndex++)
|
|
||||||
{
|
|
||||||
if (!string.Equals(fullPathParts[fullPathIndex - fullPathIndexOffset + mappedPathRootIndex], mappedPathRootParts[mappedPathRootIndex], StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
pathRootFound = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!pathRootFound)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return sbPathInfo.Length > 1 ? sbPathInfo.ToString().TrimEnd('/') : "/";
|
|
||||||
}
|
|
||||||
|
|
||||||
public string UserAgent => request.Headers[HeaderNames.UserAgent];
|
public string UserAgent => request.Headers[HeaderNames.UserAgent];
|
||||||
|
|
||||||
|
@ -501,19 +324,5 @@ namespace Emby.Server.Implementations.SocketSharp
|
||||||
return httpFiles;
|
return httpFiles;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ReadOnlySpan<char> NormalizePathInfo(string pathInfo, string handlerPath)
|
|
||||||
{
|
|
||||||
if (handlerPath != null)
|
|
||||||
{
|
|
||||||
var trimmed = pathInfo.AsSpan().TrimStart('/');
|
|
||||||
if (trimmed.StartsWith(handlerPath.AsSpan(), StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return trimmed.Slice(handlerPath.Length).ToString().AsSpan();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return pathInfo.AsSpan();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,8 +41,6 @@ namespace Emby.Server.Implementations.Udp
|
||||||
_socketFactory = socketFactory;
|
_socketFactory = socketFactory;
|
||||||
|
|
||||||
AddMessageResponder("who is JellyfinServer?", true, RespondToV2Message);
|
AddMessageResponder("who is JellyfinServer?", true, RespondToV2Message);
|
||||||
AddMessageResponder("who is EmbyServer?", true, RespondToV2Message);
|
|
||||||
AddMessageResponder("who is MediaBrowserServer_v2?", false, RespondToV2Message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddMessageResponder(string message, bool isSubstring, Func<string, IpEndPointInfo, Encoding, CancellationToken, Task> responder)
|
private void AddMessageResponder(string message, bool isSubstring, Func<string, IpEndPointInfo, Encoding, CancellationToken, Task> responder)
|
||||||
|
|
|
@ -12,7 +12,6 @@ using MediaBrowser.Common.Plugins;
|
||||||
using MediaBrowser.Common.Progress;
|
using MediaBrowser.Common.Progress;
|
||||||
using MediaBrowser.Common.Updates;
|
using MediaBrowser.Common.Updates;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Model.Cryptography;
|
|
||||||
using MediaBrowser.Model.Events;
|
using MediaBrowser.Model.Events;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Model.Serialization;
|
using MediaBrowser.Model.Serialization;
|
||||||
|
@ -39,11 +38,10 @@ namespace Emby.Server.Implementations.Updates
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The completed installations
|
/// The completed installations
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private ConcurrentBag<InstallationInfo> CompletedInstallationsInternal { get; set; }
|
private ConcurrentBag<InstallationInfo> _completedInstallationsInternal;
|
||||||
|
|
||||||
public IEnumerable<InstallationInfo> CompletedInstallations => CompletedInstallationsInternal;
|
public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal;
|
||||||
|
|
||||||
#region PluginUninstalled Event
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Occurs when [plugin uninstalled].
|
/// Occurs when [plugin uninstalled].
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -57,9 +55,7 @@ namespace Emby.Server.Implementations.Updates
|
||||||
{
|
{
|
||||||
PluginUninstalled?.Invoke(this, new GenericEventArgs<IPlugin> { Argument = plugin });
|
PluginUninstalled?.Invoke(this, new GenericEventArgs<IPlugin> { Argument = plugin });
|
||||||
}
|
}
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region PluginUpdated Event
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Occurs when [plugin updated].
|
/// Occurs when [plugin updated].
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -77,9 +73,7 @@ namespace Emby.Server.Implementations.Updates
|
||||||
|
|
||||||
_applicationHost.NotifyPendingRestart();
|
_applicationHost.NotifyPendingRestart();
|
||||||
}
|
}
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region PluginInstalled Event
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Occurs when [plugin updated].
|
/// Occurs when [plugin updated].
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -96,7 +90,6 @@ namespace Emby.Server.Implementations.Updates
|
||||||
|
|
||||||
_applicationHost.NotifyPendingRestart();
|
_applicationHost.NotifyPendingRestart();
|
||||||
}
|
}
|
||||||
#endregion
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The _logger
|
/// The _logger
|
||||||
|
@ -115,12 +108,8 @@ namespace Emby.Server.Implementations.Updates
|
||||||
/// <value>The application host.</value>
|
/// <value>The application host.</value>
|
||||||
private readonly IApplicationHost _applicationHost;
|
private readonly IApplicationHost _applicationHost;
|
||||||
|
|
||||||
private readonly ICryptoProvider _cryptographyProvider;
|
|
||||||
private readonly IZipClient _zipClient;
|
private readonly IZipClient _zipClient;
|
||||||
|
|
||||||
// netframework or netcore
|
|
||||||
private readonly string _packageRuntime;
|
|
||||||
|
|
||||||
public InstallationManager(
|
public InstallationManager(
|
||||||
ILoggerFactory loggerFactory,
|
ILoggerFactory loggerFactory,
|
||||||
IApplicationHost appHost,
|
IApplicationHost appHost,
|
||||||
|
@ -129,9 +118,7 @@ namespace Emby.Server.Implementations.Updates
|
||||||
IJsonSerializer jsonSerializer,
|
IJsonSerializer jsonSerializer,
|
||||||
IServerConfigurationManager config,
|
IServerConfigurationManager config,
|
||||||
IFileSystem fileSystem,
|
IFileSystem fileSystem,
|
||||||
ICryptoProvider cryptographyProvider,
|
IZipClient zipClient)
|
||||||
IZipClient zipClient,
|
|
||||||
string packageRuntime)
|
|
||||||
{
|
{
|
||||||
if (loggerFactory == null)
|
if (loggerFactory == null)
|
||||||
{
|
{
|
||||||
|
@ -139,18 +126,16 @@ namespace Emby.Server.Implementations.Updates
|
||||||
}
|
}
|
||||||
|
|
||||||
CurrentInstallations = new List<Tuple<InstallationInfo, CancellationTokenSource>>();
|
CurrentInstallations = new List<Tuple<InstallationInfo, CancellationTokenSource>>();
|
||||||
CompletedInstallationsInternal = new ConcurrentBag<InstallationInfo>();
|
_completedInstallationsInternal = new ConcurrentBag<InstallationInfo>();
|
||||||
|
|
||||||
|
_logger = loggerFactory.CreateLogger(nameof(InstallationManager));
|
||||||
_applicationHost = appHost;
|
_applicationHost = appHost;
|
||||||
_appPaths = appPaths;
|
_appPaths = appPaths;
|
||||||
_httpClient = httpClient;
|
_httpClient = httpClient;
|
||||||
_jsonSerializer = jsonSerializer;
|
_jsonSerializer = jsonSerializer;
|
||||||
_config = config;
|
_config = config;
|
||||||
_fileSystem = fileSystem;
|
_fileSystem = fileSystem;
|
||||||
_cryptographyProvider = cryptographyProvider;
|
|
||||||
_zipClient = zipClient;
|
_zipClient = zipClient;
|
||||||
_packageRuntime = packageRuntime;
|
|
||||||
_logger = loggerFactory.CreateLogger(nameof(InstallationManager));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Version GetPackageVersion(PackageVersionInfo version)
|
private static Version GetPackageVersion(PackageVersionInfo version)
|
||||||
|
@ -222,11 +207,6 @@ namespace Emby.Server.Implementations.Updates
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(version.runtimes) || version.runtimes.IndexOf(_packageRuntime, StringComparison.OrdinalIgnoreCase) == -1)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
versions.Add(version);
|
versions.Add(version);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -448,7 +428,7 @@ namespace Emby.Server.Implementations.Updates
|
||||||
CurrentInstallations.Remove(tuple);
|
CurrentInstallations.Remove(tuple);
|
||||||
}
|
}
|
||||||
|
|
||||||
CompletedInstallationsInternal.Add(installationInfo);
|
_completedInstallationsInternal.Add(installationInfo);
|
||||||
|
|
||||||
PackageInstallationCompleted?.Invoke(this, installationEventArgs);
|
PackageInstallationCompleted?.Invoke(this, installationEventArgs);
|
||||||
}
|
}
|
||||||
|
@ -529,6 +509,8 @@ namespace Emby.Server.Implementations.Updates
|
||||||
|
|
||||||
private async Task PerformPackageInstallation(IProgress<double> progress, string target, PackageVersionInfo package, CancellationToken cancellationToken)
|
private async Task PerformPackageInstallation(IProgress<double> progress, string target, PackageVersionInfo package, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
// TODO: Remove the `string target` argument as it is not used any longer
|
||||||
|
|
||||||
var extension = Path.GetExtension(package.targetFilename);
|
var extension = Path.GetExtension(package.targetFilename);
|
||||||
var isArchive = string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase);
|
var isArchive = string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
@ -538,12 +520,12 @@ namespace Emby.Server.Implementations.Updates
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (target == null)
|
// Always override the passed-in target (which is a file) and figure it out again
|
||||||
{
|
target = Path.Combine(_appPaths.PluginsPath, package.name);
|
||||||
target = Path.Combine(_appPaths.PluginsPath, Path.GetFileNameWithoutExtension(package.targetFilename));
|
_logger.LogDebug("Installing plugin to {Filename}.", target);
|
||||||
}
|
|
||||||
|
|
||||||
// Download to temporary file so that, if interrupted, it won't destroy the existing installation
|
// Download to temporary file so that, if interrupted, it won't destroy the existing installation
|
||||||
|
_logger.LogDebug("Downloading ZIP.");
|
||||||
var tempFile = await _httpClient.GetTempFile(new HttpRequestOptions
|
var tempFile = await _httpClient.GetTempFile(new HttpRequestOptions
|
||||||
{
|
{
|
||||||
Url = package.sourceUrl,
|
Url = package.sourceUrl,
|
||||||
|
@ -556,9 +538,17 @@ namespace Emby.Server.Implementations.Updates
|
||||||
|
|
||||||
// TODO: Validate with a checksum, *properly*
|
// TODO: Validate with a checksum, *properly*
|
||||||
|
|
||||||
|
// Check if the target directory already exists, and remove it if so
|
||||||
|
if (Directory.Exists(target))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Deleting existing plugin at {Filename}.", target);
|
||||||
|
Directory.Delete(target, true);
|
||||||
|
}
|
||||||
|
|
||||||
// Success - move it to the real target
|
// Success - move it to the real target
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
_logger.LogDebug("Extracting ZIP {TempFile} to {Filename}.", tempFile, target);
|
||||||
using (var stream = File.OpenRead(tempFile))
|
using (var stream = File.OpenRead(tempFile))
|
||||||
{
|
{
|
||||||
_zipClient.ExtractAllFromZip(stream, target, true);
|
_zipClient.ExtractAllFromZip(stream, target, true);
|
||||||
|
@ -572,6 +562,7 @@ namespace Emby.Server.Implementations.Updates
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
_logger.LogDebug("Deleting temporary file {Filename}.", tempFile);
|
||||||
_fileSystem.DeleteFile(tempFile);
|
_fileSystem.DeleteFile(tempFile);
|
||||||
}
|
}
|
||||||
catch (IOException ex)
|
catch (IOException ex)
|
||||||
|
@ -594,7 +585,13 @@ namespace Emby.Server.Implementations.Updates
|
||||||
_applicationHost.RemovePlugin(plugin);
|
_applicationHost.RemovePlugin(plugin);
|
||||||
|
|
||||||
var path = plugin.AssemblyFilePath;
|
var path = plugin.AssemblyFilePath;
|
||||||
_logger.LogInformation("Deleting plugin file {0}", path);
|
bool isDirectory = false;
|
||||||
|
// Check if we have a plugin directory we should remove too
|
||||||
|
if (Path.GetDirectoryName(plugin.AssemblyFilePath) != _appPaths.PluginsPath)
|
||||||
|
{
|
||||||
|
path = Path.GetDirectoryName(plugin.AssemblyFilePath);
|
||||||
|
isDirectory = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Make this case-insensitive to account for possible incorrect assembly naming
|
// Make this case-insensitive to account for possible incorrect assembly naming
|
||||||
var file = _fileSystem.GetFilePaths(Path.GetDirectoryName(path))
|
var file = _fileSystem.GetFilePaths(Path.GetDirectoryName(path))
|
||||||
|
@ -605,7 +602,16 @@ namespace Emby.Server.Implementations.Updates
|
||||||
path = file;
|
path = file;
|
||||||
}
|
}
|
||||||
|
|
||||||
_fileSystem.DeleteFile(path);
|
if (isDirectory)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Deleting plugin directory {0}", path);
|
||||||
|
Directory.Delete(path, true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Deleting plugin file {0}", path);
|
||||||
|
_fileSystem.DeleteFile(path);
|
||||||
|
}
|
||||||
|
|
||||||
var list = _config.Configuration.UninstalledPlugins.ToList();
|
var list = _config.Configuration.UninstalledPlugins.ToList();
|
||||||
var filename = Path.GetFileName(path);
|
var filename = Path.GetFileName(path);
|
||||||
|
|
|
@ -19,7 +19,6 @@ using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
using MediaBrowser.Model.Globalization;
|
using MediaBrowser.Model.Globalization;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
@ -119,6 +118,10 @@ namespace Jellyfin.Server
|
||||||
|
|
||||||
SQLitePCL.Batteries_V2.Init();
|
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);
|
||||||
|
|
||||||
// Allow all https requests
|
// Allow all https requests
|
||||||
ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(delegate { return true; });
|
ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(delegate { return true; });
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,8 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Api.Movies;
|
using MediaBrowser.Api.Movies;
|
||||||
|
@ -828,7 +830,16 @@ namespace MediaBrowser.Api.Library
|
||||||
var filename = (Path.GetFileName(path) ?? string.Empty).Replace("\"", string.Empty);
|
var filename = (Path.GetFileName(path) ?? string.Empty).Replace("\"", string.Empty);
|
||||||
if (!string.IsNullOrWhiteSpace(filename))
|
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
|
return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
@ -13,6 +14,7 @@ using MediaBrowser.Controller.Net;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Model.MediaInfo;
|
using MediaBrowser.Model.MediaInfo;
|
||||||
using MediaBrowser.Model.Serialization;
|
using MediaBrowser.Model.Serialization;
|
||||||
|
using MediaBrowser.Model.Services;
|
||||||
using Microsoft.Net.Http.Headers;
|
using Microsoft.Net.Http.Headers;
|
||||||
|
|
||||||
namespace MediaBrowser.Api.Playback.Progressive
|
namespace MediaBrowser.Api.Playback.Progressive
|
||||||
|
@ -279,10 +281,7 @@ namespace MediaBrowser.Api.Playback.Progressive
|
||||||
/// <returns>Task{System.Object}.</returns>
|
/// <returns>Task{System.Object}.</returns>
|
||||||
private async Task<object> GetStaticRemoteStreamResult(StreamState state, Dictionary<string, string> responseHeaders, bool isHeadRequest, CancellationTokenSource cancellationTokenSource)
|
private async Task<object> GetStaticRemoteStreamResult(StreamState state, Dictionary<string, string> responseHeaders, bool isHeadRequest, CancellationTokenSource cancellationTokenSource)
|
||||||
{
|
{
|
||||||
string useragent = null;
|
state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent);
|
||||||
state.RemoteHttpHeaders.TryGetValue("User-Agent", out useragent);
|
|
||||||
|
|
||||||
var trySupportSeek = false;
|
|
||||||
|
|
||||||
var options = new HttpRequestOptions
|
var options = new HttpRequestOptions
|
||||||
{
|
{
|
||||||
|
@ -292,29 +291,14 @@ namespace MediaBrowser.Api.Playback.Progressive
|
||||||
CancellationToken = cancellationTokenSource.Token
|
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);
|
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 })
|
responseHeaders[HeaderNames.ContentLength] = response.ContentLength.Value.ToString(CultureInfo.InvariantCulture);
|
||||||
{
|
|
||||||
var val = response.Headers[name];
|
|
||||||
if (!string.IsNullOrWhiteSpace(val))
|
|
||||||
{
|
|
||||||
responseHeaders[name] = val;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
responseHeaders[HeaderNames.AcceptRanges] = "none";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isHeadRequest)
|
if (isHeadRequest)
|
||||||
|
@ -356,10 +340,31 @@ namespace MediaBrowser.Api.Playback.Progressive
|
||||||
var contentType = state.GetMimeType(outputPath);
|
var contentType = state.GetMimeType(outputPath);
|
||||||
|
|
||||||
// TODO: The isHeadRequest is only here because ServiceStack will add Content-Length=0 to the response
|
// 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
|
// Headers only
|
||||||
if (isHeadRequest)
|
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);
|
var transcodingLock = ApiEntryPoint.Instance.GetTranscodingLock(outputPath);
|
||||||
|
@ -397,5 +402,22 @@ namespace MediaBrowser.Api.Playback.Progressive
|
||||||
transcodingLock.Release();
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -245,6 +245,12 @@ namespace MediaBrowser.Api.Session
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Route("/Auth/PasswordResetProviders", "GET")]
|
||||||
|
[Authenticated(Roles = "Admin")]
|
||||||
|
public class GetPasswordResetProviders : IReturn<NameIdPair[]>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
[Route("/Auth/Keys/{Key}", "DELETE")]
|
[Route("/Auth/Keys/{Key}", "DELETE")]
|
||||||
[Authenticated(Roles = "Admin")]
|
[Authenticated(Roles = "Admin")]
|
||||||
public class RevokeKey
|
public class RevokeKey
|
||||||
|
@ -294,6 +300,11 @@ namespace MediaBrowser.Api.Session
|
||||||
return _userManager.GetAuthenticationProviders();
|
return _userManager.GetAuthenticationProviders();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public object Get(GetPasswordResetProviders request)
|
||||||
|
{
|
||||||
|
return _userManager.GetPasswordResetProviders();
|
||||||
|
}
|
||||||
|
|
||||||
public void Delete(RevokeKey request)
|
public void Delete(RevokeKey request)
|
||||||
{
|
{
|
||||||
_sessionManager.RevokeToken(request.Key);
|
_sessionManager.RevokeToken(request.Key);
|
||||||
|
|
|
@ -379,10 +379,15 @@ namespace MediaBrowser.Api
|
||||||
throw new ResourceNotFoundException("User not found");
|
throw new ResourceNotFoundException("User not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(request.Password) && string.IsNullOrEmpty(request.Pw))
|
||||||
|
{
|
||||||
|
throw new MethodNotAllowedException("Hashed-only passwords are not valid for this API.");
|
||||||
|
}
|
||||||
|
|
||||||
return Post(new AuthenticateUserByName
|
return Post(new AuthenticateUserByName
|
||||||
{
|
{
|
||||||
Username = user.Name,
|
Username = user.Name,
|
||||||
Password = request.Password,
|
Password = null, // This should always be null
|
||||||
Pw = request.Pw
|
Pw = request.Pw
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using MediaBrowser.Model.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
namespace MediaBrowser.Common.Extensions
|
namespace MediaBrowser.Common.Extensions
|
||||||
{
|
{
|
||||||
|
@ -9,8 +10,6 @@ namespace MediaBrowser.Common.Extensions
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class BaseExtensions
|
public static class BaseExtensions
|
||||||
{
|
{
|
||||||
public static ICryptoProvider CryptographyProvider { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Strips the HTML.
|
/// Strips the HTML.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -31,7 +30,10 @@ namespace MediaBrowser.Common.Extensions
|
||||||
/// <returns>Guid.</returns>
|
/// <returns>Guid.</returns>
|
||||||
public static Guid GetMD5(this string str)
|
public static Guid GetMD5(this string str)
|
||||||
{
|
{
|
||||||
return CryptographyProvider.GetMD5(str);
|
using (var provider = MD5.Create())
|
||||||
|
{
|
||||||
|
return new Guid(provider.ComputeHash(Encoding.Unicode.GetBytes(str)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,30 @@ namespace MediaBrowser.Common.Extensions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Class MethodNotAllowedException
|
||||||
|
/// </summary>
|
||||||
|
public class MethodNotAllowedException : Exception
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="MethodNotAllowedException" /> class.
|
||||||
|
/// </summary>
|
||||||
|
public MethodNotAllowedException()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="MethodNotAllowedException" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">The message.</param>
|
||||||
|
public MethodNotAllowedException(string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public class RemoteServiceUnavailableException : Exception
|
public class RemoteServiceUnavailableException : Exception
|
||||||
{
|
{
|
||||||
public RemoteServiceUnavailableException()
|
public RemoteServiceUnavailableException()
|
||||||
|
|
|
@ -25,11 +25,6 @@ namespace MediaBrowser.Common
|
||||||
/// <value>The device identifier.</value>
|
/// <value>The device identifier.</value>
|
||||||
string SystemId { get; }
|
string SystemId { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Occurs when [application updated].
|
|
||||||
/// </summary>
|
|
||||||
event EventHandler<GenericEventArgs<PackageVersionInfo>> ApplicationUpdated;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether this instance has pending kernel reload.
|
/// Gets or sets a value indicating whether this instance has pending kernel reload.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
11
MediaBrowser.Common/Net/CustomHeaderNames.cs
Normal file
11
MediaBrowser.Common/Net/CustomHeaderNames.cs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
namespace MediaBrowser.Common.Net
|
||||||
|
{
|
||||||
|
public static class CustomHeaderNames
|
||||||
|
{
|
||||||
|
// Other Headers
|
||||||
|
public const string XForwardedFor = "X-Forwarded-For";
|
||||||
|
public const string XForwardedPort = "X-Forwarded-Port";
|
||||||
|
public const string XForwardedProto = "X-Forwarded-Proto";
|
||||||
|
public const string XRealIP = "X-Real-IP";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MediaBrowser.Controller.Entities;
|
||||||
|
using MediaBrowser.Model.Users;
|
||||||
|
|
||||||
|
namespace MediaBrowser.Controller.Authentication
|
||||||
|
{
|
||||||
|
public interface IPasswordResetProvider
|
||||||
|
{
|
||||||
|
string Name { get; }
|
||||||
|
bool IsEnabled { get; }
|
||||||
|
Task<ForgotPasswordResult> StartForgotPasswordProcess(User user, bool isInNetwork);
|
||||||
|
Task<PinRedeemResult> RedeemPasswordResetPin(string pin);
|
||||||
|
}
|
||||||
|
public class PasswordPinCreationResult
|
||||||
|
{
|
||||||
|
public string PinFile { get; set; }
|
||||||
|
public DateTime ExpirationDate { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -78,10 +78,25 @@ namespace MediaBrowser.Controller.Entities
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The trailer folder name
|
/// The trailer folder name
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string TrailerFolderName = "trailers";
|
public const string TrailerFolderName = "trailers";
|
||||||
public static string ThemeSongsFolderName = "theme-music";
|
public const string ThemeSongsFolderName = "theme-music";
|
||||||
public static string ThemeSongFilename = "theme";
|
public const string ThemeSongFilename = "theme";
|
||||||
public static string ThemeVideosFolderName = "backdrops";
|
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]
|
[IgnoreDataMember]
|
||||||
public Guid[] ThemeSongIds { get; set; }
|
public Guid[] ThemeSongIds { get; set; }
|
||||||
|
@ -1276,16 +1291,15 @@ namespace MediaBrowser.Controller.Entities
|
||||||
.Select(item =>
|
.Select(item =>
|
||||||
{
|
{
|
||||||
// Try to retrieve it from the db. If we don't find it, use the resolved version
|
// 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;
|
item = dbItem;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// item is new
|
// item is new
|
||||||
item.ExtraType = MediaBrowser.Model.Entities.ExtraType.ThemeVideo;
|
item.ExtraType = Model.Entities.ExtraType.ThemeVideo;
|
||||||
}
|
}
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
|
@ -1296,33 +1310,38 @@ namespace MediaBrowser.Controller.Entities
|
||||||
|
|
||||||
protected virtual BaseItem[] LoadExtras(List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
|
protected virtual BaseItem[] LoadExtras(List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
|
||||||
{
|
{
|
||||||
var files = fileSystemChildren.Where(i => i.IsDirectory)
|
var extras = new List<Video>();
|
||||||
.SelectMany(i => FileSystem.GetFiles(i.FullName));
|
|
||||||
|
|
||||||
return LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions())
|
var folders = fileSystemChildren.Where(i => i.IsDirectory).ToArray();
|
||||||
.OfType<Video>()
|
foreach (var extraFolderName in AllExtrasTypesFolderNames)
|
||||||
.Select(item =>
|
{
|
||||||
{
|
var files = folders
|
||||||
// Try to retrieve it from the db. If we don't find it, use the resolved version
|
.Where(i => string.Equals(i.Name, extraFolderName, StringComparison.OrdinalIgnoreCase))
|
||||||
var dbItem = LibraryManager.GetItemById(item.Id) as Video;
|
.SelectMany(i => FileSystem.GetFiles(i.FullName));
|
||||||
|
|
||||||
if (dbItem != null)
|
extras.AddRange(LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions())
|
||||||
|
.OfType<Video>()
|
||||||
|
.Select(item =>
|
||||||
{
|
{
|
||||||
item = dbItem;
|
// 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)
|
||||||
else
|
{
|
||||||
{
|
item = dbItem;
|
||||||
// item is new
|
}
|
||||||
item.ExtraType = MediaBrowser.Model.Entities.ExtraType.Clip;
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
return item;
|
||||||
}).OrderBy(i => i.Path).ToArray();
|
|
||||||
|
// Sort them so that the list can be easily compared for changes
|
||||||
|
}).OrderBy(i => i.Path));
|
||||||
|
}
|
||||||
|
|
||||||
|
return extras.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public Task RefreshMetadata(CancellationToken cancellationToken)
|
public Task RefreshMetadata(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem)), 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)
|
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();
|
var newExtraIds = newExtras.Select(i => i.Id).ToArray();
|
||||||
|
|
||||||
|
@ -1493,7 +1518,15 @@ namespace MediaBrowser.Controller.Entities
|
||||||
|
|
||||||
var tasks = newExtras.Select(i =>
|
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);
|
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||||
|
|
|
@ -64,21 +64,31 @@ namespace MediaBrowser.Controller.Entities
|
||||||
where T : BaseItem
|
where T : BaseItem
|
||||||
where TU : BaseItem
|
where TU : BaseItem
|
||||||
{
|
{
|
||||||
var sourceProps = typeof(T).GetProperties().Where(x => x.CanRead).ToList();
|
var destProps = typeof(TU).GetProperties().Where(x => x.CanWrite).ToList();
|
||||||
var destProps = typeof(TU).GetProperties()
|
|
||||||
.Where(x => x.CanWrite)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
foreach (var sourceProp in sourceProps)
|
foreach (var sourceProp in typeof(T).GetProperties())
|
||||||
{
|
{
|
||||||
if (destProps.Any(x => x.Name == sourceProp.Name))
|
// We should be able to write to the property
|
||||||
|
// for both the source and destination type
|
||||||
|
// This is only false when the derived type hides the base member
|
||||||
|
// (which we shouldn't copy anyway)
|
||||||
|
if (!sourceProp.CanRead || !sourceProp.CanWrite)
|
||||||
{
|
{
|
||||||
var p = destProps.First(x => x.Name == sourceProp.Name);
|
continue;
|
||||||
p.SetValue(dest, sourceProp.GetValue(source, null), null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
var v = sourceProp.GetValue(source);
|
||||||
|
if (v == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var p = destProps.Find(x => x.Name == sourceProp.Name);
|
||||||
|
if (p != null)
|
||||||
|
{
|
||||||
|
p.SetValue(dest, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -93,7 +103,5 @@ namespace MediaBrowser.Controller.Entities
|
||||||
source.DeepCopy(dest);
|
source.DeepCopy(dest);
|
||||||
return dest;
|
return dest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,8 +83,6 @@ namespace MediaBrowser.Controller
|
||||||
|
|
||||||
void EnableLoopback(string appName);
|
void EnableLoopback(string appName);
|
||||||
|
|
||||||
string PackageRuntime { get; }
|
|
||||||
|
|
||||||
WakeOnLanInfo[] GetWakeOnLanInfo();
|
WakeOnLanInfo[] GetWakeOnLanInfo();
|
||||||
|
|
||||||
string ExpandVirtualPath(string path);
|
string ExpandVirtualPath(string path);
|
||||||
|
|
|
@ -200,8 +200,9 @@ namespace MediaBrowser.Controller.Library
|
||||||
/// <returns>System.String.</returns>
|
/// <returns>System.String.</returns>
|
||||||
string MakeValidUsername(string username);
|
string MakeValidUsername(string username);
|
||||||
|
|
||||||
void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders);
|
void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders, IEnumerable<IPasswordResetProvider> passwordResetProviders);
|
||||||
|
|
||||||
NameIdPair[] GetAuthenticationProviders();
|
NameIdPair[] GetAuthenticationProviders();
|
||||||
|
NameIdPair[] GetPasswordResetProviders();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||||
private readonly int DefaultImageExtractionTimeoutMs;
|
private readonly int DefaultImageExtractionTimeoutMs;
|
||||||
private readonly string StartupOptionFFmpegPath;
|
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 List<ProcessWrapper> _runningProcesses = new List<ProcessWrapper>();
|
||||||
private readonly ILocalizationManager _localization;
|
private readonly ILocalizationManager _localization;
|
||||||
|
|
||||||
|
@ -230,6 +230,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
private string ExistsOnSystemPath(string filename)
|
private string ExistsOnSystemPath(string filename)
|
||||||
{
|
{
|
||||||
|
string inJellyfinPath = GetEncoderPathFromDirectory(System.AppContext.BaseDirectory, filename);
|
||||||
|
if (!string.IsNullOrEmpty(inJellyfinPath))
|
||||||
|
{
|
||||||
|
return inJellyfinPath;
|
||||||
|
}
|
||||||
var values = Environment.GetEnvironmentVariable("PATH");
|
var values = Environment.GetEnvironmentVariable("PATH");
|
||||||
|
|
||||||
foreach (var path in values.Split(Path.PathSeparator))
|
foreach (var path in values.Split(Path.PathSeparator))
|
||||||
|
@ -577,19 +582,27 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||||
{
|
{
|
||||||
bool ranToCompletion;
|
bool ranToCompletion;
|
||||||
|
|
||||||
StartProcess(processWrapper);
|
await _thumbnailResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
try
|
||||||
var timeoutMs = ConfigurationManager.Configuration.ImageExtractionTimeoutMs;
|
|
||||||
if (timeoutMs <= 0)
|
|
||||||
{
|
{
|
||||||
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
ranToCompletion = await process.WaitForExitAsync(timeoutMs).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (!ranToCompletion)
|
|
||||||
{
|
{
|
||||||
StopProcess(processWrapper, 1000);
|
_thumbnailResourcePool.Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
|
var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
|
||||||
|
@ -620,7 +633,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||||
return time.ToString(@"hh\:mm\:ss\.fff", UsCulture);
|
return time.ToString(@"hh\:mm\:ss\.fff", UsCulture);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ExtractVideoImagesOnInterval(string[] inputFiles,
|
public async Task ExtractVideoImagesOnInterval(
|
||||||
|
string[] inputFiles,
|
||||||
string container,
|
string container,
|
||||||
MediaStream videoStream,
|
MediaStream videoStream,
|
||||||
MediaProtocol protocol,
|
MediaProtocol protocol,
|
||||||
|
@ -631,8 +645,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||||
int? maxWidth,
|
int? maxWidth,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var resourcePool = _thumbnailResourcePool;
|
|
||||||
|
|
||||||
var inputArgument = GetInputArgument(inputFiles, protocol);
|
var inputArgument = GetInputArgument(inputFiles, protocol);
|
||||||
|
|
||||||
var vf = "fps=fps=1/" + interval.TotalSeconds.ToString(UsCulture);
|
var vf = "fps=fps=1/" + interval.TotalSeconds.ToString(UsCulture);
|
||||||
|
@ -696,7 +708,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||||
|
|
||||||
_logger.LogInformation(process.StartInfo.FileName + " " + process.StartInfo.Arguments);
|
_logger.LogInformation(process.StartInfo.FileName + " " + process.StartInfo.Arguments);
|
||||||
|
|
||||||
await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
|
await _thumbnailResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
bool ranToCompletion = false;
|
bool ranToCompletion = false;
|
||||||
|
|
||||||
|
@ -737,7 +749,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
resourcePool.Release();
|
_thumbnailResourcePool.Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
|
var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
|
||||||
|
|
|
@ -7,26 +7,6 @@ namespace MediaBrowser.Model.Services
|
||||||
/// </summary>
|
/// </summary>
|
||||||
string HttpMethod { get; }
|
string HttpMethod { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The IP Address of the X-Forwarded-For header, null if null or empty
|
|
||||||
/// </summary>
|
|
||||||
string XForwardedFor { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The Port number of the X-Forwarded-Port header, null if null or empty
|
|
||||||
/// </summary>
|
|
||||||
int? XForwardedPort { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The http or https scheme of the X-Forwarded-Proto header, null if null or empty
|
|
||||||
/// </summary>
|
|
||||||
string XForwardedProtocol { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The value of the X-Real-IP header, null if null or empty
|
|
||||||
/// </summary>
|
|
||||||
string XRealIp { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The value of the Accept HTTP Request Header
|
/// The value of the Accept HTTP Request Header
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -26,6 +26,11 @@ namespace MediaBrowser.Model.System
|
||||||
/// <value>The version.</value>
|
/// <value>The version.</value>
|
||||||
public string Version { get; set; }
|
public string Version { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The product name. This is the AssemblyProduct name.
|
||||||
|
/// </summary>
|
||||||
|
public string ProductName { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the operating system.
|
/// Gets or sets the operating system.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -32,10 +32,6 @@ namespace MediaBrowser.Model.System
|
||||||
/// <value>The display name of the operating system.</value>
|
/// <value>The display name of the operating system.</value>
|
||||||
public string OperatingSystemDisplayName { get; set; }
|
public string OperatingSystemDisplayName { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The product name. This is the AssemblyProduct name.
|
|
||||||
/// </summary>
|
|
||||||
public string ProductName { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get or sets the package name.
|
/// Get or sets the package name.
|
||||||
|
|
|
@ -75,6 +75,7 @@ namespace MediaBrowser.Model.Users
|
||||||
|
|
||||||
public int RemoteClientBitrateLimit { get; set; }
|
public int RemoteClientBitrateLimit { get; set; }
|
||||||
public string AuthenticationProviderId { get; set; }
|
public string AuthenticationProviderId { get; set; }
|
||||||
|
public string PasswordResetProviderId { get; set; }
|
||||||
|
|
||||||
public UserPolicy()
|
public UserPolicy()
|
||||||
{
|
{
|
||||||
|
|
|
@ -336,7 +336,7 @@ namespace MediaBrowser.Providers.Music
|
||||||
}
|
}
|
||||||
using (var subReader = reader.ReadSubtree())
|
using (var subReader = reader.ReadSubtree())
|
||||||
{
|
{
|
||||||
return ParseReleaseList(subReader);
|
return ParseReleaseList(subReader).ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -110,7 +110,7 @@ namespace MediaBrowser.Providers.Music
|
||||||
}
|
}
|
||||||
using (var subReader = reader.ReadSubtree())
|
using (var subReader = reader.ReadSubtree())
|
||||||
{
|
{
|
||||||
return ParseArtistList(subReader);
|
return ParseArtistList(subReader).ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -18,7 +18,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class RemoteEpisodeProvider
|
/// Class RemoteEpisodeProvider
|
||||||
/// </summary>
|
/// </summary>
|
||||||
class TvdbEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, IHasOrder
|
public class TvdbEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, IHasOrder
|
||||||
{
|
{
|
||||||
private readonly IHttpClient _httpClient;
|
private readonly IHttpClient _httpClient;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit ec5a3b6e5efb6041153b92818aee562f20ee994d
|
Subproject commit b0f7a9b67cc72de98dc357425e9d5c3894c7f377
|
|
@ -22,13 +22,13 @@
|
||||||
|
|
||||||
Jellyfin is a Free Software Media System that puts you in control of managing and streaming your media. It is an alternative to the proprietary Emby and Plex, to provide media from a dedicated server to end-user devices via multiple apps. Jellyfin is descended from Emby's 3.5.2 release and ported to the .NET Core framework to enable full cross-platform support. There are no strings attached, no premium licenses or features, and no hidden agendas: just a team who want to build something better and work together to achieve it. We welcome anyone who is interested in joining us in our quest!
|
Jellyfin is a Free Software Media System that puts you in control of managing and streaming your media. It is an alternative to the proprietary Emby and Plex, to provide media from a dedicated server to end-user devices via multiple apps. Jellyfin is descended from Emby's 3.5.2 release and ported to the .NET Core framework to enable full cross-platform support. There are no strings attached, no premium licenses or features, and no hidden agendas: just a team who want to build something better and work together to achieve it. We welcome anyone who is interested in joining us in our quest!
|
||||||
|
|
||||||
For further details, please see [our documentation page](https://jellyfin.readthedocs.io). To receive the latest updates, get help with Jellyfin, and join the community, please visit [one of our communication channels on Matrix/Riot or social media](https://jellyfin.readthedocs.io/en/latest/user-docs/getting-help).
|
For further details, please see [our documentation page](https://jellyfin.readthedocs.io). To receive the latest updates, get help with Jellyfin, and join the community, please visit [one of our communication channels on Matrix/Riot or social media](https://jellyfin.readthedocs.io/en/latest/getting-help/).
|
||||||
|
|
||||||
For more information about the project, please see our [about page](https://jellyfin.readthedocs.io/en/latest/about/).
|
For more information about the project, please see our [about page](https://jellyfin.readthedocs.io/en/latest/about/).
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<strong>Want to get started?</strong>
|
<strong>Want to get started?</strong>
|
||||||
<em>Choose from <a href="https://jellyfin.readthedocs.io/en/latest/administrator-docs/installing/">Prebuilt Packages</a> or <a href="https://jellyfin.readthedocs.io/en/latest/administrator-docs/building/">Build from Source</a>, then see our <a href="https://jellyfin.readthedocs.io/en/latest/administrator-docs/first-time/">first-time setup guide</a>.</em>
|
<em>Choose from <a href="https://jellyfin.readthedocs.io/en/latest/administrator-docs/installing/">Prebuilt Packages</a> or <a href="https://jellyfin.readthedocs.io/en/latest/administrator-docs/building/">Build from Source</a>, then see our <a href="https://jellyfin.readthedocs.io/en/latest/administrator-docs/quick-start/">quick start guide</a>.</em>
|
||||||
</p>
|
</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<strong>Want to contribute?</strong>
|
<strong>Want to contribute?</strong>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
[assembly: AssemblyVersion("10.2.2")]
|
[assembly: AssemblyVersion("10.3.3")]
|
||||||
[assembly: AssemblyFileVersion("10.2.2")]
|
[assembly: AssemblyFileVersion("10.3.3")]
|
||||||
|
|
2
build
2
build
|
@ -29,7 +29,7 @@ usage() {
|
||||||
echo -e "The web_branch defaults to the same branch name as the current main branch or can be 'local' to not touch the submodule branching."
|
echo -e "The web_branch defaults to the same branch name as the current main branch or can be 'local' to not touch the submodule branching."
|
||||||
echo -e "To build all platforms, use 'all'."
|
echo -e "To build all platforms, use 'all'."
|
||||||
echo -e "To perform all build actions, use 'all'."
|
echo -e "To perform all build actions, use 'all'."
|
||||||
echo -e "Build output files are collected at '../jellyfin-build/<platform>'."
|
echo -e "Build output files are collected at '../bin/<platform>'."
|
||||||
}
|
}
|
||||||
|
|
||||||
# Show usage on stderr with exit 1 on argless
|
# Show usage on stderr with exit 1 on argless
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
---
|
---
|
||||||
# We just wrap `build` so this is really it
|
# We just wrap `build` so this is really it
|
||||||
name: "jellyfin"
|
name: "jellyfin"
|
||||||
version: "10.2.2"
|
version: "10.3.3"
|
||||||
packages:
|
packages:
|
||||||
- debian-package-x64
|
- debian-package-x64
|
||||||
- debian-package-armhf
|
- debian-package-armhf
|
||||||
|
- debian-package-arm64
|
||||||
- ubuntu-package-x64
|
- ubuntu-package-x64
|
||||||
|
- ubuntu-package-armhf
|
||||||
|
- ubuntu-package-arm64
|
||||||
- fedora-package-x64
|
- fedora-package-x64
|
||||||
- centos-package-x64
|
- centos-package-x64
|
||||||
- linux-x64
|
- linux-x64
|
||||||
|
|
141
bump_version
141
bump_version
|
@ -9,7 +9,7 @@ usage() {
|
||||||
echo -e "bump_version - increase the shared version and generate changelogs"
|
echo -e "bump_version - increase the shared version and generate changelogs"
|
||||||
echo -e ""
|
echo -e ""
|
||||||
echo -e "Usage:"
|
echo -e "Usage:"
|
||||||
echo -e " $ bump_version [-b/--web-branch <web_branch>] <new_version>"
|
echo -e " $ bump_version <new_version>"
|
||||||
echo -e ""
|
echo -e ""
|
||||||
echo -e "The web_branch defaults to the same branch name as the current main branch."
|
echo -e "The web_branch defaults to the same branch name as the current main branch."
|
||||||
echo -e "This helps facilitate releases where both branches would be called release-X.Y.Z"
|
echo -e "This helps facilitate releases where both branches would be called release-X.Y.Z"
|
||||||
|
@ -22,14 +22,9 @@ if [[ -z $1 ]]; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
shared_version_file="./SharedVersion.cs"
|
shared_version_file="./SharedVersion.cs"
|
||||||
|
build_file="./build.yaml"
|
||||||
|
|
||||||
# Parse branch option
|
web_branch="$( git branch 2>/dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/\1/' )"
|
||||||
if [[ $1 == '-b' || $1 == '--web-branch' ]]; then
|
|
||||||
web_branch="$2"
|
|
||||||
shift 2
|
|
||||||
else
|
|
||||||
web_branch="$( git branch 2>/dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/\1/' )"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Initialize submodules
|
# Initialize submodules
|
||||||
git submodule update --init --recursive
|
git submodule update --init --recursive
|
||||||
|
@ -47,22 +42,11 @@ if ! git diff-index --quiet HEAD --; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
git fetch --all
|
git fetch --all
|
||||||
# If this is an official branch name, fetch it from origin
|
git checkout origin/${web_branch}
|
||||||
official_branches_regex="^master$|^dev$|^release-.*$|^hotfix-.*$"
|
|
||||||
if [[ ${web_branch} =~ ${official_branches_regex} ]]; then
|
|
||||||
git checkout origin/${web_branch} || {
|
|
||||||
echo "ERROR: 'jellyfin-web' branch 'origin/${web_branch}' is invalid."
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
# Otherwise, just check out the local branch (for testing, etc.)
|
|
||||||
else
|
|
||||||
git checkout ${web_branch} || {
|
|
||||||
echo "ERROR: 'jellyfin-web' branch '${web_branch}' is invalid."
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
fi
|
|
||||||
popd
|
popd
|
||||||
|
|
||||||
|
git add MediaBrowser.WebDashboard/jellyfin-web
|
||||||
|
|
||||||
new_version="$1"
|
new_version="$1"
|
||||||
|
|
||||||
# Parse the version from the AssemblyVersion
|
# Parse the version from the AssemblyVersion
|
||||||
|
@ -70,94 +54,47 @@ old_version="$(
|
||||||
grep "AssemblyVersion" ${shared_version_file} \
|
grep "AssemblyVersion" ${shared_version_file} \
|
||||||
| sed -E 's/\[assembly: ?AssemblyVersion\("([0-9\.]+)"\)\]/\1/'
|
| sed -E 's/\[assembly: ?AssemblyVersion\("([0-9\.]+)"\)\]/\1/'
|
||||||
)"
|
)"
|
||||||
|
echo $old_version
|
||||||
|
|
||||||
# Set the shared version to the specified new_version
|
# Set the shared version to the specified new_version
|
||||||
old_version_sed="$( sed 's/\./\\./g' <<<"${old_version}" )" # Escape the '.' chars
|
old_version_sed="$( sed 's/\./\\./g' <<<"${old_version}" )" # Escape the '.' chars
|
||||||
sed -i "s/${old_version_sed}/${new_version}/g" ${shared_version_file}
|
new_version_sed="$( cut -f1 -d'-' <<<"${new_version}" )"
|
||||||
|
sed -i "s/${old_version_sed}/${new_version_sed}/g" ${shared_version_file}
|
||||||
|
|
||||||
declare -a pr_merges_since_last_master
|
old_version="$(
|
||||||
declare changelog_string_github
|
grep "version:" ${build_file} \
|
||||||
declare changelog_string_deb
|
| sed -E 's/version: "([0-9\.]+[-a-z0-9]*)"/\1/'
|
||||||
declare changelog_string_yum
|
)"
|
||||||
|
echo $old_version
|
||||||
|
|
||||||
# Build up a changelog from merge commits
|
# Set the build.yaml version to the specified new_version
|
||||||
for repo in ./ MediaBrowser.WebDashboard/jellyfin-web/; do
|
old_version_sed="$( sed 's/\./\\./g' <<<"${old_version}" )" # Escape the '.' chars
|
||||||
last_master_merge_commit=""
|
sed -i "s/${old_version_sed}/${new_version}/g" ${build_file}
|
||||||
pr_merges_since_last_master=()
|
|
||||||
git_show_details=""
|
|
||||||
pull_request_id=""
|
|
||||||
pull_request_description=""
|
|
||||||
changelog_strings_repo_github=""
|
|
||||||
changelog_strings_repo_deb=""
|
|
||||||
changelog_strings_repo_yum=""
|
|
||||||
|
|
||||||
case $repo in
|
if [[ ${new_version} == *"-"* ]]; then
|
||||||
*jellyfin-web*)
|
new_version_deb="$( sed 's/-/~/g' <<<"${new_version}" )"
|
||||||
repo_name="jellyfin-web"
|
else
|
||||||
;;
|
new_version_deb="${new_version}-1"
|
||||||
*)
|
fi
|
||||||
repo_name="jellyfin"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
pushd ${repo}
|
# Set the Dockerfile web version to the specified new_version
|
||||||
|
old_version="$(
|
||||||
|
grep "JELLYFIN_WEB_VERSION=" Dockerfile \
|
||||||
|
| sed -E 's/ARG JELLYFIN_WEB_VERSION=([0-9\.]+[-a-z0-9]*)/\1/'
|
||||||
|
)"
|
||||||
|
echo $old_version
|
||||||
|
|
||||||
# Find the last release commit, so we know what's happened since
|
old_version_sed="$( sed 's/\./\\./g' <<<"${old_version}" )" # Escape the '.' chars
|
||||||
last_master_branch="release-${old_version}"
|
sed -i "s/${old_version_sed}/${new_version}/g" Dockerfile*
|
||||||
last_master_merge_commit="$(
|
|
||||||
git log --merges --pretty=oneline \
|
|
||||||
| grep -F "${last_master_branch}" \
|
|
||||||
| awk '{ print $1 }' \
|
|
||||||
|| true # Don't die here with errexit
|
|
||||||
)"
|
|
||||||
if [[ -z ${last_master_merge_commit} ]]; then
|
|
||||||
# This repo has no last proper commit, so just skip it
|
|
||||||
popd
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
# Get all the PR merge commits since the last master merge commit in `jellyfin`
|
|
||||||
pr_merges_since_last_master+=( $(
|
|
||||||
git log --merges --pretty=oneline ${last_master_merge_commit}..HEAD \
|
|
||||||
| grep -F "Merge pull request" \
|
|
||||||
| awk '{ print $1 }'
|
|
||||||
) )
|
|
||||||
|
|
||||||
for commit_hash in ${pr_merges_since_last_master[@]}; do
|
|
||||||
git_show_details="$( git show ${commit_hash} )"
|
|
||||||
pull_request_id="$(
|
|
||||||
awk '
|
|
||||||
/Merge pull request/{ print $4 }
|
|
||||||
{ next }
|
|
||||||
' <<<"${git_show_details}"
|
|
||||||
)"
|
|
||||||
pull_request_description="$(
|
|
||||||
awk '
|
|
||||||
/^[a-zA-Z]/{ next }
|
|
||||||
/^ Merge/{ next }
|
|
||||||
/^$/{ next }
|
|
||||||
{ print $0 }
|
|
||||||
' <<<"${git_show_details}"
|
|
||||||
)"
|
|
||||||
pull_request_description="$( sed ':a;N;$!ba;s/\n//g; s/ \+//g' <<<"${pull_request_description}" )"
|
|
||||||
changelog_strings_repo_github="${changelog_strings_repo_github}\n* ${pull_request_id}: ${pull_request_description}"
|
|
||||||
changelog_strings_repo_deb="${changelog_strings_repo_deb}\n * $( sed 's/#/PR/' <<<"${pull_request_id}" ) ${pull_request_description}"
|
|
||||||
changelog_strings_repo_yum="${changelog_strings_repo_yum}\n- $( sed 's/#/PR/' <<<"${pull_request_id}" ) ${pull_request_description}"
|
|
||||||
done
|
|
||||||
|
|
||||||
changelog_string_github="${changelog_string_github}\n#### ${repo_name}:\n$( echo -e "${changelog_strings_repo_github}" | sort -nk2 )\n"
|
|
||||||
changelog_string_deb="${changelog_string_deb}\n * ${repo_name}:$( echo -e "${changelog_strings_repo_deb}" | sort -nk2 )"
|
|
||||||
changelog_string_yum="${changelog_string_yum}\n- ${repo_name}:$( echo -e "${changelog_strings_repo_yum}" | sort -nk2 )"
|
|
||||||
|
|
||||||
popd
|
|
||||||
done
|
|
||||||
|
|
||||||
# Write out a temporary Debian changelog with our new stuff appended and some templated formatting
|
# Write out a temporary Debian changelog with our new stuff appended and some templated formatting
|
||||||
debian_changelog_file="deployment/debian-package-x64/pkg-src/changelog"
|
debian_changelog_file="deployment/debian-package-x64/pkg-src/changelog"
|
||||||
debian_changelog_temp="$( mktemp )"
|
debian_changelog_temp="$( mktemp )"
|
||||||
# Create new temp file with our changelog
|
# Create new temp file with our changelog
|
||||||
echo -e "### DEBIAN PACKAGE CHANGELOG: Verify this file looks correct or edit accordingly, then delete this line, write, and exit.
|
echo -e "### DEBIAN PACKAGE CHANGELOG: Verify this file looks correct or edit accordingly, then delete this line, write, and exit.
|
||||||
jellyfin (${new_version}-1) unstable; urgency=medium
|
jellyfin (${new_version_deb}) unstable; urgency=medium
|
||||||
${changelog_string_deb}
|
|
||||||
|
* New upstream version ${new_version}; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v${new_version}
|
||||||
|
|
||||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> $( date --rfc-2822 )
|
-- Jellyfin Packaging Team <packaging@jellyfin.org> $( date --rfc-2822 )
|
||||||
" >> ${debian_changelog_temp}
|
" >> ${debian_changelog_temp}
|
||||||
|
@ -180,13 +117,14 @@ pushd ${fedora_spec_temp_dir}
|
||||||
# Split out the stuff before and after changelog
|
# Split out the stuff before and after changelog
|
||||||
csplit jellyfin.spec "/^%changelog/" # produces xx00 xx01
|
csplit jellyfin.spec "/^%changelog/" # produces xx00 xx01
|
||||||
# Update the version in xx00
|
# Update the version in xx00
|
||||||
sed -i "s/${old_version_sed}/${new_version}/g" xx00
|
sed -i "s/${old_version_sed}/${new_version_sed}/g" xx00
|
||||||
# Remove the header from xx01
|
# Remove the header from xx01
|
||||||
sed -i '/^%changelog/d' xx01
|
sed -i '/^%changelog/d' xx01
|
||||||
# Create new temp file with our changelog
|
# Create new temp file with our changelog
|
||||||
echo -e "### YUM SPEC CHANGELOG: Verify this file looks correct or edit accordingly, then delete this line, write, and exit.
|
echo -e "### YUM SPEC CHANGELOG: Verify this file looks correct or edit accordingly, then delete this line, write, and exit.
|
||||||
%changelog
|
%changelog
|
||||||
* $( LANG=C date '+%a %b %d %Y' ) Jellyfin Packaging Team <packaging@jellyfin.org>${changelog_string_yum}" >> ${fedora_changelog_temp}
|
* $( LANG=C date '+%a %b %d %Y' ) Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||||
|
- New upstream version ${new_version}; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v${new_version}" >> ${fedora_changelog_temp}
|
||||||
cat xx01 >> ${fedora_changelog_temp}
|
cat xx01 >> ${fedora_changelog_temp}
|
||||||
# Edit the file to verify
|
# Edit the file to verify
|
||||||
$EDITOR ${fedora_changelog_temp}
|
$EDITOR ${fedora_changelog_temp}
|
||||||
|
@ -199,10 +137,5 @@ mv ${fedora_spec_temp} ${fedora_spec_file}
|
||||||
rm -rf ${fedora_changelog_temp} ${fedora_spec_temp_dir}
|
rm -rf ${fedora_changelog_temp} ${fedora_spec_temp_dir}
|
||||||
|
|
||||||
# Stage the changed files for commit
|
# Stage the changed files for commit
|
||||||
git add ${shared_version_file} ${debian_changelog_file} ${fedora_spec_file}
|
git add ${shared_version_file} ${build_file} ${debian_changelog_file} ${fedora_spec_file} Dockerfile*
|
||||||
git status
|
git status
|
||||||
|
|
||||||
# Write out the GitHub-formatted changelog for the merge request/release pages
|
|
||||||
echo ""
|
|
||||||
echo "=== The GitHub-formatted changelog follows ==="
|
|
||||||
echo -e "${changelog_string_github}"
|
|
||||||
|
|
|
@ -18,3 +18,4 @@ rpmbuild -bb SPECS/jellyfin.spec --define "_sourcedir ${SOURCE_DIR}/SOURCES/pkg-
|
||||||
# Move the artifacts out
|
# Move the artifacts out
|
||||||
mkdir -p ${ARTIFACT_DIR}/rpm
|
mkdir -p ${ARTIFACT_DIR}/rpm
|
||||||
mv /root/rpmbuild/RPMS/x86_64/jellyfin-*.rpm /root/rpmbuild/SRPMS/jellyfin-*.src.rpm ${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
|
${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile
|
||||||
# Build the RPMs and copy out to ${package_temporary_dir}
|
# Build the RPMs and copy out to ${package_temporary_dir}
|
||||||
${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}"
|
${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
|
# Move the RPMs to the output directory
|
||||||
mkdir -p "${output_dir}"
|
mkdir -p "${output_dir}"
|
||||||
mv "${package_temporary_dir}"/rpm/* "${output_dir}"
|
mv "${package_temporary_dir}"/rpm/* "${output_dir}"
|
||||||
|
|
|
@ -17,12 +17,12 @@ DEFAULT_PKG_DIR="pkg-dist"
|
||||||
DEFAULT_DOCKERFILE="Dockerfile"
|
DEFAULT_DOCKERFILE="Dockerfile"
|
||||||
DEFAULT_ARCHIVE_CMD="tar -xvzf"
|
DEFAULT_ARCHIVE_CMD="tar -xvzf"
|
||||||
|
|
||||||
# Parse the version from the AssemblyVersion
|
# Parse the version from the build.yaml version
|
||||||
get_version()
|
get_version()
|
||||||
(
|
(
|
||||||
local ROOT=${1-$DEFAULT_ROOT}
|
local ROOT=${1-$DEFAULT_ROOT}
|
||||||
grep "AssemblyVersion" ${ROOT}/SharedVersion.cs \
|
grep "version:" ${ROOT}/build.yaml \
|
||||||
| sed -E 's/\[assembly: ?AssemblyVersion\("([0-9\.]+)"\)\]/\1/'
|
| sed -E 's/version: "([0-9\.]+.*)"/\1/'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Run a build
|
# Run a build
|
||||||
|
|
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
|
# Move the artifacts out
|
||||||
mkdir -p ${ARTIFACT_DIR}/deb
|
mkdir -p ${ARTIFACT_DIR}/deb
|
||||||
mv /jellyfin_* ${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
|
esac
|
||||||
|
|
||||||
|
# Prepare temporary package dir
|
||||||
|
mkdir -p "${package_temporary_dir}"
|
||||||
# Set up the build environment Docker image
|
# Set up the build environment Docker image
|
||||||
${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE}
|
${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE}
|
||||||
# Build the DEBs and copy out to ${package_temporary_dir}
|
# Build the DEBs and copy out to ${package_temporary_dir}
|
||||||
${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}"
|
${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
|
# Move the DEBs to the output directory
|
||||||
mkdir -p "${output_dir}"
|
mkdir -p "${output_dir}"
|
||||||
mv "${package_temporary_dir}"/deb/* "${output_dir}"
|
mv "${package_temporary_dir}"/deb/* "${output_dir}"
|
||||||
|
|
|
@ -17,3 +17,4 @@ dpkg-buildpackage -us -uc
|
||||||
# Move the artifacts out
|
# Move the artifacts out
|
||||||
mkdir -p ${ARTIFACT_DIR}/deb
|
mkdir -p ${ARTIFACT_DIR}/deb
|
||||||
mv /jellyfin_* ${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=""
|
docker_sudo=""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Prepare temporary package dir
|
||||||
|
mkdir -p "${package_temporary_dir}"
|
||||||
# Set up the build environment Docker image
|
# Set up the build environment Docker image
|
||||||
${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile
|
${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile
|
||||||
# Build the DEBs and copy out to ${package_temporary_dir}
|
# Build the DEBs and copy out to ${package_temporary_dir}
|
||||||
${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}"
|
${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
|
# Move the DEBs to the output directory
|
||||||
mkdir -p "${output_dir}"
|
mkdir -p "${output_dir}"
|
||||||
mv "${package_temporary_dir}"/deb/* "${output_dir}"
|
mv "${package_temporary_dir}"/deb/* "${output_dir}"
|
||||||
|
|
|
@ -1,3 +1,27 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
-- Jellyfin Packaging Team <packaging@jellyfin.org> Fri, 19 Apr 2019 14:24:29 -0400
|
||||||
|
|
||||||
jellyfin (10.2.2-1) unstable; urgency=medium
|
jellyfin (10.2.2-1) unstable; urgency=medium
|
||||||
|
|
||||||
* jellyfin:
|
* jellyfin:
|
||||||
|
|
|
@ -22,7 +22,7 @@ JELLYFIN_CACHE_DIR="/var/cache/jellyfin"
|
||||||
JELLYFIN_RESTART_OPT="--restartpath=/usr/lib/jellyfin/restart.sh"
|
JELLYFIN_RESTART_OPT="--restartpath=/usr/lib/jellyfin/restart.sh"
|
||||||
|
|
||||||
# ffmpeg binary paths, overriding the system values
|
# ffmpeg binary paths, overriding the system values
|
||||||
JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/share/jellyfin-ffmpeg/ffmpeg"
|
JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/lib/jellyfin-ffmpeg/ffmpeg"
|
||||||
|
|
||||||
# [OPTIONAL] run Jellyfin as a headless service
|
# [OPTIONAL] run Jellyfin as a headless service
|
||||||
#JELLYFIN_SERVICE_OPT="--service"
|
#JELLYFIN_SERVICE_OPT="--service"
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user