Merge remote-tracking branch 'upstream/master' into schedules-direct

This commit is contained in:
Cody Robibero
2021-10-08 07:49:40 -06:00
452 changed files with 6467 additions and 4633 deletions

View File

@@ -38,7 +38,6 @@ using Emby.Server.Implementations.Playlists;
using Emby.Server.Implementations.Plugins;
using Emby.Server.Implementations.QuickConnect;
using Emby.Server.Implementations.ScheduledTasks;
using Emby.Server.Implementations.Security;
using Emby.Server.Implementations.Serialization;
using Emby.Server.Implementations.Session;
using Emby.Server.Implementations.SyncPlay;
@@ -59,7 +58,6 @@ using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
@@ -75,7 +73,6 @@ using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.QuickConnect;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Controller.Subtitles;
@@ -117,6 +114,11 @@ namespace Emby.Server.Implementations
/// </summary>
private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
/// <summary>
/// The disposable parts.
/// </summary>
private readonly List<IDisposable> _disposableParts = new List<IDisposable>();
private readonly IFileSystem _fileSystemManager;
private readonly IConfiguration _startupConfig;
private readonly IXmlSerializer _xmlSerializer;
@@ -128,104 +130,15 @@ namespace Emby.Server.Implementations
private ISessionManager _sessionManager;
private string[] _urlPrefixes;
/// <summary>
/// Gets a value indicating whether this instance can self restart.
/// </summary>
public bool CanSelfRestart => _startupOptions.RestartPath != null;
public bool CoreStartupHasCompleted { get; private set; }
public virtual bool CanLaunchWebBrowser
{
get
{
if (!Environment.UserInteractive)
{
return false;
}
if (_startupOptions.IsService)
{
return false;
}
return OperatingSystem.IsWindows() || OperatingSystem.IsMacOS();
}
}
/// <summary>
/// Gets the <see cref="INetworkManager"/> singleton instance.
/// </summary>
public INetworkManager NetManager { get; internal set; }
/// <summary>
/// Occurs when [has pending restart changed].
/// </summary>
public event EventHandler HasPendingRestartChanged;
/// <summary>
/// Gets a value indicating whether this instance has changes that require the entire application to restart.
/// </summary>
/// <value><c>true</c> if this instance has pending application restart; otherwise, <c>false</c>.</value>
public bool HasPendingRestart { get; private set; }
/// <inheritdoc />
public bool IsShuttingDown { get; private set; }
/// <summary>
/// Gets the logger.
/// </summary>
protected ILogger<ApplicationHost> Logger { get; }
protected IServiceCollection ServiceCollection { get; }
/// <summary>
/// Gets the logger factory.
/// </summary>
protected ILoggerFactory LoggerFactory { get; }
/// <summary>
/// Gets or sets the application paths.
/// </summary>
/// <value>The application paths.</value>
protected IServerApplicationPaths ApplicationPaths { get; set; }
/// <summary>
/// Gets or sets all concrete types.
/// </summary>
/// <value>All concrete types.</value>
private Type[] _allConcreteTypes;
/// <summary>
/// The disposable parts.
/// </summary>
private readonly List<IDisposable> _disposableParts = new List<IDisposable>();
private DeviceId _deviceId;
/// <summary>
/// Gets or sets the configuration manager.
/// </summary>
/// <value>The configuration manager.</value>
public ServerConfigurationManager ConfigurationManager { get; set; }
/// <summary>
/// Gets or sets the service provider.
/// </summary>
public IServiceProvider ServiceProvider { get; set; }
/// <summary>
/// Gets the http port for the webhost.
/// </summary>
public int HttpPort { get; private set; }
/// <summary>
/// Gets the https port for the webhost.
/// </summary>
public int HttpsPort { get; private set; }
/// <summary>
/// Gets the value of the PublishedServerUrl setting.
/// </summary>
public string PublishedServerUrl => _startupOptions.PublishedServerUrl ?? _startupConfig[UdpServer.AddressOverrideConfigKey];
private bool _disposed = false;
/// <summary>
/// Initializes a new instance of the <see cref="ApplicationHost"/> class.
@@ -268,6 +181,143 @@ namespace Emby.Server.Implementations
ApplicationVersion);
}
/// <summary>
/// Occurs when [has pending restart changed].
/// </summary>
public event EventHandler HasPendingRestartChanged;
/// <summary>
/// Gets a value indicating whether this instance can self restart.
/// </summary>
public bool CanSelfRestart => _startupOptions.RestartPath != null;
public bool CoreStartupHasCompleted { get; private set; }
public virtual bool CanLaunchWebBrowser
{
get
{
if (!Environment.UserInteractive)
{
return false;
}
if (_startupOptions.IsService)
{
return false;
}
return OperatingSystem.IsWindows() || OperatingSystem.IsMacOS();
}
}
/// <summary>
/// Gets the <see cref="INetworkManager"/> singleton instance.
/// </summary>
public INetworkManager NetManager { get; internal set; }
/// <summary>
/// Gets a value indicating whether this instance has changes that require the entire application to restart.
/// </summary>
/// <value><c>true</c> if this instance has pending application restart; otherwise, <c>false</c>.</value>
public bool HasPendingRestart { get; private set; }
/// <inheritdoc />
public bool IsShuttingDown { get; private set; }
/// <summary>
/// Gets the logger.
/// </summary>
protected ILogger<ApplicationHost> Logger { get; }
protected IServiceCollection ServiceCollection { get; }
/// <summary>
/// Gets the logger factory.
/// </summary>
protected ILoggerFactory LoggerFactory { get; }
/// <summary>
/// Gets or sets the application paths.
/// </summary>
/// <value>The application paths.</value>
protected IServerApplicationPaths ApplicationPaths { get; set; }
/// <summary>
/// Gets or sets the configuration manager.
/// </summary>
/// <value>The configuration manager.</value>
public ServerConfigurationManager ConfigurationManager { get; set; }
/// <summary>
/// Gets or sets the service provider.
/// </summary>
public IServiceProvider ServiceProvider { get; set; }
/// <summary>
/// Gets the http port for the webhost.
/// </summary>
public int HttpPort { get; private set; }
/// <summary>
/// Gets the https port for the webhost.
/// </summary>
public int HttpsPort { get; private set; }
/// <summary>
/// Gets the value of the PublishedServerUrl setting.
/// </summary>
public string PublishedServerUrl => _startupOptions.PublishedServerUrl ?? _startupConfig[UdpServer.AddressOverrideConfigKey];
/// <inheritdoc />
public Version ApplicationVersion { get; }
/// <inheritdoc />
public string ApplicationVersionString { get; }
/// <summary>
/// Gets the current application user agent.
/// </summary>
/// <value>The application user agent.</value>
public string ApplicationUserAgent { get; }
/// <summary>
/// Gets the email address for use within a comment section of a user agent field.
/// Presently used to provide contact information to MusicBrainz service.
/// </summary>
public string ApplicationUserAgentAddress => "team@jellyfin.org";
/// <summary>
/// Gets the current application name.
/// </summary>
/// <value>The application name.</value>
public string ApplicationProductName { get; } = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly().Location).ProductName;
public string SystemId
{
get
{
_deviceId ??= new DeviceId(ApplicationPaths, LoggerFactory);
return _deviceId.Value;
}
}
/// <inheritdoc/>
public string Name => ApplicationProductName;
private string CertificatePath { get; set; }
public X509Certificate2 Certificate { get; private set; }
/// <inheritdoc/>
public bool ListenWithHttps => Certificate != null && ConfigurationManager.GetNetworkConfiguration().EnableHttps;
public string FriendlyName =>
string.IsNullOrEmpty(ConfigurationManager.Configuration.ServerName)
? Environment.MachineName
: ConfigurationManager.Configuration.ServerName;
/// <summary>
/// Temporary function to migration network settings out of system.xml and into network.xml.
/// TODO: remove at the point when a fixed migration path has been decided upon.
@@ -300,45 +350,6 @@ namespace Emby.Server.Implementations
.Replace(appPaths.InternalMetadataPath, appPaths.VirtualInternalMetadataPath, StringComparison.OrdinalIgnoreCase);
}
/// <inheritdoc />
public Version ApplicationVersion { get; }
/// <inheritdoc />
public string ApplicationVersionString { get; }
/// <summary>
/// Gets the current application user agent.
/// </summary>
/// <value>The application user agent.</value>
public string ApplicationUserAgent { get; }
/// <summary>
/// Gets the email address for use within a comment section of a user agent field.
/// Presently used to provide contact information to MusicBrainz service.
/// </summary>
public string ApplicationUserAgentAddress => "team@jellyfin.org";
/// <summary>
/// Gets the current application name.
/// </summary>
/// <value>The application name.</value>
public string ApplicationProductName { get; } = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly().Location).ProductName;
private DeviceId _deviceId;
public string SystemId
{
get
{
_deviceId ??= new DeviceId(ApplicationPaths, LoggerFactory);
return _deviceId.Value;
}
}
/// <inheritdoc/>
public string Name => ApplicationProductName;
/// <summary>
/// Creates an instance of type and resolves all constructor dependencies.
/// </summary>
@@ -537,12 +548,8 @@ namespace Emby.Server.Implementations
HttpsPort = NetworkConfiguration.DefaultHttpsPort;
}
CertificateInfo = new CertificateInfo
{
Path = networkConfiguration.CertificatePath,
Password = networkConfiguration.CertificatePassword
};
Certificate = GetCertificate(CertificateInfo);
CertificatePath = networkConfiguration.CertificatePath;
Certificate = GetCertificate(CertificatePath, networkConfiguration.CertificatePassword);
RegisterServices();
@@ -595,8 +602,6 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
ServiceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
ServiceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
ServiceCollection.AddSingleton<EncodingHelper>();
@@ -618,8 +623,6 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>();
ServiceCollection.AddSingleton<IDeviceManager, DeviceManager>();
ServiceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>();
ServiceCollection.AddSingleton<ISubtitleManager, SubtitleManager>();
@@ -655,8 +658,7 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
ServiceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>();
ServiceCollection.AddSingleton<ISessionContext, SessionContext>();
ServiceCollection.AddScoped<ISessionContext, SessionContext>();
ServiceCollection.AddSingleton<IAuthService, AuthService>();
ServiceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
@@ -685,8 +687,6 @@ namespace Emby.Server.Implementations
_mediaEncoder = Resolve<IMediaEncoder>();
_sessionManager = Resolve<ISessionManager>();
((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
SetStaticProperties();
var userDataRepo = (SqliteUserDataRepository)Resolve<IUserDataRepository>();
@@ -725,30 +725,27 @@ namespace Emby.Server.Implementations
logger.LogInformation("Application directory: {ApplicationPath}", appPaths.ProgramSystemPath);
}
private X509Certificate2 GetCertificate(CertificateInfo info)
private X509Certificate2 GetCertificate(string path, string password)
{
var certificateLocation = info?.Path;
if (string.IsNullOrWhiteSpace(certificateLocation))
if (string.IsNullOrWhiteSpace(path))
{
return null;
}
try
{
if (!File.Exists(certificateLocation))
if (!File.Exists(path))
{
return null;
}
// Don't use an empty string password
var password = string.IsNullOrWhiteSpace(info.Password) ? null : info.Password;
password = string.IsNullOrWhiteSpace(password) ? null : password;
var localCert = new X509Certificate2(certificateLocation, password, X509KeyStorageFlags.UserKeySet);
// localCert.PrivateKey = PrivateKey.CreateFromFile(pvk_file).RSA;
var localCert = new X509Certificate2(path, password, X509KeyStorageFlags.UserKeySet);
if (!localCert.HasPrivateKey)
{
Logger.LogError("No private key included in SSL cert {CertificateLocation}.", certificateLocation);
Logger.LogError("No private key included in SSL cert {CertificateLocation}.", path);
return null;
}
@@ -756,7 +753,7 @@ namespace Emby.Server.Implementations
}
catch (Exception ex)
{
Logger.LogError(ex, "Error loading cert from {CertificateLocation}", certificateLocation);
Logger.LogError(ex, "Error loading cert from {CertificateLocation}", path);
return null;
}
}
@@ -867,10 +864,6 @@ namespace Emby.Server.Implementations
}
}
private CertificateInfo CertificateInfo { get; set; }
public X509Certificate2 Certificate { get; private set; }
private IEnumerable<string> GetUrlPrefixes()
{
var hosts = new[] { "+" };
@@ -882,7 +875,7 @@ namespace Emby.Server.Implementations
"http://" + i + ":" + HttpPort + "/"
};
if (CertificateInfo != null)
if (Certificate != null)
{
prefixes.Add("https://" + i + ":" + HttpsPort + "/");
}
@@ -946,7 +939,7 @@ namespace Emby.Server.Implementations
var newPath = networkConfig.CertificatePath;
if (!string.IsNullOrWhiteSpace(newPath)
&& !string.Equals(CertificateInfo?.Path, newPath, StringComparison.Ordinal))
&& !string.Equals(CertificatePath, newPath, StringComparison.Ordinal))
{
if (File.Exists(newPath))
{
@@ -1124,9 +1117,6 @@ namespace Emby.Server.Implementations
};
}
/// <inheritdoc/>
public bool ListenWithHttps => Certificate != null && ConfigurationManager.GetNetworkConfiguration().EnableHttps;
/// <inheritdoc/>
public string GetSmartApiUrl(IPAddress remoteAddr, int? port = null)
{
@@ -1213,14 +1203,7 @@ namespace Emby.Server.Implementations
}.ToString().TrimEnd('/');
}
public string FriendlyName =>
string.IsNullOrEmpty(ConfigurationManager.Configuration.ServerName)
? Environment.MachineName
: ConfigurationManager.Configuration.ServerName;
/// <summary>
/// Shuts down.
/// </summary>
/// <inheritdoc />
public async Task Shutdown()
{
if (IsShuttingDown)
@@ -1258,41 +1241,7 @@ namespace Emby.Server.Implementations
}
}
public virtual void LaunchUrl(string url)
{
if (!CanLaunchWebBrowser)
{
throw new NotSupportedException();
}
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = url,
UseShellExecute = true,
ErrorDialog = false
},
EnableRaisingEvents = true
};
process.Exited += (sender, args) => ((Process)sender).Dispose();
try
{
process.Start();
}
catch (Exception ex)
{
Logger.LogError(ex, "Error launching url: {url}", url);
throw;
}
}
private bool _disposed = false;
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
@@ -1337,11 +1286,4 @@ namespace Emby.Server.Implementations
_disposed = true;
}
}
internal class CertificateInfo
{
public string Path { get; set; }
public string Password { get; set; }
}
}

View File

@@ -45,6 +45,7 @@ namespace Emby.Server.Implementations.Archiving
options.Overwrite = true;
}
Directory.CreateDirectory(targetPath);
reader.WriteAllToDirectory(targetPath, options);
}
@@ -58,6 +59,7 @@ namespace Emby.Server.Implementations.Archiving
Overwrite = overwriteExistingFiles
};
Directory.CreateDirectory(targetPath);
reader.WriteAllToDirectory(targetPath, options);
}
@@ -71,6 +73,7 @@ namespace Emby.Server.Implementations.Archiving
Overwrite = overwriteExistingFiles
};
Directory.CreateDirectory(targetPath);
reader.WriteAllToDirectory(targetPath, options);
}
@@ -120,6 +123,7 @@ namespace Emby.Server.Implementations.Archiving
Overwrite = overwriteExistingFiles
};
Directory.CreateDirectory(targetPath);
reader.WriteAllToDirectory(targetPath, options);
}
@@ -151,6 +155,7 @@ namespace Emby.Server.Implementations.Archiving
Overwrite = overwriteExistingFiles
};
Directory.CreateDirectory(targetPath);
reader.WriteAllToDirectory(targetPath, options);
}
}

View File

@@ -10,8 +10,8 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
@@ -815,7 +815,7 @@ namespace Emby.Server.Implementations.Channels
{
if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
{
await using FileStream jsonStream = File.OpenRead(cachePath);
await using FileStream jsonStream = AsyncFile.OpenRead(cachePath);
var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (cachedResult != null)
{
@@ -838,7 +838,7 @@ namespace Emby.Server.Implementations.Channels
{
if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
{
await using FileStream jsonStream = File.OpenRead(cachePath);
await using FileStream jsonStream = AsyncFile.OpenRead(cachePath);
var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (cachedResult != null)
{

View File

@@ -196,8 +196,8 @@ namespace Emby.Server.Implementations.Collections
}
/// <inheritdoc />
public Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> ids)
=> AddToCollectionAsync(collectionId, ids, true, new MetadataRefreshOptions(new DirectoryService(_fileSystem)));
public Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> itemIds)
=> AddToCollectionAsync(collectionId, itemIds, true, new MetadataRefreshOptions(new DirectoryService(_fileSystem)));
private async Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> ids, bool fireEvent, MetadataRefreshOptions refreshOptions)
{

View File

@@ -98,7 +98,7 @@ namespace Emby.Server.Implementations.Data
/// <value>The write connection.</value>
protected SQLiteDatabaseConnection WriteConnection { get; set; }
protected ManagedConnection GetConnection(bool _ = false)
protected ManagedConnection GetConnection(bool readOnly = false)
{
WriteLock.Wait();
if (WriteConnection != null)
@@ -249,55 +249,4 @@ namespace Emby.Server.Implementations.Data
_disposed = true;
}
}
/// <summary>
/// The disk synchronization mode, controls how aggressively SQLite will write data
/// all the way out to physical storage.
/// </summary>
public enum SynchronousMode
{
/// <summary>
/// SQLite continues without syncing as soon as it has handed data off to the operating system.
/// </summary>
Off = 0,
/// <summary>
/// SQLite database engine will still sync at the most critical moments.
/// </summary>
Normal = 1,
/// <summary>
/// SQLite database engine will use the xSync method of the VFS
/// to ensure that all content is safely written to the disk surface prior to continuing.
/// </summary>
Full = 2,
/// <summary>
/// EXTRA synchronous is like FULL with the addition that the directory containing a rollback journal
/// is synced after that journal is unlinked to commit a transaction in DELETE mode.
/// </summary>
Extra = 3
}
/// <summary>
/// Storage mode used by temporary database files.
/// </summary>
public enum TempStoreMode
{
/// <summary>
/// The compile-time C preprocessor macro SQLITE_TEMP_STORE
/// is used to determine where temporary tables and indices are stored.
/// </summary>
Default = 0,
/// <summary>
/// Temporary tables and indices are stored in a file.
/// </summary>
File = 1,
/// <summary>
/// Temporary tables and indices are kept in as if they were pure in-memory databases memory.
/// </summary>
Memory = 2
}
}

View File

@@ -9,8 +9,10 @@ namespace Emby.Server.Implementations.Data
{
public class ManagedConnection : IDisposable
{
private SQLiteDatabaseConnection? _db;
private readonly SemaphoreSlim _writeLock;
private SQLiteDatabaseConnection? _db;
private bool _disposed = false;
public ManagedConnection(SQLiteDatabaseConnection db, SemaphoreSlim writeLock)

View File

@@ -94,7 +94,7 @@ namespace Emby.Server.Implementations.Data
dateText,
_datetimeFormats,
DateTimeFormatInfo.InvariantInfo,
DateTimeStyles.None).ToUniversalTime();
DateTimeStyles.AdjustToUniversal);
}
public static bool TryReadDateTime(this IReadOnlyList<ResultSetValue> reader, int index, out DateTime result)
@@ -108,9 +108,9 @@ namespace Emby.Server.Implementations.Data
var dateText = item.ToString();
if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.None, out var dateTimeResult))
if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AdjustToUniversal, out var dateTimeResult))
{
result = dateTimeResult.ToUniversalTime();
result = dateTimeResult;
return true;
}

View File

