Compare commits

...

50 Commits

Author SHA1 Message Date
Battseren Badral
573ce9ceaa Translated using Weblate (Mongolian)
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
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/mn/
2025-10-29 22:47:08 +00:00
Jacky He
f21fe9f95e Translated using Weblate (Chinese (Traditional Han script, Hong Kong))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant_HK/
2025-10-29 22:47:08 +00:00
Battseren Badral
f92eca3efb Translated using Weblate (Mongolian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/mn/
2025-10-29 00:03:00 +00:00
Pascal Wiesmann
7d778d7bef Translated using Weblate (Alemannic)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/gsw/
2025-10-28 09:23:31 +00:00
JJBlue
21f65e2e27 Backport pull request #15220 from jellyfin/release-10.11.z
Skip extracted files in migration if bad timestamp or no access

Original-merge: a305204cfa

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:31 -04:00
theguymadmax
28b0657608 Backport pull request #15217 from jellyfin/release-10.11.z
Normalize paths in database queries

Original-merge: 75f472e6a7

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:30 -04:00
crobibero
a489942454 Backport pull request #15212 from jellyfin/release-10.11.z
Skip invalid database migration

Original-merge: 2966d27c97

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:29 -04:00
Shadowghost
423c2654c0 Backport pull request #15209 from jellyfin/release-10.11.z
Improve symlink handling

Original-merge: e5656af1f2

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:27 -04:00
crobibero
4dc826644d Backport pull request #15197 from jellyfin/release-10.11.z
Filter plugins by id instead of name

Original-merge: 5691eee4f1

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:26 -04:00
crobibero
0f21222a0c Backport pull request #15196 from jellyfin/release-10.11.z
Skip directory entry when restoring from backup

Original-merge: 0e4031ae52

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:25 -04:00
crobibero
570b8b2eb9 Backport pull request #15194 from jellyfin/release-10.11.z
Initialize transcode marker during startup

Original-merge: 81b8b0ca4a

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:24 -04:00
Shadowghost
08fd175f5a Backport pull request #15187 from jellyfin/release-10.11.z
Fix pagination and sorting for folders

Original-merge: 7d1824ea27

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:23 -04:00
gnattu
511b5d9c53 Backport pull request #15177 from jellyfin/release-10.11.z
Make priority class setting more robust

Original-merge: 70c32a26fa

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:22 -04:00
CeruleanRed
6514196e8d Backport pull request #15176 from jellyfin/release-10.11.z
Only save chapters that are within the runtime of the video file

Original-merge: 442af96ed9

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:21 -04:00
crobibero
ed6cb30762 Backport pull request #15170 from jellyfin/release-10.11.z
Clean up BackupService

Original-merge: ac3fa3c376

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:20 -04:00
crobibero
232c0399e2 Backport pull request #15164 from jellyfin/release-10.11.z
Fix XmlOutputFormatter

Original-merge: 2b94bb54aa

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:19 -04:00
nyanmisaka
dbb015441f Backport pull request #15144 from jellyfin/release-10.11.z
Fix videos with cropping metadata are probed as anamorphic

Original-merge: 175ee12bbc

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:18 -04:00
theguymadmax
4c1c160990 Backport pull request #15133 from jellyfin/release-10.11.z
Play selected song first with instant mix

Original-merge: 1520a697ad

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:17 -04:00
MBR-0001
0931d6e4de Backport pull request #15126 from jellyfin/release-10.11.z
Fix Has(Imdb/Tmdb/Tvdb)Id checks

Original-merge: 14b3085ff1

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:15 -04:00
ivanjx
3f2ebc4179 Backport pull request #15113 from jellyfin/release-10.11.z
Add season number fallback for OMDB and TMDB plugins

Original-merge: 618ec4543e

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:14 -04:00
Shadowghost
14e8194581 Backport pull request #15112 from jellyfin/release-10.11.z
Skip extracted files in migration if bad timestamp or no access

Original-merge: 7a1c1cd342

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:13 -04:00
theguymadmax
3c4dc16003 Backport pull request #15102 from jellyfin/release-10.11.z
Make season paths case-insensitive

Original-merge: 305b0fdca3

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:12 -04:00
Bond-009
54d28d9842 Backport pull request #15098 from jellyfin/release-10.11.z
Lower required tmp dir size to 512MiB

Original-merge: 0a6e8146be

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:11 -04:00
theguymadmax
adfa520057 Backport pull request #15087 from jellyfin/release-10.11.z
Optimize WhereReferencedItemMultipleTypes filtering

Original-merge: a5bc4524d8

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:10 -04:00
theguymadmax
5deb69b23f Backport pull request #15083 from jellyfin/release-10.11.z
Fix LiveTV images not saving to database

Original-merge: d738386fe2

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:09 -04:00
nyanmisaka
348b2992d7 Backport pull request #15072 from jellyfin/release-10.11.z
Reject stream copy of HDR10+ video if the client does not support HDR10

Original-merge: a725220c21

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:08 -04:00
gnattu
9f8fb6d588 Backport pull request #15055 from jellyfin/release-10.11.z
Log the message more clear when network manager is not ready

Original-merge: a245605152

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:07 -04:00
Shadowghost
cee16d47cb Backport pull request #15054 from jellyfin/release-10.11.z
Speed-up trickplay migration

Original-merge: ca830d5be7

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:06 -04:00
Shadowghost
9e53f46ad2 Backport pull request #15032 from jellyfin/release-10.11.z
Skip invalid keyframe cache data

Original-merge: f4a53209f4

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2025-10-27 15:43:04 -04:00
Joshua M. Boniface
53dfcae1a6 Merge pull request #15236 from joshuaboniface/codeowners
Update CODEOWNERS to capture bump_version
2025-10-27 09:10:47 -04:00
kreaxv
efd659412f Translated using Weblate (Mongolian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/mn/
2025-10-27 09:41:00 +00:00
Joshua M. Boniface
c31ea251c4 Improve handling of .github dir 2025-10-26 22:17:33 -04:00
Joshua M. Boniface
285e7c6c4f Update CODEOWNERS to capture bump_version 2025-10-26 22:07:46 -04:00
Joshua M. Boniface
c274336563 Bump version to 10.12.0 (for real this time) 2025-10-26 21:52:03 -04:00
Joshua M. Boniface
d5fd5dfe6a Fix bump_version to handle spaced filename 2025-10-26 21:51:41 -04:00
Bond-009
6fa69f9fe5 Merge pull request #15206 from jellyfin/renovate/ci-deps
Update danielpalme/ReportGenerator-GitHub-Action action to v5.4.18
2025-10-26 14:50:42 +01:00
Bond-009
0b876365a1 Merge pull request #15205 from jellyfin/renovate/z440.atl.core-7.x
Update dependency z440.atl.core to 7.6.0
2025-10-26 14:39:09 +01:00
Battseren Badral
cdc8325c7b Translated using Weblate (Mongolian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/mn/
2025-10-25 21:55:13 +00:00
renovate[bot]
a6a8e29916 Update danielpalme/ReportGenerator-GitHub-Action action to v5.4.18 2025-10-25 15:32:59 +00:00
renovate[bot]
6fd3847298 Update dependency z440.atl.core to 7.6.0 2025-10-25 15:12:02 +00:00
Bond-009
3ff516a430 Merge pull request #15191 from jellyfin/renovate/ci-deps
Update github/codeql-action action to v4.31.0
2025-10-25 11:23:34 +02:00
Bond-009
d8591840f3 Merge pull request #15192 from jellyfin/renovate/major-github-artifact-actions
Update GitHub Artifact Actions (major)
2025-10-25 11:22:40 +02:00
HanHwanHo
c5affbbf71 Translated using Weblate (Korean)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ko/
2025-10-25 07:46:36 +00:00
renovate[bot]
788f090f27 Update GitHub Artifact Actions 2025-10-24 23:59:16 +00:00
renovate[bot]
0e3b6652b3 Update github/codeql-action action to v4.31.0 2025-10-24 23:59:06 +00:00
desibooklover
d167d59c23 Translated using Weblate (Punjabi)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pa/
2025-10-23 03:50:04 +00:00
desibooklover
f58b4860f7 Translated using Weblate (Hindi)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hi/
2025-10-23 03:50:04 +00:00
Grant Alexander
96b7fc0ac0 Translated using Weblate (Spanish (Mexico))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/es_MX/
2025-10-21 15:20:51 +00:00
oddife
c8ad861590 Translated using Weblate (Marathi)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/mr/
2025-10-20 21:27:58 +00:00
Jellyfin Release Bot
1a1a24cfff Bump version to 10.12.0 2025-10-19 20:45:13 -04:00
50 changed files with 445 additions and 371 deletions

15
.github/CODEOWNERS vendored
View File

@@ -1,4 +1,11 @@
# Joshua must review all changes to deployment and build.sh
.ci/* @joshuaboniface
deployment/* @joshuaboniface
build.sh @joshuaboniface
# Joshua must review all changes to bump_version and any files it touches
bump_version @joshuaboniface
.github/ISSUE_TEMPLATE @joshuaboniface
MediaBrowser.Common/MediaBrowser.Common.csproj @joshuaboniface
Jellyfin.Data/Jellyfin.Data.csproj @joshuaboniface
MediaBrowser.Controller/MediaBrowser.Controller.csproj @joshuaboniface
MediaBrowser.Model/MediaBrowser.Model.csproj @joshuaboniface
Emby.Naming/Emby.Naming.csproj @joshuaboniface
src/Jellyfin.Extensions/Jellyfin.Extensions.csproj @joshuaboniface
# Core must approve all changes within the repo config
.github/ @jellyfin/core

View File

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

View File

@@ -26,7 +26,7 @@ jobs:
dotnet build Jellyfin.Server -o ./out
- name: Upload Head
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: abi-head
retention-days: 14
@@ -65,7 +65,7 @@ jobs:
dotnet build Jellyfin.Server -o ./out
- name: Upload Head
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: abi-base
retention-days: 14
@@ -85,13 +85,13 @@ jobs:
steps:
- name: Download abi-head
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: abi-head
path: abi-head
- name: Download abi-base
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: abi-base
path: abi-base

View File

@@ -27,7 +27,7 @@ jobs:
- 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: openapi-head
retention-days: 14
@@ -61,7 +61,7 @@ jobs:
- 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: openapi-base
retention-days: 14
@@ -80,12 +80,12 @@ jobs:
- openapi-base
steps:
- name: Download openapi-head
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: openapi-head
path: openapi-head
- name: Download openapi-base
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: openapi-base
path: openapi-base
@@ -158,7 +158,7 @@ jobs:
run: |-
echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV
- name: Download openapi-head
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: openapi-head
path: openapi-head
@@ -220,7 +220,7 @@ jobs:
run: |-
echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- name: Download openapi-head
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: openapi-head
path: openapi-head

View File

@@ -35,7 +35,7 @@ jobs:
--verbosity minimal
- name: Merge code coverage results
uses: danielpalme/ReportGenerator-GitHub-Action@9870ed167742d546b99962ff815fcc1098355ed8 # v5.4.17
uses: danielpalme/ReportGenerator-GitHub-Action@f3c6b3f8a29686284ef7a7cf6dccb79a01d98444 # v5.4.18
with:
reports: "**/coverage.cobertura.xml"
targetdir: "merged/"

View File

@@ -88,7 +88,7 @@
<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" />

View File

@@ -36,7 +36,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Naming</PackageId>
<VersionPrefix>10.11.0</VersionPrefix>
<VersionPrefix>10.12.0</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,48 +252,41 @@ 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);
}
}
catch (FileNotFoundException ex)
else
{
// 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)
}
else
{
// 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;
result.Length = fileInfo.Length;
}
}
}
result.CreationTimeUtc = GetCreationTimeUtc(info);
result.LastWriteTimeUtc = GetLastWriteTimeUtc(info);
}
else
{
result.IsDirectory = info is DirectoryInfo;

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

@@ -30,7 +30,7 @@
"ItemAddedWithName": "{0} fue agregado a la biblioteca",
"ItemRemovedWithName": "{0} fue removido de la biblioteca",
"LabelIpAddressValue": "Dirección IP: {0}",
"LabelRunningTimeValue": "Tiempo de reproducción: {0}",
"LabelRunningTimeValue": "Tiempo corriendo: {0}",
"Latest": "Recientes",
"MessageApplicationUpdated": "El servidor Jellyfin ha sido actualizado",
"MessageApplicationUpdatedTo": "El servidor Jellyfin ha sido actualizado a {0}",

View File

@@ -11,7 +11,7 @@
"Collections": "Sammlungen",
"DeviceOfflineWithName": "{0} wurde getrennt",
"DeviceOnlineWithName": "{0} ist verbunden",
"FailedLoginAttemptWithUserName": "Fehlgeschlagener Anmeldeversuch von {0}",
"FailedLoginAttemptWithUserName": "Fählgschlagene Ameldeversuech vo {0}",
"Favorites": "Favorite",
"Folders": "Ordner",
"Genres": "Genre",

View File

@@ -129,5 +129,12 @@
"TaskAudioNormalization": "श्रव्य सामान्यीकरण",
"TaskAudioNormalizationDescription": "श्रव्य सामान्यीकरण के लिए फाइलें अन्वेषण करें",
"TaskDownloadMissingLyrics": "लापता गानों के बोल डाउनलोड करेँ",
"TaskDownloadMissingLyricsDescription": "गानों के बोल डाउनलोड करता है"
"TaskDownloadMissingLyricsDescription": "गानों के बोल डाउनलोड करता है",
"TaskExtractMediaSegments": "मीडिया सेगमेंट स्कैन",
"TaskExtractMediaSegmentsDescription": "मीडियासेगमेंट सक्षम प्लगइन्स से मीडिया सेगमेंट निकालता है या प्राप्त करता है।",
"TaskMoveTrickplayImages": "ट्रिकप्ले छवि स्थान माइग्रेट करें",
"TaskMoveTrickplayImagesDescription": "लाइब्रेरी सेटिंग्स के अनुसार मौजूदा ट्रिकप्ले फ़ाइलों को स्थानांतरित करता है।",
"TaskCleanCollectionsAndPlaylistsDescription": "संग्रहों और प्लेलिस्टों से उन आइटमों को हटाता है जो अब मौजूद नहीं हैं।",
"TaskCleanCollectionsAndPlaylists": "संग्रह और प्लेलिस्ट साफ़ करें",
"CleanupUserDataTask": "यूज़र डेटा की सफाई करता है।"
}

