Compare commits

...

39 Commits

Author SHA1 Message Date
Jellyfin Release Bot
40a33da2a5 Bump version to 10.11.1 2025-10-26 22:02:09 -04:00
Joshua M. Boniface
3596fc0693 Fix bump_version to handle spaced filename 2025-10-26 21:50:38 -04:00
Jellyfin Release Bot
93824dad97 Bump version to 10.11.1 2025-10-26 21:41:27 -04:00
Tim Eisele
e5656af1f2 Improve symlink handling (#15209) 2025-10-26 15:10:13 -06:00
Niels van Velzen
c127c10458 Merge pull request #15225 from Bond-009/z440ATL
Update dependency z440.atl.core to 7.6.0
2025-10-26 18:50:04 +01:00
Tim Eisele
7d1824ea27 Fix pagination and sorting for folders (#15187) 2025-10-26 11:34:11 -06:00
Cody Robibero
2966d27c97 Skip invalid database migration (#15212) 2025-10-26 11:34:04 -06:00
Ivan Kara
618ec4543e Add season number fallback for OMDB and TMDB plugins (#15113) 2025-10-26 11:33:55 -06:00
Cody Robibero
0e4031ae52 Skip extracting directory entry when restoring (#15196) 2025-10-26 11:33:47 -06:00
CeruleanRed
442af96ed9 Only save chapters that are within the runtime of the video file (#15176) 2025-10-26 10:37:16 -06:00
JJBlue
a305204cfa Skip extracted files in migration if bad timestamp or no access (#15220)
Fixes #15024
2025-10-26 10:30:43 -06:00
theguymadmax
75f472e6a7 Normalize paths in database queries (#15217) 2025-10-26 10:30:12 -06:00
Bond_009
cc32e8f7cb Update dependency z440.atl.core to 7.6.0 2025-10-26 15:16:08 +01:00
MBR-0001
14b3085ff1 Fix Has(Imdb/Tmdb/Tvdb)Id checks (#15126) 2025-10-25 16:00:55 -06:00
Cody Robibero
5691eee4f1 Prefer filting by package id instead of name (#15197) 2025-10-25 09:37:09 -06:00
theguymadmax
1520a697ad Play selected song first with instant mix (#15133) 2025-10-25 09:33:11 -06:00
Cody Robibero
81b8b0ca4a Add the transcode marker during startup instead of first transcode (#15194) 2025-10-25 09:32:15 -06:00
Cody Robibero
ac3fa3c376 Clean up backup service (#15170) 2025-10-24 17:57:34 -06:00
Tim Eisele
7a1c1cd342 Skip extracted files in migration if bad timestamp or no access (#15112) 2025-10-24 17:57:19 -06:00
gnattu
70c32a26fa Make priority class setting more robust (#15177) 2025-10-24 17:57:02 -06:00
Cody Robibero
2b94bb54aa Fix xml formatter (#15164) 2025-10-24 17:56:38 -06:00
Bond-009
0a6e8146be Lower required tmp dir size to 512MiB (#15098) 2025-10-23 16:38:27 -06:00
theguymadmax
305b0fdca3 Make season paths case-insensitive (#15102) 2025-10-23 16:38:06 -06:00
theguymadmax
d738386fe2 Fix LiveTV images not saving to database (#15083) 2025-10-23 16:37:55 -06:00
Tim Eisele
ca830d5be7 Speed-up trickplay migration (#15054) 2025-10-23 16:37:47 -06:00
theguymadmax
a5bc4524d8 Optimize artist query (#15087) 2025-10-23 16:37:29 -06:00
Nyanmisaka
175ee12bbc Fix videos with cropping metadata are probed as anamorphic (#15144) 2025-10-23 16:31:11 -06:00
Nyanmisaka
a725220c21 Reject stream copy of HDR10+ video if the client does not support HDR10 (#15072) 2025-10-21 17:20:56 -06:00
gnattu
a245605152 Log the message more clear when network manager is not ready (#15055) 2025-10-21 17:18:26 -06:00
Tim Eisele
f4a53209f4 Skip invalid keyframe cache data (#15032) 2025-10-21 17:17:56 -06:00
Jellyfin Release Bot
877251bcae Bump version to 10.11.0 2025-10-19 20:45:12 -04:00
Bond-009
ace30afcf8 Merge pull request #15016 from jellyfin/renovate/ci-deps
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Update github/codeql-action action to v4.30.9
2025-10-19 11:14:52 +02:00
rimasx
fc056b6273 Translated using Weblate (Estonian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/et/
2025-10-19 05:33:11 +00:00
rimasx
ac5efb4775 Translated using Weblate (Estonian)
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/et/
2025-10-18 11:54:27 +00:00
renovate[bot]
fefd676adc Update github/codeql-action action to v4.30.9 2025-10-17 15:56:06 +00:00
Bond-009
59c17a663c Merge pull request #15006 from jellyfin/renovate/dotnet-monorepo
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
Update dependency dotnet-ef to v9.0.10
2025-10-15 17:39:47 +02:00
Bond-009
641551e164 Merge pull request #15007 from jellyfin/renovate/microsoft
Update Microsoft to 9.0.10
2025-10-15 17:39:32 +02:00
renovate[bot]
bd543d7ac3 Update Microsoft to 9.0.10 2025-10-15 00:34:53 +00:00
renovate[bot]
545e412259 Update dependency dotnet-ef to v9.0.10 2025-10-15 00:34:44 +00:00
40 changed files with 434 additions and 384 deletions

View File

@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "9.0.9",
"version": "9.0.10",
"commands": [
"dotnet-ef"
]

View File

@@ -27,11 +27,11 @@ jobs:
dotnet-version: '9.0.x'
- name: Initialize CodeQL
uses: github/codeql-action/init@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8
uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8
uses: github/codeql-action/autobuild@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8
uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 # v4.30.9

View File

@@ -26,32 +26,32 @@
<PackageVersion Include="libse" Version="4.0.12" />
<PackageVersion Include="LrcParser" Version="2025.623.0" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.10" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.10" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.9" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.9" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.9" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.9" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.9" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.10" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
<PackageVersion Include="MimeTypes" Version="2.5.2" />
<PackageVersion Include="Morestachio" Version="5.0.1.631" />
@@ -84,11 +84,11 @@
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageVersion Include="System.Globalization" Version="4.3.0" />
<PackageVersion Include="System.Linq.Async" Version="6.0.3" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.9" />
<PackageVersion Include="System.Text.Json" Version="9.0.9" />
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.9" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.10" />
<PackageVersion Include="System.Text.Json" Version="9.0.10" />
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.10" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
<PackageVersion Include="z440.atl.core" Version="7.5.0" />
<PackageVersion Include="z440.atl.core" Version="7.6.0" />
<PackageVersion Include="TMDbLib" Version="2.3.0" />
<PackageVersion Include="UTF.Unknown" Version="2.6.0" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
@@ -96,4 +96,4 @@
<PackageVersion Include="Xunit.SkippableFact" Version="1.5.23" />
<PackageVersion Include="xunit" Version="2.9.3" />
</ItemGroup>
</Project>
</Project>

View File

@@ -36,7 +36,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Naming</PackageId>
<VersionPrefix>10.11.0</VersionPrefix>
<VersionPrefix>10.11.1</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View File

@@ -10,10 +10,10 @@ namespace Emby.Naming.TV
/// </summary>
public static partial class SeasonPathParser
{
[GeneratedRegex(@"^\s*((?<seasonnumber>(?>\d+))(?:st|nd|rd|th|\.)*(?!\s*[Ee]\d+))\s*(?:[[]*|[]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<rightpart>.*)$")]
[GeneratedRegex(@"^\s*((?<seasonnumber>(?>\d+))(?:st|nd|rd|th|\.)*(?!\s*[Ee]\d+))\s*(?:[[]*|[]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<rightpart>.*)$", RegexOptions.IgnoreCase)]
private static partial Regex ProcessPre();
[GeneratedRegex(@"^\s*(?:[[]*|[]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<seasonnumber>(?>\d+)(?!\s*[Ee]\d+))(?<rightpart>.*)$")]
[GeneratedRegex(@"^\s*(?:[[]*|[]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<seasonnumber>(?>\d+)(?!\s*[Ee]\d+))(?<rightpart>.*)$", RegexOptions.IgnoreCase)]
private static partial Regex ProcessPost();
/// <summary>
@@ -86,7 +86,7 @@ namespace Emby.Naming.TV
}
}
if (filename.StartsWith('s'))
if (filename.Length > 0 && (filename[0] == 'S' || filename[0] == 's'))
{
var testFilename = filename.AsSpan()[1..];

View File

@@ -223,7 +223,7 @@ public class ChapterManager : IChapterManager
if (saveChapters && changesMade)
{
_chapterRepository.SaveChapters(video.Id, chapters);
SaveChapters(video, chapters);
}
DeleteDeadImages(currentImages, chapters);
@@ -234,7 +234,9 @@ public class ChapterManager : IChapterManager
/// <inheritdoc />
public void SaveChapters(Video video, IReadOnlyList<ChapterInfo> chapters)
{
_chapterRepository.SaveChapters(video.Id, chapters);
// Remove any chapters that are outside of the runtime of the video
var validChapters = chapters.Where(c => c.StartPositionTicks < video.RunTimeTicks).ToList();
_chapterRepository.SaveChapters(video.Id, validChapters);
}
/// <inheritdoc />

View File

@@ -152,6 +152,10 @@ namespace Emby.Server.Implementations.IO
/// <inheritdoc />
public void MoveDirectory(string source, string destination)
{
// Make sure parent directory of target exists
var parent = Directory.GetParent(destination);
parent?.Create();
try
{
Directory.Move(source, destination);
@@ -248,47 +252,40 @@ namespace Emby.Server.Implementations.IO
{
result.IsDirectory = info is DirectoryInfo || (info.Attributes & FileAttributes.Directory) == FileAttributes.Directory;
// if (!result.IsDirectory)
// {
// result.IsHidden = (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden;
// }
if (info is FileInfo fileInfo)
{
result.Length = fileInfo.Length;
// Issue #2354 get the size of files behind symbolic links. Also Enum.HasFlag is bad as it boxes!
if ((fileInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint)
result.CreationTimeUtc = GetCreationTimeUtc(info);
result.LastWriteTimeUtc = GetLastWriteTimeUtc(info);
if (fileInfo.LinkTarget is not null)
{
try
{
using (var fileHandle = File.OpenHandle(fileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
var targetFileInfo = (FileInfo?)fileInfo.ResolveLinkTarget(returnFinalTarget: true);
if (targetFileInfo is not null)
{
result.Length = RandomAccess.GetLength(fileHandle);
result.Exists = targetFileInfo.Exists;
if (result.Exists)
{
result.Length = targetFileInfo.Length;
result.CreationTimeUtc = GetCreationTimeUtc(targetFileInfo);
result.LastWriteTimeUtc = GetLastWriteTimeUtc(targetFileInfo);
}
}
else
{
result.Exists = false;
}
}
catch (FileNotFoundException ex)
{
// Dangling symlinks cannot be detected before opening the file unfortunately...
_logger.LogError(ex, "Reading the file size of the symlink at {Path} failed. Marking the file as not existing.", fileInfo.FullName);
result.Exists = false;
}
catch (UnauthorizedAccessException ex)
{
_logger.LogError(ex, "Reading the file at {Path} failed due to a permissions exception.", fileInfo.FullName);
}
catch (IOException ex)
{
// IOException generally means the file is not accessible due to filesystem issues
// Catch this exception and mark the file as not exist to ignore it
_logger.LogError(ex, "Reading the file at {Path} failed due to an IO Exception. Marking the file as not existing", fileInfo.FullName);
result.Exists = false;
}
}
else
{
result.Length = fileInfo.Length;
}
}
result.CreationTimeUtc = GetCreationTimeUtc(info);
result.LastWriteTimeUtc = GetLastWriteTimeUtc(info);
}
else
{

View File

@@ -51,8 +51,7 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
}
// Fast path in case the ignore files isn't a symlink and is empty
if ((dirIgnoreFile.Attributes & FileAttributes.ReparsePoint) == 0
&& dirIgnoreFile.Length == 0)
if (dirIgnoreFile.LinkTarget is null && dirIgnoreFile.Length == 0)
{
return true;
}
@@ -93,6 +92,12 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
private static string GetFileContent(FileInfo dirIgnoreFile)
{
dirIgnoreFile = (FileInfo?)dirIgnoreFile.ResolveLinkTarget(returnFinalTarget: true) ?? dirIgnoreFile;
if (!dirIgnoreFile.Exists)
{
return string.Empty;
}
using (var reader = dirIgnoreFile.OpenText())
{
return reader.ReadToEnd();

View File

@@ -28,7 +28,9 @@ namespace Emby.Server.Implementations.Library
public IReadOnlyList<BaseItem> GetInstantMixFromSong(Audio item, User? user, DtoOptions dtoOptions)
{
return GetInstantMixFromGenres(item.Genres, user, dtoOptions);
var instantMixItems = GetInstantMixFromGenres(item.Genres, user, dtoOptions);
return [item, .. instantMixItems.Where(i => !i.Id.Equals(item.Id))];
}
/// <inheritdoc />

View File

@@ -1,14 +1,14 @@
{
"TaskCleanActivityLogDescription": "Kustutab määratud ajast vanemad tegevuslogi kirjed.",
"UserDownloadingItemWithValues": "{0} laeb alla {1}",
"UserDownloadingItemWithValues": "{0} laadib alla {1}",
"HeaderRecordingGroups": "Salvestusrühmad",
"TaskOptimizeDatabaseDescription": "Tihendab ja puhastab andmebaasi. Selle toimingu tegemine pärast meediakogu andmebaasiga seotud muudatuste skannimist võib jõudlust parandada.",
"TaskOptimizeDatabase": "Optimeeri andmebaasi",
"TaskDownloadMissingSubtitlesDescription": "Otsib veebist puuduvaid subtiitreid vastavalt määratud metaandmete seadetele.",
"TaskDownloadMissingSubtitles": "Laadi alla puuduvad subtiitrid",
"TaskDownloadMissingSubtitles": "Hangi puuduvad subtiitrid",
"TaskRefreshChannelsDescription": "Värskendab veebikanalite teavet.",
"TaskRefreshChannels": "Värskenda kanaleid",
"TaskCleanTranscodeDescription": "Kustutab üle ühe päeva vanused transkodeerimisfailid.",
"TaskCleanTranscodeDescription": "Kustutab üle ühe päeva vanused transkoodimisfailid.",
"TaskCleanTranscode": "Puhasta transkoodimise kataloog",
"TaskUpdatePluginsDescription": "Laadib alla ja paigaldab nende pluginate uuendused, mis on seadistatud automaatselt uuenduma.",
"TaskUpdatePlugins": "Uuenda pluginaid",
@@ -92,7 +92,7 @@
"HeaderNextUp": "Järgmisena",
"HeaderLiveTV": "Otse TV",
"HeaderFavoriteSongs": "Lemmiklood",
"HeaderFavoriteShows": "Lemmikseriaalid",
"HeaderFavoriteShows": "Lemmiksarjad",
"HeaderFavoriteEpisodes": "Lemmikepisoodid",
"HeaderFavoriteArtists": "Lemmikesitajad",
"HeaderFavoriteAlbums": "Lemmikalbumid",
@@ -122,20 +122,20 @@
"UserOnlineFromDevice": "{0} on ühendatud seadmest {1}",
"External": "Väline",
"HearingImpaired": "Kuulmispuudega",
"TaskKeyframeExtractorDescription": "Eraldab videofailidest võtmekaadreid, et luua täpsemaid HLS-i esitusloendeid. See ülesanne võib kesta pikka aega.",
"TaskKeyframeExtractor": "Võtmekaadrite eraldamine",
"TaskKeyframeExtractorDescription": "Eraldab videofailidest võtmekaadrid, et luua täpsemaid HLS-i esitusloendeid. See võib kesta pikka aega.",
"TaskKeyframeExtractor": "Eralda võtmekaadrid",
"TaskRefreshTrickplayImages": "Loo trickplay pildid",
"TaskRefreshTrickplayImagesDescription": "Loob trickplay eelvaated videotele lubatud meediakogudes.",
"TaskAudioNormalization": "Normaliseeri heli",
"TaskAudioNormalization": "Normaliseeri helitugevus",
"TaskAudioNormalizationDescription": "Otsib failidest helitugevuse normaliseerimise teavet.",
"TaskCleanCollectionsAndPlaylistsDescription": "Eemaldab kogumikest ja esitusloenditest asjad, mida enam ei eksisteeri.",
"TaskCleanCollectionsAndPlaylistsDescription": "Eemaldab kogumikest ja esitusloenditest üksused, mida enam ei eksisteeri.",
"TaskCleanCollectionsAndPlaylists": "Puhasta kogumikud ja esitusloendid",
"TaskDownloadMissingLyrics": "Lae alla puuduolev lüürika",
"TaskDownloadMissingLyricsDescription": "Lae lauludele alla lüürika",
"TaskDownloadMissingLyrics": "Hangi puuduvad laulusõnad",
"TaskDownloadMissingLyricsDescription": "Laulusõnade allalaadimine",
"TaskMoveTrickplayImagesDescription": "Liigutab trickplay pildid meediakogu sätete kohaselt.",
"TaskExtractMediaSegments": "Meediasegmentide skaneerimine",
"TaskExtractMediaSegments": "Skaneeri meediasegmente",
"TaskExtractMediaSegmentsDescription": "Eraldab või võtab meediasegmendid MediaSegment'i lubavatest pluginatest.",
"TaskMoveTrickplayImages": "Muuda trickplay piltide asukoht",
"CleanupUserDataTask": "Kasutajaandmete puhastamise ülesanne",
"CleanupUserDataTask": "Puhasta kasutajaandmed",
"CleanupUserDataTaskDescription": "Puhastab kõik kasutajaandmed (vaatamise olek, lemmikute olek jne) meediast, mis pole enam vähemalt 90 päeva saadaval olnud."
}

View File

@@ -223,15 +223,14 @@ namespace Emby.Server.Implementations.Updates
Guid id = default,
Version? specificVersion = null)
{
if (name is not null)
{
availablePackages = availablePackages.Where(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
}
if (!id.IsEmpty())
{
availablePackages = availablePackages.Where(x => x.Id.Equals(id));
}
else if (name is not null)
{
availablePackages = availablePackages.Where(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
}
if (specificVersion is not null)
{

View File

@@ -1,4 +1,8 @@
using System;
using System.Net.Mime;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Formatters;
namespace Jellyfin.Api.Formatters;
@@ -6,7 +10,7 @@ namespace Jellyfin.Api.Formatters;
/// <summary>
/// Xml output formatter.
/// </summary>
public sealed class XmlOutputFormatter : StringOutputFormatter
public sealed class XmlOutputFormatter : TextOutputFormatter
{
/// <summary>
/// Initializes a new instance of the <see cref="XmlOutputFormatter"/> class.
@@ -15,5 +19,24 @@ public sealed class XmlOutputFormatter : StringOutputFormatter
{
SupportedMediaTypes.Clear();
SupportedMediaTypes.Add(MediaTypeNames.Text.Xml);
SupportedEncodings.Add(Encoding.UTF8);
SupportedEncodings.Add(Encoding.Unicode);
}
/// <inheritdoc />
public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
{
ArgumentNullException.ThrowIfNull(context);
ArgumentNullException.ThrowIfNull(selectedEncoding);
var valueAsString = context.Object?.ToString();
if (string.IsNullOrEmpty(valueAsString))
{
return;
}
var response = context.HttpContext.Response;
await response.WriteAsync(valueAsString, selectedEncoding).ConfigureAwait(false);
}
}

View File

@@ -18,7 +18,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Data</PackageId>
<VersionPrefix>10.11.0</VersionPrefix>
<VersionPrefix>10.11.1</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View File

@@ -128,7 +128,8 @@ public class BackupService : IBackupService
var targetPath = Path.GetFullPath(Path.Combine(target, Path.GetRelativePath(source, item.FullName)));
if (!sourcePath.StartsWith(fullSourcePath, StringComparison.Ordinal)
|| !targetPath.StartsWith(fullTargetRoot, StringComparison.Ordinal))
|| !targetPath.StartsWith(fullTargetRoot, StringComparison.Ordinal)
|| Path.EndsInDirectorySeparator(item.FullName))
{
continue;
}
@@ -199,7 +200,7 @@ public class BackupService : IBackupService
var zipEntry = zipArchive.GetEntry(NormalizePathSeparator(Path.Combine("Database", $"{entityType.Type.Name}.json")));
if (zipEntry is null)
{
_logger.LogInformation("No backup of expected table {Table} is present in backup. Continue anyway.", entityType.Type.Name);
_logger.LogInformation("No backup of expected table {Table} is present in backup, continuing anyway", entityType.Type.Name);
continue;
}
@@ -223,7 +224,7 @@ public class BackupService : IBackupService
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not store entity {Entity} continue anyway.", item);
_logger.LogError(ex, "Could not store entity {Entity}, continuing anyway", item);
}
}
@@ -233,11 +234,11 @@ public class BackupService : IBackupService
_logger.LogInformation("Try restore Database");
await dbContext.SaveChangesAsync().ConfigureAwait(false);
_logger.LogInformation("Restored database.");
_logger.LogInformation("Restored database");
}
}
_logger.LogInformation("Restored Jellyfin system from {Date}.", manifest.DateCreated);
_logger.LogInformation("Restored Jellyfin system from {Date}", manifest.DateCreated);
}
}
@@ -263,6 +264,8 @@ public class BackupService : IBackupService
Options = Map(backupOptions)
};
_logger.LogInformation("Running database optimization before backup");
await _jellyfinDatabaseProvider.RunScheduledOptimisation(CancellationToken.None).ConfigureAwait(false);
var backupFolder = Path.Combine(_applicationPaths.BackupPath);
@@ -281,130 +284,154 @@ public class BackupService : IBackupService
}
var backupPath = Path.Combine(backupFolder, $"jellyfin-backup-{manifest.DateCreated.ToLocalTime():yyyyMMddHHmmss}.zip");
_logger.LogInformation("Attempt to create a new backup at {BackupPath}", backupPath);
var fileStream = File.OpenWrite(backupPath);
await using (fileStream.ConfigureAwait(false))
using (var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Create, false))
try
{
_logger.LogInformation("Start backup process.");
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
_logger.LogInformation("Attempting to create a new backup at {BackupPath}", backupPath);
var fileStream = File.OpenWrite(backupPath);
await using (fileStream.ConfigureAwait(false))
using (var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Create, false))
{
dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
static IAsyncEnumerable<object> GetValues(IQueryable dbSet)
_logger.LogInformation("Starting backup process");
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
var method = dbSet.GetType().GetMethod(nameof(DbSet<object>.AsAsyncEnumerable))!;
var enumerable = method.Invoke(dbSet, null)!;
return (IAsyncEnumerable<object>)enumerable;
}
dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
// include the migration history as well
var historyRepository = dbContext.GetService<IHistoryRepository>();
var migrations = await historyRepository.GetAppliedMigrationsAsync().ConfigureAwait(false);
ICollection<(Type Type, string SourceName, Func<IAsyncEnumerable<object>> ValueFactory)> entityTypes = [
.. typeof(JellyfinDbContext)
.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
.Where(e => e.PropertyType.IsAssignableTo(typeof(IQueryable)))
.Select(e => (Type: e.PropertyType, dbContext.Model.FindEntityType(e.PropertyType.GetGenericArguments()[0])!.GetSchemaQualifiedTableName()!, ValueFactory: new Func<IAsyncEnumerable<object>>(() => GetValues((IQueryable)e.GetValue(dbContext)!)))),
(Type: typeof(HistoryRow), SourceName: nameof(HistoryRow), ValueFactory: () => migrations.ToAsyncEnumerable())
];
manifest.DatabaseTables = entityTypes.Select(e => e.Type.Name).ToArray();
var transaction = await dbContext.Database.BeginTransactionAsync().ConfigureAwait(false);
await using (transaction.ConfigureAwait(false))
{
_logger.LogInformation("Begin Database backup");
foreach (var entityType in entityTypes)
static IAsyncEnumerable<object> GetValues(IQueryable dbSet)
{
_logger.LogInformation("Begin backup of entity {Table}", entityType.SourceName);
var zipEntry = zipArchive.CreateEntry(NormalizePathSeparator(Path.Combine("Database", $"{entityType.SourceName}.json")));
var entities = 0;
var zipEntryStream = zipEntry.Open();
await using (zipEntryStream.ConfigureAwait(false))
var method = dbSet.GetType().GetMethod(nameof(DbSet<object>.AsAsyncEnumerable))!;
var enumerable = method.Invoke(dbSet, null)!;
return (IAsyncEnumerable<object>)enumerable;
}
// include the migration history as well
var historyRepository = dbContext.GetService<IHistoryRepository>();
var migrations = await historyRepository.GetAppliedMigrationsAsync().ConfigureAwait(false);
ICollection<(Type Type, string SourceName, Func<IAsyncEnumerable<object>> ValueFactory)> entityTypes =
[
.. typeof(JellyfinDbContext)
.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
.Where(e => e.PropertyType.IsAssignableTo(typeof(IQueryable)))
.Select(e => (Type: e.PropertyType, dbContext.Model.FindEntityType(e.PropertyType.GetGenericArguments()[0])!.GetSchemaQualifiedTableName()!, ValueFactory: new Func<IAsyncEnumerable<object>>(() => GetValues((IQueryable)e.GetValue(dbContext)!)))),
(Type: typeof(HistoryRow), SourceName: nameof(HistoryRow), ValueFactory: () => migrations.ToAsyncEnumerable())
];
manifest.DatabaseTables = entityTypes.Select(e => e.Type.Name).ToArray();
var transaction = await dbContext.Database.BeginTransactionAsync().ConfigureAwait(false);
await using (transaction.ConfigureAwait(false))
{
_logger.LogInformation("Begin Database backup");
foreach (var entityType in entityTypes)
{
var jsonSerializer = new Utf8JsonWriter(zipEntryStream);
await using (jsonSerializer.ConfigureAwait(false))
_logger.LogInformation("Begin backup of entity {Table}", entityType.SourceName);
var zipEntry = zipArchive.CreateEntry(NormalizePathSeparator(Path.Combine("Database", $"{entityType.SourceName}.json")));
var entities = 0;
var zipEntryStream = zipEntry.Open();
await using (zipEntryStream.ConfigureAwait(false))
{
jsonSerializer.WriteStartArray();
var set = entityType.ValueFactory().ConfigureAwait(false);
await foreach (var item in set.ConfigureAwait(false))
var jsonSerializer = new Utf8JsonWriter(zipEntryStream);
await using (jsonSerializer.ConfigureAwait(false))
{
entities++;
try
jsonSerializer.WriteStartArray();
var set = entityType.ValueFactory().ConfigureAwait(false);
await foreach (var item in set.ConfigureAwait(false))
{
JsonSerializer.SerializeToDocument(item, _serializerSettings).WriteTo(jsonSerializer);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not load entity {Entity}", item);
throw;
entities++;
try
{
using var document = JsonSerializer.SerializeToDocument(item, _serializerSettings);
document.WriteTo(jsonSerializer);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not load entity {Entity}", item);
throw;
}
}
jsonSerializer.WriteEndArray();
}
jsonSerializer.WriteEndArray();
}
}
_logger.LogInformation("backup of entity {Table} with {Number} created", entityType.Type.Name, entities);
_logger.LogInformation("Backup of entity {Table} with {Number} created", entityType.SourceName, entities);
}
}
}
}
_logger.LogInformation("Backup of folder {Table}", _applicationPaths.ConfigurationDirectoryPath);
foreach (var item in Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.xml", SearchOption.TopDirectoryOnly)
.Union(Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.json", SearchOption.TopDirectoryOnly)))
{
zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine("Config", Path.GetFileName(item))));
}
void CopyDirectory(string source, string target, string filter = "*")
{
if (!Directory.Exists(source))
_logger.LogInformation("Backup of folder {Table}", _applicationPaths.ConfigurationDirectoryPath);
foreach (var item in Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.xml", SearchOption.TopDirectoryOnly)
.Union(Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.json", SearchOption.TopDirectoryOnly)))
{
return;
zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine("Config", Path.GetFileName(item))));
}
_logger.LogInformation("Backup of folder {Table}", source);
foreach (var item in Directory.EnumerateFiles(source, filter, SearchOption.AllDirectories))
void CopyDirectory(string source, string target, string filter = "*")
{
zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine(target, Path.GetRelativePath(source, item))));
if (!Directory.Exists(source))
{
return;
}
_logger.LogInformation("Backup of folder {Table}", source);
foreach (var item in Directory.EnumerateFiles(source, filter, SearchOption.AllDirectories))
{
zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine(target, Path.GetRelativePath(source, item))));
}
}
CopyDirectory(Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "users"), Path.Combine("Config", "users"));
CopyDirectory(Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "ScheduledTasks"), Path.Combine("Config", "ScheduledTasks"));
CopyDirectory(Path.Combine(_applicationPaths.RootFolderPath), "Root");
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "collections"), Path.Combine("Data", "collections"));
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "playlists"), Path.Combine("Data", "playlists"));
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "ScheduledTasks"), Path.Combine("Data", "ScheduledTasks"));
if (backupOptions.Subtitles)
{
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "subtitles"), Path.Combine("Data", "subtitles"));
}
if (backupOptions.Trickplay)
{
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "trickplay"), Path.Combine("Data", "trickplay"));
}
if (backupOptions.Metadata)
{
CopyDirectory(Path.Combine(_applicationPaths.InternalMetadataPath), Path.Combine("Data", "metadata"));
}
var manifestStream = zipArchive.CreateEntry(ManifestEntryName).Open();
await using (manifestStream.ConfigureAwait(false))
{
await JsonSerializer.SerializeAsync(manifestStream, manifest).ConfigureAwait(false);
}
}
CopyDirectory(Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "users"), Path.Combine("Config", "users"));
CopyDirectory(Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "ScheduledTasks"), Path.Combine("Config", "ScheduledTasks"));
CopyDirectory(Path.Combine(_applicationPaths.RootFolderPath), "Root");
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "collections"), Path.Combine("Data", "collections"));
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "playlists"), Path.Combine("Data", "playlists"));
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "ScheduledTasks"), Path.Combine("Data", "ScheduledTasks"));
if (backupOptions.Subtitles)
{
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "subtitles"), Path.Combine("Data", "subtitles"));
}
if (backupOptions.Trickplay)
{
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "trickplay"), Path.Combine("Data", "trickplay"));
}
if (backupOptions.Metadata)
{
CopyDirectory(Path.Combine(_applicationPaths.InternalMetadataPath), Path.Combine("Data", "metadata"));
}
var manifestStream = zipArchive.CreateEntry(ManifestEntryName).Open();
await using (manifestStream.ConfigureAwait(false))
{
await JsonSerializer.SerializeAsync(manifestStream, manifest).ConfigureAwait(false);
}
_logger.LogInformation("Backup created");
return Map(manifest, backupPath);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to create backup, removing {BackupPath}", backupPath);
try
{
if (File.Exists(backupPath))
{
File.Delete(backupPath);
}
}
catch (Exception innerEx)
{
_logger.LogWarning(innerEx, "Unable to remove failed backup");
}
_logger.LogInformation("Backup created");
return Map(manifest, backupPath);
throw;
}
}
/// <inheritdoc/>
@@ -422,7 +449,7 @@ public class BackupService : IBackupService
}
catch (Exception ex)
{
_logger.LogError(ex, "Tried to load archive from {Path} but failed.", archivePath);
_logger.LogWarning(ex, "Tried to load manifest from archive {Path} but failed", archivePath);
return null;
}
@@ -459,7 +486,7 @@ public class BackupService : IBackupService
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not load {BackupArchive} path.", item);
_logger.LogWarning(ex, "Tried to load manifest from archive {Path} but failed", item);
}
}

