diff options
24 files changed, 204 insertions, 131 deletions
diff --git a/.ci/azure-pipelines-api-client.yml b/.ci/azure-pipelines-api-client.yml index d120593ea..fc89b90d4 100644 --- a/.ci/azure-pipelines-api-client.yml +++ b/.ci/azure-pipelines-api-client.yml @@ -28,7 +28,7 @@ jobs: inputs: script: "wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${{ parameters.GeneratorVersion }}/openapi-generator-cli-${{ parameters.GeneratorVersion }}.jar -O openapi-generator-cli.jar" -# Generate npm api client +## Generate npm api client # Unstable - task: CmdLine@2 displayName: 'Build unstable typescript axios client' @@ -36,22 +36,32 @@ jobs: inputs: script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory) $(Build.BuildNumber)" +# Stable + - task: CmdLine@2 + displayName: 'Build stable typescript axios client' + condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') + inputs: + script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory)" + +## Run npm install + - task: Npm@1 + displayName: 'Install npm dependencies' + inputs: + command: install + workingDir: ./apiclient/generated/typescript/axios + +## Publish npm packages +# Unstable - task: Npm@1 displayName: 'Publish unstable typescript axios client' condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master') inputs: command: publish publishRegistry: useFeed - publishFeed: unstable + publishFeed: 'jellyfin/unstable' workingDir: ./apiclient/generated/typescript/axios # Stable - - task: CmdLine@2 - displayName: 'Build stable typescript axios client' - condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') - inputs: - script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory)" - - task: Npm@1 displayName: 'Publish stable typescript axios client' condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json index c1fb65743..a7c17ecb6 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-PT.json +++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json @@ -26,7 +26,7 @@ "HeaderLiveTV": "TV em Direto", "HeaderNextUp": "A Seguir", "HeaderRecordingGroups": "Grupos de Gravação", - "HomeVideos": "Videos caseiros", + "HomeVideos": "Vídeos Caseiros", "Inherit": "Herdar", "ItemAddedWithName": "{0} foi adicionado à biblioteca", "ItemRemovedWithName": "{0} foi removido da biblioteca", diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs index 80b977731..538479512 100644 --- a/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs +++ b/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs @@ -301,8 +301,7 @@ namespace Emby.Server.Implementations.SyncPlay if (_group.IsPaused) { // Pick a suitable time that accounts for latency - var delay = _group.GetHighestPing() * 2; - delay = delay < _group.DefaultPing ? _group.DefaultPing : delay; + var delay = Math.Max(_group.GetHighestPing() * 2, GroupInfo.DefaultPing); // Unpause group and set starting point in future // Clients will start playback at LastActivity (datetime) from PositionTicks (playback position) @@ -452,8 +451,7 @@ namespace Emby.Server.Implementations.SyncPlay else { // Client, that was buffering, resumed playback but did not update others in time - delay = _group.GetHighestPing() * 2; - delay = delay < _group.DefaultPing ? _group.DefaultPing : delay; + delay = Math.Max(_group.GetHighestPing() * 2, GroupInfo.DefaultPing); _group.LastActivity = currentTime.AddMilliseconds( delay); @@ -497,7 +495,7 @@ namespace Emby.Server.Implementations.SyncPlay private void HandlePingUpdateRequest(SessionInfo session, PlaybackRequest request) { // Collected pings are used to account for network latency when unpausing playback - _group.UpdatePing(session, request.Ping ?? _group.DefaultPing); + _group.UpdatePing(session, request.Ping ?? GroupInfo.DefaultPing); } /// <inheritdoc /> diff --git a/Jellyfin.Api/Controllers/ActivityLogController.cs b/Jellyfin.Api/Controllers/ActivityLogController.cs index a07cea9c0..b429cebec 100644 --- a/Jellyfin.Api/Controllers/ActivityLogController.cs +++ b/Jellyfin.Api/Controllers/ActivityLogController.cs @@ -1,7 +1,7 @@ using System; -using System.Linq; +using System.Threading.Tasks; using Jellyfin.Api.Constants; -using Jellyfin.Data.Entities; +using Jellyfin.Data.Queries; using MediaBrowser.Model.Activity; using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; @@ -39,19 +39,19 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="QueryResult{ActivityLogEntry}"/> containing the log entries.</returns> [HttpGet("Entries")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<QueryResult<ActivityLogEntry>> GetLogEntries( + public async Task<ActionResult<QueryResult<ActivityLogEntry>>> GetLogEntries( [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] DateTime? minDate, [FromQuery] bool? hasUserId) { - var filterFunc = new Func<IQueryable<ActivityLog>, IQueryable<ActivityLog>>( - entries => entries.Where(entry => entry.DateCreated >= minDate - && (!hasUserId.HasValue || (hasUserId.Value - ? entry.UserId != Guid.Empty - : entry.UserId == Guid.Empty)))); - - return _activityManager.GetPagedResult(filterFunc, startIndex, limit); + return await _activityManager.GetPagedResultAsync(new ActivityLogQuery + { + StartIndex = startIndex, + Limit = limit, + MinDate = minDate, + HasUserId = hasUserId + }).ConfigureAwait(false); } } } diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index 7afec1219..05efe2355 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -333,7 +333,7 @@ namespace Jellyfin.Api.Controllers /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param> /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param> - /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param> + /// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param> /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param> /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param> /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param> @@ -364,7 +364,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? quality, [FromQuery] string? tag, [FromQuery] bool? cropWhitespace, - [FromQuery] string? format, + [FromQuery] ImageFormat? format, [FromQuery] bool? addPlayedIndicator, [FromQuery] double? percentPlayed, [FromQuery] int? unplayedCount, @@ -443,7 +443,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? quality, [FromRoute, Required] string tag, [FromQuery] bool? cropWhitespace, - [FromRoute, Required] string format, + [FromRoute, Required] ImageFormat format, [FromQuery] bool? addPlayedIndicator, [FromRoute, Required] double percentPlayed, [FromRoute, Required] int unplayedCount, @@ -516,7 +516,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] string name, [FromRoute, Required] ImageType imageType, [FromQuery] string tag, - [FromQuery] string format, + [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, [FromQuery] double? percentPlayed, @@ -595,7 +595,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] string name, [FromRoute, Required] ImageType imageType, [FromQuery] string tag, - [FromQuery] string format, + [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, [FromQuery] double? percentPlayed, @@ -674,7 +674,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] string name, [FromRoute, Required] ImageType imageType, [FromQuery] string tag, - [FromQuery] string format, + [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, [FromQuery] double? percentPlayed, @@ -753,7 +753,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] string name, [FromRoute, Required] ImageType imageType, [FromQuery] string tag, - [FromQuery] string format, + [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, [FromQuery] double? percentPlayed, @@ -832,7 +832,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] string name, [FromRoute, Required] ImageType imageType, [FromRoute, Required] string tag, - [FromRoute, Required] string format, + [FromRoute, Required] ImageFormat format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, [FromQuery] double? percentPlayed, @@ -911,7 +911,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] Guid userId, [FromRoute, Required] ImageType imageType, [FromQuery] string? tag, - [FromQuery] string? format, + [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, [FromQuery] double? percentPlayed, @@ -1038,7 +1038,7 @@ namespace Jellyfin.Api.Controllers ImageType imageType, int? imageIndex, string? tag, - string? format, + ImageFormat? format, int? maxWidth, int? maxHeight, double? percentPlayed, @@ -1128,12 +1128,11 @@ namespace Jellyfin.Api.Controllers isHeadRequest).ConfigureAwait(false); } - private ImageFormat[] GetOutputFormats(string? format) + private ImageFormat[] GetOutputFormats(ImageFormat? format) { - if (!string.IsNullOrWhiteSpace(format) - && Enum.TryParse(format, true, out ImageFormat parsedFormat)) + if (format.HasValue) { - return new[] { parsedFormat }; + return new[] { format.Value }; } return GetClientSupportedFormats(); @@ -1157,7 +1156,7 @@ namespace Jellyfin.Api.Controllers var acceptParam = Request.Query[HeaderNames.Accept]; - var supportsWebP = SupportsFormat(supportedFormats, acceptParam, "webp", false); + var supportsWebP = SupportsFormat(supportedFormats, acceptParam, ImageFormat.Webp, false); if (!supportsWebP) { @@ -1179,7 +1178,7 @@ namespace Jellyfin.Api.Controllers formats.Add(ImageFormat.Jpg); formats.Add(ImageFormat.Png); - if (SupportsFormat(supportedFormats, acceptParam, "gif", true)) + if (SupportsFormat(supportedFormats, acceptParam, ImageFormat.Gif, true)) { formats.Add(ImageFormat.Gif); } @@ -1187,9 +1186,10 @@ namespace Jellyfin.Api.Controllers return formats.ToArray(); } - private bool SupportsFormat(IReadOnlyCollection<string> requestAcceptTypes, string acceptParam, string format, bool acceptAll) + private bool SupportsFormat(IReadOnlyCollection<string> requestAcceptTypes, string acceptParam, ImageFormat format, bool acceptAll) { - var mimeType = "image/" + format; + var normalized = format.ToString().ToLowerInvariant(); + var mimeType = "image/" + normalized; if (requestAcceptTypes.Contains(mimeType)) { @@ -1201,7 +1201,7 @@ namespace Jellyfin.Api.Controllers return true; } - return string.Equals(acceptParam, format, StringComparison.OrdinalIgnoreCase); + return string.Equals(acceptParam, normalized, StringComparison.OrdinalIgnoreCase); } private async Task<ActionResult> GetImageResult( diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index 1207fb513..e78f63b25 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -554,7 +554,7 @@ namespace Jellyfin.Api.Helpers private long? GetMaxBitrate(long? clientMaxBitrate, User user, string ipAddress) { var maxBitrate = clientMaxBitrate; - var remoteClientMaxBitrate = user?.RemoteClientBitrateLimit ?? 0; + var remoteClientMaxBitrate = user.RemoteClientBitrateLimit ?? 0; if (remoteClientMaxBitrate <= 0) { diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index 64d1227f7..0db1fabff 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -740,10 +740,7 @@ namespace Jellyfin.Api.Helpers /// <param name="state">The state.</param> private void OnFfMpegProcessExited(Process process, TranscodingJobDto job, StreamState state) { - if (job != null) - { - job.HasExited = true; - } + job.HasExited = true; _logger.LogDebug("Disposing stream resources"); state.Dispose(); diff --git a/Jellyfin.Data/Queries/ActivityLogQuery.cs b/Jellyfin.Data/Queries/ActivityLogQuery.cs new file mode 100644 index 000000000..92919d3a5 --- /dev/null +++ b/Jellyfin.Data/Queries/ActivityLogQuery.cs @@ -0,0 +1,30 @@ +using System; + +namespace Jellyfin.Data.Queries +{ + /// <summary> + /// A class representing a query to the activity logs. + /// </summary> + public class ActivityLogQuery + { + /// <summary> + /// Gets or sets the index to start at. + /// </summary> + public int? StartIndex { get; set; } + + /// <summary> + /// Gets or sets the maximum number of items to include. + /// </summary> + public int? Limit { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether to take entries with a user id. + /// </summary> + public bool? HasUserId { get; set; } + + /// <summary> + /// Gets or sets the minimum date to query for. + /// </summary> + public DateTime? MinDate { get; set; } + } +} diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs index abdd290d4..5926abfe0 100644 --- a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs +++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs @@ -3,8 +3,10 @@ using System.Linq; using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Events; +using Jellyfin.Data.Queries; using MediaBrowser.Model.Activity; using MediaBrowser.Model.Querying; +using Microsoft.EntityFrameworkCore; namespace Jellyfin.Server.Implementations.Activity { @@ -39,41 +41,37 @@ namespace Jellyfin.Server.Implementations.Activity } /// <inheritdoc/> - public QueryResult<ActivityLogEntry> GetPagedResult( - Func<IQueryable<ActivityLog>, IQueryable<ActivityLog>> func, - int? startIndex, - int? limit) + public async Task<QueryResult<ActivityLogEntry>> GetPagedResultAsync(ActivityLogQuery query) { - using var dbContext = _provider.CreateContext(); + await using var dbContext = _provider.CreateContext(); - var query = func(dbContext.ActivityLogs.OrderByDescending(entry => entry.DateCreated)); + IQueryable<ActivityLog> entries = dbContext.ActivityLogs + .AsQueryable() + .OrderByDescending(entry => entry.DateCreated); - if (startIndex.HasValue) + if (query.MinDate.HasValue) { - query = query.Skip(startIndex.Value); + entries = entries.Where(entry => entry.DateCreated >= query.MinDate); } - if (limit.HasValue) + if (query.HasUserId.HasValue) { - query = query.Take(limit.Value); + entries = entries.Where(entry => entry.UserId != Guid.Empty == query.HasUserId.Value ); } - // This converts the objects from the new database model to the old for compatibility with the existing API. - var list = query.Select(ConvertToOldModel).ToList(); - return new QueryResult<ActivityLogEntry> { - Items = list, - TotalRecordCount = func(dbContext.ActivityLogs).Count() + Items = await entries + .Skip(query.StartIndex ?? 0) + .Take(query.Limit ?? 100) + .AsAsyncEnumerable() + .Select(ConvertToOldModel) + .ToListAsync() + .ConfigureAwait(false), + TotalRecordCount = await entries.CountAsync().ConfigureAwait(false) }; } - /// <inheritdoc/> - public QueryResult<ActivityLogEntry> GetPagedResult(int? startIndex, int? limit) - { - return GetPagedResult(logs => logs, startIndex, limit); - } - private static ActivityLogEntry ConvertToOldModel(ActivityLog entry) { return new ActivityLogEntry diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index 4e79dd8d6..17ba09258 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -24,6 +24,7 @@ </ItemGroup> <ItemGroup> + <PackageReference Include="System.Linq.Async" Version="4.1.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.8"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> diff --git a/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.cs b/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.cs index 8fded7d0a..10acb4def 100644 --- a/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.cs +++ b/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.cs @@ -13,7 +13,8 @@ namespace Jellyfin.Server.Implementations.Migrations name: "MaxActiveSessions", schema: "jellyfin", table: "Users", - nullable: true); + nullable: false, + defaultValue: 0); } protected override void Down(MigrationBuilder migrationBuilder) diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs index 3c9e1aee4..16d62f482 100644 --- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs +++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs @@ -344,7 +344,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.Property<int?>("LoginAttemptsBeforeLockout") .HasColumnType("INTEGER"); - b.Property<int?>("MaxActiveSessions") + b.Property<int>("MaxActiveSessions") .HasColumnType("INTEGER"); b.Property<int?>("MaxParentalAgeRating") diff --git a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs index 46f1c618f..76f943385 100644 --- a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs +++ b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs @@ -61,6 +61,7 @@ namespace Jellyfin.Server.Implementations.Users public IList<ItemDisplayPreferences> ListItemDisplayPreferences(Guid userId, string client) { return _dbContext.ItemDisplayPreferences + .AsQueryable() .Where(prefs => prefs.UserId == userId && prefs.ItemId != Guid.Empty && string.Equals(prefs.Client, client)) .ToList(); } diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index d8ec2a3cd..437833aa3 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -108,6 +108,7 @@ namespace Jellyfin.Server.Implementations.Users { using var dbContext = _dbProvider.CreateContext(); return dbContext.Users + .AsQueryable() .Select(user => user.Id) .ToList(); } @@ -200,8 +201,8 @@ namespace Jellyfin.Server.Implementations.Users internal async Task<User> CreateUserInternalAsync(string name, JellyfinDb dbContext) { // TODO: Remove after user item data is migrated. - var max = await dbContext.Users.AnyAsync().ConfigureAwait(false) - ? await dbContext.Users.Select(u => u.InternalId).MaxAsync().ConfigureAwait(false) + var max = await dbContext.Users.AsQueryable().AnyAsync().ConfigureAwait(false) + ? await dbContext.Users.AsQueryable().Select(u => u.InternalId).MaxAsync().ConfigureAwait(false) : 0; return new User( @@ -221,7 +222,7 @@ namespace Jellyfin.Server.Implementations.Users throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)"); } - using var dbContext = _dbProvider.CreateContext(); + await using var dbContext = _dbProvider.CreateContext(); var newUser = await CreateUserInternalAsync(name, dbContext).ConfigureAwait(false); @@ -588,9 +589,9 @@ namespace Jellyfin.Server.Implementations.Users public async Task InitializeAsync() { // TODO: Refactor the startup wizard so that it doesn't require a user to already exist. - using var dbContext = _dbProvider.CreateContext(); + await using var dbContext = _dbProvider.CreateContext(); - if (await dbContext.Users.AnyAsync().ConfigureAwait(false)) + if (await dbContext.Users.AsQueryable().AnyAsync().ConfigureAwait(false)) { return; } @@ -801,7 +802,7 @@ namespace Jellyfin.Server.Implementations.Users private IList<IPasswordResetProvider> GetPasswordResetProviders(User user) { - var passwordResetProviderId = user?.PasswordResetProviderId; + var passwordResetProviderId = user.PasswordResetProviderId; var providers = _passwordResetProviders.Where(i => i.IsEnabled).ToArray(); if (!string.IsNullOrEmpty(passwordResetProviderId)) diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index f867143df..d7b9da5c2 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -263,6 +263,7 @@ namespace Jellyfin.Server.Extensions c.AddSwaggerTypeMappings(); c.OperationFilter<FileResponseFilter>(); + c.DocumentFilter<WebsocketModelFilter>(); }); } diff --git a/Jellyfin.Server/Filters/WebsocketModelFilter.cs b/Jellyfin.Server/Filters/WebsocketModelFilter.cs new file mode 100644 index 000000000..248802857 --- /dev/null +++ b/Jellyfin.Server/Filters/WebsocketModelFilter.cs @@ -0,0 +1,30 @@ +using MediaBrowser.Common.Plugins; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.SyncPlay; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Jellyfin.Server.Filters +{ + /// <summary> + /// Add models used in websocket messaging. + /// </summary> + public class WebsocketModelFilter : IDocumentFilter + { + /// <inheritdoc /> + public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) + { + context.SchemaGenerator.GenerateSchema(typeof(LibraryUpdateInfo), context.SchemaRepository); + context.SchemaGenerator.GenerateSchema(typeof(IPlugin), context.SchemaRepository); + context.SchemaGenerator.GenerateSchema(typeof(PlayRequest), context.SchemaRepository); + context.SchemaGenerator.GenerateSchema(typeof(PlaystateRequest), context.SchemaRepository); + context.SchemaGenerator.GenerateSchema(typeof(TimerEventInfo), context.SchemaRepository); + context.SchemaGenerator.GenerateSchema(typeof(SendCommand), context.SchemaRepository); + context.SchemaGenerator.GenerateSchema(typeof(GeneralCommandType), context.SchemaRepository); + + context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate<object>), context.SchemaRepository); + } + } +} diff --git a/MediaBrowser.Controller/SyncPlay/GroupInfo.cs b/MediaBrowser.Controller/SyncPlay/GroupInfo.cs index e742df517..a1cada25c 100644 --- a/MediaBrowser.Controller/SyncPlay/GroupInfo.cs +++ b/MediaBrowser.Controller/SyncPlay/GroupInfo.cs @@ -14,12 +14,12 @@ namespace MediaBrowser.Controller.SyncPlay public class GroupInfo { /// <summary> - /// Gets the default ping value used for sessions. + /// The default ping value used for sessions. /// </summary> - public long DefaultPing { get; } = 500; + public const long DefaultPing = 500; /// <summary> - /// Gets or sets the group identifier. + /// Gets the group identifier. /// </summary> /// <value>The group identifier.</value> public Guid GroupId { get; } = Guid.NewGuid(); @@ -58,7 +58,8 @@ namespace MediaBrowser.Controller.SyncPlay /// <summary> /// Checks if a session is in this group. /// </summary> - /// <value><c>true</c> if the session is in this group; <c>false</c> otherwise.</value> + /// <param name="sessionId">The session id to check.</param> + /// <returns><c>true</c> if the session is in this group; <c>false</c> otherwise.</returns> public bool ContainsSession(string sessionId) { return Participants.ContainsKey(sessionId); @@ -70,16 +71,14 @@ namespace MediaBrowser.Controller.SyncPlay /// <param name="session">The session.</param> public void AddSession(SessionInfo session) { - if (ContainsSession(session.Id)) - { - return; - } - - var member = new GroupMember(); - member.Session = session; - member.Ping = DefaultPing; - member.IsBuffering = false; - Participants[session.Id] = member; + Participants.TryAdd( + session.Id, + new GroupMember + { + Session = session, + Ping = DefaultPing, + IsBuffering = false + }); } /// <summary> @@ -88,12 +87,7 @@ namespace MediaBrowser.Controller.SyncPlay /// <param name="session">The session.</param> public void RemoveSession(SessionInfo session) { - if (!ContainsSession(session.Id)) - { - return; - } - - Participants.Remove(session.Id, out _); + Participants.Remove(session.Id); } /// <summary> @@ -103,18 +97,16 @@ namespace MediaBrowser.Controller.SyncPlay /// <param name="ping">The ping.</param> public void UpdatePing(SessionInfo session, long ping) { - if (!ContainsSession(session.Id)) + if (Participants.TryGetValue(session.Id, out GroupMember value)) { - return; + value.Ping = ping; } - - Participants[session.Id].Ping = ping; } /// <summary> /// Gets the highest ping in the group. /// </summary> - /// <value name="session">The highest ping in the group.</value> + /// <returns>The highest ping in the group.</returns> public long GetHighestPing() { long max = long.MinValue; @@ -133,18 +125,16 @@ namespace MediaBrowser.Controller.SyncPlay /// <param name="isBuffering">The state.</param> public void SetBuffering(SessionInfo session, bool isBuffering) { - if (!ContainsSession(session.Id)) + if (Participants.TryGetValue(session.Id, out GroupMember value)) { - return; + value.IsBuffering = isBuffering; } - - Participants[session.Id].IsBuffering = isBuffering; } /// <summary> /// Gets the group buffering state. /// </summary> - /// <value><c>true</c> if there is a session buffering in the group; <c>false</c> otherwise.</value> + /// <returns><c>true</c> if there is a session buffering in the group; <c>false</c> otherwise.</returns> public bool IsBuffering() { foreach (var session in Participants.Values) @@ -161,7 +151,7 @@ namespace MediaBrowser.Controller.SyncPlay /// <summary> /// Checks if the group is empty. /// </summary> - /// <value><c>true</c> if the group is empty; <c>false</c> otherwise.</value> + /// <returns><c>true</c> if the group is empty; <c>false</c> otherwise.</returns> public bool IsEmpty() { return Participants.Count == 0; diff --git a/MediaBrowser.Model/Activity/IActivityManager.cs b/MediaBrowser.Model/Activity/IActivityManager.cs index d5344494e..3e4ea208e 100644 --- a/MediaBrowser.Model/Activity/IActivityManager.cs +++ b/MediaBrowser.Model/Activity/IActivityManager.cs @@ -1,10 +1,10 @@ #pragma warning disable CS1591 using System; -using System.Linq; using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Events; +using Jellyfin.Data.Queries; using MediaBrowser.Model.Querying; namespace MediaBrowser.Model.Activity @@ -15,11 +15,6 @@ namespace MediaBrowser.Model.Activity Task CreateAsync(ActivityLog entry); - QueryResult<ActivityLogEntry> GetPagedResult(int? startIndex, int? limit); - - QueryResult<ActivityLogEntry> GetPagedResult( - Func<IQueryable<ActivityLog>, IQueryable<ActivityLog>> func, - int? startIndex, - int? limit); + Task<QueryResult<ActivityLogEntry>> GetPagedResultAsync(ActivityLogQuery query); } } diff --git a/apiclient/templates/typescript/axios/generate.sh b/apiclient/templates/typescript/axios/generate.sh index 111b71b82..8c4d74282 100644 --- a/apiclient/templates/typescript/axios/generate.sh +++ b/apiclient/templates/typescript/axios/generate.sh @@ -4,7 +4,7 @@ artifactsDirectory="${1}" buildNumber="${2}" if [[ -n ${buildNumber} ]]; then # Unstable build - additionalProperties=",snapshotVersion=\"-SNAPSHOT.${buildNumber}\",npmRepository=\"https://pkgs.dev.azure.com/jellyfin-project/jellyfin/_packaging/unstable/npm/registry/\"" + additionalProperties=",snapshotVersion=-SNAPSHOT.${buildNumber},npmRepository=https://pkgs.dev.azure.com/jellyfin-project/jellyfin/_packaging/unstable/npm/registry/" else # Stable build additionalProperties="" diff --git a/deployment/build.windows.amd64 b/deployment/build.windows.amd64 index afe490576..a9daa6a23 100755 --- a/deployment/build.windows.amd64 +++ b/deployment/build.windows.amd64 @@ -35,10 +35,6 @@ unzip ${addin_build_dir}/jellyfin-ffmpeg.zip -d ${addin_build_dir}/jellyfin-ffmp cp ${addin_build_dir}/jellyfin-ffmpeg/* ${output_dir} rm -rf ${addin_build_dir} -# Prepare scripts -cp ${SOURCE_DIR}/windows/legacy/install-jellyfin.ps1 ${output_dir}/install-jellyfin.ps1 -cp ${SOURCE_DIR}/windows/legacy/install.bat ${output_dir}/install.bat - # Create zip package pushd dist zip -qr jellyfin-server_${version}.portable.zip jellyfin-server_${version} diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec index bfb2b3be2..93fb9fb41 100644 --- a/fedora/jellyfin.spec +++ b/fedora/jellyfin.spec @@ -79,15 +79,7 @@ EOF %files server %attr(755,root,root) %{_bindir}/jellyfin -%{_libdir}/jellyfin/*.json -%{_libdir}/jellyfin/*.dll -%{_libdir}/jellyfin/*.so -%{_libdir}/jellyfin/*.a -%{_libdir}/jellyfin/createdump -%{_libdir}/jellyfin/*.xml -%{_libdir}/jellyfin/wwwroot/api-docs/* -%{_libdir}/jellyfin/wwwroot/api-docs/redoc/* -%{_libdir}/jellyfin/wwwroot/api-docs/swagger/* +%{_libdir}/jellyfin/* # Needs 755 else only root can run it since binary build by dotnet is 722 %attr(755,root,root) %{_libdir}/jellyfin/jellyfin %{_libdir}/jellyfin/SOS_README.md diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj index e3a7a5428..8a559b7b6 100644 --- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj +++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj @@ -22,7 +22,7 @@ <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="1.3.0" /> - <PackageReference Include="Moq" Version="4.14.5" /> + <PackageReference Include="Moq" Version="4.14.6" /> </ItemGroup> <!-- Code Analyzers --> diff --git a/tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs new file mode 100644 index 000000000..d9e66d677 --- /dev/null +++ b/tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs @@ -0,0 +1,32 @@ +using System; +using System.Text.Json; +using MediaBrowser.Common.Json.Converters; +using Xunit; + +namespace Jellyfin.Common.Tests.Extensions +{ + public static class JsonGuidConverterTests + { + [Fact] + public static void Deserialize_Valid_Success() + { + var options = new JsonSerializerOptions(); + options.Converters.Add(new JsonGuidConverter()); + Guid value = JsonSerializer.Deserialize<Guid>(@"""a852a27afe324084ae66db579ee3ee18""", options); + Assert.Equal(new Guid("a852a27afe324084ae66db579ee3ee18"), value); + + value = JsonSerializer.Deserialize<Guid>(@"""e9b2dcaa-529c-426e-9433-5e9981f27f2e""", options); + Assert.Equal(new Guid("e9b2dcaa-529c-426e-9433-5e9981f27f2e"), value); + } + + [Fact] + public static void Roundtrip_Valid_Success() + { + var options = new JsonSerializerOptions(); + options.Converters.Add(new JsonGuidConverter()); + Guid guid = new Guid("a852a27afe324084ae66db579ee3ee18"); + string value = JsonSerializer.Serialize(guid, options); + Assert.Equal(guid, JsonSerializer.Deserialize<Guid>(value, options)); + } + } +} diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj index d1679c279..75b67f460 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj +++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj @@ -17,7 +17,7 @@ <PackageReference Include="AutoFixture" Version="4.13.0" /> <PackageReference Include="AutoFixture.AutoMoq" Version="4.13.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" /> - <PackageReference Include="Moq" Version="4.14.5" /> + <PackageReference Include="Moq" Version="4.14.6" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="1.3.0" /> |
