Merge remote-tracking branch 'remotes/upstream/master' into kestrel_poc

This commit is contained in:
Claus Vium 2019-03-07 20:16:51 +01:00
commit 0abe57e930
94 changed files with 1778 additions and 1249 deletions

183
.ci/azure-pipelines.yml Normal file
View file

@ -0,0 +1,183 @@
name: $(Date:yyyyMMdd)$(Rev:.r)
variables:
- name: TestProjects
value: 'Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj'
- name: RestoreBuildProjects
value: 'Jellyfin.Server/Jellyfin.Server.csproj'
pr:
autoCancel: true
trigger:
batch: true
branches:
include:
- master
jobs:
- job: main_build
displayName: Main Build
pool:
vmImage: ubuntu-16.04
strategy:
matrix:
release:
BuildConfiguration: Release
debug:
BuildConfiguration: Debug
maxParallel: 2
steps:
- checkout: self
clean: true
submodules: true
persistCredentials: false
- task: DotNetCoreCLI@2
displayName: Restore
inputs:
command: restore
projects: '$(RestoreBuildProjects)'
- task: DotNetCoreCLI@2
displayName: Build
inputs:
projects: '$(RestoreBuildProjects)'
arguments: '--configuration $(BuildConfiguration)'
- task: DotNetCoreCLI@2
displayName: Test
inputs:
command: test
projects: '$(RestoreBuildProjects)'
arguments: '--configuration $(BuildConfiguration)'
enabled: false
- task: DotNetCoreCLI@2
displayName: Publish
inputs:
command: publish
publishWebProjects: false
projects: '$(RestoreBuildProjects)'
arguments: '--configuration $(BuildConfiguration) --output $(build.artifactstagingdirectory)'
zipAfterPublish: false
# - task: PublishBuildArtifacts@1
# displayName: 'Publish Artifact'
# inputs:
# PathtoPublish: '$(build.artifactstagingdirectory)'
# artifactName: 'jellyfin-build-$(BuildConfiguration)'
# zipAfterPublish: true
- task: PublishBuildArtifacts@1
displayName: 'Publish Artifact Naming'
condition: eq(variables['BuildConfiguration'], 'Release')
inputs:
PathtoPublish: '$(build.artifactstagingdirectory)/Jellyfin.Server/Emby.Naming.dll'
artifactName: 'Jellyfin.Naming'
- task: PublishBuildArtifacts@1
displayName: 'Publish Artifact Controller'
condition: eq(variables['BuildConfiguration'], 'Release')
inputs:
PathtoPublish: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Controller.dll'
artifactName: 'Jellyfin.Controller'
- task: PublishBuildArtifacts@1
displayName: 'Publish Artifact Model'
condition: eq(variables['BuildConfiguration'], 'Release')
inputs:
PathtoPublish: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Model.dll'
artifactName: 'Jellyfin.Model'
- task: PublishBuildArtifacts@1
displayName: 'Publish Artifact Common'
condition: eq(variables['BuildConfiguration'], 'Release')
inputs:
PathtoPublish: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Common.dll'
artifactName: 'Jellyfin.Common'
- job: dotnet_compat
displayName: Compatibility Check
pool:
vmImage: ubuntu-16.04
dependsOn: main_build
condition: succeeded()
strategy:
matrix:
Naming:
NugetPackageName: Jellyfin.Naming
AssemblyFileName: Emby.Naming.dll
Controller:
NugetPackageName: Jellyfin.Controller
AssemblyFileName: MediaBrowser.Controller.dll
Model:
NugetPackageName: Jellyfin.Model
AssemblyFileName: MediaBrowser.Model.dll
Common:
NugetPackageName: Jellyfin.Common
AssemblyFileName: MediaBrowser.Common.dll
maxParallel: 2
steps:
- checkout: none
- task: NuGetCommand@2
displayName: 'Download $(NugetPackageName)'
inputs:
command: custom
arguments: 'install $(NugetPackageName) -OutputDirectory $(System.ArtifactsDirectory)/packages -ExcludeVersion -DirectDownload'
- task: CopyFiles@2
displayName: Copy Nuget Assembly to current-release folder
inputs:
sourceFolder: $(System.ArtifactsDirectory)/packages/$(NugetPackageName) # Optional
contents: '**/*.dll'
targetFolder: $(System.ArtifactsDirectory)/current-release
cleanTargetFolder: true # Optional
overWrite: true # Optional
flattenFolders: true # Optional
- task: DownloadBuildArtifacts@0
displayName: Download the Assembly Build Artifact
inputs:
buildType: 'current' # Options: current, specific
allowPartiallySucceededBuilds: false # Optional
downloadType: 'single' # Options: single, specific
artifactName: '$(NugetPackageName)' # Required when downloadType == Single
downloadPath: '$(System.ArtifactsDirectory)/new-artifacts'
- task: CopyFiles@2
displayName: Copy Artifact Assembly to new-release folder
inputs:
sourceFolder: $(System.ArtifactsDirectory)/new-artifacts # Optional
contents: '**/*.dll'
targetFolder: $(System.ArtifactsDirectory)/new-release
cleanTargetFolder: true # Optional
overWrite: true # Optional
flattenFolders: true # Optional
- task: DownloadGitHubReleases@0
displayName: Download ABI compatibility check tool from GitHub
inputs:
connection: Jellyfin GitHub
userRepository: EraYaN/dotnet-compatibility
defaultVersionType: 'latest' # Options: latest, specificVersion, specificTag
#version: # Required when defaultVersionType != Latest
itemPattern: '**-ci.zip' # Optional
downloadPath: '$(System.ArtifactsDirectory)'
- task: ExtractFiles@1
displayName: Extract ABI compatibility check tool
inputs:
archiveFilePatterns: '$(System.ArtifactsDirectory)/*-ci.zip'
destinationFolder: $(System.ArtifactsDirectory)/tools
cleanDestinationFolder: true
- task: CmdLine@2
displayName: Execute ABI compatibility check tool
inputs:
script: 'dotnet tools/CompatibilityCheckerCoreCLI.dll current-release/$(AssemblyFileName) new-release/$(AssemblyFileName)'
workingDirectory: $(System.ArtifactsDirectory) # Optional
#failOnStderr: false # Optional

View file

@ -21,6 +21,9 @@
- [WillWill56](https://github.com/WillWill56)
- [Liggy](https://github.com/Liggy)
- [fruhnow](https://github.com/fruhnow)
- [Lynxy](https://github.com/Lynxy)
- [fasheng](https://github.com/fasheng)
- [ploughpuff](https://github.com/ploughpuff)
# Emby Contributors

View file

@ -4,10 +4,8 @@ FROM microsoft/dotnet:${DOTNET_VERSION}-sdk as builder
WORKDIR /repo
COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
RUN dotnet publish \
--configuration release \
--output /jellyfin \
Jellyfin.Server
RUN bash -c "source deployment/common.build.sh && \
build_jellyfin Jellyfin.Server Release linux-x64 /jellyfin"
FROM jellyfin/ffmpeg as ffmpeg
FROM microsoft/dotnet:${DOTNET_VERSION}-runtime
@ -22,6 +20,16 @@ RUN apt-get update \
&& chmod 777 /cache /config /media
COPY --from=ffmpeg / /
COPY --from=builder /jellyfin /jellyfin
ARG JELLYFIN_WEB_VERSION=10.2.2
RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& rm -rf /jellyfin/jellyfin-web \
&& mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web
EXPOSE 8096
VOLUME /cache /config /media
ENTRYPOINT dotnet /jellyfin/jellyfin.dll --datadir /config --cachedir /cache
ENTRYPOINT dotnet /jellyfin/jellyfin.dll \
--datadir /config \
--cachedir /cache \
--ffmpeg /usr/local/bin/ffmpeg \
--ffprobe /usr/local/bin/ffprobe

View file

@ -17,11 +17,8 @@ RUN find . -type f -exec sed -i 's/netcoreapp2.1/netcoreapp3.0/g' {} \;
# Discard objs - may cause failures if exists
RUN find . -type d -name obj | xargs -r rm -r
# Build
RUN dotnet publish \
-r linux-arm \
--configuration release \
--output /jellyfin \
Jellyfin.Server
RUN bash -c "source deployment/common.build.sh && \
build_jellyfin Jellyfin.Server Release linux-arm /jellyfin"
FROM microsoft/dotnet:${DOTNET_VERSION}-runtime-stretch-slim-arm32v7
@ -31,6 +28,16 @@ RUN apt-get update \
&& mkdir -p /cache /config /media \
&& chmod 777 /cache /config /media
COPY --from=builder /jellyfin /jellyfin
ARG JELLYFIN_WEB_VERSION=10.2.2
RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& rm -rf /jellyfin/jellyfin-web \
&& mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web
EXPOSE 8096
VOLUME /cache /config /media
ENTRYPOINT dotnet /jellyfin/jellyfin.dll --datadir /config --cachedir /cache
ENTRYPOINT dotnet /jellyfin/jellyfin.dll \
--datadir /config \
--cachedir /cache \
--ffmpeg /usr/bin/ffmpeg \
--ffprobe /usr/bin/ffprobe

View file

@ -18,11 +18,8 @@ RUN find . -type f -exec sed -i 's/netcoreapp2.1/netcoreapp3.0/g' {} \;
# Discard objs - may cause failures if exists
RUN find . -type d -name obj | xargs -r rm -r
# Build
RUN dotnet publish \
-r linux-arm64 \
--configuration release \
--output /jellyfin \
Jellyfin.Server
RUN bash -c "source deployment/common.build.sh && \
build_jellyfin Jellyfin.Server Release linux-arm64 /jellyfin"
FROM microsoft/dotnet:${DOTNET_VERSION}-runtime-stretch-slim-arm64v8
@ -32,6 +29,16 @@ RUN apt-get update \
&& mkdir -p /cache /config /media \
&& chmod 777 /cache /config /media
COPY --from=builder /jellyfin /jellyfin
ARG JELLYFIN_WEB_VERSION=10.2.2
RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& rm -rf /jellyfin/jellyfin-web \
&& mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web
EXPOSE 8096
VOLUME /cache /config /media
ENTRYPOINT dotnet /jellyfin/jellyfin.dll --datadir /config --cachedir /cache
ENTRYPOINT dotnet /jellyfin/jellyfin.dll \
--datadir /config \
--cachedir /cache \
--ffmpeg /usr/bin/ffmpeg \
--ffprobe /usr/bin/ffprobe

View file

@ -26,17 +26,17 @@ namespace DvdLib.Ifo
if (vmgPath == null)
{
var allIfos = allFiles.Where(i => string.Equals(i.Extension, ".ifo", StringComparison.OrdinalIgnoreCase));
foreach (var ifo in allIfos)
foreach (var ifo in allFiles)
{
var num = ifo.Name.Split('_').ElementAtOrDefault(1);
var numbersRead = new List<ushort>();
if (!string.Equals(ifo.Extension, ".ifo", StringComparison.OrdinalIgnoreCase))
{
continue;
}
if (!string.IsNullOrEmpty(num) && ushort.TryParse(num, out var ifoNumber) && !numbersRead.Contains(ifoNumber))
var nums = ifo.Name.Split(new [] { '_' }, StringSplitOptions.RemoveEmptyEntries);
if (nums.Length >= 2 && ushort.TryParse(nums[1], out var ifoNumber))
{
ReadVTS(ifoNumber, ifo.FullName);
numbersRead.Add(ifoNumber);
}
}
}
@ -76,7 +76,7 @@ namespace DvdLib.Ifo
}
}
private void ReadVTS(ushort vtsNum, List<FileSystemMetadata> allFiles)
private void ReadVTS(ushort vtsNum, IEnumerable<FileSystemMetadata> allFiles)
{
var filename = string.Format("VTS_{0:00}_0.IFO", vtsNum);

View file

@ -7,6 +7,7 @@ namespace Emby.Dlna.Configuration
public bool EnableServer { get; set; }
public bool EnableDebugLog { get; set; }
public bool BlastAliveMessages { get; set; }
public bool SendOnlyMatchedHost { get; set; }
public int ClientDiscoveryIntervalSeconds { get; set; }
public int BlastAliveMessageIntervalSeconds { get; set; }
public string DefaultUserId { get; set; }
@ -16,6 +17,7 @@ namespace Emby.Dlna.Configuration
EnablePlayTo = true;
EnableServer = true;
BlastAliveMessages = true;
SendOnlyMatchedHost = true;
ClientDiscoveryIntervalSeconds = 60;
BlastAliveMessageIntervalSeconds = 1800;
}

View file

@ -260,7 +260,7 @@ namespace Emby.Dlna.ContentDirectory
if (item.IsDisplayedAsFolder || serverItem.StubType.HasValue)
{
var childrenResult = (GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount));
var childrenResult = GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount);
_didlBuilder.WriteFolderElement(writer, item, serverItem.StubType, null, childrenResult.TotalRecordCount, filter, id);
}
@ -273,7 +273,7 @@ namespace Emby.Dlna.ContentDirectory
}
else
{
var childrenResult = (GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount));
var childrenResult = GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount);
totalCount = childrenResult.TotalRecordCount;
provided = childrenResult.Items.Length;

View file