View File

@@ -136,5 +136,7 @@
"TaskMoveTrickplayImages": "트릭플레이 이미지 위치 마이그레이션",
"TaskMoveTrickplayImagesDescription": "추출된 트릭플레이 이미지를 라이브러리 설정에 따라 이동합니다.",
"TaskDownloadMissingLyrics": "누락된 가사 다운로드",
"TaskDownloadMissingLyricsDescription": "가사 다운로드"
"TaskDownloadMissingLyricsDescription": "가사 다운로드",
"CleanupUserDataTask": "사용자 데이터 정리 작업",
"CleanupUserDataTaskDescription": "최소 90일 이상 존재하지 않는 미디어에 대한 사용자 데이터(시청 상태, 즐겨찾기 등)를 정리합니다."
}

View File

@@ -3,7 +3,7 @@
"HeaderNextUp": "Дараа нь",
"HeaderContinueWatching": "Үргэлжлүүлэн үзэх",
"Songs": "Дуунууд",
"Playlists": "Playlist-ууд",
"Playlists": "Тоглуулах жагсаалтууд",
"Movies": "Кинонууд",
"Latest": "Сүүлийн үеийн",
"Genres": "Төрлүүд",
@@ -71,7 +71,7 @@
"Forced": "Хүчээр",
"HeaderAlbumArtists": "Цомгийн уран бүтээлчид",
"HeaderFavoriteAlbums": "Дуртай цомгууд",
"HeaderLiveTV": "Шууд",
"HeaderLiveTV": "Шууд ТВ",
"HeaderRecordingGroups": "Бичлэгийн бүлгүүд",
"HearingImpaired": "Сонсголын бэрхшээлтэй",
"HomeVideos": "Үндсэн дүрсүүд",
@@ -109,7 +109,7 @@
"ScheduledTaskStartedWithName": "{0}-г эхлүүлэв",
"ServerNameNeedsToBeRestarted": "{0}-г дахин асаана уу",
"Shows": "Шоу",
"Sync": "Дахин",
"Sync": "Синхрончлох",
"System": "Систем",
"TvShows": "ТВ нэвтрүүлгүүд",
"Undefined": "Танисангүй",

