commit
cd6b7f3bdc
|
@ -147,7 +147,7 @@ namespace Emby.Common.Implementations
|
||||||
/// <value>The configuration manager.</value>
|
/// <value>The configuration manager.</value>
|
||||||
protected IConfigurationManager ConfigurationManager { get; private set; }
|
protected IConfigurationManager ConfigurationManager { get; private set; }
|
||||||
|
|
||||||
protected IFileSystem FileSystemManager { get; private set; }
|
public IFileSystem FileSystemManager { get; private set; }
|
||||||
|
|
||||||
protected IIsoManager IsoManager { get; private set; }
|
protected IIsoManager IsoManager { get; private set; }
|
||||||
|
|
||||||
|
@ -873,7 +873,13 @@ return null;
|
||||||
/// Gets or sets a value indicating whether this instance can self update.
|
/// Gets or sets a value indicating whether this instance can self update.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value><c>true</c> if this instance can self update; otherwise, <c>false</c>.</value>
|
/// <value><c>true</c> if this instance can self update; otherwise, <c>false</c>.</value>
|
||||||
public abstract bool CanSelfUpdate { get; }
|
public virtual bool CanSelfUpdate
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks for update.
|
/// Checks for update.
|
||||||
|
|
|
@ -379,7 +379,7 @@ namespace Emby.Common.Implementations.ScheduledTasks
|
||||||
/// <exception cref="System.InvalidOperationException">Cannot execute a Task that is already running</exception>
|
/// <exception cref="System.InvalidOperationException">Cannot execute a Task that is already running</exception>
|
||||||
public async Task Execute(TaskExecutionOptions options)
|
public async Task Execute(TaskExecutionOptions options)
|
||||||
{
|
{
|
||||||
var task = ExecuteInternal(options);
|
var task = Task.Run(async () => await ExecuteInternal(options).ConfigureAwait(false));
|
||||||
|
|
||||||
_currentTask = task;
|
_currentTask = task;
|
||||||
|
|
||||||
|
|
|
@ -83,7 +83,6 @@ using Emby.Dlna.MediaReceiverRegistrar;
|
||||||
using Emby.Dlna.Ssdp;
|
using Emby.Dlna.Ssdp;
|
||||||
using Emby.Server.Core;
|
using Emby.Server.Core;
|
||||||
using Emby.Server.Implementations.Activity;
|
using Emby.Server.Implementations.Activity;
|
||||||
using Emby.Server.Core.Configuration;
|
|
||||||
using Emby.Server.Implementations.Devices;
|
using Emby.Server.Implementations.Devices;
|
||||||
using Emby.Server.Implementations.FFMpeg;
|
using Emby.Server.Implementations.FFMpeg;
|
||||||
using Emby.Server.Core.IO;
|
using Emby.Server.Core.IO;
|
||||||
|
@ -91,10 +90,8 @@ using Emby.Server.Core.Localization;
|
||||||
using Emby.Server.Implementations.Migrations;
|
using Emby.Server.Implementations.Migrations;
|
||||||
using Emby.Server.Implementations.Security;
|
using Emby.Server.Implementations.Security;
|
||||||
using Emby.Server.Implementations.Social;
|
using Emby.Server.Implementations.Social;
|
||||||
using Emby.Server.Implementations.Sync;
|
|
||||||
using Emby.Server.Implementations.Channels;
|
using Emby.Server.Implementations.Channels;
|
||||||
using Emby.Server.Implementations.Collections;
|
using Emby.Server.Implementations.Collections;
|
||||||
using Emby.Server.Implementations.Connect;
|
|
||||||
using Emby.Server.Implementations.Dto;
|
using Emby.Server.Implementations.Dto;
|
||||||
using Emby.Server.Implementations.EntryPoints;
|
using Emby.Server.Implementations.EntryPoints;
|
||||||
using Emby.Server.Implementations.FileOrganization;
|
using Emby.Server.Implementations.FileOrganization;
|
||||||
|
@ -111,7 +108,6 @@ using Emby.Server.Implementations;
|
||||||
using Emby.Server.Implementations.ServerManager;
|
using Emby.Server.Implementations.ServerManager;
|
||||||
using Emby.Server.Implementations.Session;
|
using Emby.Server.Implementations.Session;
|
||||||
using Emby.Server.Implementations.Social;
|
using Emby.Server.Implementations.Social;
|
||||||
using Emby.Server.Implementations.Sync;
|
|
||||||
using Emby.Server.Implementations.TV;
|
using Emby.Server.Implementations.TV;
|
||||||
using Emby.Server.Implementations.Updates;
|
using Emby.Server.Implementations.Updates;
|
||||||
using MediaBrowser.Model.Activity;
|
using MediaBrowser.Model.Activity;
|
||||||
|
@ -134,6 +130,7 @@ using Emby.Drawing;
|
||||||
using Emby.Server.Implementations.Migrations;
|
using Emby.Server.Implementations.Migrations;
|
||||||
using MediaBrowser.Model.Diagnostics;
|
using MediaBrowser.Model.Diagnostics;
|
||||||
using Emby.Common.Implementations.Diagnostics;
|
using Emby.Common.Implementations.Diagnostics;
|
||||||
|
using Emby.Server.Implementations.Configuration;
|
||||||
|
|
||||||
namespace Emby.Server.Core
|
namespace Emby.Server.Core
|
||||||
{
|
{
|
||||||
|
@ -312,7 +309,13 @@ namespace Emby.Server.Core
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract bool SupportsRunningAsService { get; }
|
public virtual bool SupportsRunningAsService
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the name.
|
/// Gets the name.
|
||||||
|
@ -326,14 +329,26 @@ namespace Emby.Server.Core
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract bool IsRunningAsService { get; }
|
public virtual bool IsRunningAsService
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Assembly GetAssembly(Type type)
|
private Assembly GetAssembly(Type type)
|
||||||
{
|
{
|
||||||
return type.GetTypeInfo().Assembly;
|
return type.GetTypeInfo().Assembly;
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract bool SupportsAutoRunAtStartup { get; }
|
public virtual bool SupportsAutoRunAtStartup
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void SetBaseExceptionMessage()
|
private void SetBaseExceptionMessage()
|
||||||
{
|
{
|
||||||
|
@ -508,6 +523,9 @@ namespace Emby.Server.Core
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected abstract IConnectManager CreateConnectManager();
|
||||||
|
protected abstract ISyncManager CreateSyncManager();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Registers resources that classes will depend on
|
/// Registers resources that classes will depend on
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -568,9 +586,6 @@ namespace Emby.Server.Core
|
||||||
AuthenticationRepository = await GetAuthenticationRepository().ConfigureAwait(false);
|
AuthenticationRepository = await GetAuthenticationRepository().ConfigureAwait(false);
|
||||||
RegisterSingleInstance(AuthenticationRepository);
|
RegisterSingleInstance(AuthenticationRepository);
|
||||||
|
|
||||||
SyncRepository = GetSyncRepository();
|
|
||||||
RegisterSingleInstance(SyncRepository);
|
|
||||||
|
|
||||||
UserManager = new UserManager(LogManager.GetLogger("UserManager"), ServerConfigurationManager, UserRepository, XmlSerializer, NetworkManager, () => ImageProcessor, () => DtoService, () => ConnectManager, this, JsonSerializer, FileSystemManager, CryptographyProvider, _defaultUserNameFactory());
|
UserManager = new UserManager(LogManager.GetLogger("UserManager"), ServerConfigurationManager, UserRepository, XmlSerializer, NetworkManager, () => ImageProcessor, () => DtoService, () => ConnectManager, this, JsonSerializer, FileSystemManager, CryptographyProvider, _defaultUserNameFactory());
|
||||||
RegisterSingleInstance(UserManager);
|
RegisterSingleInstance(UserManager);
|
||||||
|
|
||||||
|
@ -608,7 +623,7 @@ namespace Emby.Server.Core
|
||||||
TVSeriesManager = new TVSeriesManager(UserManager, UserDataManager, LibraryManager, ServerConfigurationManager);
|
TVSeriesManager = new TVSeriesManager(UserManager, UserDataManager, LibraryManager, ServerConfigurationManager);
|
||||||
RegisterSingleInstance(TVSeriesManager);
|
RegisterSingleInstance(TVSeriesManager);
|
||||||
|
|
||||||
SyncManager = new SyncManager(LibraryManager, SyncRepository, ImageProcessor, LogManager.GetLogger("SyncManager"), UserManager, () => DtoService, this, TVSeriesManager, () => MediaEncoder, FileSystemManager, () => SubtitleEncoder, ServerConfigurationManager, UserDataManager, () => MediaSourceManager, JsonSerializer, TaskManager, MemoryStreamFactory);
|
SyncManager = CreateSyncManager();
|
||||||
RegisterSingleInstance(SyncManager);
|
RegisterSingleInstance(SyncManager);
|
||||||
|
|
||||||
DtoService = new DtoService(LogManager.GetLogger("DtoService"), LibraryManager, UserDataManager, ItemRepository, ImageProcessor, ServerConfigurationManager, FileSystemManager, ProviderManager, () => ChannelManager, SyncManager, this, () => DeviceManager, () => MediaSourceManager, () => LiveTvManager);
|
DtoService = new DtoService(LogManager.GetLogger("DtoService"), LibraryManager, UserDataManager, ItemRepository, ImageProcessor, ServerConfigurationManager, FileSystemManager, ProviderManager, () => ChannelManager, SyncManager, this, () => DeviceManager, () => MediaSourceManager, () => LiveTvManager);
|
||||||
|
@ -617,7 +632,7 @@ namespace Emby.Server.Core
|
||||||
var encryptionManager = new EncryptionManager();
|
var encryptionManager = new EncryptionManager();
|
||||||
RegisterSingleInstance<IEncryptionManager>(encryptionManager);
|
RegisterSingleInstance<IEncryptionManager>(encryptionManager);
|
||||||
|
|
||||||
ConnectManager = new ConnectManager(LogManager.GetLogger("ConnectManager"), ApplicationPaths, JsonSerializer, encryptionManager, HttpClient, this, ServerConfigurationManager, UserManager, ProviderManager, SecurityManager, FileSystemManager);
|
ConnectManager = CreateConnectManager();
|
||||||
RegisterSingleInstance(ConnectManager);
|
RegisterSingleInstance(ConnectManager);
|
||||||
|
|
||||||
DeviceManager = new DeviceManager(new DeviceRepository(ApplicationPaths, JsonSerializer, LogManager.GetLogger("DeviceManager"), FileSystemManager), UserManager, FileSystemManager, LibraryMonitor, ServerConfigurationManager, LogManager.GetLogger("DeviceManager"), NetworkManager);
|
DeviceManager = new DeviceManager(new DeviceRepository(ApplicationPaths, JsonSerializer, LogManager.GetLogger("DeviceManager"), FileSystemManager), UserManager, FileSystemManager, LibraryMonitor, ServerConfigurationManager, LogManager.GetLogger("DeviceManager"), NetworkManager);
|
||||||
|
@ -716,7 +731,13 @@ namespace Emby.Server.Core
|
||||||
await ((UserManager)UserManager).Initialize().ConfigureAwait(false);
|
await ((UserManager)UserManager).Initialize().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract bool SupportsDualModeSockets { get; }
|
protected virtual bool SupportsDualModeSockets
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private ICertificate GetCertificate(string certificateLocation)
|
private ICertificate GetCertificate(string certificateLocation)
|
||||||
{
|
{
|
||||||
|
@ -761,7 +782,77 @@ namespace Emby.Server.Core
|
||||||
return new ImageProcessor(LogManager.GetLogger("ImageProcessor"), ServerConfigurationManager.ApplicationPaths, FileSystemManager, JsonSerializer, ImageEncoder, maxConcurrentImageProcesses, () => LibraryManager, TimerFactory);
|
return new ImageProcessor(LogManager.GetLogger("ImageProcessor"), ServerConfigurationManager.ApplicationPaths, FileSystemManager, JsonSerializer, ImageEncoder, maxConcurrentImageProcesses, () => LibraryManager, TimerFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract FFMpegInstallInfo GetFfmpegInstallInfo();
|
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 = "20160215";
|
||||||
|
info.DownloadUrls = GetLinuxDownloadUrls();
|
||||||
|
}
|
||||||
|
else if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows)
|
||||||
|
{
|
||||||
|
info.FFMpegFilename = "ffmpeg.exe";
|
||||||
|
info.FFProbeFilename = "ffprobe.exe";
|
||||||
|
info.Version = "20160410";
|
||||||
|
info.ArchiveType = "7z";
|
||||||
|
info.DownloadUrls = GetWindowsDownloadUrls();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// No version available - user requirement
|
||||||
|
info.DownloadUrls = new string[] { };
|
||||||
|
}
|
||||||
|
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string[] GetWindowsDownloadUrls()
|
||||||
|
{
|
||||||
|
switch (EnvironmentInfo.SystemArchitecture)
|
||||||
|
{
|
||||||
|
case Architecture.X64:
|
||||||
|
return new[]
|
||||||
|
{
|
||||||
|
"https://github.com/MediaBrowser/Emby.Resources/raw/master/ffmpeg/windows/ffmpeg-20160410-win64.7z"
|
||||||
|
};
|
||||||
|
case Architecture.X86:
|
||||||
|
return new[]
|
||||||
|
{
|
||||||
|
"https://github.com/MediaBrowser/Emby.Resources/raw/master/ffmpeg/windows/ffmpeg-20160410-win32.7z"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return new string[] { };
|
||||||
|
}
|
||||||
|
|
||||||
|
private string[] GetLinuxDownloadUrls()
|
||||||
|
{
|
||||||
|
switch (EnvironmentInfo.SystemArchitecture)
|
||||||
|
{
|
||||||
|
case Architecture.X64:
|
||||||
|
return new[]
|
||||||
|
{
|
||||||
|
"https://github.com/MediaBrowser/Emby.Resources/raw/master/ffmpeg/linux/ffmpeg-git-20160215-64bit-static.7z"
|
||||||
|
};
|
||||||
|
case Architecture.X86:
|
||||||
|
return new[]
|
||||||
|
{
|
||||||
|
"https://github.com/MediaBrowser/Emby.Resources/raw/master/ffmpeg/linux/ffmpeg-git-20160215-32bit-static.7z"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return new string[] { };
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Registers the media encoder.
|
/// Registers the media encoder.
|
||||||
|
@ -849,21 +940,12 @@ namespace Emby.Server.Core
|
||||||
return repo;
|
return repo;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ISyncRepository GetSyncRepository()
|
|
||||||
{
|
|
||||||
var repo = new SyncRepository(LogManager.GetLogger("SyncRepository"), JsonSerializer, ServerConfigurationManager.ApplicationPaths);
|
|
||||||
|
|
||||||
repo.Initialize();
|
|
||||||
|
|
||||||
return repo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Configures the repositories.
|
/// Configures the repositories.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void ConfigureNotificationsRepository()
|
private void ConfigureNotificationsRepository()
|
||||||
{
|
{
|
||||||
var repo = new SqliteNotificationsRepository(LogManager.GetLogger("SqliteNotificationsRepository"), ServerConfigurationManager.ApplicationPaths);
|
var repo = new SqliteNotificationsRepository(LogManager.GetLogger("SqliteNotificationsRepository"), ServerConfigurationManager.ApplicationPaths, FileSystemManager);
|
||||||
|
|
||||||
repo.Initialize();
|
repo.Initialize();
|
||||||
|
|
||||||
|
@ -1158,7 +1240,7 @@ namespace Emby.Server.Core
|
||||||
list.Add(GetAssembly(typeof(InstallationManager)));
|
list.Add(GetAssembly(typeof(InstallationManager)));
|
||||||
|
|
||||||
// Emby.Server.Core
|
// Emby.Server.Core
|
||||||
list.Add(GetAssembly(typeof(ServerApplicationPaths)));
|
list.Add(GetAssembly(typeof(ApplicationHost)));
|
||||||
|
|
||||||
// MediaEncoding
|
// MediaEncoding
|
||||||
list.Add(GetAssembly(typeof(MediaEncoder)));
|
list.Add(GetAssembly(typeof(MediaEncoder)));
|
||||||
|
@ -1489,7 +1571,10 @@ namespace Emby.Server.Core
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract void AuthorizeServer();
|
protected virtual void AuthorizeServer()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
public event EventHandler HasUpdateAvailableChanged;
|
public event EventHandler HasUpdateAvailableChanged;
|
||||||
|
|
||||||
|
@ -1565,7 +1650,10 @@ namespace Emby.Server.Core
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract void ConfigureAutoRunInternal(bool autorun);
|
protected virtual void ConfigureAutoRunInternal(bool autorun)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This returns localhost in the case of no external dns, and the hostname if the
|
/// This returns localhost in the case of no external dns, and the hostname if the
|
||||||
|
@ -1631,7 +1719,10 @@ namespace Emby.Server.Core
|
||||||
EnableLoopbackInternal(appName);
|
EnableLoopbackInternal(appName);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract void EnableLoopbackInternal(string appName);
|
protected virtual void EnableLoopbackInternal(string appName)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
private void RegisterModules()
|
private void RegisterModules()
|
||||||
{
|
{
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
<SchemaVersion>2.0</SchemaVersion>
|
<SchemaVersion>2.0</SchemaVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\ServiceStack\ServiceStack.csproj" />
|
|
||||||
<ProjectReference Include="..\Emby.Drawing\Emby.Drawing.csproj" />
|
<ProjectReference Include="..\Emby.Drawing\Emby.Drawing.csproj" />
|
||||||
<ProjectReference Include="..\Emby.Photos\Emby.Photos.csproj" />
|
<ProjectReference Include="..\Emby.Photos\Emby.Photos.csproj" />
|
||||||
<ProjectReference Include="..\MediaBrowser.Api\MediaBrowser.Api.csproj" />
|
<ProjectReference Include="..\MediaBrowser.Api\MediaBrowser.Api.csproj" />
|
||||||
|
|
|
@ -187,7 +187,10 @@ namespace Emby.Server.Core.EntryPoints
|
||||||
|
|
||||||
private void ClearCreatedRules(object state)
|
private void ClearCreatedRules(object state)
|
||||||
{
|
{
|
||||||
_createdRules = new List<string>();
|
lock (_createdRules)
|
||||||
|
{
|
||||||
|
_createdRules.Clear();
|
||||||
|
}
|
||||||
lock (_usnsHandled)
|
lock (_usnsHandled)
|
||||||
{
|
{
|
||||||
_usnsHandled.Clear();
|
_usnsHandled.Clear();
|
||||||
|
@ -236,9 +239,17 @@ namespace Emby.Server.Core.EntryPoints
|
||||||
|
|
||||||
var address = device.LocalAddress.ToString();
|
var address = device.LocalAddress.ToString();
|
||||||
|
|
||||||
|
lock (_createdRules)
|
||||||
|
{
|
||||||
if (!_createdRules.Contains(address))
|
if (!_createdRules.Contains(address))
|
||||||
{
|
{
|
||||||
_createdRules.Add(address);
|
_createdRules.Add(address);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var success = await CreatePortMap(device, _appHost.HttpPort, _config.Configuration.PublicPort).ConfigureAwait(false);
|
var success = await CreatePortMap(device, _appHost.HttpPort, _config.Configuration.PublicPort).ConfigureAwait(false);
|
||||||
|
|
||||||
|
@ -247,7 +258,6 @@ namespace Emby.Server.Core.EntryPoints
|
||||||
await CreatePortMap(device, _appHost.HttpsPort, _config.Configuration.PublicHttpsPort).ConfigureAwait(false);
|
await CreatePortMap(device, _appHost.HttpsPort, _config.Configuration.PublicHttpsPort).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> CreatePortMap(INatDevice device, int privatePort, int publicPort)
|
private async Task<bool> CreatePortMap(INatDevice device, int privatePort, int publicPort)
|
||||||
{
|
{
|
||||||
|
|
16
Emby.Server.Core/Logging/ConsoleLogger.cs
Normal file
16
Emby.Server.Core/Logging/ConsoleLogger.cs
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MediaBrowser.Model.Logging;
|
||||||
|
|
||||||
|
namespace Emby.Server.Core.Logging
|
||||||
|
{
|
||||||
|
public class ConsoleLogger : IConsoleLogger
|
||||||
|
{
|
||||||
|
public void WriteLine(string message)
|
||||||
|
{
|
||||||
|
Console.WriteLine(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -56,9 +56,6 @@
|
||||||
"Emby.Drawing": {
|
"Emby.Drawing": {
|
||||||
"target": "project"
|
"target": "project"
|
||||||
},
|
},
|
||||||
"ServiceStack": {
|
|
||||||
"target": "project"
|
|
||||||
},
|
|
||||||
"SocketHttpListener.Portable": {
|
"SocketHttpListener.Portable": {
|
||||||
"target": "project"
|
"target": "project"
|
||||||
}
|
}
|
||||||
|
@ -121,9 +118,6 @@
|
||||||
},
|
},
|
||||||
"SocketHttpListener.Portable": {
|
"SocketHttpListener.Portable": {
|
||||||
"target": "project"
|
"target": "project"
|
||||||
},
|
|
||||||
"ServiceStack": {
|
|
||||||
"target": "project"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
using System.IO;
|
using System;
|
||||||
|
using System.IO;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
|
|
||||||
namespace Emby.Common.Implementations
|
namespace Emby.Server.Implementations.AppBase
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides a base class to hold common application paths used by both the Ui and Server.
|
/// Provides a base class to hold common application paths used by both the Ui and Server.
|
||||||
|
@ -12,12 +13,15 @@ namespace Emby.Common.Implementations
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="BaseApplicationPaths"/> class.
|
/// Initializes a new instance of the <see cref="BaseApplicationPaths"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected BaseApplicationPaths(string programDataPath, string appFolderPath)
|
protected BaseApplicationPaths(string programDataPath, string appFolderPath, Action<string> createDirectoryFn)
|
||||||
{
|
{
|
||||||
ProgramDataPath = programDataPath;
|
ProgramDataPath = programDataPath;
|
||||||
ProgramSystemPath = appFolderPath;
|
ProgramSystemPath = appFolderPath;
|
||||||
|
CreateDirectoryFn = createDirectoryFn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected Action<string> CreateDirectoryFn;
|
||||||
|
|
||||||
public string ProgramDataPath { get; private set; }
|
public string ProgramDataPath { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -41,7 +45,7 @@ namespace Emby.Common.Implementations
|
||||||
{
|
{
|
||||||
_dataDirectory = Path.Combine(ProgramDataPath, "data");
|
_dataDirectory = Path.Combine(ProgramDataPath, "data");
|
||||||
|
|
||||||
Directory.CreateDirectory(_dataDirectory);
|
CreateDirectoryFn(_dataDirectory);
|
||||||
}
|
}
|
||||||
|
|
||||||
return _dataDirectory;
|
return _dataDirectory;
|
||||||
|
@ -148,7 +152,7 @@ namespace Emby.Common.Implementations
|
||||||
{
|
{
|
||||||
_cachePath = Path.Combine(ProgramDataPath, "cache");
|
_cachePath = Path.Combine(ProgramDataPath, "cache");
|
||||||
|
|
||||||
Directory.CreateDirectory(_cachePath);
|
CreateDirectoryFn(_cachePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
return _cachePath;
|
return _cachePath;
|
|
@ -7,13 +7,12 @@ using System.Threading;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Common.Events;
|
using MediaBrowser.Common.Events;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using Emby.Common.Implementations;
|
|
||||||
using MediaBrowser.Model.Configuration;
|
using MediaBrowser.Model.Configuration;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Model.Logging;
|
using MediaBrowser.Model.Logging;
|
||||||
using MediaBrowser.Model.Serialization;
|
using MediaBrowser.Model.Serialization;
|
||||||
|
|
||||||
namespace Emby.Common.Implementations.Configuration
|
namespace Emby.Server.Implementations.AppBase
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class BaseConfigurationManager
|
/// Class BaseConfigurationManager
|
|
@ -4,7 +4,7 @@ using System.Linq;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Model.Serialization;
|
using MediaBrowser.Model.Serialization;
|
||||||
|
|
||||||
namespace Emby.Common.Implementations.Configuration
|
namespace Emby.Server.Implementations.AppBase
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class ConfigurationHelper
|
/// Class ConfigurationHelper
|
|
@ -2,7 +2,7 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Emby.Common.Implementations.Configuration;
|
using Emby.Server.Implementations.AppBase;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Common.Events;
|
using MediaBrowser.Common.Events;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
|
@ -17,7 +17,7 @@ using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Model.Logging;
|
using MediaBrowser.Model.Logging;
|
||||||
using MediaBrowser.Model.Serialization;
|
using MediaBrowser.Model.Serialization;
|
||||||
|
|
||||||
namespace Emby.Server.Core.Configuration
|
namespace Emby.Server.Implementations.Configuration
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class ServerConfigurationManager
|
/// Class ServerConfigurationManager
|
||||||
|
@ -187,7 +187,7 @@ namespace Emby.Server.Core.Configuration
|
||||||
// Validate
|
// Validate
|
||||||
if (!FileSystem.DirectoryExists(newPath))
|
if (!FileSystem.DirectoryExists(newPath))
|
||||||
{
|
{
|
||||||
throw new DirectoryNotFoundException(string.Format("{0} does not exist.", newPath));
|
throw new FileNotFoundException(string.Format("{0} does not exist.", newPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
EnsureWriteAccess(newPath);
|
EnsureWriteAccess(newPath);
|
|
@ -1,36 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Connect
|
|
||||||
{
|
|
||||||
public class ConnectData
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the server identifier.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The server identifier.</value>
|
|
||||||
public string ServerId { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the access key.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The access key.</value>
|
|
||||||
public string AccessKey { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the authorizations.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The authorizations.</value>
|
|
||||||
public List<ConnectAuthorizationInternal> PendingAuthorizations { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the last authorizations refresh.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The last authorizations refresh.</value>
|
|
||||||
public DateTime LastAuthorizationsRefresh { get; set; }
|
|
||||||
|
|
||||||
public ConnectData()
|
|
||||||
{
|
|
||||||
PendingAuthorizations = new List<ConnectAuthorizationInternal>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,218 +0,0 @@
|
||||||
using MediaBrowser.Common;
|
|
||||||
using MediaBrowser.Common.Configuration;
|
|
||||||
using MediaBrowser.Common.Net;
|
|
||||||
using MediaBrowser.Controller.Connect;
|
|
||||||
using MediaBrowser.Controller.Plugins;
|
|
||||||
using MediaBrowser.Model.Logging;
|
|
||||||
using MediaBrowser.Model.Net;
|
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using MediaBrowser.Controller.Security;
|
|
||||||
using MediaBrowser.Model.IO;
|
|
||||||
using MediaBrowser.Model.Threading;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Connect
|
|
||||||
{
|
|
||||||
public class ConnectEntryPoint : IServerEntryPoint
|
|
||||||
{
|
|
||||||
private ITimer _timer;
|
|
||||||
private IpAddressInfo _cachedIpAddress;
|
|
||||||
private readonly IHttpClient _httpClient;
|
|
||||||
private readonly IApplicationPaths _appPaths;
|
|
||||||
private readonly ILogger _logger;
|
|
||||||
private readonly IConnectManager _connectManager;
|
|
||||||
|
|
||||||
private readonly INetworkManager _networkManager;
|
|
||||||
private readonly IApplicationHost _appHost;
|
|
||||||
private readonly IFileSystem _fileSystem;
|
|
||||||
private readonly ITimerFactory _timerFactory;
|
|
||||||
private readonly IEncryptionManager _encryption;
|
|
||||||
|
|
||||||
public ConnectEntryPoint(IHttpClient httpClient, IApplicationPaths appPaths, ILogger logger, INetworkManager networkManager, IConnectManager connectManager, IApplicationHost appHost, IFileSystem fileSystem, ITimerFactory timerFactory, IEncryptionManager encryption)
|
|
||||||
{
|
|
||||||
_httpClient = httpClient;
|
|
||||||
_appPaths = appPaths;
|
|
||||||
_logger = logger;
|
|
||||||
_networkManager = networkManager;
|
|
||||||
_connectManager = connectManager;
|
|
||||||
_appHost = appHost;
|
|
||||||
_fileSystem = fileSystem;
|
|
||||||
_timerFactory = timerFactory;
|
|
||||||
_encryption = encryption;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Run()
|
|
||||||
{
|
|
||||||
LoadCachedAddress();
|
|
||||||
|
|
||||||
_timer = _timerFactory.Create(TimerCallback, null, TimeSpan.FromSeconds(5), TimeSpan.FromHours(1));
|
|
||||||
((ConnectManager)_connectManager).Start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly string[] _ipLookups =
|
|
||||||
{
|
|
||||||
"http://bot.whatismyipaddress.com",
|
|
||||||
"https://connect.emby.media/service/ip"
|
|
||||||
};
|
|
||||||
|
|
||||||
private async void TimerCallback(object state)
|
|
||||||
{
|
|
||||||
IpAddressInfo validIpAddress = null;
|
|
||||||
|
|
||||||
foreach (var ipLookupUrl in _ipLookups)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
validIpAddress = await GetIpAddress(ipLookupUrl).ConfigureAwait(false);
|
|
||||||
|
|
||||||
// Try to find the ipv4 address, if present
|
|
||||||
if (validIpAddress.AddressFamily != IpAddressFamily.InterNetworkV6)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (HttpException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.ErrorException("Error getting connection info", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If this produced an ipv6 address, try again
|
|
||||||
if (validIpAddress != null && validIpAddress.AddressFamily == IpAddressFamily.InterNetworkV6)
|
|
||||||
{
|
|
||||||
foreach (var ipLookupUrl in _ipLookups)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var newAddress = await GetIpAddress(ipLookupUrl, true).ConfigureAwait(false);
|
|
||||||
|
|
||||||
// Try to find the ipv4 address, if present
|
|
||||||
if (newAddress.AddressFamily != IpAddressFamily.InterNetworkV6)
|
|
||||||
{
|
|
||||||
validIpAddress = newAddress;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (HttpException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.ErrorException("Error getting connection info", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (validIpAddress != null)
|
|
||||||
{
|
|
||||||
((ConnectManager)_connectManager).OnWanAddressResolved(validIpAddress);
|
|
||||||
CacheAddress(validIpAddress);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<IpAddressInfo> GetIpAddress(string lookupUrl, bool preferIpv4 = false)
|
|
||||||
{
|
|
||||||
// Sometimes whatismyipaddress might fail, but it won't do us any good having users raise alarms over it.
|
|
||||||
var logErrors = false;
|
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
logErrors = true;
|
|
||||||
#endif
|
|
||||||
using (var stream = await _httpClient.Get(new HttpRequestOptions
|
|
||||||
{
|
|
||||||
Url = lookupUrl,
|
|
||||||
UserAgent = "Emby/" + _appHost.ApplicationVersion,
|
|
||||||
LogErrors = logErrors,
|
|
||||||
|
|
||||||
// Seeing block length errors with our server
|
|
||||||
EnableHttpCompression = false,
|
|
||||||
PreferIpv4 = preferIpv4,
|
|
||||||
BufferContent = false
|
|
||||||
|
|
||||||
}).ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
using (var reader = new StreamReader(stream))
|
|
||||||
{
|
|
||||||
var addressString = await reader.ReadToEndAsync().ConfigureAwait(false);
|
|
||||||
|
|
||||||
return _networkManager.ParseIpAddress(addressString);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string CacheFilePath
|
|
||||||
{
|
|
||||||
get { return Path.Combine(_appPaths.DataPath, "wan.dat"); }
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CacheAddress(IpAddressInfo address)
|
|
||||||
{
|
|
||||||
if (_cachedIpAddress != null && _cachedIpAddress.Equals(address))
|
|
||||||
{
|
|
||||||
// no need to update the file if the address has not changed
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var path = CacheFilePath;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_fileSystem.CreateDirectory(Path.GetDirectoryName(path));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_fileSystem.WriteAllText(path, _encryption.EncryptString(address.ToString()), Encoding.UTF8);
|
|
||||||
_cachedIpAddress = address;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.ErrorException("Error saving data", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LoadCachedAddress()
|
|
||||||
{
|
|
||||||
var path = CacheFilePath;
|
|
||||||
|
|
||||||
_logger.Info("Loading data from {0}", path);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var endpoint = _encryption.DecryptString(_fileSystem.ReadAllText(path, Encoding.UTF8));
|
|
||||||
IpAddressInfo ipAddress;
|
|
||||||
|
|
||||||
if (_networkManager.TryParseIpAddress(endpoint, out ipAddress))
|
|
||||||
{
|
|
||||||
_cachedIpAddress = ipAddress;
|
|
||||||
((ConnectManager)_connectManager).OnWanAddressResolved(ipAddress);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (IOException)
|
|
||||||
{
|
|
||||||
// File isn't there. no biggie
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.ErrorException("Error loading data", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
if (_timer != null)
|
|
||||||
{
|
|
||||||
_timer.Dispose();
|
|
||||||
_timer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,85 +0,0 @@
|
||||||
using MediaBrowser.Model.Configuration;
|
|
||||||
using MediaBrowser.Model.Connect;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Connect
|
|
||||||
{
|
|
||||||
public class ServerRegistrationResponse
|
|
||||||
{
|
|
||||||
public string Id { get; set; }
|
|
||||||
public string Url { get; set; }
|
|
||||||
public string Name { get; set; }
|
|
||||||
public string AccessKey { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class UpdateServerRegistrationResponse
|
|
||||||
{
|
|
||||||
public string Id { get; set; }
|
|
||||||
public string Url { get; set; }
|
|
||||||
public string Name { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class GetConnectUserResponse
|
|
||||||
{
|
|
||||||
public string Id { get; set; }
|
|
||||||
public string Name { get; set; }
|
|
||||||
public string DisplayName { get; set; }
|
|
||||||
public string Email { get; set; }
|
|
||||||
public bool IsActive { get; set; }
|
|
||||||
public string ImageUrl { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ServerUserAuthorizationResponse
|
|
||||||
{
|
|
||||||
public string Id { get; set; }
|
|
||||||
public string ServerId { get; set; }
|
|
||||||
public string UserId { get; set; }
|
|
||||||
public string AccessToken { get; set; }
|
|
||||||
public string DateCreated { get; set; }
|
|
||||||
public bool IsActive { get; set; }
|
|
||||||
public string AcceptStatus { get; set; }
|
|
||||||
public string UserType { get; set; }
|
|
||||||
public string UserImageUrl { get; set; }
|
|
||||||
public string UserName { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ConnectUserPreferences
|
|
||||||
{
|
|
||||||
public string[] PreferredAudioLanguages { get; set; }
|
|
||||||
public bool PlayDefaultAudioTrack { get; set; }
|
|
||||||
public string[] PreferredSubtitleLanguages { get; set; }
|
|
||||||
public SubtitlePlaybackMode SubtitleMode { get; set; }
|
|
||||||
public bool GroupMoviesIntoBoxSets { get; set; }
|
|
||||||
|
|
||||||
public ConnectUserPreferences()
|
|
||||||
{
|
|
||||||
PreferredAudioLanguages = new string[] { };
|
|
||||||
PreferredSubtitleLanguages = new string[] { };
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ConnectUserPreferences FromUserConfiguration(UserConfiguration config)
|
|
||||||
{
|
|
||||||
return new ConnectUserPreferences
|
|
||||||
{
|
|
||||||
PlayDefaultAudioTrack = config.PlayDefaultAudioTrack,
|
|
||||||
SubtitleMode = config.SubtitleMode,
|
|
||||||
PreferredAudioLanguages = string.IsNullOrWhiteSpace(config.AudioLanguagePreference) ? new string[] { } : new[] { config.AudioLanguagePreference },
|
|
||||||
PreferredSubtitleLanguages = string.IsNullOrWhiteSpace(config.SubtitleLanguagePreference) ? new string[] { } : new[] { config.SubtitleLanguagePreference }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public void MergeInto(UserConfiguration config)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class UserPreferencesDto<T>
|
|
||||||
{
|
|
||||||
public T data { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ConnectAuthorizationInternal : ConnectAuthorization
|
|
||||||
{
|
|
||||||
public string AccessToken { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Connect
|
|
||||||
{
|
|
||||||
public static class Validator
|
|
||||||
{
|
|
||||||
static readonly Regex ValidEmailRegex = CreateValidEmailRegex();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Taken from http://haacked.com/archive/2007/08/21/i-knew-how-to-validate-an-email-address-until-i.aspx
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
private static Regex CreateValidEmailRegex()
|
|
||||||
{
|
|
||||||
const string validEmailPattern = @"^(?!\.)(""([^""\r\\]|\\[""\r\\])*""|"
|
|
||||||
+ @"([-a-z0-9!#$%&'*+/=?^_`{|}~]|(?<!\.)\.)*)(?<!\.)"
|
|
||||||
+ @"@[a-z0-9][\w\.-]*[a-z0-9]\.[a-z][a-z\.]*[a-z]$";
|
|
||||||
|
|
||||||
return new Regex(validEmailPattern, RegexOptions.IgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static bool EmailIsValid(string emailAddress)
|
|
||||||
{
|
|
||||||
bool isValid = ValidEmailRegex.IsMatch(emailAddress);
|
|
||||||
|
|
||||||
return isValid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2872,7 +2872,8 @@ namespace Emby.Server.Implementations.Data
|
||||||
}
|
}
|
||||||
if (string.Equals(name, ItemSortBy.IsFavoriteOrLiked, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(name, ItemSortBy.IsFavoriteOrLiked, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return new Tuple<string, bool>("IsFavorite", true);
|
// (Select Case When Abs(COALESCE(ProductionYear, 0) - @ItemProductionYear) < 10 Then 2 Else 0 End )
|
||||||
|
return new Tuple<string, bool>("(Select Case When IsFavorite is null Then 0 Else IsFavorite End )", true);
|
||||||
}
|
}
|
||||||
if (string.Equals(name, ItemSortBy.IsFolder, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(name, ItemSortBy.IsFolder, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
@ -3874,6 +3875,25 @@ namespace Emby.Server.Implementations.Data
|
||||||
whereClauses.Add(clause);
|
whereClauses.Add(clause);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (query.AlbumIds.Length > 0)
|
||||||
|
{
|
||||||
|
var clauses = new List<string>();
|
||||||
|
var index = 0;
|
||||||
|
foreach (var albumId in query.AlbumIds)
|
||||||
|
{
|
||||||
|
var paramName = "@AlbumIds" + index;
|
||||||
|
|
||||||
|
clauses.Add("Album in (select Name from typedbaseitems where guid=" + paramName + ")");
|
||||||
|
if (statement != null)
|
||||||
|
{
|
||||||
|
statement.TryBind(paramName, albumId.ToGuidParamValue());
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
var clause = "(" + string.Join(" OR ", clauses.ToArray()) + ")";
|
||||||
|
whereClauses.Add(clause);
|
||||||
|
}
|
||||||
|
|
||||||
if (query.ExcludeArtistIds.Length > 0)
|
if (query.ExcludeArtistIds.Length > 0)
|
||||||
{
|
{
|
||||||
var clauses = new List<string>();
|
var clauses = new List<string>();
|
||||||
|
@ -4227,30 +4247,6 @@ namespace Emby.Server.Implementations.Data
|
||||||
{
|
{
|
||||||
whereClauses.Add("ProviderIds like '%tvdb=%'");
|
whereClauses.Add("ProviderIds like '%tvdb=%'");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.AlbumNames.Length > 0)
|
|
||||||
{
|
|
||||||
var clause = "(";
|
|
||||||
|
|
||||||
var index = 0;
|
|
||||||
foreach (var name in query.AlbumNames)
|
|
||||||
{
|
|
||||||
if (index > 0)
|
|
||||||
{
|
|
||||||
clause += " OR ";
|
|
||||||
}
|
|
||||||
clause += "Album=@AlbumName" + index;
|
|
||||||
|
|
||||||
if (statement != null)
|
|
||||||
{
|
|
||||||
statement.TryBind("@AlbumName" + index, name);
|
|
||||||
}
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
|
|
||||||
clause += ")";
|
|
||||||
whereClauses.Add(clause);
|
|
||||||
}
|
|
||||||
if (query.HasThemeSong.HasValue)
|
if (query.HasThemeSong.HasValue)
|
||||||
{
|
{
|
||||||
if (query.HasThemeSong.Value)
|
if (query.HasThemeSong.Value)
|
||||||
|
|
|
@ -360,7 +360,6 @@ namespace Emby.Server.Implementations.Dto
|
||||||
var collectionFolder = item as ICollectionFolder;
|
var collectionFolder = item as ICollectionFolder;
|
||||||
if (collectionFolder != null)
|
if (collectionFolder != null)
|
||||||
{
|
{
|
||||||
dto.OriginalCollectionType = collectionFolder.CollectionType;
|
|
||||||
dto.CollectionType = collectionFolder.CollectionType;
|
dto.CollectionType = collectionFolder.CollectionType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -492,7 +491,10 @@ namespace Emby.Server.Implementations.Dto
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//if (!(item is LiveTvProgram))
|
||||||
|
{
|
||||||
dto.PlayAccess = item.GetPlayAccess(user);
|
dto.PlayAccess = item.GetPlayAccess(user);
|
||||||
|
}
|
||||||
|
|
||||||
if (fields.Contains(ItemFields.BasicSyncInfo) || fields.Contains(ItemFields.SyncInfo))
|
if (fields.Contains(ItemFields.BasicSyncInfo) || fields.Contains(ItemFields.SyncInfo))
|
||||||
{
|
{
|
||||||
|
@ -502,33 +504,6 @@ namespace Emby.Server.Implementations.Dto
|
||||||
dto.SupportsSync = true;
|
dto.SupportsSync = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fields.Contains(ItemFields.SeasonUserData))
|
|
||||||
{
|
|
||||||
var episode = item as Episode;
|
|
||||||
|
|
||||||
if (episode != null)
|
|
||||||
{
|
|
||||||
var season = episode.Season;
|
|
||||||
|
|
||||||
if (season != null)
|
|
||||||
{
|
|
||||||
dto.SeasonUserData = await _userDataRepository.GetUserDataDto(season, user).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var userView = item as UserView;
|
|
||||||
if (userView != null)
|
|
||||||
{
|
|
||||||
dto.HasDynamicCategories = userView.ContainsDynamicCategories(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
var collectionFolder = item as ICollectionFolder;
|
|
||||||
if (collectionFolder != null)
|
|
||||||
{
|
|
||||||
dto.HasDynamicCategories = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private int GetChildCount(Folder folder, User user)
|
private int GetChildCount(Folder folder, User user)
|
||||||
|
@ -879,20 +854,6 @@ namespace Emby.Server.Implementations.Dto
|
||||||
}
|
}
|
||||||
dto.Container = item.Container;
|
dto.Container = item.Container;
|
||||||
|
|
||||||
var hasBudget = item as IHasBudget;
|
|
||||||
if (hasBudget != null)
|
|
||||||
{
|
|
||||||
if (fields.Contains(ItemFields.Budget))
|
|
||||||
{
|
|
||||||
dto.Budget = hasBudget.Budget;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fields.Contains(ItemFields.Revenue))
|
|
||||||
{
|
|
||||||
dto.Revenue = hasBudget.Revenue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dto.EndDate = item.EndDate;
|
dto.EndDate = item.EndDate;
|
||||||
|
|
||||||
if (fields.Contains(ItemFields.HomePageUrl))
|
if (fields.Contains(ItemFields.HomePageUrl))
|
||||||
|
@ -994,7 +955,12 @@ namespace Emby.Server.Implementations.Dto
|
||||||
}
|
}
|
||||||
|
|
||||||
dto.MediaType = item.MediaType;
|
dto.MediaType = item.MediaType;
|
||||||
|
|
||||||
|
if (!(item is LiveTvProgram))
|
||||||
|
{
|
||||||
dto.LocationType = item.LocationType;
|
dto.LocationType = item.LocationType;
|
||||||
|
}
|
||||||
|
|
||||||
if (item.IsHD.HasValue && item.IsHD.Value)
|
if (item.IsHD.HasValue && item.IsHD.Value)
|
||||||
{
|
{
|
||||||
dto.IsHD = item.IsHD;
|
dto.IsHD = item.IsHD;
|
||||||
|
@ -1065,7 +1031,7 @@ namespace Emby.Server.Implementations.Dto
|
||||||
|
|
||||||
if (fields.Contains(ItemFields.Path))
|
if (fields.Contains(ItemFields.Path))
|
||||||
{
|
{
|
||||||
dto.Path = GetMappedPath(item);
|
dto.Path = GetMappedPath(item, owner);
|
||||||
}
|
}
|
||||||
|
|
||||||
dto.PremiereDate = item.PremiereDate;
|
dto.PremiereDate = item.PremiereDate;
|
||||||
|
@ -1102,7 +1068,10 @@ namespace Emby.Server.Implementations.Dto
|
||||||
}
|
}
|
||||||
|
|
||||||
dto.Type = item.GetClientTypeName();
|
dto.Type = item.GetClientTypeName();
|
||||||
|
if ((item.CommunityRating ?? 0) > 0)
|
||||||
|
{
|
||||||
dto.CommunityRating = item.CommunityRating;
|
dto.CommunityRating = item.CommunityRating;
|
||||||
|
}
|
||||||
|
|
||||||
if (fields.Contains(ItemFields.VoteCount))
|
if (fields.Contains(ItemFields.VoteCount))
|
||||||
{
|
{
|
||||||
|
@ -1409,9 +1378,7 @@ namespace Emby.Server.Implementations.Dto
|
||||||
{
|
{
|
||||||
dto.AirDays = series.AirDays;
|
dto.AirDays = series.AirDays;
|
||||||
dto.AirTime = series.AirTime;
|
dto.AirTime = series.AirTime;
|
||||||
dto.SeriesStatus = series.Status;
|
dto.Status = series.Status.HasValue ? series.Status.Value.ToString() : null;
|
||||||
|
|
||||||
dto.AnimeSeriesIndex = series.AnimeSeriesIndex;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add SeasonInfo
|
// Add SeasonInfo
|
||||||
|
@ -1473,10 +1440,13 @@ namespace Emby.Server.Implementations.Dto
|
||||||
SetBookProperties(dto, book);
|
SetBookProperties(dto, book);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fields.Contains(ItemFields.ProductionLocations))
|
||||||
|
{
|
||||||
if (item.ProductionLocations.Count > 0 || item is Movie)
|
if (item.ProductionLocations.Count > 0 || item is Movie)
|
||||||
{
|
{
|
||||||
dto.ProductionLocations = item.ProductionLocations.ToArray();
|
dto.ProductionLocations = item.ProductionLocations.ToArray();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var photo = item as Photo;
|
var photo = item as Photo;
|
||||||
if (photo != null)
|
if (photo != null)
|
||||||
|
@ -1596,7 +1566,7 @@ namespace Emby.Server.Implementations.Dto
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetMappedPath(BaseItem item)
|
private string GetMappedPath(BaseItem item, BaseItem ownerItem)
|
||||||
{
|
{
|
||||||
var path = item.Path;
|
var path = item.Path;
|
||||||
|
|
||||||
|
@ -1604,7 +1574,7 @@ namespace Emby.Server.Implementations.Dto
|
||||||
|
|
||||||
if (locationType == LocationType.FileSystem || locationType == LocationType.Offline)
|
if (locationType == LocationType.FileSystem || locationType == LocationType.Offline)
|
||||||
{
|
{
|
||||||
path = _libraryManager.GetPathAfterNetworkSubstitution(path, item);
|
path = _libraryManager.GetPathAfterNetworkSubstitution(path, ownerItem ?? item);
|
||||||
}
|
}
|
||||||
|
|
||||||
return path;
|
return path;
|
||||||
|
@ -1630,8 +1600,20 @@ namespace Emby.Server.Implementations.Dto
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var supportedEnhancers = _imageProcessor.GetSupportedEnhancers(item, ImageType.Primary).ToList();
|
||||||
|
|
||||||
ImageSize size;
|
ImageSize size;
|
||||||
|
|
||||||
|
if (supportedEnhancers.Count == 0)
|
||||||
|
{
|
||||||
|
var defaultAspectRatio = item.GetDefaultPrimaryImageAspectRatio();
|
||||||
|
|
||||||
|
if (defaultAspectRatio.HasValue)
|
||||||
|
{
|
||||||
|
return defaultAspectRatio.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
size = _imageProcessor.GetImageSize(imageInfo);
|
size = _imageProcessor.GetImageSize(imageInfo);
|
||||||
|
@ -1642,8 +1624,6 @@ namespace Emby.Server.Implementations.Dto
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var supportedEnhancers = _imageProcessor.GetSupportedEnhancers(item, ImageType.Primary).ToList();
|
|
||||||
|
|
||||||
foreach (var enhancer in supportedEnhancers)
|
foreach (var enhancer in supportedEnhancers)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
|
@ -35,6 +35,9 @@
|
||||||
<Compile Include="Activity\ActivityLogEntryPoint.cs" />
|
<Compile Include="Activity\ActivityLogEntryPoint.cs" />
|
||||||
<Compile Include="Activity\ActivityManager.cs" />
|
<Compile Include="Activity\ActivityManager.cs" />
|
||||||
<Compile Include="Activity\ActivityRepository.cs" />
|
<Compile Include="Activity\ActivityRepository.cs" />
|
||||||
|
<Compile Include="AppBase\BaseApplicationPaths.cs" />
|
||||||
|
<Compile Include="AppBase\BaseConfigurationManager.cs" />
|
||||||
|
<Compile Include="AppBase\ConfigurationHelper.cs" />
|
||||||
<Compile Include="Branding\BrandingConfigurationFactory.cs" />
|
<Compile Include="Branding\BrandingConfigurationFactory.cs" />
|
||||||
<Compile Include="Browser\BrowserLauncher.cs" />
|
<Compile Include="Browser\BrowserLauncher.cs" />
|
||||||
<Compile Include="Channels\ChannelConfigurations.cs" />
|
<Compile Include="Channels\ChannelConfigurations.cs" />
|
||||||
|
@ -46,11 +49,7 @@
|
||||||
<Compile Include="Collections\CollectionImageProvider.cs" />
|
<Compile Include="Collections\CollectionImageProvider.cs" />
|
||||||
<Compile Include="Collections\CollectionManager.cs" />
|
<Compile Include="Collections\CollectionManager.cs" />
|
||||||
<Compile Include="Collections\CollectionsDynamicFolder.cs" />
|
<Compile Include="Collections\CollectionsDynamicFolder.cs" />
|
||||||
<Compile Include="Connect\ConnectData.cs" />
|
<Compile Include="Configuration\ServerConfigurationManager.cs" />
|
||||||
<Compile Include="Connect\ConnectEntryPoint.cs" />
|
|
||||||
<Compile Include="Connect\ConnectManager.cs" />
|
|
||||||
<Compile Include="Connect\Responses.cs" />
|
|
||||||
<Compile Include="Connect\Validator.cs" />
|
|
||||||
<Compile Include="Data\ManagedConnection.cs" />
|
<Compile Include="Data\ManagedConnection.cs" />
|
||||||
<Compile Include="Data\SqliteDisplayPreferencesRepository.cs" />
|
<Compile Include="Data\SqliteDisplayPreferencesRepository.cs" />
|
||||||
<Compile Include="Data\SqliteFileOrganizationRepository.cs" />
|
<Compile Include="Data\SqliteFileOrganizationRepository.cs" />
|
||||||
|
@ -179,6 +178,7 @@
|
||||||
<Compile Include="LiveTv\TunerHosts\MulticastStream.cs" />
|
<Compile Include="LiveTv\TunerHosts\MulticastStream.cs" />
|
||||||
<Compile Include="LiveTv\TunerHosts\QueueStream.cs" />
|
<Compile Include="LiveTv\TunerHosts\QueueStream.cs" />
|
||||||
<Compile Include="Localization\LocalizationManager.cs" />
|
<Compile Include="Localization\LocalizationManager.cs" />
|
||||||
|
<Compile Include="Logging\UnhandledExceptionWriter.cs" />
|
||||||
<Compile Include="MediaEncoder\EncodingManager.cs" />
|
<Compile Include="MediaEncoder\EncodingManager.cs" />
|
||||||
<Compile Include="Migrations\IVersionMigration.cs" />
|
<Compile Include="Migrations\IVersionMigration.cs" />
|
||||||
<Compile Include="Migrations\LibraryScanMigration.cs" />
|
<Compile Include="Migrations\LibraryScanMigration.cs" />
|
||||||
|
@ -213,8 +213,19 @@
|
||||||
<Compile Include="Security\MBLicenseFile.cs" />
|
<Compile Include="Security\MBLicenseFile.cs" />
|
||||||
<Compile Include="Security\PluginSecurityManager.cs" />
|
<Compile Include="Security\PluginSecurityManager.cs" />
|
||||||
<Compile Include="Security\RegRecord.cs" />
|
<Compile Include="Security\RegRecord.cs" />
|
||||||
|
<Compile Include="ServerApplicationPaths.cs" />
|
||||||
<Compile Include="ServerManager\ServerManager.cs" />
|
<Compile Include="ServerManager\ServerManager.cs" />
|
||||||
<Compile Include="ServerManager\WebSocketConnection.cs" />
|
<Compile Include="ServerManager\WebSocketConnection.cs" />
|
||||||
|
<Compile Include="Services\ServicePath.cs" />
|
||||||
|
<Compile Include="Services\ServiceMethod.cs" />
|
||||||
|
<Compile Include="Services\ResponseHelper.cs" />
|
||||||
|
<Compile Include="Services\HttpResult.cs" />
|
||||||
|
<Compile Include="Services\RequestHelper.cs" />
|
||||||
|
<Compile Include="Services\ServiceHandler.cs" />
|
||||||
|
<Compile Include="Services\ServiceController.cs" />
|
||||||
|
<Compile Include="Services\ServiceExec.cs" />
|
||||||
|
<Compile Include="Services\StringMapTypeDeserializer.cs" />
|
||||||
|
<Compile Include="Services\UrlExtensions.cs" />
|
||||||
<Compile Include="Session\HttpSessionController.cs" />
|
<Compile Include="Session\HttpSessionController.cs" />
|
||||||
<Compile Include="Session\SessionManager.cs" />
|
<Compile Include="Session\SessionManager.cs" />
|
||||||
<Compile Include="Session\SessionWebSocketListener.cs" />
|
<Compile Include="Session\SessionWebSocketListener.cs" />
|
||||||
|
@ -253,23 +264,6 @@
|
||||||
<Compile Include="Sorting\StartDateComparer.cs" />
|
<Compile Include="Sorting\StartDateComparer.cs" />
|
||||||
<Compile Include="Sorting\StudioComparer.cs" />
|
<Compile Include="Sorting\StudioComparer.cs" />
|
||||||
<Compile Include="StartupOptions.cs" />
|
<Compile Include="StartupOptions.cs" />
|
||||||
<Compile Include="Sync\AppSyncProvider.cs" />
|
|
||||||
<Compile Include="Sync\CloudSyncProfile.cs" />
|
|
||||||
<Compile Include="Sync\IHasSyncQuality.cs" />
|
|
||||||
<Compile Include="Sync\MediaSync.cs" />
|
|
||||||
<Compile Include="Sync\MultiProviderSync.cs" />
|
|
||||||
<Compile Include="Sync\ServerSyncScheduledTask.cs" />
|
|
||||||
<Compile Include="Sync\SyncConfig.cs" />
|
|
||||||
<Compile Include="Sync\SyncConvertScheduledTask.cs" />
|
|
||||||
<Compile Include="Sync\SyncedMediaSourceProvider.cs" />
|
|
||||||
<Compile Include="Sync\SyncHelper.cs" />
|
|
||||||
<Compile Include="Sync\SyncJobOptions.cs" />
|
|
||||||
<Compile Include="Sync\SyncJobProcessor.cs" />
|
|
||||||
<Compile Include="Sync\SyncManager.cs" />
|
|
||||||
<Compile Include="Sync\SyncNotificationEntryPoint.cs" />
|
|
||||||
<Compile Include="Sync\SyncRegistrationInfo.cs" />
|
|
||||||
<Compile Include="Sync\SyncRepository.cs" />
|
|
||||||
<Compile Include="Sync\TargetDataProvider.cs" />
|
|
||||||
<Compile Include="TV\SeriesPostScanTask.cs" />
|
<Compile Include="TV\SeriesPostScanTask.cs" />
|
||||||
<Compile Include="TV\TVSeriesManager.cs" />
|
<Compile Include="TV\TVSeriesManager.cs" />
|
||||||
<Compile Include="Udp\UdpServer.cs" />
|
<Compile Include="Udp\UdpServer.cs" />
|
||||||
|
@ -301,16 +295,12 @@
|
||||||
<Project>{2e781478-814d-4a48-9d80-bff206441a65}</Project>
|
<Project>{2e781478-814d-4a48-9d80-bff206441a65}</Project>
|
||||||
<Name>MediaBrowser.Server.Implementations</Name>
|
<Name>MediaBrowser.Server.Implementations</Name>
|
||||||
</ProjectReference>
|
</ProjectReference>
|
||||||
<ProjectReference Include="..\ServiceStack\ServiceStack.csproj">
|
|
||||||
<Project>{680a1709-25eb-4d52-a87f-ee03ffd94baa}</Project>
|
|
||||||
<Name>ServiceStack</Name>
|
|
||||||
</ProjectReference>
|
|
||||||
<ProjectReference Include="..\SocketHttpListener.Portable\SocketHttpListener.Portable.csproj">
|
<ProjectReference Include="..\SocketHttpListener.Portable\SocketHttpListener.Portable.csproj">
|
||||||
<Project>{4f26d5d8-a7b0-42b3-ba42-7cb7d245934e}</Project>
|
<Project>{4f26d5d8-a7b0-42b3-ba42-7cb7d245934e}</Project>
|
||||||
<Name>SocketHttpListener.Portable</Name>
|
<Name>SocketHttpListener.Portable</Name>
|
||||||
</ProjectReference>
|
</ProjectReference>
|
||||||
<Reference Include="Emby.XmlTv, Version=1.0.6249.32870, Culture=neutral, processorArchitecture=MSIL">
|
<Reference Include="Emby.XmlTv, Version=1.0.6251.29080, Culture=neutral, processorArchitecture=MSIL">
|
||||||
<HintPath>..\packages\Emby.XmlTv.1.0.6\lib\portable-net45+win8\Emby.XmlTv.dll</HintPath>
|
<HintPath>..\packages\Emby.XmlTv.1.0.7\lib\portable-net45+win8\Emby.XmlTv.dll</HintPath>
|
||||||
<Private>True</Private>
|
<Private>True</Private>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="MediaBrowser.Naming, Version=1.0.6201.24431, Culture=neutral, processorArchitecture=MSIL">
|
<Reference Include="MediaBrowser.Naming, Version=1.0.6201.24431, Culture=neutral, processorArchitecture=MSIL">
|
||||||
|
@ -322,7 +312,7 @@
|
||||||
<Private>True</Private>
|
<Private>True</Private>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="SQLitePCLRaw.core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=1488e028ca7ab535, processorArchitecture=MSIL">
|
<Reference Include="SQLitePCLRaw.core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=1488e028ca7ab535, processorArchitecture=MSIL">
|
||||||
<HintPath>..\packages\SQLitePCLRaw.core.1.1.1\lib\portable-net45+netcore45+wpa81+MonoAndroid10+MonoTouch10+Xamarin.iOS10\SQLitePCLRaw.core.dll</HintPath>
|
<HintPath>..\packages\SQLitePCLRaw.core.1.1.2\lib\portable-net45+netcore45+wpa81+MonoAndroid10+MonoTouch10+Xamarin.iOS10\SQLitePCLRaw.core.dll</HintPath>
|
||||||
<Private>True</Private>
|
<Private>True</Private>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="UniversalDetector, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
<Reference Include="UniversalDetector, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||||
|
|
|
@ -40,8 +40,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||||
{ "serverid", _applicationHost.SystemId },
|
{ "serverid", _applicationHost.SystemId },
|
||||||
{ "deviceid", _applicationHost.SystemId },
|
{ "deviceid", _applicationHost.SystemId },
|
||||||
{ "ver", _applicationHost.ApplicationVersion.ToString() },
|
{ "ver", _applicationHost.ApplicationVersion.ToString() },
|
||||||
{ "platform", _applicationHost.OperatingSystemDisplayName },
|
{ "platform", _applicationHost.OperatingSystemDisplayName }
|
||||||
{ "isservice", _applicationHost.IsRunningAsService.ToString().ToLower()}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var users = _userManager.Users.ToList();
|
var users = _userManager.Users.ToList();
|
||||||
|
|
|
@ -2,8 +2,6 @@
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using MediaBrowser.Model.Logging;
|
using MediaBrowser.Model.Logging;
|
||||||
using ServiceStack;
|
|
||||||
using ServiceStack.Host;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
@ -13,6 +11,7 @@ using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Emby.Server.Implementations.HttpServer;
|
using Emby.Server.Implementations.HttpServer;
|
||||||
using Emby.Server.Implementations.HttpServer.SocketSharp;
|
using Emby.Server.Implementations.HttpServer.SocketSharp;
|
||||||
|
using Emby.Server.Implementations.Services;
|
||||||
using MediaBrowser.Common.Net;
|
using MediaBrowser.Common.Net;
|
||||||
using MediaBrowser.Common.Security;
|
using MediaBrowser.Common.Security;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
|
@ -29,7 +28,7 @@ using SocketHttpListener.Primitives;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.HttpServer
|
namespace Emby.Server.Implementations.HttpServer
|
||||||
{
|
{
|
||||||
public class HttpListenerHost : ServiceStackHost, IHttpServer
|
public class HttpListenerHost : IHttpServer, IDisposable
|
||||||
{
|
{
|
||||||
private string DefaultRedirectPath { get; set; }
|
private string DefaultRedirectPath { get; set; }
|
||||||
|
|
||||||
|
@ -61,13 +60,21 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
private readonly Func<Type, Func<string, object>> _funcParseFn;
|
private readonly Func<Type, Func<string, object>> _funcParseFn;
|
||||||
private readonly bool _enableDualModeSockets;
|
private readonly bool _enableDualModeSockets;
|
||||||
|
|
||||||
|
public List<Action<IRequest, IResponse, object>> RequestFilters { get; set; }
|
||||||
|
public List<Action<IRequest, IResponse, object>> ResponseFilters { get; set; }
|
||||||
|
|
||||||
|
private readonly Dictionary<Type, Type> ServiceOperationsMap = new Dictionary<Type, Type>();
|
||||||
|
public static HttpListenerHost Instance { get; protected set; }
|
||||||
|
|
||||||
public HttpListenerHost(IServerApplicationHost applicationHost,
|
public HttpListenerHost(IServerApplicationHost applicationHost,
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
IServerConfigurationManager config,
|
IServerConfigurationManager config,
|
||||||
string serviceName,
|
string serviceName,
|
||||||
string defaultRedirectPath, INetworkManager networkManager, IMemoryStreamFactory memoryStreamProvider, ITextEncoding textEncoding, ISocketFactory socketFactory, ICryptoProvider cryptoProvider, IJsonSerializer jsonSerializer, IXmlSerializer xmlSerializer, IEnvironmentInfo environment, ICertificate certificate, IStreamFactory streamFactory, Func<Type, Func<string, object>> funcParseFn, bool enableDualModeSockets)
|
string defaultRedirectPath, INetworkManager networkManager, IMemoryStreamFactory memoryStreamProvider, ITextEncoding textEncoding, ISocketFactory socketFactory, ICryptoProvider cryptoProvider, IJsonSerializer jsonSerializer, IXmlSerializer xmlSerializer, IEnvironmentInfo environment, ICertificate certificate, IStreamFactory streamFactory, Func<Type, Func<string, object>> funcParseFn, bool enableDualModeSockets)
|
||||||
: base(serviceName)
|
: base()
|
||||||
{
|
{
|
||||||
|
Instance = this;
|
||||||
|
|
||||||
_appHost = applicationHost;
|
_appHost = applicationHost;
|
||||||
DefaultRedirectPath = defaultRedirectPath;
|
DefaultRedirectPath = defaultRedirectPath;
|
||||||
_networkManager = networkManager;
|
_networkManager = networkManager;
|
||||||
|
@ -85,6 +92,9 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
_config = config;
|
_config = config;
|
||||||
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
|
RequestFilters = new List<Action<IRequest, IResponse, object>>();
|
||||||
|
ResponseFilters = new List<Action<IRequest, IResponse, object>>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GlobalResponse { get; set; }
|
public string GlobalResponse { get; set; }
|
||||||
|
@ -99,18 +109,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
{typeof (ArgumentException), 400}
|
{typeof (ArgumentException), 400}
|
||||||
};
|
};
|
||||||
|
|
||||||
public override void Configure()
|
protected ILogger Logger
|
||||||
{
|
|
||||||
var requestFilters = _appHost.GetExports<IRequestFilter>().ToList();
|
|
||||||
foreach (var filter in requestFilters)
|
|
||||||
{
|
|
||||||
GlobalRequestFilters.Add(filter.Filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
GlobalResponseFilters.Add(new ResponseFilter(_logger).FilterResponse);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override ILogger Logger
|
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
|
@ -118,32 +117,73 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override T Resolve<T>()
|
public object CreateInstance(Type type)
|
||||||
{
|
|
||||||
return _appHost.Resolve<T>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override T TryResolve<T>()
|
|
||||||
{
|
|
||||||
return _appHost.TryResolve<T>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override object CreateInstance(Type type)
|
|
||||||
{
|
{
|
||||||
return _appHost.CreateInstance(type);
|
return _appHost.CreateInstance(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override ServiceController CreateServiceController()
|
private ServiceController CreateServiceController()
|
||||||
{
|
{
|
||||||
var types = _restServices.Select(r => r.GetType()).ToArray();
|
var types = _restServices.Select(r => r.GetType()).ToArray();
|
||||||
|
|
||||||
return new ServiceController(() => types);
|
return new ServiceController(() => types);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override ServiceStackHost Start(string listeningAtUrlBase)
|
/// <summary>
|
||||||
|
/// Applies the request filters. Returns whether or not the request has been handled
|
||||||
|
/// and no more processing should be done.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public void ApplyRequestFilters(IRequest req, IResponse res, object requestDto)
|
||||||
{
|
{
|
||||||
StartListener();
|
//Exec all RequestFilter attributes with Priority < 0
|
||||||
return this;
|
var attributes = GetRequestFilterAttributes(requestDto.GetType());
|
||||||
|
var i = 0;
|
||||||
|
for (; i < attributes.Length && attributes[i].Priority < 0; i++)
|
||||||
|
{
|
||||||
|
var attribute = attributes[i];
|
||||||
|
attribute.RequestFilter(req, res, requestDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Exec global filters
|
||||||
|
foreach (var requestFilter in RequestFilters)
|
||||||
|
{
|
||||||
|
requestFilter(req, res, requestDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Exec remaining RequestFilter attributes with Priority >= 0
|
||||||
|
for (; i < attributes.Length && attributes[i].Priority >= 0; i++)
|
||||||
|
{
|
||||||
|
var attribute = attributes[i];
|
||||||
|
attribute.RequestFilter(req, res, requestDto);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type GetServiceTypeByRequest(Type requestType)
|
||||||
|
{
|
||||||
|
Type serviceType;
|
||||||
|
ServiceOperationsMap.TryGetValue(requestType, out serviceType);
|
||||||
|
return serviceType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddServiceInfo(Type serviceType, Type requestType, Type responseType)
|
||||||
|
{
|
||||||
|
ServiceOperationsMap[requestType] = serviceType;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IHasRequestFilter[] GetRequestFilterAttributes(Type requestDtoType)
|
||||||
|
{
|
||||||
|
var attributes = requestDtoType.GetTypeInfo().GetCustomAttributes(true).OfType<IHasRequestFilter>().ToList();
|
||||||
|
|
||||||
|
var serviceType = GetServiceTypeByRequest(requestDtoType);
|
||||||
|
if (serviceType != null)
|
||||||
|
{
|
||||||
|
attributes.AddRange(serviceType.GetTypeInfo().GetCustomAttributes(true).OfType<IHasRequestFilter>());
|
||||||
|
}
|
||||||
|
|
||||||
|
attributes.Sort((x, y) => x.Priority - y.Priority);
|
||||||
|
|
||||||
|
return attributes.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -531,11 +571,11 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var handler = HttpHandlerFactory.GetHandler(httpReq, _logger);
|
var handler = GetServiceHandler(httpReq);
|
||||||
|
|
||||||
if (handler != null)
|
if (handler != null)
|
||||||
{
|
{
|
||||||
await handler.ProcessRequestAsync(httpReq, httpRes, operationName).ConfigureAwait(false);
|
await handler.ProcessRequestAsync(this, httpReq, httpRes, Logger, operationName).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -565,6 +605,35 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Entry point for HttpListener
|
||||||
|
public ServiceHandler GetServiceHandler(IHttpRequest httpReq)
|
||||||
|
{
|
||||||
|
var pathInfo = httpReq.PathInfo;
|
||||||
|
|
||||||
|
var pathParts = pathInfo.TrimStart('/').Split('/');
|
||||||
|
if (pathParts.Length == 0)
|
||||||
|
{
|
||||||
|
_logger.Error("Path parts empty for PathInfo: {0}, Url: {1}", pathInfo, httpReq.RawUrl);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
string contentType;
|
||||||
|
var restPath = ServiceHandler.FindMatchingRestPath(httpReq.HttpMethod, pathInfo, _logger, out contentType);
|
||||||
|
|
||||||
|
if (restPath != null)
|
||||||
|
{
|
||||||
|
return new ServiceHandler
|
||||||
|
{
|
||||||
|
RestPath = restPath,
|
||||||
|
ResponseContentType = contentType
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.Error("Could not find handler for {0}", pathInfo);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private void Write(IResponse response, string text)
|
private void Write(IResponse response, string text)
|
||||||
{
|
{
|
||||||
var bOutput = Encoding.UTF8.GetBytes(text);
|
var bOutput = Encoding.UTF8.GetBytes(text);
|
||||||
|
@ -580,6 +649,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
httpRes.AddHeader("Location", url);
|
httpRes.AddHeader("Location", url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ServiceController ServiceController { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds the rest handlers.
|
/// Adds the rest handlers.
|
||||||
|
@ -593,12 +663,20 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
|
|
||||||
_logger.Info("Calling ServiceStack AppHost.Init");
|
_logger.Info("Calling ServiceStack AppHost.Init");
|
||||||
|
|
||||||
base.Init();
|
ServiceController.Init(this);
|
||||||
|
|
||||||
|
var requestFilters = _appHost.GetExports<IRequestFilter>().ToList();
|
||||||
|
foreach (var filter in requestFilters)
|
||||||
|
{
|
||||||
|
RequestFilters.Add(filter.Filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override RouteAttribute[] GetRouteAttributes(Type requestType)
|
ResponseFilters.Add(new ResponseFilter(_logger).FilterResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RouteAttribute[] GetRouteAttributes(Type requestType)
|
||||||
{
|
{
|
||||||
var routes = base.GetRouteAttributes(requestType).ToList();
|
var routes = requestType.GetTypeInfo().GetCustomAttributes<RouteAttribute>(true).ToList();
|
||||||
var clone = routes.ToList();
|
var clone = routes.ToList();
|
||||||
|
|
||||||
foreach (var route in clone)
|
foreach (var route in clone)
|
||||||
|
@ -628,54 +706,27 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
return routes.ToArray();
|
return routes.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override object GetTaskResult(Task task, string requestName)
|
public Func<string, object> GetParseFn(Type propertyType)
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var taskObject = task as Task<object>;
|
|
||||||
if (taskObject != null)
|
|
||||||
{
|
|
||||||
return taskObject.Result;
|
|
||||||
}
|
|
||||||
|
|
||||||
task.Wait();
|
|
||||||
|
|
||||||
var type = task.GetType().GetTypeInfo();
|
|
||||||
if (!type.IsGenericType)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.Warn("Getting task result from " + requestName + " using reflection. For better performance have your api return Task<object>");
|
|
||||||
return type.GetDeclaredProperty("Result").GetValue(task);
|
|
||||||
}
|
|
||||||
catch (TypeAccessException)
|
|
||||||
{
|
|
||||||
return null; //return null for void Task's
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Func<string, object> GetParseFn(Type propertyType)
|
|
||||||
{
|
{
|
||||||
return _funcParseFn(propertyType);
|
return _funcParseFn(propertyType);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void SerializeToJson(object o, Stream stream)
|
public void SerializeToJson(object o, Stream stream)
|
||||||
{
|
{
|
||||||
_jsonSerializer.SerializeToStream(o, stream);
|
_jsonSerializer.SerializeToStream(o, stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void SerializeToXml(object o, Stream stream)
|
public void SerializeToXml(object o, Stream stream)
|
||||||
{
|
{
|
||||||
_xmlSerializer.SerializeToStream(o, stream);
|
_xmlSerializer.SerializeToStream(o, stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override object DeserializeXml(Type type, Stream stream)
|
public object DeserializeXml(Type type, Stream stream)
|
||||||
{
|
{
|
||||||
return _xmlSerializer.DeserializeFromStream(type, stream);
|
return _xmlSerializer.DeserializeFromStream(type, stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override object DeserializeJson(Type type, Stream stream)
|
public object DeserializeJson(Type type, Stream stream)
|
||||||
{
|
{
|
||||||
return _jsonSerializer.DeserializeFromStream(stream, type);
|
return _jsonSerializer.DeserializeFromStream(stream, type);
|
||||||
}
|
}
|
||||||
|
@ -716,8 +767,6 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
{
|
{
|
||||||
if (_disposed) return;
|
if (_disposed) return;
|
||||||
|
|
||||||
base.Dispose();
|
|
||||||
|
|
||||||
lock (_disposeLock)
|
lock (_disposeLock)
|
||||||
{
|
{
|
||||||
if (_disposed) return;
|
if (_disposed) return;
|
||||||
|
@ -731,7 +780,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Dispose(true);
|
Dispose(true);
|
||||||
GC.SuppressFinalize(this);
|
GC.SuppressFinalize(this);
|
||||||
|
@ -740,7 +789,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
public void StartServer(IEnumerable<string> urlPrefixes)
|
public void StartServer(IEnumerable<string> urlPrefixes)
|
||||||
{
|
{
|
||||||
UrlPrefixes = urlPrefixes.ToList();
|
UrlPrefixes = urlPrefixes.ToList();
|
||||||
Start(UrlPrefixes.First());
|
StartListener();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -13,10 +13,9 @@ using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
using Emby.Server.Implementations.HttpServer;
|
using Emby.Server.Implementations.HttpServer;
|
||||||
|
using Emby.Server.Implementations.Services;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Model.Services;
|
using MediaBrowser.Model.Services;
|
||||||
using ServiceStack;
|
|
||||||
using ServiceStack.Host;
|
|
||||||
using IRequest = MediaBrowser.Model.Services.IRequest;
|
using IRequest = MediaBrowser.Model.Services.IRequest;
|
||||||
using MimeTypes = MediaBrowser.Model.Net.MimeTypes;
|
using MimeTypes = MediaBrowser.Model.Net.MimeTypes;
|
||||||
using StreamWriter = Emby.Server.Implementations.HttpServer.StreamWriter;
|
using StreamWriter = Emby.Server.Implementations.HttpServer.StreamWriter;
|
||||||
|
@ -203,7 +202,11 @@ namespace Emby.Server.Implementations.HttpServer
|
||||||
// Do not use the memoryStreamFactory here, they don't place nice with compression
|
// Do not use the memoryStreamFactory here, they don't place nice with compression
|
||||||
using (var ms = new MemoryStream())
|
using (var ms = new MemoryStream())
|
||||||
{
|
{
|
||||||
ContentTypes.Instance.SerializeToStream(request, dto, ms);
|
var contentType = request.ResponseContentType;
|
||||||
|
var writerFn = RequestHelper.GetResponseWriter(HttpListenerHost.Instance, contentType);
|
||||||
|
|
||||||
|
writerFn(dto, ms);
|
||||||
|
|
||||||
ms.Position = 0;
|
ms.Position = 0;
|
||||||
|
|
||||||
var responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
var responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
|
@ -409,17 +409,17 @@ namespace Emby.Server.Implementations.Library
|
||||||
|
|
||||||
if (options.DeleteFileLocation && locationType != LocationType.Remote && locationType != LocationType.Virtual)
|
if (options.DeleteFileLocation && locationType != LocationType.Remote && locationType != LocationType.Virtual)
|
||||||
{
|
{
|
||||||
foreach (var path in item.GetDeletePaths().ToList())
|
foreach (var fileSystemInfo in item.GetDeletePaths().ToList())
|
||||||
{
|
{
|
||||||
if (_fileSystem.DirectoryExists(path))
|
if (fileSystemInfo.IsDirectory)
|
||||||
{
|
{
|
||||||
_logger.Debug("Deleting path {0}", path);
|
_logger.Debug("Deleting path {0}", fileSystemInfo.FullName);
|
||||||
_fileSystem.DeleteDirectory(path, true);
|
_fileSystem.DeleteDirectory(fileSystemInfo.FullName, true);
|
||||||
}
|
}
|
||||||
else if (_fileSystem.FileExists(path))
|
else
|
||||||
{
|
{
|
||||||
_logger.Debug("Deleting path {0}", path);
|
_logger.Debug("Deleting path {0}", fileSystemInfo.FullName);
|
||||||
_fileSystem.DeleteFile(path);
|
_fileSystem.DeleteFile(fileSystemInfo.FullName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -818,30 +818,6 @@ namespace Emby.Server.Implementations.Library
|
||||||
return _userRootFolder;
|
return _userRootFolder;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Guid? FindIdByPath(string path, bool? isFolder)
|
|
||||||
{
|
|
||||||
// If this returns multiple items it could be tricky figuring out which one is correct.
|
|
||||||
// In most cases, the newest one will be and the others obsolete but not yet cleaned up
|
|
||||||
|
|
||||||
var query = new InternalItemsQuery
|
|
||||||
{
|
|
||||||
Path = path,
|
|
||||||
IsFolder = isFolder,
|
|
||||||
SortBy = new[] { ItemSortBy.DateCreated },
|
|
||||||
SortOrder = SortOrder.Descending,
|
|
||||||
Limit = 1
|
|
||||||
};
|
|
||||||
|
|
||||||
var id = GetItemIds(query);
|
|
||||||
|
|
||||||
if (id.Count == 0)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return id[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
public BaseItem FindByPath(string path, bool? isFolder)
|
public BaseItem FindByPath(string path, bool? isFolder)
|
||||||
{
|
{
|
||||||
// If this returns multiple items it could be tricky figuring out which one is correct.
|
// If this returns multiple items it could be tricky figuring out which one is correct.
|
||||||
|
|
|
@ -248,6 +248,13 @@ namespace Emby.Server.Implementations.Library
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isPlayed = request.IsPlayed;
|
||||||
|
|
||||||
|
if (parents.OfType<ICollectionFolder>().Any(i => string.Equals(i.CollectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
isPlayed = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (parents.Count == 0)
|
if (parents.Count == 0)
|
||||||
{
|
{
|
||||||
parents = user.RootFolder.GetChildren(user, true)
|
parents = user.RootFolder.GetChildren(user, true)
|
||||||
|
@ -282,7 +289,7 @@ namespace Emby.Server.Implementations.Library
|
||||||
IsVirtualItem = false,
|
IsVirtualItem = false,
|
||||||
Limit = limit * 5,
|
Limit = limit * 5,
|
||||||
SourceTypes = parents.Count == 0 ? new[] { SourceType.Library } : new SourceType[] { },
|
SourceTypes = parents.Count == 0 ? new[] { SourceType.Library } : new SourceType[] { },
|
||||||
IsPlayed = request.IsPlayed
|
IsPlayed = isPlayed
|
||||||
|
|
||||||
}, parents);
|
}, parents);
|
||||||
}
|
}
|
||||||
|
|
|
@ -474,17 +474,30 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||||
|
|
||||||
public ChannelInfo GetEpgChannelFromTunerChannel(List<NameValuePair> mappings, ChannelInfo tunerChannel, List<ChannelInfo> epgChannels)
|
public ChannelInfo GetEpgChannelFromTunerChannel(List<NameValuePair> mappings, ChannelInfo tunerChannel, List<ChannelInfo> epgChannels)
|
||||||
{
|
{
|
||||||
var tunerChannelId = string.IsNullOrWhiteSpace(tunerChannel.TunerChannelId)
|
if (!string.IsNullOrWhiteSpace(tunerChannel.Id))
|
||||||
? tunerChannel.Id
|
|
||||||
: tunerChannel.TunerChannelId;
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(tunerChannelId))
|
|
||||||
{
|
{
|
||||||
var mappedTunerChannelId = GetMappedChannel(tunerChannelId, mappings);
|
var mappedTunerChannelId = GetMappedChannel(tunerChannel.Id, mappings);
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(mappedTunerChannelId))
|
if (string.IsNullOrWhiteSpace(mappedTunerChannelId))
|
||||||
{
|
{
|
||||||
mappedTunerChannelId = tunerChannelId;
|
mappedTunerChannelId = tunerChannel.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
var channel = epgChannels.FirstOrDefault(i => string.Equals(mappedTunerChannelId, i.Id, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (channel != null)
|
||||||
|
{
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(tunerChannel.TunerChannelId))
|
||||||
|
{
|
||||||
|
var mappedTunerChannelId = GetMappedChannel(tunerChannel.TunerChannelId, mappings);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(mappedTunerChannelId))
|
||||||
|
{
|
||||||
|
mappedTunerChannelId = tunerChannel.TunerChannelId;
|
||||||
}
|
}
|
||||||
|
|
||||||
var channel = epgChannels.FirstOrDefault(i => string.Equals(mappedTunerChannelId, i.Id, StringComparison.OrdinalIgnoreCase));
|
var channel = epgChannels.FirstOrDefault(i => string.Equals(mappedTunerChannelId, i.Id, StringComparison.OrdinalIgnoreCase));
|
||||||
|
@ -635,6 +648,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||||
existingTimer.Status == RecordingStatus.Completed)
|
existingTimer.Status == RecordingStatus.Completed)
|
||||||
{
|
{
|
||||||
existingTimer.Status = RecordingStatus.New;
|
existingTimer.Status = RecordingStatus.New;
|
||||||
|
existingTimer.IsManual = true;
|
||||||
_timerProvider.Update(existingTimer);
|
_timerProvider.Update(existingTimer);
|
||||||
return Task.FromResult(existingTimer.Id);
|
return Task.FromResult(existingTimer.Id);
|
||||||
}
|
}
|
||||||
|
@ -663,6 +677,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||||
RecordingHelper.CopyProgramInfoToTimerInfo(programInfo, timer);
|
RecordingHelper.CopyProgramInfoToTimerInfo(programInfo, timer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
timer.IsManual = true;
|
||||||
_timerProvider.Add(timer);
|
_timerProvider.Add(timer);
|
||||||
return Task.FromResult(timer.Id);
|
return Task.FromResult(timer.Id);
|
||||||
}
|
}
|
||||||
|
@ -758,6 +773,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||||
existingTimer.PostPaddingSeconds = updatedTimer.PostPaddingSeconds;
|
existingTimer.PostPaddingSeconds = updatedTimer.PostPaddingSeconds;
|
||||||
existingTimer.IsPostPaddingRequired = updatedTimer.IsPostPaddingRequired;
|
existingTimer.IsPostPaddingRequired = updatedTimer.IsPostPaddingRequired;
|
||||||
existingTimer.IsPrePaddingRequired = updatedTimer.IsPrePaddingRequired;
|
existingTimer.IsPrePaddingRequired = updatedTimer.IsPrePaddingRequired;
|
||||||
|
|
||||||
|
_timerProvider.Update(existingTimer);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.FromResult(true);
|
return Task.FromResult(true);
|
||||||
|
@ -986,6 +1003,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||||
foreach (var program in programs)
|
foreach (var program in programs)
|
||||||
{
|
{
|
||||||
program.ChannelId = channelId;
|
program.ChannelId = channelId;
|
||||||
|
|
||||||
|
if (provider.Item2.EnableNewProgramIds)
|
||||||
|
{
|
||||||
|
program.Id += "_" + channelId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (programs.Count > 0)
|
if (programs.Count > 0)
|
||||||
|
@ -1172,7 +1194,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||||
};
|
};
|
||||||
|
|
||||||
var isAudio = false;
|
var isAudio = false;
|
||||||
await new LiveStreamHelper(_mediaEncoder, _logger).AddMediaInfoWithProbe(stream, isAudio, false, cancellationToken).ConfigureAwait(false);
|
await new LiveStreamHelper(_mediaEncoder, _logger).AddMediaInfoWithProbe(stream, isAudio, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
return new List<MediaSourceInfo>
|
return new List<MediaSourceInfo>
|
||||||
{
|
{
|
||||||
|
@ -2198,6 +2220,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||||
|
|
||||||
private bool ShouldCancelTimerForSeriesTimer(SeriesTimerInfo seriesTimer, TimerInfo timer)
|
private bool ShouldCancelTimerForSeriesTimer(SeriesTimerInfo seriesTimer, TimerInfo timer)
|
||||||
{
|
{
|
||||||
|
if (timer.IsManual)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (!seriesTimer.RecordAnyTime)
|
if (!seriesTimer.RecordAnyTime)
|
||||||
{
|
{
|
||||||
if (Math.Abs(seriesTimer.StartDate.TimeOfDay.Ticks - timer.StartDate.TimeOfDay.Ticks) >= TimeSpan.FromMinutes(5).Ticks)
|
if (Math.Abs(seriesTimer.StartDate.TimeOfDay.Ticks - timer.StartDate.TimeOfDay.Ticks) >= TimeSpan.FromMinutes(5).Ticks)
|
||||||
|
|
|
@ -65,6 +65,15 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool CopySubtitles
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
//return string.Equals(OutputFormat, "mkv", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public string GetOutputPath(MediaSourceInfo mediaSource, string targetFile)
|
public string GetOutputPath(MediaSourceInfo mediaSource, string targetFile)
|
||||||
{
|
{
|
||||||
return Path.ChangeExtension(targetFile, "." + OutputFormat);
|
return Path.ChangeExtension(targetFile, "." + OutputFormat);
|
||||||
|
@ -154,9 +163,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||||
|
|
||||||
var durationParam = " -t " + _mediaEncoder.GetTimeParameter(duration.Ticks);
|
var durationParam = " -t " + _mediaEncoder.GetTimeParameter(duration.Ticks);
|
||||||
var inputModifiers = "-fflags +genpts -async 1 -vsync -1";
|
var inputModifiers = "-fflags +genpts -async 1 -vsync -1";
|
||||||
var mapArgs = string.Equals(OutputFormat, "mkv", StringComparison.OrdinalIgnoreCase) ? "-map 0" : "-sn";
|
var mapArgs = "-map 0 -ignore_unknown";
|
||||||
mapArgs = "-sn";
|
var commandLineArgs = "-i \"{0}\"{5} " + mapArgs + " {2} -map_metadata -1 -threads 0 {3}{4} -y \"{1}\"";
|
||||||
var commandLineArgs = "-i \"{0}\"{4} " + mapArgs + " {2} -map_metadata -1 -threads 0 {3} -y \"{1}\"";
|
|
||||||
|
|
||||||
long startTimeTicks = 0;
|
long startTimeTicks = 0;
|
||||||
//if (mediaSource.DateLiveStreamOpened.HasValue)
|
//if (mediaSource.DateLiveStreamOpened.HasValue)
|
||||||
|
@ -184,7 +192,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||||
(analyzeDurationSeconds * 1000000).ToString(CultureInfo.InvariantCulture);
|
(analyzeDurationSeconds * 1000000).ToString(CultureInfo.InvariantCulture);
|
||||||
inputModifiers += analyzeDuration;
|
inputModifiers += analyzeDuration;
|
||||||
|
|
||||||
commandLineArgs = string.Format(commandLineArgs, inputTempFile, targetFile, videoArgs, GetAudioArgs(mediaSource), durationParam);
|
var subtitleArgs = CopySubtitles ? " -codec:s copy" : " -sn";
|
||||||
|
|
||||||
|
commandLineArgs = string.Format(commandLineArgs, inputTempFile, targetFile, videoArgs, GetAudioArgs(mediaSource), subtitleArgs, durationParam);
|
||||||
|
|
||||||
return inputModifiers + " " + commandLineArgs;
|
return inputModifiers + " " + commandLineArgs;
|
||||||
}
|
}
|
||||||
|
@ -197,7 +207,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||||
// do not copy aac because many players have difficulty with aac_latm
|
// do not copy aac because many players have difficulty with aac_latm
|
||||||
if (_liveTvOptions.EnableOriginalAudioWithEncodedRecordings && !string.Equals(inputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase))
|
if (_liveTvOptions.EnableOriginalAudioWithEncodedRecordings && !string.Equals(inputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return "-codec:a:0 copy";
|
return "-codec:a copy";
|
||||||
}
|
}
|
||||||
|
|
||||||
var audioChannels = 2;
|
var audioChannels = 2;
|
||||||
|
@ -206,7 +216,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||||
{
|
{
|
||||||
audioChannels = audioStream.Channels ?? audioChannels;
|
audioChannels = audioStream.Channels ?? audioChannels;
|
||||||
}
|
}
|
||||||
return "-codec:a:0 aac -strict experimental -ab 320000";
|
return "-codec:a aac -strict experimental -ab 320000";
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool EncodeVideo(MediaSourceInfo mediaSource)
|
private bool EncodeVideo(MediaSourceInfo mediaSource)
|
||||||
|
@ -261,7 +271,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||||
{
|
{
|
||||||
_logger.Info("Calling recording process.WaitForExit for {0}", _targetPath);
|
_logger.Info("Calling recording process.WaitForExit for {0}", _targetPath);
|
||||||
|
|
||||||
if (_process.WaitForExit(5000))
|
if (_process.WaitForExit(10000))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,12 +81,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||||
return programsInfo;
|
return programsInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(info.ListingsId))
|
|
||||||
{
|
|
||||||
_logger.Warn("ListingsId is null, returning empty program list");
|
|
||||||
return programsInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
var dates = GetScheduleRequestDates(startDateUtc, endDateUtc);
|
var dates = GetScheduleRequestDates(startDateUtc, endDateUtc);
|
||||||
|
|
||||||
string stationID = channelId;
|
string stationID = channelId;
|
||||||
|
@ -156,7 +150,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||||
programDetails.Where(p => p.hasImageArtwork).Select(p => p.programID)
|
programDetails.Where(p => p.hasImageArtwork).Select(p => p.programID)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken);
|
var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var schedules = dailySchedules.SelectMany(d => d.programs);
|
var schedules = dailySchedules.SelectMany(d => d.programs);
|
||||||
foreach (ScheduleDirect.Program schedule in schedules)
|
foreach (ScheduleDirect.Program schedule in schedules)
|
||||||
|
|
|
@ -5,6 +5,7 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
@ -26,13 +27,15 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||||
private readonly IHttpClient _httpClient;
|
private readonly IHttpClient _httpClient;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly IFileSystem _fileSystem;
|
private readonly IFileSystem _fileSystem;
|
||||||
|
private readonly IZipClient _zipClient;
|
||||||
|
|
||||||
public XmlTvListingsProvider(IServerConfigurationManager config, IHttpClient httpClient, ILogger logger, IFileSystem fileSystem)
|
public XmlTvListingsProvider(IServerConfigurationManager config, IHttpClient httpClient, ILogger logger, IFileSystem fileSystem, IZipClient zipClient)
|
||||||
{
|
{
|
||||||
_config = config;
|
_config = config;
|
||||||
_httpClient = httpClient;
|
_httpClient = httpClient;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_fileSystem = fileSystem;
|
_fileSystem = fileSystem;
|
||||||
|
_zipClient = zipClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Name
|
public string Name
|
||||||
|
@ -63,7 +66,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||||
var cacheFile = Path.Combine(_config.ApplicationPaths.CachePath, "xmltv", cacheFilename);
|
var cacheFile = Path.Combine(_config.ApplicationPaths.CachePath, "xmltv", cacheFilename);
|
||||||
if (_fileSystem.FileExists(cacheFile))
|
if (_fileSystem.FileExists(cacheFile))
|
||||||
{
|
{
|
||||||
return cacheFile;
|
return UnzipIfNeeded(path, cacheFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.Info("Downloading xmltv listings from {0}", path);
|
_logger.Info("Downloading xmltv listings from {0}", path);
|
||||||
|
@ -103,7 +106,30 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.Debug("Returning xmltv path {0}", cacheFile);
|
_logger.Debug("Returning xmltv path {0}", cacheFile);
|
||||||
return cacheFile;
|
return UnzipIfNeeded(path, cacheFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string UnzipIfNeeded(string originalUrl, string file)
|
||||||
|
{
|
||||||
|
//var ext = Path.GetExtension(originalUrl);
|
||||||
|
|
||||||
|
//if (string.Equals(ext, ".gz", StringComparison.OrdinalIgnoreCase))
|
||||||
|
//{
|
||||||
|
// using (var stream = _fileSystem.OpenRead(file))
|
||||||
|
// {
|
||||||
|
// var tempFolder = Path.Combine(_config.ApplicationPaths.TempDirectory, Guid.NewGuid().ToString());
|
||||||
|
// _fileSystem.CreateDirectory(tempFolder);
|
||||||
|
|
||||||
|
// _zipClient.ExtractAllFromZip(stream, tempFolder, true);
|
||||||
|
|
||||||
|
// return _fileSystem.GetFiles(tempFolder, true)
|
||||||
|
// .Where(i => string.Equals(i.Extension, ".xml", StringComparison.OrdinalIgnoreCase))
|
||||||
|
// .Select(i => i.FullName)
|
||||||
|
// .FirstOrDefault();
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
return file;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)
|
public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)
|
||||||
|
@ -122,6 +148,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.Debug("Getting xmltv programs for channel {0}", channelId);
|
||||||
|
|
||||||
var path = await GetXml(info.Path, cancellationToken).ConfigureAwait(false);
|
var path = await GetXml(info.Path, cancellationToken).ConfigureAwait(false);
|
||||||
var reader = new XmlTvReader(path, GetLanguage());
|
var reader = new XmlTvReader(path, GetLanguage());
|
||||||
|
|
||||||
|
@ -140,7 +168,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||||
EpisodeNumber = p.Episode == null ? null : p.Episode.Episode,
|
EpisodeNumber = p.Episode == null ? null : p.Episode.Episode,
|
||||||
EpisodeTitle = episodeTitle,
|
EpisodeTitle = episodeTitle,
|
||||||
Genres = p.Categories,
|
Genres = p.Categories,
|
||||||
Id = String.Format("{0}_{1:O}", p.ChannelId, p.StartDate), // Construct an id from the channel and start date,
|
|
||||||
StartDate = GetDate(p.StartDate),
|
StartDate = GetDate(p.StartDate),
|
||||||
Name = p.Title,
|
Name = p.Title,
|
||||||
Overview = p.Description,
|
Overview = p.Description,
|
||||||
|
@ -180,6 +207,9 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||||
programInfo.ShowId = uniqueString.GetMD5().ToString("N");
|
programInfo.ShowId = uniqueString.GetMD5().ToString("N");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Construct an id from the channel and start date
|
||||||
|
programInfo.Id = String.Format("{0}_{1:O}", p.ChannelId, p.StartDate);
|
||||||
|
|
||||||
if (programInfo.IsMovie)
|
if (programInfo.IsMovie)
|
||||||
{
|
{
|
||||||
programInfo.IsSeries = false;
|
programInfo.IsSeries = false;
|
||||||
|
|
|
@ -22,7 +22,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AddMediaInfoWithProbe(MediaSourceInfo mediaSource, bool isAudio, bool assumeInterlaced, CancellationToken cancellationToken)
|
public async Task AddMediaInfoWithProbe(MediaSourceInfo mediaSource, bool isAudio, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var originalRuntime = mediaSource.RunTimeTicks;
|
var originalRuntime = mediaSource.RunTimeTicks;
|
||||||
|
|
||||||
|
@ -96,17 +96,6 @@ namespace Emby.Server.Implementations.LiveTv
|
||||||
videoStream.IsAVC = null;
|
videoStream.IsAVC = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (assumeInterlaced)
|
|
||||||
{
|
|
||||||
foreach (var mediaStream in mediaSource.MediaStreams)
|
|
||||||
{
|
|
||||||
if (mediaStream.Type == MediaStreamType.Video)
|
|
||||||
{
|
|
||||||
mediaStream.IsInterlaced = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to estimate this
|
// Try to estimate this
|
||||||
mediaSource.InferTotalBitrate(true);
|
mediaSource.InferTotalBitrate(true);
|
||||||
}
|
}
|
||||||
|
|
|
@ -277,6 +277,13 @@ namespace Emby.Server.Implementations.LiveTv
|
||||||
if (!string.IsNullOrWhiteSpace(programSeriesId))
|
if (!string.IsNullOrWhiteSpace(programSeriesId))
|
||||||
{
|
{
|
||||||
var program = _libraryManager.GetItemList(new InternalItemsQuery
|
var program = _libraryManager.GetItemList(new InternalItemsQuery
|
||||||
|
{
|
||||||
|
IncludeItemTypes = new string[] { typeof(Series).Name },
|
||||||
|
Name = seriesName,
|
||||||
|
Limit = 1,
|
||||||
|
ImageTypes = new ImageType[] { ImageType.Primary }
|
||||||
|
|
||||||
|
}).FirstOrDefault() ?? _libraryManager.GetItemList(new InternalItemsQuery
|
||||||
{
|
{
|
||||||
IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
|
IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
|
||||||
ExternalSeriesId = programSeriesId,
|
ExternalSeriesId = programSeriesId,
|
||||||
|
|
|
@ -598,15 +598,15 @@ namespace Emby.Server.Implementations.LiveTv
|
||||||
item.ParentId = channel.Id;
|
item.ParentId = channel.Id;
|
||||||
|
|
||||||
//item.ChannelType = channelType;
|
//item.ChannelType = channelType;
|
||||||
if (!string.Equals(item.ServiceName, serviceName, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
forceUpdate = true;
|
|
||||||
}
|
|
||||||
item.ServiceName = serviceName;
|
item.ServiceName = serviceName;
|
||||||
|
|
||||||
item.Audio = info.Audio;
|
item.Audio = info.Audio;
|
||||||
item.ChannelId = channel.Id.ToString("N");
|
item.ChannelId = channel.Id.ToString("N");
|
||||||
item.CommunityRating = item.CommunityRating ?? info.CommunityRating;
|
item.CommunityRating = item.CommunityRating ?? info.CommunityRating;
|
||||||
|
if ((item.CommunityRating ?? 0).Equals(0))
|
||||||
|
{
|
||||||
|
item.CommunityRating = null;
|
||||||
|
}
|
||||||
|
|
||||||
item.EpisodeTitle = info.EpisodeTitle;
|
item.EpisodeTitle = info.EpisodeTitle;
|
||||||
item.ExternalId = info.Id;
|
item.ExternalId = info.Id;
|
||||||
|
@ -1307,7 +1307,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||||
var isKids = false;
|
var isKids = false;
|
||||||
var iSSeries = false;
|
var iSSeries = false;
|
||||||
|
|
||||||
var channelPrograms = await service.GetProgramsAsync(GetItemExternalId(currentChannel), start, end, cancellationToken).ConfigureAwait(false);
|
var channelPrograms = (await service.GetProgramsAsync(GetItemExternalId(currentChannel), start, end, cancellationToken).ConfigureAwait(false)).ToList();
|
||||||
|
|
||||||
var existingPrograms = _libraryManager.GetItemList(new InternalItemsQuery
|
var existingPrograms = _libraryManager.GetItemList(new InternalItemsQuery
|
||||||
{
|
{
|
||||||
|
@ -1405,7 +1405,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||||
double percent = numComplete;
|
double percent = numComplete;
|
||||||
percent /= allChannelsList.Count;
|
percent /= allChannelsList.Count;
|
||||||
|
|
||||||
progress.Report(80 * percent + 10);
|
progress.Report(85 * percent + 15);
|
||||||
}
|
}
|
||||||
progress.Report(100);
|
progress.Report(100);
|
||||||
|
|
||||||
|
@ -1477,12 +1477,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||||
private DateTime _lastRecordingRefreshTime;
|
private DateTime _lastRecordingRefreshTime;
|
||||||
private async Task RefreshRecordings(CancellationToken cancellationToken)
|
private async Task RefreshRecordings(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
const int cacheMinutes = 3;
|
const int cacheMinutes = 2;
|
||||||
|
|
||||||
if ((DateTime.UtcNow - _lastRecordingRefreshTime).TotalMinutes < cacheMinutes)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _refreshRecordingsLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
await _refreshRecordingsLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
@ -1885,7 +1880,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||||
: _tvDtoService.GetInternalTimerId(service.Name, info.TimerId).ToString("N");
|
: _tvDtoService.GetInternalTimerId(service.Name, info.TimerId).ToString("N");
|
||||||
|
|
||||||
dto.StartDate = info.StartDate;
|
dto.StartDate = info.StartDate;
|
||||||
dto.RecordingStatus = info.Status;
|
dto.Status = info.Status.ToString();
|
||||||
dto.IsRepeat = info.IsRepeat;
|
dto.IsRepeat = info.IsRepeat;
|
||||||
dto.EpisodeTitle = info.EpisodeTitle;
|
dto.EpisodeTitle = info.EpisodeTitle;
|
||||||
dto.IsMovie = info.IsMovie;
|
dto.IsMovie = info.IsMovie;
|
||||||
|
@ -2866,6 +2861,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||||
{
|
{
|
||||||
info.Id = Guid.NewGuid().ToString("N");
|
info.Id = Guid.NewGuid().ToString("N");
|
||||||
config.ListingProviders.Add(info);
|
config.ListingProviders.Add(info);
|
||||||
|
info.EnableNewProgramIds = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -2930,7 +2926,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||||
var result = new TunerChannelMapping
|
var result = new TunerChannelMapping
|
||||||
{
|
{
|
||||||
Name = tunerChannel.Name,
|
Name = tunerChannel.Name,
|
||||||
Id = tunerChannel.TunerChannelId
|
Id = tunerChannel.Id
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(tunerChannel.Number))
|
if (!string.IsNullOrWhiteSpace(tunerChannel.Number))
|
||||||
|
@ -2938,11 +2934,6 @@ namespace Emby.Server.Implementations.LiveTv
|
||||||
result.Name = tunerChannel.Number + " " + result.Name;
|
result.Name = tunerChannel.Number + " " + result.Name;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(result.Id))
|
|
||||||
{
|
|
||||||
result.Id = tunerChannel.Id;
|
|
||||||
}
|
|
||||||
|
|
||||||
var providerChannel = EmbyTV.EmbyTV.Current.GetEpgChannelFromTunerChannel(mappings, tunerChannel, epgChannels);
|
var providerChannel = EmbyTV.EmbyTV.Current.GetEpgChannelFromTunerChannel(mappings, tunerChannel, epgChannels);
|
||||||
|
|
||||||
if (providerChannel != null)
|
if (providerChannel != null)
|
||||||
|
|
|
@ -126,14 +126,12 @@ namespace Emby.Server.Implementations.LiveTv
|
||||||
var keys = openToken.Split(new[] { StreamIdDelimeter }, 3);
|
var keys = openToken.Split(new[] { StreamIdDelimeter }, 3);
|
||||||
var mediaSourceId = keys.Length >= 3 ? keys[2] : null;
|
var mediaSourceId = keys.Length >= 3 ? keys[2] : null;
|
||||||
IDirectStreamProvider directStreamProvider = null;
|
IDirectStreamProvider directStreamProvider = null;
|
||||||
var assumeInterlaced = false;
|
|
||||||
|
|
||||||
if (string.Equals(keys[0], typeof(LiveTvChannel).Name, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(keys[0], typeof(LiveTvChannel).Name, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var info = await _liveTvManager.GetChannelStream(keys[1], mediaSourceId, cancellationToken).ConfigureAwait(false);
|
var info = await _liveTvManager.GetChannelStream(keys[1], mediaSourceId, cancellationToken).ConfigureAwait(false);
|
||||||
stream = info.Item1;
|
stream = info.Item1;
|
||||||
directStreamProvider = info.Item2;
|
directStreamProvider = info.Item2;
|
||||||
assumeInterlaced = info.Item3;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -148,7 +146,7 @@ namespace Emby.Server.Implementations.LiveTv
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await new LiveStreamHelper(_mediaEncoder, _logger).AddMediaInfoWithProbe(stream, isAudio, assumeInterlaced, cancellationToken).ConfigureAwait(false);
|
await new LiveStreamHelper(_mediaEncoder, _logger).AddMediaInfoWithProbe(stream, isAudio, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
|
@ -35,10 +35,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||||
MediaEncoder = mediaEncoder;
|
MediaEncoder = mediaEncoder;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract Task<IEnumerable<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken);
|
protected abstract Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken);
|
||||||
public abstract string Type { get; }
|
public abstract string Type { get; }
|
||||||
|
|
||||||
public async Task<IEnumerable<ChannelInfo>> GetChannels(TunerHostInfo tuner, bool enableCache, CancellationToken cancellationToken)
|
public async Task<List<ChannelInfo>> GetChannels(TunerHostInfo tuner, bool enableCache, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
ChannelCache cache = null;
|
ChannelCache cache = null;
|
||||||
var key = tuner.Id;
|
var key = tuner.Id;
|
||||||
|
@ -73,7 +73,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<ChannelInfo>> GetChannels(bool enableCache, CancellationToken cancellationToken)
|
public async Task<List<ChannelInfo>> GetChannels(bool enableCache, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var list = new List<ChannelInfo>();
|
var list = new List<ChannelInfo>();
|
||||||
|
|
||||||
|
|
|
@ -9,17 +9,14 @@ using MediaBrowser.Model.MediaInfo;
|
||||||
using MediaBrowser.Model.Serialization;
|
using MediaBrowser.Model.Serialization;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Common.IO;
|
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Controller.IO;
|
|
||||||
using MediaBrowser.Controller.MediaEncoding;
|
using MediaBrowser.Controller.MediaEncoding;
|
||||||
using MediaBrowser.Model.Configuration;
|
using MediaBrowser.Model.Configuration;
|
||||||
using MediaBrowser.Model.Net;
|
using MediaBrowser.Model.Net;
|
||||||
|
@ -66,7 +63,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<IEnumerable<Channels>> GetLineup(TunerHostInfo info, CancellationToken cancellationToken)
|
private async Task<List<Channels>> GetLineup(TunerHostInfo info, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var options = new HttpRequestOptions
|
var options = new HttpRequestOptions
|
||||||
{
|
{
|
||||||
|
@ -74,7 +71,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||||
CancellationToken = cancellationToken,
|
CancellationToken = cancellationToken,
|
||||||
BufferContent = false
|
BufferContent = false
|
||||||
};
|
};
|
||||||
using (var stream = await _httpClient.Get(options))
|
using (var stream = await _httpClient.Get(options).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
var lineup = JsonSerializer.DeserializeFromStream<List<Channels>>(stream) ?? new List<Channels>();
|
var lineup = JsonSerializer.DeserializeFromStream<List<Channels>>(stream) ?? new List<Channels>();
|
||||||
|
|
||||||
|
@ -87,7 +84,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task<IEnumerable<ChannelInfo>> GetChannelsInternal(TunerHostInfo info, CancellationToken cancellationToken)
|
protected override async Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo info, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var lineup = await GetLineup(info, cancellationToken).ConfigureAwait(false);
|
var lineup = await GetLineup(info, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
@ -102,7 +99,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||||
AudioCodec = i.AudioCodec,
|
AudioCodec = i.AudioCodec,
|
||||||
VideoCodec = i.VideoCodec,
|
VideoCodec = i.VideoCodec,
|
||||||
ChannelType = ChannelType.TV
|
ChannelType = ChannelType.TV
|
||||||
});
|
|
||||||
|
}).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly Dictionary<string, DiscoverResponse> _modelCache = new Dictionary<string, DiscoverResponse>();
|
private readonly Dictionary<string, DiscoverResponse> _modelCache = new Dictionary<string, DiscoverResponse>();
|
||||||
|
@ -127,7 +125,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||||
CacheMode = CacheMode.Unconditional,
|
CacheMode = CacheMode.Unconditional,
|
||||||
TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds),
|
TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds),
|
||||||
BufferContent = false
|
BufferContent = false
|
||||||
}))
|
|
||||||
|
}).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
var response = JsonSerializer.DeserializeFromStream<DiscoverResponse>(stream);
|
var response = JsonSerializer.DeserializeFromStream<DiscoverResponse>(stream);
|
||||||
|
|
||||||
|
@ -169,7 +168,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||||
CancellationToken = cancellationToken,
|
CancellationToken = cancellationToken,
|
||||||
TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds),
|
TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds),
|
||||||
BufferContent = false
|
BufferContent = false
|
||||||
}))
|
|
||||||
|
}).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
var tuners = new List<LiveTvTunerInfo>();
|
var tuners = new List<LiveTvTunerInfo>();
|
||||||
using (var sr = new StreamReader(stream, System.Text.Encoding.UTF8))
|
using (var sr = new StreamReader(stream, System.Text.Encoding.UTF8))
|
||||||
|
@ -536,7 +536,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||||
Url = string.Format("{0}/discover.json", GetApiUrl(info, false)),
|
Url = string.Format("{0}/discover.json", GetApiUrl(info, false)),
|
||||||
CancellationToken = CancellationToken.None,
|
CancellationToken = CancellationToken.None,
|
||||||
BufferContent = false
|
BufferContent = false
|
||||||
}))
|
|
||||||
|
}).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
var response = JsonSerializer.DeserializeFromStream<DiscoverResponse>(stream);
|
var response = JsonSerializer.DeserializeFromStream<DiscoverResponse>(stream);
|
||||||
|
|
||||||
|
|
|
@ -48,9 +48,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||||
|
|
||||||
private const string ChannelIdPrefix = "m3u_";
|
private const string ChannelIdPrefix = "m3u_";
|
||||||
|
|
||||||
protected override async Task<IEnumerable<ChannelInfo>> GetChannelsInternal(TunerHostInfo info, CancellationToken cancellationToken)
|
protected override async Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo info, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return await new M3uParser(Logger, _fileSystem, _httpClient, _appHost).Parse(info.Url, ChannelIdPrefix, info.Id, !info.EnableTvgId, cancellationToken).ConfigureAwait(false);
|
var result = await new M3uParser(Logger, _fileSystem, _httpClient, _appHost).Parse(info.Url, ChannelIdPrefix, info.Id, !info.EnableTvgId, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
return result.Cast<ChannelInfo>().ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken)
|
public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken)
|
||||||
|
|
|
@ -136,11 +136,26 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||||
channel.Name = GetChannelName(extInf, attributes);
|
channel.Name = GetChannelName(extInf, attributes);
|
||||||
channel.Number = GetChannelNumber(extInf, attributes, mediaUrl);
|
channel.Number = GetChannelNumber(extInf, attributes, mediaUrl);
|
||||||
|
|
||||||
var channelId = GetTunerChannelId(attributes);
|
string tvgId;
|
||||||
|
attributes.TryGetValue("tvg-id", out tvgId);
|
||||||
|
|
||||||
|
string channelId;
|
||||||
|
attributes.TryGetValue("channel-id", out channelId);
|
||||||
|
|
||||||
|
channel.TunerChannelId = string.IsNullOrWhiteSpace(tvgId) ? channelId : tvgId;
|
||||||
|
|
||||||
|
var channelIdValues = new List<string>();
|
||||||
if (!string.IsNullOrWhiteSpace(channelId))
|
if (!string.IsNullOrWhiteSpace(channelId))
|
||||||
{
|
{
|
||||||
channel.Id = channelId;
|
channelIdValues.Add(channelId);
|
||||||
channel.TunerChannelId = channelId;
|
}
|
||||||
|
if (!string.IsNullOrWhiteSpace(tvgId))
|
||||||
|
{
|
||||||
|
channelIdValues.Add(tvgId);
|
||||||
|
}
|
||||||
|
if (channelIdValues.Count > 0)
|
||||||
|
{
|
||||||
|
channel.Id = string.Join("_", channelIdValues.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
return channel;
|
return channel;
|
||||||
|
@ -296,19 +311,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetTunerChannelId(Dictionary<string, string> attributes)
|
|
||||||
{
|
|
||||||
string result;
|
|
||||||
attributes.TryGetValue("tvg-id", out result);
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(result))
|
|
||||||
{
|
|
||||||
attributes.TryGetValue("channel-id", out result);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Dictionary<string, string> ParseExtInf(string line, out string remaining)
|
private Dictionary<string, string> ParseExtInf(string line, out string remaining)
|
||||||
{
|
{
|
||||||
var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
|
@ -74,10 +74,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||||
OnFinished = OnFinished
|
OnFinished = OnFinished
|
||||||
};
|
};
|
||||||
|
|
||||||
var initial = _sharedBuffer.ToList();
|
|
||||||
var list = new List<byte>();
|
var list = new List<byte>();
|
||||||
|
foreach (var bytes in _sharedBuffer)
|
||||||
foreach (var bytes in initial)
|
|
||||||
{
|
{
|
||||||
list.AddRange(bytes);
|
list.AddRange(bytes);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,20 +2,25 @@
|
||||||
using MediaBrowser.Model.Logging;
|
using MediaBrowser.Model.Logging;
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using MediaBrowser.Model.IO;
|
||||||
|
|
||||||
namespace Emby.Server.Core
|
namespace Emby.Server.Implementations.Logging
|
||||||
{
|
{
|
||||||
public class UnhandledExceptionWriter
|
public class UnhandledExceptionWriter
|
||||||
{
|
{
|
||||||
private readonly IApplicationPaths _appPaths;
|
private readonly IApplicationPaths _appPaths;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly ILogManager _logManager;
|
private readonly ILogManager _logManager;
|
||||||
|
private readonly IFileSystem _fileSystem;
|
||||||
|
private readonly IConsoleLogger _console;
|
||||||
|
|
||||||
public UnhandledExceptionWriter(IApplicationPaths appPaths, ILogger logger, ILogManager logManager)
|
public UnhandledExceptionWriter(IApplicationPaths appPaths, ILogger logger, ILogManager logManager, IFileSystem fileSystem, IConsoleLogger console)
|
||||||
{
|
{
|
||||||
_appPaths = appPaths;
|
_appPaths = appPaths;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_logManager = logManager;
|
_logManager = logManager;
|
||||||
|
_fileSystem = fileSystem;
|
||||||
|
_console = console;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Log(Exception ex)
|
public void Log(Exception ex)
|
||||||
|
@ -24,15 +29,15 @@ namespace Emby.Server.Core
|
||||||
_logManager.Flush();
|
_logManager.Flush();
|
||||||
|
|
||||||
var path = Path.Combine(_appPaths.LogDirectoryPath, "unhandled_" + Guid.NewGuid() + ".txt");
|
var path = Path.Combine(_appPaths.LogDirectoryPath, "unhandled_" + Guid.NewGuid() + ".txt");
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
_fileSystem.CreateDirectory(Path.GetDirectoryName(path));
|
||||||
|
|
||||||
var builder = LogHelper.GetLogMessage(ex);
|
var builder = LogHelper.GetLogMessage(ex);
|
||||||
|
|
||||||
// Write to console just in case file logging fails
|
// Write to console just in case file logging fails
|
||||||
Console.WriteLine("UnhandledException");
|
_console.WriteLine("UnhandledException");
|
||||||
Console.WriteLine(builder.ToString());
|
_console.WriteLine(builder.ToString());
|
||||||
|
|
||||||
File.WriteAllText(path, builder.ToString());
|
_fileSystem.WriteAllText(path, builder.ToString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -8,6 +8,7 @@ using System.Threading.Tasks;
|
||||||
using Emby.Server.Implementations.Data;
|
using Emby.Server.Implementations.Data;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
using MediaBrowser.Controller.Notifications;
|
using MediaBrowser.Controller.Notifications;
|
||||||
|
using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Model.Logging;
|
using MediaBrowser.Model.Logging;
|
||||||
using MediaBrowser.Model.Notifications;
|
using MediaBrowser.Model.Notifications;
|
||||||
using SQLitePCL.pretty;
|
using SQLitePCL.pretty;
|
||||||
|
@ -16,8 +17,11 @@ namespace Emby.Server.Implementations.Notifications
|
||||||
{
|
{
|
||||||
public class SqliteNotificationsRepository : BaseSqliteRepository, INotificationsRepository
|
public class SqliteNotificationsRepository : BaseSqliteRepository, INotificationsRepository
|
||||||
{
|
{
|
||||||
public SqliteNotificationsRepository(ILogger logger, IServerApplicationPaths appPaths) : base(logger)
|
protected IFileSystem FileSystem { get; private set; }
|
||||||
|
|
||||||
|
public SqliteNotificationsRepository(ILogger logger, IServerApplicationPaths appPaths, IFileSystem fileSystem) : base(logger)
|
||||||
{
|
{
|
||||||
|
FileSystem = fileSystem;
|
||||||
DbFilePath = Path.Combine(appPaths.DataPath, "notifications.db");
|
DbFilePath = Path.Combine(appPaths.DataPath, "notifications.db");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,6 +30,22 @@ namespace Emby.Server.Implementations.Notifications
|
||||||
////public event EventHandler<NotificationUpdateEventArgs> NotificationUpdated;
|
////public event EventHandler<NotificationUpdateEventArgs> NotificationUpdated;
|
||||||
|
|
||||||
public void Initialize()
|
public void Initialize()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
InitializeInternal();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.ErrorException("Error loading notifications database file. Will reset and retry.", ex);
|
||||||
|
|
||||||
|
FileSystem.DeleteFile(DbFilePath);
|
||||||
|
|
||||||
|
InitializeInternal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeInternal()
|
||||||
{
|
{
|
||||||
using (var connection = CreateConnection())
|
using (var connection = CreateConnection())
|
||||||
{
|
{
|
||||||
|
|
|
@ -277,8 +277,7 @@ namespace Emby.Server.Implementations.Security
|
||||||
{ "systemid", _appHost.SystemId },
|
{ "systemid", _appHost.SystemId },
|
||||||
{ "mb2equiv", mb2Equivalent },
|
{ "mb2equiv", mb2Equivalent },
|
||||||
{ "ver", version },
|
{ "ver", version },
|
||||||
{ "platform", _appHost.OperatingSystemDisplayName },
|
{ "platform", _appHost.OperatingSystemDisplayName }
|
||||||
{ "isservice", _appHost.IsRunningAsService.ToString().ToLower() }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
try
|
try
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
using System.IO;
|
using System;
|
||||||
using Emby.Common.Implementations;
|
using System.IO;
|
||||||
|
using Emby.Server.Implementations.AppBase;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
|
|
||||||
namespace Emby.Server.Core
|
namespace Emby.Server.Implementations
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extends BaseApplicationPaths to add paths that are only applicable on the server
|
/// Extends BaseApplicationPaths to add paths that are only applicable on the server
|
||||||
|
@ -12,8 +13,8 @@ namespace Emby.Server.Core
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="BaseApplicationPaths" /> class.
|
/// Initializes a new instance of the <see cref="BaseApplicationPaths" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ServerApplicationPaths(string programDataPath, string appFolderPath, string applicationResourcesPath)
|
public ServerApplicationPaths(string programDataPath, string appFolderPath, string applicationResourcesPath, Action<string> createDirectoryFn)
|
||||||
: base(programDataPath, appFolderPath)
|
: base(programDataPath, appFolderPath, createDirectoryFn)
|
||||||
{
|
{
|
||||||
ApplicationResourcesPath = applicationResourcesPath;
|
ApplicationResourcesPath = applicationResourcesPath;
|
||||||
}
|
}
|
|
@ -303,6 +303,7 @@ namespace Emby.Server.Implementations.ServerManager
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void DisposeHttpServer()
|
private void DisposeHttpServer()
|
||||||
{
|
{
|
||||||
|
_logger.Info("Disposing web socket connections");
|
||||||
foreach (var socket in _webSocketConnections)
|
foreach (var socket in _webSocketConnections)
|
||||||
{
|
{
|
||||||
// Dispose the connection
|
// Dispose the connection
|
||||||
|
@ -314,6 +315,9 @@ namespace Emby.Server.Implementations.ServerManager
|
||||||
if (HttpServer != null)
|
if (HttpServer != null)
|
||||||
{
|
{
|
||||||
HttpServer.WebSocketConnected -= HttpServer_WebSocketConnected;
|
HttpServer.WebSocketConnected -= HttpServer_WebSocketConnected;
|
||||||
|
|
||||||
|
_logger.Info("Disposing http server");
|
||||||
|
|
||||||
HttpServer.Dispose();
|
HttpServer.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Model.Services;
|
using MediaBrowser.Model.Services;
|
||||||
using ServiceStack.Host;
|
|
||||||
|
|
||||||
namespace ServiceStack
|
namespace Emby.Server.Implementations.Services
|
||||||
{
|
{
|
||||||
public class HttpResult
|
public class HttpResult
|
||||||
: IHttpResult, IAsyncStreamWriter
|
: IHttpResult, IAsyncStreamWriter
|
||||||
|
@ -55,7 +52,7 @@ namespace ServiceStack
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await HttpResponseExtensionsInternal.WriteObject(this.RequestContext, this.Response, response).ConfigureAwait(false);
|
await ResponseHelper.WriteObject(this.RequestContext, this.Response, response).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
51
Emby.Server.Implementations/Services/RequestHelper.cs
Normal file
51
Emby.Server.Implementations/Services/RequestHelper.cs
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using Emby.Server.Implementations.HttpServer;
|
||||||
|
|
||||||
|
namespace Emby.Server.Implementations.Services
|
||||||
|
{
|
||||||
|
public class RequestHelper
|
||||||
|
{
|
||||||
|
public static Func<Type, Stream, object> GetRequestReader(HttpListenerHost host, string contentType)
|
||||||
|
{
|
||||||
|
switch (GetContentTypeWithoutEncoding(contentType))
|
||||||
|
{
|
||||||
|
case "application/xml":
|
||||||
|
case "text/xml":
|
||||||
|
case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml
|
||||||
|
return host.DeserializeXml;
|
||||||
|
|
||||||
|
case "application/json":
|
||||||
|
case "text/json":
|
||||||
|
return host.DeserializeJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Action<object, Stream> GetResponseWriter(HttpListenerHost host, string contentType)
|
||||||
|
{
|
||||||
|
switch (GetContentTypeWithoutEncoding(contentType))
|
||||||
|
{
|
||||||
|
case "application/xml":
|
||||||
|
case "text/xml":
|
||||||
|
case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml
|
||||||
|
return host.SerializeToXml;
|
||||||
|
|
||||||
|
case "application/json":
|
||||||
|
case "text/json":
|
||||||
|
return host.SerializeToJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetContentTypeWithoutEncoding(string contentType)
|
||||||
|
{
|
||||||
|
return contentType == null
|
||||||
|
? null
|
||||||
|
: contentType.Split(';')[0].ToLower().Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,21 +1,18 @@
|
||||||
//Copyright (c) Service Stack LLC. All Rights Reserved.
|
|
||||||
//License: https://raw.github.com/ServiceStack/ServiceStack/master/license.txt
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Emby.Server.Implementations.HttpServer;
|
||||||
using MediaBrowser.Model.Services;
|
using MediaBrowser.Model.Services;
|
||||||
using ServiceStack.Host;
|
|
||||||
|
|
||||||
namespace ServiceStack
|
namespace Emby.Server.Implementations.Services
|
||||||
{
|
{
|
||||||
public static class HttpResponseExtensionsInternal
|
public static class ResponseHelper
|
||||||
{
|
{
|
||||||
public static async Task<bool> WriteToOutputStream(IResponse response, object result)
|
private static async Task<bool> WriteToOutputStream(IResponse response, object result)
|
||||||
{
|
{
|
||||||
var asyncStreamWriter = result as IAsyncStreamWriter;
|
var asyncStreamWriter = result as IAsyncStreamWriter;
|
||||||
if (asyncStreamWriter != null)
|
if (asyncStreamWriter != null)
|
||||||
|
@ -54,10 +51,9 @@ namespace ServiceStack
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public static Task WriteToResponse(IResponse httpRes, IRequest httpReq, object result)
|
||||||
/// End a ServiceStack Request with no content
|
{
|
||||||
/// </summary>
|
if (result == null)
|
||||||
public static void EndRequestWithNoContent(this IResponse httpRes)
|
|
||||||
{
|
{
|
||||||
if (httpRes.StatusCode == (int)HttpStatusCode.OK)
|
if (httpRes.StatusCode == (int)HttpStatusCode.OK)
|
||||||
{
|
{
|
||||||
|
@ -65,13 +61,6 @@ namespace ServiceStack
|
||||||
}
|
}
|
||||||
|
|
||||||
httpRes.SetContentLength(0);
|
httpRes.SetContentLength(0);
|
||||||
}
|
|
||||||
|
|
||||||
public static Task WriteToResponse(this IResponse httpRes, IRequest httpReq, object result)
|
|
||||||
{
|
|
||||||
if (result == null)
|
|
||||||
{
|
|
||||||
httpRes.EndRequestWithNoContent();
|
|
||||||
return Task.FromResult(true);
|
return Task.FromResult(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,10 +69,10 @@ namespace ServiceStack
|
||||||
{
|
{
|
||||||
httpResult.RequestContext = httpReq;
|
httpResult.RequestContext = httpReq;
|
||||||
httpReq.ResponseContentType = httpResult.ContentType ?? httpReq.ResponseContentType;
|
httpReq.ResponseContentType = httpResult.ContentType ?? httpReq.ResponseContentType;
|
||||||
return httpRes.WriteToResponseInternal(httpResult, httpReq);
|
return WriteToResponseInternal(httpRes, httpResult, httpReq);
|
||||||
}
|
}
|
||||||
|
|
||||||
return httpRes.WriteToResponseInternal(result, httpReq);
|
return WriteToResponseInternal(httpRes, result, httpReq);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -94,7 +83,7 @@ namespace ServiceStack
|
||||||
/// <param name="result">Whether or not it was implicity handled by ServiceStack's built-in handlers.</param>
|
/// <param name="result">Whether or not it was implicity handled by ServiceStack's built-in handlers.</param>
|
||||||
/// <param name="request">The serialization context.</param>
|
/// <param name="request">The serialization context.</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
private static async Task WriteToResponseInternal(this IResponse response, object result, IRequest request)
|
private static async Task WriteToResponseInternal(IResponse response, object result, IRequest request)
|
||||||
{
|
{
|
||||||
var defaultContentType = request.ResponseContentType;
|
var defaultContentType = request.ResponseContentType;
|
||||||
|
|
||||||
|
@ -173,7 +162,7 @@ namespace ServiceStack
|
||||||
public static async Task WriteObject(IRequest request, object result, IResponse response)
|
public static async Task WriteObject(IRequest request, object result, IResponse response)
|
||||||
{
|
{
|
||||||
var contentType = request.ResponseContentType;
|
var contentType = request.ResponseContentType;
|
||||||
var serializer = ContentTypes.GetStreamSerializer(contentType);
|
var serializer = RequestHelper.GetResponseWriter(HttpListenerHost.Instance, contentType);
|
||||||
|
|
||||||
using (var ms = new MemoryStream())
|
using (var ms = new MemoryStream())
|
||||||
{
|
{
|
|
@ -1,13 +1,12 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Emby.Server.Implementations.HttpServer;
|
||||||
|
using MediaBrowser.Model.Logging;
|
||||||
using MediaBrowser.Model.Services;
|
using MediaBrowser.Model.Services;
|
||||||
|
|
||||||
namespace ServiceStack.Host
|
namespace Emby.Server.Implementations.Services
|
||||||
{
|
{
|
||||||
public delegate Task<object> InstanceExecFn(IRequest requestContext, object intance, object request);
|
public delegate Task<object> InstanceExecFn(IRequest requestContext, object intance, object request);
|
||||||
public delegate object ActionInvokerFn(object intance, object request);
|
public delegate object ActionInvokerFn(object intance, object request);
|
||||||
|
@ -15,21 +14,20 @@ namespace ServiceStack.Host
|
||||||
|
|
||||||
public class ServiceController
|
public class ServiceController
|
||||||
{
|
{
|
||||||
|
public static ServiceController Instance;
|
||||||
private readonly Func<IEnumerable<Type>> _resolveServicesFn;
|
private readonly Func<IEnumerable<Type>> _resolveServicesFn;
|
||||||
|
|
||||||
public ServiceController(Func<IEnumerable<Type>> resolveServicesFn)
|
public ServiceController(Func<IEnumerable<Type>> resolveServicesFn)
|
||||||
{
|
{
|
||||||
|
Instance = this;
|
||||||
_resolveServicesFn = resolveServicesFn;
|
_resolveServicesFn = resolveServicesFn;
|
||||||
this.RequestTypeFactoryMap = new Dictionary<Type, Func<IRequest, object>>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Dictionary<Type, Func<IRequest, object>> RequestTypeFactoryMap { get; set; }
|
public void Init(HttpListenerHost appHost)
|
||||||
|
|
||||||
public void Init()
|
|
||||||
{
|
{
|
||||||
foreach (var serviceType in _resolveServicesFn())
|
foreach (var serviceType in _resolveServicesFn())
|
||||||
{
|
{
|
||||||
RegisterService(serviceType);
|
RegisterService(appHost, serviceType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,15 +38,12 @@ namespace ServiceStack.Host
|
||||||
: type.GetTypeInfo().GenericTypeArguments;
|
: type.GetTypeInfo().GenericTypeArguments;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RegisterService(Type serviceType)
|
public void RegisterService(HttpListenerHost appHost, Type serviceType)
|
||||||
{
|
{
|
||||||
var processedReqs = new HashSet<Type>();
|
var processedReqs = new HashSet<Type>();
|
||||||
|
|
||||||
var actions = ServiceExecGeneral.Reset(serviceType);
|
var actions = ServiceExecGeneral.Reset(serviceType);
|
||||||
|
|
||||||
var requiresRequestStreamTypeInfo = typeof(IRequiresRequestStream).GetTypeInfo();
|
|
||||||
|
|
||||||
var appHost = ServiceStackHost.Instance;
|
|
||||||
foreach (var mi in serviceType.GetActions())
|
foreach (var mi in serviceType.GetActions())
|
||||||
{
|
{
|
||||||
var requestType = mi.GetParameters()[0].ParameterType;
|
var requestType = mi.GetParameters()[0].ParameterType;
|
||||||
|
@ -57,45 +52,62 @@ namespace ServiceStack.Host
|
||||||
|
|
||||||
ServiceExecGeneral.CreateServiceRunnersFor(requestType, actions);
|
ServiceExecGeneral.CreateServiceRunnersFor(requestType, actions);
|
||||||
|
|
||||||
var returnMarker = requestType.GetTypeWithGenericTypeDefinitionOf(typeof(IReturn<>));
|
var returnMarker = GetTypeWithGenericTypeDefinitionOf(requestType, typeof(IReturn<>));
|
||||||
var responseType = returnMarker != null ?
|
var responseType = returnMarker != null ?
|
||||||
GetGenericArguments(returnMarker)[0]
|
GetGenericArguments(returnMarker)[0]
|
||||||
: mi.ReturnType != typeof(object) && mi.ReturnType != typeof(void) ?
|
: mi.ReturnType != typeof(object) && mi.ReturnType != typeof(void) ?
|
||||||
mi.ReturnType
|
mi.ReturnType
|
||||||
: Type.GetType(requestType.FullName + "Response");
|
: Type.GetType(requestType.FullName + "Response");
|
||||||
|
|
||||||
RegisterRestPaths(requestType);
|
RegisterRestPaths(appHost, requestType);
|
||||||
|
|
||||||
appHost.Metadata.Add(serviceType, requestType, responseType);
|
appHost.AddServiceInfo(serviceType, requestType, responseType);
|
||||||
|
|
||||||
if (requiresRequestStreamTypeInfo.IsAssignableFrom(requestType.GetTypeInfo()))
|
|
||||||
{
|
|
||||||
this.RequestTypeFactoryMap[requestType] = req =>
|
|
||||||
{
|
|
||||||
var restPath = req.GetRoute();
|
|
||||||
var request = RestHandler.CreateRequest(req, restPath, req.GetRequestParams(), ServiceStackHost.Instance.CreateInstance(requestType));
|
|
||||||
|
|
||||||
var rawReq = (IRequiresRequestStream)request;
|
|
||||||
rawReq.RequestStream = req.InputStream;
|
|
||||||
return rawReq;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Type GetTypeWithGenericTypeDefinitionOf(Type type, Type genericTypeDefinition)
|
||||||
|
{
|
||||||
|
foreach (var t in type.GetTypeInfo().ImplementedInterfaces)
|
||||||
|
{
|
||||||
|
if (t.GetTypeInfo().IsGenericType && t.GetGenericTypeDefinition() == genericTypeDefinition)
|
||||||
|
{
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var genericType = FirstGenericType(type);
|
||||||
|
if (genericType != null && genericType.GetGenericTypeDefinition() == genericTypeDefinition)
|
||||||
|
{
|
||||||
|
return genericType;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Type FirstGenericType(Type type)
|
||||||
|
{
|
||||||
|
while (type != null)
|
||||||
|
{
|
||||||
|
if (type.GetTypeInfo().IsGenericType)
|
||||||
|
return type;
|
||||||
|
|
||||||
|
type = type.GetTypeInfo().BaseType;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly Dictionary<string, List<RestPath>> RestPathMap = new Dictionary<string, List<RestPath>>(StringComparer.OrdinalIgnoreCase);
|
public readonly Dictionary<string, List<RestPath>> RestPathMap = new Dictionary<string, List<RestPath>>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
public void RegisterRestPaths(Type requestType)
|
public void RegisterRestPaths(HttpListenerHost appHost, Type requestType)
|
||||||
{
|
{
|
||||||
var appHost = ServiceStackHost.Instance;
|
|
||||||
var attrs = appHost.GetRouteAttributes(requestType);
|
var attrs = appHost.GetRouteAttributes(requestType);
|
||||||
foreach (MediaBrowser.Model.Services.RouteAttribute attr in attrs)
|
foreach (RouteAttribute attr in attrs)
|
||||||
{
|
{
|
||||||
var restPath = new RestPath(requestType, attr.Path, attr.Verbs, attr.Summary, attr.Notes);
|
var restPath = new RestPath(appHost.CreateInstance, appHost.GetParseFn, requestType, attr.Path, attr.Verbs, attr.Summary, attr.Notes);
|
||||||
|
|
||||||
if (!restPath.IsValid)
|
if (!restPath.IsValid)
|
||||||
throw new NotSupportedException(string.Format(
|
throw new NotSupportedException(string.Format(
|
||||||
"RestPath '{0}' on Type '{1}' is not Valid", attr.Path, requestType.GetOperationName()));
|
"RestPath '{0}' on Type '{1}' is not Valid", attr.Path, requestType.GetMethodName()));
|
||||||
|
|
||||||
RegisterRestPath(restPath);
|
RegisterRestPath(restPath);
|
||||||
}
|
}
|
||||||
|
@ -106,10 +118,10 @@ namespace ServiceStack.Host
|
||||||
public void RegisterRestPath(RestPath restPath)
|
public void RegisterRestPath(RestPath restPath)
|
||||||
{
|
{
|
||||||
if (!restPath.Path.StartsWith("/"))
|
if (!restPath.Path.StartsWith("/"))
|
||||||
throw new ArgumentException(string.Format("Route '{0}' on '{1}' must start with a '/'", restPath.Path, restPath.RequestType.GetOperationName()));
|
throw new ArgumentException(string.Format("Route '{0}' on '{1}' must start with a '/'", restPath.Path, restPath.RequestType.GetMethodName()));
|
||||||
if (restPath.Path.IndexOfAny(InvalidRouteChars) != -1)
|
if (restPath.Path.IndexOfAny(InvalidRouteChars) != -1)
|
||||||
throw new ArgumentException(string.Format("Route '{0}' on '{1}' contains invalid chars. " +
|
throw new ArgumentException(string.Format("Route '{0}' on '{1}' contains invalid chars. " +
|
||||||
"See https://github.com/ServiceStack/ServiceStack/wiki/Routing for info on valid routes.", restPath.Path, restPath.RequestType.GetOperationName()));
|
"See https://github.com/ServiceStack/ServiceStack/wiki/Routing for info on valid routes.", restPath.Path, restPath.RequestType.GetMethodName()));
|
||||||
|
|
||||||
List<RestPath> pathsAtFirstMatch;
|
List<RestPath> pathsAtFirstMatch;
|
||||||
if (!RestPathMap.TryGetValue(restPath.FirstMatchHashKey, out pathsAtFirstMatch))
|
if (!RestPathMap.TryGetValue(restPath.FirstMatchHashKey, out pathsAtFirstMatch))
|
||||||
|
@ -120,22 +132,7 @@ namespace ServiceStack.Host
|
||||||
pathsAtFirstMatch.Add(restPath);
|
pathsAtFirstMatch.Add(restPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AfterInit()
|
public RestPath GetRestPathForRequest(string httpMethod, string pathInfo, ILogger logger)
|
||||||
{
|
|
||||||
var appHost = ServiceStackHost.Instance;
|
|
||||||
|
|
||||||
//Register any routes configured on Metadata.Routes
|
|
||||||
foreach (var restPath in appHost.RestPaths)
|
|
||||||
{
|
|
||||||
RegisterRestPath(restPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Sync the RestPaths collections
|
|
||||||
appHost.RestPaths.Clear();
|
|
||||||
appHost.RestPaths.AddRange(RestPathMap.Values.SelectMany(x => x));
|
|
||||||
}
|
|
||||||
|
|
||||||
public RestPath GetRestPathForRequest(string httpMethod, string pathInfo)
|
|
||||||
{
|
{
|
||||||
var matchUsingPathParts = RestPath.GetPathPartsForMatching(pathInfo);
|
var matchUsingPathParts = RestPath.GetPathPartsForMatching(pathInfo);
|
||||||
|
|
||||||
|
@ -144,19 +141,23 @@ namespace ServiceStack.Host
|
||||||
var yieldedHashMatches = RestPath.GetFirstMatchHashKeys(matchUsingPathParts);
|
var yieldedHashMatches = RestPath.GetFirstMatchHashKeys(matchUsingPathParts);
|
||||||
foreach (var potentialHashMatch in yieldedHashMatches)
|
foreach (var potentialHashMatch in yieldedHashMatches)
|
||||||
{
|
{
|
||||||
if (!this.RestPathMap.TryGetValue(potentialHashMatch, out firstMatches)) continue;
|
if (!this.RestPathMap.TryGetValue(potentialHashMatch, out firstMatches))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var bestScore = -1;
|
var bestScore = -1;
|
||||||
foreach (var restPath in firstMatches)
|
foreach (var restPath in firstMatches)
|
||||||
{
|
{
|
||||||
var score = restPath.MatchScore(httpMethod, matchUsingPathParts);
|
var score = restPath.MatchScore(httpMethod, matchUsingPathParts, logger);
|
||||||
if (score > bestScore) bestScore = score;
|
if (score > bestScore) bestScore = score;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bestScore > 0)
|
if (bestScore > 0)
|
||||||
{
|
{
|
||||||
foreach (var restPath in firstMatches)
|
foreach (var restPath in firstMatches)
|
||||||
{
|
{
|
||||||
if (bestScore == restPath.MatchScore(httpMethod, matchUsingPathParts))
|
if (bestScore == restPath.MatchScore(httpMethod, matchUsingPathParts, logger))
|
||||||
return restPath;
|
return restPath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -170,14 +171,14 @@ namespace ServiceStack.Host
|
||||||
var bestScore = -1;
|
var bestScore = -1;
|
||||||
foreach (var restPath in firstMatches)
|
foreach (var restPath in firstMatches)
|
||||||
{
|
{
|
||||||
var score = restPath.MatchScore(httpMethod, matchUsingPathParts);
|
var score = restPath.MatchScore(httpMethod, matchUsingPathParts, logger);
|
||||||
if (score > bestScore) bestScore = score;
|
if (score > bestScore) bestScore = score;
|
||||||
}
|
}
|
||||||
if (bestScore > 0)
|
if (bestScore > 0)
|
||||||
{
|
{
|
||||||
foreach (var restPath in firstMatches)
|
foreach (var restPath in firstMatches)
|
||||||
{
|
{
|
||||||
if (bestScore == restPath.MatchScore(httpMethod, matchUsingPathParts))
|
if (bestScore == restPath.MatchScore(httpMethod, matchUsingPathParts, logger))
|
||||||
return restPath;
|
return restPath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -186,15 +187,15 @@ namespace ServiceStack.Host
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<object> Execute(object requestDto, IRequest req)
|
public async Task<object> Execute(HttpListenerHost appHost, object requestDto, IRequest req)
|
||||||
{
|
{
|
||||||
req.Dto = requestDto;
|
req.Dto = requestDto;
|
||||||
var requestType = requestDto.GetType();
|
var requestType = requestDto.GetType();
|
||||||
req.OperationName = requestType.Name;
|
req.OperationName = requestType.Name;
|
||||||
|
|
||||||
var serviceType = ServiceStackHost.Instance.Metadata.GetServiceTypeByRequest(requestType);
|
var serviceType = appHost.GetServiceTypeByRequest(requestType);
|
||||||
|
|
||||||
var service = ServiceStackHost.Instance.CreateInstance(serviceType);
|
var service = appHost.CreateInstance(serviceType);
|
||||||
|
|
||||||
//var service = typeFactory.CreateInstance(serviceType);
|
//var service = typeFactory.CreateInstance(serviceType);
|
||||||
|
|
||||||
|
@ -208,7 +209,7 @@ namespace ServiceStack.Host
|
||||||
req.Dto = requestDto;
|
req.Dto = requestDto;
|
||||||
|
|
||||||
//Executes the service and returns the result
|
//Executes the service and returns the result
|
||||||
var response = await ServiceExecGeneral.Execute(serviceType, req, service, requestDto, requestType.GetOperationName()).ConfigureAwait(false);
|
var response = await ServiceExecGeneral.Execute(serviceType, req, service, requestDto, requestType.GetMethodName()).ConfigureAwait(false);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
|
@ -1,6 +1,3 @@
|
||||||
//Copyright (c) Service Stack LLC. All Rights Reserved.
|
|
||||||
//License: https://raw.github.com/ServiceStack/ServiceStack/master/license.txt
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
@ -9,10 +6,23 @@ using System.Reflection;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Model.Services;
|
using MediaBrowser.Model.Services;
|
||||||
|
|
||||||
namespace ServiceStack.Host
|
namespace Emby.Server.Implementations.Services
|
||||||
{
|
{
|
||||||
public static class ServiceExecExtensions
|
public static class ServiceExecExtensions
|
||||||
{
|
{
|
||||||
|
public static HashSet<string> AllVerbs = new HashSet<string>(new[] {
|
||||||
|
"OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "CONNECT", // RFC 2616
|
||||||
|
"PROPFIND", "PROPPATCH", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", // RFC 2518
|
||||||
|
"VERSION-CONTROL", "REPORT", "CHECKOUT", "CHECKIN", "UNCHECKOUT",
|
||||||
|
"MKWORKSPACE", "UPDATE", "LABEL", "MERGE", "BASELINE-CONTROL", "MKACTIVITY", // RFC 3253
|
||||||
|
"ORDERPATCH", // RFC 3648
|
||||||
|
"ACL", // RFC 3744
|
||||||
|
"PATCH", // https://datatracker.ietf.org/doc/draft-dusseault-http-patch/
|
||||||
|
"SEARCH", // https://datatracker.ietf.org/doc/draft-reschke-webdav-search/
|
||||||
|
"BCOPY", "BDELETE", "BMOVE", "BPROPFIND", "BPROPPATCH", "NOTIFY",
|
||||||
|
"POLL", "SUBSCRIBE", "UNSUBSCRIBE"
|
||||||
|
});
|
||||||
|
|
||||||
public static IEnumerable<MethodInfo> GetActions(this Type serviceType)
|
public static IEnumerable<MethodInfo> GetActions(this Type serviceType)
|
||||||
{
|
{
|
||||||
foreach (var mi in serviceType.GetRuntimeMethods().Where(i => i.IsPublic && !i.IsStatic))
|
foreach (var mi in serviceType.GetRuntimeMethods().Where(i => i.IsPublic && !i.IsStatic))
|
||||||
|
@ -20,8 +30,8 @@ namespace ServiceStack.Host
|
||||||
if (mi.GetParameters().Length != 1)
|
if (mi.GetParameters().Length != 1)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var actionName = mi.Name.ToUpper();
|
var actionName = mi.Name;
|
||||||
if (!HttpMethods.AllVerbs.Contains(actionName) && actionName != ActionContext.AnyAction)
|
if (!AllVerbs.Contains(actionName, StringComparer.OrdinalIgnoreCase) && !string.Equals(actionName, ServiceMethod.AnyAction, StringComparison.OrdinalIgnoreCase))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
yield return mi;
|
yield return mi;
|
||||||
|
@ -31,9 +41,9 @@ namespace ServiceStack.Host
|
||||||
|
|
||||||
internal static class ServiceExecGeneral
|
internal static class ServiceExecGeneral
|
||||||
{
|
{
|
||||||
public static Dictionary<string, ActionContext> execMap = new Dictionary<string, ActionContext>();
|
public static Dictionary<string, ServiceMethod> execMap = new Dictionary<string, ServiceMethod>();
|
||||||
|
|
||||||
public static void CreateServiceRunnersFor(Type requestType, List<ActionContext> actions)
|
public static void CreateServiceRunnersFor(Type requestType, List<ServiceMethod> actions)
|
||||||
{
|
{
|
||||||
foreach (var actionCtx in actions)
|
foreach (var actionCtx in actions)
|
||||||
{
|
{
|
||||||
|
@ -45,12 +55,11 @@ namespace ServiceStack.Host
|
||||||
|
|
||||||
public static async Task<object> Execute(Type serviceType, IRequest request, object instance, object requestDto, string requestName)
|
public static async Task<object> Execute(Type serviceType, IRequest request, object instance, object requestDto, string requestName)
|
||||||
{
|
{
|
||||||
var actionName = request.Verb
|
var actionName = request.Verb ?? "POST";
|
||||||
?? HttpMethods.Post; //MQ Services
|
|
||||||
|
|
||||||
ActionContext actionContext;
|
ServiceMethod actionContext;
|
||||||
if (ServiceExecGeneral.execMap.TryGetValue(ActionContext.Key(serviceType, actionName, requestName), out actionContext)
|
if (ServiceExecGeneral.execMap.TryGetValue(ServiceMethod.Key(serviceType, actionName, requestName), out actionContext)
|
||||||
|| ServiceExecGeneral.execMap.TryGetValue(ActionContext.AnyKey(serviceType, requestName), out actionContext))
|
|| ServiceExecGeneral.execMap.TryGetValue(ServiceMethod.AnyKey(serviceType, requestName), out actionContext))
|
||||||
{
|
{
|
||||||
if (actionContext.RequestFilters != null)
|
if (actionContext.RequestFilters != null)
|
||||||
{
|
{
|
||||||
|
@ -67,29 +76,29 @@ namespace ServiceStack.Host
|
||||||
if (taskResponse != null)
|
if (taskResponse != null)
|
||||||
{
|
{
|
||||||
await taskResponse.ConfigureAwait(false);
|
await taskResponse.ConfigureAwait(false);
|
||||||
response = ServiceStackHost.Instance.GetTaskResult(taskResponse, requestName);
|
response = ServiceHandler.GetTaskResult(taskResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
var expectedMethodName = actionName.Substring(0, 1) + actionName.Substring(1).ToLower();
|
var expectedMethodName = actionName.Substring(0, 1) + actionName.Substring(1).ToLower();
|
||||||
throw new NotImplementedException(string.Format("Could not find method named {1}({0}) or Any({0}) on Service {2}", requestDto.GetType().GetOperationName(), expectedMethodName, serviceType.GetOperationName()));
|
throw new NotImplementedException(string.Format("Could not find method named {1}({0}) or Any({0}) on Service {2}", requestDto.GetType().GetMethodName(), expectedMethodName, serviceType.GetMethodName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<ActionContext> Reset(Type serviceType)
|
public static List<ServiceMethod> Reset(Type serviceType)
|
||||||
{
|
{
|
||||||
var actions = new List<ActionContext>();
|
var actions = new List<ServiceMethod>();
|
||||||
|
|
||||||
foreach (var mi in serviceType.GetActions())
|
foreach (var mi in serviceType.GetActions())
|
||||||
{
|
{
|
||||||
var actionName = mi.Name.ToUpper();
|
var actionName = mi.Name;
|
||||||
var args = mi.GetParameters();
|
var args = mi.GetParameters();
|
||||||
|
|
||||||
var requestType = args[0].ParameterType;
|
var requestType = args[0].ParameterType;
|
||||||
var actionCtx = new ActionContext
|
var actionCtx = new ServiceMethod
|
||||||
{
|
{
|
||||||
Id = ActionContext.Key(serviceType, actionName, requestType.GetOperationName())
|
Id = ServiceMethod.Key(serviceType, actionName, requestType.GetMethodName())
|
||||||
};
|
};
|
||||||
|
|
||||||
try
|
try
|
296
Emby.Server.Implementations/Services/ServiceHandler.cs
Normal file
296
Emby.Server.Implementations/Services/ServiceHandler.cs
Normal file
|
@ -0,0 +1,296 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Emby.Server.Implementations.HttpServer;
|
||||||
|
using MediaBrowser.Model.Logging;
|
||||||
|
using MediaBrowser.Model.Services;
|
||||||
|
|
||||||
|
namespace Emby.Server.Implementations.Services
|
||||||
|
{
|
||||||
|
public class ServiceHandler
|
||||||
|
{
|
||||||
|
public async Task<object> HandleResponseAsync(object response)
|
||||||
|
{
|
||||||
|
var taskResponse = response as Task;
|
||||||
|
|
||||||
|
if (taskResponse == null)
|
||||||
|
{
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
await taskResponse.ConfigureAwait(false);
|
||||||
|
|
||||||
|
var taskResult = GetTaskResult(taskResponse);
|
||||||
|
|
||||||
|
var subTask = taskResult as Task;
|
||||||
|
if (subTask != null)
|
||||||
|
{
|
||||||
|
taskResult = GetTaskResult(subTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
return taskResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static object GetTaskResult(Task task)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var taskObject = task as Task<object>;
|
||||||
|
if (taskObject != null)
|
||||||
|
{
|
||||||
|
return taskObject.Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
task.Wait();
|
||||||
|
|
||||||
|
var type = task.GetType().GetTypeInfo();
|
||||||
|
if (!type.IsGenericType)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return type.GetDeclaredProperty("Result").GetValue(task);
|
||||||
|
}
|
||||||
|
catch (TypeAccessException)
|
||||||
|
{
|
||||||
|
return null; //return null for void Task's
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static object CreateContentTypeRequest(HttpListenerHost host, IRequest httpReq, Type requestType, string contentType)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(contentType) && httpReq.ContentLength > 0)
|
||||||
|
{
|
||||||
|
var deserializer = RequestHelper.GetRequestReader(host, contentType);
|
||||||
|
if (deserializer != null)
|
||||||
|
{
|
||||||
|
return deserializer(requestType, httpReq.InputStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return host.CreateInstance(requestType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RestPath FindMatchingRestPath(string httpMethod, string pathInfo, ILogger logger, out string contentType)
|
||||||
|
{
|
||||||
|
pathInfo = GetSanitizedPathInfo(pathInfo, out contentType);
|
||||||
|
|
||||||
|
return ServiceController.Instance.GetRestPathForRequest(httpMethod, pathInfo, logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetSanitizedPathInfo(string pathInfo, out string contentType)
|
||||||
|
{
|
||||||
|
contentType = null;
|
||||||
|
var pos = pathInfo.LastIndexOf('.');
|
||||||
|
if (pos >= 0)
|
||||||
|
{
|
||||||
|
var format = pathInfo.Substring(pos + 1);
|
||||||
|
contentType = GetFormatContentType(format);
|
||||||
|
if (contentType != null)
|
||||||
|
{
|
||||||
|
pathInfo = pathInfo.Substring(0, pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pathInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetFormatContentType(string format)
|
||||||
|
{
|
||||||
|
//built-in formats
|
||||||
|
if (format == "json")
|
||||||
|
return "application/json";
|
||||||
|
if (format == "xml")
|
||||||
|
return "application/xml";
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RestPath GetRestPath(string httpMethod, string pathInfo)
|
||||||
|
{
|
||||||
|
if (this.RestPath == null)
|
||||||
|
{
|
||||||
|
string contentType;
|
||||||
|
this.RestPath = FindMatchingRestPath(httpMethod, pathInfo, new NullLogger(), out contentType);
|
||||||
|
|
||||||
|
if (contentType != null)
|
||||||
|
ResponseContentType = contentType;
|
||||||
|
}
|
||||||
|
return this.RestPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RestPath RestPath { get; set; }
|
||||||
|
|
||||||
|
// Set from SSHHF.GetHandlerForPathInfo()
|
||||||
|
public string ResponseContentType { get; set; }
|
||||||
|
|
||||||
|
public async Task ProcessRequestAsync(HttpListenerHost appHost, IRequest httpReq, IResponse httpRes, ILogger logger, string operationName)
|
||||||
|
{
|
||||||
|
var restPath = GetRestPath(httpReq.Verb, httpReq.PathInfo);
|
||||||
|
if (restPath == null)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException("No RestPath found for: " + httpReq.Verb + " " + httpReq.PathInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
SetRoute(httpReq, restPath);
|
||||||
|
|
||||||
|
if (ResponseContentType != null)
|
||||||
|
httpReq.ResponseContentType = ResponseContentType;
|
||||||
|
|
||||||
|
var request = httpReq.Dto = CreateRequest(appHost, httpReq, restPath, logger);
|
||||||
|
|
||||||
|
appHost.ApplyRequestFilters(httpReq, httpRes, request);
|
||||||
|
|
||||||
|
var rawResponse = await appHost.ServiceController.Execute(appHost, request, httpReq).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var response = await HandleResponseAsync(rawResponse).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Apply response filters
|
||||||
|
foreach (var responseFilter in appHost.ResponseFilters)
|
||||||
|
{
|
||||||
|
responseFilter(httpReq, httpRes, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
await ResponseHelper.WriteToResponse(httpRes, httpReq, response).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static object CreateRequest(HttpListenerHost host, IRequest httpReq, RestPath restPath, ILogger logger)
|
||||||
|
{
|
||||||
|
var requestType = restPath.RequestType;
|
||||||
|
|
||||||
|
if (RequireqRequestStream(requestType))
|
||||||
|
{
|
||||||
|
// Used by IRequiresRequestStream
|
||||||
|
return CreateRequiresRequestStreamRequest(host, httpReq, requestType);
|
||||||
|
}
|
||||||
|
|
||||||
|
var requestParams = GetFlattenedRequestParams(httpReq);
|
||||||
|
return CreateRequest(host, httpReq, restPath, requestParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool RequireqRequestStream(Type requestType)
|
||||||
|
{
|
||||||
|
var requiresRequestStreamTypeInfo = typeof(IRequiresRequestStream).GetTypeInfo();
|
||||||
|
|
||||||
|
return requiresRequestStreamTypeInfo.IsAssignableFrom(requestType.GetTypeInfo());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IRequiresRequestStream CreateRequiresRequestStreamRequest(HttpListenerHost host, IRequest req, Type requestType)
|
||||||
|
{
|
||||||
|
var restPath = GetRoute(req);
|
||||||
|
var request = ServiceHandler.CreateRequest(req, restPath, GetRequestParams(req), host.CreateInstance(requestType));
|
||||||
|
|
||||||
|
var rawReq = (IRequiresRequestStream)request;
|
||||||
|
rawReq.RequestStream = req.InputStream;
|
||||||
|
return rawReq;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static object CreateRequest(HttpListenerHost host, IRequest httpReq, RestPath restPath, Dictionary<string, string> requestParams)
|
||||||
|
{
|
||||||
|
var requestDto = CreateContentTypeRequest(host, httpReq, restPath.RequestType, httpReq.ContentType);
|
||||||
|
|
||||||
|
return CreateRequest(httpReq, restPath, requestParams, requestDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static object CreateRequest(IRequest httpReq, RestPath restPath, Dictionary<string, string> requestParams, object requestDto)
|
||||||
|
{
|
||||||
|
string contentType;
|
||||||
|
var pathInfo = !restPath.IsWildCardPath
|
||||||
|
? GetSanitizedPathInfo(httpReq.PathInfo, out contentType)
|
||||||
|
: httpReq.PathInfo;
|
||||||
|
|
||||||
|
return restPath.CreateRequest(pathInfo, requestParams, requestDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Duplicate Params are given a unique key by appending a #1 suffix
|
||||||
|
/// </summary>
|
||||||
|
private static Dictionary<string, string> GetRequestParams(IRequest request)
|
||||||
|
{
|
||||||
|
var map = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
foreach (var name in request.QueryString.Keys)
|
||||||
|
{
|
||||||
|
if (name == null) continue; //thank you ASP.NET
|
||||||
|
|
||||||
|
var values = request.QueryString.GetValues(name);
|
||||||
|
if (values.Length == 1)
|
||||||
|
{
|
||||||
|
map[name] = values[0];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (var i = 0; i < values.Length; i++)
|
||||||
|
{
|
||||||
|
map[name + (i == 0 ? "" : "#" + i)] = values[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((IsMethod(request.Verb, "POST") || IsMethod(request.Verb, "PUT")) && request.FormData != null)
|
||||||
|
{
|
||||||
|
foreach (var name in request.FormData.Keys)
|
||||||
|
{
|
||||||
|
if (name == null) continue; //thank you ASP.NET
|
||||||
|
|
||||||
|
var values = request.FormData.GetValues(name);
|
||||||
|
if (values.Length == 1)
|
||||||
|
{
|
||||||
|
map[name] = values[0];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (var i = 0; i < values.Length; i++)
|
||||||
|
{
|
||||||
|
map[name + (i == 0 ? "" : "#" + i)] = values[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsMethod(string method, string expected)
|
||||||
|
{
|
||||||
|
return string.Equals(method, expected, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Duplicate params have their values joined together in a comma-delimited string
|
||||||
|
/// </summary>
|
||||||
|
private static Dictionary<string, string> GetFlattenedRequestParams(IRequest request)
|
||||||
|
{
|
||||||
|
var map = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
foreach (var name in request.QueryString.Keys)
|
||||||
|
{
|
||||||
|
if (name == null) continue; //thank you ASP.NET
|
||||||
|
map[name] = request.QueryString[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((IsMethod(request.Verb, "POST") || IsMethod(request.Verb, "PUT")) && request.FormData != null)
|
||||||
|
{
|
||||||
|
foreach (var name in request.FormData.Keys)
|
||||||
|
{
|
||||||
|
if (name == null) continue; //thank you ASP.NET
|
||||||
|
map[name] = request.FormData[name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetRoute(IRequest req, RestPath route)
|
||||||
|
{
|
||||||
|
req.Items["__route"] = route;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RestPath GetRoute(IRequest req)
|
||||||
|
{
|
||||||
|
object route;
|
||||||
|
req.Items.TryGetValue("__route", out route);
|
||||||
|
return route as RestPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,11 +1,8 @@
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace ServiceStack.Host
|
namespace Emby.Server.Implementations.Services
|
||||||
{
|
{
|
||||||
/// <summary>
|
public class ServiceMethod
|
||||||
/// Context to capture IService action
|
|
||||||
/// </summary>
|
|
||||||
public class ActionContext
|
|
||||||
{
|
{
|
||||||
public const string AnyAction = "ANY";
|
public const string AnyAction = "ANY";
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using ServiceStack.Serialization;
|
using MediaBrowser.Model.Logging;
|
||||||
|
|
||||||
namespace ServiceStack.Host
|
namespace Emby.Server.Implementations.Services
|
||||||
{
|
{
|
||||||
public class RestPath
|
public class RestPath
|
||||||
{
|
{
|
||||||
|
@ -49,7 +50,7 @@ namespace ServiceStack.Host
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
return allowsAllVerbs
|
return allowsAllVerbs
|
||||||
? new[] { ActionContext.AnyAction }
|
? new[] { "ANY" }
|
||||||
: AllowedVerbs.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
: AllowedVerbs.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,45 +72,50 @@ namespace ServiceStack.Host
|
||||||
public static string[] GetPathPartsForMatching(string pathInfo)
|
public static string[] GetPathPartsForMatching(string pathInfo)
|
||||||
{
|
{
|
||||||
var parts = pathInfo.ToLower().Split(PathSeperatorChar)
|
var parts = pathInfo.ToLower().Split(PathSeperatorChar)
|
||||||
.Where(x => !string.IsNullOrEmpty(x)).ToArray();
|
.Where(x => !String.IsNullOrEmpty(x)).ToArray();
|
||||||
return parts;
|
return parts;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IEnumerable<string> GetFirstMatchHashKeys(string[] pathPartsForMatching)
|
public static List<string> GetFirstMatchHashKeys(string[] pathPartsForMatching)
|
||||||
{
|
{
|
||||||
var hashPrefix = pathPartsForMatching.Length + PathSeperator;
|
var hashPrefix = pathPartsForMatching.Length + PathSeperator;
|
||||||
return GetPotentialMatchesWithPrefix(hashPrefix, pathPartsForMatching);
|
return GetPotentialMatchesWithPrefix(hashPrefix, pathPartsForMatching);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IEnumerable<string> GetFirstMatchWildCardHashKeys(string[] pathPartsForMatching)
|
public static List<string> GetFirstMatchWildCardHashKeys(string[] pathPartsForMatching)
|
||||||
{
|
{
|
||||||
const string hashPrefix = WildCard + PathSeperator;
|
const string hashPrefix = WildCard + PathSeperator;
|
||||||
return GetPotentialMatchesWithPrefix(hashPrefix, pathPartsForMatching);
|
return GetPotentialMatchesWithPrefix(hashPrefix, pathPartsForMatching);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IEnumerable<string> GetPotentialMatchesWithPrefix(string hashPrefix, string[] pathPartsForMatching)
|
private static List<string> GetPotentialMatchesWithPrefix(string hashPrefix, string[] pathPartsForMatching)
|
||||||
{
|
{
|
||||||
|
var list = new List<string>();
|
||||||
|
|
||||||
foreach (var part in pathPartsForMatching)
|
foreach (var part in pathPartsForMatching)
|
||||||
{
|
{
|
||||||
yield return hashPrefix + part;
|
list.Add(hashPrefix + part);
|
||||||
|
|
||||||
var subParts = part.Split(ComponentSeperator);
|
var subParts = part.Split(ComponentSeperator);
|
||||||
if (subParts.Length == 1) continue;
|
if (subParts.Length == 1) continue;
|
||||||
|
|
||||||
foreach (var subPart in subParts)
|
foreach (var subPart in subParts)
|
||||||
{
|
{
|
||||||
yield return hashPrefix + subPart;
|
list.Add(hashPrefix + subPart);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public RestPath(Type requestType, string path, string verbs, string summary = null, string notes = null)
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RestPath(Func<Type, object> createInstanceFn, Func<Type, Func<string, object>> getParseFn, Type requestType, string path, string verbs, string summary = null, string notes = null)
|
||||||
{
|
{
|
||||||
this.RequestType = requestType;
|
this.RequestType = requestType;
|
||||||
this.Summary = summary;
|
this.Summary = summary;
|
||||||
this.Notes = notes;
|
this.Notes = notes;
|
||||||
this.restPath = path;
|
this.restPath = path;
|
||||||
|
|
||||||
this.allowsAllVerbs = verbs == null || string.Equals(verbs, WildCard, StringComparison.OrdinalIgnoreCase);
|
this.allowsAllVerbs = verbs == null || String.Equals(verbs, WildCard, StringComparison.OrdinalIgnoreCase);
|
||||||
if (!this.allowsAllVerbs)
|
if (!this.allowsAllVerbs)
|
||||||
{
|
{
|
||||||
this.allowedVerbs = verbs.ToUpper();
|
this.allowedVerbs = verbs.ToUpper();
|
||||||
|
@ -121,7 +127,7 @@ namespace ServiceStack.Host
|
||||||
var hasSeparators = new List<bool>();
|
var hasSeparators = new List<bool>();
|
||||||
foreach (var component in this.restPath.Split(PathSeperatorChar))
|
foreach (var component in this.restPath.Split(PathSeperatorChar))
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(component)) continue;
|
if (String.IsNullOrEmpty(component)) continue;
|
||||||
|
|
||||||
if (StringContains(component, VariablePrefix)
|
if (StringContains(component, VariablePrefix)
|
||||||
&& component.IndexOf(ComponentSeperator) != -1)
|
&& component.IndexOf(ComponentSeperator) != -1)
|
||||||
|
@ -194,19 +200,95 @@ namespace ServiceStack.Host
|
||||||
this.IsValid = sbHashKey.Length > 0;
|
this.IsValid = sbHashKey.Length > 0;
|
||||||
this.UniqueMatchHashKey = sbHashKey.ToString();
|
this.UniqueMatchHashKey = sbHashKey.ToString();
|
||||||
|
|
||||||
this.typeDeserializer = new StringMapTypeDeserializer(this.RequestType);
|
this.typeDeserializer = new StringMapTypeDeserializer(createInstanceFn, getParseFn, this.RequestType);
|
||||||
RegisterCaseInsenstivePropertyNameMappings();
|
RegisterCaseInsenstivePropertyNameMappings();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RegisterCaseInsenstivePropertyNameMappings()
|
private void RegisterCaseInsenstivePropertyNameMappings()
|
||||||
{
|
{
|
||||||
foreach (var propertyInfo in RequestType.GetSerializableProperties())
|
foreach (var propertyInfo in GetSerializableProperties(RequestType))
|
||||||
{
|
{
|
||||||
var propertyName = propertyInfo.Name;
|
var propertyName = propertyInfo.Name;
|
||||||
propertyNamesMap.Add(propertyName.ToLower(), propertyName);
|
propertyNamesMap.Add(propertyName.ToLower(), propertyName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static string[] IgnoreAttributesNamed = new[] {
|
||||||
|
"IgnoreDataMemberAttribute",
|
||||||
|
"JsonIgnoreAttribute"
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
private static List<Type> _excludeTypes = new List<Type> { typeof(Stream) };
|
||||||
|
|
||||||
|
internal static PropertyInfo[] GetSerializableProperties(Type type)
|
||||||
|
{
|
||||||
|
var properties = GetPublicProperties(type);
|
||||||
|
var readableProperties = properties.Where(x => x.GetMethod != null);
|
||||||
|
|
||||||
|
// else return those properties that are not decorated with IgnoreDataMember
|
||||||
|
return readableProperties
|
||||||
|
.Where(prop => prop.GetCustomAttributes(true)
|
||||||
|
.All(attr =>
|
||||||
|
{
|
||||||
|
var name = attr.GetType().Name;
|
||||||
|
return !IgnoreAttributesNamed.Contains(name);
|
||||||
|
}))
|
||||||
|
.Where(prop => !_excludeTypes.Contains(prop.PropertyType))
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PropertyInfo[] GetPublicProperties(Type type)
|
||||||
|
{
|
||||||
|
if (type.GetTypeInfo().IsInterface)
|
||||||
|
{
|
||||||
|
var propertyInfos = new List<PropertyInfo>();
|
||||||
|
|
||||||
|
var considered = new List<Type>();
|
||||||
|
var queue = new Queue<Type>();
|
||||||
|
considered.Add(type);
|
||||||
|
queue.Enqueue(type);
|
||||||
|
|
||||||
|
while (queue.Count > 0)
|
||||||
|
{
|
||||||
|
var subType = queue.Dequeue();
|
||||||
|
foreach (var subInterface in subType.GetTypeInfo().ImplementedInterfaces)
|
||||||
|
{
|
||||||
|
if (considered.Contains(subInterface)) continue;
|
||||||
|
|
||||||
|
considered.Add(subInterface);
|
||||||
|
queue.Enqueue(subInterface);
|
||||||
|
}
|
||||||
|
|
||||||
|
var typeProperties = GetTypesPublicProperties(subType);
|
||||||
|
|
||||||
|
var newPropertyInfos = typeProperties
|
||||||
|
.Where(x => !propertyInfos.Contains(x));
|
||||||
|
|
||||||
|
propertyInfos.InsertRange(0, newPropertyInfos);
|
||||||
|
}
|
||||||
|
|
||||||
|
return propertyInfos.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetTypesPublicProperties(type)
|
||||||
|
.Where(t => t.GetIndexParameters().Length == 0) // ignore indexed properties
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static 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);
|
||||||
|
}
|
||||||
|
return pis.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public bool IsValid { get; set; }
|
public bool IsValid { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -220,16 +302,14 @@ namespace ServiceStack.Host
|
||||||
|
|
||||||
private readonly Dictionary<string, string> propertyNamesMap = new Dictionary<string, string>();
|
private readonly Dictionary<string, string> propertyNamesMap = new Dictionary<string, string>();
|
||||||
|
|
||||||
public static Func<RestPath, string, string[], int> CalculateMatchScore { get; set; }
|
public int MatchScore(string httpMethod, string[] withPathInfoParts, ILogger logger)
|
||||||
|
|
||||||
public int MatchScore(string httpMethod, string[] withPathInfoParts)
|
|
||||||
{
|
{
|
||||||
if (CalculateMatchScore != null)
|
|
||||||
return CalculateMatchScore(this, httpMethod, withPathInfoParts);
|
|
||||||
|
|
||||||
int wildcardMatchCount;
|
int wildcardMatchCount;
|
||||||
var isMatch = IsMatch(httpMethod, withPathInfoParts, out wildcardMatchCount);
|
var isMatch = IsMatch(httpMethod, withPathInfoParts, logger, out wildcardMatchCount);
|
||||||
if (!isMatch) return -1;
|
if (!isMatch)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
var score = 0;
|
var score = 0;
|
||||||
|
|
||||||
|
@ -240,7 +320,7 @@ namespace ServiceStack.Host
|
||||||
score += Math.Max((10 - VariableArgsCount), 1) * 100;
|
score += Math.Max((10 - VariableArgsCount), 1) * 100;
|
||||||
|
|
||||||
//Exact verb match is better than ANY
|
//Exact verb match is better than ANY
|
||||||
var exactVerb = string.Equals(httpMethod, AllowedVerbs, StringComparison.OrdinalIgnoreCase);
|
var exactVerb = String.Equals(httpMethod, AllowedVerbs, StringComparison.OrdinalIgnoreCase);
|
||||||
score += exactVerb ? 10 : 1;
|
score += exactVerb ? 10 : 1;
|
||||||
|
|
||||||
return score;
|
return score;
|
||||||
|
@ -255,19 +335,33 @@ namespace ServiceStack.Host
|
||||||
/// For performance withPathInfoParts should already be a lower case string
|
/// For performance withPathInfoParts should already be a lower case string
|
||||||
/// to minimize redundant matching operations.
|
/// to minimize redundant matching operations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="httpMethod"></param>
|
public bool IsMatch(string httpMethod, string[] withPathInfoParts, ILogger logger, out int wildcardMatchCount)
|
||||||
/// <param name="withPathInfoParts"></param>
|
|
||||||
/// <param name="wildcardMatchCount"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public bool IsMatch(string httpMethod, string[] withPathInfoParts, out int wildcardMatchCount)
|
|
||||||
{
|
{
|
||||||
wildcardMatchCount = 0;
|
wildcardMatchCount = 0;
|
||||||
|
|
||||||
if (withPathInfoParts.Length != this.PathComponentsCount && !this.IsWildCardPath) return false;
|
if (withPathInfoParts.Length != this.PathComponentsCount && !this.IsWildCardPath)
|
||||||
if (!this.allowsAllVerbs && !StringContains(this.allowedVerbs, httpMethod)) return false;
|
{
|
||||||
|
//logger.Info("withPathInfoParts mismatch for {0} for {1}", httpMethod, string.Join("/", withPathInfoParts));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (!ExplodeComponents(ref withPathInfoParts)) return false;
|
if (!this.allowsAllVerbs && !StringContains(this.allowedVerbs, httpMethod))
|
||||||
if (this.TotalComponentsCount != withPathInfoParts.Length && !this.IsWildCardPath) return false;
|
{
|
||||||
|
//logger.Info("allowsAllVerbs mismatch for {0} for {1} allowedverbs {2}", httpMethod, string.Join("/", withPathInfoParts), this.allowedVerbs);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ExplodeComponents(ref withPathInfoParts))
|
||||||
|
{
|
||||||
|
//logger.Info("ExplodeComponents mismatch for {0} for {1}", httpMethod, string.Join("/", withPathInfoParts));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.TotalComponentsCount != withPathInfoParts.Length && !this.IsWildCardPath)
|
||||||
|
{
|
||||||
|
//logger.Info("TotalComponentsCount mismatch for {0} for {1}", httpMethod, string.Join("/", withPathInfoParts));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
int pathIx = 0;
|
int pathIx = 0;
|
||||||
for (var i = 0; i < this.TotalComponentsCount; i++)
|
for (var i = 0; i < this.TotalComponentsCount; i++)
|
||||||
|
@ -277,7 +371,7 @@ namespace ServiceStack.Host
|
||||||
if (i < this.TotalComponentsCount - 1)
|
if (i < this.TotalComponentsCount - 1)
|
||||||
{
|
{
|
||||||
// Continue to consume up until a match with the next literal
|
// Continue to consume up until a match with the next literal
|
||||||
while (pathIx < withPathInfoParts.Length && withPathInfoParts[pathIx] != this.literalsToMatch[i + 1])
|
while (pathIx < withPathInfoParts.Length && !LiteralsEqual(withPathInfoParts[pathIx], this.literalsToMatch[i + 1]))
|
||||||
{
|
{
|
||||||
pathIx++;
|
pathIx++;
|
||||||
wildcardMatchCount++;
|
wildcardMatchCount++;
|
||||||
|
@ -286,6 +380,7 @@ namespace ServiceStack.Host
|
||||||
// Ensure there are still enough parts left to match the remainder
|
// Ensure there are still enough parts left to match the remainder
|
||||||
if ((withPathInfoParts.Length - pathIx) < (this.TotalComponentsCount - i - 1))
|
if ((withPathInfoParts.Length - pathIx) < (this.TotalComponentsCount - i - 1))
|
||||||
{
|
{
|
||||||
|
//logger.Info("withPathInfoParts length mismatch for {0} for {1}", httpMethod, string.Join("/", withPathInfoParts));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -306,7 +401,11 @@ namespace ServiceStack.Host
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (withPathInfoParts.Length <= pathIx || withPathInfoParts[pathIx] != literalToMatch) return false;
|
if (withPathInfoParts.Length <= pathIx || !LiteralsEqual(withPathInfoParts[pathIx], literalToMatch))
|
||||||
|
{
|
||||||
|
//logger.Info("withPathInfoParts2 length mismatch for {0} for {1}. not equals: {2} != {3}.", httpMethod, string.Join("/", withPathInfoParts), withPathInfoParts[pathIx], literalToMatch);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
pathIx++;
|
pathIx++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -314,13 +413,29 @@ namespace ServiceStack.Host
|
||||||
return pathIx == withPathInfoParts.Length;
|
return pathIx == withPathInfoParts.Length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private 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)
|
private bool ExplodeComponents(ref string[] withPathInfoParts)
|
||||||
{
|
{
|
||||||
var totalComponents = new List<string>();
|
var totalComponents = new List<string>();
|
||||||
for (var i = 0; i < withPathInfoParts.Length; i++)
|
for (var i = 0; i < withPathInfoParts.Length; i++)
|
||||||
{
|
{
|
||||||
var component = withPathInfoParts[i];
|
var component = withPathInfoParts[i];
|
||||||
if (string.IsNullOrEmpty(component)) continue;
|
if (String.IsNullOrEmpty(component)) continue;
|
||||||
|
|
||||||
if (this.PathComponentsCount != this.TotalComponentsCount
|
if (this.PathComponentsCount != this.TotalComponentsCount
|
||||||
&& this.componentsWithSeparators[i])
|
&& this.componentsWithSeparators[i])
|
||||||
|
@ -342,7 +457,7 @@ namespace ServiceStack.Host
|
||||||
public object CreateRequest(string pathInfo, Dictionary<string, string> queryStringAndFormData, object fromInstance)
|
public object CreateRequest(string pathInfo, Dictionary<string, string> queryStringAndFormData, object fromInstance)
|
||||||
{
|
{
|
||||||
var requestComponents = pathInfo.Split(PathSeperatorChar)
|
var requestComponents = pathInfo.Split(PathSeperatorChar)
|
||||||
.Where(x => !string.IsNullOrEmpty(x)).ToArray();
|
.Where(x => !String.IsNullOrEmpty(x)).ToArray();
|
||||||
|
|
||||||
ExplodeComponents(ref requestComponents);
|
ExplodeComponents(ref requestComponents);
|
||||||
|
|
||||||
|
@ -352,7 +467,7 @@ namespace ServiceStack.Host
|
||||||
&& requestComponents.Length >= this.TotalComponentsCount - this.wildcardCount;
|
&& requestComponents.Length >= this.TotalComponentsCount - this.wildcardCount;
|
||||||
|
|
||||||
if (!isValidWildCardPath)
|
if (!isValidWildCardPath)
|
||||||
throw new ArgumentException(string.Format(
|
throw new ArgumentException(String.Format(
|
||||||
"Path Mismatch: Request Path '{0}' has invalid number of components compared to: '{1}'",
|
"Path Mismatch: Request Path '{0}' has invalid number of components compared to: '{1}'",
|
||||||
pathInfo, this.restPath));
|
pathInfo, this.restPath));
|
||||||
}
|
}
|
||||||
|
@ -371,14 +486,14 @@ namespace ServiceStack.Host
|
||||||
string propertyNameOnRequest;
|
string propertyNameOnRequest;
|
||||||
if (!this.propertyNamesMap.TryGetValue(variableName.ToLower(), out propertyNameOnRequest))
|
if (!this.propertyNamesMap.TryGetValue(variableName.ToLower(), out propertyNameOnRequest))
|
||||||
{
|
{
|
||||||
if (string.Equals("ignore", variableName, StringComparison.OrdinalIgnoreCase))
|
if (String.Equals("ignore", variableName, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
pathIx++;
|
pathIx++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new ArgumentException("Could not find property "
|
throw new ArgumentException("Could not find property "
|
||||||
+ variableName + " on " + RequestType.GetOperationName());
|
+ variableName + " on " + RequestType.GetMethodName());
|
||||||
}
|
}
|
||||||
|
|
||||||
var value = requestComponents.Length > pathIx ? requestComponents[pathIx] : null; //wildcard has arg mismatch
|
var value = requestComponents.Length > pathIx ? requestComponents[pathIx] : null; //wildcard has arg mismatch
|
||||||
|
@ -401,12 +516,12 @@ namespace ServiceStack.Host
|
||||||
// hits a match for the next element in the definition (which must be a literal)
|
// hits a match for the next element in the definition (which must be a literal)
|
||||||
// It may consume 0 or more path parts
|
// It may consume 0 or more path parts
|
||||||
var stopLiteral = i == this.TotalComponentsCount - 1 ? null : this.literalsToMatch[i + 1];
|
var stopLiteral = i == this.TotalComponentsCount - 1 ? null : this.literalsToMatch[i + 1];
|
||||||
if (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
|
if (!String.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.Append(value);
|
sb.Append(value);
|
||||||
pathIx++;
|
pathIx++;
|
||||||
while (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
|
while (!String.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
sb.Append(PathSeperatorChar + requestComponents[pathIx++]);
|
sb.Append(PathSeperatorChar + requestComponents[pathIx++]);
|
||||||
}
|
}
|
|
@ -1,10 +1,9 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Runtime.Serialization;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
namespace ServiceStack.Serialization
|
namespace Emby.Server.Implementations.Services
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Serializer cache of delegates required to create a type from a string map (e.g. for REST urls)
|
/// Serializer cache of delegates required to create a type from a string map (e.g. for REST urls)
|
||||||
|
@ -30,29 +29,28 @@ namespace ServiceStack.Serialization
|
||||||
|
|
||||||
public Func<string, object> GetParseFn(Type propertyType)
|
public Func<string, object> GetParseFn(Type propertyType)
|
||||||
{
|
{
|
||||||
//Don't JSV-decode string values for string properties
|
|
||||||
if (propertyType == typeof(string))
|
if (propertyType == typeof(string))
|
||||||
return s => s;
|
return s => s;
|
||||||
|
|
||||||
return ServiceStackHost.Instance.GetParseFn(propertyType);
|
return _GetParseFn(propertyType);
|
||||||
}
|
}
|
||||||
|
|
||||||
public StringMapTypeDeserializer(Type type)
|
private readonly Func<Type, object> _CreateInstanceFn;
|
||||||
|
private readonly Func<Type, Func<string, object>> _GetParseFn;
|
||||||
|
|
||||||
|
public StringMapTypeDeserializer(Func<Type, object> createInstanceFn, Func<Type, Func<string, object>> getParseFn, Type type)
|
||||||
{
|
{
|
||||||
|
_CreateInstanceFn = createInstanceFn;
|
||||||
|
_GetParseFn = getParseFn;
|
||||||
this.type = type;
|
this.type = type;
|
||||||
|
|
||||||
foreach (var propertyInfo in type.GetSerializableProperties())
|
foreach (var propertyInfo in RestPath.GetSerializableProperties(type))
|
||||||
{
|
{
|
||||||
var propertySetFn = TypeAccessor.GetSetPropertyMethod(type, propertyInfo);
|
var propertySetFn = TypeAccessor.GetSetPropertyMethod(type, propertyInfo);
|
||||||
var propertyType = propertyInfo.PropertyType;
|
var propertyType = propertyInfo.PropertyType;
|
||||||
var propertyParseStringFn = GetParseFn(propertyType);
|
var propertyParseStringFn = GetParseFn(propertyType);
|
||||||
var propertySerializer = new PropertySerializerEntry(propertySetFn, propertyParseStringFn) { PropertyType = propertyType };
|
var propertySerializer = new PropertySerializerEntry(propertySetFn, propertyParseStringFn) { PropertyType = propertyType };
|
||||||
|
|
||||||
var attr = propertyInfo.AllAttributes<DataMemberAttribute>().FirstOrDefault();
|
|
||||||
if (attr != null && attr.Name != null)
|
|
||||||
{
|
|
||||||
propertySetterMap[attr.Name] = propertySerializer;
|
|
||||||
}
|
|
||||||
propertySetterMap[propertyInfo.Name] = propertySerializer;
|
propertySetterMap[propertyInfo.Name] = propertySerializer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -64,7 +62,7 @@ namespace ServiceStack.Serialization
|
||||||
PropertySerializerEntry propertySerializerEntry = null;
|
PropertySerializerEntry propertySerializerEntry = null;
|
||||||
|
|
||||||
if (instance == null)
|
if (instance == null)
|
||||||
instance = ServiceStackHost.Instance.CreateInstance(type);
|
instance = _CreateInstanceFn(type);
|
||||||
|
|
||||||
foreach (var pair in keyValuePairs.Where(x => !string.IsNullOrEmpty(x.Value)))
|
foreach (var pair in keyValuePairs.Where(x => !string.IsNullOrEmpty(x.Value)))
|
||||||
{
|
{
|
|
@ -1,6 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace ServiceStack
|
namespace Emby.Server.Implementations.Services
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Donated by Ivan Korneliuk from his post:
|
/// Donated by Ivan Korneliuk from his post:
|
||||||
|
@ -10,7 +10,7 @@ namespace ServiceStack
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class UrlExtensions
|
public static class UrlExtensions
|
||||||
{
|
{
|
||||||
public static string GetOperationName(this Type type)
|
public static string GetMethodName(this Type type)
|
||||||
{
|
{
|
||||||
var typeName = type.FullName != null //can be null, e.g. generic types
|
var typeName = type.FullName != null //can be null, e.g. generic types
|
||||||
? LeftPart(type.FullName, "[[") //Generic Fullname
|
? LeftPart(type.FullName, "[[") //Generic Fullname
|
|
@ -954,8 +954,11 @@ namespace Emby.Server.Implementations.Session
|
||||||
{
|
{
|
||||||
var session = GetSessionToRemoteControl(sessionId);
|
var session = GetSessionToRemoteControl(sessionId);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(controllingSessionId))
|
||||||
|
{
|
||||||
var controllingSession = GetSession(controllingSessionId);
|
var controllingSession = GetSession(controllingSessionId);
|
||||||
AssertCanControl(session, controllingSession);
|
AssertCanControl(session, controllingSession);
|
||||||
|
}
|
||||||
|
|
||||||
return session.SessionController.SendGeneralCommand(command, cancellationToken);
|
return session.SessionController.SendGeneralCommand(command, cancellationToken);
|
||||||
}
|
}
|
||||||
|
@ -1042,12 +1045,15 @@ namespace Emby.Server.Implementations.Session
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(controllingSessionId))
|
||||||
|
{
|
||||||
var controllingSession = GetSession(controllingSessionId);
|
var controllingSession = GetSession(controllingSessionId);
|
||||||
AssertCanControl(session, controllingSession);
|
AssertCanControl(session, controllingSession);
|
||||||
if (controllingSession.UserId.HasValue)
|
if (controllingSession.UserId.HasValue)
|
||||||
{
|
{
|
||||||
command.ControllingUserId = controllingSession.UserId.Value.ToString("N");
|
command.ControllingUserId = controllingSession.UserId.Value.ToString("N");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await session.SessionController.SendPlayCommand(command, cancellationToken).ConfigureAwait(false);
|
await session.SessionController.SendPlayCommand(command, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
@ -1136,12 +1142,15 @@ namespace Emby.Server.Implementations.Session
|
||||||
{
|
{
|
||||||
var session = GetSessionToRemoteControl(sessionId);
|
var session = GetSessionToRemoteControl(sessionId);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(controllingSessionId))
|
||||||
|
{
|
||||||
var controllingSession = GetSession(controllingSessionId);
|
var controllingSession = GetSession(controllingSessionId);
|
||||||
AssertCanControl(session, controllingSession);
|
AssertCanControl(session, controllingSession);
|
||||||
if (controllingSession.UserId.HasValue)
|
if (controllingSession.UserId.HasValue)
|
||||||
{
|
{
|
||||||
command.ControllingUserId = controllingSession.UserId.Value.ToString("N");
|
command.ControllingUserId = controllingSession.UserId.Value.ToString("N");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return session.SessionController.SendPlaystateCommand(command, cancellationToken);
|
return session.SessionController.SendPlaystateCommand(command, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,118 +0,0 @@
|
||||||
using MediaBrowser.Controller.Devices;
|
|
||||||
using MediaBrowser.Controller.Sync;
|
|
||||||
using MediaBrowser.Model.Devices;
|
|
||||||
using MediaBrowser.Model.Dlna;
|
|
||||||
using MediaBrowser.Model.Sync;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Sync
|
|
||||||
{
|
|
||||||
public class AppSyncProvider : ISyncProvider, IHasUniqueTargetIds, IHasSyncQuality, IHasDuplicateCheck
|
|
||||||
{
|
|
||||||
private readonly IDeviceManager _deviceManager;
|
|
||||||
|
|
||||||
public AppSyncProvider(IDeviceManager deviceManager)
|
|
||||||
{
|
|
||||||
_deviceManager = deviceManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<SyncTarget> GetSyncTargets(string userId)
|
|
||||||
{
|
|
||||||
return _deviceManager.GetDevices(new DeviceQuery
|
|
||||||
{
|
|
||||||
SupportsSync = true,
|
|
||||||
UserId = userId
|
|
||||||
|
|
||||||
}).Items.Select(i => new SyncTarget
|
|
||||||
{
|
|
||||||
Id = i.Id,
|
|
||||||
Name = i.Name
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public DeviceProfile GetDeviceProfile(SyncTarget target, string profile, string quality)
|
|
||||||
{
|
|
||||||
var caps = _deviceManager.GetCapabilities(target.Id);
|
|
||||||
|
|
||||||
var deviceProfile = caps == null || caps.DeviceProfile == null ? new DeviceProfile() : caps.DeviceProfile;
|
|
||||||
deviceProfile.MaxStaticBitrate = SyncHelper.AdjustBitrate(deviceProfile.MaxStaticBitrate, quality);
|
|
||||||
|
|
||||||
return deviceProfile;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Name
|
|
||||||
{
|
|
||||||
get { return "Mobile Sync"; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<SyncTarget> GetAllSyncTargets()
|
|
||||||
{
|
|
||||||
return _deviceManager.GetDevices(new DeviceQuery
|
|
||||||
{
|
|
||||||
SupportsSync = true
|
|
||||||
|
|
||||||
}).Items.Select(i => new SyncTarget
|
|
||||||
{
|
|
||||||
Id = i.Id,
|
|
||||||
Name = i.Name
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<SyncQualityOption> GetQualityOptions(SyncTarget target)
|
|
||||||
{
|
|
||||||
return new List<SyncQualityOption>
|
|
||||||
{
|
|
||||||
new SyncQualityOption
|
|
||||||
{
|
|
||||||
Name = "Original",
|
|
||||||
Id = "original",
|
|
||||||
Description = "Syncs original files as-is, regardless of whether the device is capable of playing them or not."
|
|
||||||
},
|
|
||||||
new SyncQualityOption
|
|
||||||
{
|
|
||||||
Name = "High",
|
|
||||||
Id = "high",
|
|
||||||
IsDefault = true
|
|
||||||
},
|
|
||||||
new SyncQualityOption
|
|
||||||
{
|
|
||||||
Name = "Medium",
|
|
||||||
Id = "medium"
|
|
||||||
},
|
|
||||||
new SyncQualityOption
|
|
||||||
{
|
|
||||||
Name = "Low",
|
|
||||||
Id = "low"
|
|
||||||
},
|
|
||||||
new SyncQualityOption
|
|
||||||
{
|
|
||||||
Name = "Custom",
|
|
||||||
Id = "custom"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<SyncProfileOption> GetProfileOptions(SyncTarget target)
|
|
||||||
{
|
|
||||||
return new List<SyncProfileOption>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public SyncJobOptions GetSyncJobOptions(SyncTarget target, string profile, string quality)
|
|
||||||
{
|
|
||||||
var isConverting = !string.Equals(quality, "original", StringComparison.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
return new SyncJobOptions
|
|
||||||
{
|
|
||||||
DeviceProfile = GetDeviceProfile(target, profile, quality),
|
|
||||||
IsConverting = isConverting
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool AllowDuplicateJobItem(SyncJobItem original, SyncJobItem duplicate)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,288 +0,0 @@
|
||||||
using MediaBrowser.Model.Dlna;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Sync
|
|
||||||
{
|
|
||||||
public class CloudSyncProfile : DeviceProfile
|
|
||||||
{
|
|
||||||
public CloudSyncProfile(bool supportsAc3, bool supportsDca)
|
|
||||||
{
|
|
||||||
Name = "Cloud Sync";
|
|
||||||
|
|
||||||
MaxStreamingBitrate = 20000000;
|
|
||||||
MaxStaticBitrate = 20000000;
|
|
||||||
|
|
||||||
var mkvAudio = "aac,mp3";
|
|
||||||
var mp4Audio = "aac";
|
|
||||||
|
|
||||||
if (supportsAc3)
|
|
||||||
{
|
|
||||||
mkvAudio += ",ac3";
|
|
||||||
mp4Audio += ",ac3";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (supportsDca)
|
|
||||||
{
|
|
||||||
mkvAudio += ",dca,dts";
|
|
||||||
}
|
|
||||||
|
|
||||||
var videoProfile = "high|main|baseline|constrained baseline";
|
|
||||||
var videoLevel = "40";
|
|
||||||
|
|
||||||
DirectPlayProfiles = new[]
|
|
||||||
{
|
|
||||||
//new DirectPlayProfile
|
|
||||||
//{
|
|
||||||
// Container = "mkv",
|
|
||||||
// VideoCodec = "h264,mpeg4",
|
|
||||||
// AudioCodec = mkvAudio,
|
|
||||||
// Type = DlnaProfileType.Video
|
|
||||||
//},
|
|
||||||
new DirectPlayProfile
|
|
||||||
{
|
|
||||||
Container = "mp4,mov,m4v",
|
|
||||||
VideoCodec = "h264,mpeg4",
|
|
||||||
AudioCodec = mp4Audio,
|
|
||||||
Type = DlnaProfileType.Video
|
|
||||||
},
|
|
||||||
new DirectPlayProfile
|
|
||||||
{
|
|
||||||
Container = "mp3",
|
|
||||||
Type = DlnaProfileType.Audio
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ContainerProfiles = new[]
|
|
||||||
{
|
|
||||||
new ContainerProfile
|
|
||||||
{
|
|
||||||
Type = DlnaProfileType.Video,
|
|
||||||
Conditions = new []
|
|
||||||
{
|
|
||||||
new ProfileCondition
|
|
||||||
{
|
|
||||||
Condition = ProfileConditionType.NotEquals,
|
|
||||||
Property = ProfileConditionValue.NumAudioStreams,
|
|
||||||
Value = "0",
|
|
||||||
IsRequired = false
|
|
||||||
},
|
|
||||||
new ProfileCondition
|
|
||||||
{
|
|
||||||
Condition = ProfileConditionType.Equals,
|
|
||||||
Property = ProfileConditionValue.NumVideoStreams,
|
|
||||||
Value = "1",
|
|
||||||
IsRequired = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var codecProfiles = new List<CodecProfile>
|
|
||||||
{
|
|
||||||
new CodecProfile
|
|
||||||
{
|
|
||||||
Type = CodecType.Video,
|
|
||||||
Codec = "h264",
|
|
||||||
Conditions = new []
|
|
||||||
{
|
|
||||||
new ProfileCondition
|
|
||||||
{
|
|
||||||
Condition = ProfileConditionType.LessThanEqual,
|
|
||||||
Property = ProfileConditionValue.VideoBitDepth,
|
|
||||||
Value = "8",
|
|
||||||
IsRequired = false
|
|
||||||
},
|
|
||||||
new ProfileCondition
|
|
||||||
{
|
|
||||||
Condition = ProfileConditionType.LessThanEqual,
|
|
||||||
Property = ProfileConditionValue.Width,
|
|
||||||
Value = "1920",
|
|
||||||
IsRequired = true
|
|
||||||
},
|
|
||||||
new ProfileCondition
|
|
||||||
{
|
|
||||||
Condition = ProfileConditionType.LessThanEqual,
|
|
||||||
Property = ProfileConditionValue.Height,
|
|
||||||
Value = "1080",
|
|
||||||
IsRequired = true
|
|
||||||
},
|
|
||||||
new ProfileCondition
|
|
||||||
{
|
|
||||||
Condition = ProfileConditionType.LessThanEqual,
|
|
||||||
Property = ProfileConditionValue.RefFrames,
|
|
||||||
Value = "4",
|
|
||||||
IsRequired = false
|
|
||||||
},
|
|
||||||
new ProfileCondition
|
|
||||||
{
|
|
||||||
Condition = ProfileConditionType.LessThanEqual,
|
|
||||||
Property = ProfileConditionValue.VideoFramerate,
|
|
||||||
Value = "30",
|
|
||||||
IsRequired = false
|
|
||||||
},
|
|
||||||
new ProfileCondition
|
|
||||||
{
|
|
||||||
Condition = ProfileConditionType.Equals,
|
|
||||||
Property = ProfileConditionValue.IsAnamorphic,
|
|
||||||
Value = "false",
|
|
||||||
IsRequired = false
|
|
||||||
},
|
|
||||||
new ProfileCondition
|
|
||||||
{
|
|
||||||
Condition = ProfileConditionType.LessThanEqual,
|
|
||||||
Property = ProfileConditionValue.VideoLevel,
|
|
||||||
Value = videoLevel,
|
|
||||||
IsRequired = false
|
|
||||||
},
|
|
||||||
new ProfileCondition
|
|
||||||
{
|
|
||||||
Condition = ProfileConditionType.EqualsAny,
|
|
||||||
Property = ProfileConditionValue.VideoProfile,
|
|
||||||
Value = videoProfile,
|
|
||||||
IsRequired = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
new CodecProfile
|
|
||||||
{
|
|
||||||
Type = CodecType.Video,
|
|
||||||
Codec = "mpeg4",
|
|
||||||
Conditions = new []
|
|
||||||
{
|
|
||||||
new ProfileCondition
|
|
||||||
{
|
|
||||||
Condition = ProfileConditionType.LessThanEqual,
|
|
||||||
Property = ProfileConditionValue.VideoBitDepth,
|
|
||||||
Value = "8",
|
|
||||||
IsRequired = false
|
|
||||||
},
|
|
||||||
new ProfileCondition
|
|
||||||
{
|
|
||||||
Condition = ProfileConditionType.LessThanEqual,
|
|
||||||
Property = ProfileConditionValue.Width,
|
|
||||||
Value = "1920",
|
|
||||||
IsRequired = true
|
|
||||||
},
|
|
||||||
new ProfileCondition
|
|
||||||
{
|
|
||||||
Condition = ProfileConditionType.LessThanEqual,
|
|
||||||
Property = ProfileConditionValue.Height,
|
|
||||||
Value = "1080",
|
|
||||||
IsRequired = true
|
|
||||||
},
|
|
||||||
new ProfileCondition
|
|
||||||
{
|
|
||||||
Condition = ProfileConditionType.LessThanEqual,
|
|
||||||
Property = ProfileConditionValue.RefFrames,
|
|
||||||
Value = "4",
|
|
||||||
IsRequired = false
|
|
||||||
},
|
|
||||||
new ProfileCondition
|
|
||||||
{
|
|
||||||
Condition = ProfileConditionType.LessThanEqual,
|
|
||||||
Property = ProfileConditionValue.VideoFramerate,
|
|
||||||
Value = "30",
|
|
||||||
IsRequired = false
|
|
||||||
},
|
|
||||||
new ProfileCondition
|
|
||||||
{
|
|
||||||
Condition = ProfileConditionType.Equals,
|
|
||||||
Property = ProfileConditionValue.IsAnamorphic,
|
|
||||||
Value = "false",
|
|
||||||
IsRequired = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
codecProfiles.Add(new CodecProfile
|
|
||||||
{
|
|
||||||
Type = CodecType.VideoAudio,
|
|
||||||
Codec = "ac3",
|
|
||||||
Conditions = new[]
|
|
||||||
{
|
|
||||||
new ProfileCondition
|
|
||||||
{
|
|
||||||
Condition = ProfileConditionType.LessThanEqual,
|
|
||||||
Property = ProfileConditionValue.AudioChannels,
|
|
||||||
Value = "6",
|
|
||||||
IsRequired = false
|
|
||||||
},
|
|
||||||
new ProfileCondition
|
|
||||||
{
|
|
||||||
Condition = ProfileConditionType.LessThanEqual,
|
|
||||||
Property = ProfileConditionValue.AudioBitrate,
|
|
||||||
Value = "320000",
|
|
||||||
IsRequired = true
|
|
||||||
},
|
|
||||||
new ProfileCondition
|
|
||||||
{
|
|
||||||
Condition = ProfileConditionType.Equals,
|
|
||||||
Property = ProfileConditionValue.IsSecondaryAudio,
|
|
||||||
Value = "false",
|
|
||||||
IsRequired = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
codecProfiles.Add(new CodecProfile
|
|
||||||
{
|
|
||||||
Type = CodecType.VideoAudio,
|
|
||||||
Codec = "aac,mp3",
|
|
||||||
Conditions = new[]
|
|
||||||
{
|
|
||||||
new ProfileCondition
|
|
||||||
{
|
|
||||||
Condition = ProfileConditionType.Equals,
|
|
||||||
Property = ProfileConditionValue.IsSecondaryAudio,
|
|
||||||
Value = "false",
|
|
||||||
IsRequired = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
CodecProfiles = codecProfiles.ToArray();
|
|
||||||
|
|
||||||
SubtitleProfiles = new[]
|
|
||||||
{
|
|
||||||
new SubtitleProfile
|
|
||||||
{
|
|
||||||
Format = "srt",
|
|
||||||
Method = SubtitleDeliveryMethod.External
|
|
||||||
},
|
|
||||||
new SubtitleProfile
|
|
||||||
{
|
|
||||||
Format = "vtt",
|
|
||||||
Method = SubtitleDeliveryMethod.External
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
TranscodingProfiles = new[]
|
|
||||||
{
|
|
||||||
new TranscodingProfile
|
|
||||||
{
|
|
||||||
Container = "mp3",
|
|
||||||
AudioCodec = "mp3",
|
|
||||||
Type = DlnaProfileType.Audio,
|
|
||||||
Context = EncodingContext.Static
|
|
||||||
},
|
|
||||||
|
|
||||||
new TranscodingProfile
|
|
||||||
{
|
|
||||||
Container = "mp4",
|
|
||||||
Type = DlnaProfileType.Video,
|
|
||||||
AudioCodec = "aac",
|
|
||||||
VideoCodec = "h264",
|
|
||||||
Context = EncodingContext.Static
|
|
||||||
},
|
|
||||||
|
|
||||||
new TranscodingProfile
|
|
||||||
{
|
|
||||||
Container = "jpeg",
|
|
||||||
Type = DlnaProfileType.Photo,
|
|
||||||
Context = EncodingContext.Static
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
using MediaBrowser.Model.Sync;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Sync
|
|
||||||
{
|
|
||||||
public interface IHasSyncQuality
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the device profile.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="target">The target.</param>
|
|
||||||
/// <param name="profile">The profile.</param>
|
|
||||||
/// <param name="quality">The quality.</param>
|
|
||||||
/// <returns>DeviceProfile.</returns>
|
|
||||||
SyncJobOptions GetSyncJobOptions(SyncTarget target, string profile, string quality);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the quality options.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="target">The target.</param>
|
|
||||||
/// <returns>IEnumerable<SyncQualityOption>.</returns>
|
|
||||||
IEnumerable<SyncQualityOption> GetQualityOptions(SyncTarget target);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the profile options.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="target">The target.</param>
|
|
||||||
/// <returns>IEnumerable<SyncQualityOption>.</returns>
|
|
||||||
IEnumerable<SyncProfileOption> GetProfileOptions(SyncTarget target);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,500 +0,0 @@
|
||||||
using MediaBrowser.Common.Configuration;
|
|
||||||
using MediaBrowser.Common.Progress;
|
|
||||||
using MediaBrowser.Controller;
|
|
||||||
using MediaBrowser.Controller.IO;
|
|
||||||
using MediaBrowser.Controller.Sync;
|
|
||||||
using MediaBrowser.Model.Dto;
|
|
||||||
using MediaBrowser.Model.Entities;
|
|
||||||
using MediaBrowser.Model.Logging;
|
|
||||||
using MediaBrowser.Model.MediaInfo;
|
|
||||||
using MediaBrowser.Model.Sync;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Emby.Server.Implementations.IO;
|
|
||||||
using MediaBrowser.Model.Cryptography;
|
|
||||||
using MediaBrowser.Model.IO;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Sync
|
|
||||||
{
|
|
||||||
public class MediaSync
|
|
||||||
{
|
|
||||||
private readonly ISyncManager _syncManager;
|
|
||||||
private readonly IServerApplicationHost _appHost;
|
|
||||||
private readonly ILogger _logger;
|
|
||||||
private readonly IFileSystem _fileSystem;
|
|
||||||
private readonly IConfigurationManager _config;
|
|
||||||
private readonly ICryptoProvider _cryptographyProvider;
|
|
||||||
|
|
||||||
public const string PathSeparatorString = "/";
|
|
||||||
public const char PathSeparatorChar = '/';
|
|
||||||
|
|
||||||
public MediaSync(ILogger logger, ISyncManager syncManager, IServerApplicationHost appHost, IFileSystem fileSystem, IConfigurationManager config, ICryptoProvider cryptographyProvider)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_syncManager = syncManager;
|
|
||||||
_appHost = appHost;
|
|
||||||
_fileSystem = fileSystem;
|
|
||||||
_config = config;
|
|
||||||
_cryptographyProvider = cryptographyProvider;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Sync(IServerSyncProvider provider,
|
|
||||||
ISyncDataProvider dataProvider,
|
|
||||||
SyncTarget target,
|
|
||||||
IProgress<double> progress,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var serverId = _appHost.SystemId;
|
|
||||||
var serverName = _appHost.FriendlyName;
|
|
||||||
|
|
||||||
await SyncData(provider, dataProvider, serverId, target, cancellationToken).ConfigureAwait(false);
|
|
||||||
progress.Report(3);
|
|
||||||
|
|
||||||
var innerProgress = new ActionableProgress<double>();
|
|
||||||
innerProgress.RegisterAction(pct =>
|
|
||||||
{
|
|
||||||
var totalProgress = pct * .97;
|
|
||||||
totalProgress += 1;
|
|
||||||
progress.Report(totalProgress);
|
|
||||||
});
|
|
||||||
await GetNewMedia(provider, dataProvider, target, serverId, serverName, innerProgress, cancellationToken);
|
|
||||||
|
|
||||||
// Do the data sync twice so the server knows what was removed from the device
|
|
||||||
await SyncData(provider, dataProvider, serverId, target, cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
progress.Report(100);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SyncData(IServerSyncProvider provider,
|
|
||||||
ISyncDataProvider dataProvider,
|
|
||||||
string serverId,
|
|
||||||
SyncTarget target,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var localItems = await dataProvider.GetLocalItems(target, serverId).ConfigureAwait(false);
|
|
||||||
var remoteFiles = await provider.GetFiles(target, cancellationToken).ConfigureAwait(false);
|
|
||||||
var remoteIds = remoteFiles.Items.Select(i => i.FullName).ToList();
|
|
||||||
|
|
||||||
var jobItemIds = new List<string>();
|
|
||||||
|
|
||||||
foreach (var localItem in localItems)
|
|
||||||
{
|
|
||||||
if (remoteIds.Contains(localItem.FileId, StringComparer.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
jobItemIds.Add(localItem.SyncJobItemId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await _syncManager.SyncData(new SyncDataRequest
|
|
||||||
{
|
|
||||||
TargetId = target.Id,
|
|
||||||
SyncJobItemIds = jobItemIds
|
|
||||||
|
|
||||||
}).ConfigureAwait(false);
|
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
foreach (var itemIdToRemove in result.ItemIdsToRemove)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await RemoveItem(provider, dataProvider, serverId, itemIdToRemove, target, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.ErrorException("Error deleting item from device. Id: {0}", ex, itemIdToRemove);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task GetNewMedia(IServerSyncProvider provider,
|
|
||||||
ISyncDataProvider dataProvider,
|
|
||||||
SyncTarget target,
|
|
||||||
string serverId,
|
|
||||||
string serverName,
|
|
||||||
IProgress<double> progress,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var jobItems = await _syncManager.GetReadySyncItems(target.Id).ConfigureAwait(false);
|
|
||||||
|
|
||||||
var numComplete = 0;
|
|
||||||
double startingPercent = 0;
|
|
||||||
double percentPerItem = 1;
|
|
||||||
if (jobItems.Count > 0)
|
|
||||||
{
|
|
||||||
percentPerItem /= jobItems.Count;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var jobItem in jobItems)
|
|
||||||
{
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
var currentPercent = startingPercent;
|
|
||||||
var innerProgress = new ActionableProgress<double>();
|
|
||||||
innerProgress.RegisterAction(pct =>
|
|
||||||
{
|
|
||||||
var totalProgress = pct * percentPerItem;
|
|
||||||
totalProgress += currentPercent;
|
|
||||||
progress.Report(totalProgress);
|
|
||||||
});
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await GetItem(provider, dataProvider, target, serverId, serverName, jobItem, innerProgress, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.ErrorException("Error syncing item", ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
numComplete++;
|
|
||||||
startingPercent = numComplete;
|
|
||||||
startingPercent /= jobItems.Count;
|
|
||||||
startingPercent *= 100;
|
|
||||||
progress.Report(startingPercent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task GetItem(IServerSyncProvider provider,
|
|
||||||
ISyncDataProvider dataProvider,
|
|
||||||
SyncTarget target,
|
|
||||||
string serverId,
|
|
||||||
string serverName,
|
|
||||||
SyncedItem jobItem,
|
|
||||||
IProgress<double> progress,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var libraryItem = jobItem.Item;
|
|
||||||
var internalSyncJobItem = _syncManager.GetJobItem(jobItem.SyncJobItemId);
|
|
||||||
var internalSyncJob = _syncManager.GetJob(jobItem.SyncJobId);
|
|
||||||
|
|
||||||
var localItem = CreateLocalItem(provider, jobItem, internalSyncJob, target, libraryItem, serverId, serverName, jobItem.OriginalFileName);
|
|
||||||
|
|
||||||
await _syncManager.ReportSyncJobItemTransferBeginning(internalSyncJobItem.Id);
|
|
||||||
|
|
||||||
var transferSuccess = false;
|
|
||||||
Exception transferException = null;
|
|
||||||
|
|
||||||
var options = _config.GetSyncOptions();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var fileTransferProgress = new ActionableProgress<double>();
|
|
||||||
fileTransferProgress.RegisterAction(pct => progress.Report(pct * .92));
|
|
||||||
|
|
||||||
var sendFileResult = await SendFile(provider, internalSyncJobItem.OutputPath, localItem.LocalPath.Split(PathSeparatorChar), target, options, fileTransferProgress, cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (localItem.Item.MediaSources != null)
|
|
||||||
{
|
|
||||||
var mediaSource = localItem.Item.MediaSources.FirstOrDefault();
|
|
||||||
if (mediaSource != null)
|
|
||||||
{
|
|
||||||
mediaSource.Path = sendFileResult.Path;
|
|
||||||
mediaSource.Protocol = sendFileResult.Protocol;
|
|
||||||
mediaSource.RequiredHttpHeaders = sendFileResult.RequiredHttpHeaders;
|
|
||||||
mediaSource.SupportsTranscoding = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
localItem.FileId = sendFileResult.Id;
|
|
||||||
|
|
||||||
// Create db record
|
|
||||||
await dataProvider.AddOrUpdate(target, localItem).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (localItem.Item.MediaSources != null)
|
|
||||||
{
|
|
||||||
var mediaSource = localItem.Item.MediaSources.FirstOrDefault();
|
|
||||||
if (mediaSource != null)
|
|
||||||
{
|
|
||||||
await SendSubtitles(localItem, mediaSource, provider, dataProvider, target, options, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
progress.Report(92);
|
|
||||||
|
|
||||||
transferSuccess = true;
|
|
||||||
|
|
||||||
progress.Report(99);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.ErrorException("Error transferring sync job file", ex);
|
|
||||||
transferException = ex;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (transferSuccess)
|
|
||||||
{
|
|
||||||
await _syncManager.ReportSyncJobItemTransferred(jobItem.SyncJobItemId).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await _syncManager.ReportSyncJobItemTransferFailed(jobItem.SyncJobItemId).ConfigureAwait(false);
|
|
||||||
|
|
||||||
throw transferException;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SendSubtitles(LocalItem localItem, MediaSourceInfo mediaSource, IServerSyncProvider provider, ISyncDataProvider dataProvider, SyncTarget target, SyncOptions options, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var failedSubtitles = new List<MediaStream>();
|
|
||||||
var requiresSave = false;
|
|
||||||
|
|
||||||
foreach (var mediaStream in mediaSource.MediaStreams
|
|
||||||
.Where(i => i.Type == MediaStreamType.Subtitle && i.IsExternal)
|
|
||||||
.ToList())
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var remotePath = GetRemoteSubtitlePath(localItem, mediaStream, provider, target);
|
|
||||||
var sendFileResult = await SendFile(provider, mediaStream.Path, remotePath, target, options, new Progress<double>(), cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
// This is the path that will be used when talking to the provider
|
|
||||||
mediaStream.ExternalId = sendFileResult.Id;
|
|
||||||
|
|
||||||
// Keep track of all additional files for cleanup later.
|
|
||||||
localItem.AdditionalFiles.Add(sendFileResult.Id);
|
|
||||||
|
|
||||||
// This is the public path clients will use
|
|
||||||
mediaStream.Path = sendFileResult.Path;
|
|
||||||
requiresSave = true;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.ErrorException("Error sending subtitle stream", ex);
|
|
||||||
failedSubtitles.Add(mediaStream);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (failedSubtitles.Count > 0)
|
|
||||||
{
|
|
||||||
mediaSource.MediaStreams = mediaSource.MediaStreams.Except(failedSubtitles).ToList();
|
|
||||||
requiresSave = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requiresSave)
|
|
||||||
{
|
|
||||||
await dataProvider.AddOrUpdate(target, localItem).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string[] GetRemoteSubtitlePath(LocalItem item, MediaStream stream, IServerSyncProvider provider, SyncTarget target)
|
|
||||||
{
|
|
||||||
var filename = GetSubtitleSaveFileName(item, stream.Language, stream.IsForced) + "." + stream.Codec.ToLower();
|
|
||||||
|
|
||||||
var pathParts = item.LocalPath.Split(PathSeparatorChar);
|
|
||||||
var list = pathParts.Take(pathParts.Length - 1).ToList();
|
|
||||||
list.Add(filename);
|
|
||||||
|
|
||||||
return list.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetSubtitleSaveFileName(LocalItem item, string language, bool isForced)
|
|
||||||
{
|
|
||||||
var path = item.LocalPath;
|
|
||||||
|
|
||||||
var name = Path.GetFileNameWithoutExtension(path);
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(language))
|
|
||||||
{
|
|
||||||
name += "." + language.ToLower();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isForced)
|
|
||||||
{
|
|
||||||
name += ".foreign";
|
|
||||||
}
|
|
||||||
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RemoveItem(IServerSyncProvider provider,
|
|
||||||
ISyncDataProvider dataProvider,
|
|
||||||
string serverId,
|
|
||||||
string syncJobItemId,
|
|
||||||
SyncTarget target,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var localItems = await dataProvider.GetItemsBySyncJobItemId(target, serverId, syncJobItemId);
|
|
||||||
|
|
||||||
foreach (var localItem in localItems)
|
|
||||||
{
|
|
||||||
var files = localItem.AdditionalFiles.ToList();
|
|
||||||
|
|
||||||
foreach (var file in files)
|
|
||||||
{
|
|
||||||
_logger.Debug("Removing {0} from {1}.", file, target.Name);
|
|
||||||
await provider.DeleteFile(file, target, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.Debug("Removing {0} from {1}.", localItem.FileId, target.Name);
|
|
||||||
await provider.DeleteFile(localItem.FileId, target, cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
await dataProvider.Delete(target, localItem.Id).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<SyncedFileInfo> SendFile(IServerSyncProvider provider, string inputPath, string[] pathParts, SyncTarget target, SyncOptions options, IProgress<double> progress, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
_logger.Debug("Sending {0} to {1}. Remote path: {2}", inputPath, provider.Name, string.Join("/", pathParts));
|
|
||||||
var supportsDirectCopy = provider as ISupportsDirectCopy;
|
|
||||||
if (supportsDirectCopy != null)
|
|
||||||
{
|
|
||||||
return await supportsDirectCopy.SendFile(inputPath, pathParts, target, progress, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
using (var fileStream = _fileSystem.GetFileStream(inputPath, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read, true))
|
|
||||||
{
|
|
||||||
Stream stream = fileStream;
|
|
||||||
|
|
||||||
if (options.UploadSpeedLimitBytes > 0 && provider is IRemoteSyncProvider)
|
|
||||||
{
|
|
||||||
stream = new ThrottledStream(stream, options.UploadSpeedLimitBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await provider.SendFile(stream, pathParts, target, progress, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetLocalId(string jobItemId, string itemId)
|
|
||||||
{
|
|
||||||
var bytes = Encoding.UTF8.GetBytes(jobItemId + itemId);
|
|
||||||
bytes = CreateMd5(bytes);
|
|
||||||
return BitConverter.ToString(bytes, 0, bytes.Length).Replace("-", string.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[] CreateMd5(byte[] value)
|
|
||||||
{
|
|
||||||
return _cryptographyProvider.ComputeMD5(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalItem CreateLocalItem(IServerSyncProvider provider, SyncedItem syncedItem, SyncJob job, SyncTarget target, BaseItemDto libraryItem, string serverId, string serverName, string originalFileName)
|
|
||||||
{
|
|
||||||
var path = GetDirectoryPath(provider, job, syncedItem, libraryItem, serverName);
|
|
||||||
path.Add(GetLocalFileName(provider, libraryItem, originalFileName));
|
|
||||||
|
|
||||||
var localPath = string.Join(PathSeparatorString, path.ToArray());
|
|
||||||
|
|
||||||
foreach (var mediaSource in libraryItem.MediaSources)
|
|
||||||
{
|
|
||||||
mediaSource.Path = localPath;
|
|
||||||
mediaSource.Protocol = MediaProtocol.File;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new LocalItem
|
|
||||||
{
|
|
||||||
Item = libraryItem,
|
|
||||||
ItemId = libraryItem.Id,
|
|
||||||
ServerId = serverId,
|
|
||||||
LocalPath = localPath,
|
|
||||||
Id = GetLocalId(syncedItem.SyncJobItemId, libraryItem.Id),
|
|
||||||
SyncJobItemId = syncedItem.SyncJobItemId
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<string> GetDirectoryPath(IServerSyncProvider provider, SyncJob job, SyncedItem syncedItem, BaseItemDto item, string serverName)
|
|
||||||
{
|
|
||||||
var parts = new List<string>
|
|
||||||
{
|
|
||||||
serverName
|
|
||||||
};
|
|
||||||
|
|
||||||
var profileOption = _syncManager.GetProfileOptions(job.TargetId)
|
|
||||||
.FirstOrDefault(i => string.Equals(i.Id, job.Profile, StringComparison.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
string name;
|
|
||||||
|
|
||||||
if (profileOption != null && !string.IsNullOrWhiteSpace(profileOption.Name))
|
|
||||||
{
|
|
||||||
name = profileOption.Name;
|
|
||||||
|
|
||||||
if (job.Bitrate.HasValue)
|
|
||||||
{
|
|
||||||
name += "-" + job.Bitrate.Value.ToString(CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var qualityOption = _syncManager.GetQualityOptions(job.TargetId)
|
|
||||||
.FirstOrDefault(i => string.Equals(i.Id, job.Quality, StringComparison.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
if (qualityOption != null && !string.IsNullOrWhiteSpace(qualityOption.Name))
|
|
||||||
{
|
|
||||||
name += "-" + qualityOption.Name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
name = syncedItem.SyncJobName + "-" + syncedItem.SyncJobDateCreated
|
|
||||||
.ToLocalTime()
|
|
||||||
.ToString("g")
|
|
||||||
.Replace(" ", "-");
|
|
||||||
}
|
|
||||||
|
|
||||||
name = GetValidFilename(provider, name);
|
|
||||||
parts.Add(name);
|
|
||||||
|
|
||||||
if (item.IsType("episode"))
|
|
||||||
{
|
|
||||||
parts.Add("TV");
|
|
||||||
if (!string.IsNullOrWhiteSpace(item.SeriesName))
|
|
||||||
{
|
|
||||||
parts.Add(item.SeriesName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (item.IsVideo)
|
|
||||||
{
|
|
||||||
parts.Add("Videos");
|
|
||||||
parts.Add(item.Name);
|
|
||||||
}
|
|
||||||
else if (item.IsAudio)
|
|
||||||
{
|
|
||||||
parts.Add("Music");
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(item.AlbumArtist))
|
|
||||||
{
|
|
||||||
parts.Add(item.AlbumArtist);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(item.Album))
|
|
||||||
{
|
|
||||||
parts.Add(item.Album);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
parts.Add("Photos");
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(item.Album))
|
|
||||||
{
|
|
||||||
parts.Add(item.Album);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return parts.Select(i => GetValidFilename(provider, i)).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetLocalFileName(IServerSyncProvider provider, BaseItemDto item, string originalFileName)
|
|
||||||
{
|
|
||||||
var filename = originalFileName;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(filename))
|
|
||||||
{
|
|
||||||
filename = item.Name;
|
|
||||||
}
|
|
||||||
|
|
||||||
return GetValidFilename(provider, filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetValidFilename(IServerSyncProvider provider, string filename)
|
|
||||||
{
|
|
||||||
// We can always add this method to the sync provider if it's really needed
|
|
||||||
return _fileSystem.GetValidFilename(filename);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,79 +0,0 @@
|
||||||
using MediaBrowser.Common.Configuration;
|
|
||||||
using MediaBrowser.Common.Progress;
|
|
||||||
using MediaBrowser.Controller;
|
|
||||||
using MediaBrowser.Controller.Sync;
|
|
||||||
using MediaBrowser.Model.Logging;
|
|
||||||
using MediaBrowser.Model.Sync;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using MediaBrowser.Common.IO;
|
|
||||||
using MediaBrowser.Controller.IO;
|
|
||||||
using MediaBrowser.Model.Cryptography;
|
|
||||||
using MediaBrowser.Model.IO;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Sync
|
|
||||||
{
|
|
||||||
public class MultiProviderSync
|
|
||||||
{
|
|
||||||
private readonly SyncManager _syncManager;
|
|
||||||
private readonly IServerApplicationHost _appHost;
|
|
||||||
private readonly ILogger _logger;
|
|
||||||
private readonly IFileSystem _fileSystem;
|
|
||||||
private readonly IConfigurationManager _config;
|
|
||||||
private readonly ICryptoProvider _cryptographyProvider;
|
|
||||||
|
|
||||||
public MultiProviderSync(SyncManager syncManager, IServerApplicationHost appHost, ILogger logger, IFileSystem fileSystem, IConfigurationManager config, ICryptoProvider cryptographyProvider)
|
|
||||||
{
|
|
||||||
_syncManager = syncManager;
|
|
||||||
_appHost = appHost;
|
|
||||||
_logger = logger;
|
|
||||||
_fileSystem = fileSystem;
|
|
||||||
_config = config;
|
|
||||||
_cryptographyProvider = cryptographyProvider;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Sync(IEnumerable<IServerSyncProvider> providers, IProgress<double> progress, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var targets = providers
|
|
||||||
.SelectMany(i => i.GetAllSyncTargets().Select(t => new Tuple<IServerSyncProvider, SyncTarget>(i, t)))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var numComplete = 0;
|
|
||||||
double startingPercent = 0;
|
|
||||||
double percentPerItem = 1;
|
|
||||||
if (targets.Count > 0)
|
|
||||||
{
|
|
||||||
percentPerItem /= targets.Count;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var target in targets)
|
|
||||||
{
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
var currentPercent = startingPercent;
|
|
||||||
var innerProgress = new ActionableProgress<double>();
|
|
||||||
innerProgress.RegisterAction(pct =>
|
|
||||||
{
|
|
||||||
var totalProgress = pct * percentPerItem;
|
|
||||||
totalProgress += currentPercent;
|
|
||||||
progress.Report(totalProgress);
|
|
||||||
});
|
|
||||||
|
|
||||||
var dataProvider = _syncManager.GetDataProvider(target.Item1, target.Item2);
|
|
||||||
|
|
||||||
await new MediaSync(_logger, _syncManager, _appHost, _fileSystem, _config, _cryptographyProvider)
|
|
||||||
.Sync(target.Item1, dataProvider, target.Item2, innerProgress, cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
numComplete++;
|
|
||||||
startingPercent = numComplete;
|
|
||||||
startingPercent /= targets.Count;
|
|
||||||
startingPercent *= 100;
|
|
||||||
progress.Report(startingPercent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,95 +0,0 @@
|
||||||
using MediaBrowser.Common.Configuration;
|
|
||||||
using MediaBrowser.Controller;
|
|
||||||
using MediaBrowser.Controller.Sync;
|
|
||||||
using MediaBrowser.Model.Logging;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using MediaBrowser.Model.Cryptography;
|
|
||||||
using MediaBrowser.Model.IO;
|
|
||||||
using MediaBrowser.Model.Tasks;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Sync
|
|
||||||
{
|
|
||||||
class ServerSyncScheduledTask : IScheduledTask, IConfigurableScheduledTask
|
|
||||||
{
|
|
||||||
private readonly ISyncManager _syncManager;
|
|
||||||
private readonly ILogger _logger;
|
|
||||||
private readonly IFileSystem _fileSystem;
|
|
||||||
private readonly IServerApplicationHost _appHost;
|
|
||||||
private readonly IConfigurationManager _config;
|
|
||||||
private readonly ICryptoProvider _cryptographyProvider;
|
|
||||||
|
|
||||||
public ServerSyncScheduledTask(ISyncManager syncManager, ILogger logger, IFileSystem fileSystem, IServerApplicationHost appHost, IConfigurationManager config, ICryptoProvider cryptographyProvider)
|
|
||||||
{
|
|
||||||
_syncManager = syncManager;
|
|
||||||
_logger = logger;
|
|
||||||
_fileSystem = fileSystem;
|
|
||||||
_appHost = appHost;
|
|
||||||
_config = config;
|
|
||||||
_cryptographyProvider = cryptographyProvider;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Name
|
|
||||||
{
|
|
||||||
get { return "Cloud & Folder Sync"; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Description
|
|
||||||
{
|
|
||||||
get { return "Sync media to the cloud"; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Category
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return "Sync";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
|
|
||||||
{
|
|
||||||
return new MultiProviderSync((SyncManager)_syncManager, _appHost, _logger, _fileSystem, _config, _cryptographyProvider)
|
|
||||||
.Sync(ServerSyncProviders, progress, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<IServerSyncProvider> ServerSyncProviders
|
|
||||||
{
|
|
||||||
get { return ((SyncManager)_syncManager).ServerSyncProviders; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates the triggers that define when the task will run
|
|
||||||
/// </summary>
|
|
||||||
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
|
|
||||||
{
|
|
||||||
return new[] {
|
|
||||||
|
|
||||||
// Every so often
|
|
||||||
new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(3).Ticks}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
public bool IsHidden
|
|
||||||
{
|
|
||||||
get { return !IsEnabled; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsEnabled
|
|
||||||
{
|
|
||||||
get { return ServerSyncProviders.Any(); }
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsLogged
|
|
||||||
{
|
|
||||||
get { return true; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Key
|
|
||||||
{
|
|
||||||
get { return "ServerSync"; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
using MediaBrowser.Common.Configuration;
|
|
||||||
using MediaBrowser.Model.Sync;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Sync
|
|
||||||
{
|
|
||||||
public class SyncConfigurationFactory : IConfigurationFactory
|
|
||||||
{
|
|
||||||
public IEnumerable<ConfigurationStore> GetConfigurations()
|
|
||||||
{
|
|
||||||
return new List<ConfigurationStore>
|
|
||||||
{
|
|
||||||
new ConfigurationStore
|
|
||||||
{
|
|
||||||
ConfigurationType = typeof(SyncOptions),
|
|
||||||
Key = "sync"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class SyncExtensions
|
|
||||||
{
|
|
||||||
public static SyncOptions GetSyncOptions(this IConfigurationManager config)
|
|
||||||
{
|
|
||||||
return config.GetConfiguration<SyncOptions>("sync");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,89 +0,0 @@
|
||||||
using MediaBrowser.Common.Configuration;
|
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using MediaBrowser.Controller.MediaEncoding;
|
|
||||||
using MediaBrowser.Controller.Sync;
|
|
||||||
using MediaBrowser.Controller.TV;
|
|
||||||
using MediaBrowser.Model.Logging;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using MediaBrowser.Common.IO;
|
|
||||||
using MediaBrowser.Controller.IO;
|
|
||||||
using MediaBrowser.Model.IO;
|
|
||||||
using MediaBrowser.Model.Tasks;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Sync
|
|
||||||
{
|
|
||||||
public class SyncConvertScheduledTask : IScheduledTask
|
|
||||||
{
|
|
||||||
private readonly ILibraryManager _libraryManager;
|
|
||||||
private readonly ISyncRepository _syncRepo;
|
|
||||||
private readonly ISyncManager _syncManager;
|
|
||||||
private readonly ILogger _logger;
|
|
||||||
private readonly IUserManager _userManager;
|
|
||||||
private readonly ITVSeriesManager _tvSeriesManager;
|
|
||||||
private readonly IMediaEncoder _mediaEncoder;
|
|
||||||
private readonly ISubtitleEncoder _subtitleEncoder;
|
|
||||||
private readonly IConfigurationManager _config;
|
|
||||||
private readonly IFileSystem _fileSystem;
|
|
||||||
private readonly IMediaSourceManager _mediaSourceManager;
|
|
||||||
|
|
||||||
public SyncConvertScheduledTask(ILibraryManager libraryManager, ISyncRepository syncRepo, ISyncManager syncManager, ILogger logger, IUserManager userManager, ITVSeriesManager tvSeriesManager, IMediaEncoder mediaEncoder, ISubtitleEncoder subtitleEncoder, IConfigurationManager config, IFileSystem fileSystem, IMediaSourceManager mediaSourceManager)
|
|
||||||
{
|
|
||||||
_libraryManager = libraryManager;
|
|
||||||
_syncRepo = syncRepo;
|
|
||||||
_syncManager = syncManager;
|
|
||||||
_logger = logger;
|
|
||||||
_userManager = userManager;
|
|
||||||
_tvSeriesManager = tvSeriesManager;
|
|
||||||
_mediaEncoder = mediaEncoder;
|
|
||||||
_subtitleEncoder = subtitleEncoder;
|
|
||||||
_config = config;
|
|
||||||
_fileSystem = fileSystem;
|
|
||||||
_mediaSourceManager = mediaSourceManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Name
|
|
||||||
{
|
|
||||||
get { return "Convert media"; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Description
|
|
||||||
{
|
|
||||||
get { return "Runs scheduled sync jobs"; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Category
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return "Sync";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
|
|
||||||
{
|
|
||||||
return new SyncJobProcessor(_libraryManager, _syncRepo, (SyncManager)_syncManager, _logger, _userManager, _tvSeriesManager, _mediaEncoder, _subtitleEncoder, _config, _fileSystem, _mediaSourceManager)
|
|
||||||
.Sync(progress, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates the triggers that define when the task will run
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>IEnumerable{BaseTaskTrigger}.</returns>
|
|
||||||
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
|
|
||||||
{
|
|
||||||
return new[] {
|
|
||||||
|
|
||||||
// Every so often
|
|
||||||
new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(3).Ticks}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Key
|
|
||||||
{
|
|
||||||
get { return "SyncPrepare"; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Sync
|
|
||||||
{
|
|
||||||
public class SyncHelper
|
|
||||||
{
|
|
||||||
public static long? AdjustBitrate(long? profileBitrate, string quality)
|
|
||||||
{
|
|
||||||
if (profileBitrate.HasValue)
|
|
||||||
{
|
|
||||||
if (string.Equals(quality, "medium", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
profileBitrate = Math.Min(profileBitrate.Value, 4000000);
|
|
||||||
}
|
|
||||||
else if (string.Equals(quality, "low", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
profileBitrate = Math.Min(profileBitrate.Value, 1500000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return profileBitrate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
using MediaBrowser.Model.Dlna;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Sync
|
|
||||||
{
|
|
||||||
public class SyncJobOptions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the conversion options.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The conversion options.</value>
|
|
||||||
public DeviceProfile DeviceProfile { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a value indicating whether this instance is converting.
|
|
||||||
/// </summary>
|
|
||||||
/// <value><c>true</c> if this instance is converting; otherwise, <c>false</c>.</value>
|
|
||||||
public bool IsConverting { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,998 +0,0 @@
|
||||||
using MediaBrowser.Common.Configuration;
|
|
||||||
using MediaBrowser.Common.IO;
|
|
||||||
using MediaBrowser.Common.Progress;
|
|
||||||
using MediaBrowser.Controller.Entities;
|
|
||||||
using MediaBrowser.Controller.Entities.Audio;
|
|
||||||
using MediaBrowser.Controller.Entities.TV;
|
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using MediaBrowser.Controller.MediaEncoding;
|
|
||||||
using MediaBrowser.Controller.Sync;
|
|
||||||
using MediaBrowser.Controller.TV;
|
|
||||||
using MediaBrowser.Model.Dlna;
|
|
||||||
using MediaBrowser.Model.Dto;
|
|
||||||
using MediaBrowser.Model.Entities;
|
|
||||||
using MediaBrowser.Model.Logging;
|
|
||||||
using MediaBrowser.Model.MediaInfo;
|
|
||||||
using MediaBrowser.Model.Querying;
|
|
||||||
using MediaBrowser.Model.Session;
|
|
||||||
using MediaBrowser.Model.Sync;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using MediaBrowser.Controller.IO;
|
|
||||||
using MediaBrowser.Model.IO;
|
|
||||||
using MediaBrowser.Model.Extensions;
|
|
||||||
using MediaBrowser.Model.IO;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Sync
|
|
||||||
{
|
|
||||||
public class SyncJobProcessor
|
|
||||||
{
|
|
||||||
private readonly ILibraryManager _libraryManager;
|
|
||||||
private readonly ISyncRepository _syncRepo;
|
|
||||||
private readonly SyncManager _syncManager;
|
|
||||||
private readonly ILogger _logger;
|
|
||||||
private readonly IUserManager _userManager;
|
|
||||||
private readonly ITVSeriesManager _tvSeriesManager;
|
|
||||||
private readonly IMediaEncoder _mediaEncoder;
|
|
||||||
private readonly ISubtitleEncoder _subtitleEncoder;
|
|
||||||
private readonly IConfigurationManager _config;
|
|
||||||
private readonly IFileSystem _fileSystem;
|
|
||||||
private readonly IMediaSourceManager _mediaSourceManager;
|
|
||||||
|
|
||||||
public SyncJobProcessor(ILibraryManager libraryManager, ISyncRepository syncRepo, SyncManager syncManager, ILogger logger, IUserManager userManager, ITVSeriesManager tvSeriesManager, IMediaEncoder mediaEncoder, ISubtitleEncoder subtitleEncoder, IConfigurationManager config, IFileSystem fileSystem, IMediaSourceManager mediaSourceManager)
|
|
||||||
{
|
|
||||||
_libraryManager = libraryManager;
|
|
||||||
_syncRepo = syncRepo;
|
|
||||||
_syncManager = syncManager;
|
|
||||||
_logger = logger;
|
|
||||||
_userManager = userManager;
|
|
||||||
_tvSeriesManager = tvSeriesManager;
|
|
||||||
_mediaEncoder = mediaEncoder;
|
|
||||||
_subtitleEncoder = subtitleEncoder;
|
|
||||||
_config = config;
|
|
||||||
_fileSystem = fileSystem;
|
|
||||||
_mediaSourceManager = mediaSourceManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task EnsureJobItems(SyncJob job, User user)
|
|
||||||
{
|
|
||||||
var items = (await GetItemsForSync(job.Category, job.ParentId, job.RequestedItemIds, user, job.UnwatchedOnly).ConfigureAwait(false))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var jobItems = _syncManager.GetJobItems(new SyncJobItemQuery
|
|
||||||
{
|
|
||||||
JobId = job.Id,
|
|
||||||
AddMetadata = false
|
|
||||||
|
|
||||||
}).Items.ToList();
|
|
||||||
|
|
||||||
foreach (var item in items)
|
|
||||||
{
|
|
||||||
// Respect ItemLimit, if set
|
|
||||||
if (job.ItemLimit.HasValue)
|
|
||||||
{
|
|
||||||
if (jobItems.Count(j => j.Status != SyncJobItemStatus.RemovedFromDevice && j.Status != SyncJobItemStatus.Failed) >= job.ItemLimit.Value)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var itemId = item.Id.ToString("N");
|
|
||||||
|
|
||||||
var jobItem = jobItems.FirstOrDefault(i => string.Equals(i.ItemId, itemId, StringComparison.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
if (jobItem != null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var index = jobItems.Count == 0 ?
|
|
||||||
0 :
|
|
||||||
jobItems.Select(i => i.JobItemIndex).Max() + 1;
|
|
||||||
|
|
||||||
jobItem = new SyncJobItem
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid().ToString("N"),
|
|
||||||
ItemId = itemId,
|
|
||||||
ItemName = GetSyncJobItemName(item),
|
|
||||||
JobId = job.Id,
|
|
||||||
TargetId = job.TargetId,
|
|
||||||
DateCreated = DateTime.UtcNow,
|
|
||||||
JobItemIndex = index
|
|
||||||
};
|
|
||||||
|
|
||||||
await _syncRepo.Create(jobItem).ConfigureAwait(false);
|
|
||||||
_syncManager.OnSyncJobItemCreated(jobItem);
|
|
||||||
|
|
||||||
jobItems.Add(jobItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
jobItems = jobItems
|
|
||||||
.OrderBy(i => i.DateCreated)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
await UpdateJobStatus(job, jobItems).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetSyncJobItemName(BaseItem item)
|
|
||||||
{
|
|
||||||
var name = item.Name;
|
|
||||||
var episode = item as Episode;
|
|
||||||
|
|
||||||
if (episode != null)
|
|
||||||
{
|
|
||||||
if (episode.IndexNumber.HasValue)
|
|
||||||
{
|
|
||||||
name = "E" + episode.IndexNumber.Value.ToString(CultureInfo.InvariantCulture) + " - " + name;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (episode.ParentIndexNumber.HasValue)
|
|
||||||
{
|
|
||||||
name = "S" + episode.ParentIndexNumber.Value.ToString(CultureInfo.InvariantCulture) + ", " + name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task UpdateJobStatus(string id)
|
|
||||||
{
|
|
||||||
var job = _syncRepo.GetJob(id);
|
|
||||||
|
|
||||||
if (job == null)
|
|
||||||
{
|
|
||||||
return Task.FromResult(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = _syncManager.GetJobItems(new SyncJobItemQuery
|
|
||||||
{
|
|
||||||
JobId = job.Id,
|
|
||||||
AddMetadata = false
|
|
||||||
});
|
|
||||||
|
|
||||||
return UpdateJobStatus(job, result.Items.ToList());
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task UpdateJobStatus(SyncJob job, List<SyncJobItem> jobItems)
|
|
||||||
{
|
|
||||||
job.ItemCount = jobItems.Count;
|
|
||||||
|
|
||||||
double pct = 0;
|
|
||||||
|
|
||||||
foreach (var item in jobItems)
|
|
||||||
{
|
|
||||||
if (item.Status == SyncJobItemStatus.Failed || item.Status == SyncJobItemStatus.Synced || item.Status == SyncJobItemStatus.RemovedFromDevice || item.Status == SyncJobItemStatus.Cancelled)
|
|
||||||
{
|
|
||||||
pct += 100;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
pct += item.Progress ?? 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (job.ItemCount > 0)
|
|
||||||
{
|
|
||||||
pct /= job.ItemCount;
|
|
||||||
job.Progress = pct;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
job.Progress = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jobItems.Any(i => i.Status == SyncJobItemStatus.Transferring))
|
|
||||||
{
|
|
||||||
job.Status = SyncJobStatus.Transferring;
|
|
||||||
}
|
|
||||||
else if (jobItems.Any(i => i.Status == SyncJobItemStatus.Converting))
|
|
||||||
{
|
|
||||||
job.Status = SyncJobStatus.Converting;
|
|
||||||
}
|
|
||||||
else if (jobItems.All(i => i.Status == SyncJobItemStatus.Failed))
|
|
||||||
{
|
|
||||||
job.Status = SyncJobStatus.Failed;
|
|
||||||
}
|
|
||||||
else if (jobItems.All(i => i.Status == SyncJobItemStatus.Cancelled))
|
|
||||||
{
|
|
||||||
job.Status = SyncJobStatus.Cancelled;
|
|
||||||
}
|
|
||||||
else if (jobItems.All(i => i.Status == SyncJobItemStatus.ReadyToTransfer))
|
|
||||||
{
|
|
||||||
job.Status = SyncJobStatus.ReadyToTransfer;
|
|
||||||
}
|
|
||||||
else if (jobItems.All(i => i.Status == SyncJobItemStatus.Cancelled || i.Status == SyncJobItemStatus.Failed || i.Status == SyncJobItemStatus.Synced || i.Status == SyncJobItemStatus.RemovedFromDevice))
|
|
||||||
{
|
|
||||||
if (jobItems.Any(i => i.Status == SyncJobItemStatus.Failed))
|
|
||||||
{
|
|
||||||
job.Status = SyncJobStatus.CompletedWithError;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
job.Status = SyncJobStatus.Completed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
job.Status = SyncJobStatus.Queued;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _syncRepo.Update(job).ConfigureAwait(false);
|
|
||||||
|
|
||||||
_syncManager.OnSyncJobUpdated(job);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<BaseItem>> GetItemsForSync(SyncCategory? category, string parentId, IEnumerable<string> itemIds, User user, bool unwatchedOnly)
|
|
||||||
{
|
|
||||||
var list = new List<BaseItem>();
|
|
||||||
|
|
||||||
if (category.HasValue)
|
|
||||||
{
|
|
||||||
list = (await GetItemsForSync(category.Value, parentId, user).ConfigureAwait(false)).ToList();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
foreach (var itemId in itemIds)
|
|
||||||
{
|
|
||||||
var subList = await GetItemsForSync(itemId, user).ConfigureAwait(false);
|
|
||||||
list.AddRange(subList);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IEnumerable<BaseItem> items = list;
|
|
||||||
items = items.Where(_syncManager.SupportsSync);
|
|
||||||
|
|
||||||
if (unwatchedOnly)
|
|
||||||
{
|
|
||||||
// Avoid implicitly captured closure
|
|
||||||
var currentUser = user;
|
|
||||||
|
|
||||||
items = items.Where(i =>
|
|
||||||
{
|
|
||||||
var video = i as Video;
|
|
||||||
|
|
||||||
if (video != null)
|
|
||||||
{
|
|
||||||
return !video.IsPlayed(currentUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return items.DistinctBy(i => i.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<IEnumerable<BaseItem>> GetItemsForSync(SyncCategory category, string parentId, User user)
|
|
||||||
{
|
|
||||||
var parent = string.IsNullOrWhiteSpace(parentId)
|
|
||||||
? user.RootFolder
|
|
||||||
: (Folder)_libraryManager.GetItemById(parentId);
|
|
||||||
|
|
||||||
InternalItemsQuery query;
|
|
||||||
|
|
||||||
switch (category)
|
|
||||||
{
|
|
||||||
case SyncCategory.Latest:
|
|
||||||
query = new InternalItemsQuery
|
|
||||||
{
|
|
||||||
IsFolder = false,
|
|
||||||
SortBy = new[] { ItemSortBy.DateCreated, ItemSortBy.SortName },
|
|
||||||
SortOrder = SortOrder.Descending,
|
|
||||||
Recursive = true
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
case SyncCategory.Resume:
|
|
||||||
query = new InternalItemsQuery
|
|
||||||
{
|
|
||||||
IsFolder = false,
|
|
||||||
SortBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.SortName },
|
|
||||||
SortOrder = SortOrder.Descending,
|
|
||||||
Recursive = true,
|
|
||||||
IsResumable = true,
|
|
||||||
MediaTypes = new[] { MediaType.Video }
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SyncCategory.NextUp:
|
|
||||||
return _tvSeriesManager.GetNextUp(new NextUpQuery
|
|
||||||
{
|
|
||||||
ParentId = parentId,
|
|
||||||
UserId = user.Id.ToString("N")
|
|
||||||
}).Items;
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new ArgumentException("Unrecognized category: " + category);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parent == null)
|
|
||||||
{
|
|
||||||
return new List<BaseItem>();
|
|
||||||
}
|
|
||||||
|
|
||||||
query.User = user;
|
|
||||||
|
|
||||||
var result = await parent.GetItems(query).ConfigureAwait(false);
|
|
||||||
return result.Items;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<List<BaseItem>> GetItemsForSync(string id, User user)
|
|
||||||
{
|
|
||||||
var item = _libraryManager.GetItemById(id);
|
|
||||||
|
|
||||||
if (item == null)
|
|
||||||
{
|
|
||||||
return new List<BaseItem>();
|
|
||||||
}
|
|
||||||
|
|
||||||
var itemByName = item as IItemByName;
|
|
||||||
if (itemByName != null)
|
|
||||||
{
|
|
||||||
return itemByName.GetTaggedItems(new InternalItemsQuery(user)
|
|
||||||
{
|
|
||||||
IsFolder = false,
|
|
||||||
Recursive = true
|
|
||||||
}).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.IsFolder)
|
|
||||||
{
|
|
||||||
var folder = (Folder)item;
|
|
||||||
var itemsResult = await folder.GetItems(new InternalItemsQuery(user)
|
|
||||||
{
|
|
||||||
Recursive = true,
|
|
||||||
IsFolder = false
|
|
||||||
|
|
||||||
}).ConfigureAwait(false);
|
|
||||||
|
|
||||||
var items = itemsResult.Items;
|
|
||||||
|
|
||||||
if (!folder.IsPreSorted)
|
|
||||||
{
|
|
||||||
items = _libraryManager.Sort(items, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending)
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
return items.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new List<BaseItem> { item };
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task EnsureSyncJobItems(string targetId, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var jobResult = _syncRepo.GetJobs(new SyncJobQuery
|
|
||||||
{
|
|
||||||
SyncNewContent = true,
|
|
||||||
TargetId = targetId
|
|
||||||
});
|
|
||||||
|
|
||||||
foreach (var job in jobResult.Items)
|
|
||||||
{
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
if (job.SyncNewContent)
|
|
||||||
{
|
|
||||||
var user = _userManager.GetUserById(job.UserId);
|
|
||||||
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
await _syncManager.CancelJob(job.Id).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await EnsureJobItems(job, user).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Sync(IProgress<double> progress, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
await EnsureSyncJobItems(null, cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
// Look job items that are supposedly transfering, but need to be requeued because the synced files have been deleted somehow
|
|
||||||
await HandleDeletedSyncFiles(cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
// If it already has a converting status then is must have been aborted during conversion
|
|
||||||
var result = _syncManager.GetJobItems(new SyncJobItemQuery
|
|
||||||
{
|
|
||||||
Statuses = new[] { SyncJobItemStatus.Queued, SyncJobItemStatus.Converting },
|
|
||||||
AddMetadata = false
|
|
||||||
});
|
|
||||||
|
|
||||||
await SyncJobItems(result.Items, true, progress, cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
CleanDeadSyncFiles();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task HandleDeletedSyncFiles(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
// Look job items that are supposedly transfering, but need to be requeued because the synced files have been deleted somehow
|
|
||||||
var result = _syncManager.GetJobItems(new SyncJobItemQuery
|
|
||||||
{
|
|
||||||
Statuses = new[] { SyncJobItemStatus.ReadyToTransfer, SyncJobItemStatus.Transferring },
|
|
||||||
AddMetadata = false
|
|
||||||
});
|
|
||||||
|
|
||||||
foreach (var item in result.Items)
|
|
||||||
{
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(item.OutputPath) || !_fileSystem.FileExists(item.OutputPath))
|
|
||||||
{
|
|
||||||
item.Status = SyncJobItemStatus.Queued;
|
|
||||||
await _syncManager.UpdateSyncJobItemInternal(item).ConfigureAwait(false);
|
|
||||||
await UpdateJobStatus(item.JobId).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CleanDeadSyncFiles()
|
|
||||||
{
|
|
||||||
// TODO
|
|
||||||
// Clean files in sync temp folder that are not linked to any sync jobs
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SyncJobItems(string targetId, bool enableConversion, IProgress<double> progress,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
await EnsureSyncJobItems(targetId, cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
// If it already has a converting status then is must have been aborted during conversion
|
|
||||||
var result = _syncManager.GetJobItems(new SyncJobItemQuery
|
|
||||||
{
|
|
||||||
Statuses = new[] { SyncJobItemStatus.Queued, SyncJobItemStatus.Converting },
|
|
||||||
TargetId = targetId,
|
|
||||||
AddMetadata = false
|
|
||||||
});
|
|
||||||
|
|
||||||
await SyncJobItems(result.Items, enableConversion, progress, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SyncJobItems(SyncJobItem[] items, bool enableConversion, IProgress<double> progress, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (items.Length > 0)
|
|
||||||
{
|
|
||||||
if (!SyncRegistrationInfo.Instance.IsRegistered)
|
|
||||||
{
|
|
||||||
_logger.Debug("Cancelling sync job processing. Please obtain a supporter membership.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var numComplete = 0;
|
|
||||||
|
|
||||||
foreach (var item in items)
|
|
||||||
{
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
double percentPerItem = 1;
|
|
||||||
percentPerItem /= items.Length;
|
|
||||||
var startingPercent = numComplete * percentPerItem * 100;
|
|
||||||
|
|
||||||
var innerProgress = new ActionableProgress<double>();
|
|
||||||
innerProgress.RegisterAction(p => progress.Report(startingPercent + percentPerItem * p));
|
|
||||||
|
|
||||||
// Pull it fresh from the db just to make sure it wasn't deleted or cancelled while another item was converting
|
|
||||||
var jobItem = enableConversion ? _syncRepo.GetJobItem(item.Id) : item;
|
|
||||||
|
|
||||||
if (jobItem != null)
|
|
||||||
{
|
|
||||||
if (jobItem.Status != SyncJobItemStatus.Cancelled)
|
|
||||||
{
|
|
||||||
await ProcessJobItem(jobItem, enableConversion, innerProgress, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
await UpdateJobStatus(jobItem.JobId).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
numComplete++;
|
|
||||||
double percent = numComplete;
|
|
||||||
percent /= items.Length;
|
|
||||||
progress.Report(100 * percent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ProcessJobItem(SyncJobItem jobItem, bool enableConversion, IProgress<double> progress, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (jobItem == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException("jobItem");
|
|
||||||
}
|
|
||||||
|
|
||||||
var item = _libraryManager.GetItemById(jobItem.ItemId);
|
|
||||||
if (item == null)
|
|
||||||
{
|
|
||||||
jobItem.Status = SyncJobItemStatus.Failed;
|
|
||||||
_logger.Error("Unable to locate library item for JobItem {0}, ItemId {1}", jobItem.Id, jobItem.ItemId);
|
|
||||||
await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
jobItem.Progress = 0;
|
|
||||||
|
|
||||||
var job = _syncManager.GetJob(jobItem.JobId);
|
|
||||||
if (job == null)
|
|
||||||
{
|
|
||||||
_logger.Error("Job not found. Cannot complete the sync job.");
|
|
||||||
await _syncManager.CancelJobItem(jobItem.Id).ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var user = _userManager.GetUserById(job.UserId);
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
jobItem.Status = SyncJobItemStatus.Failed;
|
|
||||||
_logger.Error("User not found. Cannot complete the sync job.");
|
|
||||||
await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// See if there's already another active job item for the same target
|
|
||||||
var existingJobItems = _syncManager.GetJobItems(new SyncJobItemQuery
|
|
||||||
{
|
|
||||||
AddMetadata = false,
|
|
||||||
ItemId = jobItem.ItemId,
|
|
||||||
TargetId = jobItem.TargetId,
|
|
||||||
Statuses = new[] { SyncJobItemStatus.Converting, SyncJobItemStatus.Queued, SyncJobItemStatus.ReadyToTransfer, SyncJobItemStatus.Synced, SyncJobItemStatus.Transferring }
|
|
||||||
});
|
|
||||||
|
|
||||||
var duplicateJobItems = existingJobItems.Items
|
|
||||||
.Where(i => !string.Equals(i.Id, jobItem.Id, StringComparison.OrdinalIgnoreCase))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (duplicateJobItems.Count > 0)
|
|
||||||
{
|
|
||||||
var syncProvider = _syncManager.GetSyncProvider(jobItem) as IHasDuplicateCheck;
|
|
||||||
|
|
||||||
if (!duplicateJobItems.Any(i => AllowDuplicateJobItem(syncProvider, i, jobItem)))
|
|
||||||
{
|
|
||||||
_logger.Debug("Cancelling sync job item because there is already another active job for the same target.");
|
|
||||||
jobItem.Status = SyncJobItemStatus.Cancelled;
|
|
||||||
await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var syncOptions = _config.GetSyncOptions();
|
|
||||||
|
|
||||||
var video = item as Video;
|
|
||||||
if (video != null)
|
|
||||||
{
|
|
||||||
await Sync(jobItem, video, user, enableConversion, syncOptions, progress, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (item is Audio)
|
|
||||||
{
|
|
||||||
await Sync(jobItem, (Audio)item, user, enableConversion, syncOptions, progress, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (item is Photo)
|
|
||||||
{
|
|
||||||
await Sync(jobItem, (Photo)item, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await SyncGeneric(jobItem, item, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool AllowDuplicateJobItem(IHasDuplicateCheck provider, SyncJobItem original, SyncJobItem duplicate)
|
|
||||||
{
|
|
||||||
if (provider != null)
|
|
||||||
{
|
|
||||||
return provider.AllowDuplicateJobItem(original, duplicate);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task Sync(SyncJobItem jobItem, Video item, User user, bool enableConversion, SyncOptions syncOptions, IProgress<double> progress, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var job = _syncManager.GetJob(jobItem.JobId);
|
|
||||||
var jobOptions = _syncManager.GetVideoOptions(jobItem, job);
|
|
||||||
var conversionOptions = new VideoOptions
|
|
||||||
{
|
|
||||||
Profile = jobOptions.DeviceProfile
|
|
||||||
};
|
|
||||||
|
|
||||||
conversionOptions.DeviceId = jobItem.TargetId;
|
|
||||||
conversionOptions.Context = EncodingContext.Static;
|
|
||||||
conversionOptions.ItemId = item.Id.ToString("N");
|
|
||||||
conversionOptions.MediaSources = _mediaSourceManager.GetStaticMediaSources(item, false, user).ToList();
|
|
||||||
|
|
||||||
var streamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildVideoItem(conversionOptions);
|
|
||||||
var mediaSource = streamInfo.MediaSource;
|
|
||||||
|
|
||||||
// No sense creating external subs if we're already burning one into the video
|
|
||||||
var externalSubs = streamInfo.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode ?
|
|
||||||
new List<SubtitleStreamInfo>() :
|
|
||||||
streamInfo.GetExternalSubtitles(false, true, null, null);
|
|
||||||
|
|
||||||
// Mark as requiring conversion if transcoding the video, or if any subtitles need to be extracted
|
|
||||||
var requiresVideoTranscoding = streamInfo.PlayMethod == PlayMethod.Transcode && jobOptions.IsConverting;
|
|
||||||
var requiresConversion = requiresVideoTranscoding || externalSubs.Any(i => RequiresExtraction(i, mediaSource));
|
|
||||||
|
|
||||||
if (requiresConversion && !enableConversion)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
jobItem.MediaSourceId = streamInfo.MediaSourceId;
|
|
||||||
jobItem.TemporaryPath = GetTemporaryPath(jobItem);
|
|
||||||
|
|
||||||
if (requiresConversion)
|
|
||||||
{
|
|
||||||
jobItem.Status = SyncJobItemStatus.Converting;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requiresVideoTranscoding)
|
|
||||||
{
|
|
||||||
// Save the job item now since conversion could take a while
|
|
||||||
await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false);
|
|
||||||
await UpdateJobStatus(jobItem.JobId).ConfigureAwait(false);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var lastJobUpdate = DateTime.MinValue;
|
|
||||||
var innerProgress = new ActionableProgress<double>();
|
|
||||||
innerProgress.RegisterAction(async pct =>
|
|
||||||
{
|
|
||||||
progress.Report(pct);
|
|
||||||
|
|
||||||
if ((DateTime.UtcNow - lastJobUpdate).TotalSeconds >= DatabaseProgressUpdateIntervalSeconds)
|
|
||||||
{
|
|
||||||
jobItem.Progress = pct / 2;
|
|
||||||
await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false);
|
|
||||||
await UpdateJobStatus(jobItem.JobId).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
jobItem.OutputPath = await _mediaEncoder.EncodeVideo(new EncodingJobOptions(streamInfo, conversionOptions.Profile)
|
|
||||||
{
|
|
||||||
OutputDirectory = jobItem.TemporaryPath,
|
|
||||||
CpuCoreLimit = syncOptions.TranscodingCpuCoreLimit,
|
|
||||||
ReadInputAtNativeFramerate = !syncOptions.EnableFullSpeedTranscoding
|
|
||||||
|
|
||||||
}, innerProgress, cancellationToken);
|
|
||||||
|
|
||||||
jobItem.ItemDateModifiedTicks = item.DateModified.Ticks;
|
|
||||||
_syncManager.OnConversionComplete(jobItem);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
jobItem.Status = SyncJobItemStatus.Queued;
|
|
||||||
jobItem.Progress = 0;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
jobItem.Status = SyncJobItemStatus.Failed;
|
|
||||||
_logger.ErrorException("Error during sync transcoding", ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jobItem.Status == SyncJobItemStatus.Failed || jobItem.Status == SyncJobItemStatus.Queued)
|
|
||||||
{
|
|
||||||
await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
jobItem.MediaSource = await GetEncodedMediaSource(jobItem.OutputPath, user, true).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (mediaSource.Protocol == MediaProtocol.File)
|
|
||||||
{
|
|
||||||
jobItem.OutputPath = mediaSource.Path;
|
|
||||||
}
|
|
||||||
else if (mediaSource.Protocol == MediaProtocol.Http)
|
|
||||||
{
|
|
||||||
jobItem.OutputPath = await DownloadFile(jobItem, mediaSource, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(string.Format("Cannot direct stream {0} protocol", mediaSource.Protocol));
|
|
||||||
}
|
|
||||||
|
|
||||||
jobItem.ItemDateModifiedTicks = item.DateModified.Ticks;
|
|
||||||
jobItem.MediaSource = mediaSource;
|
|
||||||
}
|
|
||||||
|
|
||||||
jobItem.MediaSource.SupportsTranscoding = false;
|
|
||||||
|
|
||||||
if (externalSubs.Count > 0)
|
|
||||||
{
|
|
||||||
// Save the job item now since conversion could take a while
|
|
||||||
await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false);
|
|
||||||
|
|
||||||
await ConvertSubtitles(jobItem, externalSubs, streamInfo, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
jobItem.Progress = 50;
|
|
||||||
jobItem.Status = SyncJobItemStatus.ReadyToTransfer;
|
|
||||||
await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool RequiresExtraction(SubtitleStreamInfo stream, MediaSourceInfo mediaSource)
|
|
||||||
{
|
|
||||||
var originalStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Subtitle && i.Index == stream.Index);
|
|
||||||
|
|
||||||
return originalStream != null && !originalStream.IsExternal;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ConvertSubtitles(SyncJobItem jobItem,
|
|
||||||
IEnumerable<SubtitleStreamInfo> subtitles,
|
|
||||||
StreamInfo streamInfo,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var files = new List<ItemFileInfo>();
|
|
||||||
|
|
||||||
var mediaStreams = jobItem.MediaSource.MediaStreams
|
|
||||||
.Where(i => i.Type != MediaStreamType.Subtitle || !i.IsExternal)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var startingIndex = mediaStreams.Count == 0 ?
|
|
||||||
0 :
|
|
||||||
mediaStreams.Select(i => i.Index).Max() + 1;
|
|
||||||
|
|
||||||
foreach (var subtitle in subtitles)
|
|
||||||
{
|
|
||||||
var fileInfo = await ConvertSubtitles(jobItem.TemporaryPath, streamInfo, subtitle, cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
// Reset this to a value that will be based on the output media
|
|
||||||
fileInfo.Index = startingIndex;
|
|
||||||
files.Add(fileInfo);
|
|
||||||
|
|
||||||
mediaStreams.Add(new MediaStream
|
|
||||||
{
|
|
||||||
Index = startingIndex,
|
|
||||||
Codec = subtitle.Format,
|
|
||||||
IsForced = subtitle.IsForced,
|
|
||||||
IsExternal = true,
|
|
||||||
Language = subtitle.Language,
|
|
||||||
Path = fileInfo.Path,
|
|
||||||
SupportsExternalStream = true,
|
|
||||||
Type = MediaStreamType.Subtitle
|
|
||||||
});
|
|
||||||
|
|
||||||
startingIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
jobItem.AdditionalFiles.AddRange(files);
|
|
||||||
|
|
||||||
jobItem.MediaSource.MediaStreams = mediaStreams;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<ItemFileInfo> ConvertSubtitles(string temporaryPath, StreamInfo streamInfo, SubtitleStreamInfo subtitleStreamInfo, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var subtitleStreamIndex = subtitleStreamInfo.Index;
|
|
||||||
|
|
||||||
var filename = Guid.NewGuid() + "." + subtitleStreamInfo.Format.ToLower();
|
|
||||||
|
|
||||||
var path = Path.Combine(temporaryPath, filename);
|
|
||||||
|
|
||||||
_fileSystem.CreateDirectory(Path.GetDirectoryName(path));
|
|
||||||
|
|
||||||
using (var stream = await _subtitleEncoder.GetSubtitles(streamInfo.ItemId, streamInfo.MediaSourceId, subtitleStreamIndex, subtitleStreamInfo.Format, 0, null, false, cancellationToken).ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
using (var fs = _fileSystem.GetFileStream(path, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true))
|
|
||||||
{
|
|
||||||
await stream.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ItemFileInfo
|
|
||||||
{
|
|
||||||
Name = Path.GetFileName(path),
|
|
||||||
Path = path,
|
|
||||||
Type = ItemFileType.Subtitles,
|
|
||||||
Index = subtitleStreamIndex
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private const int DatabaseProgressUpdateIntervalSeconds = 2;
|
|
||||||
|
|
||||||
private async Task Sync(SyncJobItem jobItem, Audio item, User user, bool enableConversion, SyncOptions syncOptions, IProgress<double> progress, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var job = _syncManager.GetJob(jobItem.JobId);
|
|
||||||
var jobOptions = _syncManager.GetAudioOptions(jobItem, job);
|
|
||||||
var conversionOptions = new AudioOptions
|
|
||||||
{
|
|
||||||
Profile = jobOptions.DeviceProfile
|
|
||||||
};
|
|
||||||
|
|
||||||
conversionOptions.DeviceId = jobItem.TargetId;
|
|
||||||
conversionOptions.Context = EncodingContext.Static;
|
|
||||||
conversionOptions.ItemId = item.Id.ToString("N");
|
|
||||||
conversionOptions.MediaSources = _mediaSourceManager.GetStaticMediaSources(item, false, user).ToList();
|
|
||||||
|
|
||||||
var streamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildAudioItem(conversionOptions);
|
|
||||||
var mediaSource = streamInfo.MediaSource;
|
|
||||||
|
|
||||||
jobItem.MediaSourceId = streamInfo.MediaSourceId;
|
|
||||||
jobItem.TemporaryPath = GetTemporaryPath(jobItem);
|
|
||||||
|
|
||||||
if (streamInfo.PlayMethod == PlayMethod.Transcode && jobOptions.IsConverting)
|
|
||||||
{
|
|
||||||
if (!enableConversion)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
jobItem.Status = SyncJobItemStatus.Converting;
|
|
||||||
await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false);
|
|
||||||
await UpdateJobStatus(jobItem.JobId).ConfigureAwait(false);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var lastJobUpdate = DateTime.MinValue;
|
|
||||||
var innerProgress = new ActionableProgress<double>();
|
|
||||||
innerProgress.RegisterAction(async pct =>
|
|
||||||
{
|
|
||||||
progress.Report(pct);
|
|
||||||
|
|
||||||
if ((DateTime.UtcNow - lastJobUpdate).TotalSeconds >= DatabaseProgressUpdateIntervalSeconds)
|
|
||||||
{
|
|
||||||
jobItem.Progress = pct / 2;
|
|
||||||
await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false);
|
|
||||||
await UpdateJobStatus(jobItem.JobId).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
jobItem.OutputPath = await _mediaEncoder.EncodeAudio(new EncodingJobOptions(streamInfo, conversionOptions.Profile)
|
|
||||||
{
|
|
||||||
OutputDirectory = jobItem.TemporaryPath,
|
|
||||||
CpuCoreLimit = syncOptions.TranscodingCpuCoreLimit
|
|
||||||
|
|
||||||
}, innerProgress, cancellationToken);
|
|
||||||
|
|
||||||
jobItem.ItemDateModifiedTicks = item.DateModified.Ticks;
|
|
||||||
_syncManager.OnConversionComplete(jobItem);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
jobItem.Status = SyncJobItemStatus.Queued;
|
|
||||||
jobItem.Progress = 0;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
jobItem.Status = SyncJobItemStatus.Failed;
|
|
||||||
_logger.ErrorException("Error during sync transcoding", ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jobItem.Status == SyncJobItemStatus.Failed || jobItem.Status == SyncJobItemStatus.Queued)
|
|
||||||
{
|
|
||||||
await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
jobItem.MediaSource = await GetEncodedMediaSource(jobItem.OutputPath, user, false).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (mediaSource.Protocol == MediaProtocol.File)
|
|
||||||
{
|
|
||||||
jobItem.OutputPath = mediaSource.Path;
|
|
||||||
}
|
|
||||||
else if (mediaSource.Protocol == MediaProtocol.Http)
|
|
||||||
{
|
|
||||||
jobItem.OutputPath = await DownloadFile(jobItem, mediaSource, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(string.Format("Cannot direct stream {0} protocol", mediaSource.Protocol));
|
|
||||||
}
|
|
||||||
|
|
||||||
jobItem.ItemDateModifiedTicks = item.DateModified.Ticks;
|
|
||||||
jobItem.MediaSource = mediaSource;
|
|
||||||
}
|
|
||||||
|
|
||||||
jobItem.MediaSource.SupportsTranscoding = false;
|
|
||||||
|
|
||||||
jobItem.Progress = 50;
|
|
||||||
jobItem.Status = SyncJobItemStatus.ReadyToTransfer;
|
|
||||||
await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task Sync(SyncJobItem jobItem, Photo item, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
jobItem.OutputPath = item.Path;
|
|
||||||
|
|
||||||
jobItem.Progress = 50;
|
|
||||||
jobItem.Status = SyncJobItemStatus.ReadyToTransfer;
|
|
||||||
jobItem.ItemDateModifiedTicks = item.DateModified.Ticks;
|
|
||||||
await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SyncGeneric(SyncJobItem jobItem, BaseItem item, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
jobItem.OutputPath = item.Path;
|
|
||||||
|
|
||||||
jobItem.Progress = 50;
|
|
||||||
jobItem.Status = SyncJobItemStatus.ReadyToTransfer;
|
|
||||||
jobItem.ItemDateModifiedTicks = item.DateModified.Ticks;
|
|
||||||
await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<string> DownloadFile(SyncJobItem jobItem, MediaSourceInfo mediaSource, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
// TODO: Download
|
|
||||||
return mediaSource.Path;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetTemporaryPath(SyncJob job)
|
|
||||||
{
|
|
||||||
return GetTemporaryPath(job.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetTemporaryPath(string jobId)
|
|
||||||
{
|
|
||||||
var basePath = _config.GetSyncOptions().TemporaryPath;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(basePath))
|
|
||||||
{
|
|
||||||
basePath = Path.Combine(_config.CommonApplicationPaths.ProgramDataPath, "sync");
|
|
||||||
}
|
|
||||||
|
|
||||||
return Path.Combine(basePath, jobId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetTemporaryPath(SyncJobItem jobItem)
|
|
||||||
{
|
|
||||||
return Path.Combine(GetTemporaryPath(jobItem.JobId), jobItem.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<MediaSourceInfo> GetEncodedMediaSource(string path, User user, bool isVideo)
|
|
||||||
{
|
|
||||||
var item = _libraryManager.ResolvePath(_fileSystem.GetFileSystemInfo(path));
|
|
||||||
|
|
||||||
await item.RefreshMetadata(CancellationToken.None).ConfigureAwait(false);
|
|
||||||
|
|
||||||
var hasMediaSources = item as IHasMediaSources;
|
|
||||||
|
|
||||||
var mediaSources = _mediaSourceManager.GetStaticMediaSources(hasMediaSources, false).ToList();
|
|
||||||
|
|
||||||
var preferredAudio = string.IsNullOrEmpty(user.Configuration.AudioLanguagePreference)
|
|
||||||
? new string[] { }
|
|
||||||
: new[] { user.Configuration.AudioLanguagePreference };
|
|
||||||
|
|
||||||
var preferredSubs = string.IsNullOrEmpty(user.Configuration.SubtitleLanguagePreference)
|
|
||||||
? new List<string>() : new List<string> { user.Configuration.SubtitleLanguagePreference };
|
|
||||||
|
|
||||||
foreach (var source in mediaSources)
|
|
||||||
{
|
|
||||||
if (isVideo)
|
|
||||||
{
|
|
||||||
source.DefaultAudioStreamIndex =
|
|
||||||
MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.Configuration.PlayDefaultAudioTrack);
|
|
||||||
|
|
||||||
var defaultAudioIndex = source.DefaultAudioStreamIndex;
|
|
||||||
var audioLangage = defaultAudioIndex == null
|
|
||||||
? null
|
|
||||||
: source.MediaStreams.Where(i => i.Type == MediaStreamType.Audio && i.Index == defaultAudioIndex).Select(i => i.Language).FirstOrDefault();
|
|
||||||
|
|
||||||
source.DefaultAudioStreamIndex =
|
|
||||||
MediaStreamSelector.GetDefaultSubtitleStreamIndex(source.MediaStreams, preferredSubs, user.Configuration.SubtitleMode, audioLangage);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var audio = source.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
|
|
||||||
|
|
||||||
if (audio != null)
|
|
||||||
{
|
|
||||||
source.DefaultAudioStreamIndex = audio.Index;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return mediaSources.FirstOrDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,60 +0,0 @@
|
||||||
using System.Threading;
|
|
||||||
using MediaBrowser.Controller.Plugins;
|
|
||||||
using MediaBrowser.Controller.Session;
|
|
||||||
using MediaBrowser.Controller.Sync;
|
|
||||||
using MediaBrowser.Model.Events;
|
|
||||||
using MediaBrowser.Model.Sync;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Sync
|
|
||||||
{
|
|
||||||
public class SyncNotificationEntryPoint : IServerEntryPoint
|
|
||||||
{
|
|
||||||
private readonly ISessionManager _sessionManager;
|
|
||||||
private readonly ISyncManager _syncManager;
|
|
||||||
|
|
||||||
public SyncNotificationEntryPoint(ISyncManager syncManager, ISessionManager sessionManager)
|
|
||||||
{
|
|
||||||
_syncManager = syncManager;
|
|
||||||
_sessionManager = sessionManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Run()
|
|
||||||
{
|
|
||||||
_syncManager.SyncJobItemUpdated += _syncManager_SyncJobItemUpdated;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void _syncManager_SyncJobItemUpdated(object sender, GenericEventArgs<SyncJobItem> e)
|
|
||||||
{
|
|
||||||
var item = e.Argument;
|
|
||||||
|
|
||||||
if (item.Status == SyncJobItemStatus.ReadyToTransfer)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _sessionManager.SendMessageToUserDeviceSessions(item.TargetId, "SyncJobItemReady", item, CancellationToken.None).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.Status == SyncJobItemStatus.Cancelled)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _sessionManager.SendMessageToUserDeviceSessions(item.TargetId, "SyncJobItemCancelled", item, CancellationToken.None).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_syncManager.SyncJobItemUpdated -= _syncManager_SyncJobItemUpdated;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
using MediaBrowser.Common.Security;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Sync
|
|
||||||
{
|
|
||||||
public class SyncRegistrationInfo : IRequiresRegistration
|
|
||||||
{
|
|
||||||
private readonly ISecurityManager _securityManager;
|
|
||||||
|
|
||||||
public static SyncRegistrationInfo Instance;
|
|
||||||
|
|
||||||
public SyncRegistrationInfo(ISecurityManager securityManager)
|
|
||||||
{
|
|
||||||
_securityManager = securityManager;
|
|
||||||
Instance = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool _registered;
|
|
||||||
public bool IsRegistered
|
|
||||||
{
|
|
||||||
get { return _registered; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task LoadRegistrationInfoAsync()
|
|
||||||
{
|
|
||||||
var info = await _securityManager.GetRegistrationStatus("sync").ConfigureAwait(false);
|
|
||||||
|
|
||||||
_registered = info.IsValid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,847 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Emby.Server.Implementations.Data;
|
|
||||||
using MediaBrowser.Controller;
|
|
||||||
using MediaBrowser.Controller.Sync;
|
|
||||||
using MediaBrowser.Model.Dto;
|
|
||||||
using MediaBrowser.Model.Logging;
|
|
||||||
using MediaBrowser.Model.Querying;
|
|
||||||
using MediaBrowser.Model.Serialization;
|
|
||||||
using MediaBrowser.Model.Sync;
|
|
||||||
using SQLitePCL.pretty;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Sync
|
|
||||||
{
|
|
||||||
public class SyncRepository : BaseSqliteRepository, ISyncRepository
|
|
||||||
{
|
|
||||||
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
|
||||||
|
|
||||||
private readonly IJsonSerializer _json;
|
|
||||||
|
|
||||||
public SyncRepository(ILogger logger, IJsonSerializer json, IServerApplicationPaths appPaths)
|
|
||||||
: base(logger)
|
|
||||||
{
|
|
||||||
_json = json;
|
|
||||||
DbFilePath = Path.Combine(appPaths.DataPath, "sync14.db");
|
|
||||||
}
|
|
||||||
|
|
||||||
private class SyncSummary
|
|
||||||
{
|
|
||||||
public Dictionary<string, int> Items { get; set; }
|
|
||||||
|
|
||||||
public SyncSummary()
|
|
||||||
{
|
|
||||||
Items = new Dictionary<string, int>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Initialize()
|
|
||||||
{
|
|
||||||
using (var connection = CreateConnection())
|
|
||||||
{
|
|
||||||
RunDefaultInitialization(connection);
|
|
||||||
|
|
||||||
string[] queries = {
|
|
||||||
|
|
||||||
"create table if not exists SyncJobs (Id GUID PRIMARY KEY, TargetId TEXT NOT NULL, Name TEXT NOT NULL, Profile TEXT, Quality TEXT, Bitrate INT, Status TEXT NOT NULL, Progress FLOAT, UserId TEXT NOT NULL, ItemIds TEXT NOT NULL, Category TEXT, ParentId TEXT, UnwatchedOnly BIT, ItemLimit INT, SyncNewContent BIT, DateCreated DateTime, DateLastModified DateTime, ItemCount int)",
|
|
||||||
|
|
||||||
"create table if not exists SyncJobItems (Id GUID PRIMARY KEY, ItemId TEXT, ItemName TEXT, MediaSourceId TEXT, JobId TEXT, TemporaryPath TEXT, OutputPath TEXT, Status TEXT, TargetId TEXT, DateCreated DateTime, Progress FLOAT, AdditionalFiles TEXT, MediaSource TEXT, IsMarkedForRemoval BIT, JobItemIndex INT, ItemDateModifiedTicks BIGINT)",
|
|
||||||
|
|
||||||
"drop index if exists idx_SyncJobItems2",
|
|
||||||
"drop index if exists idx_SyncJobItems3",
|
|
||||||
"drop index if exists idx_SyncJobs1",
|
|
||||||
"drop index if exists idx_SyncJobs",
|
|
||||||
"drop index if exists idx_SyncJobItems1",
|
|
||||||
"create index if not exists idx_SyncJobItems4 on SyncJobItems(TargetId,ItemId,Status,Progress,DateCreated)",
|
|
||||||
"create index if not exists idx_SyncJobItems5 on SyncJobItems(TargetId,Status,ItemId,Progress)",
|
|
||||||
|
|
||||||
"create index if not exists idx_SyncJobs2 on SyncJobs(TargetId,Status,ItemIds,Progress)",
|
|
||||||
|
|
||||||
"pragma shrink_memory"
|
|
||||||
};
|
|
||||||
|
|
||||||
connection.RunQueries(queries);
|
|
||||||
|
|
||||||
connection.RunInTransaction(db =>
|
|
||||||
{
|
|
||||||
var existingColumnNames = GetColumnNames(db, "SyncJobs");
|
|
||||||
AddColumn(db, "SyncJobs", "Profile", "TEXT", existingColumnNames);
|
|
||||||
AddColumn(db, "SyncJobs", "Bitrate", "INT", existingColumnNames);
|
|
||||||
|
|
||||||
existingColumnNames = GetColumnNames(db, "SyncJobItems");
|
|
||||||
AddColumn(db, "SyncJobItems", "ItemDateModifiedTicks", "BIGINT", existingColumnNames);
|
|
||||||
}, TransactionMode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool EnableTempStoreMemory
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private const string BaseJobSelectText = "select Id, TargetId, Name, Profile, Quality, Bitrate, Status, Progress, UserId, ItemIds, Category, ParentId, UnwatchedOnly, ItemLimit, SyncNewContent, DateCreated, DateLastModified, ItemCount from SyncJobs";
|
|
||||||
private const string BaseJobItemSelectText = "select Id, ItemId, ItemName, MediaSourceId, JobId, TemporaryPath, OutputPath, Status, TargetId, DateCreated, Progress, AdditionalFiles, MediaSource, IsMarkedForRemoval, JobItemIndex, ItemDateModifiedTicks from SyncJobItems";
|
|
||||||
|
|
||||||
public SyncJob GetJob(string id)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(id))
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException("id");
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckDisposed();
|
|
||||||
|
|
||||||
var guid = new Guid(id);
|
|
||||||
|
|
||||||
if (guid == Guid.Empty)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException("id");
|
|
||||||
}
|
|
||||||
|
|
||||||
using (WriteLock.Read())
|
|
||||||
{
|
|
||||||
using (var connection = CreateConnection(true))
|
|
||||||
{
|
|
||||||
var commandText = BaseJobSelectText + " where Id=?";
|
|
||||||
var paramList = new List<object>();
|
|
||||||
|
|
||||||
paramList.Add(guid.ToGuidParamValue());
|
|
||||||
|
|
||||||
foreach (var row in connection.Query(commandText, paramList.ToArray()))
|
|
||||||
{
|
|
||||||
return GetJob(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private SyncJob GetJob(IReadOnlyList<IResultSetValue> reader)
|
|
||||||
{
|
|
||||||
var info = new SyncJob
|
|
||||||
{
|
|
||||||
Id = reader[0].ReadGuid().ToString("N"),
|
|
||||||
TargetId = reader[1].ToString(),
|
|
||||||
Name = reader[2].ToString()
|
|
||||||
};
|
|
||||||
|
|
||||||
if (reader[3].SQLiteType != SQLiteType.Null)
|
|
||||||
{
|
|
||||||
info.Profile = reader[3].ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reader[4].SQLiteType != SQLiteType.Null)
|
|
||||||
{
|
|
||||||
info.Quality = reader[4].ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reader[5].SQLiteType != SQLiteType.Null)
|
|
||||||
{
|
|
||||||
info.Bitrate = reader[5].ToInt();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reader[6].SQLiteType != SQLiteType.Null)
|
|
||||||
{
|
|
||||||
info.Status = (SyncJobStatus)Enum.Parse(typeof(SyncJobStatus), reader[6].ToString(), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reader[7].SQLiteType != SQLiteType.Null)
|
|
||||||
{
|
|
||||||
info.Progress = reader[7].ToDouble();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reader[8].SQLiteType != SQLiteType.Null)
|
|
||||||
{
|
|
||||||
info.UserId = reader[8].ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reader[9].SQLiteType != SQLiteType.Null)
|
|
||||||
{
|
|
||||||
info.RequestedItemIds = reader[9].ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reader[10].SQLiteType != SQLiteType.Null)
|
|
||||||
{
|
|
||||||
info.Category = (SyncCategory)Enum.Parse(typeof(SyncCategory), reader[10].ToString(), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reader[11].SQLiteType != SQLiteType.Null)
|
|
||||||
{
|
|
||||||
info.ParentId = reader[11].ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reader[12].SQLiteType != SQLiteType.Null)
|
|
||||||
{
|
|
||||||
info.UnwatchedOnly = reader[12].ToBool();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reader[13].SQLiteType != SQLiteType.Null)
|
|
||||||
{
|
|
||||||
info.ItemLimit = reader[13].ToInt();
|
|
||||||
}
|
|
||||||
|
|
||||||
info.SyncNewContent = reader[14].ToBool();
|
|
||||||
|
|
||||||
info.DateCreated = reader[15].ReadDateTime();
|
|
||||||
info.DateLastModified = reader[16].ReadDateTime();
|
|
||||||
info.ItemCount = reader[17].ToInt();
|
|
||||||
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task Create(SyncJob job)
|
|
||||||
{
|
|
||||||
return InsertOrUpdate(job, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task Update(SyncJob job)
|
|
||||||
{
|
|
||||||
return InsertOrUpdate(job, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task InsertOrUpdate(SyncJob job, bool insert)
|
|
||||||
{
|
|
||||||
if (job == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException("job");
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckDisposed();
|
|
||||||
|
|
||||||
using (WriteLock.Write())
|
|
||||||
{
|
|
||||||
using (var connection = CreateConnection())
|
|
||||||
{
|
|
||||||
string commandText;
|
|
||||||
|
|
||||||
if (insert)
|
|
||||||
{
|
|
||||||
commandText = "insert into SyncJobs (Id, TargetId, Name, Profile, Quality, Bitrate, Status, Progress, UserId, ItemIds, Category, ParentId, UnwatchedOnly, ItemLimit, SyncNewContent, DateCreated, DateLastModified, ItemCount) values (@Id, @TargetId, @Name, @Profile, @Quality, @Bitrate, @Status, @Progress, @UserId, @ItemIds, @Category, @ParentId, @UnwatchedOnly, @ItemLimit, @SyncNewContent, @DateCreated, @DateLastModified, @ItemCount)";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
commandText = "update SyncJobs set TargetId=@TargetId,Name=@Name,Profile=@Profile,Quality=@Quality,Bitrate=@Bitrate,Status=@Status,Progress=@Progress,UserId=@UserId,ItemIds=@ItemIds,Category=@Category,ParentId=@ParentId,UnwatchedOnly=@UnwatchedOnly,ItemLimit=@ItemLimit,SyncNewContent=@SyncNewContent,DateCreated=@DateCreated,DateLastModified=@DateLastModified,ItemCount=@ItemCount where Id=@Id";
|
|
||||||
}
|
|
||||||
|
|
||||||
connection.RunInTransaction(conn =>
|
|
||||||
{
|
|
||||||
using (var statement = PrepareStatementSafe(connection, commandText))
|
|
||||||
{
|
|
||||||
statement.TryBind("@TargetId", job.TargetId);
|
|
||||||
statement.TryBind("@Name", job.Name);
|
|
||||||
statement.TryBind("@Profile", job.Profile);
|
|
||||||
statement.TryBind("@Quality", job.Quality);
|
|
||||||
statement.TryBind("@Bitrate", job.Bitrate);
|
|
||||||
statement.TryBind("@Status", job.Status.ToString());
|
|
||||||
statement.TryBind("@Progress", job.Progress);
|
|
||||||
statement.TryBind("@UserId", job.UserId);
|
|
||||||
|
|
||||||
statement.TryBind("@ItemIds", string.Join(",", job.RequestedItemIds.ToArray()));
|
|
||||||
|
|
||||||
if (job.Category.HasValue)
|
|
||||||
{
|
|
||||||
statement.TryBind("@Category", job.Category.Value.ToString());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
statement.TryBindNull("@Category");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(job.ParentId))
|
|
||||||
{
|
|
||||||
statement.TryBind("@ParentId", job.ParentId);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
statement.TryBindNull("@ParentId");
|
|
||||||
}
|
|
||||||
|
|
||||||
statement.TryBind("@UnwatchedOnly", job.UnwatchedOnly);
|
|
||||||
|
|
||||||
if (job.ItemLimit.HasValue)
|
|
||||||
{
|
|
||||||
statement.TryBind("@ItemLimit", job.ItemLimit);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
statement.TryBindNull("@ItemLimit");
|
|
||||||
}
|
|
||||||
|
|
||||||
statement.TryBind("@SyncNewContent", job.SyncNewContent);
|
|
||||||
|
|
||||||
statement.TryBind("@DateCreated", job.DateCreated.ToDateTimeParamValue());
|
|
||||||
statement.TryBind("@DateLastModified", job.DateLastModified.ToDateTimeParamValue());
|
|
||||||
|
|
||||||
statement.TryBind("@ItemCount", job.ItemCount);
|
|
||||||
statement.TryBind("@Id", job.Id.ToGuidParamValue());
|
|
||||||
|
|
||||||
statement.MoveNext();
|
|
||||||
}
|
|
||||||
}, TransactionMode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DeleteJob(string id)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(id))
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException("id");
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckDisposed();
|
|
||||||
|
|
||||||
using (WriteLock.Write())
|
|
||||||
{
|
|
||||||
using (var connection = CreateConnection())
|
|
||||||
{
|
|
||||||
connection.RunInTransaction(conn =>
|
|
||||||
{
|
|
||||||
conn.Execute("delete from SyncJobs where Id=?", id.ToGuidParamValue());
|
|
||||||
conn.Execute("delete from SyncJobItems where JobId=?", id);
|
|
||||||
}, TransactionMode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public QueryResult<SyncJob> GetJobs(SyncJobQuery query)
|
|
||||||
{
|
|
||||||
if (query == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException("query");
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckDisposed();
|
|
||||||
|
|
||||||
using (WriteLock.Read())
|
|
||||||
{
|
|
||||||
using (var connection = CreateConnection(true))
|
|
||||||
{
|
|
||||||
var commandText = BaseJobSelectText;
|
|
||||||
var paramList = new List<object>();
|
|
||||||
|
|
||||||
var whereClauses = new List<string>();
|
|
||||||
|
|
||||||
if (query.Statuses.Length > 0)
|
|
||||||
{
|
|
||||||
var statuses = string.Join(",", query.Statuses.Select(i => "'" + i.ToString() + "'").ToArray());
|
|
||||||
|
|
||||||
whereClauses.Add(string.Format("Status in ({0})", statuses));
|
|
||||||
}
|
|
||||||
if (!string.IsNullOrWhiteSpace(query.TargetId))
|
|
||||||
{
|
|
||||||
whereClauses.Add("TargetId=?");
|
|
||||||
paramList.Add(query.TargetId);
|
|
||||||
}
|
|
||||||
if (!string.IsNullOrWhiteSpace(query.ExcludeTargetIds))
|
|
||||||
{
|
|
||||||
var excludeIds = (query.ExcludeTargetIds ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
if (excludeIds.Length == 1)
|
|
||||||
{
|
|
||||||
whereClauses.Add("TargetId<>?");
|
|
||||||
paramList.Add(excludeIds[0]);
|
|
||||||
}
|
|
||||||
else if (excludeIds.Length > 1)
|
|
||||||
{
|
|
||||||
whereClauses.Add("TargetId<>?");
|
|
||||||
paramList.Add(excludeIds[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!string.IsNullOrWhiteSpace(query.UserId))
|
|
||||||
{
|
|
||||||
whereClauses.Add("UserId=?");
|
|
||||||
paramList.Add(query.UserId);
|
|
||||||
}
|
|
||||||
if (!string.IsNullOrWhiteSpace(query.ItemId))
|
|
||||||
{
|
|
||||||
whereClauses.Add("ItemIds like ?");
|
|
||||||
paramList.Add("%" + query.ItemId + "%");
|
|
||||||
}
|
|
||||||
if (query.SyncNewContent.HasValue)
|
|
||||||
{
|
|
||||||
whereClauses.Add("SyncNewContent=?");
|
|
||||||
paramList.Add(query.SyncNewContent.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
commandText += " mainTable";
|
|
||||||
|
|
||||||
var whereTextWithoutPaging = whereClauses.Count == 0 ?
|
|
||||||
string.Empty :
|
|
||||||
" where " + string.Join(" AND ", whereClauses.ToArray());
|
|
||||||
|
|
||||||
var startIndex = query.StartIndex ?? 0;
|
|
||||||
if (startIndex > 0)
|
|
||||||
{
|
|
||||||
whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM SyncJobs ORDER BY (Select Max(DateLastModified) from SyncJobs where TargetId=mainTable.TargetId) DESC, DateLastModified DESC LIMIT {0})",
|
|
||||||
startIndex.ToString(_usCulture)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (whereClauses.Count > 0)
|
|
||||||
{
|
|
||||||
commandText += " where " + string.Join(" AND ", whereClauses.ToArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
commandText += " ORDER BY (Select Max(DateLastModified) from SyncJobs where TargetId=mainTable.TargetId) DESC, DateLastModified DESC";
|
|
||||||
|
|
||||||
if (query.Limit.HasValue)
|
|
||||||
{
|
|
||||||
commandText += " LIMIT " + query.Limit.Value.ToString(_usCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
var list = new List<SyncJob>();
|
|
||||||
var count = connection.Query("select count (Id) from SyncJobs" + whereTextWithoutPaging, paramList.ToArray())
|
|
||||||
.SelectScalarInt()
|
|
||||||
.First();
|
|
||||||
|
|
||||||
foreach (var row in connection.Query(commandText, paramList.ToArray()))
|
|
||||||
{
|
|
||||||
list.Add(GetJob(row));
|
|
||||||
}
|
|
||||||
|
|
||||||
return new QueryResult<SyncJob>()
|
|
||||||
{
|
|
||||||
Items = list.ToArray(),
|
|
||||||
TotalRecordCount = count
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public SyncJobItem GetJobItem(string id)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(id))
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException("id");
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckDisposed();
|
|
||||||
|
|
||||||
var guid = new Guid(id);
|
|
||||||
|
|
||||||
using (WriteLock.Read())
|
|
||||||
{
|
|
||||||
using (var connection = CreateConnection(true))
|
|
||||||
{
|
|
||||||
var commandText = BaseJobItemSelectText + " where Id=?";
|
|
||||||
var paramList = new List<object>();
|
|
||||||
|
|
||||||
paramList.Add(guid.ToGuidParamValue());
|
|
||||||
|
|
||||||
foreach (var row in connection.Query(commandText, paramList.ToArray()))
|
|
||||||
{
|
|
||||||
return GetJobItem(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private QueryResult<T> GetJobItemReader<T>(SyncJobItemQuery query, string baseSelectText, Func<IReadOnlyList<IResultSetValue>, T> itemFactory)
|
|
||||||
{
|
|
||||||
if (query == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException("query");
|
|
||||||
}
|
|
||||||
|
|
||||||
using (WriteLock.Read())
|
|
||||||
{
|
|
||||||
using (var connection = CreateConnection(true))
|
|
||||||
{
|
|
||||||
var commandText = baseSelectText;
|
|
||||||
var paramList = new List<object>();
|
|
||||||
|
|
||||||
var whereClauses = new List<string>();
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(query.JobId))
|
|
||||||
{
|
|
||||||
whereClauses.Add("JobId=?");
|
|
||||||
paramList.Add(query.JobId);
|
|
||||||
}
|
|
||||||
if (!string.IsNullOrWhiteSpace(query.ItemId))
|
|
||||||
{
|
|
||||||
whereClauses.Add("ItemId=?");
|
|
||||||
paramList.Add(query.ItemId);
|
|
||||||
}
|
|
||||||
if (!string.IsNullOrWhiteSpace(query.TargetId))
|
|
||||||
{
|
|
||||||
whereClauses.Add("TargetId=?");
|
|
||||||
paramList.Add(query.TargetId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.Statuses.Length > 0)
|
|
||||||
{
|
|
||||||
var statuses = string.Join(",", query.Statuses.Select(i => "'" + i.ToString() + "'").ToArray());
|
|
||||||
|
|
||||||
whereClauses.Add(string.Format("Status in ({0})", statuses));
|
|
||||||
}
|
|
||||||
|
|
||||||
var whereTextWithoutPaging = whereClauses.Count == 0 ?
|
|
||||||
string.Empty :
|
|
||||||
" where " + string.Join(" AND ", whereClauses.ToArray());
|
|
||||||
|
|
||||||
var startIndex = query.StartIndex ?? 0;
|
|
||||||
if (startIndex > 0)
|
|
||||||
{
|
|
||||||
whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM SyncJobItems ORDER BY JobItemIndex, DateCreated LIMIT {0})",
|
|
||||||
startIndex.ToString(_usCulture)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (whereClauses.Count > 0)
|
|
||||||
{
|
|
||||||
commandText += " where " + string.Join(" AND ", whereClauses.ToArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
commandText += " ORDER BY JobItemIndex, DateCreated";
|
|
||||||
|
|
||||||
if (query.Limit.HasValue)
|
|
||||||
{
|
|
||||||
commandText += " LIMIT " + query.Limit.Value.ToString(_usCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
var list = new List<T>();
|
|
||||||
var count = connection.Query("select count (Id) from SyncJobItems" + whereTextWithoutPaging, paramList.ToArray())
|
|
||||||
.SelectScalarInt()
|
|
||||||
.First();
|
|
||||||
|
|
||||||
foreach (var row in connection.Query(commandText, paramList.ToArray()))
|
|
||||||
{
|
|
||||||
list.Add(itemFactory(row));
|
|
||||||
}
|
|
||||||
|
|
||||||
return new QueryResult<T>()
|
|
||||||
{
|
|
||||||
Items = list.ToArray(),
|
|
||||||
TotalRecordCount = count
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Dictionary<string, SyncedItemProgress> GetSyncedItemProgresses(SyncJobItemQuery query)
|
|
||||||
{
|
|
||||||
var result = new Dictionary<string, SyncedItemProgress>();
|
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
|
||||||
|
|
||||||
using (WriteLock.Read())
|
|
||||||
{
|
|
||||||
using (var connection = CreateConnection(true))
|
|
||||||
{
|
|
||||||
var commandText = "select ItemId,Status,Progress from SyncJobItems";
|
|
||||||
var whereClauses = new List<string>();
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(query.TargetId))
|
|
||||||
{
|
|
||||||
whereClauses.Add("TargetId=@TargetId");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.Statuses.Length > 0)
|
|
||||||
{
|
|
||||||
var statuses = string.Join(",", query.Statuses.Select(i => "'" + i.ToString() + "'").ToArray());
|
|
||||||
|
|
||||||
whereClauses.Add(string.Format("Status in ({0})", statuses));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (whereClauses.Count > 0)
|
|
||||||
{
|
|
||||||
commandText += " where " + string.Join(" AND ", whereClauses.ToArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
var statementTexts = new List<string>
|
|
||||||
{
|
|
||||||
commandText
|
|
||||||
};
|
|
||||||
|
|
||||||
commandText = commandText
|
|
||||||
.Replace("select ItemId,Status,Progress from SyncJobItems", "select ItemIds,Status,Progress from SyncJobs")
|
|
||||||
.Replace("'Synced'", "'Completed','CompletedWithError'");
|
|
||||||
|
|
||||||
statementTexts.Add(commandText);
|
|
||||||
|
|
||||||
var statements = connection.PrepareAll(string.Join(";", statementTexts.ToArray()))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
using (var statement = statements[0])
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(query.TargetId))
|
|
||||||
{
|
|
||||||
statement.TryBind("@TargetId", query.TargetId);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var row in statement.ExecuteQuery())
|
|
||||||
{
|
|
||||||
AddStatusResult(row, result, false);
|
|
||||||
}
|
|
||||||
LogQueryTime("GetSyncedItemProgresses", commandText, now);
|
|
||||||
}
|
|
||||||
|
|
||||||
now = DateTime.UtcNow;
|
|
||||||
|
|
||||||
using (var statement = statements[1])
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(query.TargetId))
|
|
||||||
{
|
|
||||||
statement.TryBind("@TargetId", query.TargetId);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var row in statement.ExecuteQuery())
|
|
||||||
{
|
|
||||||
AddStatusResult(row, result, true);
|
|
||||||
}
|
|
||||||
LogQueryTime("GetSyncedItemProgresses", commandText, now);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LogQueryTime(string methodName, string commandText, DateTime startDate)
|
|
||||||
{
|
|
||||||
var elapsed = (DateTime.UtcNow - startDate).TotalMilliseconds;
|
|
||||||
|
|
||||||
var slowThreshold = 1000;
|
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
slowThreshold = 50;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (elapsed >= slowThreshold)
|
|
||||||
{
|
|
||||||
Logger.Debug("{2} query time (slow): {0}ms. Query: {1}",
|
|
||||||
Convert.ToInt32(elapsed),
|
|
||||||
commandText,
|
|
||||||
methodName);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
//Logger.Debug("{2} query time: {0}ms. Query: {1}",
|
|
||||||
// Convert.ToInt32(elapsed),
|
|
||||||
// cmd.CommandText,
|
|
||||||
// methodName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddStatusResult(IReadOnlyList<IResultSetValue> reader, Dictionary<string, SyncedItemProgress> result, bool multipleIds)
|
|
||||||
{
|
|
||||||
if (reader[0].SQLiteType == SQLiteType.Null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var itemIds = new List<string>();
|
|
||||||
|
|
||||||
var ids = reader[0].ToString();
|
|
||||||
|
|
||||||
if (multipleIds)
|
|
||||||
{
|
|
||||||
itemIds = ids.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
itemIds.Add(ids);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reader[1].SQLiteType != SQLiteType.Null)
|
|
||||||
{
|
|
||||||
SyncJobItemStatus status;
|
|
||||||
var statusString = reader[1].ToString();
|
|
||||||
if (string.Equals(statusString, "Completed", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
string.Equals(statusString, "CompletedWithError", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
status = SyncJobItemStatus.Synced;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
status = (SyncJobItemStatus)Enum.Parse(typeof(SyncJobItemStatus), statusString, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status == SyncJobItemStatus.Synced)
|
|
||||||
{
|
|
||||||
foreach (var itemId in itemIds)
|
|
||||||
{
|
|
||||||
result[itemId] = new SyncedItemProgress
|
|
||||||
{
|
|
||||||
Status = SyncJobItemStatus.Synced
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
double progress = reader[2].SQLiteType == SQLiteType.Null ? 0.0 : reader[2].ToDouble();
|
|
||||||
|
|
||||||
foreach (var itemId in itemIds)
|
|
||||||
{
|
|
||||||
SyncedItemProgress currentStatus;
|
|
||||||
if (!result.TryGetValue(itemId, out currentStatus) || (currentStatus.Status != SyncJobItemStatus.Synced && progress >= currentStatus.Progress))
|
|
||||||
{
|
|
||||||
result[itemId] = new SyncedItemProgress
|
|
||||||
{
|
|
||||||
Status = status,
|
|
||||||
Progress = progress
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public QueryResult<SyncJobItem> GetJobItems(SyncJobItemQuery query)
|
|
||||||
{
|
|
||||||
return GetJobItemReader(query, BaseJobItemSelectText, GetJobItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task Create(SyncJobItem jobItem)
|
|
||||||
{
|
|
||||||
return InsertOrUpdate(jobItem, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task Update(SyncJobItem jobItem)
|
|
||||||
{
|
|
||||||
return InsertOrUpdate(jobItem, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task InsertOrUpdate(SyncJobItem jobItem, bool insert)
|
|
||||||
{
|
|
||||||
if (jobItem == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException("jobItem");
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckDisposed();
|
|
||||||
|
|
||||||
using (WriteLock.Write())
|
|
||||||
{
|
|
||||||
using (var connection = CreateConnection())
|
|
||||||
{
|
|
||||||
string commandText;
|
|
||||||
|
|
||||||
if (insert)
|
|
||||||
{
|
|
||||||
commandText = "insert into SyncJobItems (Id, ItemId, ItemName, MediaSourceId, JobId, TemporaryPath, OutputPath, Status, TargetId, DateCreated, Progress, AdditionalFiles, MediaSource, IsMarkedForRemoval, JobItemIndex, ItemDateModifiedTicks) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// cmd
|
|
||||||
commandText = "update SyncJobItems set ItemId=?,ItemName=?,MediaSourceId=?,JobId=?,TemporaryPath=?,OutputPath=?,Status=?,TargetId=?,DateCreated=?,Progress=?,AdditionalFiles=?,MediaSource=?,IsMarkedForRemoval=?,JobItemIndex=?,ItemDateModifiedTicks=? where Id=?";
|
|
||||||
}
|
|
||||||
|
|
||||||
var paramList = new List<object>();
|
|
||||||
paramList.Add(jobItem.ItemId);
|
|
||||||
paramList.Add(jobItem.ItemName);
|
|
||||||
paramList.Add(jobItem.MediaSourceId);
|
|
||||||
paramList.Add(jobItem.JobId);
|
|
||||||
paramList.Add(jobItem.TemporaryPath);
|
|
||||||
paramList.Add(jobItem.OutputPath);
|
|
||||||
paramList.Add(jobItem.Status.ToString());
|
|
||||||
|
|
||||||
paramList.Add(jobItem.TargetId);
|
|
||||||
paramList.Add(jobItem.DateCreated.ToDateTimeParamValue());
|
|
||||||
paramList.Add(jobItem.Progress);
|
|
||||||
paramList.Add(_json.SerializeToString(jobItem.AdditionalFiles));
|
|
||||||
paramList.Add(jobItem.MediaSource == null ? null : _json.SerializeToString(jobItem.MediaSource));
|
|
||||||
paramList.Add(jobItem.IsMarkedForRemoval);
|
|
||||||
paramList.Add(jobItem.JobItemIndex);
|
|
||||||
paramList.Add(jobItem.ItemDateModifiedTicks);
|
|
||||||
|
|
||||||
if (insert)
|
|
||||||
{
|
|
||||||
paramList.Insert(0, jobItem.Id.ToGuidParamValue());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
paramList.Add(jobItem.Id.ToGuidParamValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
connection.RunInTransaction(conn =>
|
|
||||||
{
|
|
||||||
conn.Execute(commandText, paramList.ToArray());
|
|
||||||
}, TransactionMode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private SyncJobItem GetJobItem(IReadOnlyList<IResultSetValue> reader)
|
|
||||||
{
|
|
||||||
var info = new SyncJobItem
|
|
||||||
{
|
|
||||||
Id = reader[0].ReadGuid().ToString("N"),
|
|
||||||
ItemId = reader[1].ToString()
|
|
||||||
};
|
|
||||||
|
|
||||||
if (reader[2].SQLiteType != SQLiteType.Null)
|
|
||||||
{
|
|
||||||
info.ItemName = reader[2].ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reader[3].SQLiteType != SQLiteType.Null)
|
|
||||||
{
|
|
||||||
info.MediaSourceId = reader[3].ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
info.JobId = reader[4].ToString();
|
|
||||||
|
|
||||||
if (reader[5].SQLiteType != SQLiteType.Null)
|
|
||||||
{
|
|
||||||
info.TemporaryPath = reader[5].ToString();
|
|
||||||
}
|
|
||||||
if (reader[6].SQLiteType != SQLiteType.Null)
|
|
||||||
{
|
|
||||||
info.OutputPath = reader[6].ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reader[7].SQLiteType != SQLiteType.Null)
|
|
||||||
{
|
|
||||||
info.Status = (SyncJobItemStatus)Enum.Parse(typeof(SyncJobItemStatus), reader[7].ToString(), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
info.TargetId = reader[8].ToString();
|
|
||||||
|
|
||||||
info.DateCreated = reader[9].ReadDateTime();
|
|
||||||
|
|
||||||
if (reader[10].SQLiteType != SQLiteType.Null)
|
|
||||||
{
|
|
||||||
info.Progress = reader[10].ToDouble();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reader[11].SQLiteType != SQLiteType.Null)
|
|
||||||
{
|
|
||||||
var json = reader[11].ToString();
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(json))
|
|
||||||
{
|
|
||||||
info.AdditionalFiles = _json.DeserializeFromString<List<ItemFileInfo>>(json);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reader[12].SQLiteType != SQLiteType.Null)
|
|
||||||
{
|
|
||||||
var json = reader[12].ToString();
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(json))
|
|
||||||
{
|
|
||||||
info.MediaSource = _json.DeserializeFromString<MediaSourceInfo>(json);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
info.IsMarkedForRemoval = reader[13].ToBool();
|
|
||||||
info.JobItemIndex = reader[14].ToInt();
|
|
||||||
|
|
||||||
if (reader[15].SQLiteType != SQLiteType.Null)
|
|
||||||
{
|
|
||||||
info.ItemDateModifiedTicks = reader[15].ToInt64();
|
|
||||||
}
|
|
||||||
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,158 +0,0 @@
|
||||||
using MediaBrowser.Common.Extensions;
|
|
||||||
using MediaBrowser.Controller;
|
|
||||||
using MediaBrowser.Controller.Entities;
|
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using MediaBrowser.Controller.Sync;
|
|
||||||
using MediaBrowser.Model.Dto;
|
|
||||||
using MediaBrowser.Model.Logging;
|
|
||||||
using MediaBrowser.Model.Sync;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Sync
|
|
||||||
{
|
|
||||||
public class SyncedMediaSourceProvider : IMediaSourceProvider
|
|
||||||
{
|
|
||||||
private readonly SyncManager _syncManager;
|
|
||||||
private readonly IServerApplicationHost _appHost;
|
|
||||||
private readonly ILogger _logger;
|
|
||||||
|
|
||||||
public SyncedMediaSourceProvider(ISyncManager syncManager, IServerApplicationHost appHost, ILogger logger)
|
|
||||||
{
|
|
||||||
_appHost = appHost;
|
|
||||||
_logger = logger;
|
|
||||||
_syncManager = (SyncManager)syncManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<MediaSourceInfo>> GetMediaSources(IHasMediaSources item, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var jobItemResult = _syncManager.GetJobItems(new SyncJobItemQuery
|
|
||||||
{
|
|
||||||
AddMetadata = false,
|
|
||||||
Statuses = new[] { SyncJobItemStatus.Synced },
|
|
||||||
ItemId = item.Id.ToString("N")
|
|
||||||
});
|
|
||||||
|
|
||||||
var list = new List<MediaSourceInfo>();
|
|
||||||
|
|
||||||
if (jobItemResult.Items.Length > 0)
|
|
||||||
{
|
|
||||||
var targets = _syncManager.ServerSyncProviders
|
|
||||||
.SelectMany(i => i.GetAllSyncTargets().Select(t => new Tuple<IServerSyncProvider, SyncTarget>(i, t)))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var serverId = _appHost.SystemId;
|
|
||||||
|
|
||||||
foreach (var jobItem in jobItemResult.Items)
|
|
||||||
{
|
|
||||||
var targetTuple = targets.FirstOrDefault(i => string.Equals(i.Item2.Id, jobItem.TargetId, StringComparison.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
if (targetTuple != null)
|
|
||||||
{
|
|
||||||
var syncTarget = targetTuple.Item2;
|
|
||||||
var syncProvider = targetTuple.Item1;
|
|
||||||
var dataProvider = _syncManager.GetDataProvider(targetTuple.Item1, syncTarget);
|
|
||||||
|
|
||||||
var localItems = await dataProvider.GetItems(syncTarget, serverId, item.Id.ToString("N")).ConfigureAwait(false);
|
|
||||||
|
|
||||||
foreach (var localItem in localItems)
|
|
||||||
{
|
|
||||||
foreach (var mediaSource in localItem.Item.MediaSources)
|
|
||||||
{
|
|
||||||
AddMediaSource(list, localItem, mediaSource, syncProvider, syncTarget);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddMediaSource(List<MediaSourceInfo> list,
|
|
||||||
LocalItem item,
|
|
||||||
MediaSourceInfo mediaSource,
|
|
||||||
IServerSyncProvider provider,
|
|
||||||
SyncTarget target)
|
|
||||||
{
|
|
||||||
SetStaticMediaSourceInfo(item, mediaSource);
|
|
||||||
|
|
||||||
var requiresDynamicAccess = provider as IHasDynamicAccess;
|
|
||||||
|
|
||||||
if (requiresDynamicAccess != null)
|
|
||||||
{
|
|
||||||
mediaSource.RequiresOpening = true;
|
|
||||||
|
|
||||||
var keyList = new List<string>();
|
|
||||||
keyList.Add(provider.GetType().FullName.GetMD5().ToString("N"));
|
|
||||||
keyList.Add(target.Id.GetMD5().ToString("N"));
|
|
||||||
keyList.Add(item.Id);
|
|
||||||
mediaSource.OpenToken = string.Join(StreamIdDelimeterString, keyList.ToArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
list.Add(mediaSource);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
|
|
||||||
private const string StreamIdDelimeterString = "_";
|
|
||||||
|
|
||||||
public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> OpenMediaSource(string openToken, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var openKeys = openToken.Split(new[] { StreamIdDelimeterString[0] }, 3);
|
|
||||||
|
|
||||||
var provider = _syncManager.ServerSyncProviders
|
|
||||||
.FirstOrDefault(i => string.Equals(openKeys[0], i.GetType().FullName.GetMD5().ToString("N"), StringComparison.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
var target = provider.GetAllSyncTargets()
|
|
||||||
.FirstOrDefault(i => string.Equals(openKeys[1], i.Id.GetMD5().ToString("N"), StringComparison.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
var dataProvider = _syncManager.GetDataProvider(provider, target);
|
|
||||||
var localItem = await dataProvider.Get(target, openKeys[2]).ConfigureAwait(false);
|
|
||||||
|
|
||||||
var fileId = localItem.FileId;
|
|
||||||
if (string.IsNullOrWhiteSpace(fileId))
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
var requiresDynamicAccess = (IHasDynamicAccess)provider;
|
|
||||||
var dynamicInfo = await requiresDynamicAccess.GetSyncedFileInfo(fileId, target, cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
var mediaSource = localItem.Item.MediaSources.First();
|
|
||||||
mediaSource.LiveStreamId = Guid.NewGuid().ToString();
|
|
||||||
SetStaticMediaSourceInfo(localItem, mediaSource);
|
|
||||||
|
|
||||||
foreach (var stream in mediaSource.MediaStreams)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(stream.ExternalId))
|
|
||||||
{
|
|
||||||
var dynamicStreamInfo = await requiresDynamicAccess.GetSyncedFileInfo(stream.ExternalId, target, cancellationToken).ConfigureAwait(false);
|
|
||||||
stream.Path = dynamicStreamInfo.Path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mediaSource.Path = dynamicInfo.Path;
|
|
||||||
mediaSource.Protocol = dynamicInfo.Protocol;
|
|
||||||
mediaSource.RequiredHttpHeaders = dynamicInfo.RequiredHttpHeaders;
|
|
||||||
|
|
||||||
return new Tuple<MediaSourceInfo, IDirectStreamProvider>(mediaSource, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetStaticMediaSourceInfo(LocalItem item, MediaSourceInfo mediaSource)
|
|
||||||
{
|
|
||||||
mediaSource.Id = item.Id;
|
|
||||||
mediaSource.SupportsTranscoding = false;
|
|
||||||
if (mediaSource.Protocol == MediaBrowser.Model.MediaInfo.MediaProtocol.File)
|
|
||||||
{
|
|
||||||
mediaSource.ETag = item.Id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task CloseMediaSource(string liveStreamId)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,208 +0,0 @@
|
||||||
using MediaBrowser.Common.Configuration;
|
|
||||||
using MediaBrowser.Controller;
|
|
||||||
using MediaBrowser.Controller.Sync;
|
|
||||||
using MediaBrowser.Model.Logging;
|
|
||||||
using MediaBrowser.Model.Serialization;
|
|
||||||
using MediaBrowser.Model.Sync;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using MediaBrowser.Model.IO;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Sync
|
|
||||||
{
|
|
||||||
public class TargetDataProvider : ISyncDataProvider
|
|
||||||
{
|
|
||||||
private readonly SyncTarget _target;
|
|
||||||
private readonly IServerSyncProvider _provider;
|
|
||||||
|
|
||||||
private readonly SemaphoreSlim _dataLock = new SemaphoreSlim(1, 1);
|
|
||||||
private readonly SemaphoreSlim _remoteDataLock = new SemaphoreSlim(1, 1);
|
|
||||||
private List<LocalItem> _items;
|
|
||||||
|
|
||||||
private readonly ILogger _logger;
|
|
||||||
private readonly IJsonSerializer _json;
|
|
||||||
private readonly IFileSystem _fileSystem;
|
|
||||||
private readonly IApplicationPaths _appPaths;
|
|
||||||
private readonly IServerApplicationHost _appHost;
|
|
||||||
private readonly IMemoryStreamFactory _memoryStreamProvider;
|
|
||||||
|
|
||||||
public TargetDataProvider(IServerSyncProvider provider, SyncTarget target, IServerApplicationHost appHost, ILogger logger, IJsonSerializer json, IFileSystem fileSystem, IApplicationPaths appPaths, IMemoryStreamFactory memoryStreamProvider)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_json = json;
|
|
||||||
_provider = provider;
|
|
||||||
_target = target;
|
|
||||||
_fileSystem = fileSystem;
|
|
||||||
_appPaths = appPaths;
|
|
||||||
_memoryStreamProvider = memoryStreamProvider;
|
|
||||||
_appHost = appHost;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string[] GetRemotePath()
|
|
||||||
{
|
|
||||||
var parts = new List<string>
|
|
||||||
{
|
|
||||||
_appHost.FriendlyName,
|
|
||||||
"data.json"
|
|
||||||
};
|
|
||||||
|
|
||||||
parts = parts.Select(i => GetValidFilename(_provider, i)).ToList();
|
|
||||||
|
|
||||||
return parts.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetValidFilename(IServerSyncProvider provider, string filename)
|
|
||||||
{
|
|
||||||
// We can always add this method to the sync provider if it's really needed
|
|
||||||
return _fileSystem.GetValidFilename(filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<List<LocalItem>> RetrieveItems(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
_logger.Debug("Getting {0} from {1}", string.Join(MediaSync.PathSeparatorString, GetRemotePath().ToArray()), _provider.Name);
|
|
||||||
|
|
||||||
await _remoteDataLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var fileResult = await _provider.GetFiles(GetRemotePath().ToArray(), _target, cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (fileResult.Items.Length > 0)
|
|
||||||
{
|
|
||||||
using (var stream = await _provider.GetFile(fileResult.Items[0].FullName, _target, new Progress<double>(), cancellationToken))
|
|
||||||
{
|
|
||||||
return _json.DeserializeFromStream<List<LocalItem>>(stream);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_remoteDataLock.Release();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new List<LocalItem>();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task EnsureData(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (_items == null)
|
|
||||||
{
|
|
||||||
_items = await RetrieveItems(cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SaveData(List<LocalItem> items, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
using (var stream = _memoryStreamProvider.CreateNew())
|
|
||||||
{
|
|
||||||
_json.SerializeToStream(items, stream);
|
|
||||||
|
|
||||||
// Save to sync provider
|
|
||||||
stream.Position = 0;
|
|
||||||
var remotePath = GetRemotePath();
|
|
||||||
|
|
||||||
await _remoteDataLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_logger.Debug("Saving data.json to {0}. Remote path: {1}", _provider.Name, string.Join("/", remotePath));
|
|
||||||
|
|
||||||
await _provider.SendFile(stream, remotePath, _target, new Progress<double>(), cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_remoteDataLock.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<T> GetData<T>(bool enableCache, Func<List<LocalItem>, T> dataFactory)
|
|
||||||
{
|
|
||||||
if (!enableCache)
|
|
||||||
{
|
|
||||||
var items = await RetrieveItems(CancellationToken.None).ConfigureAwait(false);
|
|
||||||
var newCache = items.ToList();
|
|
||||||
var result = dataFactory(items);
|
|
||||||
await UpdateCache(newCache).ConfigureAwait(false);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _dataLock.WaitAsync().ConfigureAwait(false);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await EnsureData(CancellationToken.None).ConfigureAwait(false);
|
|
||||||
|
|
||||||
return dataFactory(_items);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_dataLock.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task UpdateData(Func<List<LocalItem>, List<LocalItem>> action)
|
|
||||||
{
|
|
||||||
var items = await RetrieveItems(CancellationToken.None).ConfigureAwait(false);
|
|
||||||
items = action(items);
|
|
||||||
await SaveData(items.ToList(), CancellationToken.None).ConfigureAwait(false);
|
|
||||||
|
|
||||||
await UpdateCache(null).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task UpdateCache(List<LocalItem> list)
|
|
||||||
{
|
|
||||||
await _dataLock.WaitAsync().ConfigureAwait(false);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_items = list;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_dataLock.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<List<LocalItem>> GetLocalItems(SyncTarget target, string serverId)
|
|
||||||
{
|
|
||||||
return GetData(false, items => items.Where(i => string.Equals(i.ServerId, serverId, StringComparison.OrdinalIgnoreCase)).ToList());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task AddOrUpdate(SyncTarget target, LocalItem item)
|
|
||||||
{
|
|
||||||
return UpdateData(items =>
|
|
||||||
{
|
|
||||||
var list = items.Where(i => !string.Equals(i.Id, item.Id, StringComparison.OrdinalIgnoreCase))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
list.Add(item);
|
|
||||||
|
|
||||||
return list;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task Delete(SyncTarget target, string id)
|
|
||||||
{
|
|
||||||
return UpdateData(items => items.Where(i => !string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase)).ToList());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<LocalItem> Get(SyncTarget target, string id)
|
|
||||||
{
|
|
||||||
return GetData(true, items => items.FirstOrDefault(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<List<LocalItem>> GetItems(SyncTarget target, string serverId, string itemId)
|
|
||||||
{
|
|
||||||
return GetData(true, items => items.Where(i => string.Equals(i.ServerId, serverId, StringComparison.OrdinalIgnoreCase) && string.Equals(i.ItemId, itemId, StringComparison.OrdinalIgnoreCase)).ToList());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<List<LocalItem>> GetItemsBySyncJobItemId(SyncTarget target, string serverId, string syncJobItemId)
|
|
||||||
{
|
|
||||||
return GetData(false, items => items.Where(i => string.Equals(i.ServerId, serverId, StringComparison.OrdinalIgnoreCase) && string.Equals(i.SyncJobItemId, syncJobItemId, StringComparison.OrdinalIgnoreCase)).ToList());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +1,8 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<packages>
|
<packages>
|
||||||
<package id="Emby.XmlTv" version="1.0.6" targetFramework="portable45-net45+win8" />
|
<package id="Emby.XmlTv" version="1.0.7" targetFramework="portable45-net45+win8" />
|
||||||
<package id="MediaBrowser.Naming" version="1.0.4" targetFramework="portable45-net45+win8" />
|
<package id="MediaBrowser.Naming" version="1.0.4" targetFramework="portable45-net45+win8" />
|
||||||
<package id="SQLitePCL.pretty" version="1.1.0" targetFramework="portable45-net45+win8" />
|
<package id="SQLitePCL.pretty" version="1.1.0" targetFramework="portable45-net45+win8" />
|
||||||
<package id="SQLitePCLRaw.core" version="1.1.1" targetFramework="portable45-net45+win8" />
|
<package id="SQLitePCLRaw.core" version="1.1.2" targetFramework="portable45-net45+win8" />
|
||||||
<package id="UniversalDetector" version="1.0.1" targetFramework="portable45-net45+win8" />
|
<package id="UniversalDetector" version="1.0.1" targetFramework="portable45-net45+win8" />
|
||||||
</packages>
|
</packages>
|
|
@ -1,177 +0,0 @@
|
||||||
using MediaBrowser.Common.Extensions;
|
|
||||||
using MediaBrowser.Controller.Connect;
|
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using MediaBrowser.Controller.Net;
|
|
||||||
using MediaBrowser.Model.Connect;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using MediaBrowser.Controller.Session;
|
|
||||||
using MediaBrowser.Model.Services;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Api
|
|
||||||
{
|
|
||||||
[Route("/Users/{Id}/Connect/Link", "POST", Summary = "Creates a Connect link for a user")]
|
|
||||||
[Authenticated(Roles = "Admin")]
|
|
||||||
public class CreateConnectLink : IReturn<UserLinkResult>
|
|
||||||
{
|
|
||||||
[ApiMember(Name = "Id", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
|
|
||||||
public string Id { get; set; }
|
|
||||||
|
|
||||||
[ApiMember(Name = "ConnectUsername", Description = "Connect username", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
|
|
||||||
public string ConnectUsername { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[Route("/Users/{Id}/Connect/Link", "DELETE", Summary = "Removes a Connect link for a user")]
|
|
||||||
[Authenticated(Roles = "Admin")]
|
|
||||||
public class DeleteConnectLink : IReturnVoid
|
|
||||||
{
|
|
||||||
[ApiMember(Name = "Id", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
|
|
||||||
public string Id { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[Route("/Connect/Invite", "POST", Summary = "Creates a Connect link for a user")]
|
|
||||||
[Authenticated(Roles = "Admin")]
|
|
||||||
public class CreateConnectInvite : IReturn<UserLinkResult>
|
|
||||||
{
|
|
||||||
[ApiMember(Name = "ConnectUsername", Description = "Connect username", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")]
|
|
||||||
public string ConnectUsername { get; set; }
|
|
||||||
|
|
||||||
[ApiMember(Name = "SendingUserId", Description = "Sending User Id", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")]
|
|
||||||
public string SendingUserId { get; set; }
|
|
||||||
|
|
||||||
[ApiMember(Name = "EnabledLibraries", Description = "EnabledLibraries", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")]
|
|
||||||
public string EnabledLibraries { get; set; }
|
|
||||||
|
|
||||||
[ApiMember(Name = "EnabledChannels", Description = "EnabledChannels", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")]
|
|
||||||
public string EnabledChannels { get; set; }
|
|
||||||
|
|
||||||
[ApiMember(Name = "EnableLiveTv", Description = "EnableLiveTv", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")]
|
|
||||||
public bool EnableLiveTv { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
[Route("/Connect/Pending", "GET", Summary = "Creates a Connect link for a user")]
|
|
||||||
[Authenticated(Roles = "Admin")]
|
|
||||||
public class GetPendingGuests : IReturn<List<ConnectAuthorization>>
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
[Route("/Connect/Pending", "DELETE", Summary = "Deletes a Connect link for a user")]
|
|
||||||
[Authenticated(Roles = "Admin")]
|
|
||||||
public class DeleteAuthorization : IReturnVoid
|
|
||||||
{
|
|
||||||
[ApiMember(Name = "Id", Description = "Authorization Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
|
|
||||||
public string Id { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[Route("/Connect/Exchange", "GET", Summary = "Gets the corresponding local user from a connect user id")]
|
|
||||||
[Authenticated]
|
|
||||||
public class GetLocalUser : IReturn<ConnectAuthenticationExchangeResult>
|
|
||||||
{
|
|
||||||
[ApiMember(Name = "ConnectUserId", Description = "ConnectUserId", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
|
|
||||||
public string ConnectUserId { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ConnectService : BaseApiService
|
|
||||||
{
|
|
||||||
private readonly IConnectManager _connectManager;
|
|
||||||
private readonly ISessionManager _sessionManager;
|
|
||||||
private readonly IAuthorizationContext _authContext;
|
|
||||||
|
|
||||||
public ConnectService(IConnectManager connectManager, ISessionManager sessionManager, IAuthorizationContext authContext)
|
|
||||||
{
|
|
||||||
_connectManager = connectManager;
|
|
||||||
_sessionManager = sessionManager;
|
|
||||||
_authContext = authContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
public object Post(CreateConnectLink request)
|
|
||||||
{
|
|
||||||
return _connectManager.LinkUser(request.Id, request.ConnectUsername);
|
|
||||||
}
|
|
||||||
|
|
||||||
public object Post(CreateConnectInvite request)
|
|
||||||
{
|
|
||||||
var enabledLibraries = (request.EnabledLibraries ?? string.Empty)
|
|
||||||
.Split(',')
|
|
||||||
.Where(i => !string.IsNullOrWhiteSpace(i))
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
var enabledChannels = (request.EnabledChannels ?? string.Empty)
|
|
||||||
.Split(',')
|
|
||||||
.Where(i => !string.IsNullOrWhiteSpace(i))
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
return _connectManager.InviteUser(new ConnectAuthorizationRequest
|
|
||||||
{
|
|
||||||
ConnectUserName = request.ConnectUsername,
|
|
||||||
SendingUserId = request.SendingUserId,
|
|
||||||
EnabledLibraries = enabledLibraries,
|
|
||||||
EnabledChannels = enabledChannels,
|
|
||||||
EnableLiveTv = request.EnableLiveTv
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Delete(DeleteConnectLink request)
|
|
||||||
{
|
|
||||||
var task = _connectManager.RemoveConnect(request.Id);
|
|
||||||
|
|
||||||
Task.WaitAll(task);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<object> Get(GetPendingGuests request)
|
|
||||||
{
|
|
||||||
var result = await _connectManager.GetPendingGuests().ConfigureAwait(false);
|
|
||||||
|
|
||||||
return ToOptimizedResult(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Delete(DeleteAuthorization request)
|
|
||||||
{
|
|
||||||
var task = _connectManager.CancelAuthorization(request.Id);
|
|
||||||
|
|
||||||
Task.WaitAll(task);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<object> Get(GetLocalUser request)
|
|
||||||
{
|
|
||||||
var user = await _connectManager.GetLocalUser(request.ConnectUserId).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
throw new ResourceNotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var auth = _authContext.GetAuthorizationInfo(Request);
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(auth.Client))
|
|
||||||
{
|
|
||||||
return ToOptimizedResult(new ConnectAuthenticationExchangeResult
|
|
||||||
{
|
|
||||||
AccessToken = user.ConnectAccessKey,
|
|
||||||
LocalUserId = user.Id.ToString("N")
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var session = await _sessionManager.CreateNewSession(new AuthenticationRequest
|
|
||||||
{
|
|
||||||
App = auth.Client,
|
|
||||||
AppVersion = auth.Version,
|
|
||||||
DeviceId = auth.DeviceId,
|
|
||||||
DeviceName = auth.Device,
|
|
||||||
RemoteEndPoint = Request.RemoteIp,
|
|
||||||
Username = user.Name,
|
|
||||||
UserId = user.Id.ToString("N")
|
|
||||||
|
|
||||||
}).ConfigureAwait(false);
|
|
||||||
|
|
||||||
return ToOptimizedResult(new ConnectAuthenticationExchangeResult
|
|
||||||
{
|
|
||||||
AccessToken = session.AccessToken,
|
|
||||||
LocalUserId = session.User.Id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -237,13 +237,6 @@ namespace MediaBrowser.Api
|
||||||
item.Name = request.Name;
|
item.Name = request.Name;
|
||||||
item.ForcedSortName = request.ForcedSortName;
|
item.ForcedSortName = request.ForcedSortName;
|
||||||
|
|
||||||
var hasBudget = item as IHasBudget;
|
|
||||||
if (hasBudget != null)
|
|
||||||
{
|
|
||||||
hasBudget.Budget = request.Budget;
|
|
||||||
hasBudget.Revenue = request.Revenue;
|
|
||||||
}
|
|
||||||
|
|
||||||
item.OriginalTitle = string.IsNullOrWhiteSpace(request.OriginalTitle) ? null : request.OriginalTitle;
|
item.OriginalTitle = string.IsNullOrWhiteSpace(request.OriginalTitle) ? null : request.OriginalTitle;
|
||||||
|
|
||||||
item.CriticRating = request.CriticRating;
|
item.CriticRating = request.CriticRating;
|
||||||
|
@ -401,10 +394,21 @@ namespace MediaBrowser.Api
|
||||||
var series = item as Series;
|
var series = item as Series;
|
||||||
if (series != null)
|
if (series != null)
|
||||||
{
|
{
|
||||||
series.Status = request.SeriesStatus;
|
series.Status = GetSeriesStatus(request);
|
||||||
series.AirDays = request.AirDays;
|
series.AirDays = request.AirDays;
|
||||||
series.AirTime = request.AirTime;
|
series.AirTime = request.AirTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private SeriesStatus? GetSeriesStatus(BaseItemDto item)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(item.Status))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (SeriesStatus)Enum.Parse(typeof(SeriesStatus), item.Status, true);
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -791,7 +791,7 @@ namespace MediaBrowser.Api.LiveTv
|
||||||
ProviderChannels = providerChannels.Select(i => new NameIdPair
|
ProviderChannels = providerChannels.Select(i => new NameIdPair
|
||||||
{
|
{
|
||||||
Name = i.Name,
|
Name = i.Name,
|
||||||
Id = string.IsNullOrWhiteSpace(i.TunerChannelId) ? i.Id : i.TunerChannelId
|
Id = i.Id
|
||||||
|
|
||||||
}).ToList(),
|
}).ToList(),
|
||||||
|
|
||||||
|
|
|
@ -51,7 +51,6 @@
|
||||||
<Compile Include="BasePeriodicWebSocketListener.cs" />
|
<Compile Include="BasePeriodicWebSocketListener.cs" />
|
||||||
<Compile Include="BrandingService.cs" />
|
<Compile Include="BrandingService.cs" />
|
||||||
<Compile Include="ChannelService.cs" />
|
<Compile Include="ChannelService.cs" />
|
||||||
<Compile Include="ConnectService.cs" />
|
|
||||||
<Compile Include="Devices\DeviceService.cs" />
|
<Compile Include="Devices\DeviceService.cs" />
|
||||||
<Compile Include="Dlna\DlnaServerService.cs" />
|
<Compile Include="Dlna\DlnaServerService.cs" />
|
||||||
<Compile Include="Dlna\DlnaService.cs" />
|
<Compile Include="Dlna\DlnaService.cs" />
|
||||||
|
@ -59,6 +58,7 @@
|
||||||
<Compile Include="IHasDtoOptions.cs" />
|
<Compile Include="IHasDtoOptions.cs" />
|
||||||
<Compile Include="Playback\MediaInfoService.cs" />
|
<Compile Include="Playback\MediaInfoService.cs" />
|
||||||
<Compile Include="Playback\TranscodingThrottler.cs" />
|
<Compile Include="Playback\TranscodingThrottler.cs" />
|
||||||
|
<Compile Include="Playback\UniversalAudioService.cs" />
|
||||||
<Compile Include="PlaylistService.cs" />
|
<Compile Include="PlaylistService.cs" />
|
||||||
<Compile Include="Reports\Activities\ReportActivitiesBuilder.cs" />
|
<Compile Include="Reports\Activities\ReportActivitiesBuilder.cs" />
|
||||||
<Compile Include="Reports\Common\HeaderActivitiesMetadata.cs" />
|
<Compile Include="Reports\Common\HeaderActivitiesMetadata.cs" />
|
||||||
|
@ -172,10 +172,6 @@
|
||||||
<Project>{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}</Project>
|
<Project>{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}</Project>
|
||||||
<Name>MediaBrowser.Controller</Name>
|
<Name>MediaBrowser.Controller</Name>
|
||||||
</ProjectReference>
|
</ProjectReference>
|
||||||
<ProjectReference Include="..\MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj">
|
|
||||||
<Project>{0bd82fa6-eb8a-4452-8af5-74f9c3849451}</Project>
|
|
||||||
<Name>MediaBrowser.MediaEncoding</Name>
|
|
||||||
</ProjectReference>
|
|
||||||
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
|
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
|
||||||
<Project>{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}</Project>
|
<Project>{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}</Project>
|
||||||
<Name>MediaBrowser.Model</Name>
|
<Name>MediaBrowser.Model</Name>
|
||||||
|
|
|
@ -22,7 +22,6 @@ using System.Threading.Tasks;
|
||||||
using MediaBrowser.Common.Net;
|
using MediaBrowser.Common.Net;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using MediaBrowser.MediaEncoding.Encoder;
|
|
||||||
using MediaBrowser.Model.Diagnostics;
|
using MediaBrowser.Model.Diagnostics;
|
||||||
|
|
||||||
namespace MediaBrowser.Api.Playback
|
namespace MediaBrowser.Api.Playback
|
||||||
|
@ -126,14 +125,10 @@ namespace MediaBrowser.Api.Playback
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the output file path.
|
/// Gets the output file path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="state">The state.</param>
|
private string GetOutputFilePath(StreamState state, string outputFileExtension)
|
||||||
/// <returns>System.String.</returns>
|
|
||||||
private string GetOutputFilePath(StreamState state)
|
|
||||||
{
|
{
|
||||||
var folder = ServerConfigurationManager.ApplicationPaths.TranscodingTempPath;
|
var folder = ServerConfigurationManager.ApplicationPaths.TranscodingTempPath;
|
||||||
|
|
||||||
var outputFileExtension = GetOutputFileExtension(state);
|
|
||||||
|
|
||||||
var data = GetCommandLineArguments("dummy\\dummy", state, false);
|
var data = GetCommandLineArguments("dummy\\dummy", state, false);
|
||||||
|
|
||||||
data += "-" + (state.Request.DeviceId ?? string.Empty);
|
data += "-" + (state.Request.DeviceId ?? string.Empty);
|
||||||
|
@ -876,11 +871,16 @@ namespace MediaBrowser.Api.Playback
|
||||||
|
|
||||||
var container = Path.GetExtension(state.RequestedUrl);
|
var container = Path.GetExtension(state.RequestedUrl);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(container))
|
||||||
|
{
|
||||||
|
container = request.Container;
|
||||||
|
}
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(container))
|
if (string.IsNullOrEmpty(container))
|
||||||
{
|
{
|
||||||
container = request.Static ?
|
container = request.Static ?
|
||||||
state.InputContainer :
|
state.InputContainer :
|
||||||
(Path.GetExtension(GetOutputFilePath(state)) ?? string.Empty).TrimStart('.');
|
GetOutputFileExtension(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
state.OutputContainer = (container ?? string.Empty).TrimStart('.');
|
state.OutputContainer = (container ?? string.Empty).TrimStart('.');
|
||||||
|
@ -923,7 +923,10 @@ namespace MediaBrowser.Api.Playback
|
||||||
ApplyDeviceProfileSettings(state);
|
ApplyDeviceProfileSettings(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
state.OutputFilePath = GetOutputFilePath(state);
|
var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
|
||||||
|
? GetOutputFileExtension(state)
|
||||||
|
: ("." + state.OutputContainer);
|
||||||
|
state.OutputFilePath = GetOutputFilePath(state, ext);
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,9 +18,6 @@ using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Common.IO;
|
|
||||||
using MediaBrowser.Controller.IO;
|
|
||||||
using MediaBrowser.Model.IO;
|
|
||||||
using MediaBrowser.Model.Services;
|
using MediaBrowser.Model.Services;
|
||||||
using MimeTypes = MediaBrowser.Model.Net.MimeTypes;
|
using MimeTypes = MediaBrowser.Model.Net.MimeTypes;
|
||||||
|
|
||||||
|
|
|
@ -127,7 +127,7 @@ namespace MediaBrowser.Api.Playback
|
||||||
|
|
||||||
SetDeviceSpecificData(item, result.MediaSource, profile, authInfo, request.MaxStreamingBitrate,
|
SetDeviceSpecificData(item, result.MediaSource, profile, authInfo, request.MaxStreamingBitrate,
|
||||||
request.StartTimeTicks ?? 0, result.MediaSource.Id, request.AudioStreamIndex,
|
request.StartTimeTicks ?? 0, result.MediaSource.Id, request.AudioStreamIndex,
|
||||||
request.SubtitleStreamIndex, request.MaxAudioChannels, request.PlaySessionId, request.UserId);
|
request.SubtitleStreamIndex, request.MaxAudioChannels, request.PlaySessionId, request.UserId, true, true, true);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -146,7 +146,7 @@ namespace MediaBrowser.Api.Playback
|
||||||
Task.WaitAll(task);
|
Task.WaitAll(task);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<object> Post(GetPostedPlaybackInfo request)
|
public async Task<PlaybackInfoResponse> GetPlaybackInfo(GetPostedPlaybackInfo request)
|
||||||
{
|
{
|
||||||
var authInfo = _authContext.GetAuthorizationInfo(Request);
|
var authInfo = _authContext.GetAuthorizationInfo(Request);
|
||||||
|
|
||||||
|
@ -169,10 +169,17 @@ namespace MediaBrowser.Api.Playback
|
||||||
{
|
{
|
||||||
var mediaSourceId = request.MediaSourceId;
|
var mediaSourceId = request.MediaSourceId;
|
||||||
|
|
||||||
SetDeviceSpecificData(request.Id, info, profile, authInfo, request.MaxStreamingBitrate ?? profile.MaxStreamingBitrate, request.StartTimeTicks ?? 0, mediaSourceId, request.AudioStreamIndex, request.SubtitleStreamIndex, request.MaxAudioChannels, request.UserId);
|
SetDeviceSpecificData(request.Id, info, profile, authInfo, request.MaxStreamingBitrate ?? profile.MaxStreamingBitrate, request.StartTimeTicks ?? 0, mediaSourceId, request.AudioStreamIndex, request.SubtitleStreamIndex, request.MaxAudioChannels, request.UserId, request.EnableDirectPlay, request.EnableDirectStream, request.EnableTranscoding);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ToOptimizedResult(info);
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<object> Post(GetPostedPlaybackInfo request)
|
||||||
|
{
|
||||||
|
var result = await GetPlaybackInfo(request).ConfigureAwait(false);
|
||||||
|
|
||||||
|
return ToOptimizedResult(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
private T Clone<T>(T obj)
|
private T Clone<T>(T obj)
|
||||||
|
@ -244,13 +251,16 @@ namespace MediaBrowser.Api.Playback
|
||||||
int? audioStreamIndex,
|
int? audioStreamIndex,
|
||||||
int? subtitleStreamIndex,
|
int? subtitleStreamIndex,
|
||||||
int? maxAudioChannels,
|
int? maxAudioChannels,
|
||||||
string userId)
|
string userId,
|
||||||
|
bool enableDirectPlay,
|
||||||
|
bool enableDirectStream,
|
||||||
|
bool enableTranscoding)
|
||||||
{
|
{
|
||||||
var item = _libraryManager.GetItemById(itemId);
|
var item = _libraryManager.GetItemById(itemId);
|
||||||
|
|
||||||
foreach (var mediaSource in result.MediaSources)
|
foreach (var mediaSource in result.MediaSources)
|
||||||
{
|
{
|
||||||
SetDeviceSpecificData(item, mediaSource, profile, auth, maxBitrate, startTimeTicks, mediaSourceId, audioStreamIndex, subtitleStreamIndex, maxAudioChannels, result.PlaySessionId, userId);
|
SetDeviceSpecificData(item, mediaSource, profile, auth, maxBitrate, startTimeTicks, mediaSourceId, audioStreamIndex, subtitleStreamIndex, maxAudioChannels, result.PlaySessionId, userId, enableDirectPlay, enableDirectStream, enableTranscoding);
|
||||||
}
|
}
|
||||||
|
|
||||||
SortMediaSources(result, maxBitrate);
|
SortMediaSources(result, maxBitrate);
|
||||||
|
@ -267,7 +277,10 @@ namespace MediaBrowser.Api.Playback
|
||||||
int? subtitleStreamIndex,
|
int? subtitleStreamIndex,
|
||||||
int? maxAudioChannels,
|
int? maxAudioChannels,
|
||||||
string playSessionId,
|
string playSessionId,
|
||||||
string userId)
|
string userId,
|
||||||
|
bool enableDirectPlay,
|
||||||
|
bool enableDirectStream,
|
||||||
|
bool enableTranscoding)
|
||||||
{
|
{
|
||||||
var streamBuilder = new StreamBuilder(_mediaEncoder, Logger);
|
var streamBuilder = new StreamBuilder(_mediaEncoder, Logger);
|
||||||
|
|
||||||
|
@ -290,6 +303,19 @@ namespace MediaBrowser.Api.Playback
|
||||||
|
|
||||||
var user = _userManager.GetUserById(userId);
|
var user = _userManager.GetUserById(userId);
|
||||||
|
|
||||||
|
if (!enableDirectPlay)
|
||||||
|
{
|
||||||
|
mediaSource.SupportsDirectPlay = false;
|
||||||
|
}
|
||||||
|
if (!enableDirectStream)
|
||||||
|
{
|
||||||
|
mediaSource.SupportsDirectStream = false;
|
||||||
|
}
|
||||||
|
if (!enableTranscoding)
|
||||||
|
{
|
||||||
|
mediaSource.SupportsTranscoding = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (mediaSource.SupportsDirectPlay)
|
if (mediaSource.SupportsDirectPlay)
|
||||||
{
|
{
|
||||||
var supportsDirectStream = mediaSource.SupportsDirectStream;
|
var supportsDirectStream = mediaSource.SupportsDirectStream;
|
||||||
|
|
|
@ -26,8 +26,6 @@ namespace MediaBrowser.Api.Playback.Progressive
|
||||||
[Route("/Audio/{Id}/stream", "HEAD", Summary = "Gets an audio stream")]
|
[Route("/Audio/{Id}/stream", "HEAD", Summary = "Gets an audio stream")]
|
||||||
public class GetAudioStream : StreamRequest
|
public class GetAudioStream : StreamRequest
|
||||||
{
|
{
|
||||||
[ApiMember(Name = "Container", Description = "Container", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
|
||||||
public string Container { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -41,6 +41,7 @@ namespace MediaBrowser.Api.Playback.Progressive
|
||||||
[Route("/Videos/{Id}/stream.wtv", "GET")]
|
[Route("/Videos/{Id}/stream.wtv", "GET")]
|
||||||
[Route("/Videos/{Id}/stream.mov", "GET")]
|
[Route("/Videos/{Id}/stream.mov", "GET")]
|
||||||
[Route("/Videos/{Id}/stream.iso", "GET")]
|
[Route("/Videos/{Id}/stream.iso", "GET")]
|
||||||
|
[Route("/Videos/{Id}/stream.flv", "GET")]
|
||||||
[Route("/Videos/{Id}/stream", "GET")]
|
[Route("/Videos/{Id}/stream", "GET")]
|
||||||
[Route("/Videos/{Id}/stream.ts", "HEAD")]
|
[Route("/Videos/{Id}/stream.ts", "HEAD")]
|
||||||
[Route("/Videos/{Id}/stream.webm", "HEAD")]
|
[Route("/Videos/{Id}/stream.webm", "HEAD")]
|
||||||
|
@ -59,6 +60,7 @@ namespace MediaBrowser.Api.Playback.Progressive
|
||||||
[Route("/Videos/{Id}/stream.m2ts", "HEAD")]
|
[Route("/Videos/{Id}/stream.m2ts", "HEAD")]
|
||||||
[Route("/Videos/{Id}/stream.mov", "HEAD")]
|
[Route("/Videos/{Id}/stream.mov", "HEAD")]
|
||||||
[Route("/Videos/{Id}/stream.iso", "HEAD")]
|
[Route("/Videos/{Id}/stream.iso", "HEAD")]
|
||||||
|
[Route("/Videos/{Id}/stream.flv", "HEAD")]
|
||||||
[Route("/Videos/{Id}/stream", "HEAD")]
|
[Route("/Videos/{Id}/stream", "HEAD")]
|
||||||
public class GetVideoStream : VideoStreamRequest
|
public class GetVideoStream : VideoStreamRequest
|
||||||
{
|
{
|
||||||
|
|
|
@ -22,6 +22,9 @@ namespace MediaBrowser.Api.Playback
|
||||||
[ApiMember(Name = "DeviceId", Description = "The device id of the client requesting. Used to stop encoding processes when needed.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
|
[ApiMember(Name = "DeviceId", Description = "The device id of the client requesting. Used to stop encoding processes when needed.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||||
public string DeviceId { get; set; }
|
public string DeviceId { get; set; }
|
||||||
|
|
||||||
|
[ApiMember(Name = "Container", Description = "Container", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||||
|
public string Container { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the audio codec.
|
/// Gets or sets the audio codec.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -13,7 +13,7 @@ using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using MediaBrowser.MediaEncoding.Encoder;
|
using MediaBrowser.Controller.MediaEncoding;
|
||||||
|
|
||||||
namespace MediaBrowser.Api.Playback
|
namespace MediaBrowser.Api.Playback
|
||||||
{
|
{
|
||||||
|
|
248
MediaBrowser.Api/Playback/UniversalAudioService.cs
Normal file
248
MediaBrowser.Api/Playback/UniversalAudioService.cs
Normal file
|
@ -0,0 +1,248 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MediaBrowser.Api.Playback.Hls;
|
||||||
|
using MediaBrowser.Api.Playback.Progressive;
|
||||||
|
using MediaBrowser.Common.Net;
|
||||||
|
using MediaBrowser.Controller.Configuration;
|
||||||
|
using MediaBrowser.Controller.Devices;
|
||||||
|
using MediaBrowser.Controller.Dlna;
|
||||||
|
using MediaBrowser.Controller.Drawing;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
|
using MediaBrowser.Controller.MediaEncoding;
|
||||||
|
using MediaBrowser.Controller.Net;
|
||||||
|
using MediaBrowser.Model.Dlna;
|
||||||
|
using MediaBrowser.Model.IO;
|
||||||
|
using MediaBrowser.Model.MediaInfo;
|
||||||
|
using MediaBrowser.Model.Serialization;
|
||||||
|
using MediaBrowser.Model.Services;
|
||||||
|
|
||||||
|
namespace MediaBrowser.Api.Playback
|
||||||
|
{
|
||||||
|
public class BaseUniversalRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the id.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The id.</value>
|
||||||
|
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
[ApiMember(Name = "MediaSourceId", Description = "The media version id, if playing an alternate version", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||||
|
public string MediaSourceId { get; set; }
|
||||||
|
|
||||||
|
[ApiMember(Name = "DeviceId", Description = "The device id of the client requesting. Used to stop encoding processes when needed.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||||
|
public string DeviceId { get; set; }
|
||||||
|
|
||||||
|
public string Token { get; set; }
|
||||||
|
|
||||||
|
public string UserId { get; set; }
|
||||||
|
public string AudioCodec { get; set; }
|
||||||
|
public string Container { get; set; }
|
||||||
|
|
||||||
|
public int? MaxAudioChannels { get; set; }
|
||||||
|
|
||||||
|
public long? MaxStreamingBitrate { get; set; }
|
||||||
|
|
||||||
|
[ApiMember(Name = "StartTimeTicks", Description = "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
||||||
|
public long? StartTimeTicks { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("/Audio/{Id}/universal.{Container}", "GET", Summary = "Gets an audio stream")]
|
||||||
|
[Route("/Audio/{Id}/universal", "GET", Summary = "Gets an audio stream")]
|
||||||
|
[Route("/Audio/{Id}/universal.{Container}", "HEAD", Summary = "Gets an audio stream")]
|
||||||
|
[Route("/Audio/{Id}/universal", "HEAD", Summary = "Gets an audio stream")]
|
||||||
|
public class GetUniversalAudioStream : BaseUniversalRequest
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
//[Authenticated]
|
||||||
|
public class UniversalAudioService : BaseApiService
|
||||||
|
{
|
||||||
|
public UniversalAudioService(IServerConfigurationManager serverConfigurationManager, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, IDeviceManager deviceManager, ISubtitleEncoder subtitleEncoder, IMediaSourceManager mediaSourceManager, IZipClient zipClient, IJsonSerializer jsonSerializer, IAuthorizationContext authorizationContext, IImageProcessor imageProcessor, INetworkManager networkManager)
|
||||||
|
{
|
||||||
|
ServerConfigurationManager = serverConfigurationManager;
|
||||||
|
UserManager = userManager;
|
||||||
|
LibraryManager = libraryManager;
|
||||||
|
IsoManager = isoManager;
|
||||||
|
MediaEncoder = mediaEncoder;
|
||||||
|
FileSystem = fileSystem;
|
||||||
|
DlnaManager = dlnaManager;
|
||||||
|
DeviceManager = deviceManager;
|
||||||
|
SubtitleEncoder = subtitleEncoder;
|
||||||
|
MediaSourceManager = mediaSourceManager;
|
||||||
|
ZipClient = zipClient;
|
||||||
|
JsonSerializer = jsonSerializer;
|
||||||
|
AuthorizationContext = authorizationContext;
|
||||||
|
ImageProcessor = imageProcessor;
|
||||||
|
NetworkManager = networkManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected IServerConfigurationManager ServerConfigurationManager { get; private set; }
|
||||||
|
protected IUserManager UserManager { get; private set; }
|
||||||
|
protected ILibraryManager LibraryManager { get; private set; }
|
||||||
|
protected IIsoManager IsoManager { get; private set; }
|
||||||
|
protected IMediaEncoder MediaEncoder { get; private set; }
|
||||||
|
protected IFileSystem FileSystem { get; private set; }
|
||||||
|
protected IDlnaManager DlnaManager { get; private set; }
|
||||||
|
protected IDeviceManager DeviceManager { get; private set; }
|
||||||
|
protected ISubtitleEncoder SubtitleEncoder { get; private set; }
|
||||||
|
protected IMediaSourceManager MediaSourceManager { get; private set; }
|
||||||
|
protected IZipClient ZipClient { get; private set; }
|
||||||
|
protected IJsonSerializer JsonSerializer { get; private set; }
|
||||||
|
protected IAuthorizationContext AuthorizationContext { get; private set; }
|
||||||
|
protected IImageProcessor ImageProcessor { get; private set; }
|
||||||
|
protected INetworkManager NetworkManager { get; private set; }
|
||||||
|
|
||||||
|
public Task<object> Get(GetUniversalAudioStream request)
|
||||||
|
{
|
||||||
|
return GetUniversalStream(request, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<object> Head(GetUniversalAudioStream request)
|
||||||
|
{
|
||||||
|
return GetUniversalStream(request, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DeviceProfile GetDeviceProfile(GetUniversalAudioStream request)
|
||||||
|
{
|
||||||
|
var deviceProfile = new DeviceProfile();
|
||||||
|
|
||||||
|
var directPlayProfiles = new List<DirectPlayProfile>();
|
||||||
|
|
||||||
|
directPlayProfiles.Add(new DirectPlayProfile
|
||||||
|
{
|
||||||
|
Type = DlnaProfileType.Audio,
|
||||||
|
Container = request.Container
|
||||||
|
});
|
||||||
|
|
||||||
|
deviceProfile.DirectPlayProfiles = directPlayProfiles.ToArray();
|
||||||
|
|
||||||
|
deviceProfile.TranscodingProfiles = new[]
|
||||||
|
{
|
||||||
|
new TranscodingProfile
|
||||||
|
{
|
||||||
|
Type = DlnaProfileType.Audio,
|
||||||
|
Context = EncodingContext.Streaming,
|
||||||
|
Container = "ts",
|
||||||
|
AudioCodec = "aac",
|
||||||
|
Protocol = "hls"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return deviceProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<object> GetUniversalStream(GetUniversalAudioStream request, bool isHeadRequest)
|
||||||
|
{
|
||||||
|
var deviceProfile = GetDeviceProfile(request);
|
||||||
|
|
||||||
|
AuthorizationContext.GetAuthorizationInfo(Request).DeviceId = request.DeviceId;
|
||||||
|
|
||||||
|
var mediaInfoService = new MediaInfoService(MediaSourceManager, DeviceManager, LibraryManager, ServerConfigurationManager, NetworkManager, MediaEncoder, UserManager, JsonSerializer, AuthorizationContext)
|
||||||
|
{
|
||||||
|
Request = Request
|
||||||
|
};
|
||||||
|
|
||||||
|
var playbackInfoResult = await mediaInfoService.GetPlaybackInfo(new GetPostedPlaybackInfo
|
||||||
|
{
|
||||||
|
Id = request.Id,
|
||||||
|
MaxAudioChannels = request.MaxAudioChannels,
|
||||||
|
MaxStreamingBitrate = request.MaxStreamingBitrate,
|
||||||
|
StartTimeTicks = request.StartTimeTicks,
|
||||||
|
UserId = request.UserId,
|
||||||
|
DeviceProfile = deviceProfile,
|
||||||
|
MediaSourceId = request.MediaSourceId
|
||||||
|
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var mediaSource = playbackInfoResult.MediaSources[0];
|
||||||
|
|
||||||
|
var isStatic = mediaSource.SupportsDirectStream;
|
||||||
|
|
||||||
|
if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var service = new DynamicHlsService(ServerConfigurationManager,
|
||||||
|
UserManager,
|
||||||
|
LibraryManager,
|
||||||
|
IsoManager,
|
||||||
|
MediaEncoder,
|
||||||
|
FileSystem,
|
||||||
|
DlnaManager,
|
||||||
|
SubtitleEncoder,
|
||||||
|
DeviceManager,
|
||||||
|
MediaSourceManager,
|
||||||
|
ZipClient,
|
||||||
|
JsonSerializer,
|
||||||
|
AuthorizationContext,
|
||||||
|
NetworkManager)
|
||||||
|
{
|
||||||
|
Request = Request
|
||||||
|
};
|
||||||
|
|
||||||
|
var transcodingProfile = deviceProfile.TranscodingProfiles[0];
|
||||||
|
|
||||||
|
var newRequest = new GetMasterHlsAudioPlaylist
|
||||||
|
{
|
||||||
|
AudioBitRate = isStatic ? (int?)null : Convert.ToInt32(Math.Min(request.MaxStreamingBitrate ?? 192000, int.MaxValue)),
|
||||||
|
AudioCodec = transcodingProfile.AudioCodec,
|
||||||
|
Container = ".m3u8",
|
||||||
|
DeviceId = request.DeviceId,
|
||||||
|
Id = request.Id,
|
||||||
|
MaxAudioChannels = request.MaxAudioChannels,
|
||||||
|
MediaSourceId = mediaSource.Id,
|
||||||
|
PlaySessionId = playbackInfoResult.PlaySessionId,
|
||||||
|
StartTimeTicks = request.StartTimeTicks,
|
||||||
|
Static = isStatic
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isHeadRequest)
|
||||||
|
{
|
||||||
|
return await service.Head(newRequest).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
return await service.Get(newRequest).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var service = new AudioService(ServerConfigurationManager,
|
||||||
|
UserManager,
|
||||||
|
LibraryManager,
|
||||||
|
IsoManager,
|
||||||
|
MediaEncoder,
|
||||||
|
FileSystem,
|
||||||
|
DlnaManager,
|
||||||
|
SubtitleEncoder,
|
||||||
|
DeviceManager,
|
||||||
|
MediaSourceManager,
|
||||||
|
ZipClient,
|
||||||
|
JsonSerializer,
|
||||||
|
AuthorizationContext,
|
||||||
|
ImageProcessor)
|
||||||
|
{
|
||||||
|
Request = Request
|
||||||
|
};
|
||||||
|
|
||||||
|
var newRequest = new GetAudioStream
|
||||||
|
{
|
||||||
|
AudioBitRate = isStatic ? (int?)null : Convert.ToInt32(Math.Min(request.MaxStreamingBitrate ?? 192000, int.MaxValue)),
|
||||||
|
//AudioCodec = request.AudioCodec,
|
||||||
|
Container = isStatic ? null : ("." + mediaSource.TranscodingContainer),
|
||||||
|
DeviceId = request.DeviceId,
|
||||||
|
Id = request.Id,
|
||||||
|
MaxAudioChannels = request.MaxAudioChannels,
|
||||||
|
MediaSourceId = mediaSource.Id,
|
||||||
|
PlaySessionId = playbackInfoResult.PlaySessionId,
|
||||||
|
StartTimeTicks = request.StartTimeTicks,
|
||||||
|
Static = isStatic
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isHeadRequest)
|
||||||
|
{
|
||||||
|
return await service.Head(newRequest).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
return await service.Get(newRequest).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -317,12 +317,6 @@ namespace MediaBrowser.Api.Reports
|
||||||
query.MaxParentalRating = _localization.GetRatingLevel(request.MaxOfficialRating);
|
query.MaxParentalRating = _localization.GetRatingLevel(request.MaxOfficialRating);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Albums
|
|
||||||
if (!string.IsNullOrEmpty(request.Albums))
|
|
||||||
{
|
|
||||||
query.AlbumNames = request.Albums.Split('|');
|
|
||||||
}
|
|
||||||
|
|
||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@ namespace MediaBrowser.Api.Sync
|
||||||
options.Add(SyncJobOption.ItemLimit);
|
options.Add(SyncJobOption.ItemLimit);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (item.IsFolderItem && !item.IsMusicGenre && !item.IsArtist && !item.IsType("musicalbum") && !item.IsGameGenre)
|
if ((item.IsFolder ?? false) && !item.IsMusicGenre && !item.IsArtist && !item.IsType("musicalbum") && !item.IsGameGenre)
|
||||||
{
|
{
|
||||||
options.Add(SyncJobOption.Quality);
|
options.Add(SyncJobOption.Quality);
|
||||||
options.Add(SyncJobOption.Profile);
|
options.Add(SyncJobOption.Profile);
|
||||||
|
@ -57,7 +57,7 @@ namespace MediaBrowser.Api.Sync
|
||||||
{
|
{
|
||||||
if (item.SupportsSync ?? false)
|
if (item.SupportsSync ?? false)
|
||||||
{
|
{
|
||||||
if (item.IsFolderItem || item.IsGameGenre || item.IsMusicGenre || item.IsGenre || item.IsArtist || item.IsStudio || item.IsPerson)
|
if ((item.IsFolder ?? false) || item.IsGameGenre || item.IsMusicGenre || item.IsGenre || item.IsArtist || item.IsStudio || item.IsPerson)
|
||||||
{
|
{
|
||||||
options.Add(SyncJobOption.SyncNewContent);
|
options.Add(SyncJobOption.SyncNewContent);
|
||||||
options.Add(SyncJobOption.ItemLimit);
|
options.Add(SyncJobOption.ItemLimit);
|
||||||
|
|
|
@ -245,25 +245,15 @@ namespace MediaBrowser.Api.UserLibrary
|
||||||
|
|
||||||
User user = null;
|
User user = null;
|
||||||
BaseItem parentItem;
|
BaseItem parentItem;
|
||||||
List<BaseItem> libraryItems = null;
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(request.UserId))
|
if (!string.IsNullOrWhiteSpace(request.UserId))
|
||||||
{
|
{
|
||||||
user = UserManager.GetUserById(request.UserId);
|
user = UserManager.GetUserById(request.UserId);
|
||||||
parentItem = string.IsNullOrEmpty(request.ParentId) ? user.RootFolder : LibraryManager.GetItemById(request.ParentId);
|
parentItem = string.IsNullOrEmpty(request.ParentId) ? user.RootFolder : LibraryManager.GetItemById(request.ParentId);
|
||||||
|
|
||||||
if (RequiresLibraryItems(request, dtoOptions))
|
|
||||||
{
|
|
||||||
libraryItems = user.RootFolder.GetRecursiveChildren(user).ToList();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
parentItem = string.IsNullOrEmpty(request.ParentId) ? LibraryManager.RootFolder : LibraryManager.GetItemById(request.ParentId);
|
parentItem = string.IsNullOrEmpty(request.ParentId) ? LibraryManager.RootFolder : LibraryManager.GetItemById(request.ParentId);
|
||||||
if (RequiresLibraryItems(request, dtoOptions))
|
|
||||||
{
|
|
||||||
libraryItems = LibraryManager.RootFolder.GetRecursiveChildren().ToList();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
IEnumerable<BaseItem> items;
|
IEnumerable<BaseItem> items;
|
||||||
|
@ -307,8 +297,6 @@ namespace MediaBrowser.Api.UserLibrary
|
||||||
|
|
||||||
var filteredItems = FilterItems(request, extractedItems, user);
|
var filteredItems = FilterItems(request, extractedItems, user);
|
||||||
|
|
||||||
filteredItems = FilterByLibraryItems(request, filteredItems.Cast<IItemByName>(), user, libraryItems).Cast<BaseItem>();
|
|
||||||
|
|
||||||
filteredItems = LibraryManager.Sort(filteredItems, user, request.GetOrderBy(), request.SortOrder ?? SortOrder.Ascending);
|
filteredItems = LibraryManager.Sort(filteredItems, user, request.GetOrderBy(), request.SortOrder ?? SortOrder.Ascending);
|
||||||
|
|
||||||
var ibnItemsArray = filteredItems.ToList();
|
var ibnItemsArray = filteredItems.ToList();
|
||||||
|
@ -334,15 +322,7 @@ namespace MediaBrowser.Api.UserLibrary
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
IEnumerable<Tuple<BaseItem, List<BaseItem>>> tuples;
|
var tuples = ibnItems.Select(i => new Tuple<BaseItem, List<BaseItem>>(i, new List<BaseItem>()));
|
||||||
if (dtoOptions.Fields.Contains(ItemFields.ItemCounts))
|
|
||||||
{
|
|
||||||
tuples = ibnItems.Select(i => new Tuple<BaseItem, List<BaseItem>>(i, ((IItemByName)i).GetTaggedItems(libraryItems).ToList()));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
tuples = ibnItems.Select(i => new Tuple<BaseItem, List<BaseItem>>(i, new List<BaseItem>()));
|
|
||||||
}
|
|
||||||
|
|
||||||
var syncProgess = DtoService.GetSyncedItemProgress(dtoOptions);
|
var syncProgess = DtoService.GetSyncedItemProgress(dtoOptions);
|
||||||
var dtos = tuples.Select(i => DtoService.GetItemByNameDto(i.Item1, dtoOptions, i.Item2, syncProgess, user));
|
var dtos = tuples.Select(i => DtoService.GetItemByNameDto(i.Item1, dtoOptions, i.Item2, syncProgess, user));
|
||||||
|
@ -352,52 +332,6 @@ namespace MediaBrowser.Api.UserLibrary
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool RequiresLibraryItems(GetItemsByName request, DtoOptions options)
|
|
||||||
{
|
|
||||||
var filters = request.GetFilters().ToList();
|
|
||||||
|
|
||||||
if (filters.Contains(ItemFilter.IsPlayed))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filters.Contains(ItemFilter.IsUnplayed))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.IsPlayed.HasValue)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return options.Fields.Contains(ItemFields.ItemCounts);
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<IItemByName> FilterByLibraryItems(GetItemsByName request, IEnumerable<IItemByName> items, User user, IEnumerable<BaseItem> libraryItems)
|
|
||||||
{
|
|
||||||
var filters = request.GetFilters().ToList();
|
|
||||||
|
|
||||||
if (filters.Contains(ItemFilter.IsPlayed))
|
|
||||||
{
|
|
||||||
items = items.Where(i => i.GetTaggedItems(libraryItems).All(l => l.IsPlayed(user)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filters.Contains(ItemFilter.IsUnplayed))
|
|
||||||
{
|
|
||||||
items = items.Where(i => i.GetTaggedItems(libraryItems).All(l => l.IsUnplayed(user)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.IsPlayed.HasValue)
|
|
||||||
{
|
|
||||||
var val = request.IsPlayed.Value;
|
|
||||||
|
|
||||||
items = items.Where(i => i.GetTaggedItems(libraryItems).All(l => l.IsPlayed(user)) == val);
|
|
||||||
}
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Filters the items.
|
/// Filters the items.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -277,6 +277,8 @@ namespace MediaBrowser.Api.UserLibrary
|
||||||
[ApiMember(Name = "Albums", Description = "Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
|
[ApiMember(Name = "Albums", Description = "Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
|
||||||
public string Albums { get; set; }
|
public string Albums { get; set; }
|
||||||
|
|
||||||
|
public string AlbumIds { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the item ids.
|
/// Gets or sets the item ids.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -9,6 +9,7 @@ using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
using MediaBrowser.Model.Globalization;
|
using MediaBrowser.Model.Globalization;
|
||||||
using MediaBrowser.Model.Services;
|
using MediaBrowser.Model.Services;
|
||||||
|
|
||||||
|
@ -359,15 +360,30 @@ namespace MediaBrowser.Api.UserLibrary
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExcludeArtistIds
|
// ExcludeArtistIds
|
||||||
if (!string.IsNullOrEmpty(request.ExcludeArtistIds))
|
if (!string.IsNullOrWhiteSpace(request.ExcludeArtistIds))
|
||||||
{
|
{
|
||||||
query.ExcludeArtistIds = request.ExcludeArtistIds.Split('|');
|
query.ExcludeArtistIds = request.ExcludeArtistIds.Split('|');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(request.AlbumIds))
|
||||||
|
{
|
||||||
|
query.AlbumIds = request.AlbumIds.Split('|');
|
||||||
|
}
|
||||||
|
|
||||||
// Albums
|
// Albums
|
||||||
if (!string.IsNullOrEmpty(request.Albums))
|
if (!string.IsNullOrEmpty(request.Albums))
|
||||||
{
|
{
|
||||||
query.AlbumNames = request.Albums.Split('|');
|
query.AlbumIds = request.Albums.Split('|').Select(i =>
|
||||||
|
{
|
||||||
|
return _libraryManager.GetItemList(new InternalItemsQuery
|
||||||
|
{
|
||||||
|
IncludeItemTypes = new[] { typeof(MusicAlbum).Name },
|
||||||
|
Name = i,
|
||||||
|
Limit = 1
|
||||||
|
|
||||||
|
}).Select(album => album.Id.ToString("N")).FirstOrDefault();
|
||||||
|
|
||||||
|
}).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Studios
|
// Studios
|
||||||
|
|
|
@ -360,7 +360,8 @@ namespace MediaBrowser.Api.UserLibrary
|
||||||
var currentUser = user;
|
var currentUser = user;
|
||||||
|
|
||||||
var dtos = series
|
var dtos = series
|
||||||
.GetRecursiveChildren(i => i is Episode && i.ParentIndexNumber.HasValue && i.ParentIndexNumber.Value == 0)
|
.GetEpisodes(user)
|
||||||
|
.Where(i => i.ParentIndexNumber.HasValue && i.ParentIndexNumber.Value == 0)
|
||||||
.OrderBy(i =>
|
.OrderBy(i =>
|
||||||
{
|
{
|
||||||
if (i.PremiereDate.HasValue)
|
if (i.PremiereDate.HasValue)
|
||||||
|
|
|
@ -46,6 +46,11 @@ namespace MediaBrowser.Controller.Entities.Audio
|
||||||
AlbumArtists = new List<string>();
|
AlbumArtists = new List<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override double? GetDefaultPrimaryImageAspectRatio()
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
[IgnoreDataMember]
|
[IgnoreDataMember]
|
||||||
public override bool SupportsPlayedStatus
|
public override bool SupportsPlayedStatus
|
||||||
{
|
{
|
||||||
|
|
|
@ -12,5 +12,10 @@ namespace MediaBrowser.Controller.Entities.Audio
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override double? GetDefaultPrimaryImageAspectRatio()
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,6 +119,11 @@ namespace MediaBrowser.Controller.Entities.Audio
|
||||||
return Tracks;
|
return Tracks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override double? GetDefaultPrimaryImageAspectRatio()
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
public override List<string> GetUserDataKeys()
|
public override List<string> GetUserDataKeys()
|
||||||
{
|
{
|
||||||
var list = base.GetUserDataKeys();
|
var list = base.GetUserDataKeys();
|
||||||
|
|
|
@ -68,6 +68,11 @@ namespace MediaBrowser.Controller.Entities.Audio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override double? GetDefaultPrimaryImageAspectRatio()
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
public override bool CanDelete()
|
public override bool CanDelete()
|
||||||
{
|
{
|
||||||
return !IsAccessedByName;
|
return !IsAccessedByName;
|
||||||
|
|
|
@ -63,6 +63,11 @@ namespace MediaBrowser.Controller.Entities.Audio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override double? GetDefaultPrimaryImageAspectRatio()
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
public override bool CanDelete()
|
public override bool CanDelete()
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -47,6 +47,11 @@ namespace MediaBrowser.Controller.Entities
|
||||||
return SeriesPresentationUniqueKey;
|
return SeriesPresentationUniqueKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override double? GetDefaultPrimaryImageAspectRatio()
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
[IgnoreDataMember]
|
[IgnoreDataMember]
|
||||||
public override bool EnableRefreshOnDateModifiedChange
|
public override bool EnableRefreshOnDateModifiedChange
|
||||||
{
|
{
|
||||||
|
|
|
@ -1260,6 +1260,11 @@ namespace MediaBrowser.Controller.Entities
|
||||||
get { return null; }
|
get { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public virtual double? GetDefaultPrimaryImageAspectRatio()
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public virtual string CreatePresentationUniqueKey()
|
public virtual string CreatePresentationUniqueKey()
|
||||||
{
|
{
|
||||||
return Id.ToString("N");
|
return Id.ToString("N");
|
||||||
|
@ -2073,9 +2078,31 @@ namespace MediaBrowser.Controller.Entities
|
||||||
/// Gets the file system path to delete when the item is to be deleted
|
/// Gets the file system path to delete when the item is to be deleted
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual IEnumerable<string> GetDeletePaths()
|
public virtual IEnumerable<FileSystemMetadata> GetDeletePaths()
|
||||||
{
|
{
|
||||||
return new[] { Path };
|
return new[] {
|
||||||
|
new FileSystemMetadata
|
||||||
|
{
|
||||||
|
FullName = Path,
|
||||||
|
IsDirectory = IsFolder
|
||||||
|
}
|
||||||
|
}.Concat(GetLocalMetadataFilesToDelete());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected List<FileSystemMetadata> GetLocalMetadataFilesToDelete()
|
||||||
|
{
|
||||||
|
if (IsFolder || !IsInMixedFolder)
|
||||||
|
{
|
||||||
|
return new List<FileSystemMetadata>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var filename = System.IO.Path.GetFileNameWithoutExtension(Path);
|
||||||
|
var extensions = new[] { ".nfo", ".xml", ".srt" }.ToList();
|
||||||
|
extensions.AddRange(SupportedImageExtensionsList);
|
||||||
|
|
||||||
|
return FileSystem.GetFiles(System.IO.Path.GetDirectoryName(Path))
|
||||||
|
.Where(i => extensions.Contains(i.Extension, StringComparer.OrdinalIgnoreCase) && System.IO.Path.GetFileNameWithoutExtension(i.FullName).StartsWith(filename, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool AllowsMultipleImages(ImageType type)
|
public bool AllowsMultipleImages(ImageType type)
|
||||||
|
|
|
@ -4,6 +4,7 @@ using MediaBrowser.Model.Entities;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Model.Serialization;
|
using MediaBrowser.Model.Serialization;
|
||||||
|
|
||||||
namespace MediaBrowser.Controller.Entities
|
namespace MediaBrowser.Controller.Entities
|
||||||
|
@ -97,11 +98,17 @@ namespace MediaBrowser.Controller.Entities
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override IEnumerable<string> GetDeletePaths()
|
public override IEnumerable<FileSystemMetadata> GetDeletePaths()
|
||||||
{
|
{
|
||||||
if (!DetectIsInMixedFolder())
|
if (!DetectIsInMixedFolder())
|
||||||
{
|
{
|
||||||
return new[] { System.IO.Path.GetDirectoryName(Path) };
|
return new[] {
|
||||||
|
new FileSystemMetadata
|
||||||
|
{
|
||||||
|
FullName = System.IO.Path.GetDirectoryName(Path),
|
||||||
|
IsDirectory = true
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return base.GetDeletePaths();
|
return base.GetDeletePaths();
|
||||||
|
|
|
@ -204,6 +204,8 @@ namespace MediaBrowser.Controller.Entities
|
||||||
/// <param name="image">The image.</param>
|
/// <param name="image">The image.</param>
|
||||||
/// <param name="index">The index.</param>
|
/// <param name="index">The index.</param>
|
||||||
void SetImage(ItemImageInfo image, int index);
|
void SetImage(ItemImageInfo image, int index);
|
||||||
|
|
||||||
|
double? GetDefaultPrimaryImageAspectRatio();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class HasImagesExtensions
|
public static class HasImagesExtensions
|
||||||
|
|
|
@ -141,7 +141,7 @@ namespace MediaBrowser.Controller.Entities
|
||||||
public string ExternalSeriesId { get; set; }
|
public string ExternalSeriesId { get; set; }
|
||||||
public string ExternalId { get; set; }
|
public string ExternalId { get; set; }
|
||||||
|
|
||||||
public string[] AlbumNames { get; set; }
|
public string[] AlbumIds { get; set; }
|
||||||
public string[] ArtistIds { get; set; }
|
public string[] ArtistIds { get; set; }
|
||||||
public string[] ExcludeArtistIds { get; set; }
|
public string[] ExcludeArtistIds { get; set; }
|
||||||
public string AncestorWithPresentationUniqueKey { get; set; }
|
public string AncestorWithPresentationUniqueKey { get; set; }
|
||||||
|
@ -202,7 +202,7 @@ namespace MediaBrowser.Controller.Entities
|
||||||
EnableTotalRecordCount = true;
|
EnableTotalRecordCount = true;
|
||||||
|
|
||||||
DtoOptions = new DtoOptions();
|
DtoOptions = new DtoOptions();
|
||||||
AlbumNames = new string[] { };
|
AlbumIds = new string[] { };
|
||||||
ArtistIds = new string[] { };
|
ArtistIds = new string[] { };
|
||||||
ExcludeArtistIds = new string[] { };
|
ExcludeArtistIds = new string[] { };
|
||||||
ExcludeProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
ExcludeProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
|
@ -57,6 +57,14 @@ namespace MediaBrowser.Controller.Entities.Movies
|
||||||
return config.BlockUnratedItems.Contains(UnratedItem.Movie);
|
return config.BlockUnratedItems.Contains(UnratedItem.Movie);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override double? GetDefaultPrimaryImageAspectRatio()
|
||||||
|
{
|
||||||
|
double value = 2;
|
||||||
|
value /= 3;
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
public override UnratedItem GetBlockUnratedType()
|
public override UnratedItem GetBlockUnratedType()
|
||||||
{
|
{
|
||||||
return UnratedItem.Movie;
|
return UnratedItem.Movie;
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user