mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-05-03 23:36:38 +01:00
Merge remote-tracking branch 'upstream/master' into perf-rebased
This commit is contained in:
8
.github/workflows/ci-codeql-analysis.yml
vendored
8
.github/workflows/ci-codeql-analysis.yml
vendored
@@ -23,18 +23,18 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
|
||||
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
|
||||
uses: github/codeql-action/init@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: +security-extended
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
|
||||
uses: github/codeql-action/autobuild@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
|
||||
uses: github/codeql-action/analyze@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
|
||||
|
||||
12
.github/workflows/ci-compat.yml
vendored
12
.github/workflows/ci-compat.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
|
||||
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
dotnet build Jellyfin.Server -o ./out
|
||||
|
||||
- name: Upload Head
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: abi-head
|
||||
retention-days: 14
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
|
||||
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
dotnet build Jellyfin.Server -o ./out
|
||||
|
||||
- name: Upload Head
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: abi-base
|
||||
retention-days: 14
|
||||
@@ -85,13 +85,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Download abi-head
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
name: abi-head
|
||||
path: abi-head
|
||||
|
||||
- name: Download abi-base
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
name: abi-base
|
||||
path: abi-base
|
||||
|
||||
16
.github/workflows/ci-openapi.yml
vendored
16
.github/workflows/ci-openapi.yml
vendored
@@ -22,14 +22,14 @@ jobs:
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
|
||||
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
- name: Generate openapi.json
|
||||
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
|
||||
|
||||
- name: Upload openapi.json
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: openapi-head
|
||||
retention-days: 14
|
||||
@@ -59,14 +59,14 @@ jobs:
|
||||
git checkout --progress --force $ANCESTOR_REF
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
|
||||
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
- name: Generate openapi.json
|
||||
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
|
||||
|
||||
- name: Upload openapi.json
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: openapi-base
|
||||
retention-days: 14
|
||||
@@ -85,13 +85,13 @@ jobs:
|
||||
- openapi-base
|
||||
steps:
|
||||
- name: Download openapi-head
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
name: openapi-head
|
||||
path: openapi-head
|
||||
|
||||
- name: Download openapi-base
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
name: openapi-base
|
||||
path: openapi-base
|
||||
@@ -119,7 +119,7 @@ jobs:
|
||||
run: |-
|
||||
echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV
|
||||
- name: Download openapi-head
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
name: openapi-head
|
||||
path: openapi-head
|
||||
@@ -180,7 +180,7 @@ jobs:
|
||||
run: |-
|
||||
echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||
- name: Download openapi-head
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
name: openapi-head
|
||||
path: openapi-head
|
||||
|
||||
4
.github/workflows/ci-tests.yml
vendored
4
.github/workflows/ci-tests.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
|
||||
- uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
|
||||
with:
|
||||
dotnet-version: ${{ env.SDK_VERSION }}
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
--verbosity minimal
|
||||
|
||||
- name: Merge code coverage results
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@ee0ae774f6d3afedcbd1683c1ab21b83670bdf8e # v5.5.1
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@2a82782178b2816d9d6960a7345fdd164791b323 # v5.5.3
|
||||
with:
|
||||
reports: "**/coverage.cobertura.xml"
|
||||
targetdir: "merged/"
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
|
||||
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.1" />
|
||||
<PackageVersion Include="prometheus-net" Version="8.2.1" />
|
||||
<PackageVersion Include="Polly" Version="8.6.5" />
|
||||
<PackageVersion Include="Polly" Version="8.6.6" />
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="10.0.0" />
|
||||
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
|
||||
<PackageVersion Include="Serilog.Expressions" Version="5.0.0" />
|
||||
|
||||
@@ -225,6 +225,7 @@ namespace Emby.Naming.Common
|
||||
".afc",
|
||||
".amf",
|
||||
".aif",
|
||||
".aifc",
|
||||
".aiff",
|
||||
".alac",
|
||||
".amr",
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace Emby.Server.Implementations.Images
|
||||
includeItemTypes = new[] { BaseItemKind.Series };
|
||||
break;
|
||||
case CollectionType.music:
|
||||
includeItemTypes = new[] { BaseItemKind.MusicAlbum };
|
||||
includeItemTypes = new[] { BaseItemKind.MusicArtist }; // Music albums usually don't have dedicated backdrops, so use artist instead
|
||||
break;
|
||||
case CollectionType.musicvideos:
|
||||
includeItemTypes = new[] { BaseItemKind.MusicVideo };
|
||||
|
||||
@@ -135,5 +135,7 @@
|
||||
"TaskExtractMediaSegments": "Media Segment Skandeer",
|
||||
"TaskExtractMediaSegmentsDescription": "Onttrek of verkry mediasegmente van MediaSegment-geaktiveerde inproppe.",
|
||||
"TaskMoveTrickplayImages": "Migreer Trickplay Beeldligging",
|
||||
"TaskMoveTrickplayImagesDescription": "Skuif ontstaande trickplay lêers volgens die biblioteekinstellings."
|
||||
"TaskMoveTrickplayImagesDescription": "Skuif ontstaande trickplay lêers volgens die biblioteekinstellings.",
|
||||
"CleanupUserDataTask": "Gebruikers data skoon maak taak",
|
||||
"CleanupUserDataTaskDescription": "Maak alle gebruikers data (kykstatus, gunstelingstatus, ens.) skoon van media wat nie meer vir ten minste 90 dae teenwoordig is nie."
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"Favorites": "Любими",
|
||||
"Folders": "Папки",
|
||||
"Genres": "Жанрове",
|
||||
"HeaderAlbumArtists": "Изпълнители на албуми",
|
||||
"HeaderAlbumArtists": "Изпълнители на албума",
|
||||
"HeaderContinueWatching": "Продължаване на гледането",
|
||||
"HeaderFavoriteAlbums": "Любими албуми",
|
||||
"HeaderFavoriteArtists": "Любими изпълнители",
|
||||
|
||||
@@ -1 +1,8 @@
|
||||
{}
|
||||
{
|
||||
"Books": "ספרים",
|
||||
"NameSeasonNumber": "עונה {0}",
|
||||
"Channels": "ערוצים",
|
||||
"Movies": "סרטים",
|
||||
"Music": "מוזיקה",
|
||||
"Collections": "אוספים"
|
||||
}
|
||||
|
||||
@@ -1403,8 +1403,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
double fps = state.TargetFramerate ?? 0.0f;
|
||||
int segmentLength = state.SegmentLength * 1000;
|
||||
|
||||
// If framerate is fractional (i.e. 23.976), we need to slightly adjust segment length
|
||||
if (Math.Abs(fps - Math.Floor(fps + 0.001f)) > 0.001)
|
||||
// If video is transcoded and framerate is fractional (i.e. 23.976), we need to slightly adjust segment length
|
||||
if (!EncodingHelper.IsCopyCodec(state.OutputVideoCodec) && Math.Abs(fps - Math.Floor(fps + 0.001f)) > 0.001)
|
||||
{
|
||||
double nearestIntFramerate = Math.Ceiling(fps);
|
||||
segmentLength = (int)Math.Ceiling(segmentLength * (nearestIntFramerate / fps));
|
||||
|
||||
@@ -249,7 +249,7 @@ public class ItemUpdateController : BaseJellyfinApiController
|
||||
item.IndexNumber = request.IndexNumber;
|
||||
item.ParentIndexNumber = request.ParentIndexNumber;
|
||||
item.Overview = request.Overview;
|
||||
item.Genres = request.Genres;
|
||||
item.Genres = request.Genres.Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
|
||||
if (item is Episode episode)
|
||||
{
|
||||
@@ -270,7 +270,7 @@ public class ItemUpdateController : BaseJellyfinApiController
|
||||
|
||||
if (request.Studios is not null)
|
||||
{
|
||||
item.Studios = Array.ConvertAll(request.Studios, x => x.Name);
|
||||
item.Studios = Array.ConvertAll(request.Studios, x => x.Name).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
}
|
||||
|
||||
if (request.DateCreated.HasValue)
|
||||
@@ -287,7 +287,7 @@ public class ItemUpdateController : BaseJellyfinApiController
|
||||
item.CustomRating = request.CustomRating;
|
||||
|
||||
var currentTags = item.Tags;
|
||||
var newTags = request.Tags;
|
||||
var newTags = request.Tags.Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
var removedTags = currentTags.Except(newTags).ToList();
|
||||
var addedTags = newTags.Except(currentTags).ToList();
|
||||
item.Tags = newTags;
|
||||
@@ -373,7 +373,7 @@ public class ItemUpdateController : BaseJellyfinApiController
|
||||
|
||||
if (request.ProductionLocations is not null)
|
||||
{
|
||||
item.ProductionLocations = request.ProductionLocations;
|
||||
item.ProductionLocations = request.ProductionLocations.Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
}
|
||||
|
||||
item.PreferredMetadataCountryCode = request.PreferredMetadataCountryCode;
|
||||
@@ -421,7 +421,7 @@ public class ItemUpdateController : BaseJellyfinApiController
|
||||
{
|
||||
if (item is IHasAlbumArtist hasAlbumArtists)
|
||||
{
|
||||
hasAlbumArtists.AlbumArtists = Array.ConvertAll(request.AlbumArtists, i => i.Name.Trim());
|
||||
hasAlbumArtists.AlbumArtists = Array.ConvertAll(request.AlbumArtists, i => i.Name.Trim()).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -429,7 +429,7 @@ public class ItemUpdateController : BaseJellyfinApiController
|
||||
{
|
||||
if (item is IHasArtist hasArtists)
|
||||
{
|
||||
hasArtists.Artists = Array.ConvertAll(request.ArtistItems, i => i.Name.Trim());
|
||||
hasArtists.Artists = Array.ConvertAll(request.ArtistItems, i => i.Name.Trim()).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ public class QuickConnectController : BaseJellyfinApiController
|
||||
/// <returns>A <see cref="QuickConnectResult"/> with a secret and code for future use or an error message.</returns>
|
||||
[HttpPost("Initiate")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ActionResult<QuickConnectResult>> InitiateQuickConnect()
|
||||
{
|
||||
try
|
||||
|
||||
@@ -255,7 +255,7 @@ internal static class BaseItemMapper
|
||||
entity.TotalBitrate = dto.TotalBitrate;
|
||||
entity.ExternalId = dto.ExternalId;
|
||||
entity.Size = dto.Size;
|
||||
entity.Genres = string.Join('|', dto.Genres);
|
||||
entity.Genres = string.Join('|', dto.Genres.Distinct(StringComparer.OrdinalIgnoreCase));
|
||||
entity.DateCreated = dto.DateCreated == DateTime.MinValue ? null : dto.DateCreated;
|
||||
entity.DateModified = dto.DateModified == DateTime.MinValue ? null : dto.DateModified;
|
||||
entity.ChannelId = dto.ChannelId;
|
||||
@@ -281,9 +281,9 @@ internal static class BaseItemMapper
|
||||
entity.ExtraType = (BaseItemExtraType)dto.ExtraType;
|
||||
}
|
||||
|
||||
entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations.Where(p => !string.IsNullOrWhiteSpace(p))) : null;
|
||||
entity.Studios = dto.Studios is not null ? string.Join('|', dto.Studios) : null;
|
||||
entity.Tags = dto.Tags is not null ? string.Join('|', dto.Tags) : null;
|
||||
entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations.Where(p => !string.IsNullOrWhiteSpace(p)).Distinct(StringComparer.OrdinalIgnoreCase)) : null;
|
||||
entity.Studios = dto.Studios is not null ? string.Join('|', dto.Studios.Distinct(StringComparer.OrdinalIgnoreCase)) : null;
|
||||
entity.Tags = dto.Tags is not null ? string.Join('|', dto.Tags.Distinct(StringComparer.OrdinalIgnoreCase)) : null;
|
||||
entity.LockedFields = dto.LockedFields is not null ? dto.LockedFields
|
||||
.Select(e => new BaseItemMetadataField()
|
||||
{
|
||||
@@ -326,12 +326,12 @@ internal static class BaseItemMapper
|
||||
|
||||
if (dto is IHasArtist hasArtists)
|
||||
{
|
||||
entity.Artists = hasArtists.Artists is not null ? string.Join('|', hasArtists.Artists) : null;
|
||||
entity.Artists = hasArtists.Artists is not null ? string.Join('|', hasArtists.Artists.Distinct(StringComparer.OrdinalIgnoreCase)) : null;
|
||||
}
|
||||
|
||||
if (dto is IHasAlbumArtist hasAlbumArtists)
|
||||
{
|
||||
entity.AlbumArtists = hasAlbumArtists.AlbumArtists is not null ? string.Join('|', hasAlbumArtists.AlbumArtists) : null;
|
||||
entity.AlbumArtists = hasAlbumArtists.AlbumArtists is not null ? string.Join('|', hasAlbumArtists.AlbumArtists.Distinct(StringComparer.OrdinalIgnoreCase)) : null;
|
||||
}
|
||||
|
||||
if (dto is LiveTvProgram program)
|
||||
|
||||
@@ -290,14 +290,15 @@ public class ItemPersistenceService : IItemPersistenceService
|
||||
.SelectMany(f => f.Values)
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
var types = allListedItemValues.Select(e => e.MagicNumber).Distinct().ToArray();
|
||||
var values = allListedItemValues.Select(e => e.Value).Distinct().ToArray();
|
||||
var allListedItemValuesSet = allListedItemValues.ToHashSet();
|
||||
|
||||
var existingValues = context.ItemValues
|
||||
.Select(e => new
|
||||
{
|
||||
item = e,
|
||||
Key = e.Type + "+" + e.Value
|
||||
})
|
||||
.Where(f => allListedItemValues.Select(e => $"{(int)e.MagicNumber}+{e.Value}").Contains(f.Key))
|
||||
.Select(e => e.item)
|
||||
.Where(e => types.Contains(e.Type) && values.Contains(e.Value))
|
||||
.AsEnumerable()
|
||||
.Where(e => allListedItemValuesSet.Contains((e.Type, e.Value)))
|
||||
.ToArray();
|
||||
var missingItemValues = allListedItemValues.Except(existingValues.Select(f => (MagicNumber: f.Type, f.Value))).Select(f => new ItemValue()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Server.ServerSetupApp;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Server.Migrations.Routines;
|
||||
|
||||
/// <summary>
|
||||
/// Migration to fix broken library subtitle download languages.
|
||||
/// </summary>
|
||||
[JellyfinMigration("2026-02-06T20:00:00", nameof(FixLibrarySubtitleDownloadLanguages))]
|
||||
internal class FixLibrarySubtitleDownloadLanguages : IAsyncMigrationRoutine
|
||||
{
|
||||
private readonly ILocalizationManager _localizationManager;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FixLibrarySubtitleDownloadLanguages"/> class.
|
||||
/// </summary>
|
||||
/// <param name="localizationManager">The Localization manager.</param>
|
||||
/// <param name="startupLogger">The startup logger for Startup UI integration.</param>
|
||||
/// <param name="libraryManager">The Library manager.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public FixLibrarySubtitleDownloadLanguages(
|
||||
ILocalizationManager localizationManager,
|
||||
IStartupLogger<FixLibrarySubtitleDownloadLanguages> startupLogger,
|
||||
ILibraryManager libraryManager,
|
||||
ILogger<FixLibrarySubtitleDownloadLanguages> logger)
|
||||
{
|
||||
_localizationManager = localizationManager;
|
||||
_libraryManager = libraryManager;
|
||||
_logger = startupLogger.With(logger);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task PerformAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Starting to fix library subtitle download languages.");
|
||||
|
||||
var virtualFolders = _libraryManager.GetVirtualFolders(false);
|
||||
|
||||
foreach (var virtualFolder in virtualFolders)
|
||||
{
|
||||
var options = virtualFolder.LibraryOptions;
|
||||
if (options.SubtitleDownloadLanguages is null || options.SubtitleDownloadLanguages.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Some virtual folders don't have a proper item id.
|
||||
if (!Guid.TryParse(virtualFolder.ItemId, out var folderId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var collectionFolder = _libraryManager.GetItemById<CollectionFolder>(folderId);
|
||||
if (collectionFolder is null)
|
||||
{
|
||||
_logger.LogWarning("Could not find collection folder for virtual folder '{LibraryName}' with id '{FolderId}'. Skipping.", virtualFolder.Name, folderId);
|
||||
continue;
|
||||
}
|
||||
|
||||
var fixedLanguages = new List<string>();
|
||||
|
||||
foreach (var language in options.SubtitleDownloadLanguages)
|
||||
{
|
||||
var foundLanguage = _localizationManager.FindLanguageInfo(language)?.ThreeLetterISOLanguageName;
|
||||
if (foundLanguage is not null)
|
||||
{
|
||||
// Converted ISO 639-2/B to T (ger to deu)
|
||||
if (!string.Equals(foundLanguage, language, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogInformation("Converted '{Language}' to '{ResolvedLanguage}' in library '{LibraryName}'.", language, foundLanguage, virtualFolder.Name);
|
||||
}
|
||||
|
||||
if (fixedLanguages.Contains(foundLanguage, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogInformation("Language '{Language}' already exists for library '{LibraryName}'. Skipping duplicate.", foundLanguage, virtualFolder.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
fixedLanguages.Add(foundLanguage);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Could not resolve language '{Language}' in library '{LibraryName}'. Skipping.", language, virtualFolder.Name);
|
||||
}
|
||||
}
|
||||
|
||||
options.SubtitleDownloadLanguages = [.. fixedLanguages];
|
||||
collectionFolder.UpdateLibraryOptions(options);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Library subtitle download languages fixed.");
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -464,6 +464,16 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
|
||||
SqliteConnection.ClearAllPools();
|
||||
|
||||
using (var checkpointConnection = new SqliteConnection($"Filename={libraryDbPath}"))
|
||||
{
|
||||
checkpointConnection.Open();
|
||||
using var cmd = checkpointConnection.CreateCommand();
|
||||
cmd.CommandText = "PRAGMA wal_checkpoint(TRUNCATE);";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
SqliteConnection.ClearAllPools();
|
||||
|
||||
_logger.LogInformation("Move {0} to {1}.", libraryDbPath, libraryDbPath + ".old");
|
||||
File.Move(libraryDbPath, libraryDbPath + ".old", true);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Chapters;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@@ -2197,17 +2196,6 @@ namespace MediaBrowser.Controller.Entities
|
||||
};
|
||||
}
|
||||
|
||||
// Music albums usually don't have dedicated backdrops, so return one from the artist instead
|
||||
if (GetType() == typeof(MusicAlbum) && imageType == ImageType.Backdrop)
|
||||
{
|
||||
var artist = FindParent<MusicArtist>();
|
||||
|
||||
if (artist is not null)
|
||||
{
|
||||
return artist.GetImages(imageType).ElementAtOrDefault(imageIndex);
|
||||
}
|
||||
}
|
||||
|
||||
return GetImages(imageType)
|
||||
.ElementAtOrDefault(imageIndex);
|
||||
}
|
||||
|
||||
@@ -863,7 +863,7 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
{
|
||||
stream.IsAnamorphic = false;
|
||||
}
|
||||
else if (string.Equals(streamInfo.SampleAspectRatio, "1:1", StringComparison.Ordinal))
|
||||
else if (IsNearSquarePixelSar(streamInfo.SampleAspectRatio))
|
||||
{
|
||||
stream.IsAnamorphic = false;
|
||||
}
|
||||
@@ -1154,6 +1154,34 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
return Math.Abs(d1 - d2) <= variance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a sample aspect ratio represents square (or near-square) pixels.
|
||||
/// Some encoders produce SARs like 3201:3200 for content that is effectively 1:1,
|
||||
/// which would be falsely classified as anamorphic by an exact string comparison.
|
||||
/// A 1% tolerance safely covers encoder rounding artifacts while preserving detection
|
||||
/// of genuine anamorphic content (closest standard is PAL 4:3 at 16:15 = 6.67% off).
|
||||
/// </summary>
|
||||
/// <param name="sar">The sample aspect ratio string in "N:D" format.</param>
|
||||
/// <returns><c>true</c> if the SAR is within 1% of 1:1; otherwise <c>false</c>.</returns>
|
||||
internal static bool IsNearSquarePixelSar(string sar)
|
||||
{
|
||||
if (string.IsNullOrEmpty(sar))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var parts = sar.Split(':');
|
||||
if (parts.Length == 2
|
||||
&& double.TryParse(parts[0], CultureInfo.InvariantCulture, out var num)
|
||||
&& double.TryParse(parts[1], CultureInfo.InvariantCulture, out var den)
|
||||
&& den > 0)
|
||||
{
|
||||
return IsClose(num / den, 1.0, 0.01);
|
||||
}
|
||||
|
||||
return string.Equals(sar, "1:1", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a frame rate from a string value in ffprobe output
|
||||
/// This could be a number or in the format of 2997/125.
|
||||
|
||||
11
README.md
11
README.md
@@ -94,13 +94,12 @@ git clone https://github.com/jellyfin/jellyfin.git
|
||||
|
||||
The server is configured to host the static files required for the [web client](https://github.com/jellyfin/jellyfin-web) in addition to serving the backend by default. Before you can run the server, you will need to get a copy of the web client since they are not included in this repository directly.
|
||||
|
||||
Note that it is also possible to [host the web client separately](#hosting-the-web-client-separately) from the web server with some additional configuration, in which case you can skip this step.
|
||||
Note that it is recommended for development to [host the web client separately](#hosting-the-web-client-separately) from the web server with some additional configuration, in which case you can skip this step.
|
||||
|
||||
There are three options to get the files for the web client.
|
||||
There are two options to get the files for the web client.
|
||||
|
||||
1. Download one of the finished builds from the [Azure DevOps pipeline](https://dev.azure.com/jellyfin-project/jellyfin/_build?definitionId=27). You can download the build for a specific release by looking at the [branches tab](https://dev.azure.com/jellyfin-project/jellyfin/_build?definitionId=27&_a=summary&repositoryFilter=6&view=branches) of the pipelines page.
|
||||
2. Build them from source following the instructions on the [jellyfin-web repository](https://github.com/jellyfin/jellyfin-web)
|
||||
3. Get the pre-built files from an existing installation of the server. For example, with a Windows server installation the client files are located at `C:\Program Files\Jellyfin\Server\jellyfin-web`
|
||||
1. Build them from source following the instructions on the [jellyfin-web repository](https://github.com/jellyfin/jellyfin-web)
|
||||
2. Get the pre-built files from an existing installation of the server. For example, with a Windows server installation the client files are located at `C:\Program Files\Jellyfin\Server\jellyfin-web`
|
||||
|
||||
### Running The Server
|
||||
|
||||
@@ -198,5 +197,5 @@ This project is supported by:
|
||||
<br/>
|
||||
<a href="https://www.digitalocean.com"><img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" height="50px" alt="DigitalOcean"></a>
|
||||
|
||||
<a href="https://www.jetbrains.com"><img src="https://gist.githubusercontent.com/anthonylavado/e8b2403deee9581e0b4cb8cd675af7db/raw/fa104b7d73f759d7262794b94569f1b89df41c0b/jetbrains.svg" height="50px" alt="JetBrains logo"></a>
|
||||
<a href="https://www.jetbrains.com"><img src="https://gist.githubusercontent.com/anthonylavado/e8b2403deee9581e0b4cb8cd675af7db/raw/199ae22980ef5da64882ec2de3e8e5c03fe535b8/jetbrains.svg" height="50px" alt="JetBrains logo"></a>
|
||||
</p>
|
||||
|
||||
@@ -85,7 +85,6 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
|
||||
"jpeg",
|
||||
"jpg",
|
||||
"png",
|
||||
"aiff",
|
||||
"cr2",
|
||||
"crw",
|
||||
"nef",
|
||||
|
||||
@@ -39,6 +39,23 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
|
||||
public void GetFrameRate_Success(string value, float? expected)
|
||||
=> Assert.Equal(expected, ProbeResultNormalizer.GetFrameRate(value));
|
||||
|
||||
[Theory]
|
||||
[InlineData("1:1", true)]
|
||||
[InlineData("3201:3200", true)]
|
||||
[InlineData("1215:1216", true)]
|
||||
[InlineData("1001:1000", true)]
|
||||
[InlineData("16:15", false)]
|
||||
[InlineData("8:9", false)]
|
||||
[InlineData("32:27", false)]
|
||||
[InlineData("10:11", false)]
|
||||
[InlineData("64:45", false)]
|
||||
[InlineData("4:3", false)]
|
||||
[InlineData("0:1", false)]
|
||||
[InlineData("", false)]
|
||||
[InlineData(null, false)]
|
||||
public void IsNearSquarePixelSar_DetectsCorrectly(string? sar, bool expected)
|
||||
=> Assert.Equal(expected, ProbeResultNormalizer.IsNearSquarePixelSar(sar));
|
||||
|
||||
[Fact]
|
||||
public void GetMediaInfo_MetaData_Success()
|
||||
{
|
||||
@@ -123,6 +140,7 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
|
||||
Assert.Equal(358, res.VideoStream.Height);
|
||||
Assert.Equal(720, res.VideoStream.Width);
|
||||
Assert.Equal("2.40:1", res.VideoStream.AspectRatio);
|
||||
Assert.True(res.VideoStream.IsAnamorphic); // SAR 32:27 — genuinely anamorphic NTSC DVD 16:9
|
||||
Assert.Equal("yuv420p", res.VideoStream.PixelFormat);
|
||||
Assert.Equal(31d, res.VideoStream.Level);
|
||||
Assert.Equal(1, res.VideoStream.RefFrames);
|
||||
|
||||
Reference in New Issue
Block a user