@ -818,10 +818,9 @@ namespace Emby.Dlna.Didl
{
AddCommonFields(item, itemStubType, context, writer, filter);
var hasArtists = item as IHasArtist;
var hasAlbumArtists = item as IHasAlbumArtist;
if (hasArtists != null)
if (item is IHasArtist hasArtists)
{
foreach (var artist in hasArtists.Artists)
{

View file

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
@ -15,7 +16,6 @@ using MediaBrowser.Controller.Drawing;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Reflection;
using MediaBrowser.Model.Serialization;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
@ -31,7 +31,7 @@ namespace Emby.Dlna
private readonly ILogger _logger;
private readonly IJsonSerializer _jsonSerializer;
private readonly IServerApplicationHost _appHost;
private readonly IAssemblyInfo _assemblyInfo;
private static readonly Assembly _assembly = typeof(DlnaManager).Assembly;
private readonly Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>> _profiles = new Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>>(StringComparer.Ordinal);
@ -41,8 +41,7 @@ namespace Emby.Dlna
IApplicationPaths appPaths,
ILoggerFactory loggerFactory,
IJsonSerializer jsonSerializer,
IServerApplicationHost appHost,
IAssemblyInfo assemblyInfo)
IServerApplicationHost appHost)
{
_xmlSerializer = xmlSerializer;
_fileSystem = fileSystem;
@ -50,7 +49,6 @@ namespace Emby.Dlna
_logger = loggerFactory.CreateLogger("Dlna");
_jsonSerializer = jsonSerializer;
_appHost = appHost;
_assemblyInfo = assemblyInfo;
}
public async Task InitProfilesAsync()
@ -367,15 +365,18 @@ namespace Emby.Dlna
var systemProfilesPath = SystemProfilesPath;
foreach (var name in _assemblyInfo.GetManifestResourceNames(GetType())
.Where(i => i.StartsWith(namespaceName))
.ToList())
foreach (var name in _assembly.GetManifestResourceNames())
{
if (!name.StartsWith(namespaceName))
{
continue;
}
var filename = Path.GetFileName(name).Substring(namespaceName.Length);
var path = Path.Combine(systemProfilesPath, filename);
using (var stream = _assemblyInfo.GetManifestResourceStream(GetType(), name))
using (var stream = _assembly.GetManifestResourceStream(name))
{
var fileInfo = _fileSystem.GetFileInfo(path);
@ -513,7 +514,7 @@ namespace Emby.Dlna
return new ImageStream
{
Format = format,
Stream = _assemblyInfo.GetManifestResourceStream(GetType(), resource)
Stream = _assembly.GetManifestResourceStream(resource)
};
}
}

View file

@ -169,9 +169,10 @@ namespace Emby.Dlna.Main
{
if (_communicationsServer == null)
{
var enableMultiSocketBinding = _environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows;
var enableMultiSocketBinding = _environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows ||
_environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Linux;
_communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding)
_communicationsServer = new SsdpCommunicationsServer(_config, _socketFactory, _networkManager, _logger, enableMultiSocketBinding)
{
IsShared = true
};
@ -229,7 +230,7 @@ namespace Emby.Dlna.Main
try
{
_Publisher = new SsdpDevicePublisher(_communicationsServer, _environmentInfo.OperatingSystemName, _environmentInfo.OperatingSystemVersion);
_Publisher = new SsdpDevicePublisher(_communicationsServer, _networkManager, _environmentInfo.OperatingSystemName, _environmentInfo.OperatingSystemVersion, _config.GetDlnaConfiguration().SendOnlyMatchedHost);
_Publisher.LogFunction = LogMessage;
_Publisher.SupportPnpRootDevice = false;
@ -245,17 +246,17 @@ namespace Emby.Dlna.Main
private async Task RegisterServerEndpoints()
{
var addresses = (await _appHost.GetLocalIpAddresses(CancellationToken.None).ConfigureAwait(false)).ToList();
var addresses = await _appHost.GetLocalIpAddresses(CancellationToken.None).ConfigureAwait(false);
var udn = CreateUuid(_appHost.SystemId);
foreach (var address in addresses)
{
// TODO: Remove this condition on platforms that support it
//if (address.AddressFamily == IpAddressFamily.InterNetworkV6)
//{
// continue;
//}
if (address.AddressFamily == IpAddressFamily.InterNetworkV6)
{
// Not support IPv6 right now
continue;
}
var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
@ -268,6 +269,8 @@ namespace Emby.Dlna.Main
{
CacheLifetime = TimeSpan.FromSeconds(1800), //How long SSDP clients can cache this info.
Location = uri, // Must point to the URL that serves your devices UPnP description document.
Address = address,
SubnetMask = _networkManager.GetLocalIpSubnetMask(address),
FriendlyName = "Jellyfin",
Manufacturer = "Jellyfin",
ModelName = "Jellyfin Server",

View file

@ -107,12 +107,18 @@ namespace Emby.Dlna.PlayTo
foreach (var arg in action.ArgumentList)
{
if (arg.Direction == "out")
{
continue;
}
if (arg.Name == "InstanceID")
{
stateString += BuildArgumentXml(arg, "0");
}
else
{
stateString += BuildArgumentXml(arg, null);
}
}
return string.Format(CommandBase, action.Name, xmlNamespace, stateString);
@ -125,11 +131,18 @@ namespace Emby.Dlna.PlayTo
foreach (var arg in action.ArgumentList)
{
if (arg.Direction == "out")
{
continue;
}
if (arg.Name == "InstanceID")
{
stateString += BuildArgumentXml(arg, "0");
}
else
{
stateString += BuildArgumentXml(arg, value.ToString(), commandParameter);
}
}
return string.Format(CommandBase, action.Name, xmlNamesapce, stateString);
@ -142,11 +155,17 @@ namespace Emby.Dlna.PlayTo
foreach (var arg in action.ArgumentList)
{
if (arg.Name == "InstanceID")
{
stateString += BuildArgumentXml(arg, "0");
}
else if (dictionary.ContainsKey(arg.Name))
{
stateString += BuildArgumentXml(arg, dictionary[arg.Name]);
}
else
{
stateString += BuildArgumentXml(arg, value.ToString());
}
}
return string.Format(CommandBase, action.Name, xmlNamesapce, stateString);

View file

@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Common;
namespace Emby.Naming.TV
@ -22,7 +21,9 @@ namespace Emby.Naming.TV
// There were no failed tests without this block, but to be safe, we can keep it until
// the regex which require file extensions are modified so that they don't need them.
if (IsDirectory)
{
path += ".mp4";
}
EpisodePathParserResult result = null;
@ -35,6 +36,7 @@ namespace Emby.Naming.TV
continue;
}
}
if (isNamed.HasValue)
{
if (expression.IsNamed != isNamed.Value)
@ -42,6 +44,7 @@ namespace Emby.Naming.TV
continue;
}
}
if (isOptimistic.HasValue)
{
if (expression.IsOptimistic != isOptimistic.Value)
@ -191,13 +194,20 @@ namespace Emby.Naming.TV
private void FillAdditional(string path, EpisodePathParserResult info, IEnumerable<EpisodeExpression> expressions)
{
var results = expressions
.Where(i => i.IsNamed)
.Select(i => Parse(path, i))
.Where(i => i.Success);
foreach (var result in results)
foreach (var i in expressions)
{
if (!i.IsNamed)
{
continue;
}
var result = Parse(path, i);
if (!result.Success)
{
continue;
}
if (string.IsNullOrEmpty(info.SeriesName))
{
info.SeriesName = result.SeriesName;
@ -208,12 +218,10 @@ namespace Emby.Naming.TV
info.EndingEpsiodeNumber = result.EndingEpsiodeNumber;
}
if (!string.IsNullOrEmpty(info.SeriesName))
if (!string.IsNullOrEmpty(info.SeriesName)
&& (!info.EpisodeNumber.HasValue || info.EndingEpsiodeNumber.HasValue))
{
if (!info.EpisodeNumber.HasValue || info.EndingEpsiodeNumber.HasValue)
{
break;
}
break;
}
}
}

View file

@ -39,8 +39,13 @@ namespace Emby.Server.Implementations.Activity
{
var result = _repo.GetActivityLogEntries(minDate, hasUserId, startIndex, limit);
foreach (var item in result.Items.Where(i => !i.UserId.Equals(Guid.Empty)))
foreach (var item in result.Items)
{
if (item.UserId == Guid.Empty)
{
continue;
}
var user = _userManager.GetUserById(item.UserId);
if (user != null)

View file

@ -28,7 +28,6 @@ using Emby.Server.Implementations.Data;
using Emby.Server.Implementations.Devices;
using Emby.Server.Implementations.Diagnostics;
using Emby.Server.Implementations.Dto;
using Emby.Server.Implementations.FFMpeg;
using Emby.Server.Implementations.HttpServer;
using Emby.Server.Implementations.HttpServer.Security;
using Emby.Server.Implementations.IO;
@ -541,7 +540,7 @@ namespace Emby.Server.Implementations
ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
MediaEncoder.Init();
MediaEncoder.SetFFmpegPath();
//if (string.IsNullOrWhiteSpace(MediaEncoder.EncoderPath))
//{
@ -813,10 +812,8 @@ namespace Emby.Server.Implementations
TVSeriesManager = new TVSeriesManager(UserManager, UserDataManager, LibraryManager, ServerConfigurationManager);
serviceCollection.AddSingleton(TVSeriesManager);
var encryptionManager = new EncryptionManager();
serviceCollection.AddSingleton<IEncryptionManager>(encryptionManager);
DeviceManager = new DeviceManager(AuthenticationRepository, JsonSerializer, LibraryManager, LocalizationManager, UserManager, FileSystemManager, LibraryMonitor, ServerConfigurationManager);
serviceCollection.AddSingleton(DeviceManager);
MediaSourceManager = new MediaSourceManager(ItemRepository, ApplicationPaths, LocalizationManager, UserManager, LibraryManager, LoggerFactory, JsonSerializer, FileSystemManager, UserDataManager, () => MediaEncoder);
@ -838,7 +835,7 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton(SessionManager);
serviceCollection.AddSingleton<IDlnaManager>(
new DlnaManager(XmlSerializer, FileSystemManager, ApplicationPaths, LoggerFactory, JsonSerializer, this, assemblyInfo));
new DlnaManager(XmlSerializer, FileSystemManager, ApplicationPaths, LoggerFactory, JsonSerializer, this));
CollectionManager = new CollectionManager(LibraryManager, ApplicationPaths, LocalizationManager, FileSystemManager, LibraryMonitor, LoggerFactory, ProviderManager);
serviceCollection.AddSingleton(CollectionManager);
@ -861,7 +858,18 @@ namespace Emby.Server.Implementations
ChapterManager = new ChapterManager(LibraryManager, LoggerFactory, ServerConfigurationManager, ItemRepository);
serviceCollection.AddSingleton(ChapterManager);
RegisterMediaEncoder(serviceCollection);
MediaEncoder = new MediaBrowser.MediaEncoding.Encoder.MediaEncoder(
LoggerFactory,
JsonSerializer,
StartupOptions.FFmpegPath,
StartupOptions.FFprobePath,
ServerConfigurationManager,
FileSystemManager,
() => SubtitleEncoder,
() => MediaSourceManager,
ProcessFactory,
5000);
serviceCollection.AddSingleton(MediaEncoder);
EncodingManager = new MediaEncoder.EncodingManager(FileSystemManager, LoggerFactory, MediaEncoder, ChapterManager, LibraryManager);
serviceCollection.AddSingleton(EncodingManager);
@ -970,85 +978,6 @@ namespace Emby.Server.Implementations
return new ImageProcessor(LoggerFactory, ServerConfigurationManager.ApplicationPaths, FileSystemManager, ImageEncoder, () => LibraryManager, () => MediaEncoder);
}
protected virtual FFMpegInstallInfo GetFfmpegInstallInfo()
{
var info = new FFMpegInstallInfo();
// Windows builds: http://ffmpeg.zeranoe.com/builds/
// Linux builds: http://johnvansickle.com/ffmpeg/
// OS X builds: http://ffmpegmac.net/
// OS X x64: http://www.evermeet.cx/ffmpeg/
if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Linux)
{
info.FFMpegFilename = "ffmpeg";
info.FFProbeFilename = "ffprobe";
info.ArchiveType = "7z";
info.Version = "20170308";
}
else if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows)
{
info.FFMpegFilename = "ffmpeg.exe";
info.FFProbeFilename = "ffprobe.exe";
info.Version = "20170308";
info.ArchiveType = "7z";
}
else if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.OSX)
{
info.FFMpegFilename = "ffmpeg";
info.FFProbeFilename = "ffprobe";
info.ArchiveType = "7z";
info.Version = "20170308";
}
return info;
}
protected FFMpegInfo GetFFMpegInfo()
{
return new FFMpegLoader(ApplicationPaths, FileSystemManager, GetFfmpegInstallInfo())
.GetFFMpegInfo(StartupOptions);
}
/// <summary>
/// Registers the media encoder.
/// </summary>
/// <returns>Task.</returns>
private void RegisterMediaEncoder(IServiceCollection serviceCollection)
{
string encoderPath = null;
string probePath = null;
var info = GetFFMpegInfo();
encoderPath = info.EncoderPath;
probePath = info.ProbePath;
var hasExternalEncoder = string.Equals(info.Version, "external", StringComparison.OrdinalIgnoreCase);
var mediaEncoder = new MediaBrowser.MediaEncoding.Encoder.MediaEncoder(
LoggerFactory,
JsonSerializer,
encoderPath,
probePath,
hasExternalEncoder,
ServerConfigurationManager,
FileSystemManager,
LiveTvManager,
IsoManager,
LibraryManager,
ChannelManager,
SessionManager,
() => SubtitleEncoder,
() => MediaSourceManager,
HttpClient,
ZipClient,
ProcessFactory,
5000);
MediaEncoder = mediaEncoder;
serviceCollection.AddSingleton(MediaEncoder);
}
/// <summary>
/// Gets the user repository.
/// </summary>
@ -1481,7 +1410,7 @@ namespace Emby.Server.Implementations
ServerName = FriendlyName,
LocalAddress = localAddress,
SupportsLibraryMonitor = true,
EncoderLocationType = MediaEncoder.EncoderLocationType,
EncoderLocation = MediaEncoder.EncoderLocation,
SystemArchitecture = EnvironmentInfo.SystemArchitecture,
SystemUpdateLevel = SystemUpdateLevel,
PackageName = StartupOptions.PackageName
@ -1598,7 +1527,7 @@ namespace Emby.Server.Implementations
if (addresses.Count == 0)
{
addresses.AddRange(NetworkManager.GetLocalIpAddresses());
addresses.AddRange(NetworkManager.GetLocalIpAddresses(ServerConfigurationManager.Configuration.IgnoreVirtualInterfaces));
}
var resultList = new List<IpAddressInfo>();

View file

@ -243,8 +243,7 @@ namespace Emby.Server.Implementations.Channels
{
foreach (var item in returnItems)
{
var task = RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None);
Task.WaitAll(task);
RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).GetAwaiter().GetResult();
}
}
@ -303,9 +302,7 @@ namespace Emby.Server.Implementations.Channels
}
numComplete++;
double percent = numComplete;
percent /= allChannelsList.Count;
double percent = (double)numComplete / allChannelsList.Count;
progress.Report(100 * percent);
}
@ -658,9 +655,7 @@ namespace Emby.Server.Implementations.Channels
foreach (var item in result.Items)
{
var folder = item as Folder;
if (folder != null)
if (item is Folder folder)
{
await GetChannelItemsInternal(new InternalItemsQuery
{

View file

@ -35,64 +35,52 @@ namespace Emby.Server.Implementations.Channels
public static string GetUserDistinctValue(User user)
{
var channels = user.Policy.EnabledChannels
.OrderBy(i => i)
.ToList();
.OrderBy(i => i);
return string.Join("|", channels.ToArray());
return string.Join("|", channels);
}
private void CleanDatabase(CancellationToken cancellationToken)
{
var installedChannelIds = ((ChannelManager)_channelManager).GetInstalledChannelIds();
var databaseIds = _libraryManager.GetItemIds(new InternalItemsQuery
var uninstalledChannels = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = new[] { typeof(Channel).Name }
IncludeItemTypes = new[] { typeof(Channel).Name },
ExcludeItemIds = installedChannelIds.ToArray()
});
var invalidIds = databaseIds
.Except(installedChannelIds)
.ToList();
foreach (var id in invalidIds)
foreach (var channel in uninstalledChannels)
{
cancellationToken.ThrowIfCancellationRequested();
CleanChannel(id, cancellationToken);
CleanChannel((Channel)channel, cancellationToken);
}
}
private void CleanChannel(Guid id, CancellationToken cancellationToken)
private void CleanChannel(Channel channel, CancellationToken cancellationToken)
{
_logger.LogInformation("Cleaning channel {0} from database", id);
_logger.LogInformation("Cleaning channel {0} from database", channel.Id);
// Delete all channel items
var allIds = _libraryManager.GetItemIds(new InternalItemsQuery
var items = _libraryManager.GetItemList(new InternalItemsQuery
{
ChannelIds = new[] { id }
ChannelIds = new[] { channel.Id }
});
foreach (var deleteId in allIds)
foreach (var item in items)
{
cancellationToken.ThrowIfCancellationRequested();
DeleteItem(deleteId);
_libraryManager.DeleteItem(item, new DeleteOptions
{
DeleteFileLocation = false
}, false);
}
// Finally, delete the channel itself
DeleteItem(id);
}
private void DeleteItem(Guid id)
{
var item = _libraryManager.GetItemById(id);
if (item == null)
{
return;
}
_libraryManager.DeleteItem(item, new DeleteOptions
_libraryManager.DeleteItem(channel, new DeleteOptions
{
DeleteFileLocation = false

View file

@ -1,13 +1,49 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Linq;
using MediaBrowser.Model.Cryptography;
namespace Emby.Server.Implementations.Cryptography
{
public class CryptographyProvider : ICryptoProvider
{
private static readonly HashSet<string> _supportedHashMethods = new HashSet<string>()
{
"MD5",
"System.Security.Cryptography.MD5",
"SHA",
"SHA1",
"System.Security.Cryptography.SHA1",
"SHA256",
"SHA-256",
"System.Security.Cryptography.SHA256",
"SHA384",
"SHA-384",
"System.Security.Cryptography.SHA384",
"SHA512",
"SHA-512",
"System.Security.Cryptography.SHA512"
};
public string DefaultHashMethod => "PBKDF2";
private RandomNumberGenerator _randomNumberGenerator;
private const int _defaultIterations = 1000;
public CryptographyProvider()
{
//FIXME: When we get DotNet Standard 2.1 we need to revisit how we do the crypto
//Currently supported hash methods from https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.cryptoconfig?view=netcore-2.1
//there might be a better way to autogenerate this list as dotnet updates, but I couldn't find one
//Please note the default method of PBKDF2 is not included, it cannot be used to generate hashes cleanly as it is actually a pbkdf with sha1
_randomNumberGenerator = RandomNumberGenerator.Create();
}
public Guid GetMD5(string str)
{
return new Guid(ComputeMD5(Encoding.Unicode.GetBytes(str)));
@ -36,5 +72,98 @@ namespace Emby.Server.Implementations.Cryptography
return provider.ComputeHash(bytes);
}
}
public IEnumerable<string> GetSupportedHashMethods()
{
return _supportedHashMethods;
}
private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations)
{
//downgrading for now as we need this library to be dotnetstandard compliant
//with this downgrade we'll add a check to make sure we're on the downgrade method at the moment
if (method == DefaultHashMethod)
{
using (var r = new Rfc2898DeriveBytes(bytes, salt, iterations))
{
return r.GetBytes(32);
}
}
throw new CryptographicException($"Cannot currently use PBKDF2 with requested hash method: {method}");
}
public byte[] ComputeHash(string hashMethod, byte[] bytes)
{
return ComputeHash(hashMethod, bytes, Array.Empty<byte>());
}
public byte[] ComputeHashWithDefaultMethod(byte[] bytes)
{
return ComputeHash(DefaultHashMethod, bytes);
}
public byte[] ComputeHash(string hashMethod, byte[] bytes, byte[] salt)
{
if (hashMethod == DefaultHashMethod)
{
return PBKDF2(hashMethod, bytes, salt, _defaultIterations);
}
else if (_supportedHashMethods.Contains(hashMethod))
{
using (var h = HashAlgorithm.Create(hashMethod))
{
if (salt.Length == 0)
{
return h.ComputeHash(bytes);
}
else
{
byte[] salted = new byte[bytes.Length + salt.Length];
Array.Copy(bytes, salted, bytes.Length);
Array.Copy(salt, 0, salted, bytes.Length, salt.Length);
return h.ComputeHash(salted);
}
}
}
else
{
throw new CryptographicException($"Requested hash method is not supported: {hashMethod}");
}
}
public byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt)
{
return PBKDF2(DefaultHashMethod, bytes, salt, _defaultIterations);
}
public byte[] ComputeHash(PasswordHash hash)
{
int iterations = _defaultIterations;
if (!hash.Parameters.ContainsKey("iterations"))
{
hash.Parameters.Add("iterations", _defaultIterations.ToString(CultureInfo.InvariantCulture));
}
else
{
try
{
iterations = int.Parse(hash.Parameters["iterations"]);
}
catch (Exception e)
{
throw new InvalidDataException($"Couldn't successfully parse iterations value from string: {hash.Parameters["iterations"]}", e);
}
}
return PBKDF2(hash.Id, hash.HashBytes, hash.SaltBytes, iterations);
}
public byte[] GenerateSalt()
{
byte[] salt = new byte[64];
_randomNumberGenerator.GetBytes(salt);
return salt;
}
}
}

View file

@ -2279,11 +2279,10 @@ namespace Emby.Server.Implementations.Data
private static readonly HashSet<string> _seriesTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Audio",
"MusicAlbum",
"MusicVideo",
"Book",
"AudioBook",
"AudioPodcast"
"Episode",
"Season"
};
private bool HasSeriesFields(InternalItemsQuery query)

View file

@ -119,9 +119,9 @@ namespace Emby.Server.Implementations.Data
{
list.Add(row[0].ReadGuidFromBlob());
}
catch
catch (Exception ex)
{
Logger.LogError(ex, "Error while getting user");
}
}
}

View file

@ -55,6 +55,8 @@ namespace Emby.Server.Implementations.Data
{
TryMigrateToLocalUsersTable(connection);
}
RemoveEmptyPasswordHashes();
}
}
@ -73,6 +75,38 @@ namespace Emby.Server.Implementations.Data
}
}
private void RemoveEmptyPasswordHashes()
{
foreach (var user in RetrieveAllUsers())
{
// If the user password is the sha1 hash of the empty string, remove it
if (!string.Equals(user.Password, "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal)
|| !string.Equals(user.Password, "$SHA1$DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal))
{
continue;
}
user.Password = null;
var serialized = _jsonSerializer.SerializeToBytes(user);
using (WriteLock.Write())
using (var connection = CreateConnection())
{
connection.RunInTransaction(db =>
{
using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId"))
{
statement.TryBind("@InternalId", user.InternalId);
statement.TryBind("@data", serialized);
statement.MoveNext();
}
}, TransactionMode);
}
}
}
/// <summary>
/// Save a user in the repo
/// </summary>

View file

@ -5,8 +5,6 @@ using System.Linq;
using System.Threading.Tasks;
using MediaBrowser.Common;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@ -21,8 +19,6 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Querying;
using Microsoft.Extensions.Logging;
@ -83,15 +79,8 @@ namespace Emby.Server.Implementations.Dto
return GetBaseItemDto(item, options, user, owner);
}
public BaseItemDto[] GetBaseItemDtos(List<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null)
{
return GetBaseItemDtos(items, items.Count, options, user, owner);
}
public BaseItemDto[] GetBaseItemDtos(BaseItem[] items, DtoOptions options, User user = null, BaseItem owner = null)
{
return GetBaseItemDtos(items, items.Length, options, user, owner);
}
public BaseItemDto[] GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null)
=> GetBaseItemDtos(items, items.Count, options, user, owner);
public BaseItemDto[] GetBaseItemDtos(IEnumerable<BaseItem> items, int itemCount, DtoOptions options, User user = null, BaseItem owner = null)
{

View file

@ -1,24 +0,0 @@
namespace Emby.Server.Implementations.FFMpeg
{
/// <summary>
/// Class FFMpegInfo
/// </summary>
public class FFMpegInfo
{
/// <summary>
/// Gets or sets the path.
/// </summary>
/// <value>The path.</value>
public string EncoderPath { get; set; }
/// <summary>
/// Gets or sets the probe path.
/// </summary>
/// <value>The probe path.</value>
public string ProbePath { get; set; }
/// <summary>
/// Gets or sets the version.
/// </summary>
/// <value>The version.</value>
public string Version { get; set; }
}
}

View file

@ -1,17 +0,0 @@
namespace Emby.Server.Implementations.FFMpeg
{
public class FFMpegInstallInfo
{
public string Version { get; set; }
public string FFMpegFilename { get; set; }
public string FFProbeFilename { get; set; }
public string ArchiveType { get; set; }
public FFMpegInstallInfo()
{
Version = "Path";
FFMpegFilename = "ffmpeg";
FFProbeFilename = "ffprobe";
}
}
}

View file

@ -1,132 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.IO;
namespace Emby.Server.Implementations.FFMpeg
{
public class FFMpegLoader
{
private readonly IApplicationPaths _appPaths;
private readonly IFileSystem _fileSystem;
private readonly FFMpegInstallInfo _ffmpegInstallInfo;
public FFMpegLoader(IApplicationPaths appPaths, IFileSystem fileSystem, FFMpegInstallInfo ffmpegInstallInfo)
{
_appPaths = appPaths;
_fileSystem = fileSystem;
_ffmpegInstallInfo = ffmpegInstallInfo;
}
public FFMpegInfo GetFFMpegInfo(IStartupOptions options)
{
var customffMpegPath = options.FFmpegPath;
var customffProbePath = options.FFprobePath;
if (!string.IsNullOrWhiteSpace(customffMpegPath) && !string.IsNullOrWhiteSpace(customffProbePath))
{
return new FFMpegInfo
{
ProbePath = customffProbePath,
EncoderPath = customffMpegPath,
Version = "external"
};
}
var downloadInfo = _ffmpegInstallInfo;
var prebuiltFolder = _appPaths.ProgramSystemPath;
var prebuiltffmpeg = Path.Combine(prebuiltFolder, downloadInfo.FFMpegFilename);
var prebuiltffprobe = Path.Combine(prebuiltFolder, downloadInfo.FFProbeFilename);
if (File.Exists(prebuiltffmpeg) && File.Exists(prebuiltffprobe))
{
return new FFMpegInfo
{
ProbePath = prebuiltffprobe,
EncoderPath = prebuiltffmpeg,
Version = "external"
};
}
var version = downloadInfo.Version;
if (string.Equals(version, "0", StringComparison.OrdinalIgnoreCase))
{
return new FFMpegInfo();
}
var rootEncoderPath = Path.Combine(_appPaths.ProgramDataPath, "ffmpeg");
var versionedDirectoryPath = Path.Combine(rootEncoderPath, version);
var info = new FFMpegInfo
{
ProbePath = Path.Combine(versionedDirectoryPath, downloadInfo.FFProbeFilename),
EncoderPath = Path.Combine(versionedDirectoryPath, downloadInfo.FFMpegFilename),
Version = version
};
Directory.CreateDirectory(versionedDirectoryPath);
var excludeFromDeletions = new List<string> { versionedDirectoryPath };
if (!File.Exists(info.ProbePath) || !File.Exists(info.EncoderPath))
{
// ffmpeg not present. See if there's an older version we can start with
var existingVersion = GetExistingVersion(info, rootEncoderPath);
// No older version. Need to download and block until complete
if (existingVersion == null)
{
return new FFMpegInfo();
}
else
{
info = existingVersion;
versionedDirectoryPath = Path.GetDirectoryName(info.EncoderPath);
excludeFromDeletions.Add(versionedDirectoryPath);
}
}
// Allow just one of these to be overridden, if desired.
if (!string.IsNullOrWhiteSpace(customffMpegPath))
{
info.EncoderPath = customffMpegPath;
}
if (!string.IsNullOrWhiteSpace(customffProbePath))
{
info.ProbePath = customffProbePath;
}
return info;
}
private FFMpegInfo GetExistingVersion(FFMpegInfo info, string rootEncoderPath)
{
var encoderFilename = Path.GetFileName(info.EncoderPath);
var probeFilename = Path.GetFileName(info.ProbePath);
foreach (var directory in _fileSystem.GetDirectoryPaths(rootEncoderPath))
{
var allFiles = _fileSystem.GetFilePaths(directory, true).ToList();
var encoder = allFiles.FirstOrDefault(i => string.Equals(Path.GetFileName(i), encoderFilename, StringComparison.OrdinalIgnoreCase));
var probe = allFiles.FirstOrDefault(i => string.Equals(Path.GetFileName(i), probeFilename, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrWhiteSpace(encoder) &&
!string.IsNullOrWhiteSpace(probe))
{
return new FFMpegInfo
{
EncoderPath = encoder,
ProbePath = probe,
Version = Path.GetFileName(Path.GetDirectoryName(probe))
};
}
}
return null;
}
}
}

View file

@ -1,4 +1,5 @@
using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MediaBrowser.Controller.Authentication;
@ -18,20 +19,64 @@ namespace Emby.Server.Implementations.Library
public string Name => "Default";
public bool IsEnabled => true;
// This is dumb and an artifact of the backwards way auth providers were designed.
// This version of authenticate was never meant to be called, but needs to be here for interface compat
// Only the providers that don't provide local user support use this
public Task<ProviderAuthenticationResult> Authenticate(string username, string password)
{
throw new NotImplementedException();
}
// This is the verson that we need to use for local users. Because reasons.
public Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser)
{
bool success = false;
if (resolvedUser == null)
{
throw new Exception("Invalid username or password");
}
var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase);
// As long as jellyfin supports passwordless users, we need this little block here to accomodate
if (IsPasswordEmpty(resolvedUser, password))
{
return Task.FromResult(new ProviderAuthenticationResult
{
Username = username
});
}
ConvertPasswordFormat(resolvedUser);
byte[] passwordbytes = Encoding.UTF8.GetBytes(password);
PasswordHash readyHash = new PasswordHash(resolvedUser.Password);
byte[] calculatedHash;
string calculatedHashString;
if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id))
{
if (string.IsNullOrEmpty(readyHash.Salt))
{
calculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes);
calculatedHashString = BitConverter.ToString(calculatedHash).Replace("-", string.Empty);
}
else
{
calculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes, readyHash.SaltBytes);
calculatedHashString = BitConverter.ToString(calculatedHash).Replace("-", string.Empty);
}
if (calculatedHashString == readyHash.Hash)
{
success = true;
// throw new Exception("Invalid username or password");
}
}
else
{
throw new Exception(string.Format($"Requested crypto method not available in provider: {readyHash.Id}"));
}
// var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase);
if (!success)
{
@ -44,46 +89,86 @@ namespace Emby.Server.Implementations.Library
});
}
// This allows us to move passwords forward to the newformat without breaking. They are still insecure, unsalted, and dumb before a password change
// but at least they are in the new format.
private void ConvertPasswordFormat(User user)
{
if (string.IsNullOrEmpty(user.Password))
{
return;
}
if (!user.Password.Contains("$"))
{
string hash = user.Password;
user.Password = string.Format("$SHA1${0}", hash);
}
if (user.EasyPassword != null && !user.EasyPassword.Contains("$"))
{
string hash = user.EasyPassword;
user.EasyPassword = string.Format("$SHA1${0}", hash);
}
}
public Task<bool> HasPassword(User user)
{
var hasConfiguredPassword = !IsPasswordEmpty(user, GetPasswordHash(user));
return Task.FromResult(hasConfiguredPassword);
}
private bool IsPasswordEmpty(User user, string passwordHash)
private bool IsPasswordEmpty(User user, string password)
{
return string.Equals(passwordHash, GetEmptyHashedString(user), StringComparison.OrdinalIgnoreCase);
return (string.IsNullOrEmpty(user.Password) && string.IsNullOrEmpty(password));
}
public Task ChangePassword(User user, string newPassword)
{
string newPasswordHash = null;
if (newPassword != null)
ConvertPasswordFormat(user);
// This is needed to support changing a no password user to a password user
if (string.IsNullOrEmpty(user.Password))
{
newPasswordHash = GetHashedString(user, newPassword);
PasswordHash newPasswordHash = new PasswordHash(_cryptographyProvider);
newPasswordHash.SaltBytes = _cryptographyProvider.GenerateSalt();
newPasswordHash.Salt = PasswordHash.ConvertToByteString(newPasswordHash.SaltBytes);
newPasswordHash.Id = _cryptographyProvider.DefaultHashMethod;
newPasswordHash.Hash = GetHashedStringChangeAuth(newPassword, newPasswordHash);
user.Password = newPasswordHash.ToString();
return Task.CompletedTask;
}
if (string.IsNullOrWhiteSpace(newPasswordHash))
PasswordHash passwordHash = new PasswordHash(user.Password);
if (passwordHash.Id == "SHA1" && string.IsNullOrEmpty(passwordHash.Salt))
{
throw new ArgumentNullException(nameof(newPasswordHash));
passwordHash.SaltBytes = _cryptographyProvider.GenerateSalt();
passwordHash.Salt = PasswordHash.ConvertToByteString(passwordHash.SaltBytes);
passwordHash.Id = _cryptographyProvider.DefaultHashMethod;
passwordHash.Hash = GetHashedStringChangeAuth(newPassword, passwordHash);
}
else if (newPassword != null)
{
passwordHash.Hash = GetHashedString(user, newPassword);
}
user.Password = newPasswordHash;
if (string.IsNullOrWhiteSpace(passwordHash.Hash))
{
throw new ArgumentNullException(nameof(passwordHash.Hash));
}
user.Password = passwordHash.ToString();
return Task.CompletedTask;
}
public string GetPasswordHash(User user)
{
return string.IsNullOrEmpty(user.Password)
? GetEmptyHashedString(user)
: user.Password;
return user.Password;
}
public string GetEmptyHashedString(User user)
public string GetHashedStringChangeAuth(string newPassword, PasswordHash passwordHash)
{
return GetHashedString(user, string.Empty);
passwordHash.HashBytes = Encoding.UTF8.GetBytes(newPassword);
return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash));
}
/// <summary>
@ -91,14 +176,28 @@ namespace Emby.Server.Implementations.Library
/// </summary>
public string GetHashedString(User user, string str)
{
var salt = user.Salt;
if (salt != null)
PasswordHash passwordHash;
if (string.IsNullOrEmpty(user.Password))
{
// return BCrypt.HashPassword(str, salt);
passwordHash = new PasswordHash(_cryptographyProvider);
}
else
{
ConvertPasswordFormat(user);
passwordHash = new PasswordHash(user.Password);
}
// legacy
return BitConverter.ToString(_cryptographyProvider.ComputeSHA1(Encoding.UTF8.GetBytes(str))).Replace("-", string.Empty);
if (passwordHash.SaltBytes != null)
{
// the password is modern format with PBKDF and we should take advantage of that
passwordHash.HashBytes = Encoding.UTF8.GetBytes(str);
return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash));
}
else
{
// the password has no salt and should be called with the older method for safety
return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash.Id, Encoding.UTF8.GetBytes(str)));
}
}
}
}