View File

@@ -614,6 +614,13 @@ public sealed class BaseItemRepository
else
{
context.BaseItemProviders.Where(e => e.ItemId == entity.Id).ExecuteDelete();
context.BaseItemImageInfos.Where(e => e.ItemId == entity.Id).ExecuteDelete();
if (entity.Images is { Count: > 0 })
{
context.BaseItemImageInfos.AddRange(entity.Images);
}
context.BaseItems.Attach(entity).State = EntityState.Modified;
}
}
@@ -1756,7 +1763,8 @@ public sealed class BaseItemRepository
if (!string.IsNullOrWhiteSpace(filter.Path))
{
baseQuery = baseQuery.Where(e => e.Path == filter.Path);
var pathToQuery = GetPathToSave(filter.Path);
baseQuery = baseQuery.Where(e => e.Path == pathToQuery);
}
if (!string.IsNullOrWhiteSpace(filter.PresentationUniqueKey))
@@ -2046,7 +2054,7 @@ public sealed class BaseItemRepository
if (filter.ExcludeArtistIds.Length > 0)
{
baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Artist, filter.ExcludeArtistIds, true);
baseQuery = baseQuery.WhereReferencedItemMultipleTypes(context, [ItemValueType.Artist, ItemValueType.AlbumArtist], filter.ExcludeArtistIds, true);
}
if (filter.GenreIds.Count > 0)
@@ -2353,17 +2361,23 @@ public sealed class BaseItemRepository
if (filter.HasImdbId.HasValue)
{
baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "imdb"));
baseQuery = filter.HasImdbId.Value
? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Imdb.ToString().ToLower()))
: baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Imdb.ToString().ToLower()));
}
if (filter.HasTmdbId.HasValue)
{
baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "tmdb"));
baseQuery = filter.HasTmdbId.Value
? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Tmdb.ToString().ToLower()))
: baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Tmdb.ToString().ToLower()));
}
if (filter.HasTvdbId.HasValue)
{
baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "tvdb"));
baseQuery = filter.HasTvdbId.Value
? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Tvdb.ToString().ToLower()))
: baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Tvdb.ToString().ToLower()));
}
var queryTopParentIds = filter.TopParentIds;