@@ -16,7 +16,6 @@ using Emby.Server.Implementations.Playlists;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
@@ -25,7 +24,6 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Persistence;
@@ -48,6 +46,11 @@ namespace Emby.Server.Implementations.Data
private const string FromText = " from TypedBaseItems A";
private const string ChaptersTableName = "Chapters2";
private const string SaveItemCommandText =
@"replace into TypedBaseItems
(guid,type,data,Path,StartDate,EndDate,ChannelId,IsMovie,IsSeries,EpisodeTitle,IsRepeat,CommunityRating,CustomRating,IndexNumber,IsLocked,Name,OfficialRating,MediaType,Overview,ParentIndexNumber,PremiereDate,ProductionYear,ParentId,Genres,InheritedParentalRatingValue,SortName,ForcedSortName,RunTimeTicks,Size,DateCreated,DateModified,PreferredMetadataLanguage,PreferredMetadataCountryCode,Width,Height,DateLastRefreshed,DateLastSaved,IsInMixedFolder,LockedFields,Studios,Audio,ExternalServiceId,Tags,IsFolder,UnratedType,TopParentId,TrailerTypes,CriticRating,CleanName,PresentationUniqueKey,OriginalTitle,PrimaryVersionId,DateLastMediaAdded,Album,IsVirtualItem,SeriesName,UserDataKey,SeasonName,SeasonId,SeriesId,ExternalSeriesId,Tagline,ProviderIds,Images,ProductionLocations,ExtraIds,TotalBitrate,ExtraType,Artists,AlbumArtists,ExternalId,SeriesPresentationUniqueKey,ShowId,OwnerId)
values (@guid,@type,@data,@Path,@StartDate,@EndDate,@ChannelId,@IsMovie,@IsSeries,@EpisodeTitle,@IsRepeat,@CommunityRating,@CustomRating,@IndexNumber,@IsLocked,@Name,@OfficialRating,@MediaType,@Overview,@ParentIndexNumber,@PremiereDate,@ProductionYear,@ParentId,@Genres,@InheritedParentalRatingValue,@SortName,@ForcedSortName,@RunTimeTicks,@Size,@DateCreated,@DateModified,@PreferredMetadataLanguage,@PreferredMetadataCountryCode,@Width,@Height,@DateLastRefreshed,@DateLastSaved,@IsInMixedFolder,@LockedFields,@Studios,@Audio,@ExternalServiceId,@Tags,@IsFolder,@UnratedType,@TopParentId,@TrailerTypes,@CriticRating,@CleanName,@PresentationUniqueKey,@OriginalTitle,@PrimaryVersionId,@DateLastMediaAdded,@Album,@IsVirtualItem,@SeriesName,@UserDataKey,@SeasonName,@SeasonId,@SeriesId,@ExternalSeriesId,@Tagline,@ProviderIds,@Images,@ProductionLocations,@ExtraIds,@TotalBitrate,@ExtraType,@Artists,@AlbumArtists,@ExternalId,@SeriesPresentationUniqueKey,@ShowId,@OwnerId)";
private readonly IServerConfigurationManager _config;
private readonly IServerApplicationHost _appHost;
private readonly ILocalizationManager _localization;
@@ -57,6 +60,231 @@ namespace Emby.Server.Implementations.Data
private readonly TypeMapper _typeMapper;
private readonly JsonSerializerOptions _jsonOptions;
private readonly ItemFields[] _allItemFields = Enum.GetValues<ItemFields>();
private static readonly string[] _retriveItemColumns =
{
"type",
"data",
"StartDate",
"EndDate",
"ChannelId",
"IsMovie",
"IsSeries",
"EpisodeTitle",
"IsRepeat",
"CommunityRating",
"CustomRating",
"IndexNumber",
"IsLocked",
"PreferredMetadataLanguage",
"PreferredMetadataCountryCode",
"Width",
"Height",
"DateLastRefreshed",
"Name",
"Path",
"PremiereDate",
"Overview",
"ParentIndexNumber",
"ProductionYear",
"OfficialRating",
"ForcedSortName",
"RunTimeTicks",
"Size",
"DateCreated",
"DateModified",
"guid",
"Genres",
"ParentId",
"Audio",
"ExternalServiceId",
"IsInMixedFolder",
"DateLastSaved",
"LockedFields",
"Studios",
"Tags",
"TrailerTypes",
"OriginalTitle",
"PrimaryVersionId",
"DateLastMediaAdded",
"Album",
"CriticRating",
"IsVirtualItem",
"SeriesName",
"SeasonName",
"SeasonId",
"SeriesId",
"PresentationUniqueKey",
"InheritedParentalRatingValue",
"ExternalSeriesId",
"Tagline",
"ProviderIds",
"Images",
"ProductionLocations",
"ExtraIds",
"TotalBitrate",
"ExtraType",
"Artists",
"AlbumArtists",
"ExternalId",
"SeriesPresentationUniqueKey",
"ShowId",
"OwnerId"
};
private static readonly string _retriveItemColumnsSelectQuery = $"select {string.Join(',', _retriveItemColumns)} from TypedBaseItems where guid = @guid";
private static readonly string[] _mediaStreamSaveColumns =
{
"ItemId",
"StreamIndex",
"StreamType",
"Codec",
"Language",
"ChannelLayout",
"Profile",
"AspectRatio",
"Path",
"IsInterlaced",
"BitRate",
"Channels",
"SampleRate",
"IsDefault",
"IsForced",
"IsExternal",
"Height",
"Width",
"AverageFrameRate",
"RealFrameRate",
"Level",
"PixelFormat",
"BitDepth",
"IsAnamorphic",
"RefFrames",
"CodecTag",
"Comment",
"NalLengthSize",
"IsAvc",
"Title",
"TimeBase",
"CodecTimeBase",
"ColorPrimaries",
"ColorSpace",
"ColorTransfer"
};
private static readonly string _mediaStreamSaveColumnsInsertQuery =
$"insert into mediastreams ({string.Join(',', _mediaStreamSaveColumns)}) values ";
private static readonly string _mediaStreamSaveColumnsSelectQuery =
$"select {string.Join(',', _mediaStreamSaveColumns)} from mediastreams where ItemId=@ItemId";
private static readonly string[] _mediaAttachmentSaveColumns =
{
"ItemId",
"AttachmentIndex",
"Codec",
"CodecTag",
"Comment",
"Filename",
"MIMEType"
};
private static readonly string _mediaAttachmentSaveColumnsSelectQuery =
$"select {string.Join(',', _mediaAttachmentSaveColumns)} from mediaattachments where ItemId=@ItemId";
private static readonly string _mediaAttachmentInsertPrefix;
private static readonly HashSet<string> _programTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Program",
"TvChannel",
"LiveTvProgram",
"LiveTvTvChannel"
};
private static readonly HashSet<string> _programExcludeParentTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Series",
"Season",
"MusicAlbum",
"MusicArtist",
"PhotoAlbum"
};
private static readonly HashSet<string> _serviceTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"TvChannel",
"LiveTvTvChannel"
};
private static readonly HashSet<string> _startDateTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Program",
"LiveTvProgram"
};
private static readonly HashSet<string> _seriesTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Book",
"AudioBook",
"Episode",
"Season"
};
private static readonly HashSet<string> _artistExcludeParentTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Series",
"Season",
"PhotoAlbum"
};
private static readonly HashSet<string> _artistsTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Audio",
"MusicAlbum",
"MusicVideo",
"AudioBook",
"AudioPodcast"
};
private static readonly Type[] _knownTypes =
{
typeof(LiveTvProgram),
typeof(LiveTvChannel),
typeof(Series),
typeof(Audio),
typeof(MusicAlbum),
typeof(MusicArtist),
typeof(MusicGenre),
typeof(MusicVideo),
typeof(Movie),
typeof(Playlist),
typeof(AudioBook),
typeof(Trailer),
typeof(BoxSet),
typeof(Episode),
typeof(Season),
typeof(Series),
typeof(Book),
typeof(CollectionFolder),
typeof(Folder),
typeof(Genre),
typeof(Person),
typeof(Photo),
typeof(PhotoAlbum),
typeof(Studio),
typeof(UserRootFolder),
typeof(UserView),
typeof(Video),
typeof(Year),
typeof(Channel),
typeof(AggregateFolder)
};
private readonly Dictionary<string, string> _types = GetTypeMapDictionary();
static SqliteItemRepository()
{
var queryPrefixText = new StringBuilder();
@@ -117,6 +345,8 @@ namespace Emby.Server.Implementations.Data
/// <summary>
/// Opens the connection to the database.
/// </summary>
/// <param name="userDataRepo">The user data repository.</param>
/// <param name="userManager">The user manager.</param>
public void Initialize(SqliteUserDataRepository userDataRepo, IUserManager userManager)
{
const string CreateMediaStreamsTableCommand
@@ -156,7 +386,7 @@ namespace Emby.Server.Implementations.Data
"drop index if exists idx_TypedBaseItems",
"drop index if exists idx_mediastreams",
"drop index if exists idx_mediastreams1",
"drop index if exists idx_"+ChaptersTableName,
"drop index if exists idx_" + ChaptersTableName,
"drop index if exists idx_UserDataKeys1",
"drop index if exists idx_UserDataKeys2",
"drop index if exists idx_TypeTopParentId3",
@@ -342,151 +572,12 @@ namespace Emby.Server.Implementations.Data
userDataRepo.Initialize(userManager, WriteLock, WriteConnection);
}
private static readonly string[] _retriveItemColumns =
{
"type",
"data",
"StartDate",
"EndDate",
"ChannelId",
"IsMovie",
"IsSeries",
"EpisodeTitle",
"IsRepeat",
"CommunityRating",
"CustomRating",
"IndexNumber",
"IsLocked",
"PreferredMetadataLanguage",
"PreferredMetadataCountryCode",
"Width",
"Height",
"DateLastRefreshed",
"Name",
"Path",
"PremiereDate",
"Overview",
"ParentIndexNumber",
"ProductionYear",
"OfficialRating",
"ForcedSortName",
"RunTimeTicks",
"Size",
"DateCreated",
"DateModified",
"guid",
"Genres",
"ParentId",
"Audio",
"ExternalServiceId",
"IsInMixedFolder",
"DateLastSaved",
"LockedFields",
"Studios",
"Tags",
"TrailerTypes",
"OriginalTitle",
"PrimaryVersionId",
"DateLastMediaAdded",
"Album",
"CriticRating",
"IsVirtualItem",
"SeriesName",
"SeasonName",
"SeasonId",
"SeriesId",
"PresentationUniqueKey",
"InheritedParentalRatingValue",
"ExternalSeriesId",
"Tagline",
"ProviderIds",
"Images",
"ProductionLocations",
"ExtraIds",
"TotalBitrate",
"ExtraType",
"Artists",
"AlbumArtists",
"ExternalId",
"SeriesPresentationUniqueKey",
"ShowId",
"OwnerId"
};
private static readonly string _retriveItemColumnsSelectQuery = $"select {string.Join(',', _retriveItemColumns)} from TypedBaseItems where guid = @guid";
private static readonly string[] _mediaStreamSaveColumns =
{
"ItemId",
"StreamIndex",
"StreamType",
"Codec",
"Language",
"ChannelLayout",
"Profile",
"AspectRatio",
"Path",
"IsInterlaced",
"BitRate",
"Channels",
"SampleRate",
"IsDefault",
"IsForced",
"IsExternal",
"Height",
"Width",
"AverageFrameRate",
"RealFrameRate",
"Level",
"PixelFormat",
"BitDepth",
"IsAnamorphic",
"RefFrames",
"CodecTag",
"Comment",
"NalLengthSize",
"IsAvc",
"Title",
"TimeBase",
"CodecTimeBase",
"ColorPrimaries",
"ColorSpace",
"ColorTransfer"
};
private static readonly string _mediaStreamSaveColumnsInsertQuery =
$"insert into mediastreams ({string.Join(',', _mediaStreamSaveColumns)}) values ";
private static readonly string _mediaStreamSaveColumnsSelectQuery =
$"select {string.Join(',', _mediaStreamSaveColumns)} from mediastreams where ItemId=@ItemId";
private static readonly string[] _mediaAttachmentSaveColumns =
{
"ItemId",
"AttachmentIndex",
"Codec",
"CodecTag",
"Comment",
"Filename",
"MIMEType"
};
private static readonly string _mediaAttachmentSaveColumnsSelectQuery =
$"select {string.Join(',', _mediaAttachmentSaveColumns)} from mediaattachments where ItemId=@ItemId";
private static readonly string _mediaAttachmentInsertPrefix;
private const string SaveItemCommandText =
@"replace into TypedBaseItems
(guid,type,data,Path,StartDate,EndDate,ChannelId,IsMovie,IsSeries,EpisodeTitle,IsRepeat,CommunityRating,CustomRating,IndexNumber,IsLocked,Name,OfficialRating,MediaType,Overview,ParentIndexNumber,PremiereDate,ProductionYear,ParentId,Genres,InheritedParentalRatingValue,SortName,ForcedSortName,RunTimeTicks,Size,DateCreated,DateModified,PreferredMetadataLanguage,PreferredMetadataCountryCode,Width,Height,DateLastRefreshed,DateLastSaved,IsInMixedFolder,LockedFields,Studios,Audio,ExternalServiceId,Tags,IsFolder,UnratedType,TopParentId,TrailerTypes,CriticRating,CleanName,PresentationUniqueKey,OriginalTitle,PrimaryVersionId,DateLastMediaAdded,Album,IsVirtualItem,SeriesName,UserDataKey,SeasonName,SeasonId,SeriesId,ExternalSeriesId,Tagline,ProviderIds,Images,ProductionLocations,ExtraIds,TotalBitrate,ExtraType,Artists,AlbumArtists,ExternalId,SeriesPresentationUniqueKey,ShowId,OwnerId)
values (@guid,@type,@data,@Path,@StartDate,@EndDate,@ChannelId,@IsMovie,@IsSeries,@EpisodeTitle,@IsRepeat,@CommunityRating,@CustomRating,@IndexNumber,@IsLocked,@Name,@OfficialRating,@MediaType,@Overview,@ParentIndexNumber,@PremiereDate,@ProductionYear,@ParentId,@Genres,@InheritedParentalRatingValue,@SortName,@ForcedSortName,@RunTimeTicks,@Size,@DateCreated,@DateModified,@PreferredMetadataLanguage,@PreferredMetadataCountryCode,@Width,@Height,@DateLastRefreshed,@DateLastSaved,@IsInMixedFolder,@LockedFields,@Studios,@Audio,@ExternalServiceId,@Tags,@IsFolder,@UnratedType,@TopParentId,@TrailerTypes,@CriticRating,@CleanName,@PresentationUniqueKey,@OriginalTitle,@PrimaryVersionId,@DateLastMediaAdded,@Album,@IsVirtualItem,@SeriesName,@UserDataKey,@SeasonName,@SeasonId,@SeriesId,@ExternalSeriesId,@Tagline,@ProviderIds,@Images,@ProductionLocations,@ExtraIds,@TotalBitrate,@ExtraType,@Artists,@AlbumArtists,@ExternalId,@SeriesPresentationUniqueKey,@ShowId,@OwnerId)";
/// <summary>
/// Save a standard item in the repo.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <exception cref="ArgumentNullException">item</exception>
/// <exception cref="ArgumentNullException"><paramref name="item"/> is <c>null</c>.</exception>
public void SaveItem(BaseItem item, CancellationToken cancellationToken)
{
if (item == null)
@@ -511,7 +602,7 @@ namespace Emby.Server.Implementations.Data
connection.RunInTransaction(
db =>
{
using (var saveImagesStatement = base.PrepareStatement(db, "Update TypedBaseItems set Images=@Images where guid=@Id"))
using (var saveImagesStatement = PrepareStatement(db, "Update TypedBaseItems set Images=@Images where guid=@Id"))
{
saveImagesStatement.TryBind("@Id", item.Id.ToByteArray());
saveImagesStatement.TryBind("@Images", SerializeImages(item.ImageInfos));
@@ -528,9 +619,7 @@ namespace Emby.Server.Implementations.Data
/// <param name="items">The items.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <exception cref="ArgumentNullException">
/// items
/// or
/// cancellationToken
/// <paramref name="items"/> or <paramref name="cancellationToken"/> is <c>null</c>.
/// </exception>
public void SaveItems(IEnumerable<BaseItem> items, CancellationToken cancellationToken)
{
@@ -1152,7 +1241,7 @@ namespace Emby.Server.Implementations.Data
return null;
}
if (Enum.TryParse(imageType.ToString(), true, out ImageType type))
if (Enum.TryParse(imageType, true, out ImageType type))
{
image.Type = type;
}
@@ -1218,8 +1307,8 @@ namespace Emby.Server.Implementations.Data
/// </summary>
/// <param name="id">The id.</param>
/// <returns>BaseItem.</returns>
/// <exception cref="ArgumentNullException">id</exception>
/// <exception cref="ArgumentException"></exception>
/// <exception cref="ArgumentNullException"><paramref name="id"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentException"><paramr name="id"/> is <seealso cref="Guid.Empty"/>.</exception>
public BaseItem RetrieveItem(Guid id)
{
if (id == Guid.Empty)
@@ -1573,7 +1662,6 @@ namespace Emby.Server.Implementations.Data
if (reader.TryGetString(index++, out var audioString))
{
// TODO Span overload coming in the future https://github.com/dotnet/runtime/issues/1916
if (Enum.TryParse(audioString, true, out ProgramAudio audio))
{
item.Audio = audio;
@@ -1612,18 +1700,16 @@ namespace Emby.Server.Implementations.Data
{
if (reader.TryGetString(index++, out var lockedFields))
{
IEnumerable<MetadataField> GetLockedFields(string s)
List<MetadataField> fields = null;
foreach (var i in lockedFields.AsSpan().Split('|'))
{
foreach (var i in s.Split('|', StringSplitOptions.RemoveEmptyEntries))
if (Enum.TryParse(i, true, out MetadataField parsedValue))
{
if (Enum.TryParse(i, true, out MetadataField parsedValue))
{
yield return parsedValue;
}
(fields ??= new List<MetadataField>()).Add(parsedValue);
}
}
item.LockedFields = GetLockedFields(lockedFields).ToArray();
item.LockedFields = fields?.ToArray() ?? Array.Empty<MetadataField>();
}
}
@@ -1649,18 +1735,16 @@ namespace Emby.Server.Implementations.Data
{
if (reader.TryGetString(index, out var trailerTypes))
{
IEnumerable<TrailerType> GetTrailerTypes(string s)
List<TrailerType> types = null;
foreach (var i in trailerTypes.AsSpan().Split('|'))
{
foreach (var i in s.Split('|', StringSplitOptions.RemoveEmptyEntries))
if (Enum.TryParse(i, true, out TrailerType parsedValue))
{
if (Enum.TryParse(i, true, out TrailerType parsedValue))
{
yield return parsedValue;
}
(types ??= new List<TrailerType>()).Add(parsedValue);
}
}
trailer.TrailerTypes = GetTrailerTypes(trailerTypes).ToArray();
trailer.TrailerTypes = types?.ToArray() ?? Array.Empty<TrailerType>();
}
}
@@ -1902,12 +1986,7 @@ namespace Emby.Server.Implementations.Data
return result;
}
/// <summary>
/// Gets chapters for an item.
/// </summary>
/// <param name="item">The item.</param>
/// <returns>IEnumerable{ChapterInfo}.</returns>
/// <exception cref="ArgumentNullException">id</exception>
/// <inheritdoc />
public List<ChapterInfo> GetChapters(BaseItem item)
{
CheckDisposed();
@@ -1930,13 +2009,7 @@ namespace Emby.Server.Implementations.Data
}
}
/// <summary>
/// Gets a single chapter for an item.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="index">The index.</param>
/// <returns>ChapterInfo.</returns>
/// <exception cref="ArgumentNullException">id</exception>
/// <inheritdoc />
public ChapterInfo GetChapter(BaseItem item, int index)
{
CheckDisposed();
@@ -2004,6 +2077,8 @@ namespace Emby.Server.Implementations.Data
/// <summary>
/// Saves the chapters.
/// </summary>
/// <param name="id">The item id.</param>
/// <param name="chapters">The chapters.</param>
public void SaveChapters(Guid id, IReadOnlyList<ChapterInfo> chapters)
{
CheckDisposed();
@@ -2048,7 +2123,7 @@ namespace Emby.Server.Implementations.Data
for (var i = startIndex; i < endIndex; i++)
{
insertText.AppendFormat("(@ItemId, @ChapterIndex{0}, @StartPositionTicks{0}, @Name{0}, @ImagePath{0}, @ImageDateModified{0}),", i.ToString(CultureInfo.InvariantCulture));
insertText.AppendFormat(CultureInfo.InvariantCulture, "(@ItemId, @ChapterIndex{0}, @StartPositionTicks{0}, @Name{0}, @ImagePath{0}, @ImageDateModified{0}),", i.ToString(CultureInfo.InvariantCulture));
}
insertText.Length -= 1; // Remove last ,
@@ -2103,8 +2178,6 @@ namespace Emby.Server.Implementations.Data
|| query.IsLiked.HasValue;
}
private readonly ItemFields[] _allFields = Enum.GetValues<ItemFields>();
private bool HasField(InternalItemsQuery query, ItemFields name)
{
switch (name)
@@ -2137,23 +2210,6 @@ namespace Emby.Server.Implementations.Data
}
}
private static readonly HashSet<string> _programExcludeParentTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Series",
"Season",
"MusicAlbum",
"MusicArtist",
"PhotoAlbum"
};
private static readonly HashSet<string> _programTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Program",
"TvChannel",
"LiveTvProgram",
"LiveTvTvChannel"
};
private bool HasProgramAttributes(InternalItemsQuery query)
{
if (_programExcludeParentTypes.Contains(query.ParentType))
@@ -2169,12 +2225,6 @@ namespace Emby.Server.Implementations.Data
return query.IncludeItemTypes.Any(x => _programTypes.Contains(x));
}
private static readonly HashSet<string> _serviceTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"TvChannel",
"LiveTvTvChannel"
};
private bool HasServiceName(InternalItemsQuery query)
{
if (_programExcludeParentTypes.Contains(query.ParentType))
@@ -2190,12 +2240,6 @@ namespace Emby.Server.Implementations.Data
return query.IncludeItemTypes.Any(x => _serviceTypes.Contains(x));
}
private static readonly HashSet<string> _startDateTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Program",
"LiveTvProgram"
};
private bool HasStartDate(InternalItemsQuery query)
{
if (_programExcludeParentTypes.Contains(query.ParentType))
@@ -2231,22 +2275,6 @@ namespace Emby.Server.Implementations.Data
return query.IncludeItemTypes.Contains("Trailer", StringComparer.OrdinalIgnoreCase);
}
private static readonly HashSet<string> _artistExcludeParentTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Series",
"Season",
"PhotoAlbum"
};
private static readonly HashSet<string> _artistsTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Audio",
"MusicAlbum",
"MusicVideo",
"AudioBook",
"AudioPodcast"
};
private bool HasArtistFields(InternalItemsQuery query)
{
if (_artistExcludeParentTypes.Contains(query.ParentType))
@@ -2262,14 +2290,6 @@ namespace Emby.Server.Implementations.Data
return query.IncludeItemTypes.Any(x => _artistsTypes.Contains(x));
}
private static readonly HashSet<string> _seriesTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Book",
"AudioBook",
"Episode",
"Season"
};
private bool HasSeriesFields(InternalItemsQuery query)
{
if (string.Equals(query.ParentType, "PhotoAlbum", StringComparison.OrdinalIgnoreCase))
@@ -2287,7 +2307,7 @@ namespace Emby.Server.Implementations.Data
private void SetFinalColumnsToSelect(InternalItemsQuery query, List<string> columns)
{
foreach (var field in _allFields)
foreach (var field in _allItemFields)
{
if (!HasField(query, field))
{
@@ -4829,40 +4849,6 @@ namespace Emby.Server.Implementations.Data
return false;
}
private static readonly Type[] _knownTypes =
{
typeof(LiveTvProgram),
typeof(LiveTvChannel),
typeof(Series),
typeof(Audio),
typeof(MusicAlbum),
typeof(MusicArtist),
typeof(MusicGenre),
typeof(MusicVideo),
typeof(Movie),
typeof(Playlist),
typeof(AudioBook),
typeof(Trailer),
typeof(BoxSet),
typeof(Episode),
typeof(Season),
typeof(Series),
typeof(Book),
typeof(CollectionFolder),
typeof(Folder),
typeof(Genre),
typeof(Person),
typeof(Photo),
typeof(PhotoAlbum),
typeof(Studio),
typeof(UserRootFolder),
typeof(UserView),
typeof(Video),
typeof(Year),
typeof(Channel),
typeof(AggregateFolder)
};
public void UpdateInheritedValues()
{
string sql = string.Join(
@@ -4904,9 +4890,6 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
return dict;
}
// Not crazy about having this all the way down here, but at least it's in one place
private readonly Dictionary<string, string> _types = GetTypeMapDictionary();
private string MapIncludeItemTypes(string value)
{
if (_types.TryGetValue(value, out string result))

View File

@@ -32,6 +32,9 @@ namespace Emby.Server.Implementations.Data
/// <summary>
/// Opens the connection to the database.
/// </summary>
/// <param name="userManager">The user manager.</param>
/// <param name="dbLock">The lock to use for database IO.</param>
/// <param name="dbConnection">The connection to use for database IO.</param>
public void Initialize(IUserManager userManager, SemaphoreSlim dbLock, SQLiteDatabaseConnection dbConnection)
{
WriteLock.Dispose();
@@ -49,8 +52,8 @@ namespace Emby.Server.Implementations.Data
connection.RunInTransaction(
db =>
{
db.ExecuteAll(string.Join(';', new[] {
db.ExecuteAll(string.Join(';', new[]
{
"create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)",
"drop index if exists idx_userdata",
@@ -129,19 +132,17 @@ namespace Emby.Server.Implementations.Data
return list;
}
/// <summary>
/// Saves the user data.
/// </summary>
public void SaveUserData(long internalUserId, string key, UserItemData userData, CancellationToken cancellationToken)
/// <inheritdoc />
public void SaveUserData(long userId, string key, UserItemData userData, CancellationToken cancellationToken)
{
if (userData == null)
{
throw new ArgumentNullException(nameof(userData));
}
if (internalUserId <= 0)
if (userId <= 0)
{
throw new ArgumentNullException(nameof(internalUserId));
throw new ArgumentNullException(nameof(userId));
}
if (string.IsNullOrEmpty(key))
@@ -149,22 +150,23 @@ namespace Emby.Server.Implementations.Data
throw new ArgumentNullException(nameof(key));
}
PersistUserData(internalUserId, key, userData, cancellationToken);
PersistUserData(userId, key, userData, cancellationToken);
}
public void SaveAllUserData(long internalUserId, UserItemData[] userData, CancellationToken cancellationToken)
/// <inheritdoc />
public void SaveAllUserData(long userId, UserItemData[] userData, CancellationToken cancellationToken)
{
if (userData == null)
{
throw new ArgumentNullException(nameof(userData));
}
if (internalUserId <= 0)
if (userId <= 0)
{
throw new ArgumentNullException(nameof(internalUserId));
throw new ArgumentNullException(nameof(userId));
}
PersistAllUserData(internalUserId, userData, cancellationToken);
PersistAllUserData(userId, userData, cancellationToken);
}
/// <summary>
@@ -263,19 +265,19 @@ namespace Emby.Server.Implementations.Data
/// <summary>
/// Gets the user data.
/// </summary>
/// <param name="internalUserId">The user id.</param>
/// <param name="userId">The user id.</param>
/// <param name="key">The key.</param>
/// <returns>Task{UserItemData}.</returns>
/// <exception cref="ArgumentNullException">
/// userId
/// or
/// key
/// key.
/// </exception>
public UserItemData GetUserData(long internalUserId, string key)
public UserItemData GetUserData(long userId, string key)
{
if (internalUserId <= 0)
if (userId <= 0)
{
throw new ArgumentNullException(nameof(internalUserId));
throw new ArgumentNullException(nameof(userId));
}
if (string.IsNullOrEmpty(key))
@@ -287,7 +289,7 @@ namespace Emby.Server.Implementations.Data
{
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
{
statement.TryBind("@UserId", internalUserId);
statement.TryBind("@UserId", userId);
statement.TryBind("@Key", key);
foreach (var row in statement.ExecuteQuery())
@@ -300,7 +302,7 @@ namespace Emby.Server.Implementations.Data
}
}
public UserItemData GetUserData(long internalUserId, List<string> keys)
public UserItemData GetUserData(long userId, List<string> keys)
{
if (keys == null)
{
@@ -312,19 +314,19 @@ namespace Emby.Server.Implementations.Data
return null;
}
return GetUserData(internalUserId, keys[0]);
return GetUserData(userId, keys[0]);
}
/// <summary>
/// Return all user-data associated with the given user.
/// </summary>
/// <param name="internalUserId">The internal user id.</param>
/// <param name="userId">The internal user id.</param>
/// <returns>The list of user item data.</returns>
public List<UserItemData> GetAllUserData(long internalUserId)
public List<UserItemData> GetAllUserData(long userId)
{
if (internalUserId <= 0)
if (userId <= 0)
{
throw new ArgumentNullException(nameof(internalUserId));
throw new ArgumentNullException(nameof(userId));
}
var list = new List<UserItemData>();
@@ -333,7 +335,7 @@ namespace Emby.Server.Implementations.Data
{
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where userId=@UserId"))
{
statement.TryBind("@UserId", internalUserId);
statement.TryBind("@UserId", userId);
foreach (var row in statement.ExecuteQuery())
{

View File

@@ -0,0 +1,30 @@
namespace Emby.Server.Implementations.Data;
/// <summary>
/// The disk synchronization mode, controls how aggressively SQLite will write data
/// all the way out to physical storage.
/// </summary>
public enum SynchronousMode
{
/// <summary>
/// SQLite continues without syncing as soon as it has handed data off to the operating system.
/// </summary>
Off = 0,
/// <summary>
/// SQLite database engine will still sync at the most critical moments.
/// </summary>
Normal = 1,
/// <summary>
/// SQLite database engine will use the xSync method of the VFS
/// to ensure that all content is safely written to the disk surface prior to continuing.
/// </summary>
Full = 2,
/// <summary>
/// EXTRA synchronous is like FULL with the addition that the directory containing a rollback journal
/// is synced after that journal is unlinked to commit a transaction in DELETE mode.
/// </summary>
Extra = 3
}

View File

@@ -0,0 +1,23 @@
namespace Emby.Server.Implementations.Data;
/// <summary>
/// Storage mode used by temporary database files.
/// </summary>
public enum TempStoreMode
{
/// <summary>
/// The compile-time C preprocessor macro SQLITE_TEMP_STORE
/// is used to determine where temporary tables and indices are stored.
/// </summary>
Default = 0,
/// <summary>
/// Temporary tables and indices are stored in a file.
/// </summary>
File = 1,
/// <summary>
/// Temporary tables and indices are kept in as if they were pure in-memory databases memory.
/// </summary>
Memory = 2
}

View File

@@ -15,9 +15,18 @@ namespace Emby.Server.Implementations.Devices
{
private readonly IApplicationPaths _appPaths;
private readonly ILogger<DeviceId> _logger;
private readonly object _syncLock = new object();
private string _id;
public DeviceId(IApplicationPaths appPaths, ILoggerFactory loggerFactory)
{
_appPaths = appPaths;
_logger = loggerFactory.CreateLogger<DeviceId>();
}
public string Value => _id ?? (_id = GetDeviceId());
private string CachePath => Path.Combine(_appPaths.DataPath, "device.txt");
private string GetCachedId()
@@ -86,15 +95,5 @@ namespace Emby.Server.Implementations.Devices
return id;
}
private string _id;
public DeviceId(IApplicationPaths appPaths, ILoggerFactory loggerFactory)
{
_appPaths = appPaths;
_logger = loggerFactory.CreateLogger<DeviceId>();
}
public string Value => _id ?? (_id = GetDeviceId());
}
}

View File

@@ -1,146 +0,0 @@
#nullable disable
#pragma warning disable CS1591
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Security;
using MediaBrowser.Model.Devices;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Session;
namespace Emby.Server.Implementations.Devices
{
public class DeviceManager : IDeviceManager
{
private readonly IUserManager _userManager;
private readonly IAuthenticationRepository _authRepo;
private readonly ConcurrentDictionary<string, ClientCapabilities> _capabilitiesMap = new ();
public DeviceManager(IAuthenticationRepository authRepo, IUserManager userManager)
{
_userManager = userManager;
_authRepo = authRepo;
}
public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
public void SaveCapabilities(string deviceId, ClientCapabilities capabilities)
{
_capabilitiesMap[deviceId] = capabilities;
}
public void UpdateDeviceOptions(string deviceId, DeviceOptions options)
{
_authRepo.UpdateDeviceOptions(deviceId, options);
DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs<Tuple<string, DeviceOptions>>(new Tuple<string, DeviceOptions>(deviceId, options)));
}
public DeviceOptions GetDeviceOptions(string deviceId)
{
return _authRepo.GetDeviceOptions(deviceId);
}
public ClientCapabilities GetCapabilities(string id)
{
return _capabilitiesMap.TryGetValue(id, out ClientCapabilities result)
? result
: new ClientCapabilities();
}
public DeviceInfo GetDevice(string id)
{
var session = _authRepo.Get(new AuthenticationInfoQuery
{
DeviceId = id
}).Items.FirstOrDefault();
var device = session == null ? null : ToDeviceInfo(session);
return device;
}
public QueryResult<DeviceInfo> GetDevices(DeviceQuery query)
{
IEnumerable<AuthenticationInfo> sessions = _authRepo.Get(new AuthenticationInfoQuery
{
// UserId = query.UserId
HasUser = true
}).Items;
// TODO: DeviceQuery doesn't seem to be used from client. Not even Swagger.
if (query.SupportsSync.HasValue)
{
var val = query.SupportsSync.Value;
sessions = sessions.Where(i => GetCapabilities(i.DeviceId).SupportsSync == val);
}
if (!query.UserId.Equals(Guid.Empty))
{
var user = _userManager.GetUserById(query.UserId);
sessions = sessions.Where(i => CanAccessDevice(user, i.DeviceId));
}
var array = sessions.Select(ToDeviceInfo).ToArray();
return new QueryResult<DeviceInfo>(array);
}
private DeviceInfo ToDeviceInfo(AuthenticationInfo authInfo)
{
var caps = GetCapabilities(authInfo.DeviceId);
return new DeviceInfo
{
AppName = authInfo.AppName,
AppVersion = authInfo.AppVersion,
Id = authInfo.DeviceId,
LastUserId = authInfo.UserId,
LastUserName = authInfo.UserName,
Name = authInfo.DeviceName,
DateLastActivity = authInfo.DateLastActivity,
IconUrl = caps?.IconUrl
};
}
public bool CanAccessDevice(User user, string deviceId)
{
if (user == null)
{
throw new ArgumentException("user not found");
}
if (string.IsNullOrEmpty(deviceId))
{
throw new ArgumentNullException(nameof(deviceId));
}
if (user.HasPermission(PermissionKind.EnableAllDevices) || user.HasPermission(PermissionKind.IsAdministrator))
{
return true;
}
if (!user.GetPreference(PreferenceKind.EnabledDevices).Contains(deviceId, StringComparer.OrdinalIgnoreCase))
{
var capabilities = GetCapabilities(deviceId);
if (capabilities != null && capabilities.SupportsPersistentIdentifier)
{
return false;
}
}
return true;
}
}
}

View File

@@ -51,8 +51,6 @@ namespace Emby.Server.Implementations.Dto
private readonly IMediaSourceManager _mediaSourceManager;
private readonly Lazy<ILiveTvManager> _livetvManagerFactory;
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
public DtoService(
ILogger<DtoService> logger,
ILibraryManager libraryManager,
@@ -75,6 +73,8 @@ namespace Emby.Server.Implementations.Dto
_livetvManagerFactory = livetvManagerFactory;
}
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
/// <inheritdoc />
public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null)
{
@@ -507,7 +507,6 @@ namespace Emby.Server.Implementations.Dto
/// </summary>
/// <param name="dto">The dto.</param>
/// <param name="item">The item.</param>
/// <returns>Task.</returns>
private void AttachPeople(BaseItemDto dto, BaseItem item)
{
// Ordering by person type to ensure actors and artists are at the front.
@@ -616,7 +615,6 @@ namespace Emby.Server.Implementations.Dto
/// </summary>
/// <param name="dto">The dto.</param>
/// <param name="item">The item.</param>
/// <returns>Task.</returns>
private void AttachStudios(BaseItemDto dto, BaseItem item)
{
dto.Studios = item.Studios
@@ -1313,9 +1311,12 @@ namespace Emby.Server.Implementations.Dto
var imageTags = dto.ImageTags;
while (((!(imageTags != null && imageTags.ContainsKey(ImageType.Logo)) && logoLimit > 0) || (!(imageTags != null && imageTags.ContainsKey(ImageType.Art)) && artLimit > 0) || (!(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && thumbLimit > 0) || parent is Series) &&
(parent ??= (isFirst ? GetImageDisplayParent(item, item) ?? owner : parent)) != null)
while ((!(imageTags != null && imageTags.ContainsKey(ImageType.Logo)) && logoLimit > 0)
|| (!(imageTags != null && imageTags.ContainsKey(ImageType.Art)) && artLimit > 0)
|| (!(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && thumbLimit > 0)
|| parent is Series)
{
parent ??= isFirst ? GetImageDisplayParent(item, item) ?? owner : parent;
if (parent == null)
{
break;
@@ -1395,7 +1396,6 @@ namespace Emby.Server.Implementations.Dto
/// </summary>
/// <param name="dto">The dto.</param>
/// <param name="item">The item.</param>
/// <returns>Task.</returns>
public void AttachPrimaryImageAspectRatio(IItemDto dto, BaseItem item)
{
dto.PrimaryImageAspectRatio = GetPrimaryImageAspectRatio(item);

View File

@@ -23,18 +23,18 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="DiscUtils.Udf" Version="0.16.4" />
<PackageReference Include="DiscUtils.Udf" Version="0.16.13" />
<PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.10" />
<PackageReference Include="Mono.Nat" Version="3.0.1" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.0" />
<PackageReference Include="sharpcompress" Version="0.28.3" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.1" />
<PackageReference Include="sharpcompress" Version="0.30.0" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
<PackageReference Include="DotNet.Glob" Version="3.1.2" />
<PackageReference Include="DotNet.Glob" Version="3.1.3" />
</ItemGroup>
<ItemGroup>
@@ -42,16 +42,11 @@
</ItemGroup>
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
<NoWarn>AD0001</NoWarn>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release'">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<!-- Code Analyzers-->

View File

@@ -9,7 +9,6 @@ using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
using Jellyfin.Networking.Configuration;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;

View File

@@ -436,7 +436,7 @@ namespace Emby.Server.Implementations.EntryPoints
/// <summary>
/// Translates the physical item to user library.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="T">The type of item.</typeparam>
/// <param name="item">The item.</param>
/// <param name="user">The user.</param>
/// <param name="includeIfNotFound">if set to <c>true</c> [include if not found].</param>

View File

@@ -1,5 +1,6 @@
#pragma warning disable CS1591
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Net;
@@ -17,9 +18,9 @@ namespace Emby.Server.Implementations.HttpServer.Security
_authorizationContext = authorizationContext;
}
public AuthorizationInfo Authenticate(HttpRequest request)
public async Task<AuthorizationInfo> Authenticate(HttpRequest request)
{
var auth = _authorizationContext.GetAuthorizationInfo(request);
var auth = await _authorizationContext.GetAuthorizationInfo(request).ConfigureAwait(false);
if (!auth.HasToken)
{

View File

@@ -1,293 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Net;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security;
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
namespace Emby.Server.Implementations.HttpServer.Security
{
public class AuthorizationContext : IAuthorizationContext
{
private readonly IAuthenticationRepository _authRepo;
private readonly IUserManager _userManager;
public AuthorizationContext(IAuthenticationRepository authRepo, IUserManager userManager)
{
_authRepo = authRepo;
_userManager = userManager;
}
public AuthorizationInfo GetAuthorizationInfo(HttpContext requestContext)
{
if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached))
{
return (AuthorizationInfo)cached!; // Cache should never contain null
}
return GetAuthorization(requestContext);
}
public AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext)
{
var auth = GetAuthorizationDictionary(requestContext);
var authInfo = GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
return authInfo;
}
/// <summary>
/// Gets the authorization.
/// </summary>
/// <param name="httpReq">The HTTP req.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
private AuthorizationInfo GetAuthorization(HttpContext httpReq)
{
var auth = GetAuthorizationDictionary(httpReq);
var authInfo = GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query);
httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
return authInfo;
}
private AuthorizationInfo GetAuthorizationInfoFromDictionary(
in Dictionary<string, string>? auth,
in IHeaderDictionary headers,
in IQueryCollection queryString)
{
string? deviceId = null;
string? device = null;
string? client = null;
string? version = null;
string? token = null;
if (auth != null)
{
auth.TryGetValue("DeviceId", out deviceId);
auth.TryGetValue("Device", out device);
auth.TryGetValue("Client", out client);
auth.TryGetValue("Version", out version);
auth.TryGetValue("Token", out token);
}
#pragma warning disable CA1508 // string.IsNullOrEmpty(token) is always false.
if (string.IsNullOrEmpty(token))
{
token = headers["X-Emby-Token"];
}
if (string.IsNullOrEmpty(token))
{
token = headers["X-MediaBrowser-Token"];
}
if (string.IsNullOrEmpty(token))
{
token = queryString["ApiKey"];
}
// TODO deprecate this query parameter.
if (string.IsNullOrEmpty(token))
{
token = queryString["api_key"];
}
var authInfo = new AuthorizationInfo
{
Client = client,
Device = device,
DeviceId = deviceId,
Version = version,
Token = token,
IsAuthenticated = false,
HasToken = false
};
if (string.IsNullOrWhiteSpace(token))
{
// Request doesn't contain a token.
return authInfo;
}
#pragma warning restore CA1508
authInfo.HasToken = true;
var result = _authRepo.Get(new AuthenticationInfoQuery
{
AccessToken = token
});
if (result.Items.Count > 0)
{
authInfo.IsAuthenticated = true;
}
var originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
if (originalAuthenticationInfo != null)
{
var updateToken = false;
// TODO: Remove these checks for IsNullOrWhiteSpace
if (string.IsNullOrWhiteSpace(authInfo.Client))
{
authInfo.Client = originalAuthenticationInfo.AppName;
}
if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
{
authInfo.DeviceId = originalAuthenticationInfo.DeviceId;
}
// Temporary. TODO - allow clients to specify that the token has been shared with a casting device
var allowTokenInfoUpdate = authInfo.Client == null || !authInfo.Client.Contains("chromecast", StringComparison.OrdinalIgnoreCase);
if (string.IsNullOrWhiteSpace(authInfo.Device))
{
authInfo.Device = originalAuthenticationInfo.DeviceName;
}
else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
{
if (allowTokenInfoUpdate)
{
updateToken = true;
originalAuthenticationInfo.DeviceName = authInfo.Device;
}
}
if (string.IsNullOrWhiteSpace(authInfo.Version))
{
authInfo.Version = originalAuthenticationInfo.AppVersion;
}
else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
{
if (allowTokenInfoUpdate)
{
updateToken = true;
originalAuthenticationInfo.AppVersion = authInfo.Version;
}
}
if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3)
{
originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
updateToken = true;
}
if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty))
{
authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId);
if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase))
{
originalAuthenticationInfo.UserName = authInfo.User.Username;
updateToken = true;
}
authInfo.IsApiKey = false;
}
else
{
authInfo.IsApiKey = true;
}
if (updateToken)
{
_authRepo.Update(originalAuthenticationInfo);
}
}
return authInfo;
}
/// <summary>
/// Gets the auth.
/// </summary>
/// <param name="httpReq">The HTTP req.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
private Dictionary<string, string>? GetAuthorizationDictionary(HttpContext httpReq)
{
var auth = httpReq.Request.Headers["X-Emby-Authorization"];
if (string.IsNullOrEmpty(auth))
{
auth = httpReq.Request.Headers[HeaderNames.Authorization];
}
return GetAuthorization(auth.Count > 0 ? auth[0] : null);
}
/// <summary>
/// Gets the auth.
/// </summary>
/// <param name="httpReq">The HTTP req.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
private Dictionary<string, string>? GetAuthorizationDictionary(HttpRequest httpReq)
{
var auth = httpReq.Headers["X-Emby-Authorization"];
if (string.IsNullOrEmpty(auth))
{
auth = httpReq.Headers[HeaderNames.Authorization];
}
return GetAuthorization(auth.Count > 0 ? auth[0] : null);
}
/// <summary>
/// Gets the authorization.
/// </summary>
/// <param name="authorizationHeader">The authorization header.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
private Dictionary<string, string>? GetAuthorization(ReadOnlySpan<char> authorizationHeader)
{
if (authorizationHeader == null)
{
return null;
}
var firstSpace = authorizationHeader.IndexOf(' ');
// There should be at least two parts
if (firstSpace == -1)
{
return null;
}
var name = authorizationHeader[..firstSpace];
if (!name.Equals("MediaBrowser", StringComparison.OrdinalIgnoreCase)
&& !name.Equals("Emby", StringComparison.OrdinalIgnoreCase))
{
return null;
}
authorizationHeader = authorizationHeader[(firstSpace + 1)..];
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var item in authorizationHeader.Split(','))
{
var trimmedItem = item.Trim();
var firstEqualsSign = trimmedItem.IndexOf('=');
if (firstEqualsSign > 0)
{
var key = trimmedItem[..firstEqualsSign].ToString();
var value = NormalizeValue(trimmedItem[(firstEqualsSign + 1)..].Trim('"').ToString());
result[key] = value;
}
}
return result;
}
private static string NormalizeValue(string value)
{
return string.IsNullOrEmpty(value) ? value : WebUtility.HtmlEncode(value);
}
}
}

View File

@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Library;
@@ -23,27 +24,33 @@ namespace Emby.Server.Implementations.HttpServer.Security
_sessionManager = sessionManager;
}
public SessionInfo GetSession(HttpContext requestContext)
public async Task<SessionInfo> GetSession(HttpContext requestContext)
{
var authorization = _authContext.GetAuthorizationInfo(requestContext);
var authorization = await _authContext.GetAuthorizationInfo(requestContext).ConfigureAwait(false);
var user = authorization.User;
return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.GetNormalizedRemoteIp().ToString(), user);
return await _sessionManager.LogSessionActivity(
authorization.Client,
authorization.Version,
authorization.DeviceId,
authorization.Device,
requestContext.GetNormalizedRemoteIp().ToString(),
user).ConfigureAwait(false);
}
public SessionInfo GetSession(object requestContext)
public Task<SessionInfo> GetSession(object requestContext)
{
return GetSession((HttpContext)requestContext);
}
public User? GetUser(HttpContext requestContext)
public async Task<User?> GetUser(HttpContext requestContext)
{
var session = GetSession(requestContext);
var session = await GetSession(requestContext).ConfigureAwait(false);
return session == null || session.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(session.UserId);
}
public User? GetUser(object requestContext)
public Task<User?> GetUser(object requestContext)
{
return GetUser(((HttpRequest)requestContext).HttpContext);
}

View File

@@ -96,7 +96,7 @@ namespace Emby.Server.Implementations.HttpServer
/// <summary>
/// Sends a message asynchronously.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="T">The type of the message.</typeparam>
/// <param name="message">The message.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
@@ -150,8 +150,8 @@ namespace Emby.Server.Implementations.HttpServer
{
await ProcessInternal(pipe.Reader).ConfigureAwait(false);
}
} while (
(_socket.State == WebSocketState.Open || _socket.State == WebSocketState.Connecting)
}
while ((_socket.State == WebSocketState.Open || _socket.State == WebSocketState.Connecting)
&& receiveresult.MessageType != WebSocketMessageType.Close);
Closed?.Invoke(this, EventArgs.Empty);

View File

@@ -35,7 +35,7 @@ namespace Emby.Server.Implementations.HttpServer
/// <inheritdoc />
public async Task WebSocketRequestHandler(HttpContext context)
{
_ = _authService.Authenticate(context.Request);
_ = await _authService.Authenticate(context.Request).ConfigureAwait(false);
try
{
_logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);

View File

@@ -41,6 +41,25 @@ namespace Emby.Server.Implementations.IO
private bool _disposed = false;
/// <summary>
/// Initializes a new instance of the <see cref="LibraryMonitor" /> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="libraryManager">The library manager.</param>
/// <param name="configurationManager">The configuration manager.</param>
/// <param name="fileSystem">The filesystem.</param>
public LibraryMonitor(
ILogger<LibraryMonitor> logger,
ILibraryManager libraryManager,
IServerConfigurationManager configurationManager,
IFileSystem fileSystem)
{
_libraryManager = libraryManager;
_logger = logger;
_configurationManager = configurationManager;
_fileSystem = fileSystem;
}
/// <summary>
/// Add the path to our temporary ignore list. Use when writing to a path within our listening scope.
/// </summary>
@@ -95,21 +114,6 @@ namespace Emby.Server.Implementations.IO
}
}
/// <summary>
/// Initializes a new instance of the <see cref="LibraryMonitor" /> class.
/// </summary>
public LibraryMonitor(
ILogger<LibraryMonitor> logger,
ILibraryManager libraryManager,
IServerConfigurationManager configurationManager,
IFileSystem fileSystem)
{
_libraryManager = libraryManager;
_logger = logger;
_configurationManager = configurationManager;
_fileSystem = fileSystem;
}
private bool IsLibraryMonitorEnabled(BaseItem item)
{
if (item is BasePluginFolder)
@@ -199,7 +203,7 @@ namespace Emby.Server.Implementations.IO
/// <param name="lst">The LST.</param>
/// <param name="path">The path.</param>
/// <returns><c>true</c> if [contains parent folder] [the specified LST]; otherwise, <c>false</c>.</returns>
/// <exception cref="ArgumentNullException">path</exception>
/// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c>.</exception>
private static bool ContainsParentFolder(IEnumerable<string> lst, string path)
{
if (string.IsNullOrEmpty(path))

View File

@@ -5,11 +5,9 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.System;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.IO
@@ -19,7 +17,7 @@ namespace Emby.Server.Implementations.IO
/// </summary>
public class ManagedFileSystem : IFileSystem
{
protected ILogger<ManagedFileSystem> Logger;
private readonly ILogger<ManagedFileSystem> _logger;
private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>();
private readonly string _tempPath;
@@ -29,7 +27,7 @@ namespace Emby.Server.Implementations.IO
ILogger<ManagedFileSystem> logger,
IApplicationPaths applicationPaths)
{
Logger = logger;
_logger = logger;
_tempPath = applicationPaths.TempDirectory;
}
@@ -43,7 +41,7 @@ namespace Emby.Server.Implementations.IO
/// </summary>
/// <param name="filename">The filename.</param>
/// <returns><c>true</c> if the specified filename is shortcut; otherwise, <c>false</c>.</returns>
/// <exception cref="ArgumentNullException">filename</exception>
/// <exception cref="ArgumentNullException"><paramref name="filename"/> is <c>null</c>.</exception>
public virtual bool IsShortcut(string filename)
{
if (string.IsNullOrEmpty(filename))
@@ -60,7 +58,7 @@ namespace Emby.Server.Implementations.IO
/// </summary>
/// <param name="filename">The filename.</param>
/// <returns>System.String.</returns>
/// <exception cref="ArgumentNullException">filename</exception>
/// <exception cref="ArgumentNullException"><paramref name="filename"/> is <c>null</c>.</exception>
public virtual string? ResolveShortcut(string filename)
{
if (string.IsNullOrEmpty(filename))
@@ -235,9 +233,9 @@ namespace Emby.Server.Implementations.IO
result.IsDirectory = info is DirectoryInfo || (info.Attributes & FileAttributes.Directory) == FileAttributes.Directory;
// if (!result.IsDirectory)
//{
// {
// result.IsHidden = (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden;
//}
// }
if (info is FileInfo fileInfo)
{
@@ -248,15 +246,15 @@ namespace Emby.Server.Implementations.IO
{
try
{
using (Stream thisFileStream = File.OpenRead(fileInfo.FullName))
using (var fileHandle = File.OpenHandle(fileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
result.Length = thisFileStream.Length;
result.Length = RandomAccess.GetLength(fileHandle);
}
}
catch (FileNotFoundException ex)
{
// Dangling symlinks cannot be detected before opening the file unfortunately...
Logger.LogError(ex, "Reading the file size of the symlink at {Path} failed. Marking the file as not existing.", fileInfo.FullName);
_logger.LogError(ex, "Reading the file size of the symlink at {Path} failed. Marking the file as not existing.", fileInfo.FullName);
result.Exists = false;
}
}
@@ -345,7 +343,7 @@ namespace Emby.Server.Implementations.IO
}
catch (Exception ex)
{
Logger.LogError(ex, "Error determining CreationTimeUtc for {FullName}", info.FullName);
_logger.LogError(ex, "Error determining CreationTimeUtc for {FullName}", info.FullName);
return DateTime.MinValue;
}
}
@@ -384,7 +382,7 @@ namespace Emby.Server.Implementations.IO
}
catch (Exception ex)
{
Logger.LogError(ex, "Error determining LastAccessTimeUtc for {FullName}", info.FullName);
_logger.LogError(ex, "Error determining LastAccessTimeUtc for {FullName}", info.FullName);
return DateTime.MinValue;
}
}
@@ -423,7 +421,7 @@ namespace Emby.Server.Implementations.IO
}
}
public virtual void SetAttributes(string path, bool isHidden, bool isReadOnly)
public virtual void SetAttributes(string path, bool isHidden, bool readOnly)
{
if (!OperatingSystem.IsWindows())
{
@@ -437,14 +435,14 @@ namespace Emby.Server.Implementations.IO
return;
}
if (info.IsReadOnly == isReadOnly && info.IsHidden == isHidden)
if (info.IsReadOnly == readOnly && info.IsHidden == isHidden)
{
return;
}
var attributes = File.GetAttributes(path);
if (isReadOnly)
if (readOnly)
{
attributes = attributes | FileAttributes.ReadOnly;
}

View File

@@ -1,7 +1,8 @@
#pragma warning disable CS1591
namespace Emby.Server.Implementations
{
/// <summary>
/// Specifies the contract for server startup options.
/// </summary>
public interface IStartupOptions
{
/// <summary>
@@ -10,7 +11,7 @@ namespace Emby.Server.Implementations
string? FFmpegPath { get; }
/// <summary>
/// Gets a value value indicating whether to run as service by the --service command line option.
/// Gets a value indicating whether to run as service by the --service command line option.
/// </summary>
bool IsService { get; }

View File

@@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.Images
public int Order => 0;
protected virtual bool Supports(BaseItem _) => true;
protected virtual bool Supports(BaseItem item) => true;
public async Task<ItemUpdateType> FetchAsync(T item, MetadataRefreshOptions options, CancellationToken cancellationToken)
{
@@ -65,13 +65,13 @@ namespace Emby.Server.Implementations.Images
if (SupportedImages.Contains(ImageType.Primary))
{
var primaryResult = await FetchAsync(item, ImageType.Primary, options, cancellationToken).ConfigureAwait(false);
updateType = updateType | primaryResult;
updateType |= primaryResult;
}
if (SupportedImages.Contains(ImageType.Thumb))
{
var thumbResult = await FetchAsync(item, ImageType.Thumb, options, cancellationToken).ConfigureAwait(false);
updateType = updateType | thumbResult;
updateType |= thumbResult;
}
return updateType;

View File

@@ -0,0 +1,67 @@
#nullable disable
#pragma warning disable CS1591
using System.Collections.Generic;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Images
{
public abstract class BaseFolderImageProvider<T> : BaseDynamicImageProvider<T>
where T : Folder, new()
{
private readonly ILibraryManager _libraryManager;
public BaseFolderImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager)
: base(fileSystem, providerManager, applicationPaths, imageProcessor)
{
_libraryManager = libraryManager;
}
protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
{
return _libraryManager.GetItemList(new InternalItemsQuery
{
Parent = item,
DtoOptions = new DtoOptions(true),
ImageTypes = new ImageType[] { ImageType.Primary },
OrderBy = new (string, SortOrder)[]
{
(ItemSortBy.IsFolder, SortOrder.Ascending),
(ItemSortBy.SortName, SortOrder.Ascending)
},
Limit = 1
});
}
protected override string CreateImage(BaseItem item, IReadOnlyCollection<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex)
{
return CreateSingleImage(itemsWithImages, outputPathWithoutExtension, ImageType.Primary);
}
protected override bool Supports(BaseItem item)
{
return item is T;
}
protected override bool HasChangedByDate(BaseItem item, ItemImageInfo image)
{
if (item is MusicAlbum)
{
return false;
}
return base.HasChangedByDate(item, image);
}
}
}

View File

@@ -2,69 +2,16 @@
#pragma warning disable CS1591
using System.Collections.Generic;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Images
{
public abstract class BaseFolderImageProvider<T> : BaseDynamicImageProvider<T>
where T : Folder, new()
{
protected ILibraryManager _libraryManager;
public BaseFolderImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager)
: base(fileSystem, providerManager, applicationPaths, imageProcessor)
{
_libraryManager = libraryManager;
}
protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
{
return _libraryManager.GetItemList(new InternalItemsQuery
{
Parent = item,
DtoOptions = new DtoOptions(true),
ImageTypes = new ImageType[] { ImageType.Primary },
OrderBy = new System.ValueTuple<string, SortOrder>[]
{
new System.ValueTuple<string, SortOrder>(ItemSortBy.IsFolder, SortOrder.Ascending),
new System.ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending)
},
Limit = 1
});
}
protected override string CreateImage(BaseItem item, IReadOnlyCollection<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex)
{
return CreateSingleImage(itemsWithImages, outputPathWithoutExtension, ImageType.Primary);
}
protected override bool Supports(BaseItem item)
{
return item is T;
}
protected override bool HasChangedByDate(BaseItem item, ItemImageInfo image)
{
if (item is MusicAlbum)
{
return false;
}
return base.HasChangedByDate(item, image);
}
}
public class FolderImageProvider : BaseFolderImageProvider<Folder>
{
public FolderImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager)
@@ -87,20 +34,4 @@ namespace Emby.Server.Implementations.Images
return true;
}
}
public class MusicAlbumImageProvider : BaseFolderImageProvider<MusicAlbum>
{
public MusicAlbumImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager)
: base(fileSystem, providerManager, applicationPaths, imageProcessor, libraryManager)
{
}
}
public class PhotoAlbumImageProvider : BaseFolderImageProvider<PhotoAlbum>
{
public PhotoAlbumImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager)
: base(fileSystem, providerManager, applicationPaths, imageProcessor, libraryManager)
{
}
}
}

View File

@@ -8,7 +8,6 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
@@ -19,46 +18,6 @@ using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Images
{
/// <summary>
/// Class MusicGenreImageProvider.
/// </summary>
public class MusicGenreImageProvider : BaseDynamicImageProvider<MusicGenre>
{
/// <summary>
/// The library manager.
/// </summary>
private readonly ILibraryManager _libraryManager;
public MusicGenreImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager) : base(fileSystem, providerManager, applicationPaths, imageProcessor)
{
_libraryManager = libraryManager;
}
/// <summary>
/// Get children objects used to create an music genre image.
/// </summary>
/// <param name="item">The music genre used to create the image.</param>
/// <returns>Any relevant children objects.</returns>
protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
{
return _libraryManager.GetItemList(new InternalItemsQuery
{
Genres = new[] { item.Name },
IncludeItemTypes = new[]
{
nameof(MusicAlbum),
nameof(MusicVideo),
nameof(Audio)
},
OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
Limit = 4,
Recursive = true,
ImageTypes = new[] { ImageType.Primary },
DtoOptions = new DtoOptions(false)
});
}
}
/// <summary>
/// Class GenreImageProvider.
/// </summary>

View File

@@ -0,0 +1,19 @@
#pragma warning disable CS1591
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
namespace Emby.Server.Implementations.Images
{
public class MusicAlbumImageProvider : BaseFolderImageProvider<MusicAlbum>
{
public MusicAlbumImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager)
: base(fileSystem, providerManager, applicationPaths, imageProcessor, libraryManager)
{
}
}
}

View File

@@ -0,0 +1,59 @@
#nullable disable
#pragma warning disable CS1591
using System.Collections.Generic;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Images
{
/// <summary>
/// Class MusicGenreImageProvider.
/// </summary>
public class MusicGenreImageProvider : BaseDynamicImageProvider<MusicGenre>
{
/// <summary>
/// The library manager.
/// </summary>
private readonly ILibraryManager _libraryManager;
public MusicGenreImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager) : base(fileSystem, providerManager, applicationPaths, imageProcessor)
{
_libraryManager = libraryManager;
}
/// <summary>
/// Get children objects used to create an music genre image.
/// </summary>
/// <param name="item">The music genre used to create the image.</param>
/// <returns>Any relevant children objects.</returns>
protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
{
return _libraryManager.GetItemList(new InternalItemsQuery
{
Genres = new[] { item.Name },
IncludeItemTypes = new[]
{
nameof(MusicAlbum),
nameof(MusicVideo),
nameof(Audio)
},
OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
Limit = 4,
Recursive = true,
ImageTypes = new[] { ImageType.Primary },
DtoOptions = new DtoOptions(false)
});
}
}
}

View File

@@ -0,0 +1,19 @@
#pragma warning disable CS1591
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
namespace Emby.Server.Implementations.Images
{
public class PhotoAlbumImageProvider : BaseFolderImageProvider<PhotoAlbum>
{
public PhotoAlbumImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager)
: base(fileSystem, providerManager, applicationPaths, imageProcessor, libraryManager)
{
}
}
}

View File

@@ -54,7 +54,7 @@ namespace Emby.Server.Implementations.Library
if (parent != null)
{
// Ignore trailer folders but allow it at the collection level
if (string.Equals(filename, BaseItem.TrailerFolderName, StringComparison.OrdinalIgnoreCase)
if (string.Equals(filename, BaseItem.TrailersFolderName, StringComparison.OrdinalIgnoreCase)
&& !(parent is AggregateFolder)
&& !(parent is UserRootFolder))
{
@@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.Library
if (parent != null)
{
// Don't resolve these into audio files
if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFilename, StringComparison.Ordinal)
if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFileName, StringComparison.Ordinal)
&& _libraryManager.IsAudioFile(filename))
{
return true;

View File

@@ -4,6 +4,7 @@
using System;
using System.Globalization;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Library;
@@ -41,6 +42,11 @@ namespace Emby.Server.Implementations.Library
return _closeFn();
}
public Stream GetStream()
{
throw new NotSupportedException();
}
public Task Open(CancellationToken openCancellationToken)
{
return Task.CompletedTask;

View File

@@ -647,7 +647,7 @@ namespace Emby.Server.Implementations.Library
/// Determines whether a path should be ignored based on its contents - called after the contents have been read.
/// </summary>
/// <param name="args">The args.</param>
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
private static bool ShouldResolvePathContents(ItemResolveArgs args)
{
// Ignore any folders containing a file called .ignore
@@ -1250,10 +1250,8 @@ namespace Emby.Server.Implementations.Library
private CollectionTypeOptions? GetCollectionType(string path)
{
var files = _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false);
foreach (var file in files)
foreach (ReadOnlySpan<char> file in files)
{
// TODO: @bond use a ReadOnlySpan<char> here when Enum.TryParse supports it
// https://github.com/dotnet/runtime/issues/20008
if (Enum.TryParse<CollectionTypeOptions>(Path.GetFileNameWithoutExtension(file), true, out var res))
{
return res;
@@ -1268,7 +1266,7 @@ namespace Emby.Server.Implementations.Library
/// </summary>
/// <param name="id">The id.</param>
/// <returns>BaseItem.</returns>
/// <exception cref="ArgumentNullException">id</exception>
/// <exception cref="ArgumentNullException"><paramref name="id"/> is <c>null</c>.</exception>
public BaseItem GetItemById(Guid id)
{
if (id == Guid.Empty)
@@ -1761,22 +1759,20 @@ namespace Emby.Server.Implementations.Library
return orderedItems ?? items;
}
public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<ValueTuple<string, SortOrder>> orderByList)
public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<ValueTuple<string, SortOrder>> orderBy)
{
var isFirst = true;
IOrderedEnumerable<BaseItem> orderedItems = null;
foreach (var orderBy in orderByList)
foreach (var (name, sortOrder) in orderBy)
{
var comparer = GetComparer(orderBy.Item1, user);
var comparer = GetComparer(name, user);
if (comparer == null)
{
continue;
}
var sortOrder = orderBy.Item2;
if (isFirst)
{
orderedItems = sortOrder == SortOrder.Descending ? items.OrderByDescending(i => i, comparer) : items.OrderBy(i => i, comparer);
@@ -2716,7 +2712,7 @@ namespace Emby.Server.Implementations.Library
var namingOptions = GetNamingOptions();
var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory)
.Where(i => string.Equals(i.Name, BaseItem.TrailerFolderName, StringComparison.OrdinalIgnoreCase))
.Where(i => string.Equals(i.Name, BaseItem.TrailersFolderName, StringComparison.OrdinalIgnoreCase))
.SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false))
.ToList();
@@ -2760,7 +2756,7 @@ namespace Emby.Server.Implementations.Library
var namingOptions = GetNamingOptions();
var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory)
.Where(i => BaseItem.AllExtrasTypesFolderNames.Contains(i.Name ?? string.Empty, StringComparer.OrdinalIgnoreCase))
.Where(i => BaseItem.AllExtrasTypesFolderNames.ContainsKey(i.Name ?? string.Empty))
.SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false))
.ToList();
@@ -3076,9 +3072,9 @@ namespace Emby.Server.Implementations.Library
});
}
public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo)
public void AddMediaPath(string virtualFolderName, MediaPathInfo mediaPath)
{
AddMediaPathInternal(virtualFolderName, pathInfo, true);
AddMediaPathInternal(virtualFolderName, mediaPath, true);
}
private void AddMediaPathInternal(string virtualFolderName, MediaPathInfo pathInfo, bool saveLibraryOptions)
@@ -3131,11 +3127,11 @@ namespace Emby.Server.Implementations.Library
}
}
public void UpdateMediaPath(string virtualFolderName, MediaPathInfo pathInfo)
public void UpdateMediaPath(string virtualFolderName, MediaPathInfo mediaPath)
{
if (pathInfo == null)
if (mediaPath == null)
{
throw new ArgumentNullException(nameof(pathInfo));
throw new ArgumentNullException(nameof(mediaPath));
}
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
@@ -3148,9 +3144,9 @@ namespace Emby.Server.Implementations.Library
var list = libraryOptions.PathInfos.ToList();
foreach (var originalPathInfo in list)
{
if (string.Equals(pathInfo.Path, originalPathInfo.Path, StringComparison.Ordinal))
if (string.Equals(mediaPath.Path, originalPathInfo.Path, StringComparison.Ordinal))
{
originalPathInfo.NetworkPath = pathInfo.NetworkPath;
originalPathInfo.NetworkPath = mediaPath.NetworkPath;
break;
}
}

View File

@@ -10,13 +10,14 @@ using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using Jellyfin.Extensions.Json;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
@@ -49,7 +50,7 @@ namespace Emby.Server.Implementations.Library
{
try
{
await using FileStream jsonStream = File.OpenRead(cacheFilePath);
await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
// _logger.LogDebug("Found cached media info");
@@ -86,7 +87,7 @@ namespace Emby.Server.Implementations.Library
if (cacheFilePath != null)
{
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
await using FileStream createStream = File.OpenWrite(cacheFilePath);
await using FileStream createStream = AsyncFile.OpenWrite(cacheFilePath);
await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
// _logger.LogDebug("Saved media info to {0}", cacheFilePath);

View File

@@ -13,9 +13,9 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using Jellyfin.Extensions.Json;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
@@ -521,7 +521,7 @@ namespace Emby.Server.Implementations.Library
// TODO: @bond Fix
var json = JsonSerializer.SerializeToUtf8Bytes(mediaSource, _jsonOptions);
_logger.LogInformation("Live stream opened: " + json);
_logger.LogInformation("Live stream opened: {@MediaSource}", mediaSource);
var clone = JsonSerializer.Deserialize<MediaSourceInfo>(json, _jsonOptions);
if (!request.UserId.Equals(Guid.Empty))
@@ -587,13 +587,6 @@ namespace Emby.Server.Implementations.Library
mediaSource.InferTotalBitrate();
}
public Task<IDirectStreamProvider> GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken)
{
var info = _openStreams.FirstOrDefault(i => i.Value != null && string.Equals(i.Value.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase));
return Task.FromResult(info.Value as IDirectStreamProvider);
}
public async Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationToken)
{
var result = await OpenLiveStreamInternal(request, cancellationToken).ConfigureAwait(false);
@@ -602,7 +595,8 @@ namespace Emby.Server.Implementations.Library
public async Task<MediaSourceInfo> GetLiveStreamMediaInfo(string id, CancellationToken cancellationToken)
{
var liveStreamInfo = await GetLiveStreamInfo(id, cancellationToken).ConfigureAwait(false);
// TODO probably shouldn't throw here but it is kept for "backwards compatibility"
var liveStreamInfo = GetLiveStreamInfo(id) ?? throw new ResourceNotFoundException();
var mediaSource = liveStreamInfo.MediaSource;
@@ -638,7 +632,7 @@ namespace Emby.Server.Implementations.Library
{
try
{
await using FileStream jsonStream = File.OpenRead(cacheFilePath);
await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
// _logger.LogDebug("Found cached media info");
@@ -771,18 +765,19 @@ namespace Emby.Server.Implementations.Library
mediaSource.InferTotalBitrate(true);
}
public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStreamWithDirectStreamProvider(string id, CancellationToken cancellationToken)
public Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStreamWithDirectStreamProvider(string id, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(id))
{
throw new ArgumentNullException(nameof(id));
}
var info = await GetLiveStreamInfo(id, cancellationToken).ConfigureAwait(false);
return new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider);
// TODO probably shouldn't throw here but it is kept for "backwards compatibility"
var info = GetLiveStreamInfo(id) ?? throw new ResourceNotFoundException();
return Task.FromResult(new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider));
}
private Task<ILiveStream> GetLiveStreamInfo(string id, CancellationToken cancellationToken)
public ILiveStream GetLiveStreamInfo(string id)
{
if (string.IsNullOrEmpty(id))
{
@@ -791,12 +786,16 @@ namespace Emby.Server.Implementations.Library
if (_openStreams.TryGetValue(id, out ILiveStream info))
{
return Task.FromResult(info);
}
else
{
return Task.FromException<ILiveStream>(new ResourceNotFoundException());
return info;
}
return null;
}
/// <inheritdoc />
public ILiveStream GetLiveStreamInfoByUniqueId(string uniqueId)
{
return _openStreams.Values.FirstOrDefault(stream => string.Equals(uniqueId, stream?.UniqueId, StringComparison.OrdinalIgnoreCase));
}
public async Task<MediaSourceInfo> GetLiveStream(string id, CancellationToken cancellationToken)

View File

@@ -36,9 +36,10 @@ namespace Emby.Server.Implementations.Library
return list.Concat(GetInstantMixFromGenres(item.Genres, user, dtoOptions)).ToList();
}
public List<BaseItem> GetInstantMixFromArtist(MusicArtist item, User user, DtoOptions dtoOptions)
/// <inheritdoc />
public List<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User user, DtoOptions dtoOptions)
{
return GetInstantMixFromGenres(item.Genres, user, dtoOptions);
return GetInstantMixFromGenres(artist.Genres, user, dtoOptions);
}
public List<BaseItem> GetInstantMixFromAlbum(MusicAlbum item, User user, DtoOptions dtoOptions)