View File

@@ -132,5 +132,10 @@
"TaskDownloadMissingLyrics": "उपलब्ध नसलेली गीतपट्टी (Lyrics) डाउनलोड करा",
"TaskAudioNormalization": "ऑडिओ सामान्यीकरण",
"TaskAudioNormalizationDescription": "ऑडिओ सामान्यीकरणाचा डाटा स्कॅन करतो.",
"TaskDownloadMissingLyricsDescription": "गाण्यांची गीतपट्टी (Lyrics) डाउनलोड करतो"
"TaskDownloadMissingLyricsDescription": "गाण्यांची गीतपट्टी (Lyrics) डाउनलोड करतो",
"TaskExtractMediaSegmentsDescription": "सक्रिय असलेल्या प्लगिनमधून मीडिया विभाग प्राप्त करते.",
"TaskMoveTrickplayImagesDescription": "लायब्ररीच्या सेटिंग्जप्रमाणे आधीपासून अस्तित्वात असलेल्या ट्रिकप्ले फाइल्सचे स्थान बदलते.",
"TaskCleanCollectionsAndPlaylistsDescription": "जे संग्रह आणि प्लेलिस्ट आता अस्तित्वात नाहीत, त्यांमधील घटक हटवते.",
"CleanupUserDataTask": "वापरकर्ता डेटाची स्वच्छता प्रक्रिया",
"CleanupUserDataTaskDescription": "९० दिवसांहून अधिक काळ अनुपस्थित असलेल्या माध्यमांवरील सर्व वापरकर्ता माहिती (जसे पाहण्याची स्थिती, आवडी इ.) हटवते."
}