View File

@@ -14,7 +14,7 @@ public static class StorageHelper
{
private const long TwoGigabyte = 2_147_483_647L;
private const long FiveHundredAndTwelveMegaByte = 536_870_911L;
private static readonly string[] _byteHumanizedSuffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB"];
private static readonly string[] _byteHumanizedSuffixes = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"];
/// <summary>
/// Tests the available storage capacity on the jellyfin paths with estimated minimum values.
@@ -27,7 +27,7 @@ public static class StorageHelper
TestDataDirectorySize(applicationPaths.LogDirectoryPath, logger, FiveHundredAndTwelveMegaByte);
TestDataDirectorySize(applicationPaths.CachePath, logger, TwoGigabyte);
TestDataDirectorySize(applicationPaths.ProgramDataPath, logger, TwoGigabyte);
TestDataDirectorySize(applicationPaths.TempDirectory, logger, TwoGigabyte);
TestDataDirectorySize(applicationPaths.TempDirectory, logger, FiveHundredAndTwelveMegaByte);
}
/// <summary>
@@ -77,7 +77,7 @@ public static class StorageHelper
var drive = new DriveInfo(path);
if (threshold != -1 && drive.AvailableFreeSpace < threshold)
{
throw new InvalidOperationException($"The path `{path}` has insufficient free space. Required: at least {HumanizeStorageSize(threshold)}.");
throw new InvalidOperationException($"The path `{path}` has insufficient free space. Available: {HumanizeStorageSize(drive.AvailableFreeSpace)}, Required: {HumanizeStorageSize(threshold)}.");
}
logger.LogInformation(

View File

@@ -1,151 +0,0 @@
// The MIT License (MIT)
//
// Copyright (c) .NET Foundation and Contributors
//
// All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
namespace Jellyfin.Server.Infrastructure
{
/// <inheritdoc />
public class SymlinkFollowingPhysicalFileResultExecutor : PhysicalFileResultExecutor
{
/// <summary>
/// Initializes a new instance of the <see cref="SymlinkFollowingPhysicalFileResultExecutor"/> class.
/// </summary>
/// <param name="loggerFactory">An instance of the <see cref="ILoggerFactory"/> interface.</param>
public SymlinkFollowingPhysicalFileResultExecutor(ILoggerFactory loggerFactory) : base(loggerFactory)
{
}
/// <inheritdoc />
protected override FileMetadata GetFileInfo(string path)
{
var fileInfo = new FileInfo(path);
var length = fileInfo.Length;
// This may or may not be fixed in .NET 6, but looks like it will not https://github.com/dotnet/aspnetcore/issues/34371
if ((fileInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint)
{
using var fileHandle = File.OpenHandle(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
length = RandomAccess.GetLength(fileHandle);
}
return new FileMetadata
{
Exists = fileInfo.Exists,
Length = length,
LastModified = fileInfo.LastWriteTimeUtc
};
}
/// <inheritdoc />
protected override async Task WriteFileAsync(ActionContext context, PhysicalFileResult result, RangeItemHeaderValue? range, long rangeLength)
{
ArgumentNullException.ThrowIfNull(context);
ArgumentNullException.ThrowIfNull(result);
if (range is not null && rangeLength == 0)
{
return;
}
// It's a bit of wasted IO to perform this check again, but non-symlinks shouldn't use this code
if (!IsSymLink(result.FileName))
{
await base.WriteFileAsync(context, result, range, rangeLength).ConfigureAwait(false);
return;
}
var response = context.HttpContext.Response;
if (range is not null)
{
await SendFileAsync(
result.FileName,
response,
offset: range.From ?? 0L,
count: rangeLength).ConfigureAwait(false);
return;
}
await SendFileAsync(
result.FileName,
response,
offset: 0,
count: null).ConfigureAwait(false);
}
private async Task SendFileAsync(string filePath, HttpResponse response, long offset, long? count, CancellationToken cancellationToken = default)
{
var fileInfo = GetFileInfo(filePath);
if (offset < 0 || offset > fileInfo.Length)
{
throw new ArgumentOutOfRangeException(nameof(offset), offset, string.Empty);
}
if (count.HasValue
&& (count.Value < 0 || count.Value > fileInfo.Length - offset))
{
throw new ArgumentOutOfRangeException(nameof(count), count, string.Empty);
}
// Copied from SendFileFallback.SendFileAsync
const int BufferSize = 1024 * 16;
var useRequestAborted = !cancellationToken.CanBeCanceled;
var localCancel = useRequestAborted ? response.HttpContext.RequestAborted : cancellationToken;
var fileStream = new FileStream(
filePath,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite,
bufferSize: BufferSize,
options: FileOptions.Asynchronous | FileOptions.SequentialScan);
await using (fileStream.ConfigureAwait(false))
{
try
{
localCancel.ThrowIfCancellationRequested();
fileStream.Seek(offset, SeekOrigin.Begin);
await StreamCopyOperation
.CopyToAsync(fileStream, response.Body, count, BufferSize, localCancel)
.ConfigureAwait(true);
}
catch (OperationCanceledException) when (useRequestAborted)
{
}
}
}
private static bool IsSymLink(string path) => (File.GetAttributes(path) & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint;
}
}

View File

@@ -55,9 +55,25 @@ namespace Jellyfin.Server.Migrations.Routines
};
var dataPath = _paths.DataPath;
using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}"))
var activityLogPath = Path.Combine(dataPath, DbFilename);
if (!File.Exists(activityLogPath))
{
_logger.LogWarning("{ActivityLogDb} doesn't exist, nothing to migrate", activityLogPath);
return;
}
using (var connection = new SqliteConnection($"Filename={activityLogPath}"))
{
connection.Open();
var tableQuery = connection.Query("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='ActivityLog';");
foreach (var row in tableQuery)
{
if (row.GetInt32(0) == 0)
{
_logger.LogWarning("Table 'ActivityLog' doesn't exist in {ActivityLogPath}, nothing to migrate", activityLogPath);
break;
}
}
using var userDbConnection = new SqliteConnection($"Filename={Path.Combine(dataPath, "users.db")}");
userDbConnection.Open();

View File

@@ -122,6 +122,16 @@ public class MigrateKeyframeData : IDatabaseMigrationRoutine
{
lastWriteTimeUtc = File.GetLastWriteTimeUtc(filePath);
}
catch (ArgumentOutOfRangeException e)
{
_logger.LogDebug("Skipping {Path}: {Exception}", filePath, e.Message);
return null;
}
catch (UnauthorizedAccessException e)
{
_logger.LogDebug("Skipping {Path}: {Exception}", filePath, e.Message);
return null;
}
catch (IOException e)
{
_logger.LogDebug("Skipping {Path}: {Exception}", filePath, e.Message);
@@ -135,14 +145,21 @@ public class MigrateKeyframeData : IDatabaseMigrationRoutine
return Path.Join(keyframeCachePath, prefix, filename);
}
private static bool TryReadFromCache(string? cachePath, [NotNullWhen(true)] out MediaEncoding.Keyframes.KeyframeData? cachedResult)
private bool TryReadFromCache(string? cachePath, [NotNullWhen(true)] out MediaEncoding.Keyframes.KeyframeData? cachedResult)
{
if (File.Exists(cachePath))
{
var bytes = File.ReadAllBytes(cachePath);
cachedResult = JsonSerializer.Deserialize<MediaEncoding.Keyframes.KeyframeData>(bytes, _jsonOptions);
try
{
var bytes = File.ReadAllBytes(cachePath);
cachedResult = JsonSerializer.Deserialize<MediaEncoding.Keyframes.KeyframeData>(bytes, _jsonOptions);
return cachedResult is not null;
return cachedResult is not null;
}
catch (JsonException jsonException)
{
_logger.LogWarning(jsonException, "Failed to read {Path}", cachePath);
}
}
cachedResult = null;

View File

@@ -57,11 +57,28 @@ public class MigrateUserDb : IMigrationRoutine
public void Perform()
{
var dataPath = _paths.DataPath;
var userDbPath = Path.Combine(dataPath, DbFilename);
if (!File.Exists(userDbPath))
{
_logger.LogWarning("{UserDbPath} doesn't exist, nothing to migrate", userDbPath);
return;
}
_logger.LogInformation("Migrating the user database may take a while, do not stop Jellyfin.");
using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}"))
using (var connection = new SqliteConnection($"Filename={userDbPath}"))
{
connection.Open();
var tableQuery = connection.Query("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='LocalUsersv2';");
foreach (var row in tableQuery)
{
if (row.GetInt32(0) == 0)
{
_logger.LogWarning("Table 'LocalUsersv2' doesn't exist in {UserDbPath}, nothing to migrate", userDbPath);
break;
}
}
using var dbContext = _provider.CreateDbContext();
var queryResult = connection.Query("SELECT * FROM LocalUsersv2");

View File

@@ -224,6 +224,18 @@ public class MoveExtractedFiles : IAsyncMigrationRoutine
return null;
}
catch (UnauthorizedAccessException e)
{
_logger.LogDebug("Skipping subtitle at index {Index} for {Path}: {Exception}", attachmentStreamIndex, mediaPath, e.Message);
return null;
}
catch (ArgumentOutOfRangeException e)
{
_logger.LogDebug("Skipping attachment at index {Index} for {Path}: {Exception}", attachmentStreamIndex, mediaPath, e.Message);
return null;
}
filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Value.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D", CultureInfo.InvariantCulture);
}
@@ -263,6 +275,18 @@ public class MoveExtractedFiles : IAsyncMigrationRoutine
{
date = File.GetLastWriteTimeUtc(path);
}
catch (ArgumentOutOfRangeException e)
{
_logger.LogDebug("Skipping subtitle at index {Index} for {Path}: {Exception}", streamIndex, path, e.Message);
return null;
}
catch (UnauthorizedAccessException e)
{
_logger.LogDebug("Skipping subtitle at index {Index} for {Path}: {Exception}", streamIndex, path, e.Message);
return null;
}
catch (IOException e)
{
_logger.LogDebug("Skipping subtitle at index {Index} for {Path}: {Exception}", streamIndex, path, e.Message);

View File

@@ -184,6 +184,12 @@ namespace Jellyfin.Server
.AddSingleton<IServiceCollection>(e))
.Build();
/*
* Initialize the transcode path marker so we avoid starting Jellyfin in a broken state.
* This should really be a part of IApplicationPaths but this path is configured differently.
*/
_ = appHost.ConfigurationManager.GetTranscodePath();
// Re-use the host service provider in the app host since ASP.NET doesn't allow a custom service collection.
appHost.ServiceProvider = _jellyfinHost.Services;
PrepareDatabaseProvider(appHost.ServiceProvider);

View File

@@ -250,6 +250,7 @@ public sealed class SetupServer : IDisposable
{ "isInReportingMode", _isUnhealthy },
{ "retryValue", retryAfterValue },
{ "logs", startupLogEntries },
{ "networkManagerReady", networkManager is not null },
{ "localNetworkRequest", networkManager is not null && context.Connection.RemoteIpAddress is not null && networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress) }
},
new ByteCounterStream(context.Response.BodyWriter.AsStream(), IODefaults.FileStreamBufferSize, true, _startupUiRenderer.ParserOptions))