View file

@ -4,6 +4,7 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Events;
@ -213,22 +214,17 @@ namespace Emby.Server.Implementations.Library
}
}
public bool IsValidUsername(string username)
public static bool IsValidUsername(string username)
{
// Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)
foreach (var currentChar in username)
{
if (!IsValidUsernameCharacter(currentChar))
{
return false;
}
}
return true;
//This is some regex that matches only on unicode "word" characters, as well as -, _ and @
//In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
// Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)
return Regex.IsMatch(username, "^[\\w-'._@]*$");
}
private static bool IsValidUsernameCharacter(char i)
{
return !char.Equals(i, '<') && !char.Equals(i, '>');
return IsValidUsername(i.ToString());
}
public string MakeValidUsername(string username)
@ -475,15 +471,10 @@ namespace Emby.Server.Implementations.Library
private string GetLocalPasswordHash(User user)
{
return string.IsNullOrEmpty(user.EasyPassword)
? _defaultAuthenticationProvider.GetEmptyHashedString(user)
? null
: user.EasyPassword;
}
private bool IsPasswordEmpty(User user, string passwordHash)
{
return string.Equals(passwordHash, _defaultAuthenticationProvider.GetEmptyHashedString(user), StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Loads the users from the repository
/// </summary>
@ -526,14 +517,14 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentNullException(nameof(user));
}
var hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result;
var hasConfiguredEasyPassword = !IsPasswordEmpty(user, GetLocalPasswordHash(user));
bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result;
bool hasConfiguredEasyPassword = string.IsNullOrEmpty(GetLocalPasswordHash(user));
var hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ?
bool hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ?
hasConfiguredEasyPassword :
hasConfiguredPassword;
var dto = new UserDto
UserDto dto = new UserDto
{
Id = user.Id,
Name = user.Name,
@ -552,7 +543,7 @@ namespace Emby.Server.Implementations.Library
dto.EnableAutoLogin = true;
}
var image = user.GetImageInfo(ImageType.Primary, 0);
ItemImageInfo image = user.GetImageInfo(ImageType.Primary, 0);
if (image != null)
{
@ -688,7 +679,7 @@ namespace Emby.Server.Implementations.Library
if (!IsValidUsername(name))
{
throw new ArgumentException("Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)");
throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)");
}
if (Users.Any(u => u.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))

View file

@ -62,10 +62,6 @@ namespace Emby.Server.Implementations.Localization
{
const string ratingsResource = "Emby.Server.Implementations.Localization.Ratings.";
Directory.CreateDirectory(LocalizationPath);
var existingFiles = GetRatingsFiles(LocalizationPath).Select(Path.GetFileName);
// Extract from the assembly
foreach (var resource in _assembly.GetManifestResourceNames())
{
@ -74,100 +70,41 @@ namespace Emby.Server.Implementations.Localization
continue;
}
string filename = "ratings-" + resource.Substring(ratingsResource.Length);
string countryCode = resource.Substring(ratingsResource.Length, 2);
var dict = new Dictionary<string, ParentalRating>(StringComparer.OrdinalIgnoreCase);
if (existingFiles.Contains(filename))
using (var str = _assembly.GetManifestResourceStream(resource))
using (var reader = new StreamReader(str))
{
continue;
}
using (var stream = _assembly.GetManifestResourceStream(resource))
{
string target = Path.Combine(LocalizationPath, filename);
_logger.LogInformation("Extracting ratings to {0}", target);
using (var fs = _fileSystem.GetFileStream(target, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read))
string line;
while ((line = await reader.ReadLineAsync()) != null)
{
await stream.CopyToAsync(fs);
if (string.IsNullOrWhiteSpace(line))
{
continue;
}
string[] parts = line.Split(',');
if (parts.Length == 2
&& int.TryParse(parts[1], NumberStyles.Integer, UsCulture, out var value))
{
dict.Add(parts[0], new ParentalRating { Name = parts[0], Value = value });
}
#if DEBUG
else
{
_logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode);
}
#endif
}
}
}
foreach (var file in GetRatingsFiles(LocalizationPath))
{
await LoadRatings(file);
_allParentalRatings[countryCode] = dict;
}
LoadAdditionalRatings();
await LoadCultures();
}
private void LoadAdditionalRatings()
{
LoadRatings("au", new[]
{
new ParentalRating("AU-G", 1),
new ParentalRating("AU-PG", 5),
new ParentalRating("AU-M", 6),
new ParentalRating("AU-MA15+", 7),
new ParentalRating("AU-M15+", 8),
new ParentalRating("AU-R18+", 9),
new ParentalRating("AU-X18+", 10),
new ParentalRating("AU-RC", 11)
});
LoadRatings("be", new[]
{
new ParentalRating("BE-AL", 1),
new ParentalRating("BE-MG6", 2),
new ParentalRating("BE-6", 3),
new ParentalRating("BE-9", 5),
new ParentalRating("BE-12", 6),
new ParentalRating("BE-16", 8)
});
LoadRatings("de", new[]
{
new ParentalRating("DE-0", 1),
new ParentalRating("FSK-0", 1),
new ParentalRating("DE-6", 5),
new ParentalRating("FSK-6", 5),
new ParentalRating("DE-12", 7),
new ParentalRating("FSK-12", 7),
new ParentalRating("DE-16", 8),
new ParentalRating("FSK-16", 8),
new ParentalRating("DE-18", 9),
new ParentalRating("FSK-18", 9)
});
LoadRatings("ru", new[]
{
new ParentalRating("RU-0+", 1),
new ParentalRating("RU-6+", 3),
new ParentalRating("RU-12+", 7),
new ParentalRating("RU-16+", 9),
new ParentalRating("RU-18+", 10)
});
}
private void LoadRatings(string country, ParentalRating[] ratings)
{
_allParentalRatings[country] = ratings.ToDictionary(i => i.Name);
}
private IEnumerable<string> GetRatingsFiles(string directory)
=> _fileSystem.GetFilePaths(directory, false)
.Where(i => string.Equals(Path.GetExtension(i), ".csv", StringComparison.OrdinalIgnoreCase))
.Where(i => Path.GetFileName(i).StartsWith("ratings-", StringComparison.OrdinalIgnoreCase));
/// <summary>
/// Gets the localization path.
/// </summary>
/// <value>The localization path.</value>
public string LocalizationPath
=> Path.Combine(_configurationManager.ApplicationPaths.ProgramDataPath, "localization");
public string NormalizeFormKD(string text)
=> text.Normalize(NormalizationForm.FormKD);
@ -288,47 +225,6 @@ namespace Emby.Server.Implementations.Localization
return value;
}
/// <summary>
/// Loads the ratings.
/// </summary>
/// <param name="file">The file.</param>
/// <returns>Dictionary{System.StringParentalRating}.</returns>
private async Task LoadRatings(string file)
{
Dictionary<string, ParentalRating> dict
= new Dictionary<string, ParentalRating>(StringComparer.OrdinalIgnoreCase);
using (var str = File.OpenRead(file))
using (var reader = new StreamReader(str))
{
string line;
while ((line = await reader.ReadLineAsync()) != null)
{
if (string.IsNullOrWhiteSpace(line))
{
continue;
}
string[] parts = line.Split(',');
if (parts.Length == 2
&& int.TryParse(parts[1], NumberStyles.Integer, UsCulture, out var value))
{
dict.Add(parts[0], (new ParentalRating { Name = parts[0], Value = value }));
}
#if DEBUG
else
{
_logger.LogWarning("Misformed line in {Path}", file);
}
#endif
}
}
var countryCode = Path.GetFileNameWithoutExtension(file).Split('-')[1];
_allParentalRatings[countryCode] = dict;
}
private static readonly string[] _unratedValues = { "n/a", "unrated", "not rated" };
/// <summary>

View file

@ -0,0 +1,8 @@
AU-G,1
AU-PG,5
AU-M,6
AU-MA15+,7
AU-M15+,8
AU-R18+,9
AU-X18+,10
AU-RC,11
1 AU-G 1
2 AU-PG 5
3 AU-M 6
4 AU-MA15+ 7
5 AU-M15+ 8
6 AU-R18+ 9
7 AU-X18+ 10
8 AU-RC 11

View file

@ -0,0 +1,6 @@
BE-AL,1
BE-MG6,2
BE-6,3
BE-9,5
BE-12,6
BE-16,8
1 BE-AL 1
2 BE-MG6 2
3 BE-6 3
4 BE-9 5
5 BE-12 6
6 BE-16 8

View file

@ -0,0 +1,10 @@
DE-0,1
FSK-0,1
DE-6,5
FSK-6,5
DE-12,7
FSK-12,7
DE-16,8
FSK-16,8
DE-18,9
FSK-18,9
1 DE-0 1
2 FSK-0 1
3 DE-6 5
4 FSK-6 5
5 DE-12 7
6 FSK-12 7
7 DE-16 8
8 FSK-16 8
9 DE-18 9
10 FSK-18 9

View file

@ -0,0 +1,5 @@
RU-0+,1
RU-6+,3
RU-12+,7
RU-16+,9
RU-18+,10
1 RU-0+ 1
2 RU-6+ 3
3 RU-12+ 7
4 RU-16+ 9
5 RU-18+ 10

View file