View File

@@ -134,6 +134,8 @@
"TaskCleanCollectionsAndPlaylistsDescription": "ਕਲੈਕਸ਼ਨਾਂ ਅਤੇ ਪਲੇਲਿਸਟਾਂ ਵਿੱਚੋਂ ਉਹ ਆਈਟਮ ਹਟਾਉਂਦਾ ਹੈ ਜੋ ਹੁਣ ਮੌਜੂਦ ਨਹੀਂ ਹਨ।",
"TaskCleanCollectionsAndPlaylists": "ਕਲੈਕਸ਼ਨਾਂ ਅਤੇ ਪਲੇਲਿਸਟਾਂ ਨੂੰ ਸਾਫ ਕਰੋ",
"TaskAudioNormalization": "ਆਵਾਜ਼ ਸਧਾਰਣੀਕਰਨ",
"TaskRefreshTrickplayImagesDescription": "ਚਲ ਰਹੀ ਲਾਇਬ੍ਰੇਰੀਆਂ ਵਿੱਚ ਵੀਡੀਓਜ਼ ਲਈ ਟ੍ਰਿਕਪਲੇ ਪ੍ਰੀਵਿਊ ਬਣਾਉਂਦਾ ਹੈ।",
"TaskKeyframeExtractorDescription": "ਕੀ-ਫ੍ਰੇਮਜ਼ ਨੂੰ ਵੀਡੀਓ ਫਾਈਲਾਂ ਵਿੱਚੋਂ ਨਿਕਾਲਦਾ ਹੈ ਤਾਂ ਜੋ ਹੋਰ ਜ਼ਿਆਦਾ ਸਟਿਕ ਹੋਣ ਵਾਲੀਆਂ HLS ਪਲੇਲਿਸਟਾਂ ਬਣਾਈਆਂ ਜਾ ਸਕਣ। ਇਹ ਕੰਮ ਲੰਬੇ ਸਮੇਂ ਤੱਕ ਚੱਲ ਸਕਦਾ ਹੈ।"
"TaskRefreshTrickplayImagesDescription": "ਵੀਡੀਓ ਲਈ ਟ੍ਰਿਕਪਲੇ ਪ੍ਰੀਵਿਊ ਬਣਾਉਂਦਾ ਹੈ (ਜੇ ਲਾਇਬ੍ਰੇਰੀ ਵਿੱਚ ਚੁਣਿਆ ਗਿਆ ਹੈ)।",
"TaskKeyframeExtractorDescription": "ਕੀ-ਫ੍ਰੇਮਜ਼ ਨੂੰ ਵੀਡੀਓ ਫਾਈਲਾਂ ਵਿੱਚੋਂ ਨਿਕਾਲਦਾ ਹੈ ਤਾਂ ਜੋ ਹੋਰ ਜ਼ਿਆਦਾ ਸਟਿਕ ਹੋਣ ਵਾਲੀਆਂ HLS ਪਲੇਲਿਸਟਾਂ ਬਣਾਈਆਂ ਜਾ ਸਕਣ। ਇਹ ਕੰਮ ਲੰਬੇ ਸਮੇਂ ਤੱਕ ਚੱਲ ਸਕਦਾ ਹੈ।",
"CleanupUserDataTaskDescription": "ਘੱਟੋ-ਘੱਟ 90 ਦਿਨਾਂ ਤੋਂ ਮੌਜੂਦ ਨਾ ਹੋਣ ਵਾਲੇ ਮੀਡੀਆ ਤੋਂ ਸਾਰੇ ਉਪਭੋਗਤਾ ਡੇਟਾ (ਵਾਚ ਸਟੇਟ, ਮਨਪਸੰਦ ਸਟੇਟਸ ਆਦਿ) ਨੂੰ ਸਾਫ਼ ਕਰਦਾ ਹੈ।",
"CleanupUserDataTask": "ਯੂਜ਼ਰ ਡਾਟਾ ਸਾਫ਼ ਕਰਨ ਦਾ ਕੰਮ"
}