View File

@@ -213,7 +213,12 @@
</ol>
</div>
{{#ELSE}}
{{#IF networkManagerReady}}
<p>Please visit this page from your local network to view detailed startup logs.</p>
{{#ELSE}}
<p>Initializing network settings. Please wait.</p>
{{/ELSE}}
{{/IF}}
{{/ELSE}}
{{/IF}}
</div>

View File

@@ -16,15 +16,12 @@ using Jellyfin.Networking.HappyEyeballs;
using Jellyfin.Server.Extensions;
using Jellyfin.Server.HealthChecks;
using Jellyfin.Server.Implementations.Extensions;
using Jellyfin.Server.Infrastructure;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.XbmcMetadata;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@@ -69,8 +66,6 @@ namespace Jellyfin.Server
options.HttpsPort = _serverApplicationHost.HttpsPort;
});
// TODO remove once this is fixed upstream https://github.com/dotnet/aspnetcore/issues/34371
services.AddSingleton<IActionResultExecutor<PhysicalFileResult>, SymlinkFollowingPhysicalFileResultExecutor>();
services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration());
services.AddJellyfinDbContext(_serverApplicationHost.ConfigurationManager, _configuration);
services.AddJellyfinApiSwagger();

View File

@@ -8,7 +8,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Common</PackageId>
<VersionPrefix>10.11.0</VersionPrefix>
<VersionPrefix>10.11.1</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View File

@@ -715,9 +715,18 @@ namespace MediaBrowser.Controller.Entities
}
else
{
items = GetRecursiveChildren(user, query, out totalCount);
// Save pagination params before clearing them to prevent pagination from happening
// before sorting. PostFilterAndSort will apply pagination after sorting.
var limit = query.Limit;
var startIndex = query.StartIndex;
query.Limit = null;
query.StartIndex = null; // override these here as they have already been applied
query.StartIndex = null;
items = GetRecursiveChildren(user, query, out totalCount);
// Restore pagination params so PostFilterAndSort can apply them after sorting
query.Limit = limit;
query.StartIndex = startIndex;
}
var result = PostFilterAndSort(items, query);
@@ -980,20 +989,16 @@ namespace MediaBrowser.Controller.Entities
else
{
// need to pass this param to the children.
// Note: Don't pass Limit/StartIndex here as pagination should happen after sorting in PostFilterAndSort
var childQuery = new InternalItemsQuery
{
DisplayAlbumFolders = query.DisplayAlbumFolders,
Limit = query.Limit,
StartIndex = query.StartIndex,
NameStartsWith = query.NameStartsWith,
NameStartsWithOrGreater = query.NameStartsWithOrGreater,
NameLessThan = query.NameLessThan
};
items = GetChildren(user, true, out totalItemCount, childQuery).Where(filter);
query.Limit = null;
query.StartIndex = null;
}
var result = PostFilterAndSort(items, query);

View File

@@ -8,7 +8,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Controller</PackageId>
<VersionPrefix>10.11.0</VersionPrefix>
<VersionPrefix>10.11.1</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View File

@@ -2390,8 +2390,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|| (requestHasSDR && videoStream.VideoRangeType == VideoRangeType.DOVIWithSDR)
|| (requestHasHDR10 && videoStream.VideoRangeType == VideoRangeType.HDR10Plus)))
{
// If the video stream is in a static HDR format, don't allow copy if the client does not support HDR10 or HLG.
if (videoStream.VideoRangeType is VideoRangeType.HDR10 or VideoRangeType.HLG)
// If the video stream is in HDR10+ or a static HDR format, don't allow copy if the client does not support HDR10 or HLG.
if (videoStream.VideoRangeType is VideoRangeType.HDR10Plus or VideoRangeType.HDR10 or VideoRangeType.HLG)
{
return false;
}

View File

@@ -1122,7 +1122,15 @@ namespace MediaBrowser.MediaEncoding.Encoder
private void StartProcess(ProcessWrapper process)
{
process.Process.Start();
process.Process.PriorityClass = ProcessPriorityClass.BelowNormal;
try
{
process.Process.PriorityClass = ProcessPriorityClass.BelowNormal;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Unable to set process priority to BelowNormal for {ProcessFileName}", process.Process.StartInfo.FileName);
}
lock (_runningProcessesLock)
{

View File

@@ -930,6 +930,15 @@ namespace MediaBrowser.MediaEncoding.Probing
{
stream.Rotation = data.Rotation;
}
// Parse video frame cropping metadata from side_data
// TODO: save them and make HW filters to apply them in HWA pipelines
else if (string.Equals(data.SideDataType, "Frame Cropping", StringComparison.OrdinalIgnoreCase))
{
// Streams containing artificially added frame cropping
// metadata should not be marked as anamorphic.
stream.IsAnamorphic = false;
}
}
}

View File

@@ -8,7 +8,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Model</PackageId>
<VersionPrefix>10.11.0</VersionPrefix>
<VersionPrefix>10.11.1</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View File

@@ -138,6 +138,8 @@ namespace MediaBrowser.Providers.Plugins.Omdb
}
var item = itemResult.Item;
item.IndexNumber = episodeNumber;
item.ParentIndexNumber = seasonNumber;
var seasonResult = await GetSeasonRootObject(seriesImdbId, seasonNumber, cancellationToken).ConfigureAwait(false);