@ -79,13 +79,13 @@ namespace Emby.Server.Implementations.Networking
private IpAddressInfo[] _localIpAddresses;
private readonly object _localIpAddressSyncLock = new object();
public IpAddressInfo[] GetLocalIpAddresses()
public IpAddressInfo[] GetLocalIpAddresses(bool ignoreVirtualInterface = true)
{
lock (_localIpAddressSyncLock)
{
if (_localIpAddresses == null)
{
var addresses = GetLocalIpAddressesInternal().Result.Select(ToIpAddressInfo).ToArray();
var addresses = GetLocalIpAddressesInternal(ignoreVirtualInterface).Result.Select(ToIpAddressInfo).ToArray();
_localIpAddresses = addresses;
@ -95,9 +95,9 @@ namespace Emby.Server.Implementations.Networking
}
}
private async Task<List<IPAddress>> GetLocalIpAddressesInternal()
private async Task<List<IPAddress>> GetLocalIpAddressesInternal(bool ignoreVirtualInterface)
{
var list = GetIPsDefault()
var list = GetIPsDefault(ignoreVirtualInterface)
.ToList();
if (list.Count == 0)
@ -383,7 +383,7 @@ namespace Emby.Server.Implementations.Networking
return Dns.GetHostAddressesAsync(hostName);
}
private List<IPAddress> GetIPsDefault()
private List<IPAddress> GetIPsDefault(bool ignoreVirtualInterface)
{
NetworkInterface[] interfaces;
@ -414,7 +414,7 @@ namespace Emby.Server.Implementations.Networking
// Try to exclude virtual adapters
// http://stackoverflow.com/questions/8089685/c-sharp-finding-my-machines-local-ip-address-and-not-the-vms
var addr = ipProperties.GatewayAddresses.FirstOrDefault();
if (addr == null || string.Equals(addr.Address.ToString(), "0.0.0.0", StringComparison.OrdinalIgnoreCase))
if (addr == null || ignoreVirtualInterface && string.Equals(addr.Address.ToString(), "0.0.0.0", StringComparison.OrdinalIgnoreCase))
{
return new List<IPAddress>();
}
@ -636,6 +636,66 @@ namespace Emby.Server.Implementations.Networking
return false;
}
public bool IsInSameSubnet(IpAddressInfo address1, IpAddressInfo address2, IpAddressInfo subnetMask)
{
IPAddress network1 = GetNetworkAddress(ToIPAddress(address1), ToIPAddress(subnetMask));
IPAddress network2 = GetNetworkAddress(ToIPAddress(address2), ToIPAddress(subnetMask));
return network1.Equals(network2);
}
private IPAddress GetNetworkAddress(IPAddress address, IPAddress subnetMask)
{
byte[] ipAdressBytes = address.GetAddressBytes();
byte[] subnetMaskBytes = subnetMask.GetAddressBytes();
if (ipAdressBytes.Length != subnetMaskBytes.Length)
{
throw new ArgumentException("Lengths of IP address and subnet mask do not match.");
}
byte[] broadcastAddress = new byte[ipAdressBytes.Length];
for (int i = 0; i < broadcastAddress.Length; i++)
{
broadcastAddress[i] = (byte)(ipAdressBytes[i] & (subnetMaskBytes[i]));
}
return new IPAddress(broadcastAddress);
}
public IpAddressInfo GetLocalIpSubnetMask(IpAddressInfo address)
{
NetworkInterface[] interfaces;
IPAddress ipaddress = ToIPAddress(address);
try
{
var validStatuses = new[] { OperationalStatus.Up, OperationalStatus.Unknown };
interfaces = NetworkInterface.GetAllNetworkInterfaces()
.Where(i => validStatuses.Contains(i.OperationalStatus))
.ToArray();
}
catch (Exception ex)
{
Logger.LogError(ex, "Error in GetAllNetworkInterfaces");
return null;
}
foreach (NetworkInterface ni in interfaces)
{
if (ni.GetIPProperties().GatewayAddresses.FirstOrDefault() != null)
{
foreach (UnicastIPAddressInformation ip in ni.GetIPProperties().UnicastAddresses)
{
if (ip.Address.Equals(ipaddress) && ip.IPv4Mask != null)
{
return ToIpAddressInfo(ip.IPv4Mask);
}
}
}
}
return null;
}
public static IpEndPointInfo ToIpEndPointInfo(IPEndPoint endpoint)
{
if (endpoint == null)

View file

@ -1,57 +0,0 @@
using System;
using System.Text;
using MediaBrowser.Controller.Security;
namespace Emby.Server.Implementations.Security
{
public class EncryptionManager : IEncryptionManager
{
/// <summary>
/// Encrypts the string.
/// </summary>
/// <param name="value">The value.</param>
/// <returns>System.String.</returns>
/// <exception cref="ArgumentNullException">value</exception>
public string EncryptString(string value)
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
return EncryptStringUniversal(value);
}
/// <summary>
/// Decrypts the string.
/// </summary>
/// <param name="value">The value.</param>
/// <returns>System.String.</returns>
/// <exception cref="ArgumentNullException">value</exception>
public string DecryptString(string value)
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
return DecryptStringUniversal(value);
}
private static string EncryptStringUniversal(string value)
{
// Yes, this isn't good, but ProtectedData in mono is throwing exceptions, so use this for now
var bytes = Encoding.UTF8.GetBytes(value);
return Convert.ToBase64String(bytes);
}
private static string DecryptStringUniversal(string value)
{
// Yes, this isn't good, but ProtectedData in mono is throwing exceptions, so use this for now
var bytes = Convert.FromBase64String(value);
return Encoding.UTF8.GetString(bytes, 0, bytes.Length);
}
}
}

View file

@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Services
private const char ComponentSeperator = '.';
private const string VariablePrefix = "{";
readonly bool[] componentsWithSeparators;
private readonly bool[] componentsWithSeparators;
private readonly string restPath;
public bool IsWildCardPath { get; private set; }
@ -54,10 +54,6 @@ namespace Emby.Server.Implementations.Services
public string Description { get; private set; }
public bool IsHidden { get; private set; }
public int Priority { get; set; } //passed back to RouteAttribute
public IEnumerable<string> PathVariables => this.variablesNames.Where(e => !string.IsNullOrWhiteSpace(e));
public static string[] GetPathPartsForMatching(string pathInfo)
{
return pathInfo.ToLowerInvariant().Split(new[] { PathSeperatorChar }, StringSplitOptions.RemoveEmptyEntries);
@ -83,9 +79,12 @@ namespace Emby.Server.Implementations.Services
{
list.Add(hashPrefix + part);
var subParts = part.Split(ComponentSeperator);
if (subParts.Length == 1) continue;
if (part.IndexOf(ComponentSeperator) == -1)
{
continue;
}
var subParts = part.Split(ComponentSeperator);
foreach (var subPart in subParts)
{
list.Add(hashPrefix + subPart);
@ -114,7 +113,7 @@ namespace Emby.Server.Implementations.Services
{
if (string.IsNullOrEmpty(component)) continue;
if (StringContains(component, VariablePrefix)
if (component.IndexOf(VariablePrefix, StringComparison.OrdinalIgnoreCase) != -1
&& component.IndexOf(ComponentSeperator) != -1)
{
hasSeparators.Add(true);
@ -165,7 +164,11 @@ namespace Emby.Server.Implementations.Services
for (var i = 0; i < components.Length - 1; i++)
{
if (!this.isWildcard[i]) continue;
if (!this.isWildcard[i])
{
continue;
}
if (this.literalsToMatch[i + 1] == null)
{
throw new ArgumentException(
@ -173,7 +176,7 @@ namespace Emby.Server.Implementations.Services
}
}
this.wildcardCount = this.isWildcard.Count(x => x);
this.wildcardCount = this.isWildcard.Length;
this.IsWildCardPath = this.wildcardCount > 0;
this.FirstMatchHashKey = !this.IsWildCardPath
@ -181,19 +184,14 @@ namespace Emby.Server.Implementations.Services
: WildCardChar + PathSeperator + firstLiteralMatch;
this.typeDeserializer = new StringMapTypeDeserializer(createInstanceFn, getParseFn, this.RequestType);
RegisterCaseInsenstivePropertyNameMappings();
_propertyNamesMap = new HashSet<string>(
GetSerializableProperties(RequestType).Select(x => x.Name),
StringComparer.OrdinalIgnoreCase);
}
private void RegisterCaseInsenstivePropertyNameMappings()
internal static string[] IgnoreAttributesNamed = new[]
{
foreach (var propertyInfo in GetSerializableProperties(RequestType))
{
var propertyName = propertyInfo.Name;
propertyNamesMap.Add(propertyName.ToLowerInvariant(), propertyName);
}
}
internal static string[] IgnoreAttributesNamed = new[] {
"IgnoreDataMemberAttribute",
"JsonIgnoreAttribute"
};
@ -201,19 +199,12 @@ namespace Emby.Server.Implementations.Services
private static Type excludeType = typeof(Stream);
internal static List<PropertyInfo> GetSerializableProperties(Type type)
internal static IEnumerable<PropertyInfo> GetSerializableProperties(Type type)
{
var list = new List<PropertyInfo>();
var props = GetPublicProperties(type);
foreach (var prop in props)
foreach (var prop in GetPublicProperties(type))
{
if (prop.GetMethod == null)
{
continue;
}
if (excludeType == prop.PropertyType)
if (prop.GetMethod == null
|| excludeType == prop.PropertyType)
{
continue;
}
@ -230,23 +221,21 @@ namespace Emby.Server.Implementations.Services
if (!ignored)
{
list.Add(prop);
yield return prop;
}
}
// else return those properties that are not decorated with IgnoreDataMember
return list;
}
private static List<PropertyInfo> GetPublicProperties(Type type)
private static IEnumerable<PropertyInfo> GetPublicProperties(Type type)
{
if (type.GetTypeInfo().IsInterface)
if (type.IsInterface)
{
var propertyInfos = new List<PropertyInfo>();
var considered = new List<Type>();
var considered = new List<Type>()
{
type
};
var queue = new Queue<Type>();
considered.Add(type);
queue.Enqueue(type);
while (queue.Count > 0)
@ -254,15 +243,16 @@ namespace Emby.Server.Implementations.Services
var subType = queue.Dequeue();
foreach (var subInterface in subType.GetTypeInfo().ImplementedInterfaces)
{
if (considered.Contains(subInterface)) continue;
if (considered.Contains(subInterface))
{
continue;
}
considered.Add(subInterface);
queue.Enqueue(subInterface);
}
var typeProperties = GetTypesPublicProperties(subType);
var newPropertyInfos = typeProperties
var newPropertyInfos = GetTypesPublicProperties(subType)
.Where(x => !propertyInfos.Contains(x));
propertyInfos.InsertRange(0, newPropertyInfos);
@ -271,28 +261,22 @@ namespace Emby.Server.Implementations.Services
return propertyInfos;
}
var list = new List<PropertyInfo>();
foreach (var t in GetTypesPublicProperties(type))
{
if (t.GetIndexParameters().Length == 0)
{
list.Add(t);
}
}
return list;
return GetTypesPublicProperties(type)
.Where(x => x.GetIndexParameters().Length == 0);
}
private static PropertyInfo[] GetTypesPublicProperties(Type subType)
private static IEnumerable<PropertyInfo> GetTypesPublicProperties(Type subType)
{
var pis = new List<PropertyInfo>();
foreach (var pi in subType.GetRuntimeProperties())
{
var mi = pi.GetMethod ?? pi.SetMethod;
if (mi != null && mi.IsStatic) continue;
pis.Add(pi);
if (mi != null && mi.IsStatic)
{
continue;
}
yield return pi;
}
return pis.ToArray();
}
/// <summary>
@ -302,7 +286,7 @@ namespace Emby.Server.Implementations.Services
private readonly StringMapTypeDeserializer typeDeserializer;
private readonly Dictionary<string, string> propertyNamesMap = new Dictionary<string, string>();
private readonly HashSet<string> _propertyNamesMap;
public int MatchScore(string httpMethod, string[] withPathInfoParts)
{
@ -312,13 +296,10 @@ namespace Emby.Server.Implementations.Services
return -1;
}
var score = 0;
//Routes with least wildcard matches get the highest score
score += Math.Max((100 - wildcardMatchCount), 1) * 1000;
//Routes with less variable (and more literal) matches
score += Math.Max((10 - VariableArgsCount), 1) * 100;
var score = Math.Max((100 - wildcardMatchCount), 1) * 1000
//Routes with less variable (and more literal) matches
+ Math.Max((10 - VariableArgsCount), 1) * 100;
//Exact verb match is better than ANY
if (Verbs.Length == 1 && string.Equals(httpMethod, Verbs[0], StringComparison.OrdinalIgnoreCase))
@ -333,11 +314,6 @@ namespace Emby.Server.Implementations.Services
return score;
}
private bool StringContains(string str1, string str2)
{
return str1.IndexOf(str2, StringComparison.OrdinalIgnoreCase) != -1;
}
/// <summary>
/// For performance withPathInfoParts should already be a lower case string
/// to minimize redundant matching operations.
@ -374,7 +350,8 @@ namespace Emby.Server.Implementations.Services
if (i < this.TotalComponentsCount - 1)
{
// Continue to consume up until a match with the next literal
while (pathIx < withPathInfoParts.Length && !LiteralsEqual(withPathInfoParts[pathIx], this.literalsToMatch[i + 1]))
while (pathIx < withPathInfoParts.Length
&& !string.Equals(withPathInfoParts[pathIx], this.literalsToMatch[i + 1], StringComparison.InvariantCultureIgnoreCase))
{
pathIx++;
wildcardMatchCount++;
@ -403,10 +380,12 @@ namespace Emby.Server.Implementations.Services
continue;
}
if (withPathInfoParts.Length <= pathIx || !LiteralsEqual(withPathInfoParts[pathIx], literalToMatch))
if (withPathInfoParts.Length <= pathIx
|| !string.Equals(withPathInfoParts[pathIx], literalToMatch, StringComparison.InvariantCultureIgnoreCase))
{
return false;
}
pathIx++;
}
}
@ -414,35 +393,26 @@ namespace Emby.Server.Implementations.Services
return pathIx == withPathInfoParts.Length;
}
private static bool LiteralsEqual(string str1, string str2)
{
// Most cases
if (string.Equals(str1, str2, StringComparison.OrdinalIgnoreCase))
{
return true;
}
// Handle turkish i
str1 = str1.ToUpperInvariant();
str2 = str2.ToUpperInvariant();
// Invariant IgnoreCase would probably be better but it's not available in PCL
return string.Equals(str1, str2, StringComparison.CurrentCultureIgnoreCase);
}
private bool ExplodeComponents(ref string[] withPathInfoParts)
{
var totalComponents = new List<string>();
for (var i = 0; i < withPathInfoParts.Length; i++)
{
var component = withPathInfoParts[i];
if (string.IsNullOrEmpty(component)) continue;
if (string.IsNullOrEmpty(component))
{
continue;
}
if (this.PathComponentsCount != this.TotalComponentsCount
&& this.componentsWithSeparators[i])
{
var subComponents = component.Split(ComponentSeperator);
if (subComponents.Length < 2) return false;
if (subComponents.Length < 2)
{
return false;
}
totalComponents.AddRange(subComponents);
}
else
@ -483,7 +453,7 @@ namespace Emby.Server.Implementations.Services
continue;
}
if (!this.propertyNamesMap.TryGetValue(variableName.ToLowerInvariant(), out var propertyNameOnRequest))
if (!this._propertyNamesMap.Contains(variableName))
{
if (string.Equals("ignore", variableName, StringComparison.OrdinalIgnoreCase))
{
@ -507,6 +477,7 @@ namespace Emby.Server.Implementations.Services
{
sb.Append(PathSeperatorChar + requestComponents[j]);
}
value = sb.ToString();
}
else
@ -517,13 +488,13 @@ namespace Emby.Server.Implementations.Services
var stopLiteral = i == this.TotalComponentsCount - 1 ? null : this.literalsToMatch[i + 1];
if (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
{
var sb = new StringBuilder();
sb.Append(value);
var sb = new StringBuilder(value);
pathIx++;
while (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
{
sb.Append(PathSeperatorChar + requestComponents[pathIx++]);
}
value = sb.ToString();
}
else
@ -538,7 +509,7 @@ namespace Emby.Server.Implementations.Services
pathIx++;
}
requestKeyValuesMap[propertyNameOnRequest] = value;
requestKeyValuesMap[variableName] = value;
}
if (queryStringAndFormData != null)

View file

@ -11,15 +11,16 @@ namespace Emby.Server.Implementations.Services
{
internal class PropertySerializerEntry
{
public PropertySerializerEntry(Action<object, object> propertySetFn, Func<string, object> propertyParseStringFn)
public PropertySerializerEntry(Action<object, object> propertySetFn, Func<string, object> propertyParseStringFn, Type propertyType)
{
PropertySetFn = propertySetFn;
PropertyParseStringFn = propertyParseStringFn;
PropertyType = PropertyType;
}
public Action<object, object> PropertySetFn;
public Func<string, object> PropertyParseStringFn;
public Type PropertyType;
public Action<object, object> PropertySetFn { get; private set; }
public Func<string, object> PropertyParseStringFn { get; private set; }
public Type PropertyType { get; private set; }
}
private readonly Type type;
@ -29,7 +30,9 @@ namespace Emby.Server.Implementations.Services
public Func<string, object> GetParseFn(Type propertyType)
{
if (propertyType == typeof(string))
{
return s => s;
}
return _GetParseFn(propertyType);
}
@ -48,7 +51,7 @@ namespace Emby.Server.Implementations.Services
var propertySetFn = TypeAccessor.GetSetPropertyMethod(type, propertyInfo);
var propertyType = propertyInfo.PropertyType;
var propertyParseStringFn = GetParseFn(propertyType);
var propertySerializer = new PropertySerializerEntry(propertySetFn, propertyParseStringFn) { PropertyType = propertyType };
var propertySerializer = new PropertySerializerEntry(propertySetFn, propertyParseStringFn, propertyType);
propertySetterMap[propertyInfo.Name] = propertySerializer;
}
@ -56,34 +59,21 @@ namespace Emby.Server.Implementations.Services
public object PopulateFromMap(object instance, IDictionary<string, string> keyValuePairs)
{
string propertyName = null;
string propertyTextValue = null;
PropertySerializerEntry propertySerializerEntry = null;
if (instance == null)
{
instance = _CreateInstanceFn(type);
}
foreach (var pair in keyValuePairs)
{
propertyName = pair.Key;
propertyTextValue = pair.Value;
string propertyName = pair.Key;
string propertyTextValue = pair.Value;
if (string.IsNullOrEmpty(propertyTextValue))
{
continue;
}
if (!propertySetterMap.TryGetValue(propertyName, out propertySerializerEntry))
{
if (propertyName == "v")
{
continue;
}
continue;
}
if (propertySerializerEntry.PropertySetFn == null)
if (string.IsNullOrEmpty(propertyTextValue)
|| !propertySetterMap.TryGetValue(propertyName, out propertySerializerEntry)
|| propertySerializerEntry.PropertySetFn == null)
{
continue;
}
@ -99,6 +89,7 @@ namespace Emby.Server.Implementations.Services
{
continue;
}
propertySerializerEntry.PropertySetFn(instance, value);
}
@ -107,7 +98,11 @@ namespace Emby.Server.Implementations.Services
public static string LeftPart(string strVal, char needle)
{
if (strVal == null) return null;
if (strVal == null)
{
return null;
}
var pos = strVal.IndexOf(needle);
return pos == -1
? strVal
@ -119,7 +114,10 @@ namespace Emby.Server.Implementations.Services
{
public static Action<object, object> GetSetPropertyMethod(Type type, PropertyInfo propertyInfo)
{
if (!propertyInfo.CanWrite || propertyInfo.GetIndexParameters().Length > 0) return null;
if (!propertyInfo.CanWrite || propertyInfo.GetIndexParameters().Length > 0)
{
return null;
}
var setMethodInfo = propertyInfo.SetMethod;
return (instance, value) => setMethodInfo.Invoke(instance, new[] { value });

View file

@ -1090,7 +1090,7 @@ namespace Emby.Server.Implementations.Session
await SendMessageToSession(session, "Play", command, cancellationToken).ConfigureAwait(false);
}
private IList<BaseItem> TranslateItemForPlayback(Guid id, User user)
private IEnumerable<BaseItem> TranslateItemForPlayback(Guid id, User user)
{
var item = _libraryManager.GetItemById(id);

View file

@ -13,9 +13,9 @@ namespace Emby.Server.Implementations.SocketSharp
{
public partial class WebSocketSharpRequest : IHttpRequest
{
internal static string GetParameter(string header, string attr)
internal static string GetParameter(ReadOnlySpan<char> header, string attr)
{
int ap = header.IndexOf(attr, StringComparison.Ordinal);
int ap = header.IndexOf(attr.AsSpan(), StringComparison.Ordinal);
if (ap == -1)
{
return null;
@ -33,18 +33,19 @@ namespace Emby.Server.Implementations.SocketSharp
ending = ' ';
}
int end = header.IndexOf(ending, ap + 1);
var slice = header.Slice(ap + 1);
int end = slice.IndexOf(ending);
if (end == -1)
{
return ending == '"' ? null : header.Substring(ap);
return ending == '"' ? null : header.Slice(ap).ToString();
}
return header.Substring(ap + 1, end - ap - 1);
return slice.Slice(0, end - ap - 1).ToString();
}
private async Task LoadMultiPart(WebROCollection form)
{
string boundary = GetParameter(ContentType, "; boundary=");
string boundary = GetParameter(ContentType.AsSpan(), "; boundary=");
if (boundary == null)
{
return;
@ -377,17 +378,17 @@ namespace Emby.Server.Implementations.SocketSharp
}
var elem = new Element();
string header;
while ((header = ReadHeaders()) != null)
ReadOnlySpan<char> header;
while ((header = ReadHeaders().AsSpan()) != null)
{
if (header.StartsWith("Content-Disposition:", StringComparison.OrdinalIgnoreCase))
if (header.StartsWith("Content-Disposition:".AsSpan(), StringComparison.OrdinalIgnoreCase))
{
elem.Name = GetContentDispositionAttribute(header, "name");
elem.Filename = StripPath(GetContentDispositionAttributeWithEncoding(header, "filename"));
}
else if (header.StartsWith("Content-Type:", StringComparison.OrdinalIgnoreCase))
else if (header.StartsWith("Content-Type:".AsSpan(), StringComparison.OrdinalIgnoreCase))
{
elem.ContentType = header.Substring("Content-Type:".Length).Trim();
elem.ContentType = header.Slice("Content-Type:".Length).Trim().ToString();
elem.Encoding = GetEncoding(elem.ContentType);
}
}
@ -435,16 +436,16 @@ namespace Emby.Server.Implementations.SocketSharp
return sb.ToString();
}
private static string GetContentDispositionAttribute(string l, string name)
private static string GetContentDispositionAttribute(ReadOnlySpan<char> l, string name)
{
int idx = l.IndexOf(name + "=\"", StringComparison.Ordinal);
int idx = l.IndexOf((name + "=\"").AsSpan(), StringComparison.Ordinal);
if (idx < 0)
{
return null;
}
int begin = idx + name.Length + "=\"".Length;
int end = l.IndexOf('"', begin);
int end = l.Slice(begin).IndexOf('"');
if (end < 0)
{
return null;
@ -455,19 +456,19 @@ namespace Emby.Server.Implementations.SocketSharp
return string.Empty;
}
return l.Substring(begin, end - begin);
return l.Slice(begin, end - begin).ToString();
}
private string GetContentDispositionAttributeWithEncoding(string l, string name)
private string GetContentDispositionAttributeWithEncoding(ReadOnlySpan<char> l, string name)
{
int idx = l.IndexOf(name + "=\"", StringComparison.Ordinal);
int idx = l.IndexOf((name + "=\"").AsSpan(), StringComparison.Ordinal);
if (idx < 0)
{
return null;
}
int begin = idx + name.Length + "=\"".Length;
int end = l.IndexOf('"', begin);
int end = l.Slice(begin).IndexOf('"');
if (end < 0)
{
return null;
@ -478,7 +479,7 @@ namespace Emby.Server.Implementations.SocketSharp
return string.Empty;
}
string temp = l.Substring(begin, end - begin);
ReadOnlySpan<char> temp = l.Slice(begin, end - begin);
byte[] source = new byte[temp.Length];
for (int i = temp.Length - 1; i >= 0; i--)
{

View file

@ -56,19 +56,37 @@ namespace Emby.Server.Implementations.SocketSharp
public string XRealIp => StringValues.IsNullOrEmpty(request.Headers["X-Real-IP"]) ? null : request.Headers["X-Real-IP"].ToString();
private string remoteIp;
public string RemoteIp
{
get
{
if (remoteIp != null)
{
return remoteIp;
}
public string RemoteIp =>
remoteIp ??
(remoteIp = CheckBadChars(XForwardedFor) ??
NormalizeIp(CheckBadChars(XRealIp) ??
(string.IsNullOrEmpty(request.HttpContext.Connection.RemoteIpAddress.ToString()) ? null : NormalizeIp(request.HttpContext.Connection.RemoteIpAddress.ToString()))));
var temp = CheckBadChars(XForwardedFor.AsSpan());
if (temp.Length != 0)
{
return remoteIp = temp.ToString();
}
temp = CheckBadChars(XRealIp.AsSpan());
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 };
// CheckBadChars - throws on invalid chars to be not found in header name/value
internal static string CheckBadChars(string name)
internal static ReadOnlySpan<char> CheckBadChars(ReadOnlySpan<char> name)
{
if (name == null || name.Length == 0)
if (name.Length == 0)
{
return name;
}
@ -99,7 +117,7 @@ namespace Emby.Server.Implementations.SocketSharp
}
else if (c == 127 || (c < ' ' && c != '\t'))
{
throw new ArgumentException("net_WebHeaderInvalidControlChars");
throw new ArgumentException("net_WebHeaderInvalidControlChars", nameof(name));
}
break;
@ -113,7 +131,7 @@ namespace Emby.Server.Implementations.SocketSharp
break;
}
throw new ArgumentException("net_WebHeaderInvalidCRLFChars");
throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name));
}
case 2:
@ -124,29 +142,29 @@ namespace Emby.Server.Implementations.SocketSharp
break;
}
throw new ArgumentException("net_WebHeaderInvalidCRLFChars");
throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name));
}
}
}
if (crlf != 0)
{
throw new ArgumentException("net_WebHeaderInvalidCRLFChars");
throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name));
}
return name;
}
private string NormalizeIp(string ip)
private ReadOnlySpan<char> NormalizeIp(ReadOnlySpan<char> ip)
{
if (!string.IsNullOrWhiteSpace(ip))
if (ip.Length != 0 && !ip.IsWhiteSpace())
{
// Handle ipv4 mapped to ipv6
const string srch = "::ffff:";
var index = ip.IndexOf(srch, StringComparison.OrdinalIgnoreCase);
var index = ip.IndexOf(srch.AsSpan(), StringComparison.OrdinalIgnoreCase);
if (index == 0)
{
ip = ip.Substring(srch.Length);
ip = ip.Slice(srch.Length);
}
}
@ -324,7 +342,7 @@ namespace Emby.Server.Implementations.SocketSharp
}
this.pathInfo = WebUtility.UrlDecode(pathInfo);
this.pathInfo = NormalizePathInfo(pathInfo, mode);
this.pathInfo = NormalizePathInfo(pathInfo, mode).ToString();
}
return this.pathInfo;
@ -436,7 +454,7 @@ namespace Emby.Server.Implementations.SocketSharp
public static Encoding GetEncoding(string contentTypeHeader)
{
var param = GetParameter(contentTypeHeader, "charset=");
var param = GetParameter(contentTypeHeader.AsSpan(), "charset=");
if (param == null)
{
return null;
@ -488,18 +506,18 @@ namespace Emby.Server.Implementations.SocketSharp
}
}
public static string NormalizePathInfo(string pathInfo, string handlerPath)
public static ReadOnlySpan<char> NormalizePathInfo(string pathInfo, string handlerPath)
{
if (handlerPath != null)
{
var trimmed = pathInfo.TrimStart('/');
if (trimmed.StartsWith(handlerPath, StringComparison.OrdinalIgnoreCase))
var trimmed = pathInfo.AsSpan().TrimStart('/');
if (trimmed.StartsWith(handlerPath.AsSpan(), StringComparison.OrdinalIgnoreCase))
{
return trimmed.Substring(handlerPath.Length);
return trimmed.Slice(handlerPath.Length).ToString().AsSpan();
}
}
return pathInfo;
return pathInfo.AsSpan();
}
}
}