View File

@@ -53,7 +53,7 @@ namespace Emby.Server.Implementations.Library
/// <param name="path">The original path.</param>
/// <param name="subPath">The original sub path.</param>
/// <param name="newSubPath">The new sub path.</param>
/// <param name="newPath">The result of the sub path replacement</param>
/// <param name="newPath">The result of the sub path replacement.</param>
/// <returns>The path after replacing the sub path.</returns>
/// <exception cref="ArgumentNullException"><paramref name="path" />, <paramref name="newSubPath" /> or <paramref name="newSubPath" /> is empty.</exception>
public static bool TryReplaceSubPath(

View File

@@ -82,6 +82,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
/// <summary>
/// Determine if the supplied file data points to a music album.
/// </summary>
/// <param name="path">The path to check.</param>
/// <param name="directoryService">The directory service.</param>
/// <returns><c>true</c> if the provided path points to a music album, <c>false</c> otherwise.</returns>
public bool IsMusicAlbum(string path, IDirectoryService directoryService)
{
return ContainsMusic(directoryService.GetFileSystemEntries(path), true, directoryService, _logger, _fileSystem, _libraryManager);

View File

@@ -21,13 +21,13 @@ namespace Emby.Server.Implementations.Library.Resolvers
public abstract class BaseVideoResolver<T> : MediaBrowser.Controller.Resolvers.ItemResolver<T>
where T : Video, new()
{
protected readonly ILibraryManager LibraryManager;
protected BaseVideoResolver(ILibraryManager libraryManager)
{
LibraryManager = libraryManager;
}
protected ILibraryManager LibraryManager { get; }
/// <summary>
/// Resolves the specified args.
/// </summary>
@@ -275,6 +275,10 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// <summary>
/// Determines whether [is DVD directory] [the specified directory name].
/// </summary>
/// <param name="fullPath">The full path of the directory.</param>
/// <param name="directoryName">The name of the directory.</param>
/// <param name="directoryService">The directory service.</param>
/// <returns><c>true</c> if the provided directory is a DVD directory, <c>false</c> otherwise.</returns>
protected bool IsDvdDirectory(string fullPath, string directoryName, IDirectoryService directoryService)
{
if (!string.Equals(directoryName, "video_ts", StringComparison.OrdinalIgnoreCase))

View File

@@ -49,13 +49,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
{
var bookFiles = args.FileSystemChildren.Where(f =>
{
var fileExtension = Path.GetExtension(f.FullName) ??
string.Empty;
var fileExtension = Path.GetExtension(f.FullName)
?? string.Empty;
return _validExtensions.Contains(
fileExtension,
StringComparer
.OrdinalIgnoreCase);
StringComparer.OrdinalIgnoreCase);
}).ToList();
// Don't return a Book if there is more (or less) than one document in the directory

View File

@@ -9,7 +9,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// <summary>
/// Class FolderResolver.
/// </summary>
public class FolderResolver : FolderResolver<Folder>
public class FolderResolver : GenericFolderResolver<Folder>
{
/// <summary>
/// Gets the priority.
@@ -32,24 +32,4 @@ namespace Emby.Server.Implementations.Library.Resolvers
return null;
}
}
/// <summary>
/// Class FolderResolver.
/// </summary>
/// <typeparam name="TItemType">The type of the T item type.</typeparam>
public abstract class FolderResolver<TItemType> : ItemResolver<TItemType>
where TItemType : Folder, new()
{
/// <summary>
/// Sets the initial item values.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="args">The args.</param>
protected override void SetInitialItemValues(TItemType item, ItemResolveArgs args)
{
base.SetInitialItemValues(item, args);
item.IsRoot = args.Parent == null;
}
}
}

View File

@@ -0,0 +1,27 @@
#nullable disable
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
namespace Emby.Server.Implementations.Library.Resolvers
{
/// <summary>
/// Class FolderResolver.
/// </summary>
/// <typeparam name="TItemType">The type of the T item type.</typeparam>
public abstract class GenericFolderResolver<TItemType> : ItemResolver<TItemType>
where TItemType : Folder, new()
{
/// <summary>
/// Sets the initial item values.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="args">The args.</param>
protected override void SetInitialItemValues(TItemType item, ItemResolveArgs args)
{
base.SetInitialItemValues(item, args);
item.IsRoot = args.Parent == null;
}
}
}

View File

@@ -12,7 +12,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
/// <summary>
/// Class BoxSetResolver.
/// </summary>
public class BoxSetResolver : FolderResolver<BoxSet>
public class BoxSetResolver : GenericFolderResolver<BoxSet>
{
/// <summary>
/// Resolves the specified args.

View File

@@ -24,6 +24,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
/// </summary>
public class MovieResolver : BaseVideoResolver<Video>, IMultiItemResolver
{
private readonly IImageProcessor _imageProcessor;
private string[] _validCollectionTypes = new[]
{
CollectionType.Movies,
@@ -33,8 +35,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
CollectionType.Photos
};
private readonly IImageProcessor _imageProcessor;
/// <summary>
/// Initializes a new instance of the <see cref="MovieResolver"/> class.
/// </summary>

View File

@@ -12,7 +12,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// <summary>
/// Class PhotoAlbumResolver.
/// </summary>
public class PhotoAlbumResolver : FolderResolver<PhotoAlbum>
public class PhotoAlbumResolver : GenericFolderResolver<PhotoAlbum>
{
private readonly IImageProcessor _imageProcessor;
private readonly ILibraryManager _libraryManager;

View File

@@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// <summary>
/// <see cref="IItemResolver"/> for <see cref="Playlist"/> library items.
/// </summary>
public class PlaylistResolver : FolderResolver<Playlist>
public class PlaylistResolver : GenericFolderResolver<Playlist>
{
private string[] _musicPlaylistCollectionTypes =
{

View File

@@ -13,7 +13,7 @@ using MediaBrowser.Model.IO;
namespace Emby.Server.Implementations.Library.Resolvers
{
public class SpecialFolderResolver : FolderResolver<Folder>
public class SpecialFolderResolver : GenericFolderResolver<Folder>
{
private readonly IFileSystem _fileSystem;
private readonly IServerApplicationPaths _appPaths;
@@ -67,7 +67,6 @@ namespace Emby.Server.Implementations.Library.Resolvers
return args.FileSystemChildren
.Where(i =>
{
try
{
return !i.IsDirectory &&

View File

@@ -47,7 +47,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
if ((season != null ||
string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ||
args.HasParent<Series>())
&& (parent is Series || !BaseItem.AllExtrasTypesFolderNames.Contains(parent.Name, StringComparer.OrdinalIgnoreCase)))
&& (parent is Series || !BaseItem.AllExtrasTypesFolderNames.ContainsKey(parent.Name)))
{
var episode = ResolveVideo<Episode>(args, false);

View File

@@ -12,7 +12,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// <summary>
/// Class SeasonResolver.
/// </summary>
public class SeasonResolver : FolderResolver<Season>
public class SeasonResolver : GenericFolderResolver<Season>
{
private readonly ILibraryManager _libraryManager;
private readonly ILocalizationManager _localization;

View File

@@ -8,7 +8,6 @@ using System.IO;
using Emby.Naming.TV;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
@@ -19,7 +18,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// <summary>
/// Class SeriesResolver.
/// </summary>
public class SeriesResolver : FolderResolver<Series>
public class SeriesResolver : GenericFolderResolver<Series>
{
private readonly ILogger<SeriesResolver> _logger;
private readonly ILibraryManager _libraryManager;

View File

@@ -73,7 +73,7 @@ namespace Emby.Server.Implementations.Library
/// <param name="query">The query.</param>
/// <param name="user">The user.</param>
/// <returns>IEnumerable{SearchHintResult}.</returns>
/// <exception cref="ArgumentNullException">searchTerm</exception>
/// <exception cref="ArgumentException"><c>query.SearchTerm</c> is <c>null</c> or empty.</exception>
private List<SearchHintInfo> GetSearchHints(SearchQuery query, User user)
{
var searchTerm = query.SearchTerm;

View File

@@ -25,8 +25,6 @@ namespace Emby.Server.Implementations.Library
/// </summary>
public class UserDataManager : IUserDataManager
{
public event EventHandler<UserDataSaveEventArgs> UserDataSaved;
private readonly ConcurrentDictionary<string, UserItemData> _userData =
new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase);
@@ -44,6 +42,8 @@ namespace Emby.Server.Implementations.Library
_repository = repository;
}
public event EventHandler<UserDataSaveEventArgs> UserDataSaved;
public void SaveUserData(Guid userId, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken)
{
var user = _userManager.GetUserById(userId);
@@ -90,10 +90,9 @@ namespace Emby.Server.Implementations.Library
/// <summary>
/// Save the provided user data for the given user. Batch operation. Does not fire any events or update the cache.
/// </summary>
/// <param name="userId"></param>
/// <param name="userData"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <param name="userId">The user id.</param>
/// <param name="userData">The user item data.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public void SaveAllUserData(Guid userId, UserItemData[] userData, CancellationToken cancellationToken)
{
var user = _userManager.GetUserById(userId);
@@ -104,8 +103,8 @@ namespace Emby.Server.Implementations.Library
/// <summary>
/// Retrieve all user data for the given user.
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
/// <param name="userId">The user id.</param>
/// <returns>A <see cref="List{UserItemData}"/> containing all of the user's item data.</returns>
public List<UserItemData> GetAllUserData(Guid userId)
{
var user = _userManager.GetUserById(userId);
@@ -177,6 +176,7 @@ namespace Emby.Server.Implementations.Library
return dto;
}
/// <inheritdoc />
public UserItemDataDto GetUserDataDto(BaseItem item, BaseItemDto itemDto, User user, DtoOptions options)
{
var userData = GetUserData(user, item);
@@ -191,7 +191,7 @@ namespace Emby.Server.Implementations.Library
/// </summary>
/// <param name="data">The data.</param>
/// <returns>DtoUserItemData.</returns>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="ArgumentNullException"><paramref name="data"/> is <c>null</c>.</exception>
private UserItemDataDto GetUserItemDataDto(UserItemData data)
{
if (data == null)
@@ -212,6 +212,7 @@ namespace Emby.Server.Implementations.Library
};
}
/// <inheritdoc />
public bool UpdatePlayState(BaseItem item, UserItemData data, long? reportedPositionTicks)
{
var playedToCompletion = false;

View File

@@ -341,14 +341,16 @@ namespace Emby.Server.Implementations.Library
mediaTypes = mediaTypes.Distinct().ToList();
}
var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Count == 0 ? new[]
{
nameof(Person),
nameof(Studio),
nameof(Year),
nameof(MusicGenre),
nameof(Genre)
} : Array.Empty<string>();
var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Count == 0
? new[]
{
nameof(Person),
nameof(Studio),
nameof(Year),
nameof(MusicGenre),
nameof(Genre)
}
: Array.Empty<string>();
var query = new InternalItemsQuery(user)
{

View File

@@ -95,10 +95,13 @@ namespace Emby.Server.Implementations.Library.Validators
_logger.LogInformation("Deleting dead {2} {0} {1}.", item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name, item.GetType().Name);
_libraryManager.DeleteItem(item, new DeleteOptions
{
DeleteFileLocation = false
}, false);
_libraryManager.DeleteItem(
item,
new DeleteOptions
{
DeleteFileLocation = false
},
false);
}
progress.Report(100);

View File

@@ -5,6 +5,7 @@ using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
@@ -46,20 +47,27 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(targetFile)));
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None))
using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous))
{
onStarted();
_logger.LogInformation("Copying recording stream to file {0}", targetFile);
_logger.LogInformation("Copying recording to file {FilePath}", targetFile);
// The media source is infinite so we need to handle stopping ourselves
using var durationToken = new CancellationTokenSource(duration);
using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token);
var linkedCancellationToken = cancellationTokenSource.Token;
await directStreamProvider.CopyToAsync(output, cancellationTokenSource.Token).ConfigureAwait(false);
await using var fileStream = new ProgressiveFileStream(directStreamProvider.GetStream());
await _streamHelper.CopyToAsync(
fileStream,
output,
IODefaults.CopyToBufferSize,
1000,
linkedCancellationToken).ConfigureAwait(false);
}
_logger.LogInformation("Recording completed to file {0}", targetFile);
_logger.LogInformation("Recording completed: {FilePath}", targetFile);
}
private async Task RecordFromMediaSource(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
@@ -72,7 +80,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(targetFile)));
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None);
await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.CopyToBufferSize, FileOptions.Asynchronous);
onStarted();

View File

@@ -610,11 +610,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
throw new NotImplementedException();
}
public Task<string> CreateTimer(TimerInfo timer, CancellationToken cancellationToken)
public Task<string> CreateTimer(TimerInfo info, CancellationToken cancellationToken)
{
var existingTimer = string.IsNullOrWhiteSpace(timer.ProgramId) ?
var existingTimer = string.IsNullOrWhiteSpace(info.ProgramId) ?
null :
_timerProvider.GetTimerByProgramId(timer.ProgramId);
_timerProvider.GetTimerByProgramId(info.ProgramId);
if (existingTimer != null)
{
@@ -632,32 +632,32 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
}
timer.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
LiveTvProgram programInfo = null;
if (!string.IsNullOrWhiteSpace(timer.ProgramId))
if (!string.IsNullOrWhiteSpace(info.ProgramId))
{
programInfo = GetProgramInfoFromCache(timer);
programInfo = GetProgramInfoFromCache(info);
}
if (programInfo == null)
{
_logger.LogInformation("Unable to find program with Id {0}. Will search using start date", timer.ProgramId);
programInfo = GetProgramInfoFromCache(timer.ChannelId, timer.StartDate);
_logger.LogInformation("Unable to find program with Id {0}. Will search using start date", info.ProgramId);
programInfo = GetProgramInfoFromCache(info.ChannelId, info.StartDate);
}
if (programInfo != null)
{
CopyProgramInfoToTimerInfo(programInfo, timer);
CopyProgramInfoToTimerInfo(programInfo, info);
}
timer.IsManual = true;
_timerProvider.Add(timer);
info.IsManual = true;
_timerProvider.Add(info);
TimerCreated?.Invoke(this, new GenericEventArgs<TimerInfo>(timer));
TimerCreated?.Invoke(this, new GenericEventArgs<TimerInfo>(info));
return Task.FromResult(timer.Id);
return Task.FromResult(info.Id);
}
public async Task<string> CreateSeriesTimer(SeriesTimerInfo info, CancellationToken cancellationToken)
@@ -1990,7 +1990,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
writer.WriteElementString(
"dateadded",
DateTime.UtcNow.ToLocalTime().ToString(DateAddedFormat, CultureInfo.InvariantCulture));
DateTime.Now.ToString(DateAddedFormat, CultureInfo.InvariantCulture));
if (item.ProductionYear.HasValue)
{

View File

@@ -94,7 +94,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
Directory.CreateDirectory(Path.GetDirectoryName(logFilePath));
// FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
_logFileStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
_logFileStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
await JsonSerializer.SerializeAsync(_logFileStream, mediaSource, _jsonOptions, cancellationToken).ConfigureAwait(false);
await _logFileStream.WriteAsync(Encoding.UTF8.GetBytes(Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine), cancellationToken).ConfigureAwait(false);
@@ -188,7 +188,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
CultureInfo.InvariantCulture,
"-i \"{0}\" {2} -map_metadata -1 -threads {6} {3}{4}{5} -y \"{1}\"",
inputTempFile,
targetFile,
targetFile.Replace("\"", "\\\""), // Escape quotes in filename
videoArgs,
GetAudioArgs(mediaSource),
subtitleArgs,
@@ -205,9 +205,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
// var audioChannels = 2;
// var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
// if (audioStream != null)
//{
// {
// audioChannels = audioStream.Channels ?? audioChannels;
//}
// }
// return "-codec:a:0 aac -strict experimental -ab 320000";
}

View File

@@ -13,6 +13,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
/// <summary>
/// Records the specified media source.
/// </summary>
/// <param name="directStreamProvider">The direct stream provider, or <c>null</c>.</param>
/// <param name="mediaSource">The media source.</param>
/// <param name="targetFile">The target file.</param>
/// <param name="duration">The duration to record.</param>
/// <param name="onStarted">An action to perform when recording starts.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A <see cref="Task"/> that represents the recording operation.</returns>
Task Record(IDirectStreamProvider? directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken);
string GetOutputPath(MediaSourceInfo mediaSource, string targetFile);

View File

@@ -1,9 +1,8 @@
#nullable disable
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text.Json;
@@ -18,7 +17,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private readonly string _dataPath;
private readonly object _fileDataLock = new object();
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private T[] _items;
private T[]? _items;
public ItemDataProvider(
ILogger logger,
@@ -34,6 +33,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
protected Func<T, T, bool> EqualityComparer { get; }
[MemberNotNull(nameof(_items))]
private void EnsureLoaded()
{
if (_items != null)
@@ -49,6 +49,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
var bytes = File.ReadAllBytes(_dataPath);
_items = JsonSerializer.Deserialize<T[]>(bytes, _jsonOptions);
if (_items == null)
{
Logger.LogError("Error deserializing {Path}, data was null", _dataPath);
_items = Array.Empty<T>();
}
return;
}
catch (JsonException ex)
@@ -62,7 +68,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private void SaveList()
{
Directory.CreateDirectory(Path.GetDirectoryName(_dataPath));
Directory.CreateDirectory(Path.GetDirectoryName(_dataPath) ?? throw new ArgumentException("Path can't be a root directory.", nameof(_dataPath)));
var jsonString = JsonSerializer.Serialize(_items, _jsonOptions);
File.WriteAllText(_dataPath, jsonString);
}

View File

@@ -10,6 +10,7 @@ using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Extensions;
using Jellyfin.XmlTv;
using Jellyfin.XmlTv.Entities;
using MediaBrowser.Common.Extensions;
@@ -81,7 +82,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(path, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await using (var fileStream = new FileStream(cacheFile, FileMode.CreateNew))
await using (var fileStream = new FileStream(cacheFile, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.CopyToBufferSize, FileOptions.Asynchronous))
{
await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
}
@@ -89,11 +90,11 @@ namespace Emby.Server.Implementations.LiveTv.Listings
return UnzipIfNeeded(path, cacheFile);
}
private string UnzipIfNeeded(string originalUrl, string file)
private string UnzipIfNeeded(ReadOnlySpan<char> originalUrl, string file)
{
string ext = Path.GetExtension(originalUrl.Split('?')[0]);
ReadOnlySpan<char> ext = Path.GetExtension(originalUrl.LeftPart('?'));
if (string.Equals(ext, ".gz", StringComparison.OrdinalIgnoreCase))
if (ext.Equals(".gz", StringComparison.OrdinalIgnoreCase))
{
try
{

View File

@@ -65,6 +65,8 @@ namespace Emby.Server.Implementations.LiveTv
private ITunerHost[] _tunerHosts = Array.Empty<ITunerHost>();
private IListingsProvider[] _listingProviders = Array.Empty<IListingsProvider>();
private bool _disposed = false;
public LiveTvManager(
IServerConfigurationManager config,
ILogger<LiveTvManager> logger,
@@ -520,7 +522,7 @@ namespace Emby.Server.Implementations.LiveTv
return item;
}
private Tuple<LiveTvProgram, bool, bool> GetProgram(ProgramInfo info, Dictionary<Guid, LiveTvProgram> allExistingPrograms, LiveTvChannel channel, ChannelType channelType, string serviceName, CancellationToken cancellationToken)
private (LiveTvProgram item, bool isNew, bool isUpdated) GetProgram(ProgramInfo info, Dictionary<Guid, LiveTvProgram> allExistingPrograms, LiveTvChannel channel)
{
var id = _tvDtoService.GetInternalProgramId(info.Id);
@@ -559,8 +561,6 @@ namespace Emby.Server.Implementations.LiveTv
item.ParentId = channel.Id;
// item.ChannelType = channelType;
item.Audio = info.Audio;
item.ChannelId = channel.Id;
item.CommunityRating ??= info.CommunityRating;
@@ -772,7 +772,7 @@ namespace Emby.Server.Implementations.LiveTv
item.OnMetadataChanged();
}
return new Tuple<LiveTvProgram, bool, bool>(item, isNew, isUpdated);
return (item, isNew, isUpdated);
}
public async Task<BaseItemDto> GetProgram(string id, CancellationToken cancellationToken, User user = null)
@@ -1187,14 +1187,14 @@ namespace Emby.Server.Implementations.LiveTv
foreach (var program in channelPrograms)
{
var programTuple = GetProgram(program, existingPrograms, currentChannel, currentChannel.ChannelType, service.Name, cancellationToken);
var programItem = programTuple.Item1;
var programTuple = GetProgram(program, existingPrograms, currentChannel);
var programItem = programTuple.item;
if (programTuple.Item2)
if (programTuple.isNew)
{
newPrograms.Add(programItem);
}
else if (programTuple.Item3)
else if (programTuple.isUpdated)
{
updatedPrograms.Add(programItem);
}
@@ -1385,10 +1385,10 @@ namespace Emby.Server.Implementations.LiveTv
// var items = allActivePaths.Select(i => _libraryManager.FindByPath(i, false)).Where(i => i != null).ToArray();
// return new QueryResult<BaseItem>
//{
// {
// Items = items,
// TotalRecordCount = items.Length
//};
// };
dtoOptions.Fields = dtoOptions.Fields.Concat(new[] { ItemFields.Tags }).Distinct().ToArray();
}
@@ -1425,16 +1425,15 @@ namespace Emby.Server.Implementations.LiveTv
return result;
}
public Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem, BaseItemDto)> tuples, IReadOnlyList<ItemFields> fields, User user = null)
public Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem, BaseItemDto)> programs, IReadOnlyList<ItemFields> fields, User user = null)
{
var programTuples = new List<Tuple<BaseItemDto, string, string>>();
var hasChannelImage = fields.Contains(ItemFields.ChannelImage);
var hasChannelInfo = fields.Contains(ItemFields.ChannelInfo);
foreach (var tuple in tuples)
foreach (var (item, dto) in programs)
{
var program = (LiveTvProgram)tuple.Item1;
var dto = tuple.Item2;
var program = (LiveTvProgram)item;
dto.StartDate = program.StartDate;
dto.EpisodeTitle = program.EpisodeTitle;
@@ -1871,11 +1870,11 @@ namespace Emby.Server.Implementations.LiveTv
return _libraryManager.GetItemById(internalChannelId);
}
public void AddChannelInfo(IReadOnlyCollection<(BaseItemDto, LiveTvChannel)> tuples, DtoOptions options, User user)
public void AddChannelInfo(IReadOnlyCollection<(BaseItemDto, LiveTvChannel)> items, DtoOptions options, User user)
{
var now = DateTime.UtcNow;
var channelIds = tuples.Select(i => i.Item2.Id).Distinct().ToArray();
var channelIds = items.Select(i => i.Item2.Id).Distinct().ToArray();
var programs = options.AddCurrentProgram ? _libraryManager.GetItemList(new InternalItemsQuery(user)
{
@@ -1896,7 +1895,7 @@ namespace Emby.Server.Implementations.LiveTv
var addCurrentProgram = options.AddCurrentProgram;
foreach (var tuple in tuples)
foreach (var tuple in items)
{
var dto = tuple.Item1;
var channel = tuple.Item2;
@@ -2118,17 +2117,13 @@ namespace Emby.Server.Implementations.LiveTv
};
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private bool _disposed = false;
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
@@ -2324,20 +2319,20 @@ namespace Emby.Server.Implementations.LiveTv
_taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
}
public async Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelId, string providerChannelId)
public async Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelNumber, string providerChannelNumber)
{
var config = GetConfiguration();
var listingsProviderInfo = config.ListingProviders.First(i => string.Equals(providerId, i.Id, StringComparison.OrdinalIgnoreCase));
listingsProviderInfo.ChannelMappings = listingsProviderInfo.ChannelMappings.Where(i => !string.Equals(i.Name, tunerChannelId, StringComparison.OrdinalIgnoreCase)).ToArray();
listingsProviderInfo.ChannelMappings = listingsProviderInfo.ChannelMappings.Where(i => !string.Equals(i.Name, tunerChannelNumber, StringComparison.OrdinalIgnoreCase)).ToArray();
if (!string.Equals(tunerChannelId, providerChannelId, StringComparison.OrdinalIgnoreCase))
if (!string.Equals(tunerChannelNumber, providerChannelNumber, StringComparison.OrdinalIgnoreCase))
{
var list = listingsProviderInfo.ChannelMappings.ToList();
list.Add(new NameValuePair
{
Name = tunerChannelId,
Value = providerChannelId
Name = tunerChannelNumber,
Value = providerChannelNumber
});
listingsProviderInfo.ChannelMappings = list.ToArray();
}
@@ -2357,10 +2352,10 @@ namespace Emby.Server.Implementations.LiveTv
_taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
return tunerChannelMappings.First(i => string.Equals(i.Id, tunerChannelId, StringComparison.OrdinalIgnoreCase));
return tunerChannelMappings.First(i => string.Equals(i.Id, tunerChannelNumber, StringComparison.OrdinalIgnoreCase));
}
public TunerChannelMapping GetTunerChannelMapping(ChannelInfo tunerChannel, NameValuePair[] mappings, List<ChannelInfo> epgChannels)
public TunerChannelMapping GetTunerChannelMapping(ChannelInfo tunerChannel, NameValuePair[] mappings, List<ChannelInfo> providerChannels)
{
var result = new TunerChannelMapping
{
@@ -2373,7 +2368,7 @@ namespace Emby.Server.Implementations.LiveTv
result.Name = tunerChannel.Number + " " + result.Name;
}
var providerChannel = EmbyTV.EmbyTV.Current.GetEpgChannelFromTunerChannel(mappings, tunerChannel, epgChannels);
var providerChannel = EmbyTV.EmbyTV.Current.GetEpgChannelFromTunerChannel(mappings, tunerChannel, providerChannels);
if (providerChannel != null)
{

View File

@@ -23,10 +23,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
public abstract class BaseTunerHost
{
protected readonly IServerConfigurationManager Config;
protected readonly ILogger<BaseTunerHost> Logger;
protected readonly IFileSystem FileSystem;
private readonly IMemoryCache _memoryCache;
protected BaseTunerHost(IServerConfigurationManager config, ILogger<BaseTunerHost> logger, IFileSystem fileSystem, IMemoryCache memoryCache)
@@ -37,12 +33,20 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
FileSystem = fileSystem;
}
protected IServerConfigurationManager Config { get; }
protected ILogger<BaseTunerHost> Logger { get; }
protected IFileSystem FileSystem { get; }
public virtual bool IsSupported => true;
protected abstract Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken);
public abstract string Type { get; }
protected virtual string ChannelIdPrefix => Type + "_";
protected abstract Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken);
public async Task<List<ChannelInfo>> GetChannels(TunerHostInfo tuner, bool enableCache, CancellationToken cancellationToken)
{
var key = tuner.Id;
@@ -92,7 +96,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
try
{
Directory.CreateDirectory(Path.GetDirectoryName(channelCacheFile));
await using var writeStream = File.OpenWrite(channelCacheFile);
await using var writeStream = AsyncFile.OpenWrite(channelCacheFile);
await JsonSerializer.SerializeAsync(writeStream, channels, cancellationToken: cancellationToken).ConfigureAwait(false);
}
catch (IOException)
@@ -108,7 +112,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
try
{
await using var readStream = File.OpenRead(channelCacheFile);
await using var readStream = AsyncFile.OpenRead(channelCacheFile);
var channels = await JsonSerializer.DeserializeAsync<List<ChannelInfo>>(readStream, cancellationToken: cancellationToken)
.ConfigureAwait(false);
list.AddRange(channels);
@@ -158,7 +162,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return new List<MediaSourceInfo>();
}
protected abstract Task<ILiveStream> GetChannelStream(TunerHostInfo tuner, ChannelInfo channel, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken);
protected abstract Task<ILiveStream> GetChannelStream(TunerHostInfo tunerHost, ChannelInfo channel, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken);
public async Task<ILiveStream> GetChannelStream(string channelId, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
{
@@ -217,8 +221,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
throw new LiveTvConflictException();
}
protected virtual string ChannelIdPrefix => Type + "_";
protected virtual bool IsValidChannelId(string channelId)
{
if (string.IsNullOrEmpty(channelId))

View File

@@ -0,0 +1,35 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
public class HdHomerunChannelCommands : IHdHomerunChannelCommands
{
private string? _channel;
private string? _profile;
public HdHomerunChannelCommands(string? channel, string? profile)
{
_channel = channel;
_profile = profile;
}
public IEnumerable<(string, string)> GetCommands()
{
if (!string.IsNullOrEmpty(_channel))
{
if (!string.IsNullOrEmpty(_profile)
&& !string.Equals(_profile, "native", StringComparison.OrdinalIgnoreCase))
{
yield return ("vchannel", $"{_channel} transcode={_profile}");
}
else
{
yield return ("vchannel", _channel);
}
}
}
}
}

View File

@@ -36,7 +36,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerApplicationHost _appHost;
private readonly ISocketFactory _socketFactory;
private readonly INetworkManager _networkManager;
private readonly IStreamHelper _streamHelper;
private readonly JsonSerializerOptions _jsonOptions;
@@ -50,7 +49,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
IHttpClientFactory httpClientFactory,
IServerApplicationHost appHost,
ISocketFactory socketFactory,
INetworkManager networkManager,
IStreamHelper streamHelper,
IMemoryCache memoryCache)
: base(config, logger, fileSystem, memoryCache)
@@ -58,7 +56,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
_httpClientFactory = httpClientFactory;
_appHost = appHost;
_socketFactory = socketFactory;
_networkManager = networkManager;
_streamHelper = streamHelper;
_jsonOptions = JsonDefaults.Options;
@@ -70,7 +67,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
protected override string ChannelIdPrefix => "hdhr_";
private string GetChannelId(TunerHostInfo info, Channels i)
private string GetChannelId(Channels i)
=> ChannelIdPrefix + i.GuideNumber;
internal async Task<List<Channels>> GetLineup(TunerHostInfo info, CancellationToken cancellationToken)
@@ -90,22 +87,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return lineup.Where(i => !i.DRM).ToList();
}
private class HdHomerunChannelInfo : ChannelInfo
protected override async Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken)
{
public bool IsLegacyTuner { get; set; }
}
protected override async Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo info, CancellationToken cancellationToken)
{
var lineup = await GetLineup(info, cancellationToken).ConfigureAwait(false);
var lineup = await GetLineup(tuner, cancellationToken).ConfigureAwait(false);
return lineup.Select(i => new HdHomerunChannelInfo
{
Name = i.GuideName,
Number = i.GuideNumber,
Id = GetChannelId(info, i),
Id = GetChannelId(i),
IsFavorite = i.Favorite,
TunerHostId = info.Id,
TunerHostId = tuner.Id,
IsHD = i.HD,
AudioCodec = i.AudioCodec,
VideoCodec = i.VideoCodec,
@@ -255,7 +247,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
var tuners = new List<LiveTvTunerInfo>();
var tuners = new List<LiveTvTunerInfo>(model.TunerCount);
var uri = new Uri(GetApiUrl(info));
@@ -264,10 +256,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
// Legacy HdHomeruns are IPv4 only
var ipInfo = IPAddress.Parse(uri.Host);
for (int i = 0; i < model.TunerCount; ++i)
for (int i = 0; i < model.TunerCount; i++)
{
var name = string.Format(CultureInfo.InvariantCulture, "Tuner {0}", i + 1);
var currentChannel = "none"; // @todo Get current channel and map back to Station Id
var currentChannel = "none"; // TODO: Get current channel and map back to Station Id
var isAvailable = await manager.CheckTunerAvailability(ipInfo, i, cancellationToken).ConfigureAwait(false);
var status = isAvailable ? LiveTvTunerStatus.Available : LiveTvTunerStatus.LiveTv;
tuners.Add(new LiveTvTunerInfo
@@ -455,28 +447,28 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
Path = url,
Protocol = MediaProtocol.Udp,
MediaStreams = new List<MediaStream>
{
new MediaStream
{
Type = MediaStreamType.Video,
// Set the index to -1 because we don't know the exact index of the video stream within the container
Index = -1,
IsInterlaced = isInterlaced,
Codec = videoCodec,
Width = width,
Height = height,
BitRate = videoBitrate,
NalLengthSize = nal
},
new MediaStream
{
Type = MediaStreamType.Audio,
// Set the index to -1 because we don't know the exact index of the audio stream within the container
Index = -1,
Codec = audioCodec,
BitRate = audioBitrate
}
},
{
new MediaStream
{
Type = MediaStreamType.Video,
// Set the index to -1 because we don't know the exact index of the video stream within the container
Index = -1,
IsInterlaced = isInterlaced,
Codec = videoCodec,
Width = width,
Height = height,
BitRate = videoBitrate,
NalLengthSize = nal
},
new MediaStream
{
Type = MediaStreamType.Audio,
// Set the index to -1 because we don't know the exact index of the audio stream within the container
Index = -1,
Codec = audioCodec,
BitRate = audioBitrate
}
},
RequiresOpening = true,
RequiresClosing = true,
BufferMs = 0,
@@ -496,57 +488,53 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return mediaSource;
}
protected override async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo info, ChannelInfo channelInfo, CancellationToken cancellationToken)
protected override async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo tuner, ChannelInfo channel, CancellationToken cancellationToken)
{
var list = new List<MediaSourceInfo>();
var channelId = channelInfo.Id;
var channelId = channel.Id;
var hdhrId = GetHdHrIdFromChannelId(channelId);
var hdHomerunChannelInfo = channelInfo as HdHomerunChannelInfo;
var isLegacyTuner = hdHomerunChannelInfo != null && hdHomerunChannelInfo.IsLegacyTuner;
if (isLegacyTuner)
if (channel is HdHomerunChannelInfo hdHomerunChannelInfo && hdHomerunChannelInfo.IsLegacyTuner)
{
list.Add(GetMediaSource(info, hdhrId, channelInfo, "native"));
list.Add(GetMediaSource(tuner, hdhrId, channel, "native"));
}
else
{
var modelInfo = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
var modelInfo = await GetModelInfo(tuner, false, cancellationToken).ConfigureAwait(false);
if (modelInfo != null && modelInfo.SupportsTranscoding)
{
if (info.AllowHWTranscoding)
if (tuner.AllowHWTranscoding)
{
list.Add(GetMediaSource(info, hdhrId, channelInfo, "heavy"));
list.Add(GetMediaSource(tuner, hdhrId, channel, "heavy"));
list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet540"));
list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet480"));
list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet360"));
list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet240"));
list.Add(GetMediaSource(info, hdhrId, channelInfo, "mobile"));
list.Add(GetMediaSource(tuner, hdhrId, channel, "internet540"));
list.Add(GetMediaSource(tuner, hdhrId, channel, "internet480"));
list.Add(GetMediaSource(tuner, hdhrId, channel, "internet360"));
list.Add(GetMediaSource(tuner, hdhrId, channel, "internet240"));
list.Add(GetMediaSource(tuner, hdhrId, channel, "mobile"));
}
list.Add(GetMediaSource(info, hdhrId, channelInfo, "native"));
list.Add(GetMediaSource(tuner, hdhrId, channel, "native"));
}
if (list.Count == 0)
{
list.Add(GetMediaSource(info, hdhrId, channelInfo, "native"));
list.Add(GetMediaSource(tuner, hdhrId, channel, "native"));
}
}
return list;
}
protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo info, ChannelInfo channelInfo, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo tunerHost, ChannelInfo channel, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
{
var tunerCount = info.TunerCount;
var tunerCount = tunerHost.TunerCount;
if (tunerCount > 0)
{
var tunerHostId = info.Id;
var tunerHostId = tunerHost.Id;
var liveStreams = currentLiveStreams.Where(i => string.Equals(i.TunerHostId, tunerHostId, StringComparison.OrdinalIgnoreCase));
if (liveStreams.Count() >= tunerCount)
@@ -555,28 +543,28 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}
}
var profile = streamId.Split('_')[0];
var profile = streamId.AsSpan().LeftPart('_').ToString();
Logger.LogInformation("GetChannelStream: channel id: {0}. stream id: {1} profile: {2}", channelInfo.Id, streamId, profile);
Logger.LogInformation("GetChannelStream: channel id: {0}. stream id: {1} profile: {2}", channel.Id, streamId, profile);
var hdhrId = GetHdHrIdFromChannelId(channelInfo.Id);
var hdhrId = GetHdHrIdFromChannelId(channel.Id);
var hdhomerunChannel = channelInfo as HdHomerunChannelInfo;
var hdhomerunChannel = channel as HdHomerunChannelInfo;
var modelInfo = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
var modelInfo = await GetModelInfo(tunerHost, false, cancellationToken).ConfigureAwait(false);
if (!modelInfo.SupportsTranscoding)
{
profile = "native";
}
var mediaSource = GetMediaSource(info, hdhrId, channelInfo, profile);
var mediaSource = GetMediaSource(tunerHost, hdhrId, channel, profile);
if (hdhomerunChannel != null && hdhomerunChannel.IsLegacyTuner)
{
return new HdHomerunUdpStream(
mediaSource,
info,
tunerHost,
streamId,
new LegacyHdHomerunChannelCommands(hdhomerunChannel.Path),
modelInfo.TunerCount,
@@ -592,7 +580,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
mediaSource.Protocol = MediaProtocol.Http;
var httpUrl = channelInfo.Path;
var httpUrl = channel.Path;
// If raw was used, the tuner doesn't support params
if (!string.IsNullOrWhiteSpace(profile) && !string.Equals(profile, "native", StringComparison.OrdinalIgnoreCase))
@@ -604,7 +592,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return new SharedHttpStream(
mediaSource,
info,
tunerHost,
streamId,
FileSystem,
_httpClientFactory,
@@ -616,7 +604,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return new HdHomerunUdpStream(
mediaSource,
info,
tunerHost,
streamId,
new HdHomerunChannelCommands(hdhomerunChannel.Number, profile),
modelInfo.TunerCount,
@@ -722,5 +710,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return hostInfo;
}
private class HdHomerunChannelInfo : ChannelInfo
{
public bool IsLegacyTuner { get; set; }
}
}
}

View File

@@ -5,12 +5,10 @@
using System;
using System.Buffers;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Globalization;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common;
@@ -18,70 +16,6 @@ using MediaBrowser.Controller.LiveTv;
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
public interface IHdHomerunChannelCommands
{
IEnumerable<(string, string)> GetCommands();
}
public class LegacyHdHomerunChannelCommands : IHdHomerunChannelCommands
{
private string _channel;
private string _program;
public LegacyHdHomerunChannelCommands(string url)
{
// parse url for channel and program
var regExp = new Regex(@"\/ch([0-9]+)-?([0-9]*)");
var match = regExp.Match(url);
if (match.Success)
{
_channel = match.Groups[1].Value;
_program = match.Groups[2].Value;
}
}
public IEnumerable<(string, string)> GetCommands()
{
if (!string.IsNullOrEmpty(_channel))
{
yield return ("channel", _channel);
}
if (!string.IsNullOrEmpty(_program))
{
yield return ("program", _program);
}
}
}
public class HdHomerunChannelCommands : IHdHomerunChannelCommands
{
private string _channel;
private string _profile;
public HdHomerunChannelCommands(string channel, string profile)
{
_channel = channel;
_profile = profile;
}
public IEnumerable<(string, string)> GetCommands()
{
if (!string.IsNullOrEmpty(_channel))
{
if (!string.IsNullOrEmpty(_profile)
&& !string.Equals(_profile, "native", StringComparison.OrdinalIgnoreCase))
{
yield return ("vchannel", $"{_channel} transcode={_profile}");
}
else
{
yield return ("vchannel", _channel);
}
}
}
}
public sealed class HdHomerunManager : IDisposable
{
public const int HdHomeRunPort = 65001;

View File

@@ -101,7 +101,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}
}
if (localAddress.IsIPv4MappedToIPv6) {
if (localAddress.IsIPv4MappedToIPv6)
{
localAddress = localAddress.MapToIPv4();
}
@@ -156,11 +157,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
await taskCompletionSource.Task.ConfigureAwait(false);
}
public string GetFilePath()
{
return TempFilePath;
}
private async Task StartStreaming(UdpClient udpClient, HdHomerunManager hdHomerunManager, IPAddress remoteAddress, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
{
using (udpClient)
@@ -184,7 +180,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
EnableStreamSharing = false;
}
await DeleteTempFiles(new List<string> { TempFilePath }).ConfigureAwait(false);
await DeleteTempFiles(TempFilePath).ConfigureAwait(false);
}
private async Task CopyTo(UdpClient udpClient, string file, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
@@ -201,7 +197,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
cancellationToken,
timeOutSource.Token))
{
var resTask = udpClient.ReceiveAsync();
var resTask = udpClient.ReceiveAsync(linkedSource.Token).AsTask();
if (await Task.WhenAny(resTask, Task.Delay(30000, linkedSource.Token)).ConfigureAwait(false) != resTask)
{
resTask.Dispose();

View File

@@ -0,0 +1,11 @@
#pragma warning disable CS1591
using System.Collections.Generic;
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
public interface IHdHomerunChannelCommands
{
IEnumerable<(string, string)> GetCommands();
}
}

View File

@@ -0,0 +1,38 @@
#pragma warning disable CS1591
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
public class LegacyHdHomerunChannelCommands : IHdHomerunChannelCommands
{
private string? _channel;
private string? _program;
public LegacyHdHomerunChannelCommands(string url)
{
// parse url for channel and program
var regExp = new Regex(@"\/ch([0-9]+)-?([0-9]*)");
var match = regExp.Match(url);
if (match.Success)
{
_channel = match.Groups[1].Value;
_program = match.Groups[2].Value;
}
}
public IEnumerable<(string, string)> GetCommands()
{
if (!string.IsNullOrEmpty(_channel))
{
yield return ("channel", _channel);
}
if (!string.IsNullOrEmpty(_program))
{
yield return ("program", _program);
}
}
}
}

View File

@@ -3,10 +3,8 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
@@ -22,14 +20,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
private readonly IConfigurationManager _configurationManager;
protected readonly IFileSystem FileSystem;
protected readonly IStreamHelper StreamHelper;
protected string TempFilePath;
protected readonly ILogger Logger;
protected readonly CancellationTokenSource LiveStreamCancellationTokenSource = new CancellationTokenSource();
public LiveStream(
MediaSourceInfo mediaSource,
TunerHostInfo tuner,
@@ -57,7 +47,15 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
SetTempFilePath("ts");
}
protected virtual int EmptyReadLimit => 1000;
protected IFileSystem FileSystem { get; }
protected IStreamHelper StreamHelper { get; }
protected ILogger Logger { get; }
protected CancellationTokenSource LiveStreamCancellationTokenSource { get; } = new CancellationTokenSource();
protected string TempFilePath { get; set; }
public MediaSourceInfo OriginalMediaSource { get; set; }
@@ -97,123 +95,50 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return Task.CompletedTask;
}
protected FileStream GetInputStream(string path, bool allowAsyncFileRead)
public Stream GetStream()
{
var stream = GetInputStream(TempFilePath);
bool seekFile = (DateTime.UtcNow - DateOpened).TotalSeconds > 10;
if (seekFile)
{
TrySeek(stream, -20000);
}
return stream;
}
protected FileStream GetInputStream(string path)
=> new FileStream(
path,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite,
IODefaults.FileStreamBufferSize,
allowAsyncFileRead ? FileOptions.SequentialScan | FileOptions.Asynchronous : FileOptions.SequentialScan);
FileOptions.SequentialScan | FileOptions.Asynchronous);
public Task DeleteTempFiles()
{
return DeleteTempFiles(GetStreamFilePaths());
}
protected async Task DeleteTempFiles(IEnumerable<string> paths, int retryCount = 0)
protected async Task DeleteTempFiles(string path, int retryCount = 0)
{
if (retryCount == 0)
{
Logger.LogInformation("Deleting temp files {0}", paths);
Logger.LogInformation("Deleting temp file {FilePath}", path);
}
var failedFiles = new List<string>();
foreach (var path in paths)
try
{
if (!File.Exists(path))
FileSystem.DeleteFile(path);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error deleting file {FilePath}", path);
if (retryCount <= 40)
{
continue;
}
try
{
FileSystem.DeleteFile(path);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error deleting file {path}", path);
failedFiles.Add(path);
await Task.Delay(500).ConfigureAwait(false);
await DeleteTempFiles(path, retryCount + 1).ConfigureAwait(false);
}
}
if (failedFiles.Count > 0 && retryCount <= 40)
{
await Task.Delay(500).ConfigureAwait(false);
await DeleteTempFiles(failedFiles, retryCount + 1).ConfigureAwait(false);
}
}
protected virtual List<string> GetStreamFilePaths()
{
return new List<string> { TempFilePath };
}
public async Task CopyToAsync(Stream stream, CancellationToken cancellationToken)
{
using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, LiveStreamCancellationTokenSource.Token);
cancellationToken = linkedCancellationTokenSource.Token;
// use non-async filestream on windows along with read due to https://github.com/dotnet/corefx/issues/6039
var allowAsync = Environment.OSVersion.Platform != PlatformID.Win32NT;
bool seekFile = (DateTime.UtcNow - DateOpened).TotalSeconds > 10;
var nextFileInfo = GetNextFile(null);
var nextFile = nextFileInfo.file;
var isLastFile = nextFileInfo.isLastFile;
while (!string.IsNullOrEmpty(nextFile))
{
var emptyReadLimit = isLastFile ? EmptyReadLimit : 1;
await CopyFile(nextFile, seekFile, emptyReadLimit, allowAsync, stream, cancellationToken).ConfigureAwait(false);
seekFile = false;
nextFileInfo = GetNextFile(nextFile);
nextFile = nextFileInfo.file;
isLastFile = nextFileInfo.isLastFile;
}
Logger.LogInformation("Live Stream ended.");
}
private (string file, bool isLastFile) GetNextFile(string currentFile)
{
var files = GetStreamFilePaths();
if (string.IsNullOrEmpty(currentFile))
{
return (files[^1], true);
}
var nextIndex = files.FindIndex(i => string.Equals(i, currentFile, StringComparison.OrdinalIgnoreCase)) + 1;
var isLastFile = nextIndex == files.Count - 1;
return (files.ElementAtOrDefault(nextIndex), isLastFile);
}
private async Task CopyFile(string path, bool seekFile, int emptyReadLimit, bool allowAsync, Stream stream, CancellationToken cancellationToken)
{
using (var inputStream = GetInputStream(path, allowAsync))
{
if (seekFile)
{
TrySeek(inputStream, -20000);
}
await StreamHelper.CopyToAsync(
inputStream,
stream,
IODefaults.CopyToBufferSize,
emptyReadLimit,
cancellationToken).ConfigureAwait(false);
}
}
private void TrySeek(FileStream stream, long offset)
private void TrySeek(Stream stream, long offset)
{
if (!stream.CanSeek)
{

View File

@@ -71,12 +71,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return ChannelIdPrefix + info.Url.GetMD5().ToString("N", CultureInfo.InvariantCulture);
}
protected override async Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo info, CancellationToken cancellationToken)
protected override async Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken)
{
var channelIdPrefix = GetFullChannelIdPrefix(info);
var channelIdPrefix = GetFullChannelIdPrefix(tuner);
return await new M3uParser(Logger, _httpClientFactory)
.Parse(info, channelIdPrefix, cancellationToken)
.Parse(tuner, channelIdPrefix, cancellationToken)
.ConfigureAwait(false);
}
@@ -96,13 +96,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return Task.FromResult(list);
}
protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo info, ChannelInfo channelInfo, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo tunerHost, ChannelInfo channel, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
{
var tunerCount = info.TunerCount;
var tunerCount = tunerHost.TunerCount;
if (tunerCount > 0)
{
var tunerHostId = info.Id;
var tunerHostId = tunerHost.Id;
var liveStreams = currentLiveStreams.Where(i => string.Equals(i.TunerHostId, tunerHostId, StringComparison.OrdinalIgnoreCase));
if (liveStreams.Count() >= tunerCount)
@@ -111,7 +111,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
}
}
var sources = await GetChannelStreamMediaSources(info, channelInfo, cancellationToken).ConfigureAwait(false);
var sources = await GetChannelStreamMediaSources(tunerHost, channel, cancellationToken).ConfigureAwait(false);
var mediaSource = sources[0];
@@ -121,11 +121,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
if (!_disallowedSharedStreamExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
{
return new SharedHttpStream(mediaSource, info, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper);
return new SharedHttpStream(mediaSource, tunerHost, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper);
}
}
return new LiveStream(mediaSource, info, FileSystem, Logger, Config, _streamHelper);
return new LiveStream(mediaSource, tunerHost, FileSystem, Logger, Config, _streamHelper);
}
public async Task Validate(TunerHostInfo info)
@@ -135,9 +135,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
}
}
protected override Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo info, ChannelInfo channelInfo, CancellationToken cancellationToken)
protected override Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo tuner, ChannelInfo channel, CancellationToken cancellationToken)
{
return Task.FromResult(new List<MediaSourceInfo> { CreateMediaSourceInfo(info, channelInfo) });
return Task.FromResult(new List<MediaSourceInfo> { CreateMediaSourceInfo(tuner, channel) });
}
protected virtual MediaSourceInfo CreateMediaSourceInfo(TunerHostInfo info, ChannelInfo channel)