View File

@@ -177,8 +177,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var item = new Episode
{
IndexNumber = info.IndexNumber,
ParentIndexNumber = info.ParentIndexNumber,
IndexNumber = episodeNumber,
ParentIndexNumber = seasonNumber,
IndexNumberEnd = info.IndexNumberEnd,
Name = episodeResult.Name,
PremiereDate = episodeResult.AirDate,

View File

@@ -1,4 +1,4 @@
using System.Reflection;
[assembly: AssemblyVersion("10.11.0")]
[assembly: AssemblyFileVersion("10.11.0")]
[assembly: AssemblyVersion("10.11.1")]
[assembly: AssemblyFileVersion("10.11.1")]

View File

@@ -58,7 +58,7 @@ for subproject in ${jellyfin_subprojects[@]}; do
done
# Set the version in the GitHub issue template file
sed -i "s|${old_version}|${new_version_sed}|g" ${issue_template_file}
sed -i "s|${old_version}|${new_version_sed}|g" "${issue_template_file}"
# Stage the changed files for commit
git add .

View File

@@ -70,13 +70,14 @@ public static class JellyfinQueryHelperExtensions
bool invert = false)
{
var itemFilter = OneOrManyExpressionBuilder<BaseItemEntity, Guid>(referenceIds, f => f.Id);
var typeFilter = OneOrManyExpressionBuilder<ItemValue, ItemValueType>(itemValueTypes, iv => iv.Type);
return baseQuery.Where(item =>
context.ItemValues
.Where(typeFilter)
.Join(context.ItemValuesMap, e => e.ItemValueId, e => e.ItemValueId, (itemVal, map) => new { itemVal, map })
.Any(val =>
itemValueTypes.Contains(val.itemVal.Type)
&& context.BaseItems.Where(itemFilter).Any(e => e.CleanName == val.itemVal.CleanValue)
context.BaseItems.Where(itemFilter).Any(e => e.CleanName == val.itemVal.CleanValue)
&& val.map.ItemId == item.Id) == EF.Constant(!invert));
}