View file

@ -495,9 +495,7 @@ namespace Emby.XmlTv.Classes
ParseMovieDbSystem(reader, result);
break;
case "SxxExx":
// TODO
// <episode-num system="SxxExx">S03E12</episode-num>
reader.Skip();
ParseSxxExxSystem(reader, result);
break;
default: // Handles empty string and nulls
reader.Skip();
@ -505,6 +503,29 @@ namespace Emby.XmlTv.Classes
}
}
public void ParseSxxExxSystem(XmlReader reader, XmlTvProgram result)
{
// <episode-num system="SxxExx">S012E32</episode-num>
var value = reader.ReadElementContentAsString();
var res = Regex.Match(value, "s([0-9]+)e([0-9]+)", RegexOptions.IgnoreCase);
if (res.Success)
{
int parsedInt;
if (int.TryParse(res.Groups[1].Value, out parsedInt))
{
result.Episode.Series = parsedInt;
}
if (int.TryParse(res.Groups[2].Value, out parsedInt))
{
result.Episode.Episode = parsedInt;
}
}
}
public void ParseMovieDbSystem(XmlReader reader, XmlTvProgram result)
{
// <episode-num system="thetvdb.com">series/248841</episode-num>

View file

@ -23,7 +23,7 @@ namespace Jellyfin.Drawing.Skia
foregroundWidth *= percent;
foregroundWidth /= 100;
paint.Color = SKColor.Parse("#FF52B54B");
paint.Color = SKColor.Parse("#FF00A4DC");
canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, Convert.ToInt32(foregroundWidth), (float)endY), paint);
}
}

View file

@ -13,7 +13,7 @@ namespace Jellyfin.Drawing.Skia
using (var paint = new SKPaint())
{
paint.Color = SKColor.Parse("#CC52B54B");
paint.Color = SKColor.Parse("#CC00A4DC");
paint.Style = SKPaintStyle.Fill;
canvas.DrawCircle((float)x, OffsetFromTopRightCorner, 20, paint);
}

View file

@ -15,7 +15,7 @@ namespace Jellyfin.Drawing.Skia
using (var paint = new SKPaint())
{
paint.Color = SKColor.Parse("#CC52B54B");
paint.Color = SKColor.Parse("#CC00A4DC");
paint.Style = SKPaintStyle.Fill;
canvas.DrawCircle((float)x, OffsetFromTopRightCorner, 20, paint);
}

View file

@ -20,10 +20,10 @@ namespace Jellyfin.Server
[Option('l', "logdir", Required = false, HelpText = "Path to use for writing log files.")]
public string LogDir { get; set; }
[Option("ffmpeg", Required = false, HelpText = "Path to external FFmpeg executable to use in place of default found in PATH. Must be specified along with --ffprobe.")]
[Option("ffmpeg", Required = false, HelpText = "Path to external FFmpeg executable to use in place of default found in PATH.")]
public string FFmpegPath { get; set; }
[Option("ffprobe", Required = false, HelpText = "Path to external FFprobe executable to use in place of default found in PATH. Must be specified along with --ffmpeg.")]
[Option("ffprobe", Required = false, HelpText = "(deprecated) Option has no effect and shall be removed in next release.")]
public string FFprobePath { get; set; }
[Option("service", Required = false, HelpText = "Run as headless service.")]

View file

@ -172,16 +172,9 @@ namespace MediaBrowser.Api
if (!string.IsNullOrWhiteSpace(hasDtoOptions.EnableImageTypes))
{
if (string.IsNullOrEmpty(hasDtoOptions.EnableImageTypes))
{
options.ImageTypes = Array.Empty<ImageType>();
}
else
{
options.ImageTypes = hasDtoOptions.EnableImageTypes.Split(new [] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(v => (ImageType)Enum.Parse(typeof(ImageType), v, true))
.ToArray();
}
options.ImageTypes = hasDtoOptions.EnableImageTypes.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(v => (ImageType)Enum.Parse(typeof(ImageType), v, true))
.ToArray();
}
}

View file

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@ -180,7 +181,7 @@ namespace MediaBrowser.Api
return ToOptimizedResult(filters);
}
private QueryFiltersLegacy GetFilters(BaseItem[] items)
private QueryFiltersLegacy GetFilters(IReadOnlyCollection<BaseItem> items)
{
var result = new QueryFiltersLegacy();

View file

@ -37,6 +37,7 @@ namespace MediaBrowser.Api.Playback.Progressive
[Route("/Videos/{Id}/stream.mov", "GET")]
[Route("/Videos/{Id}/stream.iso", "GET")]
[Route("/Videos/{Id}/stream.flv", "GET")]
[Route("/Videos/{Id}/stream.rm", "GET")]
[Route("/Videos/{Id}/stream", "GET")]
[Route("/Videos/{Id}/stream.ts", "HEAD")]
[Route("/Videos/{Id}/stream.webm", "HEAD")]

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using MediaBrowser.Controller.Dto;
@ -197,29 +198,27 @@ namespace MediaBrowser.Api.UserLibrary
request.ParentId = null;
}
var item = string.IsNullOrEmpty(request.ParentId) ?
null :
_libraryManager.GetItemById(request.ParentId);
BaseItem item = null;
if (!string.IsNullOrEmpty(request.ParentId))
{
item = _libraryManager.GetItemById(request.ParentId);
}
if (item == null)
{
item = string.IsNullOrEmpty(request.ParentId) ?
user == null ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder() :
_libraryManager.GetItemById(request.ParentId);
item = _libraryManager.GetUserRootFolder();
}
// Default list type = children
var folder = item as Folder;
Folder folder = item as Folder;
if (folder == null)
{
folder = user == null ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder();
folder = _libraryManager.GetUserRootFolder();
}
var hasCollectionType = folder as IHasCollectionType;
var isPlaylistQuery = (hasCollectionType != null && string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase));
if (isPlaylistQuery)
if (hasCollectionType != null
&& string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
{
request.Recursive = true;
request.IncludeItemTypes = "Playlist";
@ -235,20 +234,12 @@ namespace MediaBrowser.Api.UserLibrary
};
}
if (request.Recursive || !string.IsNullOrEmpty(request.Ids) || user == null)
{
return folder.GetItems(GetItemsQuery(request, dtoOptions, user));
}
var userRoot = item as UserRootFolder;
if (userRoot == null)
if (request.Recursive || !string.IsNullOrEmpty(request.Ids) || !(item is UserRootFolder))
{
return folder.GetItems(GetItemsQuery(request, dtoOptions, user));
}
var itemsArray = folder.GetChildren(user, true).ToArray();
return new QueryResult<BaseItem>
{
Items = itemsArray,

View file

@ -53,7 +53,7 @@ namespace MediaBrowser.Common.Net
/// <returns><c>true</c> if [is in local network] [the specified endpoint]; otherwise, <c>false</c>.</returns>
bool IsInLocalNetwork(string endpoint);
IpAddressInfo[] GetLocalIpAddresses();
IpAddressInfo[] GetLocalIpAddresses(bool ignoreVirtualInterface);
IpAddressInfo ParseIpAddress(string ipAddress);
@ -62,5 +62,8 @@ namespace MediaBrowser.Common.Net
Task<IpAddressInfo[]> GetHostAddressesAsync(string host);
bool IsAddressInSubnets(string addressString, string[] subnets);
bool IsInSameSubnet(IpAddressInfo address1, IpAddressInfo address2, IpAddressInfo subnetMask);
IpAddressInfo GetLocalIpSubnetMask(IpAddressInfo address);
}
}

View file

@ -36,9 +36,7 @@ namespace MediaBrowser.Controller.Dto
.ToArray();
public bool ContainsField(ItemFields field)
{
return AllItemFields.Contains(field);
}
=> Fields.Contains(field);
public DtoOptions(bool allFields)
{
@ -47,15 +45,7 @@ namespace MediaBrowser.Controller.Dto
EnableUserData = true;
AddCurrentProgram = true;
if (allFields)
{
Fields = AllItemFields;
}
else
{
Fields = new ItemFields[] { };
}
Fields = allFields ? AllItemFields : Array.Empty<ItemFields>();
ImageTypes = AllImageTypes;
}

View file

@ -57,9 +57,7 @@ namespace MediaBrowser.Controller.Dto
/// <param name="options">The options.</param>
/// <param name="user">The user.</param>
/// <param name="owner">The owner.</param>
BaseItemDto[] GetBaseItemDtos(BaseItem[] items, DtoOptions options, User user = null, BaseItem owner = null);
BaseItemDto[] GetBaseItemDtos(List<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null);
BaseItemDto[] GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null);
/// <summary>
/// Gets the item by name dto.

View file