View File

@@ -14,6 +14,7 @@ using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using Microsoft.Extensions.Logging;
@@ -50,7 +51,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
if (!info.Url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
return File.OpenRead(info.Url);
return AsyncFile.OpenRead(info.Url);
}
using var requestMessage = new HttpRequestMessage(HttpMethod.Get, info.Url);
@@ -237,7 +238,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
try
{
numberString = Path.GetFileNameWithoutExtension(mediaUrl.Split('/')[^1]);
numberString = Path.GetFileNameWithoutExtension(mediaUrl.AsSpan().RightPart('/')).ToString();
if (!IsValidChannelNumber(numberString))
{

View File

@@ -3,7 +3,6 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Net.Http;
@@ -55,39 +54,26 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
Directory.CreateDirectory(Path.GetDirectoryName(TempFilePath));
var typeName = GetType().Name;
Logger.LogInformation("Opening " + typeName + " Live stream from {0}", url);
Logger.LogInformation("Opening {StreamType} Live stream from {Url}", typeName, url);
// Response stream is disposed manually.
var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None)
.ConfigureAwait(false);
var extension = "ts";
var requiresRemux = false;
var contentType = response.Content.Headers.ContentType?.ToString() ?? string.Empty;
if (contentType.IndexOf("matroska", StringComparison.OrdinalIgnoreCase) != -1)
if (contentType.Contains("matroska", StringComparison.OrdinalIgnoreCase)
|| contentType.Contains("mp4", StringComparison.OrdinalIgnoreCase)
|| contentType.Contains("dash", StringComparison.OrdinalIgnoreCase)
|| contentType.Contains("mpegURL", StringComparison.OrdinalIgnoreCase)
|| contentType.Contains("text/", StringComparison.OrdinalIgnoreCase))
{
requiresRemux = true;
}
else if (contentType.IndexOf("mp4", StringComparison.OrdinalIgnoreCase) != -1 ||
contentType.IndexOf("dash", StringComparison.OrdinalIgnoreCase) != -1 ||
contentType.IndexOf("mpegURL", StringComparison.OrdinalIgnoreCase) != -1 ||
contentType.IndexOf("text/", StringComparison.OrdinalIgnoreCase) != -1)
{
requiresRemux = true;
// Close the stream without any sharing features
response.Dispose();
return;
}
// Close the stream without any sharing features
if (requiresRemux)
{
using (response)
{
return;
}
}
SetTempFilePath(extension);
SetTempFilePath("ts");
var taskCompletionSource = new TaskCompletionSource<bool>();
@@ -117,49 +103,46 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
if (!taskCompletionSource.Task.Result)
{
Logger.LogWarning("Zero bytes copied from stream {0} to {1} but no exception raised", GetType().Name, TempFilePath);
Logger.LogWarning("Zero bytes copied from stream {StreamType} to {FilePath} but no exception raised", GetType().Name, TempFilePath);
throw new EndOfStreamException(string.Format(CultureInfo.InvariantCulture, "Zero bytes copied from stream {0}", GetType().Name));
}
}
public string GetFilePath()
{
return TempFilePath;
}
private Task StartStreaming(HttpResponseMessage response, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
{
return Task.Run(async () =>
{
try
return Task.Run(
async () =>
{
Logger.LogInformation("Beginning {0} stream to {1}", GetType().Name, TempFilePath);
using var message = response;
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read);
await StreamHelper.CopyToAsync(
stream,
fileStream,
IODefaults.CopyToBufferSize,
() => Resolve(openTaskCompletionSource),
cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException ex)
{
Logger.LogInformation("Copying of {0} to {1} was canceled", GetType().Name, TempFilePath);
openTaskCompletionSource.TrySetException(ex);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error copying live stream {0} to {1}.", GetType().Name, TempFilePath);
openTaskCompletionSource.TrySetException(ex);
}
try
{
Logger.LogInformation("Beginning {StreamType} stream to {FilePath}", GetType().Name, TempFilePath);
using var message = response;
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
await StreamHelper.CopyToAsync(
stream,
fileStream,
IODefaults.CopyToBufferSize,
() => Resolve(openTaskCompletionSource),
cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException ex)
{
Logger.LogInformation("Copying of {StreamType} to {FilePath} was canceled", GetType().Name, TempFilePath);
openTaskCompletionSource.TrySetException(ex);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error copying live stream {StreamType} to {FilePath}", GetType().Name, TempFilePath);
openTaskCompletionSource.TrySetException(ex);
}
openTaskCompletionSource.TrySetResult(false);
openTaskCompletionSource.TrySetResult(false);
EnableStreamSharing = false;
await DeleteTempFiles(new List<string> { TempFilePath }).ConfigureAwait(false);
}, CancellationToken.None);
EnableStreamSharing = false;
await DeleteTempFiles(TempFilePath).ConfigureAwait(false);
},
CancellationToken.None);
}
private void Resolve(TaskCompletionSource<bool> openTaskCompletionSource)

View File

@@ -1,5 +1,5 @@
{
"Albums": "البومات",
"Albums": "ألبومات",
"AppDeviceValues": "تطبيق: {0}, جهاز: {1}",
"Application": "تطبيق",
"Artists": "الفنانين",
@@ -8,7 +8,7 @@
"CameraImageUploadedFrom": "صورة كاميرا جديدة تم رفعها من {0}",
"Channels": "القنوات",
"ChapterNameValue": "الفصل {0}",
"Collections": "مجموعات",
"Collections": "التجميعات",
"DeviceOfflineWithName": "قُطِع الاتصال ب{0}",
"DeviceOnlineWithName": "{0} متصل",
"FailedLoginAttemptWithUserName": "عملية تسجيل الدخول فشلت من {0}",

View File

@@ -15,7 +15,7 @@
"Favorites": "Preferits",
"Folders": "Carpetes",
"Genres": "Gèneres",
"HeaderAlbumArtists": "Artistes del Àlbum",
"HeaderAlbumArtists": "Àlbum de l'artista",
"HeaderContinueWatching": "Continua Veient",
"HeaderFavoriteAlbums": "Àlbums Preferits",
"HeaderFavoriteArtists": "Artistes Predilectes",

View File

@@ -25,7 +25,7 @@
"HeaderLiveTV": "Televize",
"HeaderNextUp": "Nadcházející",
"HeaderRecordingGroups": "Skupiny nahrávek",
"HomeVideos": "Domáci videa",
"HomeVideos": "Domácí videa",
"Inherit": "Zdědit",
"ItemAddedWithName": "{0} byl přidán do knihovny",
"ItemRemovedWithName": "{0} byl odstraněn z knihovny",

View File

@@ -118,5 +118,7 @@
"TaskCleanActivityLogDescription": "Éfface les entrées du journal plus anciennes que l'âge configuré.",
"TaskCleanActivityLog": "Nettoyer le journal d'activité",
"Undefined": "Indéfini",
"Forced": "Forcé"
"Forced": "Forcé",
"TaskOptimizeDatabaseDescription": "Compacte la base de données et tronque l'espace libre. Lancer cette tâche après avoir scanné la bibliothèque ou faire d'autres changements impliquant des modifications de la base peuvent ameliorer les performances.",
"TaskOptimizeDatabase": "Optimiser la base de données"
}

View File

@@ -105,8 +105,8 @@
"TaskRefreshPeople": "Rafraîchir les acteurs",
"TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.",
"TaskCleanLogs": "Nettoyer le répertoire des journaux",
"TaskRefreshLibraryDescription": "Scanne toute les bibliothèques pour trouver les nouveaux fichiers et rafraîchit les métadonnées.",
"TaskRefreshLibrary": "Scanner toutes les Bibliothèques",
"TaskRefreshLibraryDescription": "Scanne votre médiathèque pour trouver les nouveaux fichiers et rafraîchit les métadonnées.",
"TaskRefreshLibrary": "Scanner la médiathèque",
"TaskRefreshChapterImagesDescription": "Crée des vignettes pour les vidéos ayant des chapitres.",
"TaskRefreshChapterImages": "Extraire les images de chapitre",
"TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.",

View File

@@ -48,7 +48,7 @@
"HeaderFavoriteArtists": "Artistas Favoritos",
"HeaderFavoriteAlbums": "Álbunes Favoritos",
"HeaderContinueWatching": "Seguir mirando",
"HeaderAlbumArtists": "Artistas de Album",
"HeaderAlbumArtists": "Artistas do Album",
"Genres": "Xéneros",
"Forced": "Forzado",
"Folders": "Cartafoles",
@@ -117,5 +117,7 @@
"UserPolicyUpdatedWithName": "A política de usuario foi actualizada para {0}",
"UserPasswordChangedWithName": "Cambiouse o contrasinal para o usuario {0}",
"UserOnlineFromDevice": "{0} está en liña desde {1}",
"UserOfflineFromDevice": "{0} desconectouse desde {1}"
"UserOfflineFromDevice": "{0} desconectouse desde {1}",
"TaskOptimizeDatabaseDescription": "Compacta e libera o espazo libre da base de datos. Executar esta tarefa logo de realizar mudanzas que impliquen modificacións da base de datos ou despois de escanear a biblioteca pode traer mellorías de desempeño.",
"TaskOptimizeDatabase": "Optimizar base de datos"
}