View File

@@ -15,7 +15,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Extensions</PackageId>
<VersionPrefix>10.11.0</VersionPrefix>
<VersionPrefix>10.11.1</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View File

@@ -42,7 +42,15 @@ public static class FfProbeKeyframeExtractor
try
{
process.Start();
process.PriorityClass = ProcessPriorityClass.BelowNormal;
try
{
process.PriorityClass = ProcessPriorityClass.BelowNormal;
}
catch
{
// We do not care if process priority setting fails
// Ideally log a warning but this does not have a logger available
}
return ParseStream(process.StandardOutput);
}

View File

@@ -7,23 +7,38 @@ public class SeasonPathParserTests
{
[Theory]
[InlineData("/Drive/Season 1", "/Drive", 1, true)]
[InlineData("/Drive/SEASON 1", "/Drive", 1, true)]
[InlineData("/Drive/Staffel 1", "/Drive", 1, true)]
[InlineData("/Drive/STAFFEL 1", "/Drive", 1, true)]
[InlineData("/Drive/Stagione 1", "/Drive", 1, true)]
[InlineData("/Drive/STAGIONE 1", "/Drive", 1, true)]
[InlineData("/Drive/sæson 1", "/Drive", 1, true)]
[InlineData("/Drive/SÆSON 1", "/Drive", 1, true)]
[InlineData("/Drive/Temporada 1", "/Drive", 1, true)]
[InlineData("/Drive/TEMPORADA 1", "/Drive", 1, true)]
[InlineData("/Drive/series 1", "/Drive", 1, true)]
[InlineData("/Drive/SERIES 1", "/Drive", 1, true)]
[InlineData("/Drive/Kausi 1", "/Drive", 1, true)]
[InlineData("/Drive/KAUSI 1", "/Drive", 1, true)]
[InlineData("/Drive/Säsong 1", "/Drive", 1, true)]
[InlineData("/Drive/SÄSONG 1", "/Drive", 1, true)]
[InlineData("/Drive/Seizoen 1", "/Drive", 1, true)]
[InlineData("/Drive/SEIZOEN 1", "/Drive", 1, true)]
[InlineData("/Drive/Seasong 1", "/Drive", 1, true)]
[InlineData("/Drive/SEASONG 1", "/Drive", 1, true)]
[InlineData("/Drive/Sezon 1", "/Drive", 1, true)]
[InlineData("/Drive/SEZON 1", "/Drive", 1, true)]
[InlineData("/Drive/sezona 1", "/Drive", 1, true)]
[InlineData("/Drive/SEZONA 1", "/Drive", 1, true)]
[InlineData("/Drive/sezóna 1", "/Drive", 1, true)]
[InlineData("/Drive/SEZÓNA 1", "/Drive", 1, true)]
[InlineData("/Drive/Sezonul 1", "/Drive", 1, true)]
[InlineData("/Drive/SEZONUL 1", "/Drive", 1, true)]
[InlineData("/Drive/시즌 1", "/Drive", 1, true)]
[InlineData("/Drive/シーズン 1", "/Drive", 1, true)]
[InlineData("/Drive/сезон 1", "/Drive", 1, true)]
[InlineData("/Drive/Сезон 1", "/Drive", 1, true)]
[InlineData("/Drive/СЕЗОН 1", "/Drive", 1, true)]
[InlineData("/Drive/Season 10", "/Drive", 10, true)]
[InlineData("/Drive/Season 100", "/Drive", 100, true)]
[InlineData("/Drive/s1", "/Drive", 1, true)]
@@ -46,8 +61,11 @@ public class SeasonPathParserTests
[InlineData("/Drive/s06e05", "/Drive", null, false)]
[InlineData("/Drive/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv", "/Drive", null, false)]
[InlineData("/Drive/extras", "/Drive", 0, true)]
[InlineData("/Drive/EXTRAS", "/Drive", 0, true)]
[InlineData("/Drive/specials", "/Drive", 0, true)]
[InlineData("/Drive/SPECIALS", "/Drive", 0, true)]
[InlineData("/Drive/Episode 1 Season 2", "/Drive", null, false)]
[InlineData("/Drive/Episode 1 SEASON 2", "/Drive", null, false)]
public void GetSeasonNumberFromPathTest(string path, string? parentPath, int? seasonNumber, bool isSeasonDirectory)
{
var result = SeasonPathParser.Parse(path, parentPath, true, true);