@ -810,37 +810,19 @@ namespace MediaBrowser.Controller.Entities
{
if (query.ItemIds.Length > 0)
{
var result = LibraryManager.GetItemsResult(query);
if (query.OrderBy.Length == 0)
{
var ids = query.ItemIds.ToList();
// Try to preserve order
result.Items = result.Items.OrderBy(i => ids.IndexOf(i.Id)).ToArray();
}
return result;
return LibraryManager.GetItemsResult(query);
}
return GetItemsInternal(query);
}
public BaseItem[] GetItemList(InternalItemsQuery query)
public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query)
{
query.EnableTotalRecordCount = false;
if (query.ItemIds.Length > 0)
{
var result = LibraryManager.GetItemList(query);
if (query.OrderBy.Length == 0)
{
var ids = query.ItemIds.ToList();
// Try to preserve order
return result.OrderBy(i => ids.IndexOf(i.Id)).ToArray();
}
return result.ToArray();
return LibraryManager.GetItemList(query);
}
return GetItemsInternal(query).Items;

View file

@ -1904,7 +1904,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
flags.Add("+ignidx");
}
if (state.GenPtsInput)
if (state.GenPtsInput || string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
{
flags.Add("+genpts");
}

View file

@ -6,6 +6,7 @@ using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.System;
namespace MediaBrowser.Controller.MediaEncoding
{
@ -14,7 +15,7 @@ namespace MediaBrowser.Controller.MediaEncoding
/// </summary>
public interface IMediaEncoder : ITranscoderSupport
{
string EncoderLocationType { get; }
FFmpegLocation EncoderLocation { get; }
/// <summary>
/// Gets the encoder path.
@ -91,7 +92,7 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <returns>System.String.</returns>
string EscapeSubtitleFilterPath(string path);
void Init();
void SetFFmpegPath();
void UpdateEncoderPath(string path, string pathType);
bool SupportsEncoder(string encoder);

View file

@ -32,16 +32,17 @@ namespace MediaBrowser.Controller.MediaEncoding
var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line);
// If ffmpeg process is closed, the state is disposed, so don't write to target in that case
if (!target.CanWrite)
{
break;
}
await target.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
await target.FlushAsync().ConfigureAwait(false);
}
}
}
catch (ObjectDisposedException)
{
//TODO Investigate and properly fix.
// Don't spam the log. This doesn't seem to throw in windows, but sometimes under linux
}
catch (Exception ex)
{
_logger.LogError(ex, "Error reading ffmpeg log");

View file

@ -1,19 +0,0 @@
namespace MediaBrowser.Controller.Security
{
public interface IEncryptionManager
{
/// <summary>
/// Encrypts the string.
/// </summary>
/// <param name="value">The value.</param>
/// <returns>System.String.</returns>
string EncryptString(string value);
/// <summary>
/// Decrypts the string.
/// </summary>
/// <param name="value">The value.</param>
/// <returns>System.String.</returns>
string DecryptString(string value);
}
}

View file

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@ -65,6 +66,12 @@ namespace MediaBrowser.LocalMetadata.Images
var path = item.ContainingFolderPath;
// Exit if the cache dir does not exist, alternative solution is to create it, but that's a lot of empty dirs...
if (!Directory.Exists(path))
{
return Array.Empty<FileSystemMetadata>();
}
if (includeDirectories)
{
return directoryService.GetFileSystemEntries(path)

View file

@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text.RegularExpressions;
using MediaBrowser.Model.Diagnostics;
@ -19,7 +19,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
_processFactory = processFactory;
}
public (IEnumerable<string> decoders, IEnumerable<string> encoders) Validate(string encoderPath)
public (IEnumerable<string> decoders, IEnumerable<string> encoders) GetAvailableCoders(string encoderPath)
{
_logger.LogInformation("Validating media encoder at {EncoderPath}", encoderPath);
@ -48,6 +48,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
if (string.IsNullOrWhiteSpace(output))
{
if (logOutput)
{
_logger.LogError("FFmpeg validation: The process returned no result");
}
return false;
}
@ -55,21 +59,114 @@ namespace MediaBrowser.MediaEncoding.Encoder
if (output.IndexOf("Libav developers", StringComparison.OrdinalIgnoreCase) != -1)
{
if (logOutput)
{
_logger.LogError("FFmpeg validation: avconv instead of ffmpeg is not supported");
}
return false;
}
output = " " + output + " ";
// The min and max FFmpeg versions required to run jellyfin successfully
var minRequired = new Version(4, 0);
var maxRequired = new Version(4, 0);
for (var i = 2013; i <= 2015; i++)
// Work out what the version under test is
var underTest = GetFFmpegVersion(output);
if (logOutput)
{
var yearString = i.ToString(CultureInfo.InvariantCulture);
if (output.IndexOf(" " + yearString + " ", StringComparison.OrdinalIgnoreCase) != -1)
_logger.LogInformation("FFmpeg validation: Found ffmpeg version {0}", underTest != null ? underTest.ToString() : "unknown");
if (underTest == null) // Version is unknown
{
return false;
if (minRequired.Equals(maxRequired))
{
_logger.LogWarning("FFmpeg validation: We recommend ffmpeg version {0}", minRequired.ToString());
}
else
{
_logger.LogWarning("FFmpeg validation: We recommend a minimum of {0} and maximum of {1}", minRequired.ToString(), maxRequired.ToString());
}
}
else if (underTest.CompareTo(minRequired) < 0) // Version is below what we recommend
{
_logger.LogWarning("FFmpeg validation: The minimum recommended ffmpeg version is {0}", minRequired.ToString());
}
else if (underTest.CompareTo(maxRequired) > 0) // Version is above what we recommend
{
_logger.LogWarning("FFmpeg validation: The maximum recommended ffmpeg version is {0}", maxRequired.ToString());
}
else // Version is ok
{
_logger.LogInformation("FFmpeg validation: Found suitable ffmpeg version");
}
}
return true;
// underTest shall be null if versions is unknown
return (underTest == null) ? false : (underTest.CompareTo(minRequired) >= 0 && underTest.CompareTo(maxRequired) <= 0);
}
/// <summary>
/// Using the output from "ffmpeg -version" work out the FFmpeg version.
/// For pre-built binaries the first line should contain a string like "ffmpeg version x.y", which is easy
/// to parse. If this is not available, then we try to match known library versions to FFmpeg versions.
/// If that fails then we use one of the main libraries to determine if it's new/older than the latest
/// we have stored.
/// </summary>
/// <param name="output"></param>
/// <returns></returns>
static private Version GetFFmpegVersion(string output)
{
// For pre-built binaries the FFmpeg version should be mentioned at the very start of the output
var match = Regex.Match(output, @"ffmpeg version (\d+\.\d+)");
if (match.Success)
{
return new Version(match.Groups[1].Value);
}
else
{
// Try and use the individual library versions to determine a FFmpeg version
// This lookup table is to be maintained with the following command line:
// $ ./ffmpeg.exe -version | perl -ne ' print "$1=$2.$3," if /^(lib\w+)\s+(\d+)\.\s*(\d+)/'
var lut = new ReadOnlyDictionary<Version, string>
(new Dictionary<Version, string>
{
{ new Version("4.1"), "libavutil=56.22,libavcodec=58.35,libavformat=58.20,libavdevice=58.5,libavfilter=7.40,libswscale=5.3,libswresample=3.3,libpostproc=55.3," },
{ new Version("4.0"), "libavutil=56.14,libavcodec=58.18,libavformat=58.12,libavdevice=58.3,libavfilter=7.16,libswscale=5.1,libswresample=3.1,libpostproc=55.1," },
{ new Version("3.4"), "libavutil=55.78,libavcodec=57.107,libavformat=57.83,libavdevice=57.10,libavfilter=6.107,libswscale=4.8,libswresample=2.9,libpostproc=54.7," },
{ new Version("3.3"), "libavutil=55.58,libavcodec=57.89,libavformat=57.71,libavdevice=57.6,libavfilter=6.82,libswscale=4.6,libswresample=2.7,libpostproc=54.5," },
{ new Version("3.2"), "libavutil=55.34,libavcodec=57.64,libavformat=57.56,libavdevice=57.1,libavfilter=6.65,libswscale=4.2,libswresample=2.3,libpostproc=54.1," },
{ new Version("2.8"), "libavutil=54.31,libavcodec=56.60,libavformat=56.40,libavdevice=56.4,libavfilter=5.40,libswscale=3.1,libswresample=1.2,libpostproc=53.3," }
});
// Create a reduced version string and lookup key from dictionary
var reducedVersion = GetVersionString(output);
// Try to lookup the string and return Key, otherwise if not found returns null
return lut.FirstOrDefault(x => x.Value == reducedVersion).Key;
}
}
/// <summary>
/// Grabs the library names and major.minor version numbers from the 'ffmpeg -version' output
/// and condenses them on to one line. Output format is "name1=major.minor,name2=major.minor,etc."
/// </summary>
/// <param name="output"></param>
/// <returns></returns>
static private string GetVersionString(string output)
{
string pattern = @"((?<name>lib\w+)\s+(?<major>\d+)\.\s*(?<minor>\d+))";
RegexOptions options = RegexOptions.Multiline;
string rc = null;
foreach (Match m in Regex.Matches(output, pattern, options))
{
rc += string.Concat(m.Groups["name"], '=', m.Groups["major"], '.', m.Groups["minor"], ',');
}
return rc;
}
private static readonly string[] requiredDecoders = new[]

View file

@ -3,17 +3,14 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Session;
using MediaBrowser.MediaEncoding.Probing;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Diagnostics;
@ -22,6 +19,7 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.System;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.MediaEncoding.Encoder
@ -32,340 +30,223 @@ namespace MediaBrowser.MediaEncoding.Encoder
public class MediaEncoder : IMediaEncoder, IDisposable
{
/// <summary>
/// The _logger
/// Gets the encoder path.
/// </summary>
/// <value>The encoder path.</value>
public string EncoderPath => FFmpegPath;
/// <summary>
/// The location of the discovered FFmpeg tool.
/// </summary>
public FFmpegLocation EncoderLocation { get; private set; }
private readonly ILogger _logger;
/// <summary>
/// Gets the json serializer.
/// </summary>
/// <value>The json serializer.</value>
private readonly IJsonSerializer _jsonSerializer;
/// <summary>
/// The _thumbnail resource pool
/// </summary>
private readonly SemaphoreSlim _thumbnailResourcePool = new SemaphoreSlim(1, 1);
public string FFMpegPath { get; private set; }
public string FFProbePath { get; private set; }
private string FFmpegPath;
private string FFprobePath;
protected readonly IServerConfigurationManager ConfigurationManager;
protected readonly IFileSystem FileSystem;
protected readonly ILiveTvManager LiveTvManager;
protected readonly IIsoManager IsoManager;
protected readonly ILibraryManager LibraryManager;
protected readonly IChannelManager ChannelManager;
protected readonly ISessionManager SessionManager;
protected readonly Func<ISubtitleEncoder> SubtitleEncoder;
protected readonly Func<IMediaSourceManager> MediaSourceManager;
private readonly IHttpClient _httpClient;
private readonly IZipClient _zipClient;
private readonly IProcessFactory _processFactory;
private readonly List<ProcessWrapper> _runningProcesses = new List<ProcessWrapper>();
private readonly bool _hasExternalEncoder;
private readonly string _originalFFMpegPath;
private readonly string _originalFFProbePath;
private readonly int DefaultImageExtractionTimeoutMs;
private readonly string StartupOptionFFmpegPath;
private readonly string StartupOptionFFprobePath;
private readonly SemaphoreSlim _thumbnailResourcePool = new SemaphoreSlim(1, 1);
private readonly List<ProcessWrapper> _runningProcesses = new List<ProcessWrapper>();
public MediaEncoder(
ILoggerFactory loggerFactory,
IJsonSerializer jsonSerializer,
string ffMpegPath,
string ffProbePath,
bool hasExternalEncoder,
string startupOptionsFFmpegPath,
string startupOptionsFFprobePath,
IServerConfigurationManager configurationManager,
IFileSystem fileSystem,
ILiveTvManager liveTvManager,
IIsoManager isoManager,
ILibraryManager libraryManager,
IChannelManager channelManager,
ISessionManager sessionManager,
Func<ISubtitleEncoder> subtitleEncoder,
Func<IMediaSourceManager> mediaSourceManager,
IHttpClient httpClient,
IZipClient zipClient,
IProcessFactory processFactory,
int defaultImageExtractionTimeoutMs)
{
_logger = loggerFactory.CreateLogger(nameof(MediaEncoder));
_jsonSerializer = jsonSerializer;
StartupOptionFFmpegPath = startupOptionsFFmpegPath;
StartupOptionFFprobePath = startupOptionsFFprobePath;
ConfigurationManager = configurationManager;
FileSystem = fileSystem;
LiveTvManager = liveTvManager;
IsoManager = isoManager;
LibraryManager = libraryManager;
ChannelManager = channelManager;
SessionManager = sessionManager;
SubtitleEncoder = subtitleEncoder;
MediaSourceManager = mediaSourceManager;
_httpClient = httpClient;
_zipClient = zipClient;
_processFactory = processFactory;
DefaultImageExtractionTimeoutMs = defaultImageExtractionTimeoutMs;
FFProbePath = ffProbePath;
FFMpegPath = ffMpegPath;
_originalFFProbePath = ffProbePath;
_originalFFMpegPath = ffMpegPath;
_hasExternalEncoder = hasExternalEncoder;
}
public string EncoderLocationType
/// <summary>
/// Run at startup or if the user removes a Custom path from transcode page.
/// Sets global variables FFmpegPath.
/// Precedence is: Config > CLI > $PATH
/// </summary>
public void SetFFmpegPath()
{
get
// ToDo - Finalise removal of the --ffprobe switch
if (!string.IsNullOrEmpty(StartupOptionFFprobePath))
{
if (_hasExternalEncoder)
{
return "External";
}
if (string.IsNullOrWhiteSpace(FFMpegPath))
{
return null;
}
if (IsSystemInstalledPath(FFMpegPath))
{
return "System";
}
return "Custom";
}
}
private bool IsSystemInstalledPath(string path)
{
if (path.IndexOf("/", StringComparison.Ordinal) == -1 && path.IndexOf("\\", StringComparison.Ordinal) == -1)
{
return true;
_logger.LogWarning("--ffprobe switch is deprecated and shall be removed in the next release");
}
return false;
}
public void Init()
{
InitPaths();
if (!string.IsNullOrWhiteSpace(FFMpegPath))
// 1) Custom path stored in config/encoding xml file under tag <EncoderAppPath> takes precedence
if (!ValidatePath(ConfigurationManager.GetConfiguration<EncodingOptions>("encoding").EncoderAppPath, FFmpegLocation.Custom))
{
var result = new EncoderValidator(_logger, _processFactory).Validate(FFMpegPath);
// 2) Check if the --ffmpeg CLI switch has been given
if (!ValidatePath(StartupOptionFFmpegPath, FFmpegLocation.SetByArgument))
{
// 3) Search system $PATH environment variable for valid FFmpeg
if (!ValidatePath(ExistsOnSystemPath("ffmpeg"), FFmpegLocation.System))
{
EncoderLocation = FFmpegLocation.NotFound;
FFmpegPath = null;
}
}
}
// Write the FFmpeg path to the config/encoding.xml file as <EncoderAppPathDisplay> so it appears in UI
var config = ConfigurationManager.GetConfiguration<EncodingOptions>("encoding");
config.EncoderAppPathDisplay = FFmpegPath ?? string.Empty;
ConfigurationManager.SaveConfiguration("encoding", config);
// Only if mpeg path is set, try and set path to probe
if (FFmpegPath != null)
{
// Determine a probe path from the mpeg path
FFprobePath = Regex.Replace(FFmpegPath, @"[^\/\\]+?(\.[^\/\\\n.]+)?$", @"ffprobe$1");
// Interrogate to understand what coders are supported
var result = new EncoderValidator(_logger, _processFactory).GetAvailableCoders(FFmpegPath);
SetAvailableDecoders(result.decoders);
SetAvailableEncoders(result.encoders);
}
_logger.LogInformation("FFmpeg: {0}: {1}", EncoderLocation.ToString(), FFmpegPath ?? string.Empty);
}
private void InitPaths()
{
ConfigureEncoderPaths();
if (_hasExternalEncoder)
{
LogPaths();
return;
}
// If the path was passed in, save it into config now.
var encodingOptions = GetEncodingOptions();
var appPath = encodingOptions.EncoderAppPath;
var valueToSave = FFMpegPath;
if (!string.IsNullOrWhiteSpace(valueToSave))
{
// if using system variable, don't save this.
if (IsSystemInstalledPath(valueToSave) || _hasExternalEncoder)
{
valueToSave = null;
}
}
if (!string.Equals(valueToSave, appPath, StringComparison.Ordinal))
{
encodingOptions.EncoderAppPath = valueToSave;
ConfigurationManager.SaveConfiguration("encoding", encodingOptions);
}
}
/// <summary>
/// Triggered from the Settings > Transcoding UI page when users submits Custom FFmpeg path to use.
/// Only write the new path to xml if it exists. Do not perform validation checks on ffmpeg here.
/// </summary>
/// <param name="path"></param>
/// <param name="pathType"></param>
public void UpdateEncoderPath(string path, string pathType)
{
if (_hasExternalEncoder)
{
return;
}
string newPath;
_logger.LogInformation("Attempting to update encoder path to {0}. pathType: {1}", path ?? string.Empty, pathType ?? string.Empty);
Tuple<string, string> newPaths;
if (string.Equals(pathType, "system", StringComparison.OrdinalIgnoreCase))
{
path = "ffmpeg";
newPaths = TestForInstalledVersions();
}
else if (string.Equals(pathType, "custom", StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrWhiteSpace(path))
{
throw new ArgumentNullException(nameof(path));
}
if (!File.Exists(path) && !Directory.Exists(path))
{
throw new ResourceNotFoundException();
}
newPaths = GetEncoderPaths(path);
}
else
if (!string.Equals(pathType, "custom", StringComparison.OrdinalIgnoreCase))
{
throw new ArgumentException("Unexpected pathType value");
}
if (string.IsNullOrWhiteSpace(newPaths.Item1))
else if (string.IsNullOrWhiteSpace(path))
{
throw new ResourceNotFoundException("ffmpeg not found");
// User had cleared the custom path in UI
newPath = string.Empty;
}
if (string.IsNullOrWhiteSpace(newPaths.Item2))
else if (File.Exists(path))
{
throw new ResourceNotFoundException("ffprobe not found");
newPath = path;
}
else if (Directory.Exists(path))
{
// Given path is directory, so resolve down to filename
newPath = GetEncoderPathFromDirectory(path, "ffmpeg");
}
else
{
throw new ResourceNotFoundException();
}
path = newPaths.Item1;
if (!ValidateVersion(path, true))
{
throw new ResourceNotFoundException("ffmpeg version 3.0 or greater is required.");
}
var config = GetEncodingOptions();
config.EncoderAppPath = path;
// Write the new ffmpeg path to the xml as <EncoderAppPath>
// This ensures its not lost on next startup
var config = ConfigurationManager.GetConfiguration<EncodingOptions>("encoding");
config.EncoderAppPath = newPath;
ConfigurationManager.SaveConfiguration("encoding", config);
Init();
// Trigger SetFFmpegPath so we validate the new path and setup probe path
SetFFmpegPath();
}
private bool ValidateVersion(string path, bool logOutput)
/// <summary>
/// Validates the supplied FQPN to ensure it is a ffmpeg utility.
/// If checks pass, global variable FFmpegPath and EncoderLocation are updated.
/// </summary>
/// <param name="path">FQPN to test</param>
/// <param name="location">Location (External, Custom, System) of tool</param>
/// <returns></returns>
private bool ValidatePath(string path, FFmpegLocation location)
{
return new EncoderValidator(_logger, _processFactory).ValidateVersion(path, logOutput);
}
bool rc = false;
private void ConfigureEncoderPaths()
{
if (_hasExternalEncoder)
if (!string.IsNullOrEmpty(path))
{
return;
}
var appPath = GetEncodingOptions().EncoderAppPath;
if (string.IsNullOrWhiteSpace(appPath))
{
appPath = Path.Combine(ConfigurationManager.ApplicationPaths.ProgramDataPath, "ffmpeg");
}
var newPaths = GetEncoderPaths(appPath);
if (string.IsNullOrWhiteSpace(newPaths.Item1) || string.IsNullOrWhiteSpace(newPaths.Item2) || IsSystemInstalledPath(appPath))
{
newPaths = TestForInstalledVersions();
}
if (!string.IsNullOrWhiteSpace(newPaths.Item1) && !string.IsNullOrWhiteSpace(newPaths.Item2))
{
FFMpegPath = newPaths.Item1;
FFProbePath = newPaths.Item2;
}
LogPaths();
}
private Tuple<string, string> GetEncoderPaths(string configuredPath)
{
var appPath = configuredPath;
if (!string.IsNullOrWhiteSpace(appPath))
{
if (Directory.Exists(appPath))
if (File.Exists(path))
{
return GetPathsFromDirectory(appPath);
rc = new EncoderValidator(_logger, _processFactory).ValidateVersion(path, true);
if (!rc)
{
_logger.LogWarning("FFmpeg: {0}: Failed version check: {1}", location.ToString(), path);
}
// ToDo - Enable the ffmpeg validator. At the moment any version can be used.
rc = true;
FFmpegPath = path;
EncoderLocation = location;
}
if (File.Exists(appPath))
else
{
return new Tuple<string, string>(appPath, GetProbePathFromEncoderPath(appPath));
_logger.LogWarning("FFmpeg: {0}: File not found: {1}", location.ToString(), path);
}
}
return new Tuple<string, string>(null, null);
return rc;
}
private Tuple<string, string> TestForInstalledVersions()
private string GetEncoderPathFromDirectory(string path, string filename)
{
string encoderPath = null;
string probePath = null;
if (_hasExternalEncoder && ValidateVersion(_originalFFMpegPath, true))
try
{
encoderPath = _originalFFMpegPath;
probePath = _originalFFProbePath;
var files = FileSystem.GetFilePaths(path);
var excludeExtensions = new[] { ".c" };
return files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), filename, StringComparison.OrdinalIgnoreCase)
&& !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty));
}
if (string.IsNullOrWhiteSpace(encoderPath))
catch (Exception)
{
if (ValidateVersion("ffmpeg", true) && ValidateVersion("ffprobe", false))
// Trap all exceptions, like DirNotExists, and return null
return null;
}
}
/// <summary>
/// Search the system $PATH environment variable looking for given filename.
/// </summary>
/// <param name="fileName"></param>
/// <returns></returns>
private string ExistsOnSystemPath(string filename)
{
var values = Environment.GetEnvironmentVariable("PATH");
foreach (var path in values.Split(Path.PathSeparator))
{
var candidatePath = GetEncoderPathFromDirectory(path, filename);
if (!string.IsNullOrEmpty(candidatePath))
{
encoderPath = "ffmpeg";
probePath = "ffprobe";
return candidatePath;
}
}
return new Tuple<string, string>(encoderPath, probePath);
}
private Tuple<string, string> GetPathsFromDirectory(string path)
{
// Since we can't predict the file extension, first try directly within the folder
// If that doesn't pan out, then do a recursive search
var files = FileSystem.GetFilePaths(path);
var excludeExtensions = new[] { ".c" };
var ffmpegPath = files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffmpeg", StringComparison.OrdinalIgnoreCase) && !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty));
var ffprobePath = files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffprobe", StringComparison.OrdinalIgnoreCase) && !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty));
if (string.IsNullOrWhiteSpace(ffmpegPath) || !File.Exists(ffmpegPath))
{
files = FileSystem.GetFilePaths(path, true);
ffmpegPath = files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffmpeg", StringComparison.OrdinalIgnoreCase) && !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty));
if (!string.IsNullOrWhiteSpace(ffmpegPath))
{
ffprobePath = GetProbePathFromEncoderPath(ffmpegPath);
}
}
return new Tuple<string, string>(ffmpegPath, ffprobePath);
}
private string GetProbePathFromEncoderPath(string appPath)
{
return FileSystem.GetFilePaths(Path.GetDirectoryName(appPath))
.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffprobe", StringComparison.OrdinalIgnoreCase));
}
private void LogPaths()
{
_logger.LogInformation("FFMpeg: {0}", FFMpegPath ?? "not found");
_logger.LogInformation("FFProbe: {0}", FFProbePath ?? "not found");
}
private EncodingOptions GetEncodingOptions()
{
return ConfigurationManager.GetConfiguration<EncodingOptions>("encoding");
return null;
}
private List<string> _encoders = new List<string>();
@ -412,12 +293,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
return true;
}
/// <summary>
/// Gets the encoder path.
/// </summary>
/// <value>The encoder path.</value>
public string EncoderPath => FFMpegPath;
/// <summary>
/// Gets the media info.
/// </summary>
@ -489,7 +364,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
// Must consume both or ffmpeg may hang due to deadlocks. See comments below.
RedirectStandardOutput = true,
FileName = FFProbePath,
FileName = FFprobePath,
Arguments = string.Format(args, probeSizeArgument, inputPath).Trim(),
IsHidden = true,
@ -691,10 +566,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
{
CreateNoWindow = true,
UseShellExecute = false,
FileName = FFMpegPath,
FileName = FFmpegPath,
Arguments = args,
IsHidden = true,
ErrorDialog = false
ErrorDialog = false,
EnableRaisingEvents = true
});
_logger.LogDebug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
@ -813,10 +689,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
{
CreateNoWindow = true,
UseShellExecute = false,
FileName = FFMpegPath,
FileName = FFmpegPath,
Arguments = args,
IsHidden = true,
ErrorDialog = false
ErrorDialog = false,
EnableRaisingEvents = true
});
_logger.LogInformation(process.StartInfo.FileName + " " + process.StartInfo.Arguments);

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
@ -29,17 +30,15 @@ namespace MediaBrowser.MediaEncoding.Subtitles
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private readonly IServerConfigurationManager _config;
private readonly IEncryptionManager _encryption;
private readonly IJsonSerializer _json;
private readonly IFileSystem _fileSystem;
public OpenSubtitleDownloader(ILoggerFactory loggerFactory, IHttpClient httpClient, IServerConfigurationManager config, IEncryptionManager encryption, IJsonSerializer json, IFileSystem fileSystem)
public OpenSubtitleDownloader(ILoggerFactory loggerFactory, IHttpClient httpClient, IServerConfigurationManager config, IJsonSerializer json, IFileSystem fileSystem)
{
_logger = loggerFactory.CreateLogger(GetType().Name);
_httpClient = httpClient;
_config = config;
_encryption = encryption;
_json = json;
_fileSystem = fileSystem;
@ -63,16 +62,17 @@ namespace MediaBrowser.MediaEncoding.Subtitles
!string.IsNullOrWhiteSpace(options.OpenSubtitlesPasswordHash) &&
!options.OpenSubtitlesPasswordHash.StartsWith(PasswordHashPrefix, StringComparison.OrdinalIgnoreCase))
{
options.OpenSubtitlesPasswordHash = EncryptPassword(options.OpenSubtitlesPasswordHash);
options.OpenSubtitlesPasswordHash = EncodePassword(options.OpenSubtitlesPasswordHash);
}
}
private string EncryptPassword(string password)
private static string EncodePassword(string password)
{
return PasswordHashPrefix + _encryption.EncryptString(password);
var bytes = Encoding.UTF8.GetBytes(password);
return PasswordHashPrefix + Convert.ToBase64String(bytes);
}
private string DecryptPassword(string password)
private static string DecodePassword(string password)
{
if (password == null ||
!password.StartsWith(PasswordHashPrefix, StringComparison.OrdinalIgnoreCase))
@ -80,7 +80,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles
return string.Empty;
}
return _encryption.DecryptString(password.Substring(2));
var bytes = Convert.FromBase64String(password.Substring(2));
return Encoding.UTF8.GetString(bytes, 0, bytes.Length);
}
public string Name => "Open Subtitles";
@ -186,7 +187,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
var options = GetOptions();
var user = options.OpenSubtitlesUsername ?? string.Empty;
var password = DecryptPassword(options.OpenSubtitlesPasswordHash);
var password = DecodePassword(options.OpenSubtitlesPasswordHash);
var loginResponse = await OpenSubtitles.LogInAsync(user, password, "en", cancellationToken).ConfigureAwait(false);