View File

@@ -15,7 +15,7 @@
"Favorites": "Favoriti",
"Folders": "Mape",
"Genres": "Žanrovi",
"HeaderAlbumArtists": "Izvođači na albumu",
"HeaderAlbumArtists": "Album od izvođača",
"HeaderContinueWatching": "Nastavi gledati",
"HeaderFavoriteAlbums": "Omiljeni albumi",
"HeaderFavoriteArtists": "Omiljeni izvođači",

View File

@@ -15,7 +15,7 @@
"Favorites": "Favorieten",
"Folders": "Mappen",
"Genres": "Genres",
"HeaderAlbumArtists": "Albumartiesten",
"HeaderAlbumArtists": "Artiests Album",
"HeaderContinueWatching": "Kijken hervatten",
"HeaderFavoriteAlbums": "Favoriete albums",
"HeaderFavoriteArtists": "Favoriete artiesten",

View File

@@ -1,6 +1,6 @@
{
"HeaderLiveTV": "TV em Directo",
"Collections": "Colecções",
"HeaderLiveTV": "TV Ao Vivo",
"Collections": "Coleções",
"Books": "Livros",
"Artists": "Artistas",
"Albums": "Álbuns",
@@ -10,29 +10,29 @@
"HeaderFavoriteAlbums": "Álbuns Favoritos",
"HeaderFavoriteEpisodes": "Episódios Favoritos",
"HeaderFavoriteShows": "Séries Favoritas",
"HeaderContinueWatching": "Continuar a Assistir",
"HeaderContinueWatching": "Continuar assistindo",
"HeaderAlbumArtists": "Artistas do Álbum",
"Genres": "Géneros",
"Folders": "Directórios",
"Genres": "Gêneros",
"Folders": "Diretórios",
"Favorites": "Favoritos",
"Channels": "Canais",
"UserDownloadingItemWithValues": "{0} está a ser transferido {1}",
"UserDownloadingItemWithValues": "{0} está sendo baixado {1}",
"VersionNumber": "Versão {0}",
"ValueHasBeenAddedToLibrary": "{0} foi adicionado à sua biblioteca multimédia",
"UserStoppedPlayingItemWithValues": "{0} terminou a reprodução de {1} em {2}",
"UserStartedPlayingItemWithValues": "{0} está a reproduzir {1} em {2}",
"UserPolicyUpdatedWithName": "A política do utilizador {0} foi alterada",
"UserPasswordChangedWithName": "A palavra-passe do utilizador {0} foi alterada",
"UserOnlineFromDevice": "{0} ligou-se a partir de {1}",
"UserStartedPlayingItemWithValues": "{0} está reproduzindo {1} em {2}",
"UserPolicyUpdatedWithName": "A política do usuário {0} foi alterada",
"UserPasswordChangedWithName": "A senha do usuário {0} foi alterada",
"UserOnlineFromDevice": "{0} está online a partir de {1}",
"UserOfflineFromDevice": "{0} desconectou-se a partir de {1}",
"UserLockedOutWithName": "O utilizador {0} foi bloqueado",
"UserDeletedWithName": "O utilizador {0} foi removido",
"UserCreatedWithName": "O utilizador {0} foi criado",
"User": "Utilizador",
"UserLockedOutWithName": "O usuário {0} foi bloqueado",
"UserDeletedWithName": "O usuário {0} foi removido",
"UserCreatedWithName": "O usuário {0} foi criado",
"User": "Usuário",
"TvShows": "Séries",
"System": "Sistema",
"SubtitleDownloadFailureFromForItem": "Falha na transferência de legendas de {0} para {1}",
"StartupEmbyServerIsLoading": "O servidor Jellyfin está a iniciar. Tente novamente dentro de momentos.",
"StartupEmbyServerIsLoading": "O servidor Jellyfin está iniciando. Tente novamente dentro de momentos.",
"ServerNameNeedsToBeRestarted": "{0} necessita ser reiniciado",
"ScheduledTaskStartedWithName": "{0} iniciou",
"ScheduledTaskFailedWithName": "{0} falhou",
@@ -43,38 +43,38 @@
"Plugin": "Plugin",
"NotificationOptionVideoPlaybackStopped": "Reprodução de vídeo parada",
"NotificationOptionVideoPlayback": "Reprodução de vídeo iniciada",
"NotificationOptionUserLockedOut": "Utilizador bloqueado",
"NotificationOptionTaskFailed": "Falha em tarefa agendada",
"NotificationOptionUserLockedOut": "Usuário bloqueado",
"NotificationOptionTaskFailed": "Falha na tarefa agendada",
"NotificationOptionServerRestartRequired": "É necessário reiniciar o servidor",
"NotificationOptionPluginUpdateInstalled": "Plugin actualizado",
"NotificationOptionPluginUpdateInstalled": "Plugin atualizado",
"NotificationOptionPluginUninstalled": "Plugin desinstalado",
"NotificationOptionPluginInstalled": "Plugin instalado",
"NotificationOptionPluginError": "Falha no plugin",
"NotificationOptionNewLibraryContent": "Novo conteúdo adicionado",
"NotificationOptionInstallationFailed": "Falha de instalação",
"NotificationOptionCameraImageUploaded": "Imagem de câmara enviada",
"NotificationOptionCameraImageUploaded": "Imagem de câmera enviada",
"NotificationOptionAudioPlaybackStopped": "Reprodução Parada",
"NotificationOptionAudioPlayback": "Reprodução Iniciada",
"NotificationOptionApplicationUpdateInstalled": "A actualização da aplicação foi instalada",
"NotificationOptionApplicationUpdateAvailable": "Uma actualização da aplicação está disponível",
"NewVersionIsAvailable": "Uma nova versão do servidor Jellyfin está disponível para transferência.",
"NotificationOptionApplicationUpdateInstalled": "A atualização do aplicativo foi instalada",
"NotificationOptionApplicationUpdateAvailable": "Uma atualização do aplicativo está disponível",
"NewVersionIsAvailable": "Uma nova versão do servidor Jellyfin está disponível para download.",
"NameSeasonUnknown": "Temporada Desconhecida",
"NameSeasonNumber": "Temporada {0}",
"NameInstallFailed": "Falha na instalação de {0}",
"MusicVideos": "Videoclipes",
"Music": "Música",
"MixedContent": "Conteúdo Misto",
"MessageServerConfigurationUpdated": "A configuração do servidor foi actualizada",
"MessageNamedServerConfigurationUpdatedWithValue": "As configurações do servidor na secção {0} foram atualizadas",
"MixedContent": "Conteúdo diverso",
"MessageServerConfigurationUpdated": "A configuração do servidor foi atualizada",
"MessageNamedServerConfigurationUpdatedWithValue": "As configurações do servidor na seção {0} foram atualizadas",
"MessageApplicationUpdatedTo": "O servidor Jellyfin foi atualizado para a versão {0}",
"MessageApplicationUpdated": "O servidor Jellyfin foi actualizado",
"MessageApplicationUpdated": "O servidor Jellyfin foi atualizado",
"Latest": "Mais Recente",
"LabelRunningTimeValue": "Duração: {0}",
"LabelIpAddressValue": "Endereço de IP: {0}",
"ItemRemovedWithName": "{0} foi removido da biblioteca",
"ItemAddedWithName": "{0} foi adicionado à biblioteca",
"Inherit": "Herdar",
"HomeVideos": "Vídeos Caseiros",
"HomeVideos": "Vídeos principais",
"HeaderRecordingGroups": "Grupos de Gravação",
"ValueSpecialEpisodeName": "Episódio Especial - {0}",
"Sync": "Sincronização",
@@ -83,22 +83,22 @@
"Playlists": "Listas de Reprodução",
"Photos": "Fotografias",
"Movies": "Filmes",
"FailedLoginAttemptWithUserName": "Tentativa de ligação falhada a partir de {0}",
"DeviceOnlineWithName": "{0} está connectado",
"FailedLoginAttemptWithUserName": "Tentativa falha de login a partir de {0}",
"DeviceOnlineWithName": "{0} está conectado",
"DeviceOfflineWithName": "{0} desconectou-se",
"ChapterNameValue": "Capítulo {0}",
"CameraImageUploadedFrom": "Uma nova imagem da câmara foi enviada a partir de {0}",
"AuthenticationSucceededWithUserName": "{0} autenticado com sucesso",
"Application": "Aplicação",
"AppDeviceValues": "Aplicação {0}, Dispositivo: {1}",
"Application": "Aplicativo",
"AppDeviceValues": "Aplicativo {0}, Dispositivo: {1}",
"TaskCleanCache": "Limpar Diretório de Cache",
"TasksApplicationCategory": "Aplicação",
"TasksApplicationCategory": "Aplicativo",
"TasksLibraryCategory": "Biblioteca",
"TasksMaintenanceCategory": "Manutenção",
"TaskRefreshChannels": "Atualizar Canais",
"TaskUpdatePlugins": "Atualizar Plugins",
"TaskCleanLogsDescription": "Deletar arquivos de log que existe a mais de {0} dias.",
"TaskCleanLogs": "Limpar diretório de log",
"TaskCleanLogs": "Limpar diretório de logs",
"TaskRefreshLibrary": "Escanear biblioteca de mídias",
"TaskRefreshChapterImagesDescription": "Cria miniaturas para vídeos que têm capítulos.",
"TaskCleanCacheDescription": "Apaga ficheiros em cache que já não são usados pelo sistema.",
@@ -109,14 +109,15 @@
"TaskRefreshChannelsDescription": "Atualiza as informações do canal da Internet.",
"TaskCleanTranscodeDescription": "Apagar os ficheiros com mais de um dia, de Transcode.",
"TaskCleanTranscode": "Limpar o diretório de Transcode",
"TaskUpdatePluginsDescription": "Download e instala as atualizações para plug-ins configurados para atualização automática.",
"TaskUpdatePluginsDescription": "Baixa e instala as atualizações para plug-ins configurados para atualização automática.",
"TaskRefreshPeopleDescription": "Atualiza os metadados para atores e diretores na tua biblioteca de media.",
"TaskRefreshPeople": "Atualizar pessoas",
"TaskRefreshLibraryDescription": "Pesquisa a tua biblioteca de media por novos ficheiros e atualiza os metadados.",
"TaskCleanActivityLog": "Limpar registo de atividade",
"TaskRefreshLibraryDescription": "Pesquisa sua biblioteca de media por novos arquivos e atualiza os metadados.",
"TaskCleanActivityLog": "Limpar registro de atividade",
"Undefined": "Indefinido",
"Forced": "Forçado",
"Default": "Predefinição",
"TaskCleanActivityLogDescription": "Apaga itens no registro com idade acima do que é configurado.",
"TaskOptimizeDatabase": "Otimizar base de dados"
"TaskOptimizeDatabase": "Otimizar base de dados",
"TaskOptimizeDatabaseDescription": "Base de dados compacta e corta espaço livre. A execução desta tarefa depois de digitalizar a biblioteca ou de fazer outras alterações que impliquem modificações na base de dados pode melhorar o desempenho."
}

