mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-03-21 01:26:41 +00:00
Merge remote-tracking branch 'upstream/master' into quickconnect
This commit is contained in:
@@ -4,7 +4,6 @@ using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
@@ -46,7 +45,7 @@ using Emby.Server.Implementations.Session;
|
||||
using Emby.Server.Implementations.SyncPlay;
|
||||
using Emby.Server.Implementations.TV;
|
||||
using Emby.Server.Implementations.Updates;
|
||||
using MediaBrowser.Api;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using MediaBrowser.Common;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Events;
|
||||
@@ -98,7 +97,6 @@ using MediaBrowser.Providers.Chapters;
|
||||
using MediaBrowser.Providers.Manager;
|
||||
using MediaBrowser.Providers.Plugins.TheTvdb;
|
||||
using MediaBrowser.Providers.Subtitles;
|
||||
using MediaBrowser.WebDashboard.Api;
|
||||
using MediaBrowser.XbmcMetadata.Providers;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -556,8 +554,6 @@ namespace Emby.Server.Implementations
|
||||
serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
|
||||
serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
|
||||
|
||||
serviceCollection.AddSingleton<IDisplayPreferencesRepository, SqliteDisplayPreferencesRepository>();
|
||||
|
||||
serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
|
||||
|
||||
serviceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
|
||||
@@ -637,6 +633,8 @@ namespace Emby.Server.Implementations
|
||||
serviceCollection.AddSingleton<EncodingHelper>();
|
||||
|
||||
serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
|
||||
|
||||
serviceCollection.AddSingleton<TranscodingJobHelper>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -653,7 +651,6 @@ namespace Emby.Server.Implementations
|
||||
_httpServer = Resolve<IHttpServer>();
|
||||
_httpClient = Resolve<IHttpClient>();
|
||||
|
||||
((SqliteDisplayPreferencesRepository)Resolve<IDisplayPreferencesRepository>()).Initialize();
|
||||
((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
|
||||
|
||||
SetStaticProperties();
|
||||
@@ -1037,12 +1034,6 @@ namespace Emby.Server.Implementations
|
||||
}
|
||||
}
|
||||
|
||||
// Include composable parts in the Api assembly
|
||||
yield return typeof(ApiEntryPoint).Assembly;
|
||||
|
||||
// Include composable parts in the Dashboard assembly
|
||||
yield return typeof(DashboardService).Assembly;
|
||||
|
||||
// Include composable parts in the Model assembly
|
||||
yield return typeof(SystemInfo).Assembly;
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Browser
|
||||
@@ -24,7 +26,7 @@ namespace Emby.Server.Implementations.Browser
|
||||
/// <param name="appHost">The app host.</param>
|
||||
public static void OpenSwaggerPage(IServerApplicationHost appHost)
|
||||
{
|
||||
TryOpenUrl(appHost, "/swagger/index.html");
|
||||
TryOpenUrl(appHost, "/api-docs/swagger");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
@@ -7,6 +6,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Progress;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
@@ -22,6 +22,7 @@ using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
|
||||
using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
|
||||
@@ -45,10 +46,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IProviderManager _providerManager;
|
||||
|
||||
private readonly ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>> _channelItemMediaInfo =
|
||||
new ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>>();
|
||||
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
|
||||
|
||||
/// <summary>
|
||||
@@ -63,6 +61,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
/// <param name="userDataManager">The user data manager.</param>
|
||||
/// <param name="jsonSerializer">The JSON serializer.</param>
|
||||
/// <param name="providerManager">The provider manager.</param>
|
||||
/// <param name="memoryCache">The memory cache.</param>
|
||||
public ChannelManager(
|
||||
IUserManager userManager,
|
||||
IDtoService dtoService,
|
||||
@@ -72,7 +71,8 @@ namespace Emby.Server.Implementations.Channels
|
||||
IFileSystem fileSystem,
|
||||
IUserDataManager userDataManager,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IProviderManager providerManager)
|
||||
IProviderManager providerManager,
|
||||
IMemoryCache memoryCache)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_dtoService = dtoService;
|
||||
@@ -83,6 +83,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
_userDataManager = userDataManager;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_providerManager = providerManager;
|
||||
_memoryCache = memoryCache;
|
||||
}
|
||||
|
||||
internal IChannel[] Channels { get; private set; }
|
||||
@@ -417,20 +418,15 @@ namespace Emby.Server.Implementations.Channels
|
||||
|
||||
private async Task<IEnumerable<MediaSourceInfo>> GetChannelItemMediaSourcesInternal(IRequiresMediaInfoCallback channel, string id, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_channelItemMediaInfo.TryGetValue(id, out Tuple<DateTime, List<MediaSourceInfo>> cachedInfo))
|
||||
if (_memoryCache.TryGetValue(id, out List<MediaSourceInfo> cachedInfo))
|
||||
{
|
||||
if ((DateTime.UtcNow - cachedInfo.Item1).TotalMinutes < 5)
|
||||
{
|
||||
return cachedInfo.Item2;
|
||||
}
|
||||
return cachedInfo;
|
||||
}
|
||||
|
||||
var mediaInfo = await channel.GetChannelItemMediaInfo(id, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
var list = mediaInfo.ToList();
|
||||
|
||||
var item2 = new Tuple<DateTime, List<MediaSourceInfo>>(DateTime.UtcNow, list);
|
||||
_channelItemMediaInfo.AddOrUpdate(id, item2, (key, oldValue) => item2);
|
||||
_memoryCache.Set(id, list, DateTimeOffset.UtcNow.AddMinutes(5));
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
@@ -1,225 +0,0 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Json;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SQLitePCL.pretty;
|
||||
|
||||
namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// Class SQLiteDisplayPreferencesRepository.
|
||||
/// </summary>
|
||||
public class SqliteDisplayPreferencesRepository : BaseSqliteRepository, IDisplayPreferencesRepository
|
||||
{
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
|
||||
public SqliteDisplayPreferencesRepository(ILogger<SqliteDisplayPreferencesRepository> logger, IApplicationPaths appPaths, IFileSystem fileSystem)
|
||||
: base(logger)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
|
||||
_jsonOptions = JsonDefaults.GetOptions();
|
||||
|
||||
DbFilePath = Path.Combine(appPaths.DataPath, "displaypreferences.db");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the repository.
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
public string Name => "SQLite";
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
try
|
||||
{
|
||||
InitializeInternal();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error loading database file. Will reset and retry.");
|
||||
|
||||
_fileSystem.DeleteFile(DbFilePath);
|
||||
|
||||
InitializeInternal();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the connection to the database.
|
||||
/// </summary>
|
||||
/// <returns>Task.</returns>
|
||||
private void InitializeInternal()
|
||||
{
|
||||
string[] queries =
|
||||
{
|
||||
"create table if not exists userdisplaypreferences (id GUID NOT NULL, userId GUID NOT NULL, client text NOT NULL, data BLOB NOT NULL)",
|
||||
"create unique index if not exists userdisplaypreferencesindex on userdisplaypreferences (id, userId, client)"
|
||||
};
|
||||
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
connection.RunQueries(queries);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save the display preferences associated with an item in the repo.
|
||||
/// </summary>
|
||||
/// <param name="displayPreferences">The display preferences.</param>
|
||||
/// <param name="userId">The user id.</param>
|
||||
/// <param name="client">The client.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <exception cref="ArgumentNullException">item</exception>
|
||||
public void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, CancellationToken cancellationToken)
|
||||
{
|
||||
if (displayPreferences == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(displayPreferences));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(displayPreferences.Id))
|
||||
{
|
||||
throw new ArgumentException("Display preferences has an invalid Id", nameof(displayPreferences));
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
connection.RunInTransaction(
|
||||
db => SaveDisplayPreferences(displayPreferences, userId, client, db),
|
||||
TransactionMode);
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, IDatabaseConnection connection)
|
||||
{
|
||||
var serialized = JsonSerializer.SerializeToUtf8Bytes(displayPreferences, _jsonOptions);
|
||||
|
||||
using (var statement = connection.PrepareStatement("replace into userdisplaypreferences (id, userid, client, data) values (@id, @userId, @client, @data)"))
|
||||
{
|
||||
statement.TryBind("@id", new Guid(displayPreferences.Id).ToByteArray());
|
||||
statement.TryBind("@userId", userId.ToByteArray());
|
||||
statement.TryBind("@client", client);
|
||||
statement.TryBind("@data", serialized);
|
||||
|
||||
statement.MoveNext();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save all display preferences associated with a user in the repo.
|
||||
/// </summary>
|
||||
/// <param name="displayPreferences">The display preferences.</param>
|
||||
/// <param name="userId">The user id.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <exception cref="ArgumentNullException">item</exception>
|
||||
public void SaveAllDisplayPreferences(IEnumerable<DisplayPreferences> displayPreferences, Guid userId, CancellationToken cancellationToken)
|
||||
{
|
||||
if (displayPreferences == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(displayPreferences));
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
connection.RunInTransaction(
|
||||
db =>
|
||||
{
|
||||
foreach (var displayPreference in displayPreferences)
|
||||
{
|
||||
SaveDisplayPreferences(displayPreference, userId, displayPreference.Client, db);
|
||||
}
|
||||
},
|
||||
TransactionMode);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the display preferences.
|
||||
/// </summary>
|
||||
/// <param name="displayPreferencesId">The display preferences id.</param>
|
||||
/// <param name="userId">The user id.</param>
|
||||
/// <param name="client">The client.</param>
|
||||
/// <returns>Task{DisplayPreferences}.</returns>
|
||||
/// <exception cref="ArgumentNullException">item</exception>
|
||||
public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, Guid userId, string client)
|
||||
{
|
||||
if (string.IsNullOrEmpty(displayPreferencesId))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(displayPreferencesId));
|
||||
}
|
||||
|
||||
var guidId = displayPreferencesId.GetMD5();
|
||||
|
||||
using (var connection = GetConnection(true))
|
||||
{
|
||||
using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where id = @id and userId=@userId and client=@client"))
|
||||
{
|
||||
statement.TryBind("@id", guidId.ToByteArray());
|
||||
statement.TryBind("@userId", userId.ToByteArray());
|
||||
statement.TryBind("@client", client);
|
||||
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
{
|
||||
return Get(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new DisplayPreferences
|
||||
{
|
||||
Id = guidId.ToString("N", CultureInfo.InvariantCulture)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all display preferences for the given user.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user id.</param>
|
||||
/// <returns>Task{DisplayPreferences}.</returns>
|
||||
/// <exception cref="ArgumentNullException">item</exception>
|
||||
public IEnumerable<DisplayPreferences> GetAllDisplayPreferences(Guid userId)
|
||||
{
|
||||
var list = new List<DisplayPreferences>();
|
||||
|
||||
using (var connection = GetConnection(true))
|
||||
using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where userId=@userId"))
|
||||
{
|
||||
statement.TryBind("@userId", userId.ToByteArray());
|
||||
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
{
|
||||
list.Add(Get(row));
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private DisplayPreferences Get(IReadOnlyList<IResultSetValue> row)
|
||||
=> JsonSerializer.Deserialize<DisplayPreferences>(row[0].ToBlob(), _jsonOptions);
|
||||
|
||||
public void SaveDisplayPreferences(DisplayPreferences displayPreferences, string userId, string client, CancellationToken cancellationToken)
|
||||
=> SaveDisplayPreferences(displayPreferences, new Guid(userId), client, cancellationToken);
|
||||
|
||||
public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, string userId, string client)
|
||||
=> GetDisplayPreferences(displayPreferencesId, new Guid(userId), client);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using Emby.Server.Implementations.Playlists;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Json;
|
||||
using MediaBrowser.Controller;
|
||||
@@ -400,6 +401,8 @@ namespace Emby.Server.Implementations.Data
|
||||
"OwnerId"
|
||||
};
|
||||
|
||||
private static readonly string _retriveItemColumnsSelectQuery = $"select {string.Join(',', _retriveItemColumns)} from TypedBaseItems where guid = @guid";
|
||||
|
||||
private static readonly string[] _mediaStreamSaveColumns =
|
||||
{
|
||||
"ItemId",
|
||||
@@ -439,6 +442,12 @@ namespace Emby.Server.Implementations.Data
|
||||
"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",
|
||||
@@ -450,102 +459,15 @@ namespace Emby.Server.Implementations.Data
|
||||
"MIMEType"
|
||||
};
|
||||
|
||||
private static readonly string _mediaAttachmentSaveColumnsSelectQuery =
|
||||
$"select {string.Join(',', _mediaAttachmentSaveColumns)} from mediaattachments where ItemId=@ItemId";
|
||||
|
||||
private static readonly string _mediaAttachmentInsertPrefix;
|
||||
|
||||
private static string GetSaveItemCommandText()
|
||||
{
|
||||
var saveColumns = new[]
|
||||
{
|
||||
"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"
|
||||
};
|
||||
|
||||
var saveItemCommandCommandText = "replace into TypedBaseItems (" + string.Join(",", saveColumns) + ") values (";
|
||||
|
||||
for (var i = 0; i < saveColumns.Length; i++)
|
||||
{
|
||||
if (i != 0)
|
||||
{
|
||||
saveItemCommandCommandText += ",";
|
||||
}
|
||||
|
||||
saveItemCommandCommandText += "@" + saveColumns[i];
|
||||
}
|
||||
|
||||
return saveItemCommandCommandText + ")";
|
||||
}
|
||||
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.
|
||||
@@ -636,7 +558,7 @@ namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
var statements = PrepareAll(db, new string[]
|
||||
{
|
||||
GetSaveItemCommandText(),
|
||||
SaveItemCommandText,
|
||||
"delete from AncestorIds where ItemId=@ItemId"
|
||||
}).ToList();
|
||||
|
||||
@@ -1056,7 +978,10 @@ namespace Emby.Server.Implementations.Data
|
||||
continue;
|
||||
}
|
||||
|
||||
str.Append($"{i.Key}={i.Value}|");
|
||||
str.Append(i.Key)
|
||||
.Append('=')
|
||||
.Append(i.Value)
|
||||
.Append('|');
|
||||
}
|
||||
|
||||
if (str.Length == 0)
|
||||
@@ -1110,8 +1035,8 @@ namespace Emby.Server.Implementations.Data
|
||||
continue;
|
||||
}
|
||||
|
||||
str.Append(ToValueString(i))
|
||||
.Append('|');
|
||||
AppendItemImageInfo(str, i);
|
||||
str.Append('|');
|
||||
}
|
||||
|
||||
str.Length -= 1; // Remove last |
|
||||
@@ -1145,26 +1070,26 @@ namespace Emby.Server.Implementations.Data
|
||||
item.ImageInfos = list.ToArray();
|
||||
}
|
||||
|
||||
public string ToValueString(ItemImageInfo image)
|
||||
public void AppendItemImageInfo(StringBuilder bldr, ItemImageInfo image)
|
||||
{
|
||||
const string Delimeter = "*";
|
||||
const char Delimiter = '*';
|
||||
|
||||
var path = image.Path ?? string.Empty;
|
||||
var hash = image.BlurHash ?? string.Empty;
|
||||
|
||||
return GetPathToSave(path) +
|
||||
Delimeter +
|
||||
image.DateModified.Ticks.ToString(CultureInfo.InvariantCulture) +
|
||||
Delimeter +
|
||||
image.Type +
|
||||
Delimeter +
|
||||
image.Width.ToString(CultureInfo.InvariantCulture) +
|
||||
Delimeter +
|
||||
image.Height.ToString(CultureInfo.InvariantCulture) +
|
||||
Delimeter +
|
||||
// Replace delimiters with other characters.
|
||||
// This can be removed when we migrate to a proper DB.
|
||||
hash.Replace('*', '/').Replace('|', '\\');
|
||||
bldr.Append(GetPathToSave(path))
|
||||
.Append(Delimiter)
|
||||
.Append(image.DateModified.Ticks)
|
||||
.Append(Delimiter)
|
||||
.Append(image.Type)
|
||||
.Append(Delimiter)
|
||||
.Append(image.Width)
|
||||
.Append(Delimiter)
|
||||
.Append(image.Height)
|
||||
.Append(Delimiter)
|
||||
// Replace delimiters with other characters.
|
||||
// This can be removed when we migrate to a proper DB.
|
||||
.Append(hash.Replace('*', '/').Replace('|', '\\'));
|
||||
}
|
||||
|
||||
public ItemImageInfo ItemImageInfoFromValueString(string value)
|
||||
@@ -1226,7 +1151,7 @@ namespace Emby.Server.Implementations.Data
|
||||
|
||||
using (var connection = GetConnection(true))
|
||||
{
|
||||
using (var statement = PrepareStatement(connection, "select " + string.Join(",", _retriveItemColumns) + " from TypedBaseItems where guid = @guid"))
|
||||
using (var statement = PrepareStatement(connection, _retriveItemColumnsSelectQuery))
|
||||
{
|
||||
statement.TryBind("@guid", id);
|
||||
|
||||
@@ -2776,82 +2701,82 @@ namespace Emby.Server.Implementations.Data
|
||||
|
||||
private string FixUnicodeChars(string buffer)
|
||||
{
|
||||
if (buffer.IndexOf('\u2013') > -1)
|
||||
if (buffer.IndexOf('\u2013', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u2013', '-'); // en dash
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u2014') > -1)
|
||||
if (buffer.IndexOf('\u2014', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u2014', '-'); // em dash
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u2015') > -1)
|
||||
if (buffer.IndexOf('\u2015', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u2015', '-'); // horizontal bar
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u2017') > -1)
|
||||
if (buffer.IndexOf('\u2017', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u2017', '_'); // double low line
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u2018') > -1)
|
||||
if (buffer.IndexOf('\u2018', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u2018', '\''); // left single quotation mark
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u2019') > -1)
|
||||
if (buffer.IndexOf('\u2019', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u2019', '\''); // right single quotation mark
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u201a') > -1)
|
||||
if (buffer.IndexOf('\u201a', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u201a', ','); // single low-9 quotation mark
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u201b') > -1)
|
||||
if (buffer.IndexOf('\u201b', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u201b', '\''); // single high-reversed-9 quotation mark
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u201c') > -1)
|
||||
if (buffer.IndexOf('\u201c', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u201c', '\"'); // left double quotation mark
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u201d') > -1)
|
||||
if (buffer.IndexOf('\u201d', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u201d', '\"'); // right double quotation mark
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u201e') > -1)
|
||||
if (buffer.IndexOf('\u201e', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u201e', '\"'); // double low-9 quotation mark
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u2026') > -1)
|
||||
if (buffer.IndexOf('\u2026', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
buffer = buffer.Replace("\u2026", "..."); // horizontal ellipsis
|
||||
buffer = buffer.Replace("\u2026", "...", StringComparison.Ordinal); // horizontal ellipsis
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u2032') > -1)
|
||||
if (buffer.IndexOf('\u2032', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u2032', '\''); // prime
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u2033') > -1)
|
||||
if (buffer.IndexOf('\u2033', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u2033', '\"'); // double prime
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u0060') > -1)
|
||||
if (buffer.IndexOf('\u0060', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u0060', '\''); // grave accent
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u00B4') > -1)
|
||||
if (buffer.IndexOf('\u00B4', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u00B4', '\''); // acute accent
|
||||
}
|
||||
@@ -3000,7 +2925,6 @@ namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
connection.RunInTransaction(db =>
|
||||
{
|
||||
|
||||
var statements = PrepareAll(db, statementTexts).ToList();
|
||||
|
||||
if (!isReturningZeroItems)
|
||||
@@ -4670,8 +4594,12 @@ namespace Emby.Server.Implementations.Data
|
||||
|
||||
if (query.BlockUnratedItems.Length > 1)
|
||||
{
|
||||
var inClause = string.Join(",", query.BlockUnratedItems.Select(i => "'" + i.ToString() + "'"));
|
||||
whereClauses.Add(string.Format("(InheritedParentalRatingValue > 0 or UnratedType not in ({0}))", inClause));
|
||||
var inClause = string.Join(',', query.BlockUnratedItems.Select(i => "'" + i.ToString() + "'"));
|
||||
whereClauses.Add(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"(InheritedParentalRatingValue > 0 or UnratedType not in ({0}))",
|
||||
inClause));
|
||||
}
|
||||
|
||||
if (query.ExcludeInheritedTags.Length > 0)
|
||||
@@ -4680,7 +4608,7 @@ namespace Emby.Server.Implementations.Data
|
||||
if (statement == null)
|
||||
{
|
||||
int index = 0;
|
||||
string excludedTags = string.Join(",", query.ExcludeInheritedTags.Select(t => paramName + index++));
|
||||
string excludedTags = string.Join(',', query.ExcludeInheritedTags.Select(t => paramName + index++));
|
||||
whereClauses.Add("((select CleanValue from itemvalues where ItemId=Guid and Type=6 and cleanvalue in (" + excludedTags + ")) is null)");
|
||||
}
|
||||
else
|
||||
@@ -5734,10 +5662,10 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
||||
const int Limit = 100;
|
||||
var startIndex = 0;
|
||||
|
||||
const string StartInsertText = "insert into ItemValues (ItemId, Type, Value, CleanValue) values ";
|
||||
var insertText = new StringBuilder(StartInsertText);
|
||||
while (startIndex < values.Count)
|
||||
{
|
||||
var insertText = new StringBuilder("insert into ItemValues (ItemId, Type, Value, CleanValue) values ");
|
||||
|
||||
var endIndex = Math.Min(values.Count, startIndex + Limit);
|
||||
|
||||
for (var i = startIndex; i < endIndex; i++)
|
||||
@@ -5779,6 +5707,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
||||
}
|
||||
|
||||
startIndex += Limit;
|
||||
insertText.Length = StartInsertText.Length;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5816,10 +5745,10 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
||||
var startIndex = 0;
|
||||
var listIndex = 0;
|
||||
|
||||
const string StartInsertText = "insert into People (ItemId, Name, Role, PersonType, SortOrder, ListOrder) values ";
|
||||
var insertText = new StringBuilder(StartInsertText);
|
||||
while (startIndex < people.Count)
|
||||
{
|
||||
var insertText = new StringBuilder("insert into People (ItemId, Name, Role, PersonType, SortOrder, ListOrder) values ");
|
||||
|
||||
var endIndex = Math.Min(people.Count, startIndex + Limit);
|
||||
for (var i = startIndex; i < endIndex; i++)
|
||||
{
|
||||
@@ -5853,6 +5782,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
||||
}
|
||||
|
||||
startIndex += Limit;
|
||||
insertText.Length = StartInsertText.Length;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5891,10 +5821,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
||||
throw new ArgumentNullException(nameof(query));
|
||||
}
|
||||
|
||||
var cmdText = "select "
|
||||
+ string.Join(",", _mediaStreamSaveColumns)
|
||||
+ " from mediastreams where"
|
||||
+ " ItemId=@ItemId";
|
||||
var cmdText = _mediaStreamSaveColumnsSelectQuery;
|
||||
|
||||
if (query.Type.HasValue)
|
||||
{
|
||||
@@ -5971,18 +5898,9 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
||||
const int Limit = 10;
|
||||
var startIndex = 0;
|
||||
|
||||
var insertText = new StringBuilder(_mediaStreamSaveColumnsInsertQuery);
|
||||
while (startIndex < streams.Count)
|
||||
{
|
||||
var insertText = new StringBuilder("insert into mediastreams (");
|
||||
foreach (var column in _mediaStreamSaveColumns)
|
||||
{
|
||||
insertText.Append(column).Append(',');
|
||||
}
|
||||
|
||||
// Remove last comma
|
||||
insertText.Length--;
|
||||
insertText.Append(") values ");
|
||||
|
||||
var endIndex = Math.Min(streams.Count, startIndex + Limit);
|
||||
|
||||
for (var i = startIndex; i < endIndex; i++)
|
||||
@@ -6065,6 +5983,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
||||
}
|
||||
|
||||
startIndex += Limit;
|
||||
insertText.Length = _mediaStreamSaveColumnsInsertQuery.Length;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6248,10 +6167,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
||||
throw new ArgumentNullException(nameof(query));
|
||||
}
|
||||
|
||||
var cmdText = "select "
|
||||
+ string.Join(",", _mediaAttachmentSaveColumns)
|
||||
+ " from mediaattachments where"
|
||||
+ " ItemId=@ItemId";
|
||||
var cmdText = _mediaAttachmentSaveColumnsSelectQuery;
|
||||
|
||||
if (query.Index.HasValue)
|
||||
{
|
||||
@@ -6319,10 +6235,9 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
||||
{
|
||||
const int InsertAtOnce = 10;
|
||||
|
||||
var insertText = new StringBuilder(_mediaAttachmentInsertPrefix);
|
||||
for (var startIndex = 0; startIndex < attachments.Count; startIndex += InsertAtOnce)
|
||||
{
|
||||
var insertText = new StringBuilder(_mediaAttachmentInsertPrefix);
|
||||
|
||||
var endIndex = Math.Min(attachments.Count, startIndex + InsertAtOnce);
|
||||
|
||||
for (var i = startIndex; i < endIndex; i++)
|
||||
@@ -6368,6 +6283,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
||||
statement.Reset();
|
||||
statement.MoveNext();
|
||||
}
|
||||
|
||||
insertText.Length = _mediaAttachmentInsertPrefix.Length;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
@@ -17,16 +17,17 @@ using MediaBrowser.Model.Events;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Model.Session;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
|
||||
namespace Emby.Server.Implementations.Devices
|
||||
{
|
||||
public class DeviceManager : IDeviceManager
|
||||
{
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
private readonly IJsonSerializer _json;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly IAuthenticationRepository _authRepo;
|
||||
private readonly Dictionary<string, ClientCapabilities> _capabilitiesCache;
|
||||
private readonly object _capabilitiesSyncLock = new object();
|
||||
|
||||
public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
|
||||
@@ -35,13 +36,14 @@ namespace Emby.Server.Implementations.Devices
|
||||
IAuthenticationRepository authRepo,
|
||||
IJsonSerializer json,
|
||||
IUserManager userManager,
|
||||
IServerConfigurationManager config)
|
||||
IServerConfigurationManager config,
|
||||
IMemoryCache memoryCache)
|
||||
{
|
||||
_json = json;
|
||||
_userManager = userManager;
|
||||
_config = config;
|
||||
_memoryCache = memoryCache;
|
||||
_authRepo = authRepo;
|
||||
_capabilitiesCache = new Dictionary<string, ClientCapabilities>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public void SaveCapabilities(string deviceId, ClientCapabilities capabilities)
|
||||
@@ -51,8 +53,7 @@ namespace Emby.Server.Implementations.Devices
|
||||
|
||||
lock (_capabilitiesSyncLock)
|
||||
{
|
||||
_capabilitiesCache[deviceId] = capabilities;
|
||||
|
||||
_memoryCache.Set(deviceId, capabilities);
|
||||
_json.SerializeToFile(capabilities, path);
|
||||
}
|
||||
}
|
||||
@@ -71,13 +72,13 @@ namespace Emby.Server.Implementations.Devices
|
||||
|
||||
public ClientCapabilities GetCapabilities(string id)
|
||||
{
|
||||
if (_memoryCache.TryGetValue(id, out ClientCapabilities result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
lock (_capabilitiesSyncLock)
|
||||
{
|
||||
if (_capabilitiesCache.TryGetValue(id, out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var path = Path.Combine(GetDevicePath(id), "capabilities.json");
|
||||
try
|
||||
{
|
||||
|
||||
@@ -13,10 +13,8 @@
|
||||
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.Providers\MediaBrowser.Providers.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.WebDashboard\MediaBrowser.WebDashboard.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj" />
|
||||
<ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.Api\MediaBrowser.Api.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.LocalMetadata\MediaBrowser.LocalMetadata.csproj" />
|
||||
<ProjectReference Include="..\Emby.Photos\Emby.Photos.csproj" />
|
||||
<ProjectReference Include="..\Emby.Drawing\Emby.Drawing.csproj" />
|
||||
@@ -25,7 +23,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="IPNetwork2" Version="2.5.211" />
|
||||
<PackageReference Include="Jellyfin.XmlTv" Version="10.6.0-pre1" />
|
||||
<PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Hosting.Server.Abstractions" Version="2.2.0" />
|
||||
@@ -38,10 +36,10 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.6" />
|
||||
<PackageReference Include="Mono.Nat" Version="2.0.1" />
|
||||
<PackageReference Include="Mono.Nat" Version="2.0.2" />
|
||||
<PackageReference Include="prometheus-net.DotNetRuntime" Version="3.3.1" />
|
||||
<PackageReference Include="ServiceStack.Text.Core" Version="5.9.0" />
|
||||
<PackageReference Include="sharpcompress" Version="0.25.1" />
|
||||
<PackageReference Include="ServiceStack.Text.Core" Version="5.9.2" />
|
||||
<PackageReference Include="sharpcompress" Version="0.26.0" />
|
||||
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
|
||||
<PackageReference Include="DotNet.Glob" Version="3.0.9" />
|
||||
</ItemGroup>
|
||||
@@ -54,7 +52,7 @@
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'" >true</TreatWarningsAsErrors>
|
||||
<TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Code Analyzers-->
|
||||
|
||||
@@ -23,10 +23,12 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
public class LibraryChangedNotifier : IServerEntryPoint
|
||||
{
|
||||
/// <summary>
|
||||
/// The library manager.
|
||||
/// The library update duration.
|
||||
/// </summary>
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private const int LibraryUpdateDuration = 30000;
|
||||
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IProviderManager _providerManager;
|
||||
private readonly ISessionManager _sessionManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly ILogger<LibraryChangedNotifier> _logger;
|
||||
@@ -38,23 +40,10 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
|
||||
private readonly List<Folder> _foldersAddedTo = new List<Folder>();
|
||||
private readonly List<Folder> _foldersRemovedFrom = new List<Folder>();
|
||||
|
||||
private readonly List<BaseItem> _itemsAdded = new List<BaseItem>();
|
||||
private readonly List<BaseItem> _itemsRemoved = new List<BaseItem>();
|
||||
private readonly List<BaseItem> _itemsUpdated = new List<BaseItem>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the library update timer.
|
||||
/// </summary>
|
||||
/// <value>The library update timer.</value>
|
||||
private Timer LibraryUpdateTimer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The library update duration.
|
||||
/// </summary>
|
||||
private const int LibraryUpdateDuration = 30000;
|
||||
|
||||
private readonly IProviderManager _providerManager;
|
||||
private readonly Dictionary<Guid, DateTime> _lastProgressMessageTimes = new Dictionary<Guid, DateTime>();
|
||||
|
||||
public LibraryChangedNotifier(
|
||||
ILibraryManager libraryManager,
|
||||
@@ -70,22 +59,26 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
_providerManager = providerManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the library update timer.
|
||||
/// </summary>
|
||||
/// <value>The library update timer.</value>
|
||||
private Timer LibraryUpdateTimer { get; set; }
|
||||
|
||||
public Task RunAsync()
|
||||
{
|
||||
_libraryManager.ItemAdded += libraryManager_ItemAdded;
|
||||
_libraryManager.ItemUpdated += libraryManager_ItemUpdated;
|
||||
_libraryManager.ItemRemoved += libraryManager_ItemRemoved;
|
||||
_libraryManager.ItemAdded += OnLibraryItemAdded;
|
||||
_libraryManager.ItemUpdated += OnLibraryItemUpdated;
|
||||
_libraryManager.ItemRemoved += OnLibraryItemRemoved;
|
||||
|
||||
_providerManager.RefreshCompleted += _providerManager_RefreshCompleted;
|
||||
_providerManager.RefreshStarted += _providerManager_RefreshStarted;
|
||||
_providerManager.RefreshProgress += _providerManager_RefreshProgress;
|
||||
_providerManager.RefreshCompleted += OnProviderRefreshCompleted;
|
||||
_providerManager.RefreshStarted += OnProviderRefreshStarted;
|
||||
_providerManager.RefreshProgress += OnProviderRefreshProgress;
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Dictionary<Guid, DateTime> _lastProgressMessageTimes = new Dictionary<Guid, DateTime>();
|
||||
|
||||
private void _providerManager_RefreshProgress(object sender, GenericEventArgs<Tuple<BaseItem, double>> e)
|
||||
private void OnProviderRefreshProgress(object sender, GenericEventArgs<Tuple<BaseItem, double>> e)
|
||||
{
|
||||
var item = e.Argument.Item1;
|
||||
|
||||
@@ -122,9 +115,11 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
|
||||
foreach (var collectionFolder in collectionFolders)
|
||||
{
|
||||
var collectionFolderDict = new Dictionary<string, string>();
|
||||
collectionFolderDict["ItemId"] = collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture);
|
||||
collectionFolderDict["Progress"] = (collectionFolder.GetRefreshProgress() ?? 0).ToString(CultureInfo.InvariantCulture);
|
||||
var collectionFolderDict = new Dictionary<string, string>
|
||||
{
|
||||
["ItemId"] = collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture),
|
||||
["Progress"] = (collectionFolder.GetRefreshProgress() ?? 0).ToString(CultureInfo.InvariantCulture)
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
@@ -136,21 +131,19 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
}
|
||||
}
|
||||
|
||||
private void _providerManager_RefreshStarted(object sender, GenericEventArgs<BaseItem> e)
|
||||
private void OnProviderRefreshStarted(object sender, GenericEventArgs<BaseItem> e)
|
||||
{
|
||||
_providerManager_RefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 0)));
|
||||
OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 0)));
|
||||
}
|
||||
|
||||
private void _providerManager_RefreshCompleted(object sender, GenericEventArgs<BaseItem> e)
|
||||
private void OnProviderRefreshCompleted(object sender, GenericEventArgs<BaseItem> e)
|
||||
{
|
||||
_providerManager_RefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 100)));
|
||||
OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 100)));
|
||||
}
|
||||
|
||||
private static bool EnableRefreshMessage(BaseItem item)
|
||||
{
|
||||
var folder = item as Folder;
|
||||
|
||||
if (folder == null)
|
||||
if (!(item is Folder folder))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -183,7 +176,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
/// </summary>
|
||||
/// <param name="sender">The source of the event.</param>
|
||||
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
|
||||
void libraryManager_ItemAdded(object sender, ItemChangeEventArgs e)
|
||||
private void OnLibraryItemAdded(object sender, ItemChangeEventArgs e)
|
||||
{
|
||||
if (!FilterItem(e.Item))
|
||||
{
|
||||
@@ -205,8 +198,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite);
|
||||
}
|
||||
|
||||
var parent = e.Item.GetParent() as Folder;
|
||||
if (parent != null)
|
||||
if (e.Item.GetParent() is Folder parent)
|
||||
{
|
||||
_foldersAddedTo.Add(parent);
|
||||
}
|
||||
@@ -220,7 +212,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
/// </summary>
|
||||
/// <param name="sender">The source of the event.</param>
|
||||
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
|
||||
void libraryManager_ItemUpdated(object sender, ItemChangeEventArgs e)
|
||||
private void OnLibraryItemUpdated(object sender, ItemChangeEventArgs e)
|
||||
{
|
||||
if (!FilterItem(e.Item))
|
||||
{
|
||||
@@ -231,8 +223,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
{
|
||||
if (LibraryUpdateTimer == null)
|
||||
{
|
||||
LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration,
|
||||
Timeout.Infinite);
|
||||
LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, Timeout.Infinite);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -248,7 +239,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
/// </summary>
|
||||
/// <param name="sender">The source of the event.</param>
|
||||
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
|
||||
void libraryManager_ItemRemoved(object sender, ItemChangeEventArgs e)
|
||||
private void OnLibraryItemRemoved(object sender, ItemChangeEventArgs e)
|
||||
{
|
||||
if (!FilterItem(e.Item))
|
||||
{
|
||||
@@ -259,16 +250,14 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
{
|
||||
if (LibraryUpdateTimer == null)
|
||||
{
|
||||
LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration,
|
||||
Timeout.Infinite);
|
||||
LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, Timeout.Infinite);
|
||||
}
|
||||
else
|
||||
{
|
||||
LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite);
|
||||
}
|
||||
|
||||
var parent = e.Parent as Folder;
|
||||
if (parent != null)
|
||||
if (e.Parent is Folder parent)
|
||||
{
|
||||
_foldersRemovedFrom.Add(parent);
|
||||
}
|
||||
@@ -486,13 +475,13 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
LibraryUpdateTimer = null;
|
||||
}
|
||||
|
||||
_libraryManager.ItemAdded -= libraryManager_ItemAdded;
|
||||
_libraryManager.ItemUpdated -= libraryManager_ItemUpdated;
|
||||
_libraryManager.ItemRemoved -= libraryManager_ItemRemoved;
|
||||
_libraryManager.ItemAdded -= OnLibraryItemAdded;
|
||||
_libraryManager.ItemUpdated -= OnLibraryItemUpdated;
|
||||
_libraryManager.ItemRemoved -= OnLibraryItemRemoved;
|
||||
|
||||
_providerManager.RefreshCompleted -= _providerManager_RefreshCompleted;
|
||||
_providerManager.RefreshStarted -= _providerManager_RefreshStarted;
|
||||
_providerManager.RefreshProgress -= _providerManager_RefreshProgress;
|
||||
_providerManager.RefreshCompleted -= OnProviderRefreshCompleted;
|
||||
_providerManager.RefreshStarted -= OnProviderRefreshStarted;
|
||||
_providerManager.RefreshProgress -= OnProviderRefreshProgress;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -449,7 +449,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
httpRes.StatusCode = 200;
|
||||
foreach(var (key, value) in GetDefaultCorsHeaders(httpReq))
|
||||
foreach (var (key, value) in GetDefaultCorsHeaders(httpReq))
|
||||
{
|
||||
httpRes.Headers.Add(key, value);
|
||||
}
|
||||
@@ -486,7 +486,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
var handler = GetServiceHandler(httpReq);
|
||||
if (handler != null)
|
||||
{
|
||||
await handler.ProcessRequestAsync(this, httpReq, httpRes, _logger, cancellationToken).ConfigureAwait(false);
|
||||
await handler.ProcessRequestAsync(this, httpReq, httpRes, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -567,7 +567,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
|
||||
WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
|
||||
|
||||
var connection = new WebSocketConnection(
|
||||
using var connection = new WebSocketConnection(
|
||||
_loggerFactory.CreateLogger<WebSocketConnection>(),
|
||||
webSocket,
|
||||
context.Connection.RemoteIpAddress,
|
||||
|
||||
@@ -35,9 +35,9 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
_networkManager = networkManager;
|
||||
}
|
||||
|
||||
public void Authenticate(IRequest request, IAuthenticationAttributes authAttribtues)
|
||||
public void Authenticate(IRequest request, IAuthenticationAttributes authAttributes)
|
||||
{
|
||||
ValidateUser(request, authAttribtues);
|
||||
ValidateUser(request, authAttributes);
|
||||
}
|
||||
|
||||
public User Authenticate(HttpRequest request, IAuthenticationAttributes authAttributes)
|
||||
@@ -63,17 +63,17 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
return auth;
|
||||
}
|
||||
|
||||
private User ValidateUser(IRequest request, IAuthenticationAttributes authAttribtues)
|
||||
private User ValidateUser(IRequest request, IAuthenticationAttributes authAttributes)
|
||||
{
|
||||
// This code is executed before the service
|
||||
var auth = _authorizationContext.GetAuthorizationInfo(request);
|
||||
|
||||
if (!IsExemptFromAuthenticationToken(authAttribtues, request))
|
||||
if (!IsExemptFromAuthenticationToken(authAttributes, request))
|
||||
{
|
||||
ValidateSecurityToken(request, auth.Token);
|
||||
}
|
||||
|
||||
if (authAttribtues.AllowLocalOnly && !request.IsLocal)
|
||||
if (authAttributes.AllowLocalOnly && !request.IsLocal)
|
||||
{
|
||||
throw new SecurityException("Operation not found.");
|
||||
}
|
||||
@@ -87,14 +87,14 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
|
||||
if (user != null)
|
||||
{
|
||||
ValidateUserAccess(user, request, authAttribtues, auth);
|
||||
ValidateUserAccess(user, request, authAttributes);
|
||||
}
|
||||
|
||||
var info = GetTokenInfo(request);
|
||||
|
||||
if (!IsExemptFromRoles(auth, authAttribtues, request, info))
|
||||
if (!IsExemptFromRoles(auth, authAttributes, request, info))
|
||||
{
|
||||
var roles = authAttribtues.GetRoles();
|
||||
var roles = authAttributes.GetRoles();
|
||||
|
||||
ValidateRoles(roles, user);
|
||||
}
|
||||
@@ -118,8 +118,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
private void ValidateUserAccess(
|
||||
User user,
|
||||
IRequest request,
|
||||
IAuthenticationAttributes authAttributes,
|
||||
AuthorizationInfo auth)
|
||||
IAuthenticationAttributes authAttributes)
|
||||
{
|
||||
if (user.HasPermission(PermissionKind.IsDisabled))
|
||||
{
|
||||
@@ -158,6 +157,11 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
return true;
|
||||
}
|
||||
|
||||
if (authAttribtues.IgnoreLegacyAuth)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -237,16 +241,6 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
{
|
||||
throw new AuthenticationException("Access token is invalid or expired.");
|
||||
}
|
||||
|
||||
// if (!string.IsNullOrEmpty(info.UserId))
|
||||
//{
|
||||
// var user = _userManager.GetUserById(info.UserId);
|
||||
|
||||
// if (user == null || user.Configuration.IsDisabled)
|
||||
// {
|
||||
// throw new SecurityException("User account has been disabled.");
|
||||
// }
|
||||
//}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +97,12 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
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"];
|
||||
@@ -276,12 +282,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
|
||||
private static string NormalizeValue(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
return WebUtility.HtmlEncode(value);
|
||||
return string.IsNullOrEmpty(value) ? value : WebUtility.HtmlEncode(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// <summary>
|
||||
/// Class WebSocketConnection.
|
||||
/// </summary>
|
||||
public class WebSocketConnection : IWebSocketConnection
|
||||
public class WebSocketConnection : IWebSocketConnection, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The logger.
|
||||
@@ -119,7 +119,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
Memory<byte> memory = writer.GetMemory(512);
|
||||
try
|
||||
{
|
||||
receiveresult = await _socket.ReceiveAsync(memory, cancellationToken);
|
||||
receiveresult = await _socket.ReceiveAsync(memory, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (WebSocketException ex)
|
||||
{
|
||||
@@ -137,7 +137,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
writer.Advance(bytesRead);
|
||||
|
||||
// Make the data available to the PipeReader
|
||||
FlushResult flushResult = await writer.FlushAsync();
|
||||
FlushResult flushResult = await writer.FlushAsync().ConfigureAwait(false);
|
||||
if (flushResult.IsCompleted)
|
||||
{
|
||||
// The PipeReader stopped reading
|
||||
@@ -223,7 +223,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
|
||||
if (info.MessageType.Equals("KeepAlive", StringComparison.Ordinal))
|
||||
{
|
||||
await SendKeepAliveResponse();
|
||||
await SendKeepAliveResponse().ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -21,6 +21,7 @@ namespace Emby.Server.Implementations.IO
|
||||
private readonly List<string> _affectedPaths = new List<string>();
|
||||
private readonly object _timerLock = new object();
|
||||
private Timer _timer;
|
||||
private bool _disposed;
|
||||
|
||||
public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger)
|
||||
{
|
||||
@@ -213,11 +214,11 @@ namespace Emby.Server.Implementations.IO
|
||||
}
|
||||
}
|
||||
|
||||
private bool _disposed;
|
||||
public void Dispose()
|
||||
{
|
||||
_disposed = true;
|
||||
DisposeTimer();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Emby.Server.Implementations.Images;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Emby.Server.Implementations.Images;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
|
||||
@@ -18,7 +18,21 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
"**/small.jpg",
|
||||
"**/albumart.jpg",
|
||||
"**/*sample*",
|
||||
|
||||
// We have neither non-greedy matching or character group repetitions, working around that here.
|
||||
// https://github.com/dazinator/DotNet.Glob#patterns
|
||||
// .*/sample\..{1,5}
|
||||
"**/sample.?",
|
||||
"**/sample.??",
|
||||
"**/sample.???", // Matches sample.mkv
|
||||
"**/sample.????", // Matches sample.webm
|
||||
"**/sample.?????",
|
||||
"**/*.sample.?",
|
||||
"**/*.sample.??",
|
||||
"**/*.sample.???",
|
||||
"**/*.sample.????",
|
||||
"**/*.sample.?????",
|
||||
"**/sample/*",
|
||||
|
||||
// Directories
|
||||
"**/metadata/**",
|
||||
@@ -64,10 +78,13 @@ namespace Emby.Server.Implementations.Library
|
||||
"**/.grab/**",
|
||||
"**/.grab",
|
||||
|
||||
// Unix hidden files and directories
|
||||
"**/.*/**",
|
||||
// Unix hidden files
|
||||
"**/.*",
|
||||
|
||||
// Mac - if you ever remove the above.
|
||||
// "**/._*",
|
||||
// "**/.DS_Store",
|
||||
|
||||
// thumbs.db
|
||||
"**/thumbs.db",
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
@@ -46,11 +45,11 @@ using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using MediaBrowser.Providers.MediaInfo;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
|
||||
using Genre = MediaBrowser.Controller.Entities.Genre;
|
||||
using Person = MediaBrowser.Controller.Entities.Person;
|
||||
using SortOrder = MediaBrowser.Model.Entities.SortOrder;
|
||||
using VideoResolver = Emby.Naming.Video.VideoResolver;
|
||||
|
||||
namespace Emby.Server.Implementations.Library
|
||||
@@ -63,6 +62,7 @@ namespace Emby.Server.Implementations.Library
|
||||
private const string ShortcutFileExtension = ".mblink";
|
||||
|
||||
private readonly ILogger<LibraryManager> _logger;
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
private readonly ITaskManager _taskManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IUserDataManager _userDataRepository;
|
||||
@@ -74,7 +74,6 @@ namespace Emby.Server.Implementations.Library
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IItemRepository _itemRepository;
|
||||
private readonly ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache;
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
|
||||
/// <summary>
|
||||
@@ -112,6 +111,7 @@ namespace Emby.Server.Implementations.Library
|
||||
/// <param name="mediaEncoder">The media encoder.</param>
|
||||
/// <param name="itemRepository">The item repository.</param>
|
||||
/// <param name="imageProcessor">The image processor.</param>
|
||||
/// <param name="memoryCache">The memory cache.</param>
|
||||
public LibraryManager(
|
||||
IServerApplicationHost appHost,
|
||||
ILogger<LibraryManager> logger,
|
||||
@@ -125,7 +125,8 @@ namespace Emby.Server.Implementations.Library
|
||||
Lazy<IUserViewManager> userviewManagerFactory,
|
||||
IMediaEncoder mediaEncoder,
|
||||
IItemRepository itemRepository,
|
||||
IImageProcessor imageProcessor)
|
||||
IImageProcessor imageProcessor,
|
||||
IMemoryCache memoryCache)
|
||||
{
|
||||
_appHost = appHost;
|
||||
_logger = logger;
|
||||
@@ -140,8 +141,7 @@ namespace Emby.Server.Implementations.Library
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_itemRepository = itemRepository;
|
||||
_imageProcessor = imageProcessor;
|
||||
|
||||
_libraryItemsCache = new ConcurrentDictionary<Guid, BaseItem>();
|
||||
_memoryCache = memoryCache;
|
||||
|
||||
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
|
||||
|
||||
@@ -299,7 +299,7 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
_libraryItemsCache.AddOrUpdate(item.Id, item, delegate { return item; });
|
||||
_memoryCache.Set(item.Id, item);
|
||||
}
|
||||
|
||||
public void DeleteItem(BaseItem item, DeleteOptions options)
|
||||
@@ -447,7 +447,7 @@ namespace Emby.Server.Implementations.Library
|
||||
_itemRepository.DeleteItem(child.Id);
|
||||
}
|
||||
|
||||
_libraryItemsCache.TryRemove(item.Id, out BaseItem removed);
|
||||
_memoryCache.Remove(item.Id);
|
||||
|
||||
ReportItemRemoved(item, parent);
|
||||
}
|
||||
@@ -1248,7 +1248,7 @@ namespace Emby.Server.Implementations.Library
|
||||
throw new ArgumentException("Guid can't be empty", nameof(id));
|
||||
}
|
||||
|
||||
if (_libraryItemsCache.TryGetValue(id, out BaseItem item))
|
||||
if (_memoryCache.TryGetValue(id, out BaseItem item))
|
||||
{
|
||||
return item;
|
||||
}
|
||||
@@ -1591,7 +1591,6 @@ namespace Emby.Server.Implementations.Library
|
||||
public async Task<IEnumerable<Video>> GetIntros(BaseItem item, User user)
|
||||
{
|
||||
var tasks = IntroProviders
|
||||
.OrderBy(i => i.GetType().Name.Contains("Default", StringComparison.OrdinalIgnoreCase) ? 1 : 0)
|
||||
.Take(1)
|
||||
.Select(i => GetIntros(i, item, user));
|
||||
|
||||
|
||||
@@ -46,8 +46,6 @@ namespace Emby.Server.Implementations.Library
|
||||
private readonly Dictionary<string, ILiveStream> _openStreams = new Dictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
|
||||
|
||||
private readonly object _disposeLock = new object();
|
||||
|
||||
private IMediaSourceProvider[] _providers;
|
||||
|
||||
public MediaSourceManager(
|
||||
@@ -623,12 +621,14 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
if (liveStreamInfo is IDirectStreamProvider)
|
||||
{
|
||||
var info = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest
|
||||
{
|
||||
MediaSource = mediaSource,
|
||||
ExtractChapters = false,
|
||||
MediaType = DlnaProfileType.Video
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
var info = await _mediaEncoder.GetMediaInfo(
|
||||
new MediaInfoRequest
|
||||
{
|
||||
MediaSource = mediaSource,
|
||||
ExtractChapters = false,
|
||||
MediaType = DlnaProfileType.Video
|
||||
},
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
mediaSource.MediaStreams = info.MediaStreams;
|
||||
mediaSource.Container = info.Container;
|
||||
@@ -859,11 +859,11 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
private Tuple<IMediaSourceProvider, string> GetProvider(string key)
|
||||
private (IMediaSourceProvider, string) GetProvider(string key)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key))
|
||||
{
|
||||
throw new ArgumentException("key");
|
||||
throw new ArgumentException("Key can't be empty.", nameof(key));
|
||||
}
|
||||
|
||||
var keys = key.Split(new[] { LiveStreamIdDelimeter }, 2);
|
||||
@@ -873,7 +873,7 @@ namespace Emby.Server.Implementations.Library
|
||||
var splitIndex = key.IndexOf(LiveStreamIdDelimeter, StringComparison.Ordinal);
|
||||
var keyId = key.Substring(splitIndex + 1);
|
||||
|
||||
return new Tuple<IMediaSourceProvider, string>(provider, keyId);
|
||||
return (provider, keyId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -893,15 +893,12 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
if (dispose)
|
||||
{
|
||||
lock (_disposeLock)
|
||||
foreach (var key in _openStreams.Keys.ToList())
|
||||
{
|
||||
foreach (var key in _openStreams.Keys.ToList())
|
||||
{
|
||||
var task = CloseLiveStream(key);
|
||||
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
CloseLiveStream(key).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
_liveStreamSemaphore.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
|
||||
|
||||
|
||||
@@ -4,12 +4,12 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Extensions;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using MediaBrowser.Model.Search;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -12,6 +12,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using Emby.Server.Implementations.Library;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
|
||||
@@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
}
|
||||
}
|
||||
|
||||
public class HdHomerunManager : IDisposable
|
||||
public sealed class HdHomerunManager : IDisposable
|
||||
{
|
||||
public const int HdHomeRunPort = 65001;
|
||||
|
||||
@@ -105,6 +105,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
StopStreaming(socket).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken)
|
||||
@@ -162,7 +164,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
}
|
||||
|
||||
_activeTuner = i;
|
||||
var lockKeyString = string.Format("{0:d}", lockKeyValue);
|
||||
var lockKeyString = string.Format(CultureInfo.InvariantCulture, "{0:d}", lockKeyValue);
|
||||
var lockkeyMsg = CreateSetMessage(i, "lockkey", lockKeyString, null);
|
||||
await stream.WriteAsync(lockkeyMsg, 0, lockkeyMsg.Length, cancellationToken).ConfigureAwait(false);
|
||||
int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
|
||||
@@ -173,8 +175,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
continue;
|
||||
}
|
||||
|
||||
var commandList = commands.GetCommands();
|
||||
foreach (var command in commandList)
|
||||
foreach (var command in commands.GetCommands())
|
||||
{
|
||||
var channelMsg = CreateSetMessage(i, command.Item1, command.Item2, lockKeyValue);
|
||||
await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false);
|
||||
@@ -188,7 +189,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
}
|
||||
}
|
||||
|
||||
var targetValue = string.Format("rtp://{0}:{1}", localIp, localPort);
|
||||
var targetValue = string.Format(CultureInfo.InvariantCulture, "rtp://{0}:{1}", localIp, localPort);
|
||||
var targetMsg = CreateSetMessage(i, "target", targetValue, lockKeyValue);
|
||||
|
||||
await stream.WriteAsync(targetMsg, 0, targetMsg.Length, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"CameraImageUploadedFrom": "একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে {0} থেকে",
|
||||
"Books": "বই",
|
||||
"AuthenticationSucceededWithUserName": "{0} যাচাই সফল",
|
||||
"Artists": "শিল্পী",
|
||||
"Artists": "শিল্পীরা",
|
||||
"Application": "অ্যাপ্লিকেশন",
|
||||
"Albums": "অ্যালবামগুলো",
|
||||
"HeaderFavoriteEpisodes": "প্রিব পর্বগুলো",
|
||||
@@ -19,7 +19,7 @@
|
||||
"Genres": "ঘরানা",
|
||||
"Folders": "ফোল্ডারগুলো",
|
||||
"Favorites": "ফেভারিটগুলো",
|
||||
"FailedLoginAttemptWithUserName": "{0} থেকে লগিন করতে ব্যর্থ",
|
||||
"FailedLoginAttemptWithUserName": "{0} লগিন করতে ব্যর্থ হয়েছে",
|
||||
"AppDeviceValues": "এপ: {0}, ডিভাইস: {0}",
|
||||
"VersionNumber": "সংস্করণ {0}",
|
||||
"ValueSpecialEpisodeName": "বিশেষ - {0}",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"Artists": "Interpreten",
|
||||
"AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich angemeldet",
|
||||
"Books": "Bücher",
|
||||
"CameraImageUploadedFrom": "Ein neues Foto wurde von {0} hochgeladen",
|
||||
"CameraImageUploadedFrom": "Ein neues Kamerafoto wurde von {0} hochgeladen",
|
||||
"Channels": "Kanäle",
|
||||
"ChapterNameValue": "Kapitel {0}",
|
||||
"Collections": "Sammlungen",
|
||||
@@ -101,12 +101,12 @@
|
||||
"TaskCleanTranscode": "Lösche Transkodier Pfad",
|
||||
"TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche dazu eingestellt sind automatisch zu updaten und installiert sie.",
|
||||
"TaskUpdatePlugins": "Update Plugins",
|
||||
"TaskRefreshPeopleDescription": "Erneuert Metadaten für Schausteller und Regisseure in deinen Bibliotheken.",
|
||||
"TaskRefreshPeople": "Erneuere Schausteller",
|
||||
"TaskRefreshPeopleDescription": "Erneuert Metadaten für Schauspieler und Regisseure in deinen Bibliotheken.",
|
||||
"TaskRefreshPeople": "Erneuere Schauspieler",
|
||||
"TaskCleanLogsDescription": "Lösche Log Dateien die älter als {0} Tage sind.",
|
||||
"TaskCleanLogs": "Lösche Log Pfad",
|
||||
"TaskRefreshLibraryDescription": "Scanne alle Bibliotheken für hinzugefügte Datein und erneuere Metadaten.",
|
||||
"TaskRefreshLibrary": "Scanne alle Bibliotheken",
|
||||
"TaskRefreshLibrary": "Scanne Medien-Bibliothek",
|
||||
"TaskRefreshChapterImagesDescription": "Kreiert Vorschaubilder für Videos welche Kapitel haben.",
|
||||
"TaskRefreshChapterImages": "Extrahiert Kapitel-Bilder",
|
||||
"TaskCleanCacheDescription": "Löscht Zwischenspeicherdatein die nicht länger von System gebraucht werden.",
|
||||
|
||||
@@ -18,13 +18,13 @@
|
||||
"HeaderAlbumArtists": "אמני האלבום",
|
||||
"HeaderCameraUploads": "העלאות ממצלמה",
|
||||
"HeaderContinueWatching": "המשך לצפות",
|
||||
"HeaderFavoriteAlbums": "אלבומים שאהבתי",
|
||||
"HeaderFavoriteAlbums": "אלבומים מועדפים",
|
||||
"HeaderFavoriteArtists": "אמנים מועדפים",
|
||||
"HeaderFavoriteEpisodes": "פרקים מועדפים",
|
||||
"HeaderFavoriteShows": "סדרות מועדפות",
|
||||
"HeaderFavoriteShows": "תוכניות מועדפות",
|
||||
"HeaderFavoriteSongs": "שירים מועדפים",
|
||||
"HeaderLiveTV": "שידורים חיים",
|
||||
"HeaderNextUp": "הבא",
|
||||
"HeaderNextUp": "הבא בתור",
|
||||
"HeaderRecordingGroups": "קבוצות הקלטה",
|
||||
"HomeVideos": "סרטונים בייתים",
|
||||
"Inherit": "הורש",
|
||||
@@ -45,37 +45,37 @@
|
||||
"NameSeasonNumber": "עונה {0}",
|
||||
"NameSeasonUnknown": "עונה לא ידועה",
|
||||
"NewVersionIsAvailable": "גרסה חדשה של שרת Jellyfin זמינה להורדה.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Application update available",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Application update installed",
|
||||
"NotificationOptionAudioPlayback": "Audio playback started",
|
||||
"NotificationOptionAudioPlaybackStopped": "Audio playback stopped",
|
||||
"NotificationOptionCameraImageUploaded": "Camera image uploaded",
|
||||
"NotificationOptionApplicationUpdateAvailable": "קיים עדכון זמין ליישום",
|
||||
"NotificationOptionApplicationUpdateInstalled": "עדכון ליישום הותקן",
|
||||
"NotificationOptionAudioPlayback": "ניגון שמע החל",
|
||||
"NotificationOptionAudioPlaybackStopped": "ניגון שמע הופסק",
|
||||
"NotificationOptionCameraImageUploaded": "תמונת מצלמה הועלתה",
|
||||
"NotificationOptionInstallationFailed": "התקנה נכשלה",
|
||||
"NotificationOptionNewLibraryContent": "New content added",
|
||||
"NotificationOptionPluginError": "Plugin failure",
|
||||
"NotificationOptionNewLibraryContent": "תוכן חדש הוסף",
|
||||
"NotificationOptionPluginError": "כשלון בתוסף",
|
||||
"NotificationOptionPluginInstalled": "התוסף הותקן",
|
||||
"NotificationOptionPluginUninstalled": "התוסף הוסר",
|
||||
"NotificationOptionPluginUpdateInstalled": "העדכון לתוסף הותקן",
|
||||
"NotificationOptionServerRestartRequired": "יש לאתחל את השרת",
|
||||
"NotificationOptionTaskFailed": "Scheduled task failure",
|
||||
"NotificationOptionUserLockedOut": "User locked out",
|
||||
"NotificationOptionVideoPlayback": "Video playback started",
|
||||
"NotificationOptionVideoPlaybackStopped": "Video playback stopped",
|
||||
"NotificationOptionTaskFailed": "משימה מתוזמנת נכשלה",
|
||||
"NotificationOptionUserLockedOut": "משתמש ננעל",
|
||||
"NotificationOptionVideoPlayback": "ניגון וידאו החל",
|
||||
"NotificationOptionVideoPlaybackStopped": "ניגון וידאו הופסק",
|
||||
"Photos": "תמונות",
|
||||
"Playlists": "רשימות הפעלה",
|
||||
"Plugin": "Plugin",
|
||||
"PluginInstalledWithName": "{0} was installed",
|
||||
"PluginUninstalledWithName": "{0} was uninstalled",
|
||||
"PluginUpdatedWithName": "{0} was updated",
|
||||
"PluginInstalledWithName": "{0} הותקן",
|
||||
"PluginUninstalledWithName": "{0} הוסר",
|
||||
"PluginUpdatedWithName": "{0} עודכן",
|
||||
"ProviderValue": "Provider: {0}",
|
||||
"ScheduledTaskFailedWithName": "{0} failed",
|
||||
"ScheduledTaskStartedWithName": "{0} started",
|
||||
"ServerNameNeedsToBeRestarted": "{0} needs to be restarted",
|
||||
"ScheduledTaskFailedWithName": "{0} נכשל",
|
||||
"ScheduledTaskStartedWithName": "{0} החל",
|
||||
"ServerNameNeedsToBeRestarted": "{0} דורש הפעלה מחדש",
|
||||
"Shows": "סדרות",
|
||||
"Songs": "שירים",
|
||||
"StartupEmbyServerIsLoading": "שרת Jellyfin בהליכי טעינה. אנא נסה שנית בעוד זמן קצר.",
|
||||
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
||||
"SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
|
||||
"SubtitleDownloadFailureFromForItem": "הורדת כתוביות נכשלה מ-{0} עבור {1}",
|
||||
"Sync": "סנכרן",
|
||||
"System": "System",
|
||||
"TvShows": "סדרות טלוויזיה",
|
||||
@@ -83,14 +83,14 @@
|
||||
"UserCreatedWithName": "המשתמש {0} נוצר",
|
||||
"UserDeletedWithName": "המשתמש {0} הוסר",
|
||||
"UserDownloadingItemWithValues": "{0} מוריד את {1}",
|
||||
"UserLockedOutWithName": "User {0} has been locked out",
|
||||
"UserOfflineFromDevice": "{0} has disconnected from {1}",
|
||||
"UserOnlineFromDevice": "{0} is online from {1}",
|
||||
"UserPasswordChangedWithName": "Password has been changed for user {0}",
|
||||
"UserPolicyUpdatedWithName": "User policy has been updated for {0}",
|
||||
"UserLockedOutWithName": "המשתמש {0} ננעל",
|
||||
"UserOfflineFromDevice": "{0} התנתק מ-{1}",
|
||||
"UserOnlineFromDevice": "{0} מחובר מ-{1}",
|
||||
"UserPasswordChangedWithName": "הסיסמה שונתה עבור המשתמש {0}",
|
||||
"UserPolicyUpdatedWithName": "מדיניות המשתמש {0} עודכנה",
|
||||
"UserStartedPlayingItemWithValues": "{0} מנגן את {1} על {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} סיים לנגן את {1} על {2}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
|
||||
"ValueHasBeenAddedToLibrary": "{0} התווסף לספריית המדיה שלך",
|
||||
"ValueSpecialEpisodeName": "מיוחד- {0}",
|
||||
"VersionNumber": "Version {0}",
|
||||
"TaskRefreshLibrary": "סרוק ספריית מדיה",
|
||||
@@ -109,7 +109,7 @@
|
||||
"TaskRefreshChapterImagesDescription": "יוצר תמונות ממוזערות לסרטונים שיש להם פרקים.",
|
||||
"TasksChannelsCategory": "ערוצי אינטרנט",
|
||||
"TaskDownloadMissingSubtitlesDescription": "חפש באינטרנט עבור הכתוביות החסרות בהתבסס על המטה-דיאטה.",
|
||||
"TaskDownloadMissingSubtitles": "הורד כתוביות חסרות.",
|
||||
"TaskDownloadMissingSubtitles": "הורד כתוביות חסרות",
|
||||
"TaskRefreshChannelsDescription": "רענן פרטי ערוץ אינטרנטי.",
|
||||
"TaskRefreshChannels": "רענן ערוץ",
|
||||
"TaskCleanTranscodeDescription": "מחק קבצי transcode שנוצרו מלפני יותר מיום.",
|
||||
|
||||
@@ -84,8 +84,8 @@
|
||||
"UserDeletedWithName": "L'utente {0} è stato rimosso",
|
||||
"UserDownloadingItemWithValues": "{0} sta scaricando {1}",
|
||||
"UserLockedOutWithName": "L'utente {0} è stato bloccato",
|
||||
"UserOfflineFromDevice": "{0} è stato disconnesso da {1}",
|
||||
"UserOnlineFromDevice": "{0} è online da {1}",
|
||||
"UserOfflineFromDevice": "{0} si è disconnesso su {1}",
|
||||
"UserOnlineFromDevice": "{0} è online su {1}",
|
||||
"UserPasswordChangedWithName": "La password è stata cambiata per l'utente {0}",
|
||||
"UserPolicyUpdatedWithName": "La policy dell'utente è stata aggiornata per {0}",
|
||||
"UserStartedPlayingItemWithValues": "{0} ha avviato la riproduzione di {1} su {2}",
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"MusicVideos": "Музичні відео",
|
||||
"MusicVideos": "Музичні кліпи",
|
||||
"Music": "Музика",
|
||||
"Movies": "Фільми",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin Server був оновлений до версії {0}",
|
||||
"MessageApplicationUpdated": "Jellyfin Server був оновлений",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin Server оновлено до версії {0}",
|
||||
"MessageApplicationUpdated": "Jellyfin Server оновлено",
|
||||
"Latest": "Останні",
|
||||
"LabelIpAddressValue": "IP-адреси: {0}",
|
||||
"ItemRemovedWithName": "{0} видалено з бібліотеки",
|
||||
"ItemAddedWithName": "{0} додано до бібліотеки",
|
||||
"LabelIpAddressValue": "IP-адреса: {0}",
|
||||
"ItemRemovedWithName": "{0} видалено з медіатеки",
|
||||
"ItemAddedWithName": "{0} додано до медіатеки",
|
||||
"HeaderNextUp": "Наступний",
|
||||
"HeaderLiveTV": "Ефірне ТБ",
|
||||
"HeaderFavoriteSongs": "Улюблені пісні",
|
||||
@@ -17,20 +17,101 @@
|
||||
"HeaderFavoriteAlbums": "Улюблені альбоми",
|
||||
"HeaderContinueWatching": "Продовжити перегляд",
|
||||
"HeaderCameraUploads": "Завантажено з камери",
|
||||
"HeaderAlbumArtists": "Виконавці альбомів",
|
||||
"HeaderAlbumArtists": "Виконавці альбому",
|
||||
"Genres": "Жанри",
|
||||
"Folders": "Директорії",
|
||||
"Folders": "Каталоги",
|
||||
"Favorites": "Улюблені",
|
||||
"DeviceOnlineWithName": "{0} під'єднано",
|
||||
"DeviceOfflineWithName": "{0} від'єднано",
|
||||
"DeviceOnlineWithName": "Пристрій {0} підключився",
|
||||
"DeviceOfflineWithName": "Пристрій {0} відключився",
|
||||
"Collections": "Колекції",
|
||||
"ChapterNameValue": "Глава {0}",
|
||||
"ChapterNameValue": "Розділ {0}",
|
||||
"Channels": "Канали",
|
||||
"CameraImageUploadedFrom": "Нова фотографія завантажена з {0}",
|
||||
"Books": "Книги",
|
||||
"AuthenticationSucceededWithUserName": "{0} успішно авторизовані",
|
||||
"AuthenticationSucceededWithUserName": "{0} успішно авторизований",
|
||||
"Artists": "Виконавці",
|
||||
"Application": "Додаток",
|
||||
"AppDeviceValues": "Додаток: {0}, Пристрій: {1}",
|
||||
"Albums": "Альбоми"
|
||||
"Albums": "Альбоми",
|
||||
"NotificationOptionServerRestartRequired": "Необхідно перезапустити сервер",
|
||||
"NotificationOptionPluginUpdateInstalled": "Встановлено оновлення плагіна",
|
||||
"NotificationOptionPluginUninstalled": "Плагін видалено",
|
||||
"NotificationOptionPluginInstalled": "Плагін встановлено",
|
||||
"NotificationOptionPluginError": "Помилка плагіна",
|
||||
"NotificationOptionNewLibraryContent": "Додано новий контент",
|
||||
"HomeVideos": "Домашнє відео",
|
||||
"FailedLoginAttemptWithUserName": "Невдала спроба входу від {0}",
|
||||
"LabelRunningTimeValue": "Тривалість: {0}",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Шукає в Інтернеті відсутні субтитри на основі конфігурації метаданих.",
|
||||
"TaskDownloadMissingSubtitles": "Завантажити відсутні субтитри",
|
||||
"TaskRefreshChannelsDescription": "Оновлення інформації про Інтернет-канали.",
|
||||
"TaskRefreshChannels": "Оновити канали",
|
||||
"TaskCleanTranscodeDescription": "Вилучає файли для перекодування старше одного дня.",
|
||||
"TaskCleanTranscode": "Очистити каталог перекодування",
|
||||
"TaskUpdatePluginsDescription": "Завантажує та встановлює оновлення для плагінів, налаштованих на автоматичне оновлення.",
|
||||
"TaskUpdatePlugins": "Оновити плагіни",
|
||||
"TaskRefreshPeopleDescription": "Оновлення метаданих для акторів та режисерів у вашій медіатеці.",
|
||||
"TaskRefreshPeople": "Оновити людей",
|
||||
"TaskCleanLogsDescription": "Видаляє файли журналу, яким більше {0} днів.",
|
||||
"TaskCleanLogs": "Очистити журнали",
|
||||
"TaskRefreshLibraryDescription": "Сканує медіатеку на нові файли та оновлює метадані.",
|
||||
"TaskRefreshLibrary": "Сканувати медіатеку",
|
||||
"TaskRefreshChapterImagesDescription": "Створює ескізи для відео, які мають розділи.",
|
||||
"TaskRefreshChapterImages": "Створити ескізи розділів",
|
||||
"TaskCleanCacheDescription": "Видаляє файли кешу, які більше не потрібні системі.",
|
||||
"TaskCleanCache": "Очистити кеш",
|
||||
"TasksChannelsCategory": "Інтернет-канали",
|
||||
"TasksApplicationCategory": "Додаток",
|
||||
"TasksLibraryCategory": "Медіатека",
|
||||
"TasksMaintenanceCategory": "Обслуговування",
|
||||
"VersionNumber": "Версія {0}",
|
||||
"ValueSpecialEpisodeName": "Спецепізод - {0}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} додано до медіатеки",
|
||||
"UserStoppedPlayingItemWithValues": "{0} закінчив відтворення {1} на {2}",
|
||||
"UserStartedPlayingItemWithValues": "{0} відтворює {1} на {2}",
|
||||
"UserPolicyUpdatedWithName": "Політика користувача оновлена для {0}",
|
||||
"UserPasswordChangedWithName": "Пароль змінено для користувача {0}",
|
||||
"UserOnlineFromDevice": "{0} підключився з {1}",
|
||||
"UserOfflineFromDevice": "{0} відключився від {1}",
|
||||
"UserLockedOutWithName": "Користувача {0} заблоковано",
|
||||
"UserDownloadingItemWithValues": "{0} завантажує {1}",
|
||||
"UserDeletedWithName": "Користувача {0} видалено",
|
||||
"UserCreatedWithName": "Користувача {0} створено",
|
||||
"User": "Користувач",
|
||||
"TvShows": "ТВ-шоу",
|
||||
"System": "Система",
|
||||
"Sync": "Синхронізація",
|
||||
"SubtitleDownloadFailureFromForItem": "Не вдалося завантажити субтитри з {0} для {1}",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin Server завантажується. Будь ласка, спробуйте трішки пізніше.",
|
||||
"Songs": "Пісні",
|
||||
"Shows": "Шоу",
|
||||
"ServerNameNeedsToBeRestarted": "{0} потрібно перезапустити",
|
||||
"ScheduledTaskStartedWithName": "{0} розпочато",
|
||||
"ScheduledTaskFailedWithName": "Помилка {0}",
|
||||
"ProviderValue": "Постачальник: {0}",
|
||||
"PluginUpdatedWithName": "{0} оновлено",
|
||||
"PluginUninstalledWithName": "{0} видалено",
|
||||
"PluginInstalledWithName": "{0} встановлено",
|
||||
"Plugin": "Плагін",
|
||||
"Playlists": "Плейлисти",
|
||||
"Photos": "Фотографії",
|
||||
"NotificationOptionVideoPlaybackStopped": "Відтворення відео зупинено",
|
||||
"NotificationOptionVideoPlayback": "Розпочато відтворення відео",
|
||||
"NotificationOptionUserLockedOut": "Користувача заблоковано",
|
||||
"NotificationOptionTaskFailed": "Помилка запланованого завдання",
|
||||
"NotificationOptionInstallationFailed": "Помилка встановлення",
|
||||
"NotificationOptionCameraImageUploaded": "Фотографію завантажено",
|
||||
"NotificationOptionAudioPlaybackStopped": "Відтворення аудіо зупинено",
|
||||
"NotificationOptionAudioPlayback": "Розпочато відтворення аудіо",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Встановлено оновлення додатка",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Доступне оновлення додатка",
|
||||
"NewVersionIsAvailable": "Для завантаження доступна нова версія Jellyfin Server.",
|
||||
"NameSeasonUnknown": "Сезон Невідомий",
|
||||
"NameSeasonNumber": "Сезон {0}",
|
||||
"NameInstallFailed": "Не вдалося встановити {0}",
|
||||
"MixedContent": "Змішаний контент",
|
||||
"MessageServerConfigurationUpdated": "Конфігурація сервера оновлена",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Розділ конфігурації сервера {0} оновлено",
|
||||
"Inherit": "Успадкувати",
|
||||
"HeaderRecordingGroups": "Групи запису"
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
"HeaderRecordingGroups": "錄製組",
|
||||
"Inherit": "繼承",
|
||||
"SubtitleDownloadFailureFromForItem": "無法為 {1} 從 {0} 下載字幕",
|
||||
"TaskDownloadMissingSubtitlesDescription": "在網路上透過描述資料搜尋遺失的字幕。",
|
||||
"TaskDownloadMissingSubtitlesDescription": "在網路上透過中繼資料搜尋遺失的字幕。",
|
||||
"TaskDownloadMissingSubtitles": "下載遺失的字幕",
|
||||
"TaskRefreshChannels": "重新整理頻道",
|
||||
"TaskUpdatePlugins": "更新插件",
|
||||
|
||||
@@ -15,13 +15,11 @@ namespace Emby.Server.Implementations.Net
|
||||
public sealed class UdpSocket : ISocket, IDisposable
|
||||
{
|
||||
private Socket _socket;
|
||||
private int _localPort;
|
||||
private readonly int _localPort;
|
||||
private bool _disposed = false;
|
||||
|
||||
public Socket Socket => _socket;
|
||||
|
||||
public IPAddress LocalIPAddress { get; }
|
||||
|
||||
private readonly SocketAsyncEventArgs _receiveSocketAsyncEventArgs = new SocketAsyncEventArgs()
|
||||
{
|
||||
SocketFlags = SocketFlags.None
|
||||
@@ -51,18 +49,33 @@ namespace Emby.Server.Implementations.Net
|
||||
InitReceiveSocketAsyncEventArgs();
|
||||
}
|
||||
|
||||
public UdpSocket(Socket socket, IPEndPoint endPoint)
|
||||
{
|
||||
if (socket == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(socket));
|
||||
}
|
||||
|
||||
_socket = socket;
|
||||
_socket.Connect(endPoint);
|
||||
|
||||
InitReceiveSocketAsyncEventArgs();
|
||||
}
|
||||
|
||||
public IPAddress LocalIPAddress { get; }
|
||||
|
||||
private void InitReceiveSocketAsyncEventArgs()
|
||||
{
|
||||
var receiveBuffer = new byte[8192];
|
||||
_receiveSocketAsyncEventArgs.SetBuffer(receiveBuffer, 0, receiveBuffer.Length);
|
||||
_receiveSocketAsyncEventArgs.Completed += _receiveSocketAsyncEventArgs_Completed;
|
||||
_receiveSocketAsyncEventArgs.Completed += OnReceiveSocketAsyncEventArgsCompleted;
|
||||
|
||||
var sendBuffer = new byte[8192];
|
||||
_sendSocketAsyncEventArgs.SetBuffer(sendBuffer, 0, sendBuffer.Length);
|
||||
_sendSocketAsyncEventArgs.Completed += _sendSocketAsyncEventArgs_Completed;
|
||||
_sendSocketAsyncEventArgs.Completed += OnSendSocketAsyncEventArgsCompleted;
|
||||
}
|
||||
|
||||
private void _receiveSocketAsyncEventArgs_Completed(object sender, SocketAsyncEventArgs e)
|
||||
private void OnReceiveSocketAsyncEventArgsCompleted(object sender, SocketAsyncEventArgs e)
|
||||
{
|
||||
var tcs = _currentReceiveTaskCompletionSource;
|
||||
if (tcs != null)
|
||||
@@ -86,7 +99,7 @@ namespace Emby.Server.Implementations.Net
|
||||
}
|
||||
}
|
||||
|
||||
private void _sendSocketAsyncEventArgs_Completed(object sender, SocketAsyncEventArgs e)
|
||||
private void OnSendSocketAsyncEventArgsCompleted(object sender, SocketAsyncEventArgs e)
|
||||
{
|
||||
var tcs = _currentSendTaskCompletionSource;
|
||||
if (tcs != null)
|
||||
@@ -104,19 +117,6 @@ namespace Emby.Server.Implementations.Net
|
||||
}
|
||||
}
|
||||
|
||||
public UdpSocket(Socket socket, IPEndPoint endPoint)
|
||||
{
|
||||
if (socket == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(socket));
|
||||
}
|
||||
|
||||
_socket = socket;
|
||||
_socket.Connect(endPoint);
|
||||
|
||||
InitReceiveSocketAsyncEventArgs();
|
||||
}
|
||||
|
||||
public IAsyncResult BeginReceive(byte[] buffer, int offset, int count, AsyncCallback callback)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
@@ -247,6 +247,7 @@ namespace Emby.Server.Implementations.Net
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
@@ -255,6 +256,8 @@ namespace Emby.Server.Implementations.Net
|
||||
}
|
||||
|
||||
_socket?.Dispose();
|
||||
_receiveSocketAsyncEventArgs.Dispose();
|
||||
_sendSocketAsyncEventArgs.Dispose();
|
||||
_currentReceiveTaskCompletionSource?.TrySetCanceled();
|
||||
_currentSendTaskCompletionSource?.TrySetCanceled();
|
||||
|
||||
|
||||
@@ -165,7 +165,7 @@ namespace Emby.Server.Implementations.Networking
|
||||
(octet[0] == 127) || // RFC1122
|
||||
(octet[0] == 169 && octet[1] == 254)) // RFC3927
|
||||
{
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (checkSubnets && IsInPrivateAddressSpaceAndLocalSubnet(endpoint))
|
||||
|
||||
@@ -349,16 +349,14 @@ namespace Emby.Server.Implementations.Playlists
|
||||
AlbumTitle = child.Album
|
||||
};
|
||||
|
||||
var hasAlbumArtist = child as IHasAlbumArtist;
|
||||
if (hasAlbumArtist != null)
|
||||
if (child is IHasAlbumArtist hasAlbumArtist)
|
||||
{
|
||||
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
|
||||
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
|
||||
}
|
||||
|
||||
var hasArtist = child as IHasArtist;
|
||||
if (hasArtist != null)
|
||||
if (child is IHasArtist hasArtist)
|
||||
{
|
||||
entry.TrackArtist = hasArtist.Artists.FirstOrDefault();
|
||||
entry.TrackArtist = hasArtist.Artists.Count > 0 ? hasArtist.Artists[0] : null;
|
||||
}
|
||||
|
||||
if (child.RunTimeTicks.HasValue)
|
||||
@@ -385,16 +383,14 @@ namespace Emby.Server.Implementations.Playlists
|
||||
AlbumTitle = child.Album
|
||||
};
|
||||
|
||||
var hasAlbumArtist = child as IHasAlbumArtist;
|
||||
if (hasAlbumArtist != null)
|
||||
if (child is IHasAlbumArtist hasAlbumArtist)
|
||||
{
|
||||
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
|
||||
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
|
||||
}
|
||||
|
||||
var hasArtist = child as IHasArtist;
|
||||
if (hasArtist != null)
|
||||
if (child is IHasArtist hasArtist)
|
||||
{
|
||||
entry.TrackArtist = hasArtist.Artists.FirstOrDefault();
|
||||
entry.TrackArtist = hasArtist.Artists.Count > 0 ? hasArtist.Artists[0] : null;
|
||||
}
|
||||
|
||||
if (child.RunTimeTicks.HasValue)
|
||||
@@ -411,8 +407,10 @@ namespace Emby.Server.Implementations.Playlists
|
||||
|
||||
if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var playlist = new M3uPlaylist();
|
||||
playlist.IsExtended = true;
|
||||
var playlist = new M3uPlaylist
|
||||
{
|
||||
IsExtended = true
|
||||
};
|
||||
foreach (var child in item.GetLinkedChildren())
|
||||
{
|
||||
var entry = new M3uPlaylistEntry()
|
||||
@@ -422,10 +420,9 @@ namespace Emby.Server.Implementations.Playlists
|
||||
Album = child.Album
|
||||
};
|
||||
|
||||
var hasAlbumArtist = child as IHasAlbumArtist;
|
||||
if (hasAlbumArtist != null)
|
||||
if (child is IHasAlbumArtist hasAlbumArtist)
|
||||
{
|
||||
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
|
||||
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
|
||||
}
|
||||
|
||||
if (child.RunTimeTicks.HasValue)
|
||||
@@ -453,10 +450,9 @@ namespace Emby.Server.Implementations.Playlists
|
||||
Album = child.Album
|
||||
};
|
||||
|
||||
var hasAlbumArtist = child as IHasAlbumArtist;
|
||||
if (hasAlbumArtist != null)
|
||||
if (child is IHasAlbumArtist hasAlbumArtist)
|
||||
{
|
||||
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
|
||||
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
|
||||
}
|
||||
|
||||
if (child.RunTimeTicks.HasValue)
|
||||
@@ -514,7 +510,7 @@ namespace Emby.Server.Implementations.Playlists
|
||||
|
||||
if (!folderPath.EndsWith(Path.DirectorySeparatorChar))
|
||||
{
|
||||
folderPath = folderPath + Path.DirectorySeparatorChar;
|
||||
folderPath += Path.DirectorySeparatorChar;
|
||||
}
|
||||
|
||||
var folderUri = new Uri(folderPath);
|
||||
@@ -537,32 +533,12 @@ namespace Emby.Server.Implementations.Playlists
|
||||
return relativePath;
|
||||
}
|
||||
|
||||
private static string UnEscape(string content)
|
||||
{
|
||||
if (content == null)
|
||||
{
|
||||
return content;
|
||||
}
|
||||
|
||||
return content.Replace("&", "&").Replace("'", "'").Replace(""", "\"").Replace(">", ">").Replace("<", "<");
|
||||
}
|
||||
|
||||
private static string Escape(string content)
|
||||
{
|
||||
if (content == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return content.Replace("&", "&").Replace("'", "'").Replace("\"", """).Replace(">", ">").Replace("<", "<");
|
||||
}
|
||||
|
||||
public Folder GetPlaylistsFolder(Guid userId)
|
||||
{
|
||||
var typeName = "PlaylistsFolder";
|
||||
const string TypeName = "PlaylistsFolder";
|
||||
|
||||
return _libraryManager.RootFolder.Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, typeName, StringComparison.Ordinal)) ??
|
||||
_libraryManager.GetUserRootFolder().Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, typeName, StringComparison.Ordinal));
|
||||
return _libraryManager.RootFolder.Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, TypeName, StringComparison.Ordinal)) ??
|
||||
_libraryManager.GetUserRootFolder().Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, TypeName, StringComparison.Ordinal));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Server.Implementations.HttpServer;
|
||||
using MediaBrowser.Model.Services;
|
||||
@@ -91,12 +92,22 @@ namespace Emby.Server.Implementations.Services
|
||||
{
|
||||
if (restPath.Path[0] != '/')
|
||||
{
|
||||
throw new ArgumentException(string.Format("Route '{0}' on '{1}' must start with a '/'", restPath.Path, restPath.RequestType.GetMethodName()));
|
||||
throw new ArgumentException(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"Route '{0}' on '{1}' must start with a '/'",
|
||||
restPath.Path,
|
||||
restPath.RequestType.GetMethodName()));
|
||||
}
|
||||
|
||||
if (restPath.Path.IndexOfAny(InvalidRouteChars) != -1)
|
||||
{
|
||||
throw new ArgumentException(string.Format("Route '{0}' on '{1}' contains invalid chars. ", restPath.Path, restPath.RequestType.GetMethodName()));
|
||||
throw new ArgumentException(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"Route '{0}' on '{1}' contains invalid chars. ",
|
||||
restPath.Path,
|
||||
restPath.RequestType.GetMethodName()));
|
||||
}
|
||||
|
||||
if (RestPathMap.TryGetValue(restPath.FirstMatchHashKey, out List<RestPath> pathsAtFirstMatch))
|
||||
@@ -179,8 +190,7 @@ namespace Emby.Server.Implementations.Services
|
||||
|
||||
var service = httpHost.CreateInstance(serviceType);
|
||||
|
||||
var serviceRequiresContext = service as IRequiresRequest;
|
||||
if (serviceRequiresContext != null)
|
||||
if (service is IRequiresRequest serviceRequiresContext)
|
||||
{
|
||||
serviceRequiresContext.Request = req;
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ namespace Emby.Server.Implementations.Services
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task ProcessRequestAsync(HttpListenerHost httpHost, IRequest httpReq, HttpResponse httpRes, ILogger logger, CancellationToken cancellationToken)
|
||||
public async Task ProcessRequestAsync(HttpListenerHost httpHost, IRequest httpReq, HttpResponse httpRes, CancellationToken cancellationToken)
|
||||
{
|
||||
httpReq.Items["__route"] = _restPath;
|
||||
|
||||
@@ -80,10 +80,10 @@ namespace Emby.Server.Implementations.Services
|
||||
httpReq.ResponseContentType = _responseContentType;
|
||||
}
|
||||
|
||||
var request = await CreateRequest(httpHost, httpReq, _restPath, logger).ConfigureAwait(false);
|
||||
var request = await CreateRequest(httpHost, httpReq, _restPath).ConfigureAwait(false);
|
||||
|
||||
httpHost.ApplyRequestFilters(httpReq, httpRes, request);
|
||||
|
||||
|
||||
httpRes.HttpContext.SetServiceStackRequest(httpReq);
|
||||
var response = await httpHost.ServiceController.Execute(httpHost, request, httpReq).ConfigureAwait(false);
|
||||
|
||||
@@ -96,7 +96,7 @@ namespace Emby.Server.Implementations.Services
|
||||
await ResponseHelper.WriteToResponse(httpRes, httpReq, response, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public static async Task<object> CreateRequest(HttpListenerHost host, IRequest httpReq, RestPath restPath, ILogger logger)
|
||||
public static async Task<object> CreateRequest(HttpListenerHost host, IRequest httpReq, RestPath restPath)
|
||||
{
|
||||
var requestType = restPath.RequestType;
|
||||
|
||||
|
||||
@@ -1,287 +0,0 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Emby.Server.Implementations.HttpServer;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Services;
|
||||
|
||||
namespace Emby.Server.Implementations.Services
|
||||
{
|
||||
[Route("/swagger", "GET", Summary = "Gets the swagger specifications")]
|
||||
[Route("/swagger.json", "GET", Summary = "Gets the swagger specifications")]
|
||||
public class GetSwaggerSpec : IReturn<SwaggerSpec>
|
||||
{
|
||||
}
|
||||
|
||||
public class SwaggerSpec
|
||||
{
|
||||
public string swagger { get; set; }
|
||||
|
||||
public string[] schemes { get; set; }
|
||||
|
||||
public SwaggerInfo info { get; set; }
|
||||
|
||||
public string host { get; set; }
|
||||
|
||||
public string basePath { get; set; }
|
||||
|
||||
public SwaggerTag[] tags { get; set; }
|
||||
|
||||
public IDictionary<string, Dictionary<string, SwaggerMethod>> paths { get; set; }
|
||||
|
||||
public Dictionary<string, SwaggerDefinition> definitions { get; set; }
|
||||
|
||||
public SwaggerComponents components { get; set; }
|
||||
}
|
||||
|
||||
public class SwaggerComponents
|
||||
{
|
||||
public Dictionary<string, SwaggerSecurityScheme> securitySchemes { get; set; }
|
||||
}
|
||||
|
||||
public class SwaggerSecurityScheme
|
||||
{
|
||||
public string name { get; set; }
|
||||
|
||||
public string type { get; set; }
|
||||
|
||||
public string @in { get; set; }
|
||||
}
|
||||
|
||||
public class SwaggerInfo
|
||||
{
|
||||
public string description { get; set; }
|
||||
|
||||
public string version { get; set; }
|
||||
|
||||
public string title { get; set; }
|
||||
|
||||
public string termsOfService { get; set; }
|
||||
|
||||
public SwaggerConcactInfo contact { get; set; }
|
||||
}
|
||||
|
||||
public class SwaggerConcactInfo
|
||||
{
|
||||
public string email { get; set; }
|
||||
|
||||
public string name { get; set; }
|
||||
|
||||
public string url { get; set; }
|
||||
}
|
||||
|
||||
public class SwaggerTag
|
||||
{
|
||||
public string description { get; set; }
|
||||
|
||||
public string name { get; set; }
|
||||
}
|
||||
|
||||
public class SwaggerMethod
|
||||
{
|
||||
public string summary { get; set; }
|
||||
|
||||
public string description { get; set; }
|
||||
|
||||
public string[] tags { get; set; }
|
||||
|
||||
public string operationId { get; set; }
|
||||
|
||||
public string[] consumes { get; set; }
|
||||
|
||||
public string[] produces { get; set; }
|
||||
|
||||
public SwaggerParam[] parameters { get; set; }
|
||||
|
||||
public Dictionary<string, SwaggerResponse> responses { get; set; }
|
||||
|
||||
public Dictionary<string, string[]>[] security { get; set; }
|
||||
}
|
||||
|
||||
public class SwaggerParam
|
||||
{
|
||||
public string @in { get; set; }
|
||||
|
||||
public string name { get; set; }
|
||||
|
||||
public string description { get; set; }
|
||||
|
||||
public bool required { get; set; }
|
||||
|
||||
public string type { get; set; }
|
||||
|
||||
public string collectionFormat { get; set; }
|
||||
}
|
||||
|
||||
public class SwaggerResponse
|
||||
{
|
||||
public string description { get; set; }
|
||||
|
||||
// ex. "$ref":"#/definitions/Pet"
|
||||
public Dictionary<string, string> schema { get; set; }
|
||||
}
|
||||
|
||||
public class SwaggerDefinition
|
||||
{
|
||||
public string type { get; set; }
|
||||
|
||||
public Dictionary<string, SwaggerProperty> properties { get; set; }
|
||||
}
|
||||
|
||||
public class SwaggerProperty
|
||||
{
|
||||
public string type { get; set; }
|
||||
|
||||
public string format { get; set; }
|
||||
|
||||
public string description { get; set; }
|
||||
|
||||
public string[] @enum { get; set; }
|
||||
|
||||
public string @default { get; set; }
|
||||
}
|
||||
|
||||
public class SwaggerService : IService, IRequiresRequest
|
||||
{
|
||||
private readonly IHttpServer _httpServer;
|
||||
private SwaggerSpec _spec;
|
||||
|
||||
public IRequest Request { get; set; }
|
||||
|
||||
public SwaggerService(IHttpServer httpServer)
|
||||
{
|
||||
_httpServer = httpServer;
|
||||
}
|
||||
|
||||
public object Get(GetSwaggerSpec request)
|
||||
{
|
||||
return _spec ?? (_spec = GetSpec());
|
||||
}
|
||||
|
||||
private SwaggerSpec GetSpec()
|
||||
{
|
||||
string host = null;
|
||||
Uri uri;
|
||||
if (Uri.TryCreate(Request.RawUrl, UriKind.Absolute, out uri))
|
||||
{
|
||||
host = uri.Host;
|
||||
}
|
||||
|
||||
var securitySchemes = new Dictionary<string, SwaggerSecurityScheme>();
|
||||
|
||||
securitySchemes["api_key"] = new SwaggerSecurityScheme
|
||||
{
|
||||
name = "api_key",
|
||||
type = "apiKey",
|
||||
@in = "query"
|
||||
};
|
||||
|
||||
var spec = new SwaggerSpec
|
||||
{
|
||||
schemes = new[] { "http" },
|
||||
tags = GetTags(),
|
||||
swagger = "2.0",
|
||||
info = new SwaggerInfo
|
||||
{
|
||||
title = "Jellyfin Server API",
|
||||
version = "1.0.0",
|
||||
description = "Explore the Jellyfin Server API",
|
||||
contact = new SwaggerConcactInfo
|
||||
{
|
||||
name = "Jellyfin Community",
|
||||
url = "https://jellyfin.readthedocs.io/en/latest/user-docs/getting-help/"
|
||||
}
|
||||
},
|
||||
paths = GetPaths(),
|
||||
definitions = GetDefinitions(),
|
||||
basePath = "/jellyfin",
|
||||
host = host,
|
||||
|
||||
components = new SwaggerComponents
|
||||
{
|
||||
securitySchemes = securitySchemes
|
||||
}
|
||||
};
|
||||
|
||||
return spec;
|
||||
}
|
||||
|
||||
|
||||
private SwaggerTag[] GetTags()
|
||||
{
|
||||
return Array.Empty<SwaggerTag>();
|
||||
}
|
||||
|
||||
private Dictionary<string, SwaggerDefinition> GetDefinitions()
|
||||
{
|
||||
return new Dictionary<string, SwaggerDefinition>();
|
||||
}
|
||||
|
||||
private IDictionary<string, Dictionary<string, SwaggerMethod>> GetPaths()
|
||||
{
|
||||
var paths = new SortedDictionary<string, Dictionary<string, SwaggerMethod>>();
|
||||
|
||||
// REVIEW: this can be done better
|
||||
var all = ((HttpListenerHost)_httpServer).ServiceController.RestPathMap.OrderBy(i => i.Key, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
|
||||
foreach (var current in all)
|
||||
{
|
||||
foreach (var info in current.Value)
|
||||
{
|
||||
if (info.IsHidden)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (info.Path.StartsWith("/mediabrowser", StringComparison.OrdinalIgnoreCase)
|
||||
|| info.Path.StartsWith("/jellyfin", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
paths[info.Path] = GetPathInfo(info);
|
||||
}
|
||||
}
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
private Dictionary<string, SwaggerMethod> GetPathInfo(RestPath info)
|
||||
{
|
||||
var result = new Dictionary<string, SwaggerMethod>();
|
||||
|
||||
foreach (var verb in info.Verbs)
|
||||
{
|
||||
var responses = new Dictionary<string, SwaggerResponse>
|
||||
{
|
||||
{ "200", new SwaggerResponse { description = "OK" } }
|
||||
};
|
||||
|
||||
var apiKeySecurity = new Dictionary<string, string[]>
|
||||
{
|
||||
{ "api_key", Array.Empty<string>() }
|
||||
};
|
||||
|
||||
result[verb.ToLowerInvariant()] = new SwaggerMethod
|
||||
{
|
||||
summary = info.Summary,
|
||||
description = info.Description,
|
||||
produces = new[] { "application/json" },
|
||||
consumes = new[] { "application/json" },
|
||||
operationId = info.RequestType.Name,
|
||||
tags = Array.Empty<string>(),
|
||||
|
||||
parameters = Array.Empty<SwaggerParam>(),
|
||||
|
||||
responses = responses,
|
||||
|
||||
security = new[] { apiKeySecurity }
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -848,8 +848,8 @@ namespace Emby.Server.Implementations.Session
|
||||
/// </summary>
|
||||
/// <param name="info">The info.</param>
|
||||
/// <returns>Task.</returns>
|
||||
/// <exception cref="ArgumentNullException">info</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">positionTicks</exception>
|
||||
/// <exception cref="ArgumentNullException"><c>info</c> is <c>null</c>.</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException"><c>info.PositionTicks</c> is <c>null</c> or negative.</exception>
|
||||
public async Task OnPlaybackStopped(PlaybackStopInfo info)
|
||||
{
|
||||
CheckDisposed();
|
||||
|
||||
@@ -93,7 +93,7 @@ namespace Emby.Server.Implementations.Session
|
||||
if (session != null)
|
||||
{
|
||||
EnsureController(session, e.Argument);
|
||||
await KeepAliveWebSocket(e.Argument);
|
||||
await KeepAliveWebSocket(e.Argument).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -177,7 +177,7 @@ namespace Emby.Server.Implementations.Session
|
||||
// Notify WebSocket about timeout
|
||||
try
|
||||
{
|
||||
await SendForceKeepAlive(webSocket);
|
||||
await SendForceKeepAlive(webSocket).ConfigureAwait(false);
|
||||
}
|
||||
catch (WebSocketException exception)
|
||||
{
|
||||
@@ -233,6 +233,7 @@ namespace Emby.Server.Implementations.Session
|
||||
if (_keepAliveCancellationToken != null)
|
||||
{
|
||||
_keepAliveCancellationToken.Cancel();
|
||||
_keepAliveCancellationToken.Dispose();
|
||||
_keepAliveCancellationToken = null;
|
||||
}
|
||||
}
|
||||
@@ -268,7 +269,7 @@ namespace Emby.Server.Implementations.Session
|
||||
lost = _webSockets.Where(i => (DateTime.UtcNow - i.LastKeepAliveDate).TotalSeconds >= WebSocketLostTimeout).ToList();
|
||||
}
|
||||
|
||||
if (inactive.Any())
|
||||
if (inactive.Count > 0)
|
||||
{
|
||||
_logger.LogInformation("Sending ForceKeepAlive message to {0} inactive WebSockets.", inactive.Count);
|
||||
}
|
||||
@@ -277,7 +278,7 @@ namespace Emby.Server.Implementations.Session
|
||||
{
|
||||
try
|
||||
{
|
||||
await SendForceKeepAlive(webSocket);
|
||||
await SendForceKeepAlive(webSocket).ConfigureAwait(false);
|
||||
}
|
||||
catch (WebSocketException exception)
|
||||
{
|
||||
@@ -288,7 +289,7 @@ namespace Emby.Server.Implementations.Session
|
||||
|
||||
lock (_webSocketsLock)
|
||||
{
|
||||
if (lost.Any())
|
||||
if (lost.Count > 0)
|
||||
{
|
||||
_logger.LogInformation("Lost {0} WebSockets.", lost.Count);
|
||||
foreach (var webSocket in lost)
|
||||
@@ -298,7 +299,7 @@ namespace Emby.Server.Implementations.Session
|
||||
}
|
||||
}
|
||||
|
||||
if (!_webSockets.Any())
|
||||
if (_webSockets.Count == 0)
|
||||
{
|
||||
StopKeepAlive();
|
||||
}
|
||||
@@ -312,11 +313,13 @@ namespace Emby.Server.Implementations.Session
|
||||
/// <returns>Task.</returns>
|
||||
private Task SendForceKeepAlive(IWebSocketConnection webSocket)
|
||||
{
|
||||
return webSocket.SendAsync(new WebSocketMessage<int>
|
||||
{
|
||||
MessageType = "ForceKeepAlive",
|
||||
Data = WebSocketLostTimeout
|
||||
}, CancellationToken.None);
|
||||
return webSocket.SendAsync(
|
||||
new WebSocketMessage<int>
|
||||
{
|
||||
MessageType = "ForceKeepAlive",
|
||||
Data = WebSocketLostTimeout
|
||||
},
|
||||
CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -330,12 +333,11 @@ namespace Emby.Server.Implementations.Session
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
await callback();
|
||||
Task task = Task.Delay(interval, cancellationToken);
|
||||
await callback().ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
await task;
|
||||
await Task.Delay(interval, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
|
||||
@@ -154,8 +154,8 @@ namespace Emby.Server.Implementations.Sorting
|
||||
|
||||
private static int CompareEpisodes(Episode x, Episode y)
|
||||
{
|
||||
var xValue = (x.ParentIndexNumber ?? -1) * 1000 + (x.IndexNumber ?? -1);
|
||||
var yValue = (y.ParentIndexNumber ?? -1) * 1000 + (y.IndexNumber ?? -1);
|
||||
var xValue = ((x.ParentIndexNumber ?? -1) * 1000) + (x.IndexNumber ?? -1);
|
||||
var yValue = ((y.ParentIndexNumber ?? -1) * 1000) + (y.IndexNumber ?? -1);
|
||||
|
||||
return xValue.CompareTo(yValue);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -27,14 +28,17 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||
/// All sessions will receive the message.
|
||||
/// </summary>
|
||||
AllGroup = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Only the specified session will receive the message.
|
||||
/// </summary>
|
||||
CurrentSession = 1,
|
||||
|
||||
/// <summary>
|
||||
/// All sessions, except the current one, will receive the message.
|
||||
/// </summary>
|
||||
AllExceptCurrentSession = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Only sessions that are not buffering will receive the message.
|
||||
/// </summary>
|
||||
@@ -56,15 +60,6 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||
/// </summary>
|
||||
private readonly GroupInfo _group = new GroupInfo();
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid GetGroupId() => _group.GroupId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid GetPlayingItemId() => _group.PlayingItem.Id;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsGroupEmpty() => _group.IsEmpty();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SyncPlayController" /> class.
|
||||
/// </summary>
|
||||
@@ -78,6 +73,15 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||
_syncPlayManager = syncPlayManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid GetGroupId() => _group.GroupId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid GetPlayingItemId() => _group.PlayingItem.Id;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsGroupEmpty() => _group.IsEmpty();
|
||||
|
||||
/// <summary>
|
||||
/// Converts DateTime to UTC string.
|
||||
/// </summary>
|
||||
@@ -85,7 +89,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||
/// <value>The UTC string.</value>
|
||||
private string DateToUTCString(DateTime date)
|
||||
{
|
||||
return date.ToUniversalTime().ToString("o");
|
||||
return date.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -94,23 +98,23 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||
/// <param name="from">The current session.</param>
|
||||
/// <param name="type">The filtering type.</param>
|
||||
/// <value>The array of sessions matching the filter.</value>
|
||||
private SessionInfo[] FilterSessions(SessionInfo from, BroadcastType type)
|
||||
private IEnumerable<SessionInfo> FilterSessions(SessionInfo from, BroadcastType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case BroadcastType.CurrentSession:
|
||||
return new SessionInfo[] { from };
|
||||
case BroadcastType.AllGroup:
|
||||
return _group.Participants.Values.Select(
|
||||
session => session.Session).ToArray();
|
||||
return _group.Participants.Values
|
||||
.Select(session => session.Session);
|
||||
case BroadcastType.AllExceptCurrentSession:
|
||||
return _group.Participants.Values.Select(
|
||||
session => session.Session).Where(
|
||||
session => !session.Id.Equals(from.Id)).ToArray();
|
||||
return _group.Participants.Values
|
||||
.Select(session => session.Session)
|
||||
.Where(session => !session.Id.Equals(from.Id, StringComparison.Ordinal));
|
||||
case BroadcastType.AllReady:
|
||||
return _group.Participants.Values.Where(
|
||||
session => !session.IsBuffering).Select(
|
||||
session => session.Session).ToArray();
|
||||
return _group.Participants.Values
|
||||
.Where(session => !session.IsBuffering)
|
||||
.Select(session => session.Session);
|
||||
default:
|
||||
return Array.Empty<SessionInfo>();
|
||||
}
|
||||
@@ -128,10 +132,9 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||
{
|
||||
IEnumerable<Task> GetTasks()
|
||||
{
|
||||
SessionInfo[] sessions = FilterSessions(from, type);
|
||||
foreach (var session in sessions)
|
||||
foreach (var session in FilterSessions(from, type))
|
||||
{
|
||||
yield return _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), message, cancellationToken);
|
||||
yield return _sessionManager.SendSyncPlayGroupUpdate(session.Id, message, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,10 +153,9 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||
{
|
||||
IEnumerable<Task> GetTasks()
|
||||
{
|
||||
SessionInfo[] sessions = FilterSessions(from, type);
|
||||
foreach (var session in sessions)
|
||||
foreach (var session in FilterSessions(from, type))
|
||||
{
|
||||
yield return _sessionManager.SendSyncPlayCommand(session.Id.ToString(), message, cancellationToken);
|
||||
yield return _sessionManager.SendSyncPlayCommand(session.Id, message, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,9 +238,11 @@ namespace Emby.Server.Implementations.SyncPlay
|
||||
}
|
||||
else
|
||||
{
|
||||
var playRequest = new PlayRequest();
|
||||
playRequest.ItemIds = new Guid[] { _group.PlayingItem.Id };
|
||||
playRequest.StartPositionTicks = _group.PositionTicks;
|
||||
var playRequest = new PlayRequest
|
||||
{
|
||||
ItemIds = new Guid[] { _group.PlayingItem.Id },
|
||||
StartPositionTicks = _group.PositionTicks
|
||||
};
|
||||
var update = NewSyncPlayGroupUpdate(GroupUpdateType.PrepareSession, playRequest);
|
||||
SendGroupUpdate(session, BroadcastType.CurrentSession, update, cancellationToken);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user