View file

@ -8,7 +8,14 @@ namespace MediaBrowser.Model.Configuration
public bool EnableThrottling { get; set; }
public int ThrottleDelaySeconds { get; set; }
public string HardwareAccelerationType { get; set; }
/// <summary>
/// FFmpeg path as set by the user via the UI
/// </summary>
public string EncoderAppPath { get; set; }
/// <summary>
/// The current FFmpeg path being used by the system and displayed on the transcode page
/// </summary>
public string EncoderAppPathDisplay { get; set; }
public string VaapiDevice { get; set; }
public int H264Crf { get; set; }
public string H264Preset { get; set; }

View file

@ -178,6 +178,7 @@ namespace MediaBrowser.Model.Configuration
public string[] LocalNetworkSubnets { get; set; }
public string[] LocalNetworkAddresses { get; set; }
public string[] CodecsUsed { get; set; }
public bool IgnoreVirtualInterfaces { get; set; }
public bool EnableExternalContentInSuggestions { get; set; }
public bool RequireHttps { get; set; }
public bool IsBehindProxy { get; set; }
@ -205,6 +206,7 @@ namespace MediaBrowser.Model.Configuration
CodecsUsed = Array.Empty<string>();
ImageExtractionTimeoutMs = 0;
PathSubstitutions = Array.Empty<PathSubstitution>();
IgnoreVirtualInterfaces = false;
EnableSimpleArtistDetection = true;
DisplaySpecialsWithinSeasons = true;

View file

@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Collections.Generic;
namespace MediaBrowser.Model.Cryptography
{
@ -9,5 +10,13 @@ namespace MediaBrowser.Model.Cryptography
byte[] ComputeMD5(Stream str);
byte[] ComputeMD5(byte[] bytes);
byte[] ComputeSHA1(byte[] bytes);
IEnumerable<string> GetSupportedHashMethods();
byte[] ComputeHash(string HashMethod, byte[] bytes);
byte[] ComputeHashWithDefaultMethod(byte[] bytes);
byte[] ComputeHash(string HashMethod, byte[] bytes, byte[] salt);
byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt);
byte[] ComputeHash(PasswordHash hash);
byte[] GenerateSalt();
string DefaultHashMethod { get; }
}
}

View file

@ -0,0 +1,153 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace MediaBrowser.Model.Cryptography
{
public class PasswordHash
{
// Defined from this hash storage spec
// https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md
// $<id>[$<param>=<value>(,<param>=<value>)*][$<salt>[$<hash>]]
// with one slight amendment to ease the transition, we're writing out the bytes in hex
// rather than making them a BASE64 string with stripped padding
private string _id;
private Dictionary<string, string> _parameters = new Dictionary<string, string>();
private string _salt;
private byte[] _saltBytes;
private string _hash;
private byte[] _hashBytes;
public string Id { get => _id; set => _id = value; }
public Dictionary<string, string> Parameters { get => _parameters; set => _parameters = value; }
public string Salt { get => _salt; set => _salt = value; }
public byte[] SaltBytes { get => _saltBytes; set => _saltBytes = value; }
public string Hash { get => _hash; set => _hash = value; }
public byte[] HashBytes { get => _hashBytes; set => _hashBytes = value; }
public PasswordHash(string storageString)
{
string[] splitted = storageString.Split('$');
_id = splitted[1];
if (splitted[2].Contains("="))
{
foreach (string paramset in (splitted[2].Split(',')))
{
if (!string.IsNullOrEmpty(paramset))
{
string[] fields = paramset.Split('=');
if (fields.Length == 2)
{
_parameters.Add(fields[0], fields[1]);
}
else
{
throw new Exception($"Malformed parameter in password hash string {paramset}");
}
}
}
if (splitted.Length == 5)
{
_salt = splitted[3];
_saltBytes = ConvertFromByteString(_salt);
_hash = splitted[4];
_hashBytes = ConvertFromByteString(_hash);
}
else
{
_salt = string.Empty;
_hash = splitted[3];
_hashBytes = ConvertFromByteString(_hash);
}
}
else
{
if (splitted.Length == 4)
{
_salt = splitted[2];
_saltBytes = ConvertFromByteString(_salt);
_hash = splitted[3];
_hashBytes = ConvertFromByteString(_hash);
}
else
{
_salt = string.Empty;
_hash = splitted[2];
_hashBytes = ConvertFromByteString(_hash);
}
}
}
public PasswordHash(ICryptoProvider cryptoProvider)
{
_id = cryptoProvider.DefaultHashMethod;
_saltBytes = cryptoProvider.GenerateSalt();
_salt = ConvertToByteString(SaltBytes);
}
public static byte[] ConvertFromByteString(string byteString)
{
byte[] bytes = new byte[byteString.Length / 2];
for (int i = 0; i < byteString.Length; i += 2)
{
// TODO: NetStandard2.1 switch this to use a span instead of a substring.
bytes[i / 2] = Convert.ToByte(byteString.Substring(i, 2), 16);
}
return bytes;
}
public static string ConvertToByteString(byte[] bytes)
{
return BitConverter.ToString(bytes).Replace("-", "");
}
private string SerializeParameters()
{
string returnString = string.Empty;
foreach (var KVP in _parameters)
{
returnString += $",{KVP.Key}={KVP.Value}";
}
if ((!string.IsNullOrEmpty(returnString)) && returnString[0] == ',')
{
returnString = returnString.Remove(0, 1);
}
return returnString;
}
public override string ToString()
{
string outString = "$" + _id;
string paramstring = SerializeParameters();
if (!string.IsNullOrEmpty(paramstring))
{
outString += $"${paramstring}";
}
if (!string.IsNullOrEmpty(_salt))
{
outString += $"${_salt}";
}
outString += $"${_hash}";
return outString;
}
}
}

View file

@ -10,6 +10,7 @@ namespace MediaBrowser.Model.Net
public static IpAddressInfo IPv6Loopback = new IpAddressInfo("::1", IpAddressFamily.InterNetworkV6);
public string Address { get; set; }
public IpAddressInfo SubnetMask { get; set; }
public IpAddressFamily AddressFamily { get; set; }
public IpAddressInfo(string address, IpAddressFamily addressFamily)

View file

@ -4,6 +4,21 @@ using MediaBrowser.Model.Updates;
namespace MediaBrowser.Model.System
{
/// <summary>
/// Enum describing the location of the FFmpeg tool.
/// </summary>
public enum FFmpegLocation
{
/// <summary>No path to FFmpeg found.</summary>
NotFound,
/// <summary>Path supplied via command line using switch --ffmpeg.</summary>
SetByArgument,
/// <summary>User has supplied path via Transcoding UI page.</summary>
Custom,
/// <summary>FFmpeg tool found on system $PATH.</summary>
System
};
/// <summary>
/// Class SystemInfo
/// </summary>
@ -122,7 +137,7 @@ namespace MediaBrowser.Model.System
/// <value><c>true</c> if this instance has update available; otherwise, <c>false</c>.</value>
public bool HasUpdateAvailable { get; set; }
public string EncoderLocationType { get; set; }
public FFmpegLocation EncoderLocation { get; set; }
public Architecture SystemArchitecture { get; set; }

View file

@ -77,7 +77,7 @@ namespace MediaBrowser.Model.Users
public UserPolicy()
{
EnableContentDeletion = true;
EnableContentDeletion = false;
EnableContentDeletionFromFolders = Array.Empty<string>();
EnableSyncTranscoding = true;

View file

@ -92,10 +92,7 @@ namespace MediaBrowser.Providers.Manager
catch (Exception ex)
{
localImagesFailed = true;
if (!(item is IItemByName))
{
Logger.LogError(ex, "Error validating images for {0}", item.Path ?? item.Name ?? "Unknown name");
}
Logger.LogError(ex, "Error validating images for {0}", item.Path ?? item.Name ?? "Unknown name");
}
var metadataResult = new MetadataResult<TItemType>

View file

@ -9,7 +9,7 @@
<a href="https://github.com/jellyfin/jellyfin"><img alt="GPL 2.0 License" src="https://img.shields.io/github/license/jellyfin/jellyfin.svg"/></a>
<a href="https://github.com/jellyfin/jellyfin/releases"><img alt="Current Release" src="https://img.shields.io/github/release/jellyfin/jellyfin.svg"/></a>
<a href="https://translate.jellyfin.org/engage/jellyfin/?utm_source=widget"><img alt="Translations" src="https://translate.jellyfin.org/widgets/jellyfin/-/svg-badge.svg"/></a>
<a href="https://cloud.drone.io/jellyfin/jellyfin"><img alt="Build Status" src="https://cloud.drone.io/api/badges/jellyfin/jellyfin/status.svg"/></a>
<a href="https://dev.azure.com/jellyfin-project/jellyfin/_build?definitionId=1"><img alt="Azure DevOps builds" src="https://dev.azure.com/jellyfin-project/jellyfin/_apis/build/status/Jellyfin%20CI"></a>
<a href="https://hub.docker.com/r/jellyfin/jellyfin"><img alt="Docker Pull Count" src="https://img.shields.io/docker/pulls/jellyfin/jellyfin.svg"/></a>
</br>
<a href="https://opencollective.com/jellyfin"><img alt="Donate" src="https://img.shields.io/opencollective/all/jellyfin.svg?label=backers"/></a>

View file

@ -45,8 +45,8 @@ namespace Rssdp.Infrastructure
/// <summary>
/// Sends a message to the SSDP multicast address and port.
/// </summary>
Task SendMulticastMessage(string message, CancellationToken cancellationToken);
Task SendMulticastMessage(string message, int sendCount, CancellationToken cancellationToken);
Task SendMulticastMessage(string message, IpAddressInfo fromLocalIpAddress, CancellationToken cancellationToken);
Task SendMulticastMessage(string message, int sendCount, IpAddressInfo fromLocalIpAddress, CancellationToken cancellationToken);
#endregion
@ -63,4 +63,4 @@ namespace Rssdp.Infrastructure
#endregion
}
}
}

View file

@ -3,6 +3,7 @@
<ItemGroup>
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
</ItemGroup>
<PropertyGroup>

View file