View File

@@ -74,7 +74,7 @@
"NameSeasonUnknown": "Sezon i panjohur",
"NameSeasonNumber": "Sezoni {0}",
"NameInstallFailed": "Instalimi i {0} dështoi",
"MusicVideos": "Videot muzikore",
"MusicVideos": "Video muzikore",
"Music": "Muzikë",
"Movies": "Filmat",
"MixedContent": "Përmbajtje e përzier",
@@ -96,7 +96,7 @@
"HeaderFavoriteArtists": "Artistët e preferuar",
"HeaderFavoriteAlbums": "Albumet e preferuar",
"HeaderContinueWatching": "Vazhdo të shikosh",
"HeaderAlbumArtists": "Artistët e albumeve",
"HeaderAlbumArtists": "Artistët e Albumeve",
"Genres": "Zhanret",
"Folders": "Skedarët",
"Favorites": "Të preferuarat",

View File

@@ -90,7 +90,7 @@
"NameSeasonUnknown": "نامعلوم باب",
"NameSeasonNumber": "باب {0}",
"NameInstallFailed": "{0} تنصیب ناکام ہوگئی",
"MusicVideos": "موسیقی ویڈیو",
"MusicVideos": "ویڈیو موسیقی",
"Music": "موسیقی",
"MixedContent": "مخلوط مواد",
"MessageServerConfigurationUpdated": "سرور کو اپ ڈیٹ کر دیا گیا ہے",
@@ -99,18 +99,19 @@
"MessageApplicationUpdated": "جیلیفن سرور کو اپ ڈیٹ کر دیا گیا ہے",
"Latest": "تازہ ترین",
"LabelRunningTimeValue": "چلانے کی مدت",
"LabelIpAddressValue": "ای پی پتے {0}",
"LabelIpAddressValue": "آئ پی ایڈریس {0}",
"ItemRemovedWithName": "لائبریری سے ہٹا دیا گیا ھے",
"ItemAddedWithName": "[0} لائبریری میں شامل کیا گیا ھے",
"Inherit": "وراثت میں",
"HomeVideos": "ہوم ویڈیو",
"HeaderRecordingGroups": "ریکارڈنگ گروپس",
"FailedLoginAttemptWithUserName": "لاگن کئ کوشش ناکام {0}",
"FailedLoginAttemptWithUserName": "{0} سے لاگ ان کی ناکام کوشش",
"DeviceOnlineWithName": "{0} متصل ھو چکا ھے",
"DeviceOfflineWithName": "{0} منقطع ھو چکا ھے",
"ChapterNameValue": "باب",
"AuthenticationSucceededWithUserName": "{0} کامیابی کے ساتھ تصدیق ھوچکی ھے",
"CameraImageUploadedFrom": "ایک نئی کیمرہ تصویر اپ لوڈ کی گئی ہے {0}",
"Application": "پروگرام",
"AppDeviceValues": "پروگرام:{0}, آلہ:{1}"
"AppDeviceValues": "پروگرام:{0}, ڈیوائس:{1}",
"Forced": "جَبری"
}