View File

@@ -137,5 +137,6 @@
"TaskCleanCollectionsAndPlaylistsDescription": "從資料庫及播放清單中移除已不存在的項目。",
"TaskMoveTrickplayImagesDescription": "根據媒體庫設定移動現有的 Trickplay 檔案。",
"TaskMoveTrickplayImages": "轉移 Trickplay 影像位置",
"CleanupUserDataTask": "用戶資料清理工作"
"CleanupUserDataTask": "用戶資料清理工作",
"CleanupUserDataTaskDescription": "從用戶數據中清除已經被刪除超過 90 日的媒體相關資料。"
}

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.12.0</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,16 +284,20 @@ 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);
try
{
_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))
{
_logger.LogInformation("Start backup process.");
_logger.LogInformation("Starting backup process");
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
static IAsyncEnumerable<object> GetValues(IQueryable dbSet)
{
var method = dbSet.GetType().GetMethod(nameof(DbSet<object>.AsAsyncEnumerable))!;
@@ -302,7 +309,8 @@ public class BackupService : IBackupService
var historyRepository = dbContext.GetService<IHistoryRepository>();
var migrations = await historyRepository.GetAppliedMigrationsAsync().ConfigureAwait(false);
ICollection<(Type Type, string SourceName, Func<IAsyncEnumerable<object>> ValueFactory)> entityTypes = [
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)))
@@ -335,7 +343,8 @@ public class BackupService : IBackupService
entities++;
try
{
JsonSerializer.SerializeToDocument(item, _serializerSettings).WriteTo(jsonSerializer);
using var document = JsonSerializer.SerializeToDocument(item, _serializerSettings);
document.WriteTo(jsonSerializer);
}
catch (Exception ex)
{
@@ -348,7 +357,7 @@ public class BackupService : IBackupService
}
}
_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);
}
}
}
@@ -406,6 +415,24 @@ public class BackupService : IBackupService
_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");
}
throw;
}
}
/// <inheritdoc/>
public async Task<BackupManifestDto?> GetBackupManifest(string archivePath)
@@ -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,15 +145,22 @@ 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))
{
try
{
var bytes = File.ReadAllBytes(cachePath);
cachedResult = JsonSerializer.Deserialize<MediaEncoding.Keyframes.KeyframeData>(bytes, _jsonOptions);
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.12.0</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.12.0</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();
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.12.0</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.12.0")]
[assembly: AssemblyFileVersion("10.12.0")]

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.12.0</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();
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);