@ -9,6 +9,7 @@ using System.Threading.Tasks;
using MediaBrowser.Common.Net;
using Microsoft.Extensions.Logging;
using MediaBrowser.Model.Net;
using MediaBrowser.Controller.Configuration;
namespace Rssdp.Infrastructure
{
@ -45,6 +46,7 @@ namespace Rssdp.Infrastructure
private readonly ILogger _logger;
private ISocketFactory _SocketFactory;
private readonly INetworkManager _networkManager;
private readonly IServerConfigurationManager _config;
private int _LocalPort;
private int _MulticastTtl;
@ -74,9 +76,11 @@ namespace Rssdp.Infrastructure
/// Minimum constructor.
/// </summary>
/// <exception cref="ArgumentNullException">The <paramref name="socketFactory"/> argument is null.</exception>
public SsdpCommunicationsServer(ISocketFactory socketFactory, INetworkManager networkManager, ILogger logger, bool enableMultiSocketBinding)
public SsdpCommunicationsServer(IServerConfigurationManager config, ISocketFactory socketFactory,
INetworkManager networkManager, ILogger logger, bool enableMultiSocketBinding)
: this(socketFactory, 0, SsdpConstants.SsdpDefaultMulticastTimeToLive, networkManager, logger, enableMultiSocketBinding)
{
_config = config;
}
/// <summary>
@ -236,15 +240,15 @@ namespace Rssdp.Infrastructure
}
}
public Task SendMulticastMessage(string message, CancellationToken cancellationToken)
public Task SendMulticastMessage(string message, IpAddressInfo fromLocalIpAddress, CancellationToken cancellationToken)
{
return SendMulticastMessage(message, SsdpConstants.UdpResendCount, cancellationToken);
return SendMulticastMessage(message, SsdpConstants.UdpResendCount, fromLocalIpAddress, cancellationToken);
}
/// <summary>
/// Sends a message to the SSDP multicast address and port.
/// </summary>
public async Task SendMulticastMessage(string message, int sendCount, CancellationToken cancellationToken)
public async Task SendMulticastMessage(string message, int sendCount, IpAddressInfo fromLocalIpAddress, CancellationToken cancellationToken)
{
if (message == null) throw new ArgumentNullException(nameof(message));
@ -264,7 +268,7 @@ namespace Rssdp.Infrastructure
IpAddress = new IpAddressInfo(SsdpConstants.MulticastLocalAdminAddress, IpAddressFamily.InterNetwork),
Port = SsdpConstants.MulticastPort
}, cancellationToken).ConfigureAwait(false);
}, fromLocalIpAddress, cancellationToken).ConfigureAwait(false);
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
}
@ -332,14 +336,15 @@ namespace Rssdp.Infrastructure
#region Private Methods
private Task SendMessageIfSocketNotDisposed(byte[] messageData, IpEndPointInfo destination, CancellationToken cancellationToken)
private Task SendMessageIfSocketNotDisposed(byte[] messageData, IpEndPointInfo destination, IpAddressInfo fromLocalIpAddress, CancellationToken cancellationToken)
{
var sockets = _sendSockets;
if (sockets != null)
{
sockets = sockets.ToList();
var tasks = sockets.Select(s => SendFromSocket(s, messageData, destination, cancellationToken));
var tasks = sockets.Where(s => (fromLocalIpAddress == null || fromLocalIpAddress.Equals(s.LocalIPAddress)))
.Select(s => SendFromSocket(s, messageData, destination, cancellationToken));
return Task.WhenAll(tasks);
}
@ -363,11 +368,11 @@ namespace Rssdp.Infrastructure
if (_enableMultiSocketBinding)
{
foreach (var address in _networkManager.GetLocalIpAddresses())
foreach (var address in _networkManager.GetLocalIpAddresses(_config.Configuration.IgnoreVirtualInterfaces))
{
if (address.AddressFamily == IpAddressFamily.InterNetworkV6)
{
// Not supported ?
// Not support IPv6 right now
continue;
}

View file

@ -354,7 +354,7 @@ namespace Rssdp.Infrastructure
var message = BuildMessage(header, values);
return _CommunicationsServer.SendMulticastMessage(message, cancellationToken);
return _CommunicationsServer.SendMulticastMessage(message, null, cancellationToken);
}
private void ProcessSearchResponseMessage(HttpResponseMessage message, IpAddressInfo localIpAddress)

View file

@ -7,6 +7,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.Net;
using MediaBrowser.Common.Net;
using Rssdp;
namespace Rssdp.Infrastructure
@ -16,10 +17,12 @@ namespace Rssdp.Infrastructure
/// </summary>
public class SsdpDevicePublisher : DisposableManagedObjectBase, ISsdpDevicePublisher
{
private readonly INetworkManager _networkManager;
private ISsdpCommunicationsServer _CommsServer;
private string _OSName;
private string _OSVersion;
private bool _sendOnlyMatchedHost;
private bool _SupportPnpRootDevice;
@ -37,9 +40,11 @@ namespace Rssdp.Infrastructure
/// <summary>
/// Default constructor.
/// </summary>
public SsdpDevicePublisher(ISsdpCommunicationsServer communicationsServer, string osName, string osVersion)
public SsdpDevicePublisher(ISsdpCommunicationsServer communicationsServer, INetworkManager networkManager,
string osName, string osVersion, bool sendOnlyMatchedHost)
{
if (communicationsServer == null) throw new ArgumentNullException(nameof(communicationsServer));
if (networkManager == null) throw new ArgumentNullException(nameof(networkManager));
if (osName == null) throw new ArgumentNullException(nameof(osName));
if (osName.Length == 0) throw new ArgumentException("osName cannot be an empty string.", nameof(osName));
if (osVersion == null) throw new ArgumentNullException(nameof(osVersion));
@ -51,10 +56,12 @@ namespace Rssdp.Infrastructure
_RecentSearchRequests = new Dictionary<string, SearchRequest>(StringComparer.OrdinalIgnoreCase);
_Random = new Random();
_networkManager = networkManager;
_CommsServer = communicationsServer;
_CommsServer.RequestReceived += CommsServer_RequestReceived;
_OSName = osName;
_OSVersion = osVersion;
_sendOnlyMatchedHost = sendOnlyMatchedHost;
_CommsServer.BeginListeningForBroadcasts();
}
@ -250,7 +257,11 @@ namespace Rssdp.Infrastructure
foreach (var device in deviceList)
{
SendDeviceSearchResponses(device, remoteEndPoint, receivedOnlocalIpAddress, cancellationToken);
if (!_sendOnlyMatchedHost ||
_networkManager.IsInSameSubnet(device.ToRootDevice().Address, remoteEndPoint.IpAddress, device.ToRootDevice().SubnetMask))
{
SendDeviceSearchResponses(device, remoteEndPoint, receivedOnlocalIpAddress, cancellationToken);
}
}
}
else
@ -427,7 +438,7 @@ namespace Rssdp.Infrastructure
var message = BuildMessage(header, values);
_CommsServer.SendMulticastMessage(message, cancellationToken);
_CommsServer.SendMulticastMessage(message, _sendOnlyMatchedHost ? rootDevice.Address : null, cancellationToken);
//WriteTrace(String.Format("Sent alive notification"), device);
}
@ -472,7 +483,7 @@ namespace Rssdp.Infrastructure
var sendCount = IsDisposed ? 1 : 3;
WriteTrace(String.Format("Sent byebye notification"), device);
return _CommsServer.SendMulticastMessage(message, sendCount, cancellationToken);
return _CommsServer.SendMulticastMessage(message, sendCount, _sendOnlyMatchedHost ? device.ToRootDevice().Address : null, cancellationToken);
}
private void DisposeRebroadcastTimer()

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Text;
using System.Xml;
using Rssdp.Infrastructure;
using MediaBrowser.Model.Net;
namespace Rssdp
{
@ -52,6 +53,15 @@ namespace Rssdp
/// </summary>
public Uri Location { get; set; }
/// <summary>
/// Gets or sets the Address used to check if the received message from same interface with this device/tree. Required.
/// </summary>
public IpAddressInfo Address { get; set; }
/// <summary>
/// Gets or sets the SubnetMask used to check if the received message from same interface with this device/tree. Required.
/// </summary>
public IpAddressInfo SubnetMask { get; set; }
/// <summary>
/// The base URL to use for all relative url's provided in other propertise (and those of child devices). Optional.

View file

@ -1,4 +1,4 @@
using System.Reflection;
[assembly: AssemblyVersion("10.2.1")]
[assembly: AssemblyFileVersion("10.2.1")]
[assembly: AssemblyVersion("10.2.2")]
[assembly: AssemblyFileVersion("10.2.2")]

60
build
View file

@ -26,7 +26,7 @@ usage() {
echo -e " $ build [-k/--keep-artifacts] [-b/--web-branch <web_branch>] <platform> <action>"
echo -e ""
echo -e "The 'keep-artifacts' option preserves build artifacts, e.g. Docker images for system package builds."
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 or can be 'local' to not touch the submodule branching."
echo -e "To build all platforms, use 'all'."
echo -e "To perform all build actions, use 'all'."
echo -e "Build output files are collected at '../jellyfin-build/<platform>'."
@ -164,38 +164,40 @@ for target_platform in ${platform[@]}; do
fi
done
# Initialize submodules
git submodule update --init --recursive
if [[ ${web_branch} != 'local' ]]; then
# Initialize submodules
git submodule update --init --recursive
# configure branch
pushd MediaBrowser.WebDashboard/jellyfin-web
# configure branch
pushd MediaBrowser.WebDashboard/jellyfin-web
if ! git diff-index --quiet HEAD --; then
if ! git diff-index --quiet HEAD --; then
popd
echo
echo "ERROR: Your 'jellyfin-web' submodule working directory is not clean!"
echo "This script will overwrite your unstaged and unpushed changes."
echo "Please do development on 'jellyfin-web' outside of the submodule."
exit 1
fi
git fetch --all
# If this is an official branch name, fetch it from origin
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
echo
echo "ERROR: Your 'jellyfin-web' submodule working directory is not clean!"
echo "This script will overwrite your unstaged and unpushed changes."
echo "Please do development on 'jellyfin-web' outside of the submodule."
exit 1
fi
git fetch --all
# If this is an official branch name, fetch it from origin
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
# Execute each platform and action in order, if said action is enabled
pushd deployment/
for target_platform in ${platform[@]}; do
@ -217,7 +219,7 @@ for target_platform in ${platform[@]}; do
done
if [[ -d pkg-dist/ ]]; then
echo -e ">> Collecting build artifacts"
target_dir="../../../jellyfin-build/${target_platform}"
target_dir="../../../bin/${target_platform}"
mkdir -p ${target_dir}
mv pkg-dist/* ${target_dir}/
fi

15
build.yaml Normal file
View file

@ -0,0 +1,15 @@
---
# We just wrap `build` so this is really it
name: "jellyfin"
version: "10.2.2"
packages:
- debian-package-x64
- debian-package-armhf
- ubuntu-package-x64
- fedora-package-x64
- centos-package-x64
- linux-x64
- macos
- portable
- win-x64
- win-x86

View file

@ -15,7 +15,6 @@ DEFAULT_CONFIG="Release"
DEFAULT_OUTPUT_DIR="dist/jellyfin-git"
DEFAULT_PKG_DIR="pkg-dist"
DEFAULT_DOCKERFILE="Dockerfile"
DEFAULT_IMAGE_TAG="jellyfin:"`git rev-parse --abbrev-ref HEAD`
DEFAULT_ARCHIVE_CMD="tar -xvzf"
# Parse the version from the AssemblyVersion
@ -36,9 +35,9 @@ build_jellyfin()
echo -e "${CYAN}Building jellyfin in '${ROOT}' for ${DOTNETRUNTIME} with configuration ${CONFIG} and output directory '${OUTPUT_DIR}'.${NC}"
if [[ $DOTNETRUNTIME == 'framework' ]]; then
dotnet publish "${ROOT}" --configuration "${CONFIG}" --output="${OUTPUT_DIR}"
dotnet publish "${ROOT}" --configuration "${CONFIG}" --output="${OUTPUT_DIR}" "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
else
dotnet publish "${ROOT}" --configuration "${CONFIG}" --output="${OUTPUT_DIR}" --self-contained --runtime ${DOTNETRUNTIME}
dotnet publish "${ROOT}" --configuration "${CONFIG}" --output="${OUTPUT_DIR}" --self-contained --runtime ${DOTNETRUNTIME} "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
fi
EXIT_CODE=$?
if [ $EXIT_CODE -eq 0 ]; then
@ -53,7 +52,7 @@ build_jellyfin_docker()
(
BUILD_CONTEXT=${1-$DEFAULT_BUILD_CONTEXT}
DOCKERFILE=${2-$DEFAULT_DOCKERFILE}
IMAGE_TAG=${3-$DEFAULT_IMAGE_TAG}
IMAGE_TAG=${3-"jellyfin:$(git rev-parse --abbrev-ref HEAD)"}
echo -e "${CYAN}Building jellyfin docker image in '${BUILD_CONTEXT}' with Dockerfile '${DOCKERFILE}' and tag '${IMAGE_TAG}'.${NC}"
docker build -t ${IMAGE_TAG} -f ${DOCKERFILE} ${BUILD_CONTEXT}

View file

@ -0,0 +1,42 @@
FROM debian:9
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG PLATFORM_DIR=/jellyfin/deployment/debian-package-armhf
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 armhf \
&& apt-get update \
&& apt-get install -y cross-gcc-dev \
&& TARGET_LIST="armhf" cross-gcc-gensource 6 \
&& cd cross-gcc-packages-amd64/cross-gcc-6-armhf \
&& apt-get install -y gcc-6-source libstdc++6-armhf-cross binutils-arm-linux-gnueabihf 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:armhf linux-libc-dev:armhf libgcc1:armhf libcurl4-openssl-dev:armhf libfontconfig1-dev:armhf libfreetype6-dev:armhf liblttng-ust0:armhf libstdc++6:armhf
# 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"]

View file

@ -0,0 +1,34 @@
FROM debian:9
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG PLATFORM_DIR=/jellyfin/deployment/debian-package-armhf
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=armhf
# 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"]

View 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_armhf-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

View file

@ -0,0 +1 @@
docker

View file

@ -0,0 +1,20 @@
#!/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 -aarmhf
# Move the artifacts out
mkdir -p ${ARTIFACT_DIR}/deb
mv /jellyfin_* ${ARTIFACT_DIR}/deb/

View file

@ -0,0 +1,42 @@
#!/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_armhf-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.armhf"
;;
esac
# Set up the build environment Docker image
${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE}
# Build the DEBs and copy out to ${package_temporary_dir}
${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}"
# Correct ownership on the DEBs (as current user, then as root if that fails)
chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null \
|| sudo chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null
# Move the DEBs to the output directory
mkdir -p "${output_dir}"
mv "${package_temporary_dir}"/deb/* "${output_dir}"

View file

@ -0,0 +1 @@
../debian-package-x64/pkg-src

View file

@ -1,3 +1,20 @@
jellyfin (10.2.2-1) unstable; urgency=medium
* jellyfin:
* PR968 Release 10.2.z copr autobuild
* PR964 Install the dotnet runtime package in Fedora build
* PR979 Build Package releases without debug turned on
* PR990 Fix slow local image validation
* PR991 Fix the ffmpeg compatibility
* PR992 Add Debian armhf (Raspberry Pi) build plus crossbuild
* PR998 Set EnableRaisingEvents to true for processes that require it
* PR1017 Set ffmpeg+ffprobe paths in Docker container
* jellyfin-web:
* PR152 Go back on Media stop
* PR156 Fix volume slider not working on nowplayingbar
-- Jellyfin Packaging Team <packaging@jellyfin.org> Thu, 28 Feb 2019 15:32:16 -0500
jellyfin (10.2.1-1) unstable; urgency=medium
* jellyfin:

View file

@ -21,9 +21,9 @@ JELLYFIN_CACHE_DIRECTORY="/var/cache/jellyfin"
# Restart script for in-app server control
JELLYFIN_RESTART_OPT="--restartpath=/usr/lib/jellyfin/restart.sh"
# [OPTIONAL] ffmpeg binary paths, overriding the UI-configured values
#JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/bin/ffmpeg"
#JELLYFIN_FFPROBE_OPT="--ffprobe=/usr/bin/ffprobe"
# ffmpeg binary paths, overriding the system values
JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/share/jellyfin-ffmpeg/ffmpeg"
JELLYFIN_FFPROBE_OPT="--ffprobe=/usr/share/jellyfin-ffmpeg/ffprobe"
# [OPTIONAL] run Jellyfin as a headless service
#JELLYFIN_SERVICE_OPT="--service"

View file

@ -20,7 +20,7 @@ Conflicts: mediabrowser, emby, emby-server-beta, jellyfin-dev, emby-server
Architecture: any
Depends: at,
libsqlite3-0,
ffmpeg (<7:4.1) | jellyfin-ffmpeg,
jellyfin-ffmpeg,
libfontconfig1,
libfreetype6,
libssl1.0.0 | libssl1.0.2

View file

@ -2,7 +2,23 @@
CONFIG := Release
TERM := xterm
SHELL := /bin/bash
DOTNETRUNTIME := debian-x64
HOST_ARCH := $(shell arch)
BUILD_ARCH := ${DEB_HOST_MULTIARCH}
ifeq ($(HOST_ARCH),x86_64)
ifeq ($(BUILD_ARCH),arm-linux-gnueabihf)
# Cross-building ARM on AMD64
DOTNETRUNTIME := debian-arm
else
# Building AMD64
DOTNETRUNTIME := debian-x64
endif
endif
ifeq ($(HOST_ARCH),armv7l)
# Building ARM
DOTNETRUNTIME := debian-arm
endif
export DH_VERBOSE=1
export DOTNET_CLI_TELEMETRY_OPTOUT=1
@ -16,7 +32,8 @@ override_dh_auto_test:
override_dh_clistrip:
override_dh_auto_build:
dotnet publish --configuration $(CONFIG) --output='$(CURDIR)/usr/lib/jellyfin/bin' --self-contained --runtime $(DOTNETRUNTIME) Jellyfin.Server
dotnet publish --configuration $(CONFIG) --output='$(CURDIR)/usr/lib/jellyfin/bin' --self-contained --runtime $(DOTNETRUNTIME) \
"-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" Jellyfin.Server
override_dh_auto_clean:
dotnet clean -maxcpucount:1 --configuration $(CONFIG) Jellyfin.Server || true

View file

@ -13,7 +13,7 @@ RUN dnf update -y \
&& dnf install -y @buildsys-build rpmdevtools dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel \
&& dnf copr enable -y @dotnet-sig/dotnet \
&& rpmdev-setuptree \
&& dnf install -y dotnet-sdk-${SDK_VERSION} \
&& dnf install -y dotnet-sdk-${SDK_VERSION} dotnet-runtime-${SDK_VERSION} \
&& ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh \
&& mkdir -p ${SOURCE_DIR}/SPECS \
&& ln -s ${PLATFORM_DIR}/pkg-src/jellyfin.spec ${SOURCE_DIR}/SPECS/jellyfin.spec \

View file

@ -7,7 +7,7 @@
%endif
Name: jellyfin
Version: 10.2.1
Version: 10.2.2
Release: 1%{?dist}
Summary: The Free Software Media Browser
License: GPLv2
@ -27,7 +27,7 @@ BuildRequires: libcurl-devel, fontconfig-devel, freetype-devel, openssl-devel,
Requires: libcurl, fontconfig, freetype, openssl, glibc libicu
# Requirements not packaged in main repos
# COPR @dotnet-sig/dotnet
BuildRequires: dotnet-sdk-2.2
BuildRequires: dotnet-runtime-2.2, dotnet-sdk-2.2
# RPMfusion free
Requires: ffmpeg
@ -49,7 +49,8 @@ Jellyfin is a free software media system that puts you in control of managing an
%install
export DOTNET_CLI_TELEMETRY_OPTOUT=1
export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
dotnet publish --configuration Release --output='%{buildroot}%{_libdir}/jellyfin' --self-contained --runtime %{dotnet_runtime} Jellyfin.Server
dotnet publish --configuration Release --output='%{buildroot}%{_libdir}/jellyfin' --self-contained --runtime %{dotnet_runtime} \
"-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" Jellyfin.Server
%{__install} -D -m 0644 LICENSE %{buildroot}%{_datadir}/licenses/%{name}/LICENSE
%{__install} -D -m 0644 %{SOURCE5} %{buildroot}%{_sysconfdir}/systemd/system/%{name}.service.d/override.conf
%{__install} -D -m 0644 Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/%{name}/logging.json
@ -73,7 +74,6 @@ EOF
%{_libdir}/%{name}/jellyfin-web/*
%attr(755,root,root) %{_bindir}/%{name}
%{_libdir}/%{name}/*.json
%{_libdir}/%{name}/*.pdb
%{_libdir}/%{name}/*.dll
%{_libdir}/%{name}/*.so
%{_libdir}/%{name}/*.a
@ -140,6 +140,19 @@ fi
%systemd_postun_with_restart jellyfin.service
%changelog
* Thu Feb 28 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
- jellyfin:
- PR968 Release 10.2.z copr autobuild
- PR964 Install the dotnet runtime package in Fedora build
- PR979 Build Package releases without debug turned on
- PR990 Fix slow local image validation
- PR991 Fix the ffmpeg compatibility
- PR992 Add Debian armhf (Raspberry Pi) build plus crossbuild
- PR998 Set EnableRaisingEvents to true for processes that require it
- PR1017 Set ffmpeg+ffprobe paths in Docker container
- jellyfin-web:
- PR152 Go back on Media stop
- PR156 Fix volume slider not working on nowplayingbar
* Wed Feb 20 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
- jellyfin:
- PR920 Fix cachedir missing from Docker container

View file

@ -21,8 +21,8 @@ package_win64() (
cp ${TEMP_DIR}/${FFMPEG_VERSION}/bin/ffmpeg.exe ${OUTPUT_DIR}/ffmpeg.exe
cp ${TEMP_DIR}/${FFMPEG_VERSION}/bin/ffprobe.exe ${OUTPUT_DIR}/ffprobe.exe
rm -r ${TEMP_DIR}
cp ${ROOT}/deployment/win-generic/install-jellyfin.ps1 ${OUTPUT_DIR}/install-jellyfin.ps1
cp ${ROOT}/deployment/win-generic/install.bat ${OUTPUT_DIR}/install.bat
cp ${ROOT}/deployment/windows/install-jellyfin.ps1 ${OUTPUT_DIR}/install-jellyfin.ps1
cp ${ROOT}/deployment/windows/install.bat ${OUTPUT_DIR}/install.bat
mkdir -p ${PKG_DIR}
pushd ${OUTPUT_DIR}
${ARCHIVE_CMD} ${ROOT}/${PKG_DIR}/`basename "${OUTPUT_DIR}"`.zip .

View file

@ -20,8 +20,8 @@ package_win32() (
cp ${TEMP_DIR}/${FFMPEG_VERSION}/bin/ffmpeg.exe ${OUTPUT_DIR}/ffmpeg.exe
cp ${TEMP_DIR}/${FFMPEG_VERSION}/bin/ffprobe.exe ${OUTPUT_DIR}/ffprobe.exe
rm -r ${TEMP_DIR}
cp ${ROOT}/deployment/win-generic/install-jellyfin.ps1 ${OUTPUT_DIR}/install-jellyfin.ps1
cp ${ROOT}/deployment/win-generic/install.bat ${OUTPUT_DIR}/install.bat
cp ${ROOT}/deployment/windows/install-jellyfin.ps1 ${OUTPUT_DIR}/install-jellyfin.ps1
cp ${ROOT}/deployment/windows/install.bat ${OUTPUT_DIR}/install.bat
mkdir -p ${PKG_DIR}
pushd ${OUTPUT_DIR}
${ARCHIVE_CMD} ${ROOT}/${PKG_DIR}/`basename "${OUTPUT_DIR}"`.zip .