View File

@@ -10,11 +10,11 @@
"Photos": "Ảnh",
"Playlists": "Danh sách phát",
"Shows": "Chương Trình TV",
"Songs": "Các Bài Hát",
"Songs": "Bài Hát",
"Sync": "Đồng Bộ",
"ValueSpecialEpisodeName": "Đặc Biệt - {0}",
"Albums": "Tuyển Tập",
"Artists": "Các Nghệ Sĩ",
"Artists": "Ca Sĩ",
"TaskDownloadMissingSubtitlesDescription": "Tìm kiếm phụ đề bị thiếu trên Internet dựa trên cấu hình dữ liệu mô tả.",
"TaskDownloadMissingSubtitles": "Tải Xuống Phụ Đề Bị Thiếu",
"TaskRefreshChannelsDescription": "Làm mới thông tin kênh internet.",
@@ -32,7 +32,7 @@
"TaskRefreshChapterImagesDescription": "Tạo hình thu nhỏ cho video có các phân cảnh.",
"TaskRefreshChapterImages": "Trích Xuất Ảnh Phân Cảnh",
"TaskCleanCacheDescription": "Xóa các tệp cache không còn cần thiết của hệ thống.",
"TaskCleanCache": "Làm Sạch Thư Mục Cache",
"TaskCleanCache": "Làm Sạch Thư Mục Bộ Nhớ Đệm",
"TasksChannelsCategory": "Kênh Internet",
"TasksApplicationCategory": "Ứng Dụng",
"TasksLibraryCategory": "Thư Viện",

View File

