mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-01-15 23:58:57 +00:00
Compare commits
120 Commits
v10.11.0-r
...
v10.11.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4187c6f620 | ||
|
|
e7dbb3afec | ||
|
|
f994dd6211 | ||
|
|
da254ee968 | ||
|
|
4ad3141875 | ||
|
|
b5f0199a25 | ||
|
|
6bf88c049e | ||
|
|
40a33da2a5 | ||
|
|
3596fc0693 | ||
|
|
93824dad97 | ||
|
|
e5656af1f2 | ||
|
|
c127c10458 | ||
|
|
7d1824ea27 | ||
|
|
2966d27c97 | ||
|
|
618ec4543e | ||
|
|
0e4031ae52 | ||
|
|
442af96ed9 | ||
|
|
a305204cfa | ||
|
|
75f472e6a7 | ||
|
|
cc32e8f7cb | ||
|
|
14b3085ff1 | ||
|
|
5691eee4f1 | ||
|
|
1520a697ad | ||
|
|
81b8b0ca4a | ||
|
|
ac3fa3c376 | ||
|
|
7a1c1cd342 | ||
|
|
70c32a26fa | ||
|
|
2b94bb54aa | ||
|
|
0a6e8146be | ||
|
|
305b0fdca3 | ||
|
|
d738386fe2 | ||
|
|
ca830d5be7 | ||
|
|
a5bc4524d8 | ||
|
|
175ee12bbc | ||
|
|
a725220c21 | ||
|
|
a245605152 | ||
|
|
f4a53209f4 | ||
|
|
877251bcae | ||
|
|
ace30afcf8 | ||
|
|
fc056b6273 | ||
|
|
ac5efb4775 | ||
|
|
fefd676adc | ||
|
|
59c17a663c | ||
|
|
641551e164 | ||
|
|
bd543d7ac3 | ||
|
|
545e412259 | ||
|
|
7dff92bb82 | ||
|
|
b36aab9399 | ||
|
|
2c7d2d4719 | ||
|
|
5c519270b8 | ||
|
|
55047b1183 | ||
|
|
794e1361d7 | ||
|
|
27c9c9c0ed | ||
|
|
68636b2390 | ||
|
|
2e6430c4f4 | ||
|
|
c88d792963 | ||
|
|
73dbc9e89f | ||
|
|
cf3edd9875 | ||
|
|
ef0131ad69 | ||
|
|
056c318f04 | ||
|
|
49c3443b0c | ||
|
|
e415718fe7 | ||
|
|
8abcfb2a80 | ||
|
|
9aadf97958 | ||
|
|
9e57121171 | ||
|
|
b471811920 | ||
|
|
3cb99add76 | ||
|
|
001f1c4377 | ||
|
|
9ef3706b44 | ||
|
|
864d6d0b8f | ||
|
|
a565e4896e | ||
|
|
ceef9143ad | ||
|
|
a7a92509c7 | ||
|
|
e876e784da | ||
|
|
9b7d5edc86 | ||
|
|
f01cddf273 | ||
|
|
0d4bd0495b | ||
|
|
6f9c4dea6e | ||
|
|
8c51920911 | ||
|
|
8f2fd65810 | ||
|
|
953659980f | ||
|
|
8ab1fecb70 | ||
|
|
f5d42ee180 | ||
|
|
e28d547006 | ||
|
|
b3b9f74014 | ||
|
|
07d31c6ba5 | ||
|
|
a9198e865e | ||
|
|
79ff0b0b00 | ||
|
|
2b45a984dd | ||
|
|
739642b330 | ||
|
|
6097045d71 | ||
|
|
51e20a14c2 | ||
|
|
eb0d05cf1e | ||
|
|
d3d5915f31 | ||
|
|
288640a5d0 | ||
|
|
ff0a1b999f | ||
|
|
da0fe7455e | ||
|
|
bf69f9d8a8 | ||
|
|
badf22fcc2 | ||
|
|
b59e9f90f0 | ||
|
|
056b92dbd5 | ||
|
|
ba80f5e416 | ||
|
|
97ec4c1da2 | ||
|
|
894ba1a410 | ||
|
|
0a0aaefad5 | ||
|
|
c8b97bf533 | ||
|
|
cfa4e357ea | ||
|
|
0f42aa892e | ||
|
|
cce6bf27e0 | ||
|
|
d6cebf1e67 | ||
|
|
c053a6cd78 | ||
|
|
d8c62420bf | ||
|
|
d483c3efe6 | ||
|
|
275c1a3cc1 | ||
|
|
4942b2c15f | ||
|
|
3fc71293b4 | ||
|
|
8ea9bece03 | ||
|
|
baa7f5f0b0 | ||
|
|
b9c96f3d2c | ||
|
|
08f9b932ac |
@@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "9.0.9",
|
||||
"version": "9.0.10",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
|
||||
6
.github/workflows/ci-codeql-analysis.yml
vendored
6
.github/workflows/ci-codeql-analysis.yml
vendored
@@ -27,11 +27,11 @@ jobs:
|
||||
dotnet-version: '9.0.x'
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
|
||||
uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: +security-extended
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
|
||||
uses: github/codeql-action/autobuild@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
|
||||
uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
|
||||
|
||||
8
.github/workflows/ci-compat.yml
vendored
8
.github/workflows/ci-compat.yml
vendored
@@ -105,7 +105,7 @@ jobs:
|
||||
run: |
|
||||
{
|
||||
echo 'body<<EOF'
|
||||
for file in Jellyfin.Data.dll MediaBrowser.Common.dll MediaBrowser.Controller.dll MediaBrowser.Model.dll Emby.Naming.dll Jellyfin.Extensions.dll Jellyfin.MediaEncoding.Keyframes.dll Jellyfin.Database.Implementations.dll Jellyfin.CodeAnalysis.dll; do
|
||||
for file in Jellyfin.Data.dll MediaBrowser.Common.dll MediaBrowser.Controller.dll MediaBrowser.Model.dll Emby.Naming.dll Jellyfin.Extensions.dll Jellyfin.MediaEncoding.Keyframes.dll Jellyfin.Database.Implementations.dll; do
|
||||
COMPAT_OUTPUT="$( { apicompat --left ./abi-base/${file} --right ./abi-head/${file}; } 2>&1 )"
|
||||
if [ "APICompat ran successfully without finding any breaking changes." != "${COMPAT_OUTPUT}" ]; then
|
||||
printf "\n${file}\n${COMPAT_OUTPUT}\n"
|
||||
@@ -115,7 +115,7 @@ jobs:
|
||||
} >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Find difference comment
|
||||
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
|
||||
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
|
||||
id: find-comment
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
@@ -123,7 +123,7 @@ jobs:
|
||||
body-includes: abi-diff-workflow-comment
|
||||
|
||||
- name: Reply or edit difference comment (changed)
|
||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
|
||||
if: ${{ steps.diff.outputs.body != '' }}
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
@@ -142,7 +142,7 @@ jobs:
|
||||
</details>
|
||||
|
||||
- name: Reply or edit difference comment (unchanged)
|
||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
|
||||
if: ${{ steps.diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
|
||||
6
.github/workflows/ci-openapi.yml
vendored
6
.github/workflows/ci-openapi.yml
vendored
@@ -120,14 +120,14 @@ jobs:
|
||||
echo "" >> openapi-changes-reply.md
|
||||
echo "</details>" >> openapi-changes-reply.md
|
||||
- name: Find difference comment
|
||||
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
|
||||
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
|
||||
id: find-comment
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
direction: last
|
||||
body-includes: openapi-diff-workflow-comment
|
||||
- name: Reply or edit difference comment (changed)
|
||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
|
||||
if: ${{ steps.read-diff.outputs.ApiChanged == '1' }}
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
@@ -135,7 +135,7 @@ jobs:
|
||||
edit-mode: replace
|
||||
body-path: openapi-changes-reply.md
|
||||
- name: Edit difference comment (unchanged)
|
||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
|
||||
if: ${{ steps.read-diff.outputs.ApiChanged == '0' && steps.find-comment.outputs.comment-id != '' }}
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
|
||||
2
.github/workflows/ci-tests.yml
vendored
2
.github/workflows/ci-tests.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
--verbosity minimal
|
||||
|
||||
- name: Merge code coverage results
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@1978db745da4a573ca4baa2d0f67175df51a148c # v5.4.16
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@9870ed167742d546b99962ff815fcc1098355ed8 # v5.4.17
|
||||
with:
|
||||
reports: "**/coverage.cobertura.xml"
|
||||
targetdir: "merged/"
|
||||
|
||||
4
.github/workflows/commands.yml
vendored
4
.github/workflows/commands.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Notify as seen
|
||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
comment-id: ${{ github.event.comment.id }}
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
- name: install python
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
with:
|
||||
python-version: '3.13'
|
||||
python-version: '3.14'
|
||||
cache: 'pip'
|
||||
- name: install python packages
|
||||
run: pip install -r rename/requirements.txt
|
||||
|
||||
2
.github/workflows/issue-stale.yml
vendored
2
.github/workflows/issue-stale.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ contains(github.repository, 'jellyfin/') }}
|
||||
steps:
|
||||
- uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0
|
||||
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||
with:
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
ascending: true
|
||||
|
||||
2
.github/workflows/issue-template-check.yml
vendored
2
.github/workflows/issue-template-check.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: install python
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
with:
|
||||
python-version: '3.13'
|
||||
python-version: '3.14'
|
||||
cache: 'pip'
|
||||
- name: install python packages
|
||||
run: pip install -r main-repo-triage/requirements.txt
|
||||
|
||||
2
.github/workflows/pull-request-stale.yaml
vendored
2
.github/workflows/pull-request-stale.yaml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ contains(github.repository, 'jellyfin/') }}
|
||||
steps:
|
||||
- uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0
|
||||
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||
with:
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
ascending: true
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Custom Analyzers -->
|
||||
<ItemGroup Condition=" '$(MSBuildProjectName)' != 'Jellyfin.CodeAnalysis' ">
|
||||
<ItemGroup Condition=" '$(MSBuildProjectName)' != 'Jellyfin.CodeAnalysis' AND '$(Configuration)' == 'Debug' ">
|
||||
<ProjectReference Include="$(MSBuildThisFileDirectory)src/Jellyfin.CodeAnalysis/Jellyfin.CodeAnalysis.csproj" OutputItemType="Analyzer" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
</PropertyGroup>
|
||||
<!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
|
||||
<ItemGroup Label="Package Dependencies">
|
||||
<PackageVersion Include="AsyncKeyedLock" Version="7.1.6" />
|
||||
<PackageVersion Include="AsyncKeyedLock" Version="7.1.7" />
|
||||
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
|
||||
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
|
||||
<PackageVersion Include="AutoFixture" Version="4.18.1" />
|
||||
@@ -26,33 +26,33 @@
|
||||
<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.NET.Test.Sdk" Version="17.14.1" />
|
||||
<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" />
|
||||
<PackageVersion Include="Moq" Version="4.18.4" />
|
||||
@@ -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>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Naming</PackageId>
|
||||
<VersionPrefix>10.11.0</VersionPrefix>
|
||||
<VersionPrefix>10.11.2</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -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..];
|
||||
|
||||
@@ -113,8 +113,10 @@ namespace Emby.Naming.TV
|
||||
var numberString = match.Groups["seasonnumber"];
|
||||
if (numberString.Success)
|
||||
{
|
||||
var seasonNumber = int.Parse(numberString.Value, CultureInfo.InvariantCulture);
|
||||
return (seasonNumber, true);
|
||||
if (int.TryParse(numberString.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var seasonNumber))
|
||||
{
|
||||
return (seasonNumber, true);
|
||||
}
|
||||
}
|
||||
|
||||
return (null, false);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
@@ -224,7 +223,7 @@ public class ChapterManager : IChapterManager
|
||||
|
||||
if (saveChapters && changesMade)
|
||||
{
|
||||
_chapterRepository.SaveChapters(video.Id, chapters);
|
||||
SaveChapters(video, chapters);
|
||||
}
|
||||
|
||||
DeleteDeadImages(currentImages, chapters);
|
||||
@@ -235,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 />
|
||||
@@ -251,23 +252,9 @@ public class ChapterManager : IChapterManager
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void DeleteChapterImages(Video video)
|
||||
public async Task DeleteChapterDataAsync(Guid itemId, CancellationToken cancellationToken)
|
||||
{
|
||||
var path = _pathManager.GetChapterImageFolderPath(video);
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(path))
|
||||
{
|
||||
_logger.LogInformation("Removing chapter images for {Name} [{Id}]", video.Name, video.Id);
|
||||
Directory.Delete(path, true);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning("Failed to remove chapter image folder for {Item}: {Exception}", video.Id, ex);
|
||||
}
|
||||
|
||||
_chapterRepository.DeleteChapters(video.Id);
|
||||
await _chapterRepository.DeleteChaptersAsync(itemId, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private IReadOnlyList<string> GetSavedChapterImages(Video video, IDirectoryService directoryService)
|
||||
|
||||
@@ -104,6 +104,8 @@ namespace Emby.Server.Implementations.Collections
|
||||
|
||||
await _libraryManager.AddVirtualFolder(name, CollectionTypeOptions.boxsets, libraryOptions, true).ConfigureAwait(false);
|
||||
|
||||
_libraryManager.RootFolder.Children = null;
|
||||
|
||||
return FindFolders(path).First();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Chapters;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.MediaSegments;
|
||||
@@ -20,6 +21,7 @@ public class ExternalDataManager : IExternalDataManager
|
||||
private readonly IMediaSegmentManager _mediaSegmentManager;
|
||||
private readonly IPathManager _pathManager;
|
||||
private readonly ITrickplayManager _trickplayManager;
|
||||
private readonly IChapterManager _chapterManager;
|
||||
private readonly ILogger<ExternalDataManager> _logger;
|
||||
|
||||
/// <summary>
|
||||
@@ -29,18 +31,21 @@ public class ExternalDataManager : IExternalDataManager
|
||||
/// <param name="mediaSegmentManager">The media segment manager.</param>
|
||||
/// <param name="pathManager">The path manager.</param>
|
||||
/// <param name="trickplayManager">The trickplay manager.</param>
|
||||
/// <param name="chapterManager">The chapter manager.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public ExternalDataManager(
|
||||
IKeyframeManager keyframeManager,
|
||||
IMediaSegmentManager mediaSegmentManager,
|
||||
IPathManager pathManager,
|
||||
ITrickplayManager trickplayManager,
|
||||
IChapterManager chapterManager,
|
||||
ILogger<ExternalDataManager> logger)
|
||||
{
|
||||
_keyframeManager = keyframeManager;
|
||||
_mediaSegmentManager = mediaSegmentManager;
|
||||
_pathManager = pathManager;
|
||||
_trickplayManager = trickplayManager;
|
||||
_chapterManager = chapterManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -67,5 +72,6 @@ public class ExternalDataManager : IExternalDataManager
|
||||
await _keyframeManager.DeleteKeyframeDataAsync(itemId, cancellationToken).ConfigureAwait(false);
|
||||
await _mediaSegmentManager.DeleteSegmentsAsync(itemId, cancellationToken).ConfigureAwait(false);
|
||||
await _trickplayManager.DeleteTrickplayDataAsync(itemId, cancellationToken).ConfigureAwait(false);
|
||||
await _chapterManager.DeleteChapterDataAsync(itemId, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -528,7 +528,7 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
Genre => _configurationManager.ApplicationPaths.GenrePath,
|
||||
MusicArtist => _configurationManager.ApplicationPaths.ArtistsPath,
|
||||
MusicGenre => _configurationManager.ApplicationPaths.GenrePath,
|
||||
MusicGenre => _configurationManager.ApplicationPaths.MusicGenrePath,
|
||||
Person => _configurationManager.ApplicationPaths.PeoplePath,
|
||||
Studio => _configurationManager.ApplicationPaths.StudioPath,
|
||||
Year => _configurationManager.ApplicationPaths.YearPath,
|
||||
@@ -2129,6 +2129,8 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
item.ValidateImages();
|
||||
|
||||
_itemRepository.SaveImages(item);
|
||||
|
||||
RegisterItem(item);
|
||||
@@ -3051,10 +3053,10 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
finally
|
||||
{
|
||||
await ValidateTopLibraryFolders(CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
if (refreshLibrary)
|
||||
{
|
||||
await ValidateTopLibraryFolders(CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
StartScanInBackground();
|
||||
}
|
||||
else
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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",
|
||||
@@ -41,10 +41,10 @@
|
||||
"StartupEmbyServerIsLoading": "Jellyfin server laadib. Proovi varsti uuesti.",
|
||||
"User": "Kasutaja",
|
||||
"Undefined": "Määratlemata",
|
||||
"TvShows": "Seriaalid",
|
||||
"TvShows": "Sarjad",
|
||||
"System": "Süsteem",
|
||||
"Sync": "Sünkrooni",
|
||||
"Songs": "Laulud",
|
||||
"Songs": "Lood",
|
||||
"Shows": "Sarjad",
|
||||
"ServerNameNeedsToBeRestarted": "{0} tuleb taaskäivitada",
|
||||
"ScheduledTaskFailedWithName": "{0} nurjus",
|
||||
@@ -92,7 +92,7 @@
|
||||
"HeaderNextUp": "Järgmisena",
|
||||
"HeaderLiveTV": "Otse TV",
|
||||
"HeaderFavoriteSongs": "Lemmiklood",
|
||||
"HeaderFavoriteShows": "Lemmikseriaalid",
|
||||
"HeaderFavoriteShows": "Lemmiksarjad",
|
||||
"HeaderFavoriteEpisodes": "Lemmikepisoodid",
|
||||
"HeaderFavoriteArtists": "Lemmikesitajad",
|
||||
"HeaderFavoriteAlbums": "Lemmikalbumid",
|
||||
@@ -122,18 +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õtmekaadri ekstraktor",
|
||||
"TaskRefreshTrickplayImages": "Loo eelvaate pildid",
|
||||
"TaskRefreshTrickplayImagesDescription": "Loob eelvaated videotele, kus lubatud.",
|
||||
"TaskAudioNormalization": "Heli Normaliseerimine",
|
||||
"TaskAudioNormalizationDescription": "Skaneerib faile heli normaliseerimise andmete jaoks.",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Eemaldab kogumikest ja esitusloenditest asjad, mida enam ei eksisteeri.",
|
||||
"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 helitugevus",
|
||||
"TaskAudioNormalizationDescription": "Otsib failidest helitugevuse normaliseerimise teavet.",
|
||||
"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": "Migreeri trickplay piltide asukoht"
|
||||
"TaskMoveTrickplayImages": "Muuda trickplay piltide asukoht",
|
||||
"CleanupUserDataTask": "Puhasta kasutajaandmed",
|
||||
"CleanupUserDataTaskDescription": "Puhastab kõik kasutajaandmed (vaatamise olek, lemmikute olek jne) meediast, mis pole enam vähemalt 90 päeva saadaval olnud."
|
||||
}
|
||||
|
||||
@@ -1,74 +1,74 @@
|
||||
{
|
||||
"Albums": "Álbumes",
|
||||
"Albums": "Álbums",
|
||||
"Collections": "Coleccións",
|
||||
"ChapterNameValue": "Capítulo {0}",
|
||||
"Channels": "Canles",
|
||||
"CameraImageUploadedFrom": "Cargouse unha nova imaxe da cámara desde {0}",
|
||||
"CameraImageUploadedFrom": "Cargouse unha nova imaxe de cámara dende {0}",
|
||||
"Books": "Libros",
|
||||
"AuthenticationSucceededWithUserName": "{0} autenticouse correctamente",
|
||||
"Artists": "Artistas",
|
||||
"Application": "Aplicativo",
|
||||
"NotificationOptionServerRestartRequired": "Necesario un reinicio do servidor",
|
||||
"NotificationOptionPluginUpdateInstalled": "Actualización do Plugin instalada",
|
||||
"Application": "Aplicación",
|
||||
"NotificationOptionServerRestartRequired": "Necesario o reinicio do servidor",
|
||||
"NotificationOptionPluginUpdateInstalled": "Actualización do plugin instalada",
|
||||
"NotificationOptionPluginUninstalled": "Plugin desinstalado",
|
||||
"NotificationOptionPluginInstalled": "Plugin instalado",
|
||||
"NotificationOptionPluginError": "Fallo do Plugin",
|
||||
"NotificationOptionPluginError": "Fallo do plugin",
|
||||
"NotificationOptionNewLibraryContent": "Novo contido engadido",
|
||||
"NotificationOptionInstallationFailed": "Fallo na instalación",
|
||||
"NotificationOptionCameraImageUploaded": "Imaxe da cámara subida",
|
||||
"NotificationOptionAudioPlaybackStopped": "Reproducción de audio parada",
|
||||
"NotificationOptionCameraImageUploaded": "Imaxe da cámara cargada",
|
||||
"NotificationOptionAudioPlaybackStopped": "Reproducción de audio detida",
|
||||
"NotificationOptionAudioPlayback": "Reproducción de audio comezada",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Actualización da aplicación instalada",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Actualización da aplicación dispoñible",
|
||||
"NewVersionIsAvailable": "Unha nova versión do Servidor Jellyfin está dispoñible para descarga.",
|
||||
"NewVersionIsAvailable": "Nova versión do Servidor Jellyfin dispoñible para descargar.",
|
||||
"NameSeasonUnknown": "Tempada descoñecida",
|
||||
"NameSeasonNumber": "Tempada {0}",
|
||||
"NameInstallFailed": "{0} instalación fallida",
|
||||
"MusicVideos": "Vídeos Musicais",
|
||||
"MusicVideos": "Vídeos musicais",
|
||||
"Music": "Música",
|
||||
"Movies": "Películas",
|
||||
"MixedContent": "Contido Mixto",
|
||||
"MessageServerConfigurationUpdated": "A configuración do servidor foi actualizada",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "A sección de configuración {0} do servidor foi actualizada",
|
||||
"MessageApplicationUpdatedTo": "O servidor Jellyfin foi actualizado a {0}",
|
||||
"MessageApplicationUpdated": "O servidor Jellyfin foi actualizado",
|
||||
"MixedContent": "Contido mixto",
|
||||
"MessageServerConfigurationUpdated": "Actualizouse a configuración do servidor",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Actualizouse a sección de configuración {0} do servidor",
|
||||
"MessageApplicationUpdatedTo": "O servidor Jellyfin actualizouse a {0}",
|
||||
"MessageApplicationUpdated": "O servidor Jellyfin actualizouse",
|
||||
"Latest": "Último",
|
||||
"LabelRunningTimeValue": "Tempo de execución: {0}",
|
||||
"LabelRunningTimeValue": "Tempo en execución: {0}",
|
||||
"LabelIpAddressValue": "Enderezo IP: {0}",
|
||||
"ItemRemovedWithName": "{0} foi eliminado da biblioteca",
|
||||
"ItemAddedWithName": "{0} foi engadido a biblioteca",
|
||||
"ItemRemovedWithName": "{0} eliminouse da biblioteca",
|
||||
"ItemAddedWithName": "{0} engadiuse á biblioteca",
|
||||
"Inherit": "Herdar",
|
||||
"HomeVideos": "Videos caseiros",
|
||||
"HeaderRecordingGroups": "Grupos de Grabación",
|
||||
"HeaderRecordingGroups": "Grupos de grabación",
|
||||
"HeaderNextUp": "De seguido",
|
||||
"HeaderLiveTV": "TV en directo",
|
||||
"HeaderFavoriteSongs": "Cancións Favoritas",
|
||||
"HeaderFavoriteShows": "Series de TV Favoritas",
|
||||
"HeaderFavoriteEpisodes": "Episodios Favoritos",
|
||||
"HeaderFavoriteArtists": "Artistas Favoritos",
|
||||
"HeaderFavoriteAlbums": "Álbunes Favoritos",
|
||||
"HeaderFavoriteSongs": "Cancións favoritas",
|
||||
"HeaderFavoriteShows": "Series de TV favoritas",
|
||||
"HeaderFavoriteEpisodes": "Episodios favoritos",
|
||||
"HeaderFavoriteArtists": "Artistas favoritos",
|
||||
"HeaderFavoriteAlbums": "Álbums favoritos",
|
||||
"HeaderContinueWatching": "Seguir vendo",
|
||||
"HeaderAlbumArtists": "Artistas do Album",
|
||||
"HeaderAlbumArtists": "Artistas do álbum",
|
||||
"Genres": "Xéneros",
|
||||
"Forced": "Forzado",
|
||||
"Folders": "Cartafoles",
|
||||
"Favorites": "Favoritos",
|
||||
"FailedLoginAttemptWithUserName": "Intento de incio de sesión fallido {0}",
|
||||
"FailedLoginAttemptWithUserName": "Fallo de intento de inicio de sesión dende {0}",
|
||||
"DeviceOnlineWithName": "{0} conectouse",
|
||||
"DeviceOfflineWithName": "{0} desconectouse",
|
||||
"Default": "Por defecto",
|
||||
"AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}",
|
||||
"TaskCleanLogs": "Limpar Carpeta de Rexistros",
|
||||
"TaskCleanActivityLog": "Limpar Rexistro de Actividade",
|
||||
"TasksChannelsCategory": "Canáis de Internet",
|
||||
"TaskUpdatePlugins": "Actualizar Plugins",
|
||||
"TaskCleanLogs": "Limpar directorio de rexistros",
|
||||
"TaskCleanActivityLog": "Limpar rexistro de actividade",
|
||||
"TasksChannelsCategory": "Canles da Internet",
|
||||
"TaskUpdatePlugins": "Actualizar plugins",
|
||||
"User": "Usuario",
|
||||
"Undefined": "Sen definir",
|
||||
"TvShows": "Programas de TV",
|
||||
"System": "Sistema",
|
||||
"Sync": "Sincronizar",
|
||||
"SubtitleDownloadFailureFromForItem": "Fallou a descarga de subtítulos para {1} dende {0}",
|
||||
"StartupEmbyServerIsLoading": "O Servidor Jellyfin está cargando. Por favor, reinténteo en breve.",
|
||||
"StartupEmbyServerIsLoading": "O servidor Jellyfin está cargando. Por favor, ténteo axiña outra vez.",
|
||||
"Songs": "Cancións",
|
||||
"Shows": "Programas",
|
||||
"ServerNameNeedsToBeRestarted": "{0} precisa ser reiniciado",
|
||||
@@ -85,57 +85,57 @@
|
||||
"UserDeletedWithName": "O usuario {0} foi borrado",
|
||||
"UserCreatedWithName": "O usuario {0} foi creado",
|
||||
"Plugin": "Plugin",
|
||||
"NotificationOptionVideoPlaybackStopped": "Reproducción de vídeo parada",
|
||||
"NotificationOptionVideoPlaybackStopped": "Reproducción de vídeo detida",
|
||||
"NotificationOptionVideoPlayback": "Reproducción de vídeo iniciada",
|
||||
"NotificationOptionUserLockedOut": "Usuario bloqueado",
|
||||
"NotificationOptionTaskFailed": "Falla na tarefa axendada",
|
||||
"TaskCleanTranscodeDescription": "Borra os arquivos de transcode anteriores a un día.",
|
||||
"TaskCleanTranscode": "Limpar Directorio de Transcode",
|
||||
"TaskCleanTranscodeDescription": "Borra os ficheiros de transcodificación de hai más dun día.",
|
||||
"TaskCleanTranscode": "Limpar o directorio de transcodificación",
|
||||
"UserStoppedPlayingItemWithValues": "{0} rematou de reproducir {1} en {2}",
|
||||
"UserStartedPlayingItemWithValues": "{0} está reproducindo {1} en {2}",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Busca en internet por subtítulos que faltan baseado na configuración de metadatos.",
|
||||
"UserStartedPlayingItemWithValues": "{0} está a reproducir {1} en {2}",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Procura na internet os subtítulos que faltan segundo a configuración de metadatos.",
|
||||
"TaskDownloadMissingSubtitles": "Descargar subtítulos que faltan",
|
||||
"TaskRefreshChannelsDescription": "Refresca a información do canle de internet.",
|
||||
"TaskRefreshChannels": "Refrescar Canles",
|
||||
"TaskUpdatePluginsDescription": "Descarga e instala actualizacións para plugins que están configurados para actualizarse automáticamente.",
|
||||
"TaskRefreshPeopleDescription": "Actualiza os metadatos dos actores e directores na túa libraría multimedia.",
|
||||
"TaskRefreshPeople": "Refrescar Persoas",
|
||||
"TaskCleanLogsDescription": "Borra arquivos de rexistro que son mais antigos que {0} días.",
|
||||
"TaskRefreshLibraryDescription": "Escanea a tua libraría multimedia buscando novos arquivos e refrescando os metadatos.",
|
||||
"TaskRefreshLibrary": "Escanear Libraría Multimedia",
|
||||
"TaskRefreshChapterImagesDescription": "Crea previsualizacións para videos que teñen capítulos.",
|
||||
"TaskRefreshChapterImages": "Extraer Imaxes dos Capítulos",
|
||||
"TaskRefreshChannelsDescription": "Refresca a información da canle de internet.",
|
||||
"TaskRefreshChannels": "Refrescar canles",
|
||||
"TaskUpdatePluginsDescription": "Descarga e instala actualizacións dos plugins configurados para actualizarse automáticamente.",
|
||||
"TaskRefreshPeopleDescription": "Actualiza os metadatos dos actores e directores na túa biblioteca de medios.",
|
||||
"TaskRefreshPeople": "Refrescar persoas",
|
||||
"TaskCleanLogsDescription": "Borra ficheiros de rexistro con máis de {0} días de antigüidade.",
|
||||
"TaskRefreshLibraryDescription": "Escanea a túa biblioteca de medios á procura de novos ficheiros e refresca os metadatos.",
|
||||
"TaskRefreshLibrary": "Escanear a biblioteca de medios",
|
||||
"TaskRefreshChapterImagesDescription": "Crea miniaturas dos vídeos que teñen capítulos.",
|
||||
"TaskRefreshChapterImages": "Extraer imaxes dos capítulos",
|
||||
"TaskCleanCacheDescription": "Borra ficheiros da caché que xa non son necesarios para o sistema.",
|
||||
"TaskCleanCache": "Limpa Directorio de Caché",
|
||||
"TaskCleanActivityLogDescription": "Borra as entradas no rexistro de actividade anteriores á data configurada.",
|
||||
"TaskCleanCache": "Limpar directorio de caché",
|
||||
"TaskCleanActivityLogDescription": "Borra do rexistro de actividade as entradas anteriores á data configurada.",
|
||||
"TasksApplicationCategory": "Aplicación",
|
||||
"ValueSpecialEpisodeName": "Especial - {0}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} foi engadido a túa libraría multimedia",
|
||||
"TasksLibraryCategory": "Libraría",
|
||||
"ValueHasBeenAddedToLibrary": "{0} engadiuse á túa biblioteca de medios",
|
||||
"TasksLibraryCategory": "Biblioteca",
|
||||
"TasksMaintenanceCategory": "Mantemento",
|
||||
"VersionNumber": "Versión {0}",
|
||||
"UserPolicyUpdatedWithName": "A política de usuario foi actualizada para {0}",
|
||||
"UserPasswordChangedWithName": "Cambiouse o contrasinal para o usuario {0}",
|
||||
"UserOnlineFromDevice": "{0} está en liña desde {1}",
|
||||
"UserOfflineFromDevice": "{0} desconectouse desde {1}",
|
||||
"TaskOptimizeDatabaseDescription": "Compacta e libera o espazo libre da base de datos. Executar esta tarefa logo de realizar mudanzas que impliquen modificacións da base de datos ou despois de escanear a biblioteca pode traer mellorías de desempeño.",
|
||||
"UserOfflineFromDevice": "{0} desconectouse dende {1}",
|
||||
"TaskOptimizeDatabaseDescription": "Compacta e libera espazo na base de datos. Executar esta tarefa logo de facer cambios que muden a base de datos ou despois de escanear a biblioteca pode mellorar o rendemento.",
|
||||
"TaskOptimizeDatabase": "Optimizar base de datos",
|
||||
"TaskKeyframeExtractorDescription": "Extrae fragmentos do vídeo para crear listas de reprodución HLS máis precisas. Podería levarlle bastante tempo.",
|
||||
"TaskKeyframeExtractorDescription": "Extrae fotogramas clave dos vídeos para crear listas de reprodución HLS máis precisas. Podería levar moito tempo.",
|
||||
"External": "Externo",
|
||||
"HearingImpaired": "Problemas de audición",
|
||||
"TaskKeyframeExtractor": "Extractor de fragmentos",
|
||||
"TaskAudioNormalization": "Normalización do audio",
|
||||
"TaskRefreshTrickplayImagesDescription": "Crea vistas previas de reprodución con truco para vídeos en bibliotecas activadas.",
|
||||
"TaskKeyframeExtractor": "Extractor de fotogramas clave",
|
||||
"TaskAudioNormalization": "Normalización de volume",
|
||||
"TaskRefreshTrickplayImagesDescription": "Crea miniaturas de previsualización para os vídeos nas bibliotecas habilitadas.",
|
||||
"TaskDownloadMissingLyrics": "Descargar letras que faltan",
|
||||
"TaskDownloadMissingLyricsDescription": "Descargas de letras das cancións",
|
||||
"TaskDownloadMissingLyricsDescription": "Descarga as letras das cancións",
|
||||
"TaskCleanCollectionsAndPlaylists": "Limpar coleccións e listas de reprodución",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Elimina elementos de coleccións e listas de reprodución que xa non existen.",
|
||||
"TaskExtractMediaSegmentsDescription": "Extrae ou obtén segmentos multimedia de complementos habilitados para o Segmento de medios.",
|
||||
"TaskExtractMediaSegments": "Escaneo de segmentos multimedia",
|
||||
"TaskMoveTrickplayImages": "Migrar a localización da imaxe de Trickplay",
|
||||
"TaskMoveTrickplayImagesDescription": "Move os ficheiros de reprodución con trickplay existentes segundo a configuración da biblioteca.",
|
||||
"TaskRefreshTrickplayImages": "Xerar imaxes de Trickplay",
|
||||
"TaskAudioNormalizationDescription": "Analiza ficheiros para obter datos de normalización de audio.",
|
||||
"CleanupUserDataTask": "Tarefa de limpeza de datos do usuario",
|
||||
"CleanupUserDataTaskDescription": "Limpa todos os datos do usuario (Estado de visualización, estado de favorito, etc) da multimedia que leve non presente polo menos durante 90 días."
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Quita ítems que xa non existen das coleccións e listas de reprodución.",
|
||||
"TaskExtractMediaSegmentsDescription": "Procura segmentos de medios cos plugins habilitados.",
|
||||
"TaskExtractMediaSegments": "Escaneo de segmentos de medios",
|
||||
"TaskMoveTrickplayImages": "Migrar as miniaturas de previsualización a outra ubicación",
|
||||
"TaskMoveTrickplayImagesDescription": "Move as miniaturas de previsualización segundo a configuración da biblioteca.",
|
||||
"TaskRefreshTrickplayImages": "Xerar miniaturas de previsualización",
|
||||
"TaskAudioNormalizationDescription": "Escanea ficheiros á procura de datos de normalización de volume.",
|
||||
"CleanupUserDataTask": "Tarefa de limpeza de datos dos usuarios",
|
||||
"CleanupUserDataTaskDescription": "Limpa todos os datos do usuario (estado de visualización, de favorito etc.) dos medios ausentes polo menos 90 días."
|
||||
}
|
||||
|
||||
@@ -125,8 +125,8 @@
|
||||
"TaskKeyframeExtractor": "Izvoditelj ključnog okvira",
|
||||
"TaskOptimizeDatabaseDescription": "Sažima bazu podataka i uklanja prazan prostor. Pokretanje ovog zadatka, može poboljšati performanse nakon provođenja indeksiranja biblioteke ili provođenja drugih promjena koje utječu na bazu podataka.",
|
||||
"HearingImpaired": "Oštećen sluh",
|
||||
"TaskRefreshTrickplayImages": "Generiraj Trickplay Slike",
|
||||
"TaskRefreshTrickplayImagesDescription": "Kreira trickplay pretpreglede za videe u omogućenim knjižnicama.",
|
||||
"TaskRefreshTrickplayImages": "Generiraj slike brzog pregledavanja",
|
||||
"TaskRefreshTrickplayImagesDescription": "Stvara preglede brzog pregledavanja za videa u aktiviranim bibliotekama.",
|
||||
"TaskAudioNormalization": "Normalizacija zvuka",
|
||||
"TaskAudioNormalizationDescription": "Skenira datoteke u potrazi za podacima o normalizaciji zvuka.",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Uklanja stavke iz zbirki i popisa za reprodukciju koje više ne postoje.",
|
||||
@@ -135,8 +135,8 @@
|
||||
"TaskDownloadMissingLyrics": "Preuzmi tekstove koji nedostaju",
|
||||
"TaskDownloadMissingLyricsDescription": "Preuzmi tekstove pjesama",
|
||||
"TaskExtractMediaSegmentsDescription": "Izvlači ili pribavlja dijelove medija iz omogućenih media pluginova.",
|
||||
"TaskMoveTrickplayImages": "Preseli lokaciju Trickplay slika",
|
||||
"TaskMoveTrickplayImagesDescription": "Preseli lokaciju Trickplay slika prema postavkama zbirke.",
|
||||
"TaskMoveTrickplayImages": "Premjesti mjesto slika brzog pregledavanja",
|
||||
"TaskMoveTrickplayImagesDescription": "Premješta postojeće datoteke brzog pregledavanja prema postavkama biblioteke.",
|
||||
"CleanupUserDataTask": "Zadatak čišćenja korisničkih podataka",
|
||||
"CleanupUserDataTaskDescription": "Briše sve korisničke podatke (stanje gledanja, status favorita itd.) s medija koji više nisu prisutni najmanje 90 dana."
|
||||
}
|
||||
|
||||
@@ -1,3 +1,62 @@
|
||||
{
|
||||
"Books": "liv"
|
||||
"Books": "Liv",
|
||||
"TasksLibraryCategory": "Libreri",
|
||||
"Albums": "Albòm yo",
|
||||
"Artists": "Atis yo",
|
||||
"Application": "Aplikasyon",
|
||||
"Channels": "Kanal yo",
|
||||
"ChapterNameValue": "Chapit {0}",
|
||||
"Default": "Defo",
|
||||
"DeviceOnlineWithName": "{0} konekte",
|
||||
"DeviceOfflineWithName": "{0} dekonekte",
|
||||
"External": "Extèn",
|
||||
"Collections": "Koleksyon yo",
|
||||
"Favorites": "Pi Renmen",
|
||||
"Folders": "Dosye",
|
||||
"Genres": "Jan yo",
|
||||
"Forced": "Fòse",
|
||||
"HeaderAlbumArtists": "Albòm Atis",
|
||||
"HeaderContinueWatching": "Kontinye Kade",
|
||||
"HeaderFavoriteAlbums": "Albòm Pi Renmen",
|
||||
"HeaderFavoriteArtists": "Atis Pi Renmen",
|
||||
"HeaderFavoriteEpisodes": "Epizòd Pi Renmen",
|
||||
"HeaderFavoriteShows": "Emisyon Pi Renmen",
|
||||
"HeaderFavoriteSongs": "Mizik Pi Renmen",
|
||||
"HeaderLiveTV": "Televizyon an Direk",
|
||||
"HeaderNextUp": "Pwochen an",
|
||||
"HomeVideos": "Videyo Lakay",
|
||||
"Latest": "Pi Resan",
|
||||
"MessageApplicationUpdated": "Sèvè Jellyfin met a jou",
|
||||
"MessageApplicationUpdatedTo": "Sèvè Jellyfin met a jou sou {0}",
|
||||
"Movies": "Fim",
|
||||
"MixedContent": "Kontni Melanje",
|
||||
"Music": "Mizik",
|
||||
"MusicVideos": "Videyo Mizik",
|
||||
"NameInstallFailed": "{0} enstalasyon fe fayit",
|
||||
"NameSeasonNumber": "Sezon {0}",
|
||||
"NameSeasonUnknown": "Sezon Enkoni",
|
||||
"NotificationOptionCameraImageUploaded": "Imaj Kamera telechaje",
|
||||
"NotificationOptionInstallationFailed": "Enstalasyon echwe",
|
||||
"Photos": "Foto",
|
||||
"PluginInstalledWithName": "{0} te enstale",
|
||||
"PluginUninstalledWithName": "{0} te dezenstale",
|
||||
"PluginUpdatedWithName": "{0} te mi a jou",
|
||||
"ScheduledTaskFailedWithName": "{0} echwe",
|
||||
"ScheduledTaskStartedWithName": "{0} komanse",
|
||||
"Songs": "Mizik yo",
|
||||
"Shows": "Emisyon yo",
|
||||
"System": "Sistèm",
|
||||
"TvShows": "Emisyon Tele",
|
||||
"User": "Itilizatè",
|
||||
"UserCreatedWithName": "Itilizatè {0} kreye",
|
||||
"UserDeletedWithName": "Itilizatè {0} a efase",
|
||||
"UserDownloadingItemWithValues": "{0} ap telechaje {1}",
|
||||
"UserOfflineFromDevice": "{0} dekonekte de {1}",
|
||||
"UserStartedPlayingItemWithValues": "{0} ap jwe {1} sou {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} fin jwe {1} sou {2}",
|
||||
"UserPasswordChangedWithName": "Modpas la chanje pou Itilizatè {0}",
|
||||
"ValueSpecialEpisodeName": "Spesyal - {0}",
|
||||
"VersionNumber": "Vesyon {0}",
|
||||
"TasksApplicationCategory": "Aplikasyon",
|
||||
"TasksMaintenanceCategory": "Antretyen"
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"Books": "Номууд",
|
||||
"Books": "Номнууд",
|
||||
"HeaderNextUp": "Дараа нь",
|
||||
"HeaderContinueWatching": "Үргэлжлүүлэн үзэх",
|
||||
"Songs": "Дуунууд",
|
||||
"Playlists": "Тоглуулах жагсаалт",
|
||||
"Movies": "Кино",
|
||||
"Playlists": "Playlist-ууд",
|
||||
"Movies": "Кинонууд",
|
||||
"Latest": "Сүүлийн үеийн",
|
||||
"Genres": "Төрлүүд",
|
||||
"Favorites": "Дуртай",
|
||||
"Collections": "Багц",
|
||||
"Collections": "Цуглуулгууд",
|
||||
"Artists": "Уран бүтээлчид",
|
||||
"Albums": "Цомгууд",
|
||||
"Albums": "Дуут цомгууд",
|
||||
"TaskExtractMediaSegments": "Медиа сегмент шалга",
|
||||
"TaskExtractMediaSegmentsDescription": "MediaSegment идэвхжүүлсэн залгаасуудаас медиа сегментүүдийг задлах эсвэл олж авах.",
|
||||
"TaskMoveTrickplayImages": "Трикплэй зургуудын байршлыг шилжүүлэх",
|
||||
@@ -63,11 +63,11 @@
|
||||
"CameraImageUploadedFrom": "{0}-с шинэ зураг байршуулагдлаа",
|
||||
"Channels": "Сувгууд",
|
||||
"ChapterNameValue": "{0}-р бүлэг",
|
||||
"Default": "Өгөгдмөл",
|
||||
"Default": "Анхдагч",
|
||||
"DeviceOfflineWithName": "{0}-н холболт саллаа",
|
||||
"DeviceOnlineWithName": "{0} холбогдлоо",
|
||||
"FailedLoginAttemptWithUserName": "{0}-н нэвтрэх оролдлого амжилтгүй",
|
||||
"Folders": "Хавтаснууд",
|
||||
"Folders": "Хавтасууд",
|
||||
"Forced": "Хүчээр",
|
||||
"HeaderAlbumArtists": "Цомгийн уран бүтээлчид",
|
||||
"HeaderFavoriteAlbums": "Дуртай цомгууд",
|
||||
@@ -84,8 +84,8 @@
|
||||
"MessageApplicationUpdatedTo": "Jellyfin Server {0} болж шинэчлэгдлээ",
|
||||
"MessageServerConfigurationUpdated": "Server-н тохиргоо шинэчлэгдлээ",
|
||||
"MixedContent": "Холимог агуулга",
|
||||
"Music": "Дуу",
|
||||
"MusicVideos": "Дууны клип",
|
||||
"Music": "Хөгжим",
|
||||
"MusicVideos": "Дууны клипүүд",
|
||||
"NameInstallFailed": "{0} суулгахад алдаа гарлаа",
|
||||
"NameSeasonNumber": "{0}-р улирал",
|
||||
"NameSeasonUnknown": "Улирал олдсонгүй",
|
||||
@@ -101,14 +101,14 @@
|
||||
"NotificationOptionUserLockedOut": "Хэрэглэгчийг түгжив",
|
||||
"NotificationOptionVideoPlayback": "Бичлэгийг тоглуулж эхлэв",
|
||||
"Photos": "Зургууд",
|
||||
"Plugin": "Plugin",
|
||||
"Plugin": "Плагин",
|
||||
"PluginInstalledWithName": "{0}-г суулгалаа",
|
||||
"PluginUninstalledWithName": "{0}-г устгалаа",
|
||||
"PluginUpdatedWithName": "{0}-г шинэчиллээ",
|
||||
"ProviderValue": "Нийлүүлэгч: {0}",
|
||||
"ScheduledTaskStartedWithName": "{0}-г эхлүүлэв",
|
||||
"ServerNameNeedsToBeRestarted": "{0}-г дахин асаана уу",
|
||||
"Shows": "Нэвтрүүлгүүд",
|
||||
"Shows": "Шоу",
|
||||
"Sync": "Дахин",
|
||||
"System": "Систем",
|
||||
"TvShows": "ТВ нэвтрүүлгүүд",
|
||||
@@ -122,7 +122,7 @@
|
||||
"UserPolicyUpdatedWithName": "Хэрэглэгчийн журмыг {0}-д зориулан шинэчиллээ",
|
||||
"UserStartedPlayingItemWithValues": "{0}-г {2} дээр {1}-г тоглуулж байна",
|
||||
"UserStoppedPlayingItemWithValues": "{0}-г {2} дээр {1}-г тоглуулж дуусгалаа",
|
||||
"ValueSpecialEpisodeName": "Тусгай - {0}",
|
||||
"ValueSpecialEpisodeName": "Онцгой - {0}",
|
||||
"VersionNumber": "Хувилбар {0}",
|
||||
"TasksMaintenanceCategory": "Засвар",
|
||||
"TasksLibraryCategory": "Сан",
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
{
|
||||
"Books": "کتابیں"
|
||||
"Books": "کتابیں",
|
||||
"AppDeviceValues": "ایپ: {0}، ڈیوائس: {1}",
|
||||
"Albums": "البمز",
|
||||
"Application": "ایپلی کیشن",
|
||||
"Artists": "فنکار",
|
||||
"AuthenticationSucceededWithUserName": "{0} کی کامیابی سے تصدیق ہو چکی ہے",
|
||||
"CameraImageUploadedFrom": "ایک نئی کیمرے کی تصویر {0} سے اپ لوڈ کی گئی ہے",
|
||||
"Channels": "چینلز",
|
||||
"ChapterNameValue": "باب {0}",
|
||||
"Collections": "مجموعے",
|
||||
"Default": "ڈیفالٹ",
|
||||
"DeviceOfflineWithName": "{0} نے رابطہ منقطع کر دیا ہے",
|
||||
"DeviceOnlineWithName": "{0} منسلک ہے",
|
||||
"External": "بیرونی"
|
||||
}
|
||||
|
||||
@@ -123,5 +123,9 @@
|
||||
"TaskCleanActivityLogDescription": "تشکیل شدہ عمر سے زیادہ پرانی سرگرمی لاگ اندراجات کو حذف کرتا ہے۔",
|
||||
"External": "بیرونی",
|
||||
"HearingImpaired": "قوت سماعت سے محروم",
|
||||
"TaskCleanActivityLog": "سرگرمی لاگ کو صاف کریں"
|
||||
"TaskCleanActivityLog": "سرگرمی لاگ کو صاف کریں",
|
||||
"TaskDownloadMissingLyrics": "غائب بول ڈاؤن لوڈ کریں",
|
||||
"TaskDownloadMissingLyricsDescription": "گانے کے غائب بول ڈاؤن لوڈ کریں",
|
||||
"TaskAudioNormalization": "آڈیو نارملائزیشن",
|
||||
"TaskAudioNormalizationDescription": "آڈیو نارملائزیشن ڈیٹا کے لیے فائلوں کو سکین کرتا ہے۔"
|
||||
}
|
||||
|
||||
@@ -261,14 +261,22 @@ public partial class AudioNormalizationTask : IScheduledTask
|
||||
|
||||
using var reader = process.StandardError;
|
||||
float? lufs = null;
|
||||
var foundLufs = false;
|
||||
await foreach (var line in reader.ReadAllLinesAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
Match match = LUFSRegex().Match(line);
|
||||
if (match.Success)
|
||||
if (foundLufs)
|
||||
{
|
||||
lufs = float.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
|
||||
Match match = LUFSRegex().Match(line);
|
||||
if (!match.Success)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
lufs = float.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);
|
||||
foundLufs = true;
|
||||
}
|
||||
|
||||
if (lufs is null)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -1625,8 +1625,11 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
|
||||
var useLegacySegmentOption = _mediaEncoder.EncoderVersion < _minFFmpegHlsSegmentOptions;
|
||||
|
||||
// fMP4 needs this flag to write the audio packet DTS/PTS including the initial delay into MOOF::TRAF::TFDT
|
||||
hlsArguments += $" {(useLegacySegmentOption ? "-hls_ts_options" : "-hls_segment_options")} movflags=+frag_discont";
|
||||
if (state.VideoStream is not null && state.IsOutputVideo)
|
||||
{
|
||||
// fMP4 needs this flag to write the audio packet DTS/PTS including the initial delay into MOOF::TRAF::TFDT
|
||||
hlsArguments += $" {(useLegacySegmentOption ? "-hls_ts_options" : "-hls_segment_options")} movflags=+frag_discont";
|
||||
}
|
||||
|
||||
segmentFormat = "fmp4" + outputFmp4HeaderArg;
|
||||
}
|
||||
|
||||
@@ -779,12 +779,14 @@ public class LibraryController : BaseJellyfinApiController
|
||||
var query = new InternalItemsQuery(user)
|
||||
{
|
||||
Genres = item.Genres,
|
||||
Tags = item.Tags,
|
||||
Limit = limit,
|
||||
IncludeItemTypes = includeItemTypes.ToArray(),
|
||||
DtoOptions = dtoOptions,
|
||||
EnableTotalRecordCount = !isMovie ?? true,
|
||||
EnableGroupByMetadataKey = isMovie ?? false,
|
||||
ExcludeItemIds = [itemId]
|
||||
ExcludeItemIds = [itemId],
|
||||
OrderBy = [(ItemSortBy.Random, SortOrder.Ascending)]
|
||||
};
|
||||
|
||||
// ExcludeArtistIds
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Data</PackageId>
|
||||
<VersionPrefix>10.11.0</VersionPrefix>
|
||||
<VersionPrefix>10.11.2</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ public sealed class BaseItemRepository
|
||||
private static readonly IReadOnlyList<ItemValueType> _getAlbumArtistValueTypes = [ItemValueType.AlbumArtist];
|
||||
private static readonly IReadOnlyList<ItemValueType> _getStudiosValueTypes = [ItemValueType.Studios];
|
||||
private static readonly IReadOnlyList<ItemValueType> _getGenreValueTypes = [ItemValueType.Genre];
|
||||
private static readonly IReadOnlyList<char> SearchWildcardTerms = ['%', '_', '[', ']', '^'];
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BaseItemRepository"/> class.
|
||||
@@ -266,12 +267,13 @@ public sealed class BaseItemRepository
|
||||
IQueryable<BaseItemEntity> dbQuery = PrepareItemQuery(context, filter);
|
||||
|
||||
dbQuery = TranslateQuery(dbQuery, context, filter);
|
||||
dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
|
||||
|
||||
if (filter.EnableTotalRecordCount)
|
||||
{
|
||||
result.TotalRecordCount = dbQuery.Count();
|
||||
}
|
||||
|
||||
dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
|
||||
dbQuery = ApplyQueryPaging(dbQuery, filter);
|
||||
|
||||
result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
|
||||
@@ -612,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;
|
||||
}
|
||||
}
|
||||
@@ -1230,8 +1239,20 @@ public sealed class BaseItemRepository
|
||||
ExcludeItemIds = filter.ExcludeItemIds
|
||||
};
|
||||
|
||||
var query = TranslateQuery(innerQuery, context, outerQueryFilter)
|
||||
.GroupBy(e => e.PresentationUniqueKey);
|
||||
var masterQuery = TranslateQuery(innerQuery, context, outerQueryFilter)
|
||||
.GroupBy(e => e.PresentationUniqueKey)
|
||||
.Select(e => e.FirstOrDefault())
|
||||
.Select(e => e!.Id);
|
||||
|
||||
var query = context.BaseItems
|
||||
.Include(e => e.TrailerTypes)
|
||||
.Include(e => e.Provider)
|
||||
.Include(e => e.LockedFields)
|
||||
.Include(e => e.Images)
|
||||
.AsSingleQuery()
|
||||
.Where(e => masterQuery.Contains(e.Id));
|
||||
|
||||
query = ApplyOrder(query, filter);
|
||||
|
||||
var result = new QueryResult<(BaseItemDto, ItemCounts?)>();
|
||||
if (filter.EnableTotalRecordCount)
|
||||
@@ -1286,12 +1307,7 @@ public sealed class BaseItemRepository
|
||||
|
||||
var resultQuery = query.Select(e => new
|
||||
{
|
||||
item = e.AsQueryable()
|
||||
.Include(e => e.TrailerTypes)
|
||||
.Include(e => e.Provider)
|
||||
.Include(e => e.LockedFields)
|
||||
.Include(e => e.Images)
|
||||
.AsSingleQuery().First(),
|
||||
item = e,
|
||||
// TODO: This is bad refactor!
|
||||
itemCount = new ItemCounts()
|
||||
{
|
||||
@@ -1323,13 +1339,6 @@ public sealed class BaseItemRepository
|
||||
result.Items =
|
||||
[
|
||||
.. query
|
||||
.Select(e => e.AsQueryable()
|
||||
.Include(e => e.TrailerTypes)
|
||||
.Include(e => e.Provider)
|
||||
.Include(e => e.LockedFields)
|
||||
.Include(e => e.Images)
|
||||
.AsSingleQuery()
|
||||
.First())
|
||||
.AsEnumerable()
|
||||
.Where(e => e is not null)
|
||||
.Select<BaseItemEntity, (BaseItemDto, ItemCounts?)>(e =>
|
||||
@@ -1693,7 +1702,15 @@ public sealed class BaseItemRepository
|
||||
if (!string.IsNullOrEmpty(filter.SearchTerm))
|
||||
{
|
||||
var searchTerm = filter.SearchTerm.ToLower();
|
||||
baseQuery = baseQuery.Where(e => e.CleanName!.ToLower().Contains(searchTerm) || (e.OriginalTitle != null && e.OriginalTitle.ToLower().Contains(searchTerm)));
|
||||
if (SearchWildcardTerms.Any(f => searchTerm.Contains(f)))
|
||||
{
|
||||
searchTerm = $"%{searchTerm.Trim('%')}%";
|
||||
baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName!.ToLower(), searchTerm) || (e.OriginalTitle != null && EF.Functions.Like(e.OriginalTitle.ToLower(), searchTerm)));
|
||||
}
|
||||
else
|
||||
{
|
||||
baseQuery = baseQuery.Where(e => e.CleanName!.ToLower().Contains(searchTerm) || (e.OriginalTitle != null && e.OriginalTitle.ToLower().Contains(searchTerm)));
|
||||
}
|
||||
}
|
||||
|
||||
if (filter.IsFolder.HasValue)
|
||||
@@ -1746,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))
|
||||
@@ -1865,10 +1883,17 @@ public sealed class BaseItemRepository
|
||||
|
||||
if (filter.PersonIds.Length > 0)
|
||||
{
|
||||
var peopleEntityIds = context.BaseItems
|
||||
.WhereOneOrMany(filter.PersonIds, b => b.Id)
|
||||
.Join(
|
||||
context.Peoples,
|
||||
b => b.Name,
|
||||
p => p.Name,
|
||||
(b, p) => p.Id);
|
||||
|
||||
baseQuery = baseQuery
|
||||
.Where(e =>
|
||||
context.PeopleBaseItemMap.Where(w => context.BaseItems.Where(r => filter.PersonIds.Contains(r.Id)).Any(f => f.Name == w.People.Name))
|
||||
.Any(f => f.ItemId == e.Id));
|
||||
.Where(e => context.PeopleBaseItemMap
|
||||
.Any(m => m.ItemId == e.Id && peopleEntityIds.Contains(m.PeopleId)));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.Person))
|
||||
@@ -1904,9 +1929,17 @@ public sealed class BaseItemRepository
|
||||
var nameContains = filter.NameContains;
|
||||
if (!string.IsNullOrWhiteSpace(nameContains))
|
||||
{
|
||||
baseQuery = baseQuery.Where(e =>
|
||||
e.CleanName!.Contains(nameContains)
|
||||
|| e.OriginalTitle!.ToLower().Contains(nameContains!));
|
||||
if (SearchWildcardTerms.Any(f => nameContains.Contains(f)))
|
||||
{
|
||||
nameContains = $"%{nameContains.Trim('%')}%";
|
||||
baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName, nameContains) || EF.Functions.Like(e.OriginalTitle, nameContains));
|
||||
}
|
||||
else
|
||||
{
|
||||
baseQuery = baseQuery.Where(e =>
|
||||
e.CleanName!.Contains(nameContains)
|
||||
|| e.OriginalTitle!.ToLower().Contains(nameContains!));
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.NameStartsWith))
|
||||
@@ -1989,7 +2022,7 @@ public sealed class BaseItemRepository
|
||||
|
||||
if (filter.ArtistIds.Length > 0)
|
||||
{
|
||||
baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Artist, filter.ArtistIds);
|
||||
baseQuery = baseQuery.WhereReferencedItemMultipleTypes(context, [ItemValueType.Artist, ItemValueType.AlbumArtist], filter.ArtistIds);
|
||||
}
|
||||
|
||||
if (filter.AlbumArtistIds.Length > 0)
|
||||
@@ -1999,7 +2032,18 @@ public sealed class BaseItemRepository
|
||||
|
||||
if (filter.ContributingArtistIds.Length > 0)
|
||||
{
|
||||
baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Artist, filter.ContributingArtistIds);
|
||||
var contributingNames = context.BaseItems
|
||||
.Where(b => filter.ContributingArtistIds.Contains(b.Id))
|
||||
.Select(b => b.CleanName);
|
||||
|
||||
baseQuery = baseQuery.Where(e =>
|
||||
e.ItemValues!.Any(ivm =>
|
||||
ivm.ItemValue.Type == ItemValueType.Artist &&
|
||||
contributingNames.Contains(ivm.ItemValue.CleanValue))
|
||||
&&
|
||||
!e.ItemValues!.Any(ivm =>
|
||||
ivm.ItemValue.Type == ItemValueType.AlbumArtist &&
|
||||
contributingNames.Contains(ivm.ItemValue.CleanValue)));
|
||||
}
|
||||
|
||||
if (filter.AlbumIds.Length > 0)
|
||||
@@ -2010,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)
|
||||
@@ -2054,22 +2098,26 @@ public sealed class BaseItemRepository
|
||||
if (filter.MinParentalRating != null)
|
||||
{
|
||||
var min = filter.MinParentalRating;
|
||||
minParentalRatingFilter = e => e.InheritedParentalRatingValue >= min.Score || e.InheritedParentalRatingValue == null;
|
||||
if (min.SubScore != null)
|
||||
{
|
||||
minParentalRatingFilter = minParentalRatingFilter.And(e => e.InheritedParentalRatingValue >= min.SubScore || e.InheritedParentalRatingValue == null);
|
||||
}
|
||||
var minScore = min.Score;
|
||||
var minSubScore = min.SubScore ?? 0;
|
||||
|
||||
minParentalRatingFilter = e =>
|
||||
e.InheritedParentalRatingValue == null ||
|
||||
e.InheritedParentalRatingValue > minScore ||
|
||||
(e.InheritedParentalRatingValue == minScore && (e.InheritedParentalRatingSubValue ?? 0) >= minSubScore);
|
||||
}
|
||||
|
||||
Expression<Func<BaseItemEntity, bool>>? maxParentalRatingFilter = null;
|
||||
if (filter.MaxParentalRating != null)
|
||||
{
|
||||
var max = filter.MaxParentalRating;
|
||||
maxParentalRatingFilter = e => e.InheritedParentalRatingValue <= max.Score || e.InheritedParentalRatingValue == null;
|
||||
if (max.SubScore != null)
|
||||
{
|
||||
maxParentalRatingFilter = maxParentalRatingFilter.And(e => e.InheritedParentalRatingValue <= max.SubScore || e.InheritedParentalRatingValue == null);
|
||||
}
|
||||
var maxScore = max.Score;
|
||||
var maxSubScore = max.SubScore ?? 0;
|
||||
|
||||
maxParentalRatingFilter = e =>
|
||||
e.InheritedParentalRatingValue == null ||
|
||||
e.InheritedParentalRatingValue < maxScore ||
|
||||
(e.InheritedParentalRatingValue == maxScore && (e.InheritedParentalRatingSubValue ?? 0) <= maxSubScore);
|
||||
}
|
||||
|
||||
if (filter.HasParentalRating ?? false)
|
||||
@@ -2313,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;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
@@ -82,11 +84,14 @@ public class ChapterRepository : IChapterRepository
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void DeleteChapters(Guid itemId)
|
||||
public async Task DeleteChaptersAsync(Guid itemId, CancellationToken cancellationToken)
|
||||
{
|
||||
using var context = _dbProvider.CreateDbContext();
|
||||
context.Chapters.Where(c => c.ItemId.Equals(itemId)).ExecuteDelete();
|
||||
context.SaveChanges();
|
||||
var dbContext = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
{
|
||||
await dbContext.Chapters.Where(c => c.ItemId.Equals(itemId)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
|
||||
await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private Chapter Map(ChapterInfo chapterInfo, int index, Guid itemId)
|
||||
|
||||
@@ -95,6 +95,7 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
|
||||
.ToArray();
|
||||
|
||||
var toAdd = people
|
||||
.Where(e => e.Type is not PersonKind.Artist && e.Type is not PersonKind.AlbumArtist)
|
||||
.Where(e => !existingPersons.Any(f => f.Name == e.Name && f.PersonType == e.Type.ToString()))
|
||||
.Select(Map);
|
||||
context.Peoples.AddRange(toAdd);
|
||||
@@ -108,6 +109,11 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
|
||||
|
||||
foreach (var person in people)
|
||||
{
|
||||
if (person.Type == PersonKind.Artist || person.Type == PersonKind.AlbumArtist)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var entityPerson = personsEntities.First(e => e.Name == person.Name && e.PersonType == person.Type.ToString());
|
||||
var existingMap = existingMaps.FirstOrDefault(e => e.People.Name == person.Name && e.People.PersonType == person.Type.ToString() && e.Role == person.Role);
|
||||
if (existingMap is null)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
@@ -92,33 +93,38 @@ namespace Jellyfin.Server.Implementations.Users
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ForgotPasswordResult> StartForgotPasswordProcess(User user, bool isInNetwork)
|
||||
public async Task<ForgotPasswordResult> StartForgotPasswordProcess(User? user, string enteredUsername, bool isInNetwork)
|
||||
{
|
||||
byte[] bytes = new byte[4];
|
||||
RandomNumberGenerator.Fill(bytes);
|
||||
string pin = BitConverter.ToString(bytes);
|
||||
|
||||
DateTime expireTime = DateTime.UtcNow.AddMinutes(30);
|
||||
string filePath = _passwordResetFileBase + user.Id + ".json";
|
||||
SerializablePasswordReset spr = new SerializablePasswordReset
|
||||
{
|
||||
ExpirationDate = expireTime,
|
||||
Pin = pin,
|
||||
PinFile = filePath,
|
||||
UserName = user.Username
|
||||
};
|
||||
var usernameHash = enteredUsername.ToUpperInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||
var pinFile = _passwordResetFileBase + usernameHash + ".json";
|
||||
|
||||
FileStream fileStream = AsyncFile.OpenWrite(filePath);
|
||||
await using (fileStream.ConfigureAwait(false))
|
||||
if (user is not null && isInNetwork)
|
||||
{
|
||||
await JsonSerializer.SerializeAsync(fileStream, spr).ConfigureAwait(false);
|
||||
byte[] bytes = new byte[4];
|
||||
RandomNumberGenerator.Fill(bytes);
|
||||
string pin = BitConverter.ToString(bytes);
|
||||
|
||||
SerializablePasswordReset spr = new SerializablePasswordReset
|
||||
{
|
||||
ExpirationDate = expireTime,
|
||||
Pin = pin,
|
||||
PinFile = pinFile,
|
||||
UserName = user.Username
|
||||
};
|
||||
|
||||
FileStream fileStream = AsyncFile.Create(pinFile);
|
||||
await using (fileStream.ConfigureAwait(false))
|
||||
{
|
||||
await JsonSerializer.SerializeAsync(fileStream, spr).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
return new ForgotPasswordResult
|
||||
{
|
||||
Action = ForgotPasswordAction.PinCode,
|
||||
PinExpirationDate = expireTime,
|
||||
PinFile = filePath
|
||||
PinFile = pinFile
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -508,23 +508,18 @@ namespace Jellyfin.Server.Implementations.Users
|
||||
public async Task<ForgotPasswordResult> StartForgotPasswordProcess(string enteredUsername, bool isInNetwork)
|
||||
{
|
||||
var user = string.IsNullOrWhiteSpace(enteredUsername) ? null : GetUserByName(enteredUsername);
|
||||
var passwordResetProvider = GetPasswordResetProvider(user);
|
||||
|
||||
var result = await passwordResetProvider
|
||||
.StartForgotPasswordProcess(user, enteredUsername, isInNetwork)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (user is not null && isInNetwork)
|
||||
{
|
||||
var passwordResetProvider = GetPasswordResetProvider(user);
|
||||
var result = await passwordResetProvider
|
||||
.StartForgotPasswordProcess(user, isInNetwork)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
await UpdateUserAsync(user).ConfigureAwait(false);
|
||||
return result;
|
||||
}
|
||||
|
||||
return new ForgotPasswordResult
|
||||
{
|
||||
Action = ForgotPasswordAction.InNetworkRequired,
|
||||
PinFile = string.Empty
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -760,8 +755,13 @@ namespace Jellyfin.Server.Implementations.Users
|
||||
return GetAuthenticationProviders(user)[0];
|
||||
}
|
||||
|
||||
private IPasswordResetProvider GetPasswordResetProvider(User user)
|
||||
private IPasswordResetProvider GetPasswordResetProvider(User? user)
|
||||
{
|
||||
if (user is null)
|
||||
{
|
||||
return _defaultPasswordResetProvider;
|
||||
}
|
||||
|
||||
return GetPasswordResetProviders(user)[0];
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ internal class RetryOnTemporarilyUnavailableFilter : IOperationFilter
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
operation.Responses.Add(
|
||||
operation.Responses.TryAdd(
|
||||
"503",
|
||||
new OpenApiResponse
|
||||
{
|
||||
|
||||
@@ -66,15 +66,8 @@ public class SecurityRequirementsOperationFilter : IOperationFilter
|
||||
return;
|
||||
}
|
||||
|
||||
if (!operation.Responses.ContainsKey("401"))
|
||||
{
|
||||
operation.Responses.Add("401", new OpenApiResponse { Description = "Unauthorized" });
|
||||
}
|
||||
|
||||
if (!operation.Responses.ContainsKey("403"))
|
||||
{
|
||||
operation.Responses.Add("403", new OpenApiResponse { Description = "Forbidden" });
|
||||
}
|
||||
operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" });
|
||||
operation.Responses.TryAdd("403", new OpenApiResponse { Description = "Forbidden" });
|
||||
|
||||
var scheme = new OpenApiSecurityScheme
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
47
Jellyfin.Server/Migrations/Routines/CleanMusicArtist.cs
Normal file
47
Jellyfin.Server/Migrations/Routines/CleanMusicArtist.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using Jellyfin.Server.ServerSetupApp;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Server.Migrations.Routines;
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up all Music artists that have been migrated in the 10.11 RC migrations.
|
||||
/// </summary>
|
||||
[JellyfinMigration("2025-10-09T20:00:00", nameof(CleanMusicArtist))]
|
||||
[JellyfinMigrationBackup(JellyfinDb = true)]
|
||||
public class CleanMusicArtist : IAsyncMigrationRoutine
|
||||
{
|
||||
private readonly IStartupLogger<CleanMusicArtist> _startupLogger;
|
||||
private readonly IDbContextFactory<JellyfinDbContext> _dbContextFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CleanMusicArtist"/> class.
|
||||
/// </summary>
|
||||
/// <param name="startupLogger">The startup logger.</param>
|
||||
/// <param name="dbContextFactory">The Db context factory.</param>
|
||||
public CleanMusicArtist(IStartupLogger<CleanMusicArtist> startupLogger, IDbContextFactory<JellyfinDbContext> dbContextFactory)
|
||||
{
|
||||
_startupLogger = startupLogger;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task PerformAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using (context.ConfigureAwait(false))
|
||||
{
|
||||
var peoples = context.Peoples.Where(e => e.PersonType == nameof(PersonKind.Artist) || e.PersonType == nameof(PersonKind.AlbumArtist));
|
||||
_startupLogger.LogInformation("Delete {Number} Artist and Album Artist person types from db", await peoples.CountAsync(cancellationToken).ConfigureAwait(false));
|
||||
|
||||
await peoples
|
||||
.ExecuteDeleteAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
using var userDbConnection = new SqliteConnection($"Filename={Path.Combine(dataPath, "users.db")}");
|
||||
userDbConnection.Open();
|
||||
|
||||
@@ -50,9 +50,28 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
public void Perform()
|
||||
{
|
||||
var dataPath = _appPaths.DataPath;
|
||||
using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}"))
|
||||
var dbFilePath = Path.Combine(dataPath, DbFilename);
|
||||
|
||||
if (!File.Exists(dbFilePath))
|
||||
{
|
||||
_logger.LogWarning("{Path} doesn't exist, nothing to migrate", dbFilePath);
|
||||
return;
|
||||
}
|
||||
|
||||
using (var connection = new SqliteConnection($"Filename={dbFilePath}"))
|
||||
{
|
||||
connection.Open();
|
||||
|
||||
var tableQuery = connection.Query("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='Tokens';");
|
||||
foreach (var row in tableQuery)
|
||||
{
|
||||
if (row.GetInt32(0) == 0)
|
||||
{
|
||||
_logger.LogWarning("Table 'Tokens' doesn't exist in {Path}, nothing to migrate", dbFilePath);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
using var dbContext = _dbProvider.CreateDbContext();
|
||||
|
||||
var authenticatedDevices = connection.Query("SELECT * FROM Tokens");
|
||||
|
||||
@@ -78,9 +78,27 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
var displayPrefs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var customDisplayPrefs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var dbFilePath = Path.Combine(_paths.DataPath, DbFilename);
|
||||
|
||||
if (!File.Exists(dbFilePath))
|
||||
{
|
||||
_logger.LogWarning("{Path} doesn't exist, nothing to migrate", dbFilePath);
|
||||
return;
|
||||
}
|
||||
|
||||
using (var connection = new SqliteConnection($"Filename={dbFilePath}"))
|
||||
{
|
||||
connection.Open();
|
||||
|
||||
var tableQuery = connection.Query("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='userdisplaypreferences';");
|
||||
foreach (var row in tableQuery)
|
||||
{
|
||||
if (row.GetInt32(0) == 0)
|
||||
{
|
||||
_logger.LogWarning("Table 'userdisplaypreferences' doesn't exist in {Path}, nothing to migrate", dbFilePath);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
using var dbContext = _provider.CreateDbContext();
|
||||
|
||||
var results = connection.Query("SELECT * FROM userdisplaypreferences");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -257,6 +257,11 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!baseItemIds.Contains(refItem.Id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
userData.ItemId = refItem.Id;
|
||||
operation.JellyfinDbContext.UserData.Add(userData);
|
||||
}
|
||||
@@ -287,7 +292,13 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
{
|
||||
foreach (SqliteDataReader dto in connection.Query(mediaStreamQuery))
|
||||
{
|
||||
operation.JellyfinDbContext.MediaStreamInfos.Add(GetMediaStream(dto));
|
||||
var entity = GetMediaStream(dto);
|
||||
if (!baseItemIds.Contains(entity.ItemId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
operation.JellyfinDbContext.MediaStreamInfos.Add(entity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,7 +321,13 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
{
|
||||
foreach (SqliteDataReader dto in connection.Query(mediaAttachmentQuery))
|
||||
{
|
||||
operation.JellyfinDbContext.AttachmentStreamInfos.Add(GetMediaAttachment(dto));
|
||||
var entity = GetMediaAttachment(dto);
|
||||
if (!baseItemIds.Contains(entity.ItemId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
operation.JellyfinDbContext.AttachmentStreamInfos.Add(entity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,6 +413,11 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
foreach (SqliteDataReader dto in connection.Query(chapterQuery))
|
||||
{
|
||||
var chapter = GetChapter(dto);
|
||||
if (!baseItemIds.Contains(chapter.ItemId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
operation.JellyfinDbContext.Chapters.Add(chapter);
|
||||
}
|
||||
}
|
||||
@@ -422,6 +444,11 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
foreach (SqliteDataReader dto in connection.Query(ancestorIdsQuery))
|
||||
{
|
||||
var ancestorId = GetAncestorId(dto);
|
||||
if (!baseItemIds.Contains(ancestorId.ItemId) || !baseItemIds.Contains(ancestorId.ParentItemId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
operation.JellyfinDbContext.AncestorIds.Add(ancestorId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
using var dbContext = _provider.CreateDbContext();
|
||||
|
||||
var queryResult = connection.Query("SELECT * FROM LocalUsersv2");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Common</PackageId>
|
||||
<VersionPrefix>10.11.0</VersionPrefix>
|
||||
<VersionPrefix>10.11.2</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
@@ -15,11 +13,12 @@ namespace MediaBrowser.Controller.Authentication
|
||||
|
||||
bool IsEnabled { get; }
|
||||
|
||||
Task<ForgotPasswordResult> StartForgotPasswordProcess(User user, bool isInNetwork);
|
||||
Task<ForgotPasswordResult> StartForgotPasswordProcess(User? user, string enteredUsername, bool isInNetwork);
|
||||
|
||||
Task<PinRedeemResult> RedeemPasswordResetPin(string pin);
|
||||
}
|
||||
|
||||
#nullable disable
|
||||
public class PasswordPinCreationResult
|
||||
{
|
||||
public string PinFile { get; set; }
|
||||
|
||||
@@ -48,8 +48,10 @@ public interface IChapterManager
|
||||
Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the chapter images.
|
||||
/// Deletes the chapter data.
|
||||
/// </summary>
|
||||
/// <param name="video">Video to use.</param>
|
||||
void DeleteChapterImages(Video video);
|
||||
/// <param name="itemId">The item id.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task DeleteChapterDataAsync(Guid itemId, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -337,6 +337,11 @@ namespace MediaBrowser.Controller.Entities
|
||||
|
||||
try
|
||||
{
|
||||
if (GetParents().Any(f => f.Id.Equals(Id)))
|
||||
{
|
||||
throw new InvalidOperationException("Recursive datastructure detected abort processing this item.");
|
||||
}
|
||||
|
||||
await ValidateChildrenInternal2(progress, recursive, refreshChildMetadata, allowRemoveRoot, refreshOptions, directoryService, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
@@ -452,6 +457,12 @@ namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
foreach (var item in itemsRemoved)
|
||||
{
|
||||
if (!item.CanDelete())
|
||||
{
|
||||
Logger.LogDebug("Item marked as non-removable, skipping: {Path}", item.Path ?? item.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.IsFileProtocol)
|
||||
{
|
||||
Logger.LogDebug("Removed item: {Path}", item.Path);
|
||||
@@ -704,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);
|
||||
@@ -969,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);
|
||||
@@ -1346,6 +1362,14 @@ namespace MediaBrowser.Controller.Entities
|
||||
var realChildren = visibleChildren
|
||||
.Where(e => query is null || UserViewBuilder.FilterItem(e, query))
|
||||
.ToArray();
|
||||
|
||||
if (this is BoxSet && (query.OrderBy is null || query.OrderBy.Count == 0))
|
||||
{
|
||||
realChildren = realChildren
|
||||
.OrderBy(e => e.ProductionYear ?? int.MaxValue)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
var childCount = realChildren.Length;
|
||||
if (result.Count < limit)
|
||||
{
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Controller</PackageId>
|
||||
<VersionPrefix>10.11.0</VersionPrefix>
|
||||
<VersionPrefix>10.11.2</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -6552,7 +6552,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
if (isD3d11Supported && isCodecAvailable)
|
||||
{
|
||||
return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11 -noautorotate" + stripRotationDataArgs : string.Empty)
|
||||
+ (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty);
|
||||
+ (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + " -threads 2" + (isAv1 ? " -c:v av1" : string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Controller.Persistence;
|
||||
@@ -13,7 +15,9 @@ public interface IChapterRepository
|
||||
/// Deletes the chapters.
|
||||
/// </summary>
|
||||
/// <param name="itemId">The item.</param>
|
||||
void DeleteChapters(Guid itemId);
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task DeleteChaptersAsync(Guid itemId, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the chapters.
|
||||
|
||||
@@ -18,10 +18,16 @@ namespace MediaBrowser.MediaEncoding.Configuration
|
||||
|
||||
public void Validate(object oldConfig, object newConfig)
|
||||
{
|
||||
var newPath = ((EncodingOptions)newConfig).TranscodingTempPath;
|
||||
var oldEncodingOptions = (EncodingOptions)oldConfig;
|
||||
var newEncodingOptions = (EncodingOptions)newConfig;
|
||||
|
||||
ArgumentNullException.ThrowIfNull(oldEncodingOptions, nameof(oldConfig));
|
||||
ArgumentNullException.ThrowIfNull(newEncodingOptions, nameof(newConfig));
|
||||
|
||||
var newPath = newEncodingOptions.TranscodingTempPath;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(newPath)
|
||||
&& !string.Equals(((EncodingOptions)oldConfig).TranscodingTempPath, newPath, StringComparison.Ordinal))
|
||||
&& !string.Equals(oldEncodingOptions.TranscodingTempPath, newPath, StringComparison.Ordinal))
|
||||
{
|
||||
// Validate
|
||||
if (!Directory.Exists(newPath))
|
||||
@@ -33,6 +39,12 @@ namespace MediaBrowser.MediaEncoding.Configuration
|
||||
newPath));
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(newEncodingOptions.EncoderAppPath)
|
||||
&& !string.Equals(oldEncodingOptions.EncoderAppPath, newEncodingOptions.EncoderAppPath, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException("Unable to update encoder app path.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Model</PackageId>
|
||||
<VersionPrefix>10.11.0</VersionPrefix>
|
||||
<VersionPrefix>10.11.2</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
namespace MediaBrowser.Model.Users
|
||||
{
|
||||
public enum ForgotPasswordAction
|
||||
{
|
||||
[Obsolete("Returning different actions represents a security concern.")]
|
||||
ContactAdmin = 0,
|
||||
PinCode = 1,
|
||||
[Obsolete("Returning different actions represents a security concern.")]
|
||||
InNetworkRequired = 2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,6 +229,11 @@ namespace MediaBrowser.Providers.Manager
|
||||
if (file is not null)
|
||||
{
|
||||
item.DateModified = file.LastWriteTimeUtc;
|
||||
|
||||
if (!file.IsDirectory)
|
||||
{
|
||||
item.Size = file.Length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -533,14 +533,14 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|
||||
private bool TryGetSanitizedAdditionalFields(Track track, string field, out string? value)
|
||||
{
|
||||
var hasField = track.AdditionalFields.TryGetValue(field, out value);
|
||||
var hasField = TryGetAdditionalFieldWithFallback(track, field, out value);
|
||||
value = GetSanitizedStringTag(value, track.Path);
|
||||
return hasField;
|
||||
}
|
||||
|
||||
private bool TryGetSanitizedUFIDFields(Track track, out string? owner, out string? identifier)
|
||||
{
|
||||
var hasField = track.AdditionalFields.TryGetValue("UFID", out string? value);
|
||||
var hasField = TryGetAdditionalFieldWithFallback(track, "UFID", out string? value);
|
||||
if (hasField && !string.IsNullOrEmpty(value))
|
||||
{
|
||||
string[] parts = value.Split('\0');
|
||||
@@ -556,5 +556,37 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
identifier = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Build the explicit mka-style fallback key (e.g., ARTISTS -> track.artists, "MusicBrainz Artist Id" -> track.musicbrainz_artist_id)
|
||||
private static string GetMkaFallbackKey(string key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
return key;
|
||||
}
|
||||
|
||||
var normalized = key.Trim().Replace(' ', '_').ToLowerInvariant();
|
||||
return "track." + normalized;
|
||||
}
|
||||
|
||||
// First try the normal key exactly; if missing, try the mka-style fallback key.
|
||||
private bool TryGetAdditionalFieldWithFallback(Track track, string key, out string? value)
|
||||
{
|
||||
// Prefer the normal key (as-is, case-sensitive)
|
||||
if (track.AdditionalFields.TryGetValue(key, out value))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fallback to mka-style: "track." + lower-case(original key)
|
||||
var fallbackKey = GetMkaFallbackKey(key);
|
||||
if (track.AdditionalFields.TryGetValue(fallbackKey, out value))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ public class AudioDbArtistExternalUrlProvider : IExternalUrlProvider
|
||||
var baseUrl = "https://www.theaudiodb.com/";
|
||||
switch (item)
|
||||
{
|
||||
case MusicAlbum:
|
||||
case MusicArtist:
|
||||
case Person:
|
||||
yield return baseUrl + $"artist/{externalId}";
|
||||
break;
|
||||
|
||||
@@ -21,7 +21,7 @@ public class MusicBrainzArtistExternalUrlProvider : IExternalUrlProvider
|
||||
{
|
||||
switch (item)
|
||||
{
|
||||
case MusicAlbum:
|
||||
case MusicArtist:
|
||||
case Person:
|
||||
yield return Plugin.Instance!.Configuration.Server + $"/artist/{externalId}";
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
|
||||
var language = item.GetPreferredMetadataLanguage();
|
||||
|
||||
// TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here
|
||||
var collection = await _tmdbClientManager.GetCollectionAsync(tmdbId, null, null, cancellationToken).ConfigureAwait(false);
|
||||
var collection = await _tmdbClientManager.GetCollectionAsync(tmdbId, null, null, null, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (collection?.Images is null)
|
||||
{
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
|
||||
|
||||
if (tmdbId > 0)
|
||||
{
|
||||
var collection = await _tmdbClientManager.GetCollectionAsync(tmdbId, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken).ConfigureAwait(false);
|
||||
var collection = await _tmdbClientManager.GetCollectionAsync(tmdbId, language, TmdbUtils.GetImageLanguagesParam(language, searchInfo.MetadataCountryCode), searchInfo.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (collection is null)
|
||||
{
|
||||
@@ -70,7 +70,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
|
||||
return new[] { result };
|
||||
}
|
||||
|
||||
var collectionSearchResults = await _tmdbClientManager.SearchCollectionAsync(searchInfo.Name, language, cancellationToken).ConfigureAwait(false);
|
||||
var collectionSearchResults = await _tmdbClientManager.SearchCollectionAsync(searchInfo.Name, language, searchInfo.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var collections = new RemoteSearchResult[collectionSearchResults.Count];
|
||||
for (var i = 0; i < collectionSearchResults.Count; i++)
|
||||
@@ -95,6 +95,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
|
||||
{
|
||||
var tmdbId = Convert.ToInt32(info.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
|
||||
var language = info.MetadataLanguage;
|
||||
|
||||
// We don't already have an Id, need to fetch it
|
||||
if (tmdbId <= 0)
|
||||
{
|
||||
@@ -102,7 +103,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
|
||||
// Caller provides the filename with extension stripped and NOT the parsed filename
|
||||
var parsedName = _libraryManager.ParseName(info.Name);
|
||||
var cleanedName = TmdbUtils.CleanName(parsedName.Name);
|
||||
var searchResults = await _tmdbClientManager.SearchCollectionAsync(cleanedName, language, cancellationToken).ConfigureAwait(false);
|
||||
var searchResults = await _tmdbClientManager.SearchCollectionAsync(cleanedName, language, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (searchResults is not null && searchResults.Count > 0)
|
||||
{
|
||||
@@ -114,7 +115,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
|
||||
|
||||
if (tmdbId > 0)
|
||||
{
|
||||
var collection = await _tmdbClientManager.GetCollectionAsync(tmdbId, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken).ConfigureAwait(false);
|
||||
var collection = await _tmdbClientManager.GetCollectionAsync(tmdbId, language, TmdbUtils.GetImageLanguagesParam(language, info.MetadataCountryCode), info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (collection is not null)
|
||||
{
|
||||
|
||||
@@ -59,6 +59,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
|
||||
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
|
||||
{
|
||||
var language = item.GetPreferredMetadataLanguage();
|
||||
var countryCode = item.GetPreferredMetadataCountryCode();
|
||||
|
||||
var movieTmdbId = Convert.ToInt32(item.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
|
||||
if (movieTmdbId <= 0)
|
||||
@@ -69,7 +70,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
|
||||
return Enumerable.Empty<RemoteImageInfo>();
|
||||
}
|
||||
|
||||
var movieResult = await _tmdbClientManager.FindByExternalIdAsync(movieImdbId, FindExternalSource.Imdb, language, cancellationToken).ConfigureAwait(false);
|
||||
var movieResult = await _tmdbClientManager.FindByExternalIdAsync(movieImdbId, FindExternalSource.Imdb, language, countryCode, cancellationToken).ConfigureAwait(false);
|
||||
if (movieResult?.MovieResults is not null && movieResult.MovieResults.Count > 0)
|
||||
{
|
||||
movieTmdbId = movieResult.MovieResults[0].Id;
|
||||
@@ -83,7 +84,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
|
||||
|
||||
// TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here
|
||||
var movie = await _tmdbClientManager
|
||||
.GetMovieAsync(movieTmdbId, null, null, cancellationToken)
|
||||
.GetMovieAsync(movieTmdbId, null, null, null, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (movie?.Images is null)
|
||||
|
||||
@@ -59,7 +59,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
|
||||
.GetMovieAsync(
|
||||
int.Parse(id, CultureInfo.InvariantCulture),
|
||||
searchInfo.MetadataLanguage,
|
||||
TmdbUtils.GetImageLanguagesParam(searchInfo.MetadataLanguage),
|
||||
TmdbUtils.GetImageLanguagesParam(searchInfo.MetadataLanguage, searchInfo.MetadataCountryCode),
|
||||
searchInfo.MetadataCountryCode,
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
@@ -93,7 +94,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
|
||||
var result = await _tmdbClientManager.FindByExternalIdAsync(
|
||||
id,
|
||||
FindExternalSource.Imdb,
|
||||
TmdbUtils.GetImageLanguagesParam(searchInfo.MetadataLanguage),
|
||||
TmdbUtils.GetImageLanguagesParam(searchInfo.MetadataLanguage, searchInfo.MetadataCountryCode),
|
||||
searchInfo.MetadataCountryCode,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
movieResults = result?.MovieResults;
|
||||
}
|
||||
@@ -103,7 +105,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
|
||||
var result = await _tmdbClientManager.FindByExternalIdAsync(
|
||||
id,
|
||||
FindExternalSource.TvDb,
|
||||
TmdbUtils.GetImageLanguagesParam(searchInfo.MetadataLanguage),
|
||||
TmdbUtils.GetImageLanguagesParam(searchInfo.MetadataLanguage, searchInfo.MetadataCountryCode),
|
||||
searchInfo.MetadataCountryCode,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
movieResults = result?.MovieResults;
|
||||
}
|
||||
@@ -111,7 +114,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
|
||||
if (movieResults is null)
|
||||
{
|
||||
movieResults = await _tmdbClientManager
|
||||
.SearchMovieAsync(searchInfo.Name, searchInfo.Year ?? 0, searchInfo.MetadataLanguage, cancellationToken)
|
||||
.SearchMovieAsync(searchInfo.Name, searchInfo.Year ?? 0, searchInfo.MetadataLanguage, searchInfo.MetadataCountryCode, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -152,7 +155,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
|
||||
// Caller provides the filename with extension stripped and NOT the parsed filename
|
||||
var parsedName = _libraryManager.ParseName(info.Name);
|
||||
var cleanedName = TmdbUtils.CleanName(parsedName.Name);
|
||||
var searchResults = await _tmdbClientManager.SearchMovieAsync(cleanedName, info.Year ?? parsedName.Year ?? 0, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var searchResults = await _tmdbClientManager.SearchMovieAsync(cleanedName, info.Year ?? parsedName.Year ?? 0, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (searchResults.Count > 0)
|
||||
{
|
||||
@@ -162,7 +166,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
|
||||
|
||||
if (string.IsNullOrEmpty(tmdbId) && !string.IsNullOrEmpty(imdbId))
|
||||
{
|
||||
var movieResultFromImdbId = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
|
||||
var movieResultFromImdbId = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
|
||||
if (movieResultFromImdbId?.MovieResults.Count > 0)
|
||||
{
|
||||
tmdbId = movieResultFromImdbId.MovieResults[0].Id.ToString(CultureInfo.InvariantCulture);
|
||||
@@ -175,7 +179,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
|
||||
}
|
||||
|
||||
var movieResult = await _tmdbClientManager
|
||||
.GetMovieAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
|
||||
.GetMovieAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage, info.MetadataCountryCode), info.MetadataCountryCode, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (movieResult is null)
|
||||
|
||||
@@ -60,7 +60,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
|
||||
}
|
||||
|
||||
var language = item.GetPreferredMetadataLanguage();
|
||||
var personResult = await _tmdbClientManager.GetPersonAsync(int.Parse(personTmdbId, CultureInfo.InvariantCulture), language, cancellationToken).ConfigureAwait(false);
|
||||
var countryCode = item.GetPreferredMetadataCountryCode();
|
||||
var personResult = await _tmdbClientManager.GetPersonAsync(int.Parse(personTmdbId, CultureInfo.InvariantCulture), language, countryCode, cancellationToken).ConfigureAwait(false);
|
||||
if (personResult?.Images?.Profiles is null)
|
||||
{
|
||||
return Enumerable.Empty<RemoteImageInfo>();
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
|
||||
{
|
||||
if (searchInfo.TryGetProviderId(MetadataProvider.Tmdb, out var personTmdbId))
|
||||
{
|
||||
var personResult = await _tmdbClientManager.GetPersonAsync(int.Parse(personTmdbId, CultureInfo.InvariantCulture), searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false);
|
||||
var personResult = await _tmdbClientManager.GetPersonAsync(int.Parse(personTmdbId, CultureInfo.InvariantCulture), searchInfo.MetadataLanguage, searchInfo.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (personResult is not null)
|
||||
{
|
||||
@@ -101,7 +101,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
|
||||
|
||||
if (personTmdbId > 0)
|
||||
{
|
||||
var person = await _tmdbClientManager.GetPersonAsync(personTmdbId, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
|
||||
var person = await _tmdbClientManager.GetPersonAsync(personTmdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
|
||||
if (person is null)
|
||||
{
|
||||
return result;
|
||||
|
||||
@@ -75,7 +75,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
|
||||
// TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here
|
||||
var episodeResult = await _tmdbClientManager
|
||||
.GetEpisodeAsync(seriesTmdbId, seasonNumber, episodeNumber.Value, series.DisplayOrder, null, null, cancellationToken)
|
||||
.GetEpisodeAsync(seriesTmdbId, seasonNumber, episodeNumber.Value, series.DisplayOrder, null, null, null, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var stills = episodeResult?.Images?.Stills;
|
||||
|
||||
@@ -113,7 +113,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
List<TvEpisode>? result = null;
|
||||
for (int? episode = startindex; episode <= endindex; episode++)
|
||||
{
|
||||
var episodeInfo = await _tmdbClientManager.GetEpisodeAsync(seriesTmdbId, seasonNumber, episode.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken).ConfigureAwait(false);
|
||||
var episodeInfo = await _tmdbClientManager.GetEpisodeAsync(seriesTmdbId, seasonNumber, episode.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage, info.MetadataCountryCode), info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
|
||||
if (episodeInfo is not null)
|
||||
{
|
||||
(result ??= new List<TvEpisode>()).Add(episodeInfo);
|
||||
@@ -157,7 +157,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
else
|
||||
{
|
||||
episodeResult = await _tmdbClientManager
|
||||
.GetEpisodeAsync(seriesTmdbId, seasonNumber, episodeNumber.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
|
||||
.GetEpisodeAsync(seriesTmdbId, seasonNumber, episodeNumber.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage, info.MetadataCountryCode), info.MetadataCountryCode, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -68,7 +68,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
|
||||
// TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here
|
||||
var seasonResult = await _tmdbClientManager
|
||||
.GetSeasonAsync(seriesTmdbId, season.IndexNumber.Value, null, null, cancellationToken)
|
||||
.GetSeasonAsync(seriesTmdbId, season.IndexNumber.Value, null, null, null, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var posters = seasonResult?.Images?.Posters;
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
}
|
||||
|
||||
var seasonResult = await _tmdbClientManager
|
||||
.GetSeasonAsync(Convert.ToInt32(seriesTmdbId, CultureInfo.InvariantCulture), seasonNumber.Value, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
|
||||
.GetSeasonAsync(Convert.ToInt32(seriesTmdbId, CultureInfo.InvariantCulture), seasonNumber.Value, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage, info.MetadataCountryCode), info.MetadataCountryCode, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (seasonResult is null)
|
||||
|
||||
@@ -68,7 +68,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
|
||||
// TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here
|
||||
var series = await _tmdbClientManager
|
||||
.GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), null, null, cancellationToken)
|
||||
.GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), null, null, null, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (series?.Images is null)
|
||||
|
||||
@@ -57,7 +57,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
if (searchInfo.TryGetProviderId(MetadataProvider.Tmdb, out var tmdbId))
|
||||
{
|
||||
var series = await _tmdbClientManager
|
||||
.GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), searchInfo.MetadataLanguage, searchInfo.MetadataLanguage, cancellationToken)
|
||||
.GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), searchInfo.MetadataLanguage, searchInfo.MetadataLanguage, searchInfo.MetadataCountryCode, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (series is not null)
|
||||
@@ -71,7 +71,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
if (searchInfo.TryGetProviderId(MetadataProvider.Imdb, out var imdbId))
|
||||
{
|
||||
var findResult = await _tmdbClientManager
|
||||
.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, searchInfo.MetadataLanguage, cancellationToken)
|
||||
.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, searchInfo.MetadataLanguage, searchInfo.MetadataCountryCode, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var tvResults = findResult?.TvResults;
|
||||
@@ -92,7 +92,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
if (searchInfo.TryGetProviderId(MetadataProvider.Tvdb, out var tvdbId))
|
||||
{
|
||||
var findResult = await _tmdbClientManager
|
||||
.FindByExternalIdAsync(tvdbId, FindExternalSource.TvDb, searchInfo.MetadataLanguage, cancellationToken)
|
||||
.FindByExternalIdAsync(tvdbId, FindExternalSource.TvDb, searchInfo.MetadataLanguage, searchInfo.MetadataCountryCode, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var tvResults = findResult?.TvResults;
|
||||
@@ -110,7 +110,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
}
|
||||
}
|
||||
|
||||
var tvSearchResults = await _tmdbClientManager.SearchSeriesAsync(searchInfo.Name, searchInfo.MetadataLanguage, cancellationToken: cancellationToken)
|
||||
var tvSearchResults = await _tmdbClientManager.SearchSeriesAsync(searchInfo.Name, searchInfo.MetadataLanguage, searchInfo.MetadataCountryCode, cancellationToken: cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var remoteResults = new RemoteSearchResult[tvSearchResults.Count];
|
||||
@@ -173,7 +173,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
|
||||
if (string.IsNullOrEmpty(tmdbId) && info.TryGetProviderId(MetadataProvider.Imdb, out var imdbId))
|
||||
{
|
||||
var searchResult = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
|
||||
var searchResult = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
|
||||
if (searchResult?.TvResults.Count > 0)
|
||||
{
|
||||
tmdbId = searchResult.TvResults[0].Id.ToString(CultureInfo.InvariantCulture);
|
||||
@@ -182,7 +182,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
|
||||
if (string.IsNullOrEmpty(tmdbId) && info.TryGetProviderId(MetadataProvider.Tvdb, out var tvdbId))
|
||||
{
|
||||
var searchResult = await _tmdbClientManager.FindByExternalIdAsync(tvdbId, FindExternalSource.TvDb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
|
||||
var searchResult = await _tmdbClientManager.FindByExternalIdAsync(tvdbId, FindExternalSource.TvDb, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
|
||||
if (searchResult?.TvResults.Count > 0)
|
||||
{
|
||||
tmdbId = searchResult.TvResults[0].Id.ToString(CultureInfo.InvariantCulture);
|
||||
@@ -196,7 +196,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
// Caller provides the filename with extension stripped and NOT the parsed filename
|
||||
var parsedName = _libraryManager.ParseName(info.Name);
|
||||
var cleanedName = TmdbUtils.CleanName(parsedName.Name);
|
||||
var searchResults = await _tmdbClientManager.SearchSeriesAsync(cleanedName, info.MetadataLanguage, info.Year ?? parsedName.Year ?? 0, cancellationToken).ConfigureAwait(false);
|
||||
var searchResults = await _tmdbClientManager.SearchSeriesAsync(cleanedName, info.MetadataLanguage, info.MetadataCountryCode, info.Year ?? parsedName.Year ?? 0, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (searchResults.Count > 0)
|
||||
{
|
||||
@@ -212,7 +212,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var tvShow = await _tmdbClientManager
|
||||
.GetSeriesAsync(tmdbIdInt, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
|
||||
.GetSeriesAsync(tmdbIdInt, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage, info.MetadataCountryCode), info.MetadataCountryCode, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (tvShow is null)
|
||||
|
||||
@@ -51,9 +51,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
/// <param name="tmdbId">The movie's TMDb id.</param>
|
||||
/// <param name="language">The movie's language.</param>
|
||||
/// <param name="imageLanguages">A comma-separated list of image languages.</param>
|
||||
/// <param name="countryCode">The country code, ISO 3166-1.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The TMDb movie or null if not found.</returns>
|
||||
public async Task<Movie?> GetMovieAsync(int tmdbId, string? language, string? imageLanguages, CancellationToken cancellationToken)
|
||||
public async Task<Movie?> GetMovieAsync(int tmdbId, string? language, string? imageLanguages, string? countryCode, CancellationToken cancellationToken)
|
||||
{
|
||||
var key = $"movie-{tmdbId.ToString(CultureInfo.InvariantCulture)}-{language}";
|
||||
if (_memoryCache.TryGetValue(key, out Movie? movie))
|
||||
@@ -71,7 +72,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
|
||||
movie = await _tmDbClient.GetMovieAsync(
|
||||
tmdbId,
|
||||
TmdbUtils.NormalizeLanguage(language),
|
||||
TmdbUtils.NormalizeLanguage(language, countryCode),
|
||||
imageLanguages,
|
||||
extraMethods,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
@@ -90,9 +91,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
/// <param name="tmdbId">The collection's TMDb id.</param>
|
||||
/// <param name="language">The collection's language.</param>
|
||||
/// <param name="imageLanguages">A comma-separated list of image languages.</param>
|
||||
/// <param name="countryCode">The country code, ISO 3166-1.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The TMDb collection or null if not found.</returns>
|
||||
public async Task<Collection?> GetCollectionAsync(int tmdbId, string? language, string? imageLanguages, CancellationToken cancellationToken)
|
||||
public async Task<Collection?> GetCollectionAsync(int tmdbId, string? language, string? imageLanguages, string? countryCode, CancellationToken cancellationToken)
|
||||
{
|
||||
var key = $"collection-{tmdbId.ToString(CultureInfo.InvariantCulture)}-{language}";
|
||||
if (_memoryCache.TryGetValue(key, out Collection? collection))
|
||||
@@ -104,7 +106,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
|
||||
collection = await _tmDbClient.GetCollectionAsync(
|
||||
tmdbId,
|
||||
TmdbUtils.NormalizeLanguage(language),
|
||||
TmdbUtils.NormalizeLanguage(language, countryCode),
|
||||
imageLanguages,
|
||||
CollectionMethods.Images,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
@@ -123,9 +125,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
/// <param name="tmdbId">The tv show's TMDb id.</param>
|
||||
/// <param name="language">The tv show's language.</param>
|
||||
/// <param name="imageLanguages">A comma-separated list of image languages.</param>
|
||||
/// <param name="countryCode">The country code, ISO 3166-1.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The TMDb tv show information or null if not found.</returns>
|
||||
public async Task<TvShow?> GetSeriesAsync(int tmdbId, string? language, string? imageLanguages, CancellationToken cancellationToken)
|
||||
public async Task<TvShow?> GetSeriesAsync(int tmdbId, string? language, string? imageLanguages, string? countryCode, CancellationToken cancellationToken)
|
||||
{
|
||||
var key = $"series-{tmdbId.ToString(CultureInfo.InvariantCulture)}-{language}";
|
||||
if (_memoryCache.TryGetValue(key, out TvShow? series))
|
||||
@@ -143,7 +146,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
|
||||
series = await _tmDbClient.GetTvShowAsync(
|
||||
tmdbId,
|
||||
language: TmdbUtils.NormalizeLanguage(language),
|
||||
language: TmdbUtils.NormalizeLanguage(language, countryCode),
|
||||
includeImageLanguage: imageLanguages,
|
||||
extraMethods: extraMethods,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
@@ -163,9 +166,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
/// <param name="displayOrder">The display order.</param>
|
||||
/// <param name="language">The tv show's language.</param>
|
||||
/// <param name="imageLanguages">A comma-separated list of image languages.</param>
|
||||
/// <param name="countryCode">The country code, ISO 3166-1.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The TMDb tv show episode group information or null if not found.</returns>
|
||||
private async Task<TvGroupCollection?> GetSeriesGroupAsync(int tvShowId, string displayOrder, string? language, string? imageLanguages, CancellationToken cancellationToken)
|
||||
private async Task<TvGroupCollection?> GetSeriesGroupAsync(int tvShowId, string displayOrder, string? language, string? imageLanguages, string? countryCode, CancellationToken cancellationToken)
|
||||
{
|
||||
TvGroupType? groupType =
|
||||
string.Equals(displayOrder, "originalAirDate", StringComparison.Ordinal) ? TvGroupType.OriginalAirDate :
|
||||
@@ -190,7 +194,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
|
||||
await EnsureClientConfigAsync().ConfigureAwait(false);
|
||||
|
||||
var series = await GetSeriesAsync(tvShowId, language, imageLanguages, cancellationToken).ConfigureAwait(false);
|
||||
var series = await GetSeriesAsync(tvShowId, language, imageLanguages, countryCode, cancellationToken).ConfigureAwait(false);
|
||||
var episodeGroupId = series?.EpisodeGroups.Results.Find(g => g.Type == groupType)?.Id;
|
||||
|
||||
if (episodeGroupId is null)
|
||||
@@ -200,7 +204,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
|
||||
group = await _tmDbClient.GetTvEpisodeGroupsAsync(
|
||||
episodeGroupId,
|
||||
language: TmdbUtils.NormalizeLanguage(language),
|
||||
language: TmdbUtils.NormalizeLanguage(language, countryCode),
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (group is not null)
|
||||
@@ -218,9 +222,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
/// <param name="seasonNumber">The season number.</param>
|
||||
/// <param name="language">The tv season's language.</param>
|
||||
/// <param name="imageLanguages">A comma-separated list of image languages.</param>
|
||||
/// <param name="countryCode">The country code, ISO 3166-1.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The TMDb tv season information or null if not found.</returns>
|
||||
public async Task<TvSeason?> GetSeasonAsync(int tvShowId, int seasonNumber, string? language, string? imageLanguages, CancellationToken cancellationToken)
|
||||
public async Task<TvSeason?> GetSeasonAsync(int tvShowId, int seasonNumber, string? language, string? imageLanguages, string? countryCode, CancellationToken cancellationToken)
|
||||
{
|
||||
var key = $"season-{tvShowId.ToString(CultureInfo.InvariantCulture)}-s{seasonNumber.ToString(CultureInfo.InvariantCulture)}-{language}";
|
||||
if (_memoryCache.TryGetValue(key, out TvSeason? season))
|
||||
@@ -233,7 +238,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
season = await _tmDbClient.GetTvSeasonAsync(
|
||||
tvShowId,
|
||||
seasonNumber,
|
||||
language: TmdbUtils.NormalizeLanguage(language),
|
||||
language: TmdbUtils.NormalizeLanguage(language, countryCode),
|
||||
includeImageLanguage: imageLanguages,
|
||||
extraMethods: TvSeasonMethods.Credits | TvSeasonMethods.Images | TvSeasonMethods.ExternalIds | TvSeasonMethods.Videos,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
@@ -255,9 +260,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
/// <param name="displayOrder">The display order.</param>
|
||||
/// <param name="language">The episode's language.</param>
|
||||
/// <param name="imageLanguages">A comma-separated list of image languages.</param>
|
||||
/// <param name="countryCode">The country code, ISO 3166-1.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The TMDb tv episode information or null if not found.</returns>
|
||||
public async Task<TvEpisode?> GetEpisodeAsync(int tvShowId, int seasonNumber, int episodeNumber, string displayOrder, string? language, string? imageLanguages, CancellationToken cancellationToken)
|
||||
public async Task<TvEpisode?> GetEpisodeAsync(int tvShowId, int seasonNumber, int episodeNumber, string displayOrder, string? language, string? imageLanguages, string? countryCode, CancellationToken cancellationToken)
|
||||
{
|
||||
var key = $"episode-{tvShowId.ToString(CultureInfo.InvariantCulture)}-s{seasonNumber.ToString(CultureInfo.InvariantCulture)}e{episodeNumber.ToString(CultureInfo.InvariantCulture)}-{displayOrder}-{language}";
|
||||
if (_memoryCache.TryGetValue(key, out TvEpisode? episode))
|
||||
@@ -267,7 +273,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
|
||||
await EnsureClientConfigAsync().ConfigureAwait(false);
|
||||
|
||||
var group = await GetSeriesGroupAsync(tvShowId, displayOrder, language, imageLanguages, cancellationToken).ConfigureAwait(false);
|
||||
var group = await GetSeriesGroupAsync(tvShowId, displayOrder, language, imageLanguages, countryCode, cancellationToken).ConfigureAwait(false);
|
||||
if (group is not null)
|
||||
{
|
||||
var season = group.Groups.Find(s => s.Order == seasonNumber);
|
||||
@@ -284,7 +290,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
tvShowId,
|
||||
seasonNumber,
|
||||
episodeNumber,
|
||||
language: TmdbUtils.NormalizeLanguage(language),
|
||||
language: TmdbUtils.NormalizeLanguage(language, countryCode),
|
||||
includeImageLanguage: imageLanguages,
|
||||
extraMethods: TvEpisodeMethods.Credits | TvEpisodeMethods.Images | TvEpisodeMethods.ExternalIds | TvEpisodeMethods.Videos,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
@@ -301,10 +307,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
/// Gets a person eg. cast or crew member from the TMDb API based on its TMDb id.
|
||||
/// </summary>
|
||||
/// <param name="personTmdbId">The person's TMDb id.</param>
|
||||
/// <param name="language">The episode's language.</param>
|
||||
/// <param name="language">The person's language.</param>
|
||||
/// <param name="countryCode">The country code, ISO 3166-1.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The TMDb person information or null if not found.</returns>
|
||||
public async Task<Person?> GetPersonAsync(int personTmdbId, string language, CancellationToken cancellationToken)
|
||||
public async Task<Person?> GetPersonAsync(int personTmdbId, string language, string? countryCode, CancellationToken cancellationToken)
|
||||
{
|
||||
var key = $"person-{personTmdbId.ToString(CultureInfo.InvariantCulture)}-{language}";
|
||||
if (_memoryCache.TryGetValue(key, out Person? person))
|
||||
@@ -316,7 +323,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
|
||||
person = await _tmDbClient.GetPersonAsync(
|
||||
personTmdbId,
|
||||
TmdbUtils.NormalizeLanguage(language),
|
||||
TmdbUtils.NormalizeLanguage(language, countryCode),
|
||||
PersonMethods.TvCredits | PersonMethods.MovieCredits | PersonMethods.Images | PersonMethods.ExternalIds,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -334,12 +341,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
/// <param name="externalId">The item's external id.</param>
|
||||
/// <param name="source">The source of the id eg. IMDb.</param>
|
||||
/// <param name="language">The item's language.</param>
|
||||
/// <param name="countryCode">The country code, ISO 3166-1.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The TMDb item or null if not found.</returns>
|
||||
public async Task<FindContainer?> FindByExternalIdAsync(
|
||||
string externalId,
|
||||
FindExternalSource source,
|
||||
string language,
|
||||
string? countryCode,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var key = $"find-{source.ToString()}-{externalId.ToString(CultureInfo.InvariantCulture)}-{language}";
|
||||
@@ -353,7 +362,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
result = await _tmDbClient.FindAsync(
|
||||
source,
|
||||
externalId,
|
||||
TmdbUtils.NormalizeLanguage(language),
|
||||
TmdbUtils.NormalizeLanguage(language, countryCode),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (result is not null)
|
||||
@@ -369,10 +378,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the tv show.</param>
|
||||
/// <param name="language">The tv show's language.</param>
|
||||
/// <param name="countryCode">The country code, ISO 3166-1.</param>
|
||||
/// <param name="year">The year the tv show first aired.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The TMDb tv show information.</returns>
|
||||
public async Task<IReadOnlyList<SearchTv>> SearchSeriesAsync(string name, string language, int year = 0, CancellationToken cancellationToken = default)
|
||||
public async Task<IReadOnlyList<SearchTv>> SearchSeriesAsync(string name, string language, string? countryCode, int year = 0, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var key = $"searchseries-{name}-{year.ToString(CultureInfo.InvariantCulture)}-{language}";
|
||||
if (_memoryCache.TryGetValue(key, out SearchContainer<SearchTv>? series) && series is not null)
|
||||
@@ -383,7 +393,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
await EnsureClientConfigAsync().ConfigureAwait(false);
|
||||
|
||||
var searchResults = await _tmDbClient
|
||||
.SearchTvShowAsync(name, TmdbUtils.NormalizeLanguage(language), includeAdult: Plugin.Instance.Configuration.IncludeAdult, firstAirDateYear: year, cancellationToken: cancellationToken)
|
||||
.SearchTvShowAsync(name, TmdbUtils.NormalizeLanguage(language, countryCode), includeAdult: Plugin.Instance.Configuration.IncludeAdult, firstAirDateYear: year, cancellationToken: cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (searchResults.Results.Count > 0)
|
||||
@@ -431,7 +441,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
/// <returns>The TMDb movie information.</returns>
|
||||
public Task<IReadOnlyList<SearchMovie>> SearchMovieAsync(string name, string language, CancellationToken cancellationToken)
|
||||
{
|
||||
return SearchMovieAsync(name, 0, language, cancellationToken);
|
||||
return SearchMovieAsync(name, 0, language, null, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -440,9 +450,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
/// <param name="name">The name of the movie.</param>
|
||||
/// <param name="year">The release year of the movie.</param>
|
||||
/// <param name="language">The movie's language.</param>
|
||||
/// <param name="countryCode">The country code, ISO 3166-1.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The TMDb movie information.</returns>
|
||||
public async Task<IReadOnlyList<SearchMovie>> SearchMovieAsync(string name, int year, string language, CancellationToken cancellationToken)
|
||||
public async Task<IReadOnlyList<SearchMovie>> SearchMovieAsync(string name, int year, string language, string? countryCode, CancellationToken cancellationToken)
|
||||
{
|
||||
var key = $"moviesearch-{name}-{year.ToString(CultureInfo.InvariantCulture)}-{language}";
|
||||
if (_memoryCache.TryGetValue(key, out SearchContainer<SearchMovie>? movies) && movies is not null)
|
||||
@@ -453,7 +464,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
await EnsureClientConfigAsync().ConfigureAwait(false);
|
||||
|
||||
var searchResults = await _tmDbClient
|
||||
.SearchMovieAsync(name, TmdbUtils.NormalizeLanguage(language), includeAdult: Plugin.Instance.Configuration.IncludeAdult, year: year, cancellationToken: cancellationToken)
|
||||
.SearchMovieAsync(name, TmdbUtils.NormalizeLanguage(language, countryCode), includeAdult: Plugin.Instance.Configuration.IncludeAdult, year: year, cancellationToken: cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (searchResults.Results.Count > 0)
|
||||
@@ -469,9 +480,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the collection.</param>
|
||||
/// <param name="language">The collection's language.</param>
|
||||
/// <param name="countryCode">The country code, ISO 3166-1.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The TMDb collection information.</returns>
|
||||
public async Task<IReadOnlyList<SearchCollection>> SearchCollectionAsync(string name, string language, CancellationToken cancellationToken)
|
||||
public async Task<IReadOnlyList<SearchCollection>> SearchCollectionAsync(string name, string language, string? countryCode, CancellationToken cancellationToken)
|
||||
{
|
||||
var key = $"collectionsearch-{name}-{language}";
|
||||
if (_memoryCache.TryGetValue(key, out SearchContainer<SearchCollection>? collections) && collections is not null)
|
||||
@@ -482,7 +494,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
await EnsureClientConfigAsync().ConfigureAwait(false);
|
||||
|
||||
var searchResults = await _tmDbClient
|
||||
.SearchCollectionAsync(name, TmdbUtils.NormalizeLanguage(language), cancellationToken: cancellationToken)
|
||||
.SearchCollectionAsync(name, TmdbUtils.NormalizeLanguage(language, countryCode), cancellationToken: cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (searchResults.Results.Count > 0)
|
||||
|
||||
@@ -105,14 +105,15 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
/// Normalizes a language string for use with TMDb's include image language parameter.
|
||||
/// </summary>
|
||||
/// <param name="preferredLanguage">The preferred language as either a 2 letter code with or without country code.</param>
|
||||
/// <param name="countryCode">The country code, ISO 3166-1.</param>
|
||||
/// <returns>The comma separated language string.</returns>
|
||||
public static string GetImageLanguagesParam(string preferredLanguage)
|
||||
public static string GetImageLanguagesParam(string preferredLanguage, string? countryCode = null)
|
||||
{
|
||||
var languages = new List<string>();
|
||||
|
||||
if (!string.IsNullOrEmpty(preferredLanguage))
|
||||
{
|
||||
preferredLanguage = NormalizeLanguage(preferredLanguage);
|
||||
preferredLanguage = NormalizeLanguage(preferredLanguage, countryCode);
|
||||
|
||||
languages.Add(preferredLanguage);
|
||||
|
||||
@@ -140,15 +141,24 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
/// Normalizes a language string for use with TMDb's language parameter.
|
||||
/// </summary>
|
||||
/// <param name="language">The language code.</param>
|
||||
/// <param name="countryCode">The country code.</param>
|
||||
/// <returns>The normalized language code.</returns>
|
||||
[return: NotNullIfNotNull(nameof(language))]
|
||||
public static string? NormalizeLanguage(string? language)
|
||||
public static string? NormalizeLanguage(string? language, string? countryCode = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(language))
|
||||
{
|
||||
return language;
|
||||
}
|
||||
|
||||
// Handle es-419 (Latin American Spanish) by converting to regional variant
|
||||
if (string.Equals(language, "es-419", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(countryCode))
|
||||
{
|
||||
language = string.Equals(countryCode, "AR", StringComparison.OrdinalIgnoreCase)
|
||||
? "es-AR"
|
||||
: "es-MX";
|
||||
}
|
||||
|
||||
// TMDb requires this to be uppercase
|
||||
// Everything after the hyphen must be written in uppercase due to a way TMDb wrote their API.
|
||||
// See here: https://www.themoviedb.org/talk/5119221d760ee36c642af4ad?page=3#56e372a0c3a3685a9e0019ab
|
||||
@@ -185,7 +195,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
return requestLanguage;
|
||||
}
|
||||
|
||||
return imageLanguage;
|
||||
// TMDb now returns xx for no language instead of an empty string.
|
||||
return string.Equals(imageLanguage, "xx", StringComparison.OrdinalIgnoreCase)
|
||||
? string.Empty
|
||||
: imageLanguage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -107,6 +107,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
||||
// Additional Mappings
|
||||
_validProviderIds.Add("collectionnumber", "TmdbCollection");
|
||||
_validProviderIds.Add("tmdbcolid", "TmdbCollection");
|
||||
_validProviderIds.Add("tmdbcol", "TmdbCollection");
|
||||
_validProviderIds.Add("imdb_id", "Imdb");
|
||||
|
||||
Fetch(item, metadataFile, GetXmlReaderSettings(), cancellationToken);
|
||||
@@ -315,7 +316,11 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
||||
if (userData is not null)
|
||||
{
|
||||
userData.Played = played;
|
||||
_userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
|
||||
|
||||
if (!item.Id.IsEmpty())
|
||||
{
|
||||
_userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -332,7 +337,11 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
||||
if (userData is not null)
|
||||
{
|
||||
userData.PlayCount = count;
|
||||
_userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
|
||||
|
||||
if (!item.Id.IsEmpty())
|
||||
{
|
||||
_userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -349,7 +358,11 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
||||
if (userData is not null)
|
||||
{
|
||||
userData.LastPlayedDate = lastPlayed;
|
||||
_userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
|
||||
|
||||
if (!item.Id.IsEmpty())
|
||||
{
|
||||
_userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -590,7 +603,18 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
||||
|
||||
var provider = reader.GetAttribute("type");
|
||||
var providerId = reader.ReadElementContentAsString();
|
||||
item.TrySetProviderId(provider, providerId);
|
||||
|
||||
if (!string.IsNullOrEmpty(provider))
|
||||
{
|
||||
if (_validProviderIds.TryGetValue(provider, out string? normalizedProvider))
|
||||
{
|
||||
item.TrySetProviderId(normalizedProvider, providerId);
|
||||
}
|
||||
else
|
||||
{
|
||||
item.TrySetProviderId(provider, providerId);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case "thumb":
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Reflection;
|
||||
|
||||
[assembly: AssemblyVersion("10.11.0")]
|
||||
[assembly: AssemblyFileVersion("10.11.0")]
|
||||
[assembly: AssemblyVersion("10.11.2")]
|
||||
[assembly: AssemblyFileVersion("10.11.2")]
|
||||
|
||||
@@ -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 .
|
||||
|
||||
@@ -7,24 +7,6 @@
|
||||
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
|
||||
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Stability)'=='Unstable'">
|
||||
<!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. -->
|
||||
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.CodeAnalysis</PackageId>
|
||||
<VersionPrefix>10.11.0</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -53,6 +53,34 @@ public static class JellyfinQueryHelperExtensions
|
||||
return baseQuery.Where(ReferencedItemFilterExpressionBuilder(context, itemValueType, referenceIds, invert));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a query that checks referenced ItemValues for a cross BaseItem lookup.
|
||||
/// </summary>
|
||||
/// <param name="baseQuery">The source query.</param>
|
||||
/// <param name="context">The database context.</param>
|
||||
/// <param name="itemValueTypes">The type of item value to reference.</param>
|
||||
/// <param name="referenceIds">The list of BaseItem ids to check matches.</param>
|
||||
/// <param name="invert">If set an exclusion check is performed instead.</param>
|
||||
/// <returns>A Query.</returns>
|
||||
public static IQueryable<BaseItemEntity> WhereReferencedItemMultipleTypes(
|
||||
this IQueryable<BaseItemEntity> baseQuery,
|
||||
JellyfinDbContext context,
|
||||
IList<ItemValueType> itemValueTypes,
|
||||
IList<Guid> referenceIds,
|
||||
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 =>
|
||||
context.BaseItems.Where(itemFilter).Any(e => e.CleanName == val.itemVal.CleanValue)
|
||||
&& val.map.ItemId == item.Id) == EF.Constant(!invert));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a query expression that checks referenced ItemValues for a cross BaseItem lookup.
|
||||
/// </summary>
|
||||
|
||||
@@ -52,10 +52,14 @@ public class OptimisticLockBehavior : IEntityFrameworkCoreLockingBehavior
|
||||
|
||||
_logger = logger;
|
||||
_writePolicy = Policy
|
||||
.HandleInner<Exception>(e => e.Message.Contains("database is locked", StringComparison.InvariantCultureIgnoreCase))
|
||||
.HandleInner<Exception>(e =>
|
||||
e.Message.Contains("database is locked", StringComparison.InvariantCultureIgnoreCase) ||
|
||||
e.Message.Contains("database table is locked", StringComparison.InvariantCultureIgnoreCase))
|
||||
.WaitAndRetry(sleepDurations.Length, backoffProvider, RetryHandle);
|
||||
_writeAsyncPolicy = Policy
|
||||
.HandleInner<Exception>(e => e.Message.Contains("database is locked", StringComparison.InvariantCultureIgnoreCase))
|
||||
.HandleInner<Exception>(e =>
|
||||
e.Message.Contains("database is locked", StringComparison.InvariantCultureIgnoreCase) ||
|
||||
e.Message.Contains("database table is locked", StringComparison.InvariantCultureIgnoreCase))
|
||||
.WaitAndRetryAsync(sleepDurations.Length, backoffProvider, RetryHandle);
|
||||
|
||||
void RetryHandle(Exception exception, TimeSpan timespan, int retryNo, Context context)
|
||||
|
||||
@@ -15,22 +15,22 @@ DELETE FROM BaseItems
|
||||
WHERE
|
||||
ParentId IS NOT NULL
|
||||
AND
|
||||
NOT EXISTS(SELECT 1 FROM BaseItems parent WHERE ParentId = parent.Id);
|
||||
NOT EXISTS(SELECT 1 FROM BaseItems parent WHERE parent.Id = BaseItems.ParentId);
|
||||
DELETE FROM BaseItems
|
||||
WHERE
|
||||
ParentId IS NOT NULL
|
||||
AND
|
||||
NOT EXISTS(SELECT 1 FROM BaseItems parent WHERE ParentId = parent.Id);
|
||||
NOT EXISTS(SELECT 1 FROM BaseItems parent WHERE parent.Id = BaseItems.ParentId);
|
||||
DELETE FROM BaseItems
|
||||
WHERE
|
||||
ParentId IS NOT NULL
|
||||
AND
|
||||
NOT EXISTS(SELECT 1 FROM BaseItems parent WHERE ParentId = parent.Id);
|
||||
NOT EXISTS(SELECT 1 FROM BaseItems parent WHERE parent.Id = BaseItems.ParentId);
|
||||
DELETE FROM BaseItems
|
||||
WHERE
|
||||
ParentId IS NOT NULL
|
||||
AND
|
||||
NOT EXISTS(SELECT 1 FROM BaseItems parent WHERE ParentId = parent.Id);
|
||||
NOT EXISTS(SELECT 1 FROM BaseItems parent WHERE parent.Id = BaseItems.ParentId);
|
||||
""");
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_BaseItems_BaseItems_ParentId",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Extensions</PackageId>
|
||||
<VersionPrefix>10.11.0</VersionPrefix>
|
||||
<VersionPrefix>10.11.2</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,14 @@ 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)]
|
||||
[InlineData("/media/YouTube/Devyn Johnston/2024-01-24 4070 Ti SUPER in under 7 minutes", "/media/YouTube/Devyn Johnston", null, false)]
|
||||
[InlineData("/media/YouTube/Devyn Johnston/2025-01-28 5090 vs 2 SFF Cases", "/media/YouTube/Devyn Johnston", null, false)]
|
||||
[InlineData("/Drive/202401244070", "/Drive", null, false)]
|
||||
public void GetSeasonNumberFromPathTest(string path, string? parentPath, int? seasonNumber, bool isSeasonDirectory)
|
||||
{
|
||||
var result = SeasonPathParser.Parse(path, parentPath, true, true);
|
||||
|
||||
@@ -29,6 +29,7 @@ namespace Jellyfin.Providers.Tests.Tmdb
|
||||
[InlineData("fr-CA", "fr-BE", "fr-CA")]
|
||||
[InlineData("fr-CA", "fr", "fr-CA")]
|
||||
[InlineData("de", "en-US", "de")]
|
||||
[InlineData("", "en-US", "")]
|
||||
public static void AdjustImageLanguage_Valid_Success(string imageLanguage, string requestLanguage, string? expected)
|
||||
{
|
||||
Assert.Equal(expected, TmdbUtils.AdjustImageLanguage(imageLanguage, requestLanguage));
|
||||
|
||||
@@ -275,5 +275,24 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
|
||||
Assert.StartsWith(">>", item.Overview, StringComparison.InvariantCulture);
|
||||
Assert.EndsWith("<<", item.Overview, StringComparison.InvariantCulture);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_TmdbcolUniqueId_NormalizedToTmdbCollection()
|
||||
{
|
||||
var result = new MetadataResult<Video>()
|
||||
{
|
||||
Item = new Movie()
|
||||
};
|
||||
|
||||
_parser.Fetch(result, "Test Data/Lilo & Stitch.nfo", CancellationToken.None);
|
||||
var item = (Movie)result.Item;
|
||||
|
||||
// Verify that <uniqueid type="tmdbcol"> is normalized to TmdbCollection
|
||||
Assert.True(item.ProviderIds.ContainsKey(MetadataProvider.TmdbCollection.ToString()));
|
||||
Assert.Equal("97020", item.ProviderIds[MetadataProvider.TmdbCollection.ToString()]);
|
||||
|
||||
// Verify that the lowercase "tmdbcol" is NOT in the provider IDs
|
||||
Assert.False(item.ProviderIds.ContainsKey("tmdbcol"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<movie>
|
||||
<title>Lilo & Stitch</title>
|
||||
<originaltitle>Lilo & Stitch</originaltitle>
|
||||
<uniqueid type="tmdbcol" default="false">97020</uniqueid>
|
||||
<set>Lilo & Stitch Collection</set>
|
||||
<plot>>>As Stitch, a runaway genetic experiment from a faraway planet, wreaks havoc on the Hawaiian Islands, he becomes the mischievous adopted alien "puppy" of an independent little girl named Lilo and learns about loyalty, friendship, and ʻohana, the Hawaiian tradition of family.<<</plot>
|
||||
</movie>
|
||||
|
||||
Reference in New Issue
Block a user