@@ -23,7 +23,6 @@ namespace Emby.Server.Implementations.MediaEncoder
{
public class EncodingManager : IEncodingManager
{
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private readonly IFileSystem _fileSystem;
private readonly ILogger<EncodingManager> _logger;
private readonly IMediaEncoder _encoder;
@@ -193,7 +192,7 @@ namespace Emby.Server.Implementations.MediaEncoder
private string GetChapterImagePath(Video video, long chapterPositionTicks)
{
var filename = video.DateModified.Ticks.ToString(_usCulture) + "_" + chapterPositionTicks.ToString(_usCulture) + ".jpg";
var filename = video.DateModified.Ticks.ToString(CultureInfo.InvariantCulture) + "_" + chapterPositionTicks.ToString(CultureInfo.InvariantCulture) + ".jpg";
return Path.Combine(GetChapterImagesPath(video), filename);
}

View File

@@ -11,6 +11,7 @@ namespace Emby.Server.Implementations.Net
{
public class SocketFactory : ISocketFactory
{
/// <inheritdoc />
public ISocket CreateUdpBroadcastSocket(int localPort)
{
if (localPort < 0)
@@ -35,11 +36,8 @@ namespace Emby.Server.Implementations.Net
}
}
/// <summary>
/// Creates a new UDP acceptSocket that is a member of the SSDP multicast local admin group and binds it to the specified local port.
/// </summary>
/// <returns>An implementation of the <see cref="ISocket"/> interface used by RSSDP components to perform acceptSocket operations.</returns>
public ISocket CreateSsdpUdpSocket(IPAddress localIpAddress, int localPort)
/// <inheritdoc />
public ISocket CreateSsdpUdpSocket(IPAddress localIp, int localPort)
{
if (localPort < 0)
{
@@ -53,8 +51,8 @@ namespace Emby.Server.Implementations.Net
retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, 4);
retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(IPAddress.Parse("239.255.255.250"), localIpAddress));
return new UdpSocket(retVal, localPort, localIpAddress);
retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(IPAddress.Parse("239.255.255.250"), localIp));
return new UdpSocket(retVal, localPort, localIp);
}
catch
{
@@ -64,13 +62,7 @@ namespace Emby.Server.Implementations.Net
}
}
/// <summary>
/// Creates a new UDP acceptSocket that is a member of the specified multicast IP address, and binds it to the specified local port.
/// </summary>
/// <param name="ipAddress">The multicast IP address to make the acceptSocket a member of.</param>
/// <param name="multicastTimeToLive">The multicast time to live value for the acceptSocket.</param>
/// <param name="localPort">The number of the local port to bind to.</param>
/// <returns></returns>
/// <inheritdoc />
public ISocket CreateUdpMulticastSocket(string ipAddress, int multicastTimeToLive, int localPort)
{
if (ipAddress == null)

View File

@@ -16,11 +16,7 @@ namespace Emby.Server.Implementations.Net
public sealed class UdpSocket : ISocket, IDisposable
{
private Socket _socket;
private readonly int _localPort;
private bool _disposed = false;
public Socket Socket => _socket;
private readonly SocketAsyncEventArgs _receiveSocketAsyncEventArgs = new SocketAsyncEventArgs()
{
@@ -32,6 +28,8 @@ namespace Emby.Server.Implementations.Net
SocketFlags = SocketFlags.None
};
private Socket _socket;
private bool _disposed = false;
private TaskCompletionSource<SocketReceiveResult> _currentReceiveTaskCompletionSource;
private TaskCompletionSource<int> _currentSendTaskCompletionSource;
@@ -64,6 +62,8 @@ namespace Emby.Server.Implementations.Net
InitReceiveSocketAsyncEventArgs();
}
public Socket Socket => _socket;
public IPAddress LocalIPAddress { get; }
private void InitReceiveSocketAsyncEventArgs()
@@ -191,7 +191,7 @@ namespace Emby.Server.Implementations.Net
return taskCompletion.Task;
}
public Task SendToAsync(byte[] buffer, int offset, int size, IPEndPoint endPoint, CancellationToken cancellationToken)
public Task SendToAsync(byte[] buffer, int offset, int bytes, IPEndPoint endPoint, CancellationToken cancellationToken)
{
ThrowIfDisposed();
@@ -214,7 +214,7 @@ namespace Emby.Server.Implementations.Net
}
};
var result = BeginSendTo(buffer, offset, size, endPoint, new AsyncCallback(callback), null);
var result = BeginSendTo(buffer, offset, bytes, endPoint, new AsyncCallback(callback), null);
if (result.CompletedSynchronously)
{

View File

@@ -17,6 +17,15 @@ namespace Emby.Server.Implementations.Playlists
Name = "Playlists";
}
[JsonIgnore]
public override bool IsHidden => true;
[JsonIgnore]
public override bool SupportsInheritedParentImages => false;
[JsonIgnore]
public override string CollectionType => MediaBrowser.Model.Entities.CollectionType.Playlists;
public override bool IsVisible(User user)
{
return base.IsVisible(user) && GetChildren(user, true).Any();
@@ -27,15 +36,6 @@ namespace Emby.Server.Implementations.Playlists
return base.GetEligibleChildrenForRecursiveChildren(user).OfType<Playlist>();
}
[JsonIgnore]
public override bool IsHidden => true;
[JsonIgnore]
public override bool SupportsInheritedParentImages => false;
[JsonIgnore]
public override string CollectionType => MediaBrowser.Model.Entities.CollectionType.Playlists;
protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
{
if (query.User == null)

View File

@@ -8,13 +8,14 @@ using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using MediaBrowser.Common;
using MediaBrowser.Common.Extensions;
using Jellyfin.Extensions.Json;
using Jellyfin.Extensions.Json.Converters;
using MediaBrowser.Common;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Updates;
using Microsoft.Extensions.DependencyInjection;
@@ -38,14 +39,6 @@ namespace Emby.Server.Implementations.Plugins
private IHttpClientFactory? _httpClientFactory;
private IHttpClientFactory HttpClientFactory
{
get
{
return _httpClientFactory ?? (_httpClientFactory = _appHost.Resolve<IHttpClientFactory>());
}
}
/// <summary>
/// Initializes a new instance of the <see cref="PluginManager"/> class.
/// </summary>
@@ -85,6 +78,14 @@ namespace Emby.Server.Implementations.Plugins
_plugins = Directory.Exists(_pluginsPath) ? DiscoverPlugins().ToList() : new List<LocalPlugin>();
}
private IHttpClientFactory HttpClientFactory
{
get
{
return _httpClientFactory ??= _appHost.Resolve<IHttpClientFactory>();
}
}
/// <summary>
/// Gets the Plugins.
/// </summary>
@@ -371,7 +372,7 @@ namespace Emby.Server.Implementations.Plugins
var url = new Uri(packageInfo.ImageUrl);
imagePath = Path.Join(path, url.Segments[^1]);
await using var fileStream = File.OpenWrite(imagePath);
await using var fileStream = AsyncFile.OpenWrite(imagePath);
try
{

View File

@@ -3,12 +3,13 @@ using System.Collections.Concurrent;
using System.Globalization;
using System.Linq;
using System.Security.Cryptography;
using System.Threading.Tasks;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.QuickConnect;
using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.QuickConnect;
using Microsoft.Extensions.Logging;
@@ -17,13 +18,8 @@ namespace Emby.Server.Implementations.QuickConnect
/// <summary>
/// Quick connect implementation.
/// </summary>
public class QuickConnectManager : IQuickConnect, IDisposable
public class QuickConnectManager : IQuickConnect
{
/// <summary>
/// The name of internal access tokens.
/// </summary>
private const string TokenName = "QuickConnect";
/// <summary>
/// The length of user facing codes.
/// </summary>
@@ -34,13 +30,12 @@ namespace Emby.Server.Implementations.QuickConnect
/// </summary>
private const int Timeout = 10;
private readonly RNGCryptoServiceProvider _rng = new();
private readonly ConcurrentDictionary<string, QuickConnectResult> _currentRequests = new();
private readonly ConcurrentDictionary<string, QuickConnectResult> _currentRequests = new ();
private readonly ConcurrentDictionary<string, (DateTime Timestamp, AuthenticationResult AuthenticationResult)> _authorizedSecrets = new ();
private readonly IServerConfigurationManager _config;
private readonly ILogger<QuickConnectManager> _logger;
private readonly IServerApplicationHost _appHost;
private readonly IAuthenticationRepository _authenticationRepository;
private readonly ISessionManager _sessionManager;
/// <summary>
/// Initializes a new instance of the <see cref="QuickConnectManager"/> class.
@@ -48,18 +43,15 @@ namespace Emby.Server.Implementations.QuickConnect
/// </summary>
/// <param name="config">Configuration.</param>
/// <param name="logger">Logger.</param>
/// <param name="appHost">Application host.</param>
/// <param name="authenticationRepository">Authentication repository.</param>
/// <param name="sessionManager">Session Manager.</param>
public QuickConnectManager(
IServerConfigurationManager config,
ILogger<QuickConnectManager> logger,
IServerApplicationHost appHost,
IAuthenticationRepository authenticationRepository)
ISessionManager sessionManager)
{
_config = config;
_logger = logger;
_appHost = appHost;
_authenticationRepository = authenticationRepository;
_sessionManager = sessionManager;
}
/// <inheritdoc />
@@ -77,14 +69,41 @@ namespace Emby.Server.Implementations.QuickConnect
}
/// <inheritdoc/>
public QuickConnectResult TryConnect()
public QuickConnectResult TryConnect(AuthorizationInfo authorizationInfo)
{
if (string.IsNullOrEmpty(authorizationInfo.DeviceId))
{
throw new ArgumentException(nameof(authorizationInfo.DeviceId) + " is required");
}
if (string.IsNullOrEmpty(authorizationInfo.Device))
{
throw new ArgumentException(nameof(authorizationInfo.Device) + " is required");
}
if (string.IsNullOrEmpty(authorizationInfo.Client))
{
throw new ArgumentException(nameof(authorizationInfo.Client) + " is required");
}
if (string.IsNullOrEmpty(authorizationInfo.Version))
{
throw new ArgumentException(nameof(authorizationInfo.Version) + "is required");
}
AssertActive();
ExpireRequests();
var secret = GenerateSecureRandom();
var code = GenerateCode();
var result = new QuickConnectResult(secret, code, DateTime.UtcNow);
var result = new QuickConnectResult(
secret,
code,
DateTime.UtcNow,
authorizationInfo.DeviceId,
authorizationInfo.Device,
authorizationInfo.Client,
authorizationInfo.Version);
_currentRequests[code] = result;
return result;
@@ -120,7 +139,7 @@ namespace Emby.Server.Implementations.QuickConnect
uint scale = uint.MaxValue;
while (scale == uint.MaxValue)
{
_rng.GetBytes(raw);
RandomNumberGenerator.Fill(raw);
scale = BitConverter.ToUInt32(raw);
}
@@ -129,7 +148,7 @@ namespace Emby.Server.Implementations.QuickConnect
}
/// <inheritdoc/>
public bool AuthorizeRequest(Guid userId, string code)
public async Task<bool> AuthorizeRequest(Guid userId, string code)
{
AssertActive();
ExpireRequests();
@@ -144,53 +163,45 @@ namespace Emby.Server.Implementations.QuickConnect
throw new InvalidOperationException("Request is already authorized");
}
var token = Guid.NewGuid();
result.Authentication = token;
// Change the time on the request so it expires one minute into the future. It can't expire immediately as otherwise some clients wouldn't ever see that they have been authenticated.
result.DateAdded = DateTime.Now.Add(TimeSpan.FromMinutes(1));
result.DateAdded = DateTime.UtcNow.Add(TimeSpan.FromMinutes(1));
_authenticationRepository.Create(new AuthenticationInfo
var authenticationResult = await _sessionManager.AuthenticateDirect(new AuthenticationRequest
{
AppName = TokenName,
AccessToken = token.ToString("N", CultureInfo.InvariantCulture),
DateCreated = DateTime.UtcNow,
DeviceId = _appHost.SystemId,
DeviceName = _appHost.FriendlyName,
AppVersion = _appHost.ApplicationVersionString,
UserId = userId
});
UserId = userId,
DeviceId = result.DeviceId,
DeviceName = result.DeviceName,
App = result.AppName,
AppVersion = result.AppVersion
}).ConfigureAwait(false);
_logger.LogDebug("Authorizing device with code {Code} to login as user {userId}", code, userId);
_authorizedSecrets[result.Secret] = (DateTime.UtcNow, authenticationResult);
result.Authenticated = true;
_currentRequests[code] = result;
_logger.LogDebug("Authorizing device with code {Code} to login as user {UserId}", code, userId);
return true;
}
/// <summary>
/// Dispose.
/// </summary>
public void Dispose()
/// <inheritdoc/>
public AuthenticationResult GetAuthorizedRequest(string secret)
{
Dispose(true);
GC.SuppressFinalize(this);
}
AssertActive();
ExpireRequests();
/// <summary>
/// Dispose.
/// </summary>
/// <param name="disposing">Dispose unmanaged resources.</param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
if (!_authorizedSecrets.TryGetValue(secret, out var result))
{
_rng.Dispose();
throw new ResourceNotFoundException("Unable to find request");
}
return result.AuthenticationResult;
}
private string GenerateSecureRandom(int length = 32)
{
Span<byte> bytes = stackalloc byte[length];
_rng.GetBytes(bytes);
RandomNumberGenerator.Fill(bytes);
return Convert.ToHexString(bytes);
}
@@ -218,6 +229,18 @@ namespace Emby.Server.Implementations.QuickConnect
}
}
}
foreach (var (secret, (timestamp, _)) in _authorizedSecrets)
{
if (expireAll || timestamp < minTime)
{
_logger.LogDebug("Removing expired secret {Secret}", secret);
if (!_authorizedSecrets.TryRemove(secret, out _))
{
_logger.LogWarning("Secret {Secret} already expired", secret);
}
}
}
}
}
}

View File

@@ -10,9 +10,9 @@ using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Progress;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
@@ -24,6 +24,11 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// </summary>
public class ScheduledTaskWorker : IScheduledTaskWorker
{
/// <summary>
/// The options for the json Serializer.
/// </summary>
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
/// <summary>
/// Gets or sets the application paths.
/// </summary>
@@ -66,11 +71,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// </summary>
private string _id;
/// <summary>
/// The options for the json Serializer.
/// </summary>
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
/// <summary>
/// Initializes a new instance of the <see cref="ScheduledTaskWorker" /> class.
/// </summary>
@@ -365,7 +365,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// </summary>
/// <param name="options">Task options.</param>
/// <returns>Task.</returns>
/// <exception cref="InvalidOperationException">Cannot execute a Task that is already running</exception>
/// <exception cref="InvalidOperationException">Cannot execute a Task that is already running.</exception>
public async Task Execute(TaskOptions options)
{
var task = Task.Run(async () => await ExecuteInternal(options).ConfigureAwait(false));

View File

@@ -19,16 +19,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// </summary>
public class TaskManager : ITaskManager
{
public event EventHandler<GenericEventArgs<IScheduledTaskWorker>> TaskExecuting;
public event EventHandler<TaskCompletionEventArgs> TaskCompleted;
/// <summary>
/// Gets the list of Scheduled Tasks.
/// </summary>
/// <value>The scheduled tasks.</value>
public IScheduledTaskWorker[] ScheduledTasks { get; private set; }
/// <summary>
/// The _task queue.
/// </summary>
@@ -53,10 +43,20 @@ namespace Emby.Server.Implementations.ScheduledTasks
ScheduledTasks = Array.Empty<IScheduledTaskWorker>();
}
public event EventHandler<GenericEventArgs<IScheduledTaskWorker>> TaskExecuting;
public event EventHandler<TaskCompletionEventArgs> TaskCompleted;
/// <summary>
/// Gets the list of Scheduled Tasks.
/// </summary>
/// <value>The scheduled tasks.</value>
public IScheduledTaskWorker[] ScheduledTasks { get; private set; }
/// <summary>
/// Cancels if running and queue.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="T">The task type.</typeparam>
/// <param name="options">Task options.</param>
public void CancelIfRunningAndQueue<T>(TaskOptions options)
where T : IScheduledTask
@@ -76,7 +76,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// <summary>
/// Cancels if running.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="T">The task type.</typeparam>
public void CancelIfRunning<T>()
where T : IScheduledTask
{
@@ -87,7 +87,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// <summary>
/// Queues the scheduled task.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="T">The task type.</typeparam>
/// <param name="options">Task options.</param>
public void QueueScheduledTask<T>(TaskOptions options)
where T : IScheduledTask

View File

@@ -39,6 +39,12 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// <summary>
/// Initializes a new instance of the <see cref="ChapterImagesTask" /> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>.
/// <param name="itemRepo">The item repository.</param>
/// <param name="appPaths">The application paths.</param>
/// <param name="encodingManager">The encoding manager.</param>
/// <param name="fileSystem">The filesystem.</param>
/// <param name="localization">The localization manager.</param>
public ChapterImagesTask(
ILibraryManager libraryManager,
IItemRepository itemRepo,

View File

@@ -29,6 +29,10 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
/// <summary>
/// Initializes a new instance of the <see cref="DeleteCacheFileTask" /> class.
/// </summary>
/// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
public DeleteCacheFileTask(
IApplicationPaths appPaths,
ILogger<DeleteCacheFileTask> logger,

View File

@@ -22,6 +22,9 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
/// <summary>
/// Initializes a new instance of the <see cref="OptimizeDatabaseTask" /> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="localization">The localization manager.</param>
/// <param name="provider">The jellyfin DB context provider.</param>
public OptimizeDatabaseTask(
ILogger<OptimizeDatabaseTask> logger,
ILocalizationManager localization,

View File

@@ -32,9 +32,24 @@ namespace Emby.Server.Implementations.ScheduledTasks
_localization = localization;
}
public string Name => _localization.GetLocalizedString("TaskRefreshPeople");
public string Description => _localization.GetLocalizedString("TaskRefreshPeopleDescription");
public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
public string Key => "RefreshPeople";
public bool IsHidden => false;
public bool IsEnabled => true;
public bool IsLogged => true;
/// <summary>
/// Creates the triggers that define when the task will run.
/// </summary>
/// <returns>An <see cref="IEnumerable{TaskTriggerInfo}"/> containing the default trigger infos for this task.</returns>
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
{
return new[]
@@ -57,19 +72,5 @@ namespace Emby.Server.Implementations.ScheduledTasks
{
return _libraryManager.ValidatePeople(cancellationToken, progress);
}
public string Name => _localization.GetLocalizedString("TaskRefreshPeople");
public string Description => _localization.GetLocalizedString("TaskRefreshPeopleDescription");
public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
public string Key => "RefreshPeople";
public bool IsHidden => false;
public bool IsEnabled => true;
public bool IsLogged => true;
}
}

View File

@@ -33,6 +33,18 @@ namespace Emby.Server.Implementations.ScheduledTasks
_localization = localization;
}
/// <inheritdoc />
public string Name => _localization.GetLocalizedString("TaskRefreshLibrary");
/// <inheritdoc />
public string Description => _localization.GetLocalizedString("TaskRefreshLibraryDescription");
/// <inheritdoc />
public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
/// <inheritdoc />
public string Key => "RefreshLibrary";
/// <summary>
/// Creates the triggers that define when the task will run.
/// </summary>
@@ -60,26 +72,5 @@ namespace Emby.Server.Implementations.ScheduledTasks
return ((LibraryManager)_libraryManager).ValidateMediaLibraryInternal(progress, cancellationToken);
}
/// <inheritdoc />
public string Name => _localization.GetLocalizedString("TaskRefreshLibrary");
/// <inheritdoc />
public string Description => _localization.GetLocalizedString("TaskRefreshLibraryDescription");
/// <inheritdoc />
public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
/// <inheritdoc />
public string Key => "RefreshLibrary";
/// <inheritdoc />
public bool IsHidden => false;
/// <inheritdoc />
public bool IsEnabled => true;
/// <inheritdoc />
public bool IsLogged => true;
}
}

View File

@@ -1,408 +0,0 @@
#nullable disable
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using Emby.Server.Implementations.Data;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Security;
using MediaBrowser.Model.Devices;
using MediaBrowser.Model.Querying;
using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
namespace Emby.Server.Implementations.Security
{
public class AuthenticationRepository : BaseSqliteRepository, IAuthenticationRepository
{
public AuthenticationRepository(ILogger<AuthenticationRepository> logger, IServerConfigurationManager config)
: base(logger)
{
DbFilePath = Path.Combine(config.ApplicationPaths.DataPath, "authentication.db");
}
public void Initialize()
{
string[] queries =
{
"create table if not exists Tokens (Id INTEGER PRIMARY KEY, AccessToken TEXT NOT NULL, DeviceId TEXT NOT NULL, AppName TEXT NOT NULL, AppVersion TEXT NOT NULL, DeviceName TEXT NOT NULL, UserId TEXT, UserName TEXT, IsActive BIT NOT NULL, DateCreated DATETIME NOT NULL, DateLastActivity DATETIME NOT NULL)",
"create table if not exists Devices (Id TEXT NOT NULL PRIMARY KEY, CustomName TEXT, Capabilities TEXT)",
"drop index if exists idx_AccessTokens",
"drop index if exists Tokens1",
"drop index if exists Tokens2",
"create index if not exists Tokens3 on Tokens (AccessToken, DateLastActivity)",
"create index if not exists Tokens4 on Tokens (Id, DateLastActivity)",
"create index if not exists Devices1 on Devices (Id)"
};
using (var connection = GetConnection())
{
var tableNewlyCreated = !TableExists(connection, "Tokens");
connection.RunQueries(queries);
TryMigrate(connection, tableNewlyCreated);
}
}
private void TryMigrate(ManagedConnection connection, bool tableNewlyCreated)
{
try
{
if (tableNewlyCreated && TableExists(connection, "AccessTokens"))
{
connection.RunInTransaction(
db =>
{
var existingColumnNames = GetColumnNames(db, "AccessTokens");
AddColumn(db, "AccessTokens", "UserName", "TEXT", existingColumnNames);
AddColumn(db, "AccessTokens", "DateLastActivity", "DATETIME", existingColumnNames);
AddColumn(db, "AccessTokens", "AppVersion", "TEXT", existingColumnNames);
}, TransactionMode);
connection.RunQueries(new[]
{
"update accesstokens set DateLastActivity=DateCreated where DateLastActivity is null",
"update accesstokens set DeviceName='Unknown' where DeviceName is null",
"update accesstokens set AppName='Unknown' where AppName is null",
"update accesstokens set AppVersion='1' where AppVersion is null",
"INSERT INTO Tokens (AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, UserName, IsActive, DateCreated, DateLastActivity) SELECT AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, UserName, IsActive, DateCreated, DateLastActivity FROM AccessTokens where deviceid not null and devicename not null and appname not null and isactive=1"
});
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Error migrating authentication database");
}
}
public void Create(AuthenticationInfo info)
{
if (info == null)
{
throw new ArgumentNullException(nameof(info));
}
using (var connection = GetConnection())
{
connection.RunInTransaction(
db =>
{
using (var statement = db.PrepareStatement("insert into Tokens (AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, UserName, IsActive, DateCreated, DateLastActivity) values (@AccessToken, @DeviceId, @AppName, @AppVersion, @DeviceName, @UserId, @UserName, @IsActive, @DateCreated, @DateLastActivity)"))
{
statement.TryBind("@AccessToken", info.AccessToken);
statement.TryBind("@DeviceId", info.DeviceId);
statement.TryBind("@AppName", info.AppName);
statement.TryBind("@AppVersion", info.AppVersion);
statement.TryBind("@DeviceName", info.DeviceName);
statement.TryBind("@UserId", info.UserId.Equals(Guid.Empty) ? null : info.UserId.ToString("N", CultureInfo.InvariantCulture));
statement.TryBind("@UserName", info.UserName);
statement.TryBind("@IsActive", true);
statement.TryBind("@DateCreated", info.DateCreated.ToDateTimeParamValue());
statement.TryBind("@DateLastActivity", info.DateLastActivity.ToDateTimeParamValue());
statement.MoveNext();
}
}, TransactionMode);
}
}
public void Update(AuthenticationInfo info)
{
if (info == null)
{
throw new ArgumentNullException(nameof(info));
}
using (var connection = GetConnection())
{
connection.RunInTransaction(
db =>
{
using (var statement = db.PrepareStatement("Update Tokens set AccessToken=@AccessToken, DeviceId=@DeviceId, AppName=@AppName, AppVersion=@AppVersion, DeviceName=@DeviceName, UserId=@UserId, UserName=@UserName, DateCreated=@DateCreated, DateLastActivity=@DateLastActivity where Id=@Id"))
{
statement.TryBind("@Id", info.Id);
statement.TryBind("@AccessToken", info.AccessToken);
statement.TryBind("@DeviceId", info.DeviceId);
statement.TryBind("@AppName", info.AppName);
statement.TryBind("@AppVersion", info.AppVersion);
statement.TryBind("@DeviceName", info.DeviceName);
statement.TryBind("@UserId", info.UserId.Equals(Guid.Empty) ? null : info.UserId.ToString("N", CultureInfo.InvariantCulture));
statement.TryBind("@UserName", info.UserName);
statement.TryBind("@DateCreated", info.DateCreated.ToDateTimeParamValue());
statement.TryBind("@DateLastActivity", info.DateLastActivity.ToDateTimeParamValue());
statement.MoveNext();
}
}, TransactionMode);
}
}
public void Delete(AuthenticationInfo info)
{
if (info == null)
{
throw new ArgumentNullException(nameof(info));
}
using (var connection = GetConnection())
{
connection.RunInTransaction(
db =>
{
using (var statement = db.PrepareStatement("Delete from Tokens where Id=@Id"))
{
statement.TryBind("@Id", info.Id);
statement.MoveNext();
}
}, TransactionMode);
}
}
private const string BaseSelectText = "select Tokens.Id, AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, UserName, DateCreated, DateLastActivity, Devices.CustomName from Tokens left join Devices on Tokens.DeviceId=Devices.Id";
private static void BindAuthenticationQueryParams(AuthenticationInfoQuery query, IStatement statement)
{
if (!string.IsNullOrEmpty(query.AccessToken))
{
statement.TryBind("@AccessToken", query.AccessToken);
}
if (!query.UserId.Equals(Guid.Empty))
{
statement.TryBind("@UserId", query.UserId.ToString("N", CultureInfo.InvariantCulture));
}
if (!string.IsNullOrEmpty(query.DeviceId))
{
statement.TryBind("@DeviceId", query.DeviceId);
}
}
public QueryResult<AuthenticationInfo> Get(AuthenticationInfoQuery query)
{
if (query == null)
{
throw new ArgumentNullException(nameof(query));
}
var commandText = BaseSelectText;
var whereClauses = new List<string>();
if (!string.IsNullOrEmpty(query.AccessToken))
{
whereClauses.Add("AccessToken=@AccessToken");
}
if (!string.IsNullOrEmpty(query.DeviceId))
{
whereClauses.Add("DeviceId=@DeviceId");
}
if (!query.UserId.Equals(Guid.Empty))
{
whereClauses.Add("UserId=@UserId");
}
if (query.HasUser.HasValue)
{
if (query.HasUser.Value)
{
whereClauses.Add("UserId not null");
}
else
{
whereClauses.Add("UserId is null");
}
}
var whereTextWithoutPaging = whereClauses.Count == 0 ?
string.Empty :
" where " + string.Join(" AND ", whereClauses.ToArray());
commandText += whereTextWithoutPaging;
commandText += " ORDER BY DateLastActivity desc";
if (query.Limit.HasValue || query.StartIndex.HasValue)
{
var offset = query.StartIndex ?? 0;
if (query.Limit.HasValue || offset > 0)
{
commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture);
}
if (offset > 0)
{
commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture);
}
}
var statementTexts = new[]
{
commandText,
"select count (Id) from Tokens" + whereTextWithoutPaging
};
var list = new List<AuthenticationInfo>();
var result = new QueryResult<AuthenticationInfo>();
using (var connection = GetConnection(true))
{
connection.RunInTransaction(
db =>
{
var statements = PrepareAll(db, statementTexts);
using (var statement = statements[0])
{
BindAuthenticationQueryParams(query, statement);
foreach (var row in statement.ExecuteQuery())
{
list.Add(Get(row));
}
using (var totalCountStatement = statements[1])
{
BindAuthenticationQueryParams(query, totalCountStatement);
result.TotalRecordCount = totalCountStatement.ExecuteQuery()
.SelectScalarInt()
.First();
}
}
},
ReadTransactionMode);
}
result.Items = list;
return result;
}
private static AuthenticationInfo Get(IReadOnlyList<ResultSetValue> reader)
{
var info = new AuthenticationInfo
{
Id = reader[0].ToInt64(),
AccessToken = reader[1].ToString()
};
if (reader.TryGetString(2, out var deviceId))
{
info.DeviceId = deviceId;
}
if (reader.TryGetString(3, out var appName))
{
info.AppName = appName;
}
if (reader.TryGetString(4, out var appVersion))
{
info.AppVersion = appVersion;
}
if (reader.TryGetString(6, out var userId))
{
info.UserId = new Guid(userId);
}
if (reader.TryGetString(7, out var userName))
{
info.UserName = userName;
}
info.DateCreated = reader[8].ReadDateTime();
if (reader.TryReadDateTime(9, out var dateLastActivity))
{
info.DateLastActivity = dateLastActivity;
}
else
{
info.DateLastActivity = info.DateCreated;
}
if (reader.TryGetString(10, out var customName))
{
info.DeviceName = customName;
}
else if (reader.TryGetString(5, out var deviceName))
{
info.DeviceName = deviceName;
}
return info;
}
public DeviceOptions GetDeviceOptions(string deviceId)
{
using (var connection = GetConnection(true))
{
return connection.RunInTransaction(
db =>
{
using (var statement = base.PrepareStatement(db, "select CustomName from Devices where Id=@DeviceId"))
{
statement.TryBind("@DeviceId", deviceId);
var result = new DeviceOptions();
foreach (var row in statement.ExecuteQuery())
{
if (row.TryGetString(0, out var customName))
{
result.CustomName = customName;
}
}
return result;
}
}, ReadTransactionMode);
}
}
public void UpdateDeviceOptions(string deviceId, DeviceOptions options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
using (var connection = GetConnection())
{
connection.RunInTransaction(
db =>
{
using (var statement = db.PrepareStatement("replace into devices (Id, CustomName, Capabilities) VALUES (@Id, @CustomName, (Select Capabilities from Devices where Id=@Id))"))
{
statement.TryBind("@Id", deviceId);
if (string.IsNullOrWhiteSpace(options.CustomName))
{
statement.TryBindNull("@CustomName");
}
else
{
statement.TryBind("@CustomName", options.CustomName);
}
statement.MoveNext();
}
}, TransactionMode);
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More