mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-02-25 05:53:18 +00:00
Compare commits
129 Commits
v10.9.10
...
feature/en
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36f4d105cb | ||
|
|
3738f87278 | ||
|
|
741a01db3b | ||
|
|
726026f0ae | ||
|
|
c7f87c0d69 | ||
|
|
4fa3c30df2 | ||
|
|
0d0a2b4d58 | ||
|
|
c1032967c2 | ||
|
|
4035f6aa21 | ||
|
|
dc2db22c3d | ||
|
|
2599babe31 | ||
|
|
8424ff5b61 | ||
|
|
b123f7ffcd | ||
|
|
9563e4f85e | ||
|
|
c4b7c91f3a | ||
|
|
1a94976752 | ||
|
|
407dc9272c | ||
|
|
5d4880c497 | ||
|
|
c0364fc766 | ||
|
|
76abff2fba | ||
|
|
a7d28045cb | ||
|
|
885df54cca | ||
|
|
93e66746f9 | ||
|
|
1f8dcea494 | ||
|
|
bbe2891ec5 | ||
|
|
de291fd7de | ||
|
|
01f88e4de5 | ||
|
|
b3bb031fca | ||
|
|
24d6532cf1 | ||
|
|
f3e39e87d7 | ||
|
|
4d5428ea90 | ||
|
|
c175371557 | ||
|
|
fc14c08bcc | ||
|
|
30b4ddeddf | ||
|
|
876ae44b8a | ||
|
|
39ae56db0a | ||
|
|
35bc6866d5 | ||
|
|
654dd2b704 | ||
|
|
2faa8c141f | ||
|
|
ac0064110b | ||
|
|
2af1ae5d8a | ||
|
|
833a1da355 | ||
|
|
e6dab2fa11 | ||
|
|
5c828df567 | ||
|
|
c7e0be3c3b | ||
|
|
e7145acd56 | ||
|
|
debd9eb8ce | ||
|
|
c3091b75a3 | ||
|
|
4430706915 | ||
|
|
487ebd3ca8 | ||
|
|
45400ac301 | ||
|
|
cbb99b6e6e | ||
|
|
063fabd344 | ||
|
|
cb9c848918 | ||
|
|
b8898e2338 | ||
|
|
41e92b34ad | ||
|
|
109112ba93 | ||
|
|
c975d50cdc | ||
|
|
575584b68f | ||
|
|
113d00f840 | ||
|
|
1567031046 | ||
|
|
98842b9357 | ||
|
|
8037382e8f | ||
|
|
70e85cb6c4 | ||
|
|
624ad9cb98 | ||
|
|
69ae006f37 | ||
|
|
d4f0b03982 | ||
|
|
d257c3c1bb | ||
|
|
03c23e15b3 | ||
|
|
00de8316ca | ||
|
|
e37e88f92f | ||
|
|
40820e3b41 | ||
|
|
c0f5fe9bd3 | ||
|
|
cbaafbc132 | ||
|
|
1f2c73b40a | ||
|
|
01946c6ef5 | ||
|
|
4ded042dde | ||
|
|
424ca49c26 | ||
|
|
a2eb4c5e60 | ||
|
|
0c159cd8b6 | ||
|
|
7336427ce6 | ||
|
|
8b938e2696 | ||
|
|
a7b2b92f2b | ||
|
|
5fe7d7f0bf | ||
|
|
e109e54949 | ||
|
|
8139179780 | ||
|
|
9a1a588857 | ||
|
|
b063dfd2e3 | ||
|
|
29a293f9e7 | ||
|
|
77c3ddc7ca | ||
|
|
9b978578ce | ||
|
|
4385430f05 | ||
|
|
4e2b30b193 | ||
|
|
7604c4b0f1 | ||
|
|
cb3691dd0d | ||
|
|
45fc7342f5 | ||
|
|
0cc5cc796d | ||
|
|
860c7da6e8 | ||
|
|
d318010c67 | ||
|
|
37b2d3aa2c | ||
|
|
279e91bb3d | ||
|
|
e619e19242 | ||
|
|
01c352d2e8 | ||
|
|
23d0537fb3 | ||
|
|
d622fc9281 | ||
|
|
435023a8f9 | ||
|
|
0173f7642b | ||
|
|
60fb3d5c06 | ||
|
|
69d4886697 | ||
|
|
610e56baaf | ||
|
|
5ac518b02a | ||
|
|
3564b00fc0 | ||
|
|
a118498f79 | ||
|
|
e5ecdcf8c9 | ||
|
|
1e0c7f05e6 | ||
|
|
bd255b3553 | ||
|
|
f568aed520 | ||
|
|
27ecf175d8 | ||
|
|
11a454c0fc | ||
|
|
8dd91ce9f8 | ||
|
|
92e5f946c1 | ||
|
|
fd250e4fe1 | ||
|
|
1ec130757d | ||
|
|
ce3e287892 | ||
|
|
13ed3329e0 | ||
|
|
25c23af865 | ||
|
|
4b7c41ee0f | ||
|
|
717b726329 | ||
|
|
04022f85af |
@@ -3,7 +3,7 @@
|
|||||||
"isRoot": true,
|
"isRoot": true,
|
||||||
"tools": {
|
"tools": {
|
||||||
"dotnet-ef": {
|
"dotnet-ef": {
|
||||||
"version": "8.0.7",
|
"version": "8.0.6",
|
||||||
"commands": [
|
"commands": [
|
||||||
"dotnet-ef"
|
"dotnet-ef"
|
||||||
]
|
]
|
||||||
|
|||||||
7
.github/ISSUE_TEMPLATE/issue report.yml
vendored
7
.github/ISSUE_TEMPLATE/issue report.yml
vendored
@@ -38,10 +38,11 @@ body:
|
|||||||
label: Jellyfin Version
|
label: Jellyfin Version
|
||||||
description: What version of Jellyfin are you running?
|
description: What version of Jellyfin are you running?
|
||||||
options:
|
options:
|
||||||
|
- 10.9.0
|
||||||
- 10.8.13
|
- 10.8.13
|
||||||
- 10.8.12
|
- 10.8.12 or older (please specify)
|
||||||
- 10.8.11 or older (please specify)
|
- Weekly unstable (please specify)
|
||||||
- Unstable (master branch)
|
- Master branch
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: input
|
- type: input
|
||||||
|
|||||||
6
.github/workflows/ci-codeql-analysis.yml
vendored
6
.github/workflows/ci-codeql-analysis.yml
vendored
@@ -27,11 +27,11 @@ jobs:
|
|||||||
dotnet-version: '8.0.x'
|
dotnet-version: '8.0.x'
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
|
uses: github/codeql-action/init@f079b8493333aace61c81488f8bd40919487bd9f # v3.25.7
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
queries: +security-extended
|
queries: +security-extended
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
|
uses: github/codeql-action/autobuild@f079b8493333aace61c81488f8bd40919487bd9f # v3.25.7
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
|
uses: github/codeql-action/analyze@f079b8493333aace61c81488f8bd40919487bd9f # v3.25.7
|
||||||
|
|||||||
6
.github/workflows/ci-openapi.yml
vendored
6
.github/workflows/ci-openapi.yml
vendored
@@ -5,7 +5,7 @@ on:
|
|||||||
- master
|
- master
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- 'v*'
|
||||||
pull_request_target:
|
pull_request:
|
||||||
|
|
||||||
permissions: {}
|
permissions: {}
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ jobs:
|
|||||||
pull-requests: write # to create or update comment (peter-evans/create-or-update-comment)
|
pull-requests: write # to create or update comment (peter-evans/create-or-update-comment)
|
||||||
|
|
||||||
name: OpenAPI - Difference
|
name: OpenAPI - Difference
|
||||||
if: ${{ github.event_name == 'pull_request_target' }}
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- openapi-head
|
- openapi-head
|
||||||
@@ -148,7 +148,7 @@ jobs:
|
|||||||
|
|
||||||
publish-unstable:
|
publish-unstable:
|
||||||
name: OpenAPI - Publish Unstable Spec
|
name: OpenAPI - Publish Unstable Spec
|
||||||
if: ${{ github.event_name != 'pull_request_target' && !startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
|
if: ${{ github.event_name != 'pull_request' && !startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- openapi-head
|
- openapi-head
|
||||||
|
|||||||
2
.github/workflows/ci-tests.yml
vendored
2
.github/workflows/ci-tests.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
|||||||
--verbosity minimal
|
--verbosity minimal
|
||||||
|
|
||||||
- name: Merge code coverage results
|
- name: Merge code coverage results
|
||||||
uses: danielpalme/ReportGenerator-GitHub-Action@6b06171d1a131e7fd85121120a1c00c1ed03e033 # 5.3.0
|
uses: danielpalme/ReportGenerator-GitHub-Action@fa728091745cdd279fddda1e0e80fb29265d0977 # 5.3.5
|
||||||
with:
|
with:
|
||||||
reports: "**/coverage.cobertura.xml"
|
reports: "**/coverage.cobertura.xml"
|
||||||
targetdir: "merged/"
|
targetdir: "merged/"
|
||||||
|
|||||||
2
.github/workflows/commands.yml
vendored
2
.github/workflows/commands.yml
vendored
@@ -4,7 +4,7 @@ on:
|
|||||||
types:
|
types:
|
||||||
- created
|
- created
|
||||||
- edited
|
- edited
|
||||||
pull_request_target:
|
pull_request:
|
||||||
types:
|
types:
|
||||||
- labeled
|
- labeled
|
||||||
- synchronize
|
- synchronize
|
||||||
|
|||||||
2
.github/workflows/project-automation.yml
vendored
2
.github/workflows/project-automation.yml
vendored
@@ -4,7 +4,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
pull_request_target:
|
pull_request:
|
||||||
issue_comment:
|
issue_comment:
|
||||||
|
|
||||||
permissions: {}
|
permissions: {}
|
||||||
|
|||||||
6
.github/workflows/pull-request-conflict.yml
vendored
6
.github/workflows/pull-request-conflict.yml
vendored
@@ -4,7 +4,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
pull_request_target:
|
pull_request:
|
||||||
issue_comment:
|
issue_comment:
|
||||||
|
|
||||||
permissions: {}
|
permissions: {}
|
||||||
@@ -15,8 +15,8 @@ jobs:
|
|||||||
if: ${{ github.repository == 'jellyfin/jellyfin' }}
|
if: ${{ github.repository == 'jellyfin/jellyfin' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Apply label
|
- name: Apply label
|
||||||
uses: eps1lon/actions-label-merge-conflict@6d74047dcef155976a15e4a124dde2c7fe0c5522 # v3.0.1
|
uses: eps1lon/actions-label-merge-conflict@1b1b1fcde06a9b3d089f3464c96417961dde1168 # v3.0.2
|
||||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
|
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request'}}
|
||||||
with:
|
with:
|
||||||
dirtyLabel: 'merge conflict'
|
dirtyLabel: 'merge conflict'
|
||||||
commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
|
commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
|
||||||
|
|||||||
@@ -16,33 +16,33 @@
|
|||||||
<PackageVersion Include="Diacritics" Version="3.3.29" />
|
<PackageVersion Include="Diacritics" Version="3.3.29" />
|
||||||
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
|
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
|
||||||
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
|
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
|
||||||
|
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.5.0" />
|
||||||
<PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
|
<PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
|
||||||
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.2" />
|
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.2" />
|
||||||
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
|
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
|
||||||
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.7" />
|
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.7" />
|
||||||
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
|
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
|
||||||
<PackageVersion Include="libse" Version="4.0.7" />
|
<PackageVersion Include="libse" Version="4.0.5" />
|
||||||
<PackageVersion Include="LrcParser" Version="2023.524.0" />
|
<PackageVersion Include="LrcParser" Version="2023.524.0" />
|
||||||
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
|
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.7" />
|
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.6" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
|
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.6" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.7" />
|
|
||||||
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
|
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
|
||||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.7" />
|
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.6" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.7" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.7" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.6" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.7" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.6" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.7" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.6" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
|
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
|
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.7" />
|
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.6" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.7" />
|
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.6" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
|
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
|
||||||
@@ -58,12 +58,12 @@
|
|||||||
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
|
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
|
||||||
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
|
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
|
||||||
<PackageVersion Include="prometheus-net" Version="8.2.1" />
|
<PackageVersion Include="prometheus-net" Version="8.2.1" />
|
||||||
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.2" />
|
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.1" />
|
||||||
<PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
|
<PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
|
||||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.2" />
|
<PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.0" />
|
||||||
<PackageVersion Include="Serilog.Sinks.Async" Version="2.0.0" />
|
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
|
||||||
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
|
<PackageVersion Include="Serilog.Sinks.Console" Version="5.0.1" />
|
||||||
<PackageVersion Include="Serilog.Sinks.File" Version="6.0.0" />
|
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||||
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
|
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
|
||||||
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
|
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
|
||||||
<PackageVersion Include="SharpFuzz" Version="2.1.1" />
|
<PackageVersion Include="SharpFuzz" Version="2.1.1" />
|
||||||
@@ -73,19 +73,19 @@
|
|||||||
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
|
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
|
||||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||||
<PackageVersion Include="Svg.Skia" Version="1.0.0.18" />
|
<PackageVersion Include="Svg.Skia" Version="1.0.0.18" />
|
||||||
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.6.2" />
|
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
|
||||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
||||||
<PackageVersion Include="System.Globalization" Version="4.3.0" />
|
<PackageVersion Include="System.Globalization" Version="4.3.0" />
|
||||||
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
|
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
|
||||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="8.0.0" />
|
<PackageVersion Include="System.Text.Encoding.CodePages" Version="8.0.0" />
|
||||||
<PackageVersion Include="System.Text.Json" Version="8.0.4" />
|
<PackageVersion Include="System.Text.Json" Version="8.0.3" />
|
||||||
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="8.0.1" />
|
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="8.0.0" />
|
||||||
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
||||||
<PackageVersion Include="TMDbLib" Version="2.2.0" />
|
<PackageVersion Include="TMDbLib" Version="2.2.0" />
|
||||||
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
|
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
|
||||||
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
||||||
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.1" />
|
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.8" />
|
||||||
<PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
|
<PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
|
||||||
<PackageVersion Include="xunit" Version="2.8.1" />
|
<PackageVersion Include="xunit" Version="2.7.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Naming</PackageId>
|
<PackageId>Jellyfin.Naming</PackageId>
|
||||||
<VersionPrefix>10.9.10</VersionPrefix>
|
<VersionPrefix>10.10.0</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ namespace Emby.Naming.ExternalFiles
|
|||||||
pathInfo.Language = culture.ThreeLetterISOLanguageName;
|
pathInfo.Language = culture.ThreeLetterISOLanguageName;
|
||||||
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
|
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
else if (_namingOptions.MediaHearingImpairedFlags.Any(s => currentSliceWithoutSeparator.Equals(s, StringComparison.OrdinalIgnoreCase)))
|
else if (_namingOptions.MediaHearingImpairedFlags.Any(s => currentSliceWithoutSeparator.Contains(s, StringComparison.OrdinalIgnoreCase)))
|
||||||
{
|
{
|
||||||
pathInfo.IsHearingImpaired = true;
|
pathInfo.IsHearingImpaired = true;
|
||||||
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
|
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ namespace Emby.Server.Implementations
|
|||||||
{ FfmpegAnalyzeDurationKey, "200M" },
|
{ FfmpegAnalyzeDurationKey, "200M" },
|
||||||
{ PlaylistsAllowDuplicatesKey, bool.FalseString },
|
{ PlaylistsAllowDuplicatesKey, bool.FalseString },
|
||||||
{ BindToUnixSocketKey, bool.FalseString },
|
{ BindToUnixSocketKey, bool.FalseString },
|
||||||
{ SqliteCacheSizeKey, "20000" }
|
{ SqliteCacheSizeKey, "20000" },
|
||||||
|
{ SqliteDisableSecondLevelCacheKey, bool.FalseString }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
|
||||||
using Jellyfin.Extensions;
|
using Jellyfin.Extensions;
|
||||||
using Microsoft.Data.Sqlite;
|
using Microsoft.Data.Sqlite;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -14,8 +13,6 @@ namespace Emby.Server.Implementations.Data
|
|||||||
public abstract class BaseSqliteRepository : IDisposable
|
public abstract class BaseSqliteRepository : IDisposable
|
||||||
{
|
{
|
||||||
private bool _disposed = false;
|
private bool _disposed = false;
|
||||||
private SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1);
|
|
||||||
private SqliteConnection _writeConnection;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="BaseSqliteRepository"/> class.
|
/// Initializes a new instance of the <see cref="BaseSqliteRepository"/> class.
|
||||||
@@ -101,55 +98,9 @@ namespace Emby.Server.Implementations.Data
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected ManagedConnection GetConnection(bool readOnly = false)
|
protected SqliteConnection GetConnection()
|
||||||
{
|
{
|
||||||
if (!readOnly)
|
var connection = new SqliteConnection($"Filename={DbFilePath}");
|
||||||
{
|
|
||||||
_writeLock.Wait();
|
|
||||||
if (_writeConnection is not null)
|
|
||||||
{
|
|
||||||
return new ManagedConnection(_writeConnection, _writeLock);
|
|
||||||
}
|
|
||||||
|
|
||||||
var writeConnection = new SqliteConnection($"Filename={DbFilePath};Pooling=False");
|
|
||||||
writeConnection.Open();
|
|
||||||
|
|
||||||
if (CacheSize.HasValue)
|
|
||||||
{
|
|
||||||
writeConnection.Execute("PRAGMA cache_size=" + CacheSize.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(LockingMode))
|
|
||||||
{
|
|
||||||
writeConnection.Execute("PRAGMA locking_mode=" + LockingMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(JournalMode))
|
|
||||||
{
|
|
||||||
writeConnection.Execute("PRAGMA journal_mode=" + JournalMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (JournalSizeLimit.HasValue)
|
|
||||||
{
|
|
||||||
writeConnection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Synchronous.HasValue)
|
|
||||||
{
|
|
||||||
writeConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (PageSize.HasValue)
|
|
||||||
{
|
|
||||||
writeConnection.Execute("PRAGMA page_size=" + PageSize.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
writeConnection.Execute("PRAGMA temp_store=" + (int)TempStore);
|
|
||||||
|
|
||||||
return new ManagedConnection(_writeConnection = writeConnection, _writeLock);
|
|
||||||
}
|
|
||||||
|
|
||||||
var connection = new SqliteConnection($"Filename={DbFilePath};Mode=ReadOnly");
|
|
||||||
connection.Open();
|
connection.Open();
|
||||||
|
|
||||||
if (CacheSize.HasValue)
|
if (CacheSize.HasValue)
|
||||||
@@ -184,17 +135,17 @@ namespace Emby.Server.Implementations.Data
|
|||||||
|
|
||||||
connection.Execute("PRAGMA temp_store=" + (int)TempStore);
|
connection.Execute("PRAGMA temp_store=" + (int)TempStore);
|
||||||
|
|
||||||
return new ManagedConnection(connection, null);
|
return connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SqliteCommand PrepareStatement(ManagedConnection connection, string sql)
|
public SqliteCommand PrepareStatement(SqliteConnection connection, string sql)
|
||||||
{
|
{
|
||||||
var command = connection.CreateCommand();
|
var command = connection.CreateCommand();
|
||||||
command.CommandText = sql;
|
command.CommandText = sql;
|
||||||
return command;
|
return command;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected bool TableExists(ManagedConnection connection, string name)
|
protected bool TableExists(SqliteConnection connection, string name)
|
||||||
{
|
{
|
||||||
using var statement = PrepareStatement(connection, "select DISTINCT tbl_name from sqlite_master");
|
using var statement = PrepareStatement(connection, "select DISTINCT tbl_name from sqlite_master");
|
||||||
foreach (var row in statement.ExecuteQuery())
|
foreach (var row in statement.ExecuteQuery())
|
||||||
@@ -208,7 +159,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected List<string> GetColumnNames(ManagedConnection connection, string table)
|
protected List<string> GetColumnNames(SqliteConnection connection, string table)
|
||||||
{
|
{
|
||||||
var columnNames = new List<string>();
|
var columnNames = new List<string>();
|
||||||
|
|
||||||
@@ -223,7 +174,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
return columnNames;
|
return columnNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void AddColumn(ManagedConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
|
protected void AddColumn(SqliteConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
|
||||||
{
|
{
|
||||||
if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase))
|
if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
@@ -256,24 +207,6 @@ namespace Emby.Server.Implementations.Data
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dispose)
|
|
||||||
{
|
|
||||||
_writeLock.Wait();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_writeConnection.Dispose();
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_writeLock.Release();
|
|
||||||
}
|
|
||||||
|
|
||||||
_writeLock.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
_writeConnection = null;
|
|
||||||
_writeLock = null;
|
|
||||||
|
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading;
|
|
||||||
using Microsoft.Data.Sqlite;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Data;
|
|
||||||
|
|
||||||
public sealed class ManagedConnection : IDisposable
|
|
||||||
{
|
|
||||||
private readonly SemaphoreSlim? _writeLock;
|
|
||||||
|
|
||||||
private SqliteConnection _db;
|
|
||||||
|
|
||||||
private bool _disposed = false;
|
|
||||||
|
|
||||||
public ManagedConnection(SqliteConnection db, SemaphoreSlim? writeLock)
|
|
||||||
{
|
|
||||||
_db = db;
|
|
||||||
_writeLock = writeLock;
|
|
||||||
}
|
|
||||||
|
|
||||||
public SqliteTransaction BeginTransaction()
|
|
||||||
=> _db.BeginTransaction();
|
|
||||||
|
|
||||||
public SqliteCommand CreateCommand()
|
|
||||||
=> _db.CreateCommand();
|
|
||||||
|
|
||||||
public void Execute(string commandText)
|
|
||||||
=> _db.Execute(commandText);
|
|
||||||
|
|
||||||
public SqliteCommand PrepareStatement(string sql)
|
|
||||||
=> _db.PrepareStatement(sql);
|
|
||||||
|
|
||||||
public IEnumerable<SqliteDataReader> Query(string commandText)
|
|
||||||
=> _db.Query(commandText);
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_writeLock is null)
|
|
||||||
{
|
|
||||||
// Read connections are managed with an internal pool
|
|
||||||
_db.Dispose();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Write lock is managed by BaseSqliteRepository
|
|
||||||
// Don't dispose here
|
|
||||||
_writeLock.Release();
|
|
||||||
}
|
|
||||||
|
|
||||||
_db = null!;
|
|
||||||
|
|
||||||
_disposed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -601,7 +601,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
transaction.Commit();
|
transaction.Commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SaveItemsInTransaction(ManagedConnection db, IEnumerable<(BaseItem Item, List<Guid> AncestorIds, BaseItem TopParent, string UserDataKey, List<string> InheritedTags)> tuples)
|
private void SaveItemsInTransaction(SqliteConnection db, IEnumerable<(BaseItem Item, List<Guid> AncestorIds, BaseItem TopParent, string UserDataKey, List<string> InheritedTags)> tuples)
|
||||||
{
|
{
|
||||||
using (var saveItemStatement = PrepareStatement(db, SaveItemCommandText))
|
using (var saveItemStatement = PrepareStatement(db, SaveItemCommandText))
|
||||||
using (var deleteAncestorsStatement = PrepareStatement(db, "delete from AncestorIds where ItemId=@ItemId"))
|
using (var deleteAncestorsStatement = PrepareStatement(db, "delete from AncestorIds where ItemId=@ItemId"))
|
||||||
@@ -1261,7 +1261,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
|
|
||||||
CheckDisposed();
|
CheckDisposed();
|
||||||
|
|
||||||
using (var connection = GetConnection(true))
|
using (var connection = GetConnection())
|
||||||
using (var statement = PrepareStatement(connection, _retrieveItemColumnsSelectQuery))
|
using (var statement = PrepareStatement(connection, _retrieveItemColumnsSelectQuery))
|
||||||
{
|
{
|
||||||
statement.TryBind("@guid", id);
|
statement.TryBind("@guid", id);
|
||||||
@@ -1887,7 +1887,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
CheckDisposed();
|
CheckDisposed();
|
||||||
|
|
||||||
var chapters = new List<ChapterInfo>();
|
var chapters = new List<ChapterInfo>();
|
||||||
using (var connection = GetConnection(true))
|
using (var connection = GetConnection())
|
||||||
using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc"))
|
using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc"))
|
||||||
{
|
{
|
||||||
statement.TryBind("@ItemId", item.Id);
|
statement.TryBind("@ItemId", item.Id);
|
||||||
@@ -1906,7 +1906,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
{
|
{
|
||||||
CheckDisposed();
|
CheckDisposed();
|
||||||
|
|
||||||
using (var connection = GetConnection(true))
|
using (var connection = GetConnection())
|
||||||
using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId and ChapterIndex=@ChapterIndex"))
|
using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId and ChapterIndex=@ChapterIndex"))
|
||||||
{
|
{
|
||||||
statement.TryBind("@ItemId", item.Id);
|
statement.TryBind("@ItemId", item.Id);
|
||||||
@@ -1980,7 +1980,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
transaction.Commit();
|
transaction.Commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InsertChapters(Guid idBlob, IReadOnlyList<ChapterInfo> chapters, ManagedConnection db)
|
private void InsertChapters(Guid idBlob, IReadOnlyList<ChapterInfo> chapters, SqliteConnection db)
|
||||||
{
|
{
|
||||||
var startIndex = 0;
|
var startIndex = 0;
|
||||||
var limit = 100;
|
var limit = 100;
|
||||||
@@ -2469,7 +2469,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
var commandText = commandTextBuilder.ToString();
|
var commandText = commandTextBuilder.ToString();
|
||||||
|
|
||||||
using (new QueryTimeLogger(Logger, commandText))
|
using (new QueryTimeLogger(Logger, commandText))
|
||||||
using (var connection = GetConnection(true))
|
using (var connection = GetConnection())
|
||||||
using (var statement = PrepareStatement(connection, commandText))
|
using (var statement = PrepareStatement(connection, commandText))
|
||||||
{
|
{
|
||||||
if (EnableJoinUserData(query))
|
if (EnableJoinUserData(query))
|
||||||
@@ -2537,7 +2537,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
var commandText = commandTextBuilder.ToString();
|
var commandText = commandTextBuilder.ToString();
|
||||||
var items = new List<BaseItem>();
|
var items = new List<BaseItem>();
|
||||||
using (new QueryTimeLogger(Logger, commandText))
|
using (new QueryTimeLogger(Logger, commandText))
|
||||||
using (var connection = GetConnection(true))
|
using (var connection = GetConnection())
|
||||||
using (var statement = PrepareStatement(connection, commandText))
|
using (var statement = PrepareStatement(connection, commandText))
|
||||||
{
|
{
|
||||||
if (EnableJoinUserData(query))
|
if (EnableJoinUserData(query))
|
||||||
@@ -2745,7 +2745,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
|
|
||||||
var list = new List<BaseItem>();
|
var list = new List<BaseItem>();
|
||||||
var result = new QueryResult<BaseItem>();
|
var result = new QueryResult<BaseItem>();
|
||||||
using var connection = GetConnection(true);
|
using var connection = GetConnection();
|
||||||
using var transaction = connection.BeginTransaction();
|
using var transaction = connection.BeginTransaction();
|
||||||
if (!isReturningZeroItems)
|
if (!isReturningZeroItems)
|
||||||
{
|
{
|
||||||
@@ -2927,7 +2927,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
var commandText = commandTextBuilder.ToString();
|
var commandText = commandTextBuilder.ToString();
|
||||||
var list = new List<Guid>();
|
var list = new List<Guid>();
|
||||||
using (new QueryTimeLogger(Logger, commandText))
|
using (new QueryTimeLogger(Logger, commandText))
|
||||||
using (var connection = GetConnection(true))
|
using (var connection = GetConnection())
|
||||||
using (var statement = PrepareStatement(connection, commandText))
|
using (var statement = PrepareStatement(connection, commandText))
|
||||||
{
|
{
|
||||||
if (EnableJoinUserData(query))
|
if (EnableJoinUserData(query))
|
||||||
@@ -4476,7 +4476,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||||||
transaction.Commit();
|
transaction.Commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExecuteWithSingleParam(ManagedConnection db, string query, Guid value)
|
private void ExecuteWithSingleParam(SqliteConnection db, string query, Guid value)
|
||||||
{
|
{
|
||||||
using (var statement = PrepareStatement(db, query))
|
using (var statement = PrepareStatement(db, query))
|
||||||
{
|
{
|
||||||
@@ -4509,7 +4509,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||||||
}
|
}
|
||||||
|
|
||||||
var list = new List<string>();
|
var list = new List<string>();
|
||||||
using (var connection = GetConnection(true))
|
using (var connection = GetConnection())
|
||||||
using (var statement = PrepareStatement(connection, commandText.ToString()))
|
using (var statement = PrepareStatement(connection, commandText.ToString()))
|
||||||
{
|
{
|
||||||
// Run this again to bind the params
|
// Run this again to bind the params
|
||||||
@@ -4547,7 +4547,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||||||
}
|
}
|
||||||
|
|
||||||
var list = new List<PersonInfo>();
|
var list = new List<PersonInfo>();
|
||||||
using (var connection = GetConnection(true))
|
using (var connection = GetConnection())
|
||||||
using (var statement = PrepareStatement(connection, commandText.ToString()))
|
using (var statement = PrepareStatement(connection, commandText.ToString()))
|
||||||
{
|
{
|
||||||
// Run this again to bind the params
|
// Run this again to bind the params
|
||||||
@@ -4632,7 +4632,7 @@ AND Type = @InternalPersonType)");
|
|||||||
return whereClauses;
|
return whereClauses;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateAncestors(Guid itemId, List<Guid> ancestorIds, ManagedConnection db, SqliteCommand deleteAncestorsStatement)
|
private void UpdateAncestors(Guid itemId, List<Guid> ancestorIds, SqliteConnection db, SqliteCommand deleteAncestorsStatement)
|
||||||
{
|
{
|
||||||
if (itemId.IsEmpty())
|
if (itemId.IsEmpty())
|
||||||
{
|
{
|
||||||
@@ -4787,7 +4787,7 @@ AND Type = @InternalPersonType)");
|
|||||||
|
|
||||||
var list = new List<string>();
|
var list = new List<string>();
|
||||||
using (new QueryTimeLogger(Logger, commandText))
|
using (new QueryTimeLogger(Logger, commandText))
|
||||||
using (var connection = GetConnection(true))
|
using (var connection = GetConnection())
|
||||||
using (var statement = PrepareStatement(connection, commandText))
|
using (var statement = PrepareStatement(connection, commandText))
|
||||||
{
|
{
|
||||||
foreach (var row in statement.ExecuteQuery())
|
foreach (var row in statement.ExecuteQuery())
|
||||||
@@ -4987,8 +4987,8 @@ AND Type = @InternalPersonType)");
|
|||||||
var list = new List<(BaseItem, ItemCounts)>();
|
var list = new List<(BaseItem, ItemCounts)>();
|
||||||
var result = new QueryResult<(BaseItem, ItemCounts)>();
|
var result = new QueryResult<(BaseItem, ItemCounts)>();
|
||||||
using (new QueryTimeLogger(Logger, commandText))
|
using (new QueryTimeLogger(Logger, commandText))
|
||||||
using (var connection = GetConnection(true))
|
using (var connection = GetConnection())
|
||||||
using (var transaction = connection.BeginTransaction())
|
using (var transaction = connection.BeginTransaction(deferred: true))
|
||||||
{
|
{
|
||||||
if (!isReturningZeroItems)
|
if (!isReturningZeroItems)
|
||||||
{
|
{
|
||||||
@@ -5148,7 +5148,7 @@ AND Type = @InternalPersonType)");
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateItemValues(Guid itemId, List<(int MagicNumber, string Value)> values, ManagedConnection db)
|
private void UpdateItemValues(Guid itemId, List<(int MagicNumber, string Value)> values, SqliteConnection db)
|
||||||
{
|
{
|
||||||
if (itemId.IsEmpty())
|
if (itemId.IsEmpty())
|
||||||
{
|
{
|
||||||
@@ -5167,7 +5167,7 @@ AND Type = @InternalPersonType)");
|
|||||||
InsertItemValues(itemId, values, db);
|
InsertItemValues(itemId, values, db);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InsertItemValues(Guid id, List<(int MagicNumber, string Value)> values, ManagedConnection db)
|
private void InsertItemValues(Guid id, List<(int MagicNumber, string Value)> values, SqliteConnection db)
|
||||||
{
|
{
|
||||||
const int Limit = 100;
|
const int Limit = 100;
|
||||||
var startIndex = 0;
|
var startIndex = 0;
|
||||||
@@ -5239,7 +5239,7 @@ AND Type = @InternalPersonType)");
|
|||||||
transaction.Commit();
|
transaction.Commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InsertPeople(Guid id, List<PersonInfo> people, ManagedConnection db)
|
private void InsertPeople(Guid id, List<PersonInfo> people, SqliteConnection db)
|
||||||
{
|
{
|
||||||
const int Limit = 100;
|
const int Limit = 100;
|
||||||
var startIndex = 0;
|
var startIndex = 0;
|
||||||
@@ -5335,7 +5335,7 @@ AND Type = @InternalPersonType)");
|
|||||||
|
|
||||||
cmdText += " order by StreamIndex ASC";
|
cmdText += " order by StreamIndex ASC";
|
||||||
|
|
||||||
using (var connection = GetConnection(true))
|
using (var connection = GetConnection())
|
||||||
{
|
{
|
||||||
var list = new List<MediaStream>();
|
var list = new List<MediaStream>();
|
||||||
|
|
||||||
@@ -5388,7 +5388,7 @@ AND Type = @InternalPersonType)");
|
|||||||
transaction.Commit();
|
transaction.Commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InsertMediaStreams(Guid id, IReadOnlyList<MediaStream> streams, ManagedConnection db)
|
private void InsertMediaStreams(Guid id, IReadOnlyList<MediaStream> streams, SqliteConnection db)
|
||||||
{
|
{
|
||||||
const int Limit = 10;
|
const int Limit = 10;
|
||||||
var startIndex = 0;
|
var startIndex = 0;
|
||||||
@@ -5694,17 +5694,13 @@ AND Type = @InternalPersonType)");
|
|||||||
|
|
||||||
item.IsHearingImpaired = reader.TryGetBoolean(43, out var result) && result;
|
item.IsHearingImpaired = reader.TryGetBoolean(43, out var result) && result;
|
||||||
|
|
||||||
if (item.Type is MediaStreamType.Audio or MediaStreamType.Subtitle)
|
if (item.Type == MediaStreamType.Subtitle)
|
||||||
{
|
{
|
||||||
|
item.LocalizedUndefined = _localization.GetLocalizedString("Undefined");
|
||||||
item.LocalizedDefault = _localization.GetLocalizedString("Default");
|
item.LocalizedDefault = _localization.GetLocalizedString("Default");
|
||||||
|
item.LocalizedForced = _localization.GetLocalizedString("Forced");
|
||||||
item.LocalizedExternal = _localization.GetLocalizedString("External");
|
item.LocalizedExternal = _localization.GetLocalizedString("External");
|
||||||
|
item.LocalizedHearingImpaired = _localization.GetLocalizedString("HearingImpaired");
|
||||||
if (item.Type is MediaStreamType.Subtitle)
|
|
||||||
{
|
|
||||||
item.LocalizedUndefined = _localization.GetLocalizedString("Undefined");
|
|
||||||
item.LocalizedForced = _localization.GetLocalizedString("Forced");
|
|
||||||
item.LocalizedHearingImpaired = _localization.GetLocalizedString("HearingImpaired");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
@@ -5726,7 +5722,7 @@ AND Type = @InternalPersonType)");
|
|||||||
cmdText += " order by AttachmentIndex ASC";
|
cmdText += " order by AttachmentIndex ASC";
|
||||||
|
|
||||||
var list = new List<MediaAttachment>();
|
var list = new List<MediaAttachment>();
|
||||||
using (var connection = GetConnection(true))
|
using (var connection = GetConnection())
|
||||||
using (var statement = PrepareStatement(connection, cmdText))
|
using (var statement = PrepareStatement(connection, cmdText))
|
||||||
{
|
{
|
||||||
statement.TryBind("@ItemId", query.ItemId);
|
statement.TryBind("@ItemId", query.ItemId);
|
||||||
@@ -5776,7 +5772,7 @@ AND Type = @InternalPersonType)");
|
|||||||
private void InsertMediaAttachments(
|
private void InsertMediaAttachments(
|
||||||
Guid id,
|
Guid id,
|
||||||
IReadOnlyList<MediaAttachment> attachments,
|
IReadOnlyList<MediaAttachment> attachments,
|
||||||
ManagedConnection db,
|
SqliteConnection db,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
const int InsertAtOnce = 10;
|
const int InsertAtOnce = 10;
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ImportUserIds(ManagedConnection db, IEnumerable<User> users)
|
private void ImportUserIds(SqliteConnection db, IEnumerable<User> users)
|
||||||
{
|
{
|
||||||
var userIdsWithUserData = GetAllUserIdsWithUserData(db);
|
var userIdsWithUserData = GetAllUserIdsWithUserData(db);
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Guid> GetAllUserIdsWithUserData(ManagedConnection db)
|
private List<Guid> GetAllUserIdsWithUserData(SqliteConnection db)
|
||||||
{
|
{
|
||||||
var list = new List<Guid>();
|
var list = new List<Guid>();
|
||||||
|
|
||||||
@@ -176,7 +176,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void SaveUserData(ManagedConnection db, long internalUserId, string key, UserItemData userData)
|
private static void SaveUserData(SqliteConnection db, long internalUserId, string key, UserItemData userData)
|
||||||
{
|
{
|
||||||
using (var statement = db.PrepareStatement("replace into UserDatas (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)"))
|
using (var statement = db.PrepareStatement("replace into UserDatas (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)"))
|
||||||
{
|
{
|
||||||
@@ -267,7 +267,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
|
|
||||||
ArgumentException.ThrowIfNullOrEmpty(key);
|
ArgumentException.ThrowIfNullOrEmpty(key);
|
||||||
|
|
||||||
using (var connection = GetConnection(true))
|
using (var connection = GetConnection())
|
||||||
{
|
{
|
||||||
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
|
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -389,7 +389,7 @@ namespace Emby.Server.Implementations.IO
|
|||||||
var info = new FileInfo(path);
|
var info = new FileInfo(path);
|
||||||
|
|
||||||
if (info.Exists &&
|
if (info.Exists &&
|
||||||
(info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden != isHidden)
|
((info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) != isHidden)
|
||||||
{
|
{
|
||||||
if (isHidden)
|
if (isHidden)
|
||||||
{
|
{
|
||||||
@@ -417,8 +417,8 @@ namespace Emby.Server.Implementations.IO
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly == readOnly
|
if (((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) == readOnly
|
||||||
&& (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden == isHidden)
|
&& ((info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) == isHidden)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1029,7 +1029,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false)
|
private async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false)
|
||||||
{
|
{
|
||||||
await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
@@ -1884,7 +1884,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var index = item.GetImageIndex(img);
|
var index = item.GetImageIndex(img);
|
||||||
image = await ConvertImageToLocal(item, img, index, true).ConfigureAwait(false);
|
image = await ConvertImageToLocal(item, img, index, removeOnFailure: true).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (ArgumentException)
|
catch (ArgumentException)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
|||||||
IndexNumber = seasonParserResult.SeasonNumber,
|
IndexNumber = seasonParserResult.SeasonNumber,
|
||||||
SeriesId = series.Id,
|
SeriesId = series.Id,
|
||||||
SeriesName = series.Name,
|
SeriesName = series.Name,
|
||||||
Path = seasonParserResult.IsSeasonFolder ? path : null
|
Path = seasonParserResult.IsSeasonFolder ? path : args.Parent.Path
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!season.IndexNumber.HasValue || !seasonParserResult.IsSeasonFolder)
|
if (!season.IndexNumber.HasValue || !seasonParserResult.IsSeasonFolder)
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
{}
|
{
|
||||||
|
"Albums": "аальбомқәа"
|
||||||
|
}
|
||||||
|
|||||||
@@ -127,5 +127,7 @@
|
|||||||
"TaskRefreshTrickplayImages": "Стварыце выявы Trickplay",
|
"TaskRefreshTrickplayImages": "Стварыце выявы Trickplay",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Стварае прагляд відэаролікаў для Trickplay у падключаных бібліятэках.",
|
"TaskRefreshTrickplayImagesDescription": "Стварае прагляд відэаролікаў для Trickplay у падключаных бібліятэках.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Ачысціце калекцыі і спісы прайгравання",
|
"TaskCleanCollectionsAndPlaylists": "Ачысціце калекцыі і спісы прайгравання",
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Выдаляе элементы з калекцый і спісаў прайгравання, якія больш не існуюць."
|
"TaskCleanCollectionsAndPlaylistsDescription": "Выдаляе элементы з калекцый і спісаў прайгравання, якія больш не існуюць.",
|
||||||
|
"TaskAudioNormalizationDescription": "Сканіруе файлы на прадмет нармалізацыі гуку.",
|
||||||
|
"TaskAudioNormalization": "Нармалізацыя гуку"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
"HeaderFavoriteEpisodes": "Oblíbené epizody",
|
"HeaderFavoriteEpisodes": "Oblíbené epizody",
|
||||||
"HeaderFavoriteShows": "Oblíbené seriály",
|
"HeaderFavoriteShows": "Oblíbené seriály",
|
||||||
"HeaderFavoriteSongs": "Oblíbená hudba",
|
"HeaderFavoriteSongs": "Oblíbená hudba",
|
||||||
"HeaderLiveTV": "Živý přenos",
|
"HeaderLiveTV": "TV vysílání",
|
||||||
"HeaderNextUp": "Další díly",
|
"HeaderNextUp": "Další díly",
|
||||||
"HeaderRecordingGroups": "Skupiny nahrávek",
|
"HeaderRecordingGroups": "Skupiny nahrávek",
|
||||||
"HomeVideos": "Domácí videa",
|
"HomeVideos": "Domácí videa",
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"Genres": "Genrer",
|
"Genres": "Genrer",
|
||||||
"HeaderAlbumArtists": "Albumkunstnere",
|
"HeaderAlbumArtists": "Albumkunstnere",
|
||||||
"HeaderContinueWatching": "Fortsæt afspilning",
|
"HeaderContinueWatching": "Fortsæt afspilning",
|
||||||
"HeaderFavoriteAlbums": "Favoritalbummer",
|
"HeaderFavoriteAlbums": "Favoritalbum",
|
||||||
"HeaderFavoriteArtists": "Favoritkunstnere",
|
"HeaderFavoriteArtists": "Favoritkunstnere",
|
||||||
"HeaderFavoriteEpisodes": "Yndlingsafsnit",
|
"HeaderFavoriteEpisodes": "Yndlingsafsnit",
|
||||||
"HeaderFavoriteShows": "Yndlingsserier",
|
"HeaderFavoriteShows": "Yndlingsserier",
|
||||||
@@ -87,21 +87,21 @@
|
|||||||
"UserOnlineFromDevice": "{0} er online fra {1}",
|
"UserOnlineFromDevice": "{0} er online fra {1}",
|
||||||
"UserPasswordChangedWithName": "Adgangskode er ændret for brugeren {0}",
|
"UserPasswordChangedWithName": "Adgangskode er ændret for brugeren {0}",
|
||||||
"UserPolicyUpdatedWithName": "Brugerpolitikken er blevet opdateret for {0}",
|
"UserPolicyUpdatedWithName": "Brugerpolitikken er blevet opdateret for {0}",
|
||||||
"UserStartedPlayingItemWithValues": "{0} har påbegyndt afspilning af {1}",
|
"UserStartedPlayingItemWithValues": "{0} har påbegyndt afspilning af {1} på {2}",
|
||||||
"UserStoppedPlayingItemWithValues": "{0} har afsluttet afspilning af {1} på {2}",
|
"UserStoppedPlayingItemWithValues": "{0} har afsluttet afspilning af {1} på {2}",
|
||||||
"ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek",
|
"ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek",
|
||||||
"ValueSpecialEpisodeName": "Special - {0}",
|
"ValueSpecialEpisodeName": "Special - {0}",
|
||||||
"VersionNumber": "Version {0}",
|
"VersionNumber": "Version {0}",
|
||||||
"TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata-konfigurationen.",
|
"TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata-konfigurationen.",
|
||||||
"TaskDownloadMissingSubtitles": "Hent manglende undertekster",
|
"TaskDownloadMissingSubtitles": "Hent manglende undertekster",
|
||||||
"TaskUpdatePluginsDescription": "Henter og installerer opdateringer for plugins, som er indstillet til at blive opdateret automatisk.",
|
"TaskUpdatePluginsDescription": "Henter og installerer opdateringer for plugins, som er konfigurerede til at blive opdateret automatisk.",
|
||||||
"TaskUpdatePlugins": "Opdater Plugins",
|
"TaskUpdatePlugins": "Opdater Plugins",
|
||||||
"TaskCleanLogsDescription": "Sletter log-filer som er mere end {0} dage gamle.",
|
"TaskCleanLogsDescription": "Sletter log-filer som er mere end {0} dage gamle.",
|
||||||
"TaskCleanLogs": "Ryd Log-mappe",
|
"TaskCleanLogs": "Ryd Log-mappe",
|
||||||
"TaskRefreshLibraryDescription": "Scanner dit mediebibliotek for nye filer og opdateret metadata.",
|
"TaskRefreshLibraryDescription": "Scanner dit mediebibliotek for nye filer og opdateret metadata.",
|
||||||
"TaskRefreshLibrary": "Scan Mediebibliotek",
|
"TaskRefreshLibrary": "Scan Mediebibliotek",
|
||||||
"TaskCleanCacheDescription": "Sletter cache-filer som systemet ikke længere bruger.",
|
"TaskCleanCacheDescription": "Sletter cache-filer som systemet ikke længere bruger.",
|
||||||
"TaskCleanCache": "Ryd Cache-mappe",
|
"TaskCleanCache": "Ryd cache-mappe",
|
||||||
"TasksChannelsCategory": "Internetkanaler",
|
"TasksChannelsCategory": "Internetkanaler",
|
||||||
"TasksApplicationCategory": "Applikation",
|
"TasksApplicationCategory": "Applikation",
|
||||||
"TasksLibraryCategory": "Bibliotek",
|
"TasksLibraryCategory": "Bibliotek",
|
||||||
@@ -128,5 +128,7 @@
|
|||||||
"TaskRefreshTrickplayImages": "Generér Trickplay Billeder",
|
"TaskRefreshTrickplayImages": "Generér Trickplay Billeder",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Laver trickplay forhåndsvisninger for videoer i aktiverede biblioteker.",
|
"TaskRefreshTrickplayImagesDescription": "Laver trickplay forhåndsvisninger for videoer i aktiverede biblioteker.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Ryd op i samlinger og afspilningslister",
|
"TaskCleanCollectionsAndPlaylists": "Ryd op i samlinger og afspilningslister",
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Fjerner enheder fra samlinger og afspilningslister der ikke eksisterer længere."
|
"TaskCleanCollectionsAndPlaylistsDescription": "Fjerner elementer fra samlinger og afspilningslister der ikke eksisterer længere.",
|
||||||
|
"TaskAudioNormalizationDescription": "Skanner filer for data vedrørende audio-normalisering.",
|
||||||
|
"TaskAudioNormalization": "Audio-normalisering"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,5 +126,9 @@
|
|||||||
"External": "Εξωτερικό",
|
"External": "Εξωτερικό",
|
||||||
"HearingImpaired": "Με προβλήματα ακοής",
|
"HearingImpaired": "Με προβλήματα ακοής",
|
||||||
"TaskRefreshTrickplayImages": "Δημιουργήστε εικόνες Trickplay",
|
"TaskRefreshTrickplayImages": "Δημιουργήστε εικόνες Trickplay",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Δημιουργεί προεπισκοπήσεις trickplay για βίντεο σε ενεργοποιημένες βιβλιοθήκες."
|
"TaskRefreshTrickplayImagesDescription": "Δημιουργεί προεπισκοπήσεις trickplay για βίντεο σε ενεργοποιημένες βιβλιοθήκες.",
|
||||||
|
"TaskAudioNormalization": "Ομοιομορφία ήχου",
|
||||||
|
"TaskAudioNormalizationDescription": "Ανίχνευση αρχείων για δεδομένα ομοιομορφίας ήχου.",
|
||||||
|
"TaskCleanCollectionsAndPlaylists": "Καθαρισμός συλλογών και λιστών αναπαραγωγής",
|
||||||
|
"TaskCleanCollectionsAndPlaylistsDescription": "Αφαιρούνται στοιχεία από τις συλλογές και τις λίστες αναπαραγωγής που δεν υπάρχουν πλέον."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"Collections": "Colecciones",
|
"Collections": "Colecciones",
|
||||||
"DeviceOfflineWithName": "{0} se ha desconectado",
|
"DeviceOfflineWithName": "{0} se ha desconectado",
|
||||||
"DeviceOnlineWithName": "{0} está conectado",
|
"DeviceOnlineWithName": "{0} está conectado",
|
||||||
"FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión desde {0}",
|
"FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión de {0}",
|
||||||
"Favorites": "Favoritos",
|
"Favorites": "Favoritos",
|
||||||
"Folders": "Carpetas",
|
"Folders": "Carpetas",
|
||||||
"Genres": "Géneros",
|
"Genres": "Géneros",
|
||||||
@@ -124,5 +124,11 @@
|
|||||||
"TaskKeyframeExtractorDescription": "Extrae los cuadros clave de los archivos de vídeo para crear listas HLS más precisas. Esta tarea puede tardar un buen rato.",
|
"TaskKeyframeExtractorDescription": "Extrae los cuadros clave de los archivos de vídeo para crear listas HLS más precisas. Esta tarea puede tardar un buen rato.",
|
||||||
"TaskKeyframeExtractor": "Extractor de Cuadros Clave",
|
"TaskKeyframeExtractor": "Extractor de Cuadros Clave",
|
||||||
"External": "Externo",
|
"External": "Externo",
|
||||||
"HearingImpaired": "Discapacidad Auditiva"
|
"HearingImpaired": "Discapacidad Auditiva",
|
||||||
|
"TaskRefreshTrickplayImagesDescription": "Crea previsualizaciones para la barra de reproducción en las bibliotecas habilitadas.",
|
||||||
|
"TaskRefreshTrickplayImages": "Generar imágenes de la barra de reproducción",
|
||||||
|
"TaskAudioNormalization": "Normalización de audio",
|
||||||
|
"TaskAudioNormalizationDescription": "Analiza los archivos para normalizar el audio.",
|
||||||
|
"TaskCleanCollectionsAndPlaylists": "Limpieza de colecciones y listas de reproducción",
|
||||||
|
"TaskCleanCollectionsAndPlaylistsDescription": "Quita elementos que ya no existen de colecciones y listas de reproducción."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"Collections": "Colecciones",
|
"Collections": "Colecciones",
|
||||||
"DeviceOfflineWithName": "{0} se ha desconectado",
|
"DeviceOfflineWithName": "{0} se ha desconectado",
|
||||||
"DeviceOnlineWithName": "{0} está conectado",
|
"DeviceOnlineWithName": "{0} está conectado",
|
||||||
"FailedLoginAttemptWithUserName": "Error al intentar iniciar sesión desde {0}",
|
"FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión de {0}",
|
||||||
"Favorites": "Favoritos",
|
"Favorites": "Favoritos",
|
||||||
"Folders": "Carpetas",
|
"Folders": "Carpetas",
|
||||||
"Genres": "Géneros",
|
"Genres": "Géneros",
|
||||||
|
|||||||
@@ -12,14 +12,118 @@
|
|||||||
"Application": "Aplicación",
|
"Application": "Aplicación",
|
||||||
"AppDeviceValues": "App: {0}, Dispositivo: {1}",
|
"AppDeviceValues": "App: {0}, Dispositivo: {1}",
|
||||||
"HeaderContinueWatching": "Continuar Viendo",
|
"HeaderContinueWatching": "Continuar Viendo",
|
||||||
"HeaderAlbumArtists": "Artistas del Álbum",
|
"HeaderAlbumArtists": "Artistas del álbum",
|
||||||
"Genres": "Géneros",
|
"Genres": "Géneros",
|
||||||
"Folders": "Carpetas",
|
"Folders": "Carpetas",
|
||||||
"Favorites": "Favoritos",
|
"Favorites": "Favoritos",
|
||||||
"FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido de {0}",
|
"FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido desde {0}",
|
||||||
"HeaderFavoriteSongs": "Canciones Favoritas",
|
"HeaderFavoriteSongs": "Canciones Favoritas",
|
||||||
"HeaderFavoriteEpisodes": "Episodios Favoritos",
|
"HeaderFavoriteEpisodes": "Episodios Favoritos",
|
||||||
"HeaderFavoriteArtists": "Artistas Favoritos",
|
"HeaderFavoriteArtists": "Artistas Favoritos",
|
||||||
"External": "Externo",
|
"External": "Externo",
|
||||||
"Default": "Predeterminado"
|
"Default": "Predeterminado",
|
||||||
|
"Movies": "Películas",
|
||||||
|
"MessageNamedServerConfigurationUpdatedWithValue": "La sección {0} de la configuración ha sido actualizada",
|
||||||
|
"MixedContent": "Contenido mixto",
|
||||||
|
"Music": "Música",
|
||||||
|
"NotificationOptionCameraImageUploaded": "Imagen de la cámara subida",
|
||||||
|
"NotificationOptionServerRestartRequired": "Se necesita reiniciar el servidor",
|
||||||
|
"NotificationOptionVideoPlayback": "Reproducción de video iniciada",
|
||||||
|
"Sync": "Sincronizar",
|
||||||
|
"Shows": "Series",
|
||||||
|
"UserDownloadingItemWithValues": "{0} está descargando {1}",
|
||||||
|
"UserOfflineFromDevice": "{0} se ha desconectado desde {1}",
|
||||||
|
"UserOnlineFromDevice": "{0} está en línea desde {1}",
|
||||||
|
"TasksChannelsCategory": "Canales de Internet",
|
||||||
|
"TaskRefreshChannelsDescription": "Actualiza la información de canales de Internet.",
|
||||||
|
"TaskDownloadMissingSubtitles": "Descargar subtítulos faltantes",
|
||||||
|
"TaskOptimizeDatabaseDescription": "Compacta la base de datos y libera espacio. Ejecutar esta tarea después de escanear la biblioteca o hacer otros cambios que impliquen modificaciones en la base de datos puede mejorar el rendimiento.",
|
||||||
|
"TaskKeyframeExtractorDescription": "Extrae Fotogramas Clave de los archivos de vídeo para crear Listas de Reproducción HLS más precisas. Esta tarea puede durar mucho tiempo.",
|
||||||
|
"TaskAudioNormalization": "Normalización de audio",
|
||||||
|
"TaskAudioNormalizationDescription": "Escanear archivos para la normalización de data.",
|
||||||
|
"TaskCleanCollectionsAndPlaylists": "Limpiar colecciones y listas de reproducción",
|
||||||
|
"TaskCleanCollectionsAndPlaylistsDescription": "Remover elementos de colecciones y listas de reproducción que no existen.",
|
||||||
|
"TvShows": "Series de TV",
|
||||||
|
"UserStartedPlayingItemWithValues": "{0} está reproduciendo {1} en {2}",
|
||||||
|
"TaskRefreshChannels": "Actualizar canales",
|
||||||
|
"Photos": "Fotos",
|
||||||
|
"HeaderFavoriteShows": "Programas favoritos",
|
||||||
|
"TaskCleanActivityLog": "Limpiar registro de actividades",
|
||||||
|
"UserPasswordChangedWithName": "Se ha cambiado la contraseña para el usuario {0}",
|
||||||
|
"System": "Sistema",
|
||||||
|
"User": "Usuario",
|
||||||
|
"Forced": "Forzado",
|
||||||
|
"PluginInstalledWithName": "{0} ha sido instalado",
|
||||||
|
"HeaderFavoriteAlbums": "Álbumes favoritos",
|
||||||
|
"TaskUpdatePlugins": "Actualizar Plugins",
|
||||||
|
"Latest": "Recientes",
|
||||||
|
"UserStoppedPlayingItemWithValues": "{0} ha terminado de reproducir {1} en {2}",
|
||||||
|
"Songs": "Canciones",
|
||||||
|
"NotificationOptionPluginError": "Falla de plugin",
|
||||||
|
"ScheduledTaskStartedWithName": "{0} iniciado",
|
||||||
|
"TasksApplicationCategory": "Aplicación",
|
||||||
|
"UserDeletedWithName": "El usuario {0} ha sido eliminado",
|
||||||
|
"TaskRefreshChapterImages": "Extraer imágenes de los capítulos",
|
||||||
|
"TaskUpdatePluginsDescription": "Descarga e instala actualizaciones para plugins que están configurados para actualizarse automáticamente.",
|
||||||
|
"TaskRefreshPeopleDescription": "Actualiza metadatos de actores y directores en tu biblioteca de medios.",
|
||||||
|
"NotificationOptionUserLockedOut": "Usuario bloqueado",
|
||||||
|
"TaskCleanTranscodeDescription": "Elimina archivos transcodificados que tengan más de un día.",
|
||||||
|
"TaskCleanTranscode": "Limpiar el directorio de transcodificaciones",
|
||||||
|
"NotificationOptionPluginUpdateInstalled": "Actualización de plugin instalada",
|
||||||
|
"NotificationOptionAudioPlaybackStopped": "Reproducción de audio detenida",
|
||||||
|
"TasksLibraryCategory": "Biblioteca",
|
||||||
|
"NotificationOptionPluginInstalled": "Plugin instalado",
|
||||||
|
"UserPolicyUpdatedWithName": "La política de usuario ha sido actualizada para {0}",
|
||||||
|
"VersionNumber": "Versión {0}",
|
||||||
|
"HeaderNextUp": "A continuación",
|
||||||
|
"ValueHasBeenAddedToLibrary": "{0} se ha añadido a tu biblioteca",
|
||||||
|
"LabelIpAddressValue": "Dirección IP: {0}",
|
||||||
|
"NameSeasonNumber": "Temporada {0}",
|
||||||
|
"NotificationOptionNewLibraryContent": "Nuevo contenido agregado",
|
||||||
|
"Plugin": "Plugin",
|
||||||
|
"NotificationOptionAudioPlayback": "Reproducción de audio iniciada",
|
||||||
|
"NotificationOptionTaskFailed": "Falló la tarea programada",
|
||||||
|
"LabelRunningTimeValue": "Tiempo en ejecución: {0}",
|
||||||
|
"SubtitleDownloadFailureFromForItem": "Falló la descarga de subtítulos desde {0} para {1}",
|
||||||
|
"TaskRefreshLibrary": "Escanear biblioteca de medios",
|
||||||
|
"ServerNameNeedsToBeRestarted": "{0} debe ser reiniciado",
|
||||||
|
"TasksMaintenanceCategory": "Mantenimiento",
|
||||||
|
"ProviderValue": "Proveedor: {0}",
|
||||||
|
"UserCreatedWithName": "El usuario {0} ha sido creado",
|
||||||
|
"PluginUninstalledWithName": "{0} ha sido desinstalado",
|
||||||
|
"ValueSpecialEpisodeName": "Especial - {0}",
|
||||||
|
"ScheduledTaskFailedWithName": "{0} falló",
|
||||||
|
"TaskCleanLogs": "Limpiar directorio de registros",
|
||||||
|
"NameInstallFailed": "Falló la instalación de {0}",
|
||||||
|
"UserLockedOutWithName": "El usuario {0} ha sido bloqueado",
|
||||||
|
"TaskRefreshLibraryDescription": "Escanea tu biblioteca de medios para encontrar archivos nuevos y actualizar los metadatos.",
|
||||||
|
"StartupEmbyServerIsLoading": "El servidor Jellyfin está cargando. Por favor, intente de nuevo en un momento.",
|
||||||
|
"Playlists": "Listas de reproducción",
|
||||||
|
"TaskDownloadMissingSubtitlesDescription": "Busca subtítulos faltantes en Internet basándose en la configuración de metadatos.",
|
||||||
|
"MessageServerConfigurationUpdated": "Se ha actualizado la configuración del servidor",
|
||||||
|
"TaskRefreshPeople": "Actualizar personas",
|
||||||
|
"NotificationOptionVideoPlaybackStopped": "Reproducción de video detenida",
|
||||||
|
"HeaderLiveTV": "TV en vivo",
|
||||||
|
"NameSeasonUnknown": "Temporada desconocida",
|
||||||
|
"NotificationOptionInstallationFailed": "Fallo de instalación",
|
||||||
|
"NotificationOptionPluginUninstalled": "Plugin desinstalado",
|
||||||
|
"TaskCleanCache": "Limpiar directorio caché",
|
||||||
|
"TaskRefreshChapterImagesDescription": "Crea miniaturas para videos que tienen capítulos.",
|
||||||
|
"Inherit": "Heredar",
|
||||||
|
"HeaderRecordingGroups": "Grupos de grabación",
|
||||||
|
"ItemAddedWithName": "{0} fue agregado a la biblioteca",
|
||||||
|
"TaskOptimizeDatabase": "Optimizar base de datos",
|
||||||
|
"TaskKeyframeExtractor": "Extractor de Fotogramas Clave",
|
||||||
|
"HearingImpaired": "Discapacidad auditiva",
|
||||||
|
"HomeVideos": "Videos caseros",
|
||||||
|
"ItemRemovedWithName": "{0} fue removido de la biblioteca",
|
||||||
|
"MessageApplicationUpdated": "El servidor Jellyfin ha sido actualizado",
|
||||||
|
"MessageApplicationUpdatedTo": "El servidor Jellyfin ha sido actualizado a {0}",
|
||||||
|
"MusicVideos": "Videos musicales",
|
||||||
|
"NewVersionIsAvailable": "Una nueva versión de Jellyfin está disponible para descargar.",
|
||||||
|
"PluginUpdatedWithName": "{0} ha sido actualizado",
|
||||||
|
"Undefined": "Sin definir",
|
||||||
|
"TaskCleanActivityLogDescription": "Elimina las entradas del registro de actividad anteriores al periodo configurado.",
|
||||||
|
"TaskCleanCacheDescription": "Elimina archivos caché que ya no son necesarios para el sistema.",
|
||||||
|
"TaskCleanLogsDescription": "Elimina archivos de registro con más de {0} días de antigüedad."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,5 +125,7 @@
|
|||||||
"TaskKeyframeExtractorDescription": "Eraldab videofailidest võtmekaadreid, et luua täpsemaid HLS-i esitusloendeid. See ülesanne võib kesta pikka aega.",
|
"TaskKeyframeExtractorDescription": "Eraldab videofailidest võtmekaadreid, et luua täpsemaid HLS-i esitusloendeid. See ülesanne võib kesta pikka aega.",
|
||||||
"TaskKeyframeExtractor": "Võtmekaadri ekstraktor",
|
"TaskKeyframeExtractor": "Võtmekaadri ekstraktor",
|
||||||
"TaskRefreshTrickplayImages": "Loo eelvaate pildid",
|
"TaskRefreshTrickplayImages": "Loo eelvaate pildid",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Loob eelvaated videotele, kus lubatud."
|
"TaskRefreshTrickplayImagesDescription": "Loob eelvaated videotele, kus lubatud.",
|
||||||
|
"TaskAudioNormalization": "Heli Normaliseerimine",
|
||||||
|
"TaskAudioNormalizationDescription": "Skaneerib faile heli normaliseerimise andmete jaoks."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,5 +127,7 @@
|
|||||||
"TaskRefreshTrickplayImages": "Luo Trickplay-kuvat",
|
"TaskRefreshTrickplayImages": "Luo Trickplay-kuvat",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Luo Trickplay-esikatselut käytössä olevien kirjastojen videoista.",
|
"TaskRefreshTrickplayImagesDescription": "Luo Trickplay-esikatselut käytössä olevien kirjastojen videoista.",
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Poistaa kohteet kokoelmista ja soittolistoista joita ei ole enää olemassa.",
|
"TaskCleanCollectionsAndPlaylistsDescription": "Poistaa kohteet kokoelmista ja soittolistoista joita ei ole enää olemassa.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Puhdista kokoelmat ja soittolistat"
|
"TaskCleanCollectionsAndPlaylists": "Puhdista kokoelmat ja soittolistat",
|
||||||
|
"TaskAudioNormalization": "Äänenvoimakkuuden normalisointi",
|
||||||
|
"TaskAudioNormalizationDescription": "Etsii tiedostoista äänenvoimakkuuden normalisointitietoja."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"Collections": "Collections",
|
"Collections": "Collections",
|
||||||
"DeviceOfflineWithName": "{0} s'est déconnecté",
|
"DeviceOfflineWithName": "{0} s'est déconnecté",
|
||||||
"DeviceOnlineWithName": "{0} est connecté",
|
"DeviceOnlineWithName": "{0} est connecté",
|
||||||
"FailedLoginAttemptWithUserName": "Tentative de connexion échoué par {0}",
|
"FailedLoginAttemptWithUserName": "Tentative de connexion échouée par {0}",
|
||||||
"Favorites": "Favoris",
|
"Favorites": "Favoris",
|
||||||
"Folders": "Dossiers",
|
"Folders": "Dossiers",
|
||||||
"Genres": "Genres",
|
"Genres": "Genres",
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
"MixedContent": "Contenu mixte",
|
"MixedContent": "Contenu mixte",
|
||||||
"Movies": "Films",
|
"Movies": "Films",
|
||||||
"Music": "Musique",
|
"Music": "Musique",
|
||||||
"MusicVideos": "Vidéos musicales",
|
"MusicVideos": "Vidéoclips",
|
||||||
"NameInstallFailed": "échec d'installation de {0}",
|
"NameInstallFailed": "échec d'installation de {0}",
|
||||||
"NameSeasonNumber": "Saison {0}",
|
"NameSeasonNumber": "Saison {0}",
|
||||||
"NameSeasonUnknown": "Saison Inconnue",
|
"NameSeasonUnknown": "Saison Inconnue",
|
||||||
@@ -128,5 +128,7 @@
|
|||||||
"TaskRefreshTrickplayImages": "Générer des images Trickplay",
|
"TaskRefreshTrickplayImages": "Générer des images Trickplay",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées.",
|
"TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Nettoyer les collections et les listes de lecture",
|
"TaskCleanCollectionsAndPlaylists": "Nettoyer les collections et les listes de lecture",
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Supprimer les liens inexistants des collections et des listes de lecture"
|
"TaskCleanCollectionsAndPlaylistsDescription": "Supprime les éléments des collections et des listes de lecture qui n'existent plus.",
|
||||||
|
"TaskAudioNormalization": "Normalisation audio",
|
||||||
|
"TaskAudioNormalizationDescription": "Analyse les fichiers à la recherche de données de normalisation audio."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,5 +126,9 @@
|
|||||||
"External": "חיצוני",
|
"External": "חיצוני",
|
||||||
"HearingImpaired": "לקוי שמיעה",
|
"HearingImpaired": "לקוי שמיעה",
|
||||||
"TaskRefreshTrickplayImages": "יצירת תמונות המחשה",
|
"TaskRefreshTrickplayImages": "יצירת תמונות המחשה",
|
||||||
"TaskRefreshTrickplayImagesDescription": "יוצר תמונות המחשה לסרטונים שפעילים בספריות."
|
"TaskRefreshTrickplayImagesDescription": "יוצר תמונות המחשה לסרטונים שפעילים בספריות.",
|
||||||
|
"TaskAudioNormalization": "נרמול שמע",
|
||||||
|
"TaskCleanCollectionsAndPlaylistsDescription": "מנקה פריטים לא קיימים מאוספים ורשימות השמעה.",
|
||||||
|
"TaskAudioNormalizationDescription": "מחפש קבצי נורמליזציה של שמע.",
|
||||||
|
"TaskCleanCollectionsAndPlaylists": "מנקה אוספים ורשימות השמעה"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@
|
|||||||
"Movies": "Film",
|
"Movies": "Film",
|
||||||
"MessageServerConfigurationUpdated": "Konfigurasi server telah diperbarui",
|
"MessageServerConfigurationUpdated": "Konfigurasi server telah diperbarui",
|
||||||
"MessageNamedServerConfigurationUpdatedWithValue": "Bagian konfigurasi server {0} telah diperbarui",
|
"MessageNamedServerConfigurationUpdatedWithValue": "Bagian konfigurasi server {0} telah diperbarui",
|
||||||
"FailedLoginAttemptWithUserName": "Gagal melakukan login dari {0}",
|
"FailedLoginAttemptWithUserName": "Gagal upaya login dari {0}",
|
||||||
"CameraImageUploadedFrom": "Sebuah gambar kamera baru telah diunggah dari {0}",
|
"CameraImageUploadedFrom": "Sebuah gambar kamera baru telah diunggah dari {0}",
|
||||||
"DeviceOfflineWithName": "{0} telah terputus",
|
"DeviceOfflineWithName": "{0} telah terputus",
|
||||||
"DeviceOnlineWithName": "{0} telah terhubung",
|
"DeviceOnlineWithName": "{0} telah terhubung",
|
||||||
@@ -125,5 +125,9 @@
|
|||||||
"External": "Luar",
|
"External": "Luar",
|
||||||
"HearingImpaired": "Gangguan Pendengaran",
|
"HearingImpaired": "Gangguan Pendengaran",
|
||||||
"TaskRefreshTrickplayImages": "Hasilkan Gambar Trickplay",
|
"TaskRefreshTrickplayImages": "Hasilkan Gambar Trickplay",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Buat pratinjau trickplay untuk video di perpustakaan yang diaktifkan."
|
"TaskRefreshTrickplayImagesDescription": "Buat pratinjau trickplay untuk video di perpustakaan yang diaktifkan.",
|
||||||
|
"TaskAudioNormalizationDescription": "Pindai file untuk data normalisasi audio.",
|
||||||
|
"TaskAudioNormalization": "Normalisasi Audio",
|
||||||
|
"TaskCleanCollectionsAndPlaylists": "Bersihkan koleksi dan daftar putar",
|
||||||
|
"TaskCleanCollectionsAndPlaylistsDescription": "Menghapus item dari koleksi dan daftar putar yang sudah tidak ada."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,7 +83,7 @@
|
|||||||
"UserDeletedWithName": "L'utente {0} è stato rimosso",
|
"UserDeletedWithName": "L'utente {0} è stato rimosso",
|
||||||
"UserDownloadingItemWithValues": "{0} sta scaricando {1}",
|
"UserDownloadingItemWithValues": "{0} sta scaricando {1}",
|
||||||
"UserLockedOutWithName": "L'utente {0} è stato bloccato",
|
"UserLockedOutWithName": "L'utente {0} è stato bloccato",
|
||||||
"UserOfflineFromDevice": "{0} si è disconnesso su {1}",
|
"UserOfflineFromDevice": "{0} si è disconnesso da {1}",
|
||||||
"UserOnlineFromDevice": "{0} è online su {1}",
|
"UserOnlineFromDevice": "{0} è online su {1}",
|
||||||
"UserPasswordChangedWithName": "La password è stata cambiata per l'utente {0}",
|
"UserPasswordChangedWithName": "La password è stata cambiata per l'utente {0}",
|
||||||
"UserPolicyUpdatedWithName": "La policy dell'utente è stata aggiornata per {0}",
|
"UserPolicyUpdatedWithName": "La policy dell'utente è stata aggiornata per {0}",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"Collections": "Collecties",
|
"Collections": "Collecties",
|
||||||
"DeviceOfflineWithName": "Verbinding met {0} is verbroken",
|
"DeviceOfflineWithName": "Verbinding met {0} is verbroken",
|
||||||
"DeviceOnlineWithName": "{0} is verbonden",
|
"DeviceOnlineWithName": "{0} is verbonden",
|
||||||
"FailedLoginAttemptWithUserName": "Mislukte inlogpoging van {0}",
|
"FailedLoginAttemptWithUserName": "Mislukte aanmeldpoging van {0}",
|
||||||
"Favorites": "Favorieten",
|
"Favorites": "Favorieten",
|
||||||
"Folders": "Mappen",
|
"Folders": "Mappen",
|
||||||
"Genres": "Genres",
|
"Genres": "Genres",
|
||||||
@@ -124,7 +124,7 @@
|
|||||||
"TaskKeyframeExtractorDescription": "Haalt keyframes uit videobestanden om preciezere HLS-afspeellijsten te maken. Deze taak kan lang duren.",
|
"TaskKeyframeExtractorDescription": "Haalt keyframes uit videobestanden om preciezere HLS-afspeellijsten te maken. Deze taak kan lang duren.",
|
||||||
"TaskKeyframeExtractor": "Keyframes uitpakken",
|
"TaskKeyframeExtractor": "Keyframes uitpakken",
|
||||||
"External": "Extern",
|
"External": "Extern",
|
||||||
"HearingImpaired": "Slechthorend",
|
"HearingImpaired": "Slechthorenden",
|
||||||
"TaskRefreshTrickplayImages": "Trickplay-afbeeldingen genereren",
|
"TaskRefreshTrickplayImages": "Trickplay-afbeeldingen genereren",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Creëert trickplay-voorvertoningen voor video's in bibliotheken waarvoor dit is ingeschakeld.",
|
"TaskRefreshTrickplayImagesDescription": "Creëert trickplay-voorvertoningen voor video's in bibliotheken waarvoor dit is ingeschakeld.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Collecties en afspeellijsten opruimen",
|
"TaskCleanCollectionsAndPlaylists": "Collecties en afspeellijsten opruimen",
|
||||||
|
|||||||
@@ -118,5 +118,6 @@
|
|||||||
"Undefined": "Udefinert",
|
"Undefined": "Udefinert",
|
||||||
"Forced": "Tvungen",
|
"Forced": "Tvungen",
|
||||||
"Default": "Standard",
|
"Default": "Standard",
|
||||||
"External": "Ekstern"
|
"External": "Ekstern",
|
||||||
|
"HearingImpaired": "Nedsett høyrsel"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"Collections": "Kolekcje",
|
"Collections": "Kolekcje",
|
||||||
"DeviceOfflineWithName": "{0} został rozłączony",
|
"DeviceOfflineWithName": "{0} został rozłączony",
|
||||||
"DeviceOnlineWithName": "{0} połączył się",
|
"DeviceOnlineWithName": "{0} połączył się",
|
||||||
"FailedLoginAttemptWithUserName": "Próba logowania przez {0} zakończona niepowodzeniem",
|
"FailedLoginAttemptWithUserName": "Nieudana próba logowania przez {0}",
|
||||||
"Favorites": "Ulubione",
|
"Favorites": "Ulubione",
|
||||||
"Folders": "Foldery",
|
"Folders": "Foldery",
|
||||||
"Genres": "Gatunki",
|
"Genres": "Gatunki",
|
||||||
@@ -98,8 +98,8 @@
|
|||||||
"TaskRefreshChannels": "Odśwież kanały",
|
"TaskRefreshChannels": "Odśwież kanały",
|
||||||
"TaskCleanTranscodeDescription": "Usuwa transkodowane pliki starsze niż 1 dzień.",
|
"TaskCleanTranscodeDescription": "Usuwa transkodowane pliki starsze niż 1 dzień.",
|
||||||
"TaskCleanTranscode": "Wyczyść folder transkodowania",
|
"TaskCleanTranscode": "Wyczyść folder transkodowania",
|
||||||
"TaskUpdatePluginsDescription": "Pobiera i instaluje aktualizacje dla pluginów, które są skonfigurowane do automatycznej aktualizacji.",
|
"TaskUpdatePluginsDescription": "Pobiera i instaluje aktualizacje wtyczek, które są skonfigurowane do automatycznej aktualizacji.",
|
||||||
"TaskUpdatePlugins": "Aktualizuj pluginy",
|
"TaskUpdatePlugins": "Aktualizuj wtyczki",
|
||||||
"TaskRefreshPeopleDescription": "Odświeża metadane o aktorów i reżyserów w Twojej bibliotece mediów.",
|
"TaskRefreshPeopleDescription": "Odświeża metadane o aktorów i reżyserów w Twojej bibliotece mediów.",
|
||||||
"TaskRefreshPeople": "Odśwież obsadę",
|
"TaskRefreshPeople": "Odśwież obsadę",
|
||||||
"TaskCleanLogsDescription": "Kasuje pliki logów starsze niż {0} dni.",
|
"TaskCleanLogsDescription": "Kasuje pliki logów starsze niż {0} dni.",
|
||||||
|
|||||||
@@ -130,5 +130,5 @@
|
|||||||
"TaskCleanCollectionsAndPlaylists": "Limpe coleções e playlists",
|
"TaskCleanCollectionsAndPlaylists": "Limpe coleções e playlists",
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e playlists que não existem mais.",
|
"TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e playlists que não existem mais.",
|
||||||
"TaskAudioNormalization": "Normalização de áudio",
|
"TaskAudioNormalization": "Normalização de áudio",
|
||||||
"TaskAudioNormalizationDescription": "Verifica arquivos em busca de dados de normalização de áudio."
|
"TaskAudioNormalizationDescription": "Examina os ficheiros em busca de dados de normalização de áudio."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"Collections": "Коллекции",
|
"Collections": "Коллекции",
|
||||||
"DeviceOfflineWithName": "{0} - отключено",
|
"DeviceOfflineWithName": "{0} - отключено",
|
||||||
"DeviceOnlineWithName": "{0} - подключено",
|
"DeviceOnlineWithName": "{0} - подключено",
|
||||||
"FailedLoginAttemptWithUserName": "{0} - попытка входа неудачна",
|
"FailedLoginAttemptWithUserName": "Неудачная попытка входа с {0}",
|
||||||
"Favorites": "Избранное",
|
"Favorites": "Избранное",
|
||||||
"Folders": "Папки",
|
"Folders": "Папки",
|
||||||
"Genres": "Жанры",
|
"Genres": "Жанры",
|
||||||
@@ -128,5 +128,7 @@
|
|||||||
"TaskRefreshTrickplayImages": "Сгенерировать изображения для Trickplay",
|
"TaskRefreshTrickplayImages": "Сгенерировать изображения для Trickplay",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Создает предпросмотры для Trickplay для видео в библиотеках, где эта функция включена.",
|
"TaskRefreshTrickplayImagesDescription": "Создает предпросмотры для Trickplay для видео в библиотеках, где эта функция включена.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Очистка коллекций и списков воспроизведения",
|
"TaskCleanCollectionsAndPlaylists": "Очистка коллекций и списков воспроизведения",
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Удаляет элементы из коллекций и списков воспроизведения, которые больше не существуют."
|
"TaskCleanCollectionsAndPlaylistsDescription": "Удаляет элементы из коллекций и списков воспроизведения, которые больше не существуют.",
|
||||||
|
"TaskAudioNormalization": "Нормализация звука",
|
||||||
|
"TaskAudioNormalizationDescription": "Сканирует файлы на наличие данных о нормализации звука."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,5 +127,8 @@
|
|||||||
"HearingImpaired": "Hörselskadad",
|
"HearingImpaired": "Hörselskadad",
|
||||||
"TaskRefreshTrickplayImages": "Generera Trickplay-bilder",
|
"TaskRefreshTrickplayImages": "Generera Trickplay-bilder",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Skapar trickplay-förhandsvisningar för videor i aktiverade bibliotek.",
|
"TaskRefreshTrickplayImagesDescription": "Skapar trickplay-förhandsvisningar för videor i aktiverade bibliotek.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Rensa samlingar och spellistor"
|
"TaskCleanCollectionsAndPlaylists": "Rensa upp samlingar och spellistor",
|
||||||
|
"TaskAudioNormalization": "Ljudnormalisering",
|
||||||
|
"TaskCleanCollectionsAndPlaylistsDescription": "Tar bort objekt från samlingar och spellistor som inte längre finns.",
|
||||||
|
"TaskAudioNormalizationDescription": "Skannar filer för ljudnormaliseringsdata."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,5 +125,9 @@
|
|||||||
"External": "வெளி",
|
"External": "வெளி",
|
||||||
"HearingImpaired": "செவித்திறன் குறைபாடுடையவர்",
|
"HearingImpaired": "செவித்திறன் குறைபாடுடையவர்",
|
||||||
"TaskRefreshTrickplayImages": "முன்னோட்ட படங்களை உருவாக்கு",
|
"TaskRefreshTrickplayImages": "முன்னோட்ட படங்களை உருவாக்கு",
|
||||||
"TaskRefreshTrickplayImagesDescription": "செயல்பாட்டில் உள்ள தொகுப்புகளுக்கு முன்னோட்ட படங்களை உருவாக்கும்."
|
"TaskRefreshTrickplayImagesDescription": "செயல்பாட்டில் உள்ள தொகுப்புகளுக்கு முன்னோட்ட படங்களை உருவாக்கும்.",
|
||||||
|
"TaskCleanCollectionsAndPlaylists": "சேகரிப்புகள் மற்றும் பிளேலிஸ்ட்களை சுத்தம் செய்யவும்",
|
||||||
|
"TaskCleanCollectionsAndPlaylistsDescription": "சேகரிப்புகள் மற்றும் பிளேலிஸ்ட்களில் இருந்து உருப்படிகளை நீக்குகிறது.",
|
||||||
|
"TaskAudioNormalization": "ஆடியோ இயல்பாக்கம்",
|
||||||
|
"TaskAudioNormalizationDescription": "ஆடியோ இயல்பாக்குதல் தரவுக்காக கோப்புகளை ஸ்கேன் செய்கிறது."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"Collections": "Koleksiyonlar",
|
"Collections": "Koleksiyonlar",
|
||||||
"DeviceOfflineWithName": "{0} bağlantısı kesildi",
|
"DeviceOfflineWithName": "{0} bağlantısı kesildi",
|
||||||
"DeviceOnlineWithName": "{0} bağlı",
|
"DeviceOnlineWithName": "{0} bağlı",
|
||||||
"FailedLoginAttemptWithUserName": "{0} kullanıcısının giriş denemesi başarısız oldu",
|
"FailedLoginAttemptWithUserName": "{0} kullanıcısının başarısız oturum açma girişimi",
|
||||||
"Favorites": "Favoriler",
|
"Favorites": "Favoriler",
|
||||||
"Folders": "Klasörler",
|
"Folders": "Klasörler",
|
||||||
"Genres": "Türler",
|
"Genres": "Türler",
|
||||||
|
|||||||
@@ -103,7 +103,7 @@
|
|||||||
"HeaderFavoriteEpisodes": "Tập Phim Yêu Thích",
|
"HeaderFavoriteEpisodes": "Tập Phim Yêu Thích",
|
||||||
"HeaderFavoriteArtists": "Nghệ Sĩ Yêu Thích",
|
"HeaderFavoriteArtists": "Nghệ Sĩ Yêu Thích",
|
||||||
"HeaderFavoriteAlbums": "Album Ưa Thích",
|
"HeaderFavoriteAlbums": "Album Ưa Thích",
|
||||||
"FailedLoginAttemptWithUserName": "Đăng nhập không thành công thử từ {0}",
|
"FailedLoginAttemptWithUserName": "Nỗ lực đăng nhập không thành công từ {0}",
|
||||||
"DeviceOnlineWithName": "{0} đã kết nối",
|
"DeviceOnlineWithName": "{0} đã kết nối",
|
||||||
"DeviceOfflineWithName": "{0} đã ngắt kết nối",
|
"DeviceOfflineWithName": "{0} đã ngắt kết nối",
|
||||||
"ChapterNameValue": "Phân Cảnh {0}",
|
"ChapterNameValue": "Phân Cảnh {0}",
|
||||||
@@ -127,5 +127,7 @@
|
|||||||
"TaskRefreshTrickplayImages": "Tạo Ảnh Xem Trước Trickplay",
|
"TaskRefreshTrickplayImages": "Tạo Ảnh Xem Trước Trickplay",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Tạo bản xem trước trịckplay cho video trong thư viện đã bật.",
|
"TaskRefreshTrickplayImagesDescription": "Tạo bản xem trước trịckplay cho video trong thư viện đã bật.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Dọn dẹp bộ sưu tập và danh sách phát",
|
"TaskCleanCollectionsAndPlaylists": "Dọn dẹp bộ sưu tập và danh sách phát",
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Xóa các mục khỏi bộ sưu tập và danh sách phát không còn tồn tại."
|
"TaskCleanCollectionsAndPlaylistsDescription": "Xóa các mục khỏi bộ sưu tập và danh sách phát không còn tồn tại.",
|
||||||
|
"TaskAudioNormalization": "Chuẩn Hóa Âm Thanh",
|
||||||
|
"TaskAudioNormalizationDescription": "Quét tập tin để tìm dữ liệu chuẩn hóa âm thanh."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"Collections": "合集",
|
"Collections": "合集",
|
||||||
"DeviceOfflineWithName": "{0} 已断开",
|
"DeviceOfflineWithName": "{0} 已断开",
|
||||||
"DeviceOnlineWithName": "{0} 已连接",
|
"DeviceOnlineWithName": "{0} 已连接",
|
||||||
"FailedLoginAttemptWithUserName": "从 {0} 尝试登录失败",
|
"FailedLoginAttemptWithUserName": "来自 {0} 的登录尝试失败",
|
||||||
"Favorites": "我的最爱",
|
"Favorites": "我的最爱",
|
||||||
"Folders": "文件夹",
|
"Folders": "文件夹",
|
||||||
"Genres": "类型",
|
"Genres": "类型",
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
Exempt,0
|
Exempt,0
|
||||||
G,0
|
G,0
|
||||||
7+,7
|
7+,7
|
||||||
PG,15
|
|
||||||
M,15
|
M,15
|
||||||
MA,15
|
MA,15
|
||||||
MA15+,15
|
MA15+,15
|
||||||
MA 15+,15
|
MA 15+,15
|
||||||
|
PG,16
|
||||||
16+,16
|
16+,16
|
||||||
R,18
|
R,18
|
||||||
R18+,18
|
R18+,18
|
||||||
|
|||||||
|
@@ -170,13 +170,8 @@ namespace Emby.Server.Implementations.Playlists
|
|||||||
private List<Playlist> GetUserPlaylists(Guid userId)
|
private List<Playlist> GetUserPlaylists(Guid userId)
|
||||||
{
|
{
|
||||||
var user = _userManager.GetUserById(userId);
|
var user = _userManager.GetUserById(userId);
|
||||||
var playlistsFolder = GetPlaylistsFolder(userId);
|
|
||||||
if (playlistsFolder is null)
|
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return playlistsFolder.GetChildren(user, true).OfType<Playlist>().ToList();
|
return GetPlaylistsFolder(userId).GetChildren(user, true).OfType<Playlist>().ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetTargetPath(string path)
|
private static string GetTargetPath(string path)
|
||||||
@@ -189,11 +184,11 @@ namespace Emby.Server.Implementations.Playlists
|
|||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IReadOnlyList<BaseItem> GetPlaylistItems(IEnumerable<Guid> itemIds, User user, DtoOptions options)
|
private IReadOnlyList<BaseItem> GetPlaylistItems(IEnumerable<Guid> itemIds, MediaType playlistMediaType, User user, DtoOptions options)
|
||||||
{
|
{
|
||||||
var items = itemIds.Select(_libraryManager.GetItemById).Where(i => i is not null);
|
var items = itemIds.Select(_libraryManager.GetItemById).Where(i => i is not null);
|
||||||
|
|
||||||
return Playlist.GetPlaylistItems(items, user, options);
|
return Playlist.GetPlaylistItems(playlistMediaType, items, user, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId)
|
public Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId)
|
||||||
@@ -213,7 +208,7 @@ namespace Emby.Server.Implementations.Playlists
|
|||||||
?? throw new ArgumentException("No Playlist exists with Id " + playlistId);
|
?? throw new ArgumentException("No Playlist exists with Id " + playlistId);
|
||||||
|
|
||||||
// Retrieve all the items to be added to the playlist
|
// Retrieve all the items to be added to the playlist
|
||||||
var newItems = GetPlaylistItems(newItemIds, user, options)
|
var newItems = GetPlaylistItems(newItemIds, playlist.MediaType, user, options)
|
||||||
.Where(i => i.SupportsAddingToPlaylist);
|
.Where(i => i.SupportsAddingToPlaylist);
|
||||||
|
|
||||||
// Filter out duplicate items, if necessary
|
// Filter out duplicate items, if necessary
|
||||||
|
|||||||
@@ -106,20 +106,13 @@ public partial class AudioNormalizationTask : IScheduledTask
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Calculating LUFS for album: {Album} with id: {Id}", a.Name, a.Id);
|
var tempFile = Path.Join(_configurationManager.GetTranscodePath(), Guid.NewGuid() + ".concat");
|
||||||
var tempFile = Path.Join(_configurationManager.GetTranscodePath(), a.Id + ".concat");
|
|
||||||
var inputLines = albumTracks.Select(x => string.Format(CultureInfo.InvariantCulture, "file '{0}'", x.Path.Replace("'", @"'\''", StringComparison.Ordinal)));
|
var inputLines = albumTracks.Select(x => string.Format(CultureInfo.InvariantCulture, "file '{0}'", x.Path.Replace("'", @"'\''", StringComparison.Ordinal)));
|
||||||
await File.WriteAllLinesAsync(tempFile, inputLines, cancellationToken).ConfigureAwait(false);
|
await File.WriteAllLinesAsync(tempFile, inputLines, cancellationToken).ConfigureAwait(false);
|
||||||
try
|
a.LUFS = await CalculateLUFSAsync(
|
||||||
{
|
string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile),
|
||||||
a.LUFS = await CalculateLUFSAsync(
|
cancellationToken).ConfigureAwait(false);
|
||||||
string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile),
|
File.Delete(tempFile);
|
||||||
cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
File.Delete(tempFile);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_itemRepository.SaveItems(albums, cancellationToken);
|
_itemRepository.SaveItems(albums, cancellationToken);
|
||||||
|
|||||||
@@ -127,8 +127,15 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask
|
|||||||
{
|
{
|
||||||
_logger.LogDebug("Updating {FolderName}", folder.Name);
|
_logger.LogDebug("Updating {FolderName}", folder.Name);
|
||||||
folder.LinkedChildren = folder.LinkedChildren.Except(itemsToRemove).ToArray();
|
folder.LinkedChildren = folder.LinkedChildren.Except(itemsToRemove).ToArray();
|
||||||
_providerManager.SaveMetadataAsync(folder, ItemUpdateType.MetadataEdit);
|
|
||||||
folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken);
|
folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken);
|
||||||
|
|
||||||
|
_providerManager.QueueRefresh(
|
||||||
|
folder.Id,
|
||||||
|
new MetadataRefreshOptions(new DirectoryService(_fileSystem))
|
||||||
|
{
|
||||||
|
ForceSave = true
|
||||||
|
},
|
||||||
|
RefreshPriority.High);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Controller.IO;
|
|
||||||
using MediaBrowser.Model.Globalization;
|
using MediaBrowser.Model.Globalization;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Model.Tasks;
|
using MediaBrowser.Model.Tasks;
|
||||||
@@ -134,14 +133,53 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
|
|||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
FileSystemHelper.DeleteFile(_fileSystem, file.FullName, _logger);
|
DeleteFile(file.FullName);
|
||||||
|
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
|
|
||||||
FileSystemHelper.DeleteEmptyFolders(_fileSystem, directory, _logger);
|
DeleteEmptyFolders(directory);
|
||||||
|
|
||||||
progress.Report(100);
|
progress.Report(100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DeleteEmptyFolders(string parent)
|
||||||
|
{
|
||||||
|
foreach (var directory in _fileSystem.GetDirectoryPaths(parent))
|
||||||
|
{
|
||||||
|
DeleteEmptyFolders(directory);
|
||||||
|
if (!_fileSystem.GetFileSystemEntryPaths(directory).Any())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.Delete(directory, false);
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error deleting directory {Path}", directory);
|
||||||
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error deleting directory {Path}", directory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DeleteFile(string path)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_fileSystem.DeleteFile(path);
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error deleting file {Path}", path);
|
||||||
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error deleting file {Path}", path);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Controller.IO;
|
|
||||||
using MediaBrowser.Model.Globalization;
|
using MediaBrowser.Model.Globalization;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Model.Tasks;
|
using MediaBrowser.Model.Tasks;
|
||||||
@@ -113,14 +113,53 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
|
|||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
FileSystemHelper.DeleteFile(_fileSystem, file.FullName, _logger);
|
DeleteFile(file.FullName);
|
||||||
|
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
|
|
||||||
FileSystemHelper.DeleteEmptyFolders(_fileSystem, directory, _logger);
|
DeleteEmptyFolders(directory);
|
||||||
|
|
||||||
progress.Report(100);
|
progress.Report(100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DeleteEmptyFolders(string parent)
|
||||||
|
{
|
||||||
|
foreach (var directory in _fileSystem.GetDirectoryPaths(parent))
|
||||||
|
{
|
||||||
|
DeleteEmptyFolders(directory);
|
||||||
|
if (!_fileSystem.GetFileSystemEntryPaths(directory).Any())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.Delete(directory, false);
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error deleting directory {Path}", directory);
|
||||||
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error deleting directory {Path}", directory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DeleteFile(string path)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_fileSystem.DeleteFile(path);
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error deleting file {Path}", path);
|
||||||
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error deleting file {Path}", path);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ namespace Emby.Server.Implementations.Session
|
|||||||
ArgumentException.ThrowIfNullOrEmpty(deviceId);
|
ArgumentException.ThrowIfNullOrEmpty(deviceId);
|
||||||
|
|
||||||
var activityDate = DateTime.UtcNow;
|
var activityDate = DateTime.UtcNow;
|
||||||
var session = GetSessionInfo(appName, appVersion, deviceId, deviceName, remoteEndPoint, user);
|
var session = await GetSessionInfo(appName, appVersion, deviceId, deviceName, remoteEndPoint, user).ConfigureAwait(false);
|
||||||
var lastActivityDate = session.LastActivityDate;
|
var lastActivityDate = session.LastActivityDate;
|
||||||
session.LastActivityDate = activityDate;
|
session.LastActivityDate = activityDate;
|
||||||
|
|
||||||
@@ -435,7 +435,7 @@ namespace Emby.Server.Implementations.Session
|
|||||||
/// <param name="remoteEndPoint">The remote end point.</param>
|
/// <param name="remoteEndPoint">The remote end point.</param>
|
||||||
/// <param name="user">The user.</param>
|
/// <param name="user">The user.</param>
|
||||||
/// <returns>SessionInfo.</returns>
|
/// <returns>SessionInfo.</returns>
|
||||||
private SessionInfo GetSessionInfo(
|
private async Task<SessionInfo> GetSessionInfo(
|
||||||
string appName,
|
string appName,
|
||||||
string appVersion,
|
string appVersion,
|
||||||
string deviceId,
|
string deviceId,
|
||||||
@@ -453,7 +453,7 @@ namespace Emby.Server.Implementations.Session
|
|||||||
|
|
||||||
if (!_activeConnections.TryGetValue(key, out var sessionInfo))
|
if (!_activeConnections.TryGetValue(key, out var sessionInfo))
|
||||||
{
|
{
|
||||||
sessionInfo = CreateSession(key, appName, appVersion, deviceId, deviceName, remoteEndPoint, user);
|
sessionInfo = await CreateSession(key, appName, appVersion, deviceId, deviceName, remoteEndPoint, user).ConfigureAwait(false);
|
||||||
_activeConnections[key] = sessionInfo;
|
_activeConnections[key] = sessionInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,7 +478,7 @@ namespace Emby.Server.Implementations.Session
|
|||||||
return sessionInfo;
|
return sessionInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
private SessionInfo CreateSession(
|
private async Task<SessionInfo> CreateSession(
|
||||||
string key,
|
string key,
|
||||||
string appName,
|
string appName,
|
||||||
string appVersion,
|
string appVersion,
|
||||||
@@ -508,7 +508,7 @@ namespace Emby.Server.Implementations.Session
|
|||||||
deviceName = "Network Device";
|
deviceName = "Network Device";
|
||||||
}
|
}
|
||||||
|
|
||||||
var deviceOptions = _deviceManager.GetDeviceOptions(deviceId);
|
var deviceOptions = await _deviceManager.GetDeviceOptions(deviceId).ConfigureAwait(false);
|
||||||
if (string.IsNullOrEmpty(deviceOptions.CustomName))
|
if (string.IsNullOrEmpty(deviceOptions.CustomName))
|
||||||
{
|
{
|
||||||
sessionInfo.DeviceName = deviceName;
|
sessionInfo.DeviceName = deviceName;
|
||||||
@@ -1297,7 +1297,7 @@ namespace Emby.Server.Implementations.Session
|
|||||||
return new[] { item };
|
return new[] { item };
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<BaseItem> TranslateItemForInstantMix(Guid id, User user)
|
private IEnumerable<BaseItem> TranslateItemForInstantMix(Guid id, User user)
|
||||||
{
|
{
|
||||||
var item = _libraryManager.GetItemById(id);
|
var item = _libraryManager.GetItemById(id);
|
||||||
|
|
||||||
@@ -1307,7 +1307,7 @@ namespace Emby.Server.Implementations.Session
|
|||||||
return new List<BaseItem>();
|
return new List<BaseItem>();
|
||||||
}
|
}
|
||||||
|
|
||||||
return _musicManager.GetInstantMixFromItem(item, user, new DtoOptions(false) { EnableImages = false }).ToList();
|
return _musicManager.GetInstantMixFromItem(item, user, new DtoOptions(false) { EnableImages = false });
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -1520,12 +1520,12 @@ namespace Emby.Server.Implementations.Session
|
|||||||
// This should be validated above, but if it isn't don't delete all tokens.
|
// This should be validated above, but if it isn't don't delete all tokens.
|
||||||
ArgumentException.ThrowIfNullOrEmpty(deviceId);
|
ArgumentException.ThrowIfNullOrEmpty(deviceId);
|
||||||
|
|
||||||
var existing = _deviceManager.GetDevices(
|
var existing = (await _deviceManager.GetDevices(
|
||||||
new DeviceQuery
|
new DeviceQuery
|
||||||
{
|
{
|
||||||
DeviceId = deviceId,
|
DeviceId = deviceId,
|
||||||
UserId = user.Id
|
UserId = user.Id
|
||||||
}).Items;
|
}).ConfigureAwait(false)).Items;
|
||||||
|
|
||||||
foreach (var auth in existing)
|
foreach (var auth in existing)
|
||||||
{
|
{
|
||||||
@@ -1553,12 +1553,12 @@ namespace Emby.Server.Implementations.Session
|
|||||||
|
|
||||||
ArgumentException.ThrowIfNullOrEmpty(accessToken);
|
ArgumentException.ThrowIfNullOrEmpty(accessToken);
|
||||||
|
|
||||||
var existing = _deviceManager.GetDevices(
|
var existing = (await _deviceManager.GetDevices(
|
||||||
new DeviceQuery
|
new DeviceQuery
|
||||||
{
|
{
|
||||||
Limit = 1,
|
Limit = 1,
|
||||||
AccessToken = accessToken
|
AccessToken = accessToken
|
||||||
}).Items;
|
}).ConfigureAwait(false)).Items;
|
||||||
|
|
||||||
if (existing.Count > 0)
|
if (existing.Count > 0)
|
||||||
{
|
{
|
||||||
@@ -1597,10 +1597,10 @@ namespace Emby.Server.Implementations.Session
|
|||||||
{
|
{
|
||||||
CheckDisposed();
|
CheckDisposed();
|
||||||
|
|
||||||
var existing = _deviceManager.GetDevices(new DeviceQuery
|
var existing = await _deviceManager.GetDevices(new DeviceQuery
|
||||||
{
|
{
|
||||||
UserId = userId
|
UserId = userId
|
||||||
});
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
foreach (var info in existing.Items)
|
foreach (var info in existing.Items)
|
||||||
{
|
{
|
||||||
@@ -1787,11 +1787,11 @@ namespace Emby.Server.Implementations.Session
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<SessionInfo> GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint)
|
public async Task<SessionInfo> GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint)
|
||||||
{
|
{
|
||||||
var items = _deviceManager.GetDevices(new DeviceQuery
|
var items = (await _deviceManager.GetDevices(new DeviceQuery
|
||||||
{
|
{
|
||||||
AccessToken = token,
|
AccessToken = token,
|
||||||
Limit = 1
|
Limit = 1
|
||||||
}).Items;
|
}).ConfigureAwait(false)).Items;
|
||||||
|
|
||||||
if (items.Count == 0)
|
if (items.Count == 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -47,10 +47,10 @@ public class DevicesController : BaseJellyfinApiController
|
|||||||
/// <returns>An <see cref="OkResult"/> containing the list of devices.</returns>
|
/// <returns>An <see cref="OkResult"/> containing the list of devices.</returns>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public ActionResult<QueryResult<DeviceInfo>> GetDevices([FromQuery] Guid? userId)
|
public async Task<ActionResult<QueryResult<DeviceInfo>>> GetDevices([FromQuery] Guid? userId)
|
||||||
{
|
{
|
||||||
userId = RequestHelpers.GetUserId(User, userId);
|
userId = RequestHelpers.GetUserId(User, userId);
|
||||||
return _deviceManager.GetDevicesForUser(userId);
|
return await _deviceManager.GetDevicesForUser(userId).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -63,9 +63,9 @@ public class DevicesController : BaseJellyfinApiController
|
|||||||
[HttpGet("Info")]
|
[HttpGet("Info")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public ActionResult<DeviceInfo> GetDeviceInfo([FromQuery, Required] string id)
|
public async Task<ActionResult<DeviceInfo>> GetDeviceInfo([FromQuery, Required] string id)
|
||||||
{
|
{
|
||||||
var deviceInfo = _deviceManager.GetDevice(id);
|
var deviceInfo = await _deviceManager.GetDevice(id).ConfigureAwait(false);
|
||||||
if (deviceInfo is null)
|
if (deviceInfo is null)
|
||||||
{
|
{
|
||||||
return NotFound();
|
return NotFound();
|
||||||
@@ -84,9 +84,9 @@ public class DevicesController : BaseJellyfinApiController
|
|||||||
[HttpGet("Options")]
|
[HttpGet("Options")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public ActionResult<DeviceOptions> GetDeviceOptions([FromQuery, Required] string id)
|
public async Task<ActionResult<DeviceOptions>> GetDeviceOptions([FromQuery, Required] string id)
|
||||||
{
|
{
|
||||||
var deviceInfo = _deviceManager.GetDeviceOptions(id);
|
var deviceInfo = await _deviceManager.GetDeviceOptions(id).ConfigureAwait(false);
|
||||||
if (deviceInfo is null)
|
if (deviceInfo is null)
|
||||||
{
|
{
|
||||||
return NotFound();
|
return NotFound();
|
||||||
@@ -124,13 +124,13 @@ public class DevicesController : BaseJellyfinApiController
|
|||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<ActionResult> DeleteDevice([FromQuery, Required] string id)
|
public async Task<ActionResult> DeleteDevice([FromQuery, Required] string id)
|
||||||
{
|
{
|
||||||
var existingDevice = _deviceManager.GetDevice(id);
|
var existingDevice = await _deviceManager.GetDevice(id).ConfigureAwait(false);
|
||||||
if (existingDevice is null)
|
if (existingDevice is null)
|
||||||
{
|
{
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
var sessions = _deviceManager.GetDevices(new DeviceQuery { DeviceId = id });
|
var sessions = await _deviceManager.GetDevices(new DeviceQuery { DeviceId = id }).ConfigureAwait(false);
|
||||||
|
|
||||||
foreach (var session in sessions.Items)
|
foreach (var session in sessions.Items)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2089,8 +2089,6 @@ public class ImageController : BaseJellyfinApiController
|
|||||||
Response.Headers.Append(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateImageModified).TotalSeconds).ToString(CultureInfo.InvariantCulture));
|
Response.Headers.Append(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateImageModified).TotalSeconds).ToString(CultureInfo.InvariantCulture));
|
||||||
Response.Headers.Append(HeaderNames.Vary, HeaderNames.Accept);
|
Response.Headers.Append(HeaderNames.Vary, HeaderNames.Accept);
|
||||||
|
|
||||||
Response.Headers.ContentDisposition = "attachment";
|
|
||||||
|
|
||||||
if (disableCaching)
|
if (disableCaching)
|
||||||
{
|
{
|
||||||
Response.Headers.Append(HeaderNames.CacheControl, "no-cache, no-store, must-revalidate");
|
Response.Headers.Append(HeaderNames.CacheControl, "no-cache, no-store, must-revalidate");
|
||||||
|
|||||||
@@ -80,8 +80,7 @@ public class ItemRefreshController : BaseJellyfinApiController
|
|||||||
|| imageRefreshMode == MetadataRefreshMode.FullRefresh
|
|| imageRefreshMode == MetadataRefreshMode.FullRefresh
|
||||||
|| replaceAllImages
|
|| replaceAllImages
|
||||||
|| replaceAllMetadata,
|
|| replaceAllMetadata,
|
||||||
IsAutomated = false,
|
IsAutomated = false
|
||||||
RemoveOldMetadata = replaceAllMetadata
|
|
||||||
};
|
};
|
||||||
|
|
||||||
_providerManager.QueueRefresh(item.Id, refreshOptions, RefreshPriority.High);
|
_providerManager.QueueRefresh(item.Id, refreshOptions, RefreshPriority.High);
|
||||||
|
|||||||
@@ -180,21 +180,7 @@ public class LibraryStructureController : BaseJellyfinApiController
|
|||||||
// No need to start if scanning the library because it will handle it
|
// No need to start if scanning the library because it will handle it
|
||||||
if (refreshLibrary)
|
if (refreshLibrary)
|
||||||
{
|
{
|
||||||
await _libraryManager.ValidateTopLibraryFolders(CancellationToken.None, true).ConfigureAwait(false);
|
await _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None).ConfigureAwait(false);
|
||||||
var newLib = _libraryManager.GetUserRootFolder().Children.FirstOrDefault(f => f.Path.Equals(newPath, StringComparison.OrdinalIgnoreCase));
|
|
||||||
if (newLib is CollectionFolder folder)
|
|
||||||
{
|
|
||||||
foreach (var child in folder.GetPhysicalFolders())
|
|
||||||
{
|
|
||||||
await child.RefreshMetadata(CancellationToken.None).ConfigureAwait(false);
|
|
||||||
await child.ValidateChildren(new Progress<double>(), CancellationToken.None).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// We don't know if this one can be validated individually, trigger a new validation
|
|
||||||
await _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -233,8 +233,6 @@ public class PluginsController : BaseJellyfinApiController
|
|||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
Response.Headers.ContentDisposition = "attachment";
|
|
||||||
|
|
||||||
imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath);
|
imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath);
|
||||||
return PhysicalFile(imagePath, MimeTypes.GetMimeType(imagePath));
|
return PhysicalFile(imagePath, MimeTypes.GetMimeType(imagePath));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,7 +95,6 @@ public class TrickplayController : BaseJellyfinApiController
|
|||||||
var path = _trickplayManager.GetTrickplayTilePath(item, width, index);
|
var path = _trickplayManager.GetTrickplayTilePath(item, width, index);
|
||||||
if (System.IO.File.Exists(path))
|
if (System.IO.File.Exists(path))
|
||||||
{
|
{
|
||||||
Response.Headers.ContentDisposition = "attachment";
|
|
||||||
return PhysicalFile(path, MediaTypeNames.Image.Jpeg);
|
return PhysicalFile(path, MediaTypeNames.Image.Jpeg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -154,11 +154,6 @@ public static class StreamingHelpers
|
|||||||
// Some channels from HDHomerun will experience A/V sync issues
|
// Some channels from HDHomerun will experience A/V sync issues
|
||||||
streamingRequest.SegmentContainer = "ts";
|
streamingRequest.SegmentContainer = "ts";
|
||||||
streamingRequest.VideoCodec = "h264";
|
streamingRequest.VideoCodec = "h264";
|
||||||
streamingRequest.AudioCodec = "aac";
|
|
||||||
state.SupportedVideoCodecs = ["h264"];
|
|
||||||
state.Request.VideoCodec = "h264";
|
|
||||||
state.SupportedAudioCodecs = ["aac"];
|
|
||||||
state.Request.AudioCodec = "aac";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var liveStreamInfo = await mediaSourceManager.GetLiveStreamWithDirectStreamProvider(streamingRequest.LiveStreamId, cancellationToken).ConfigureAwait(false);
|
var liveStreamInfo = await mediaSourceManager.GetLiveStreamWithDirectStreamProvider(streamingRequest.LiveStreamId, cancellationToken).ConfigureAwait(false);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Data</PackageId>
|
<PackageId>Jellyfin.Data</PackageId>
|
||||||
<VersionPrefix>10.9.10</VersionPrefix>
|
<VersionPrefix>10.10.0</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -27,8 +27,6 @@ namespace Jellyfin.Server.Implementations.Devices
|
|||||||
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
|
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
|
||||||
private readonly IUserManager _userManager;
|
private readonly IUserManager _userManager;
|
||||||
private readonly ConcurrentDictionary<string, ClientCapabilities> _capabilitiesMap = new();
|
private readonly ConcurrentDictionary<string, ClientCapabilities> _capabilitiesMap = new();
|
||||||
private readonly ConcurrentDictionary<int, Device> _devices;
|
|
||||||
private readonly ConcurrentDictionary<string, DeviceOptions> _deviceOptions;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="DeviceManager"/> class.
|
/// Initializes a new instance of the <see cref="DeviceManager"/> class.
|
||||||
@@ -39,23 +37,6 @@ namespace Jellyfin.Server.Implementations.Devices
|
|||||||
{
|
{
|
||||||
_dbProvider = dbProvider;
|
_dbProvider = dbProvider;
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_devices = new ConcurrentDictionary<int, Device>();
|
|
||||||
_deviceOptions = new ConcurrentDictionary<string, DeviceOptions>();
|
|
||||||
|
|
||||||
using var dbContext = _dbProvider.CreateDbContext();
|
|
||||||
foreach (var device in dbContext.Devices
|
|
||||||
.OrderBy(d => d.Id)
|
|
||||||
.AsEnumerable())
|
|
||||||
{
|
|
||||||
_devices.TryAdd(device.Id, device);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var deviceOption in dbContext.DeviceOptions
|
|
||||||
.OrderBy(d => d.Id)
|
|
||||||
.AsEnumerable())
|
|
||||||
{
|
|
||||||
_deviceOptions.TryAdd(deviceOption.DeviceId, deviceOption);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -85,8 +66,6 @@ namespace Jellyfin.Server.Implementations.Devices
|
|||||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
_deviceOptions[deviceId] = deviceOptions;
|
|
||||||
|
|
||||||
DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs<Tuple<string, DeviceOptions>>(new Tuple<string, DeviceOptions>(deviceId, deviceOptions)));
|
DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs<Tuple<string, DeviceOptions>>(new Tuple<string, DeviceOptions>(deviceId, deviceOptions)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,17 +76,25 @@ namespace Jellyfin.Server.Implementations.Devices
|
|||||||
await using (dbContext.ConfigureAwait(false))
|
await using (dbContext.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
dbContext.Devices.Add(device);
|
dbContext.Devices.Add(device);
|
||||||
|
|
||||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||||
_devices.TryAdd(device.Id, device);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return device;
|
return device;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public DeviceOptions GetDeviceOptions(string deviceId)
|
public async Task<DeviceOptions> GetDeviceOptions(string deviceId)
|
||||||
{
|
{
|
||||||
_deviceOptions.TryGetValue(deviceId, out var deviceOptions);
|
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
|
DeviceOptions? deviceOptions;
|
||||||
|
await using (dbContext.ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
deviceOptions = await dbContext.DeviceOptions
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(d => d.DeviceId == deviceId)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
return deviceOptions ?? new DeviceOptions(deviceId);
|
return deviceOptions ?? new DeviceOptions(deviceId);
|
||||||
}
|
}
|
||||||
@@ -121,43 +108,57 @@ namespace Jellyfin.Server.Implementations.Devices
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public DeviceInfo? GetDevice(string id)
|
public async Task<DeviceInfo?> GetDevice(string id)
|
||||||
{
|
{
|
||||||
var device = _devices.Values.Where(d => d.DeviceId == id).OrderByDescending(d => d.DateLastActivity).FirstOrDefault();
|
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
_deviceOptions.TryGetValue(id, out var deviceOption);
|
await using (dbContext.ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
var device = await dbContext.Devices
|
||||||
|
.Where(d => d.DeviceId == id)
|
||||||
|
.OrderByDescending(d => d.DateLastActivity)
|
||||||
|
.Include(d => d.User)
|
||||||
|
.SelectMany(d => dbContext.DeviceOptions.Where(o => o.DeviceId == d.DeviceId).DefaultIfEmpty(), (d, o) => new { Device = d, Options = o })
|
||||||
|
.FirstOrDefaultAsync()
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
var deviceInfo = device is null ? null : ToDeviceInfo(device, deviceOption);
|
var deviceInfo = device is null ? null : ToDeviceInfo(device.Device, device.Options);
|
||||||
return deviceInfo;
|
|
||||||
|
return deviceInfo;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public QueryResult<Device> GetDevices(DeviceQuery query)
|
public async Task<QueryResult<Device>> GetDevices(DeviceQuery query)
|
||||||
{
|
{
|
||||||
IEnumerable<Device> devices = _devices.Values
|
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
.Where(device => !query.UserId.HasValue || device.UserId.Equals(query.UserId.Value))
|
await using (dbContext.ConfigureAwait(false))
|
||||||
.Where(device => query.DeviceId == null || device.DeviceId == query.DeviceId)
|
|
||||||
.Where(device => query.AccessToken == null || device.AccessToken == query.AccessToken)
|
|
||||||
.OrderBy(d => d.Id)
|
|
||||||
.ToList();
|
|
||||||
var count = devices.Count();
|
|
||||||
|
|
||||||
if (query.Skip.HasValue)
|
|
||||||
{
|
{
|
||||||
devices = devices.Skip(query.Skip.Value);
|
var devices = dbContext.Devices
|
||||||
}
|
.OrderBy(d => d.Id)
|
||||||
|
.Where(device => !query.UserId.HasValue || device.UserId.Equals(query.UserId.Value))
|
||||||
|
.Where(device => query.DeviceId == null || device.DeviceId == query.DeviceId)
|
||||||
|
.Where(device => query.AccessToken == null || device.AccessToken == query.AccessToken);
|
||||||
|
|
||||||
if (query.Limit.HasValue)
|
var count = await devices.CountAsync().ConfigureAwait(false);
|
||||||
{
|
|
||||||
devices = devices.Take(query.Limit.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new QueryResult<Device>(query.Skip, count, devices.ToList());
|
if (query.Skip.HasValue)
|
||||||
|
{
|
||||||
|
devices = devices.Skip(query.Skip.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.Limit.HasValue)
|
||||||
|
{
|
||||||
|
devices = devices.Take(query.Limit.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new QueryResult<Device>(query.Skip, count, await devices.ToListAsync().ConfigureAwait(false));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public QueryResult<DeviceInfo> GetDeviceInfos(DeviceQuery query)
|
public async Task<QueryResult<DeviceInfo>> GetDeviceInfos(DeviceQuery query)
|
||||||
{
|
{
|
||||||
var devices = GetDevices(query);
|
var devices = await GetDevices(query).ConfigureAwait(false);
|
||||||
|
|
||||||
return new QueryResult<DeviceInfo>(
|
return new QueryResult<DeviceInfo>(
|
||||||
devices.StartIndex,
|
devices.StartIndex,
|
||||||
@@ -166,36 +167,38 @@ namespace Jellyfin.Server.Implementations.Devices
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public QueryResult<DeviceInfo> GetDevicesForUser(Guid? userId)
|
public async Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId)
|
||||||
{
|
{
|
||||||
IEnumerable<Device> devices = _devices.Values
|
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
.OrderByDescending(d => d.DateLastActivity)
|
await using (dbContext.ConfigureAwait(false))
|
||||||
.ThenBy(d => d.DeviceId);
|
|
||||||
|
|
||||||
if (!userId.IsNullOrEmpty())
|
|
||||||
{
|
{
|
||||||
var user = _userManager.GetUserById(userId.Value);
|
var sessions = dbContext.Devices
|
||||||
if (user is null)
|
.Include(d => d.User)
|
||||||
|
.OrderByDescending(d => d.DateLastActivity)
|
||||||
|
.ThenBy(d => d.DeviceId)
|
||||||
|
.SelectMany(d => dbContext.DeviceOptions.Where(o => o.DeviceId == d.DeviceId).DefaultIfEmpty(), (d, o) => new { Device = d, Options = o })
|
||||||
|
.AsAsyncEnumerable();
|
||||||
|
|
||||||
|
if (!userId.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
throw new ResourceNotFoundException();
|
var user = _userManager.GetUserById(userId.Value);
|
||||||
|
if (user is null)
|
||||||
|
{
|
||||||
|
throw new ResourceNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
sessions = sessions.Where(i => CanAccessDevice(user, i.Device.DeviceId));
|
||||||
}
|
}
|
||||||
|
|
||||||
devices = devices.Where(i => CanAccessDevice(user, i.DeviceId));
|
var array = await sessions.Select(device => ToDeviceInfo(device.Device, device.Options)).ToArrayAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
return new QueryResult<DeviceInfo>(array);
|
||||||
}
|
}
|
||||||
|
|
||||||
var array = devices.Select(device =>
|
|
||||||
{
|
|
||||||
_deviceOptions.TryGetValue(device.DeviceId, out var option);
|
|
||||||
return ToDeviceInfo(device, option);
|
|
||||||
}).ToArray();
|
|
||||||
|
|
||||||
return new QueryResult<DeviceInfo>(array);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task DeleteDevice(Device device)
|
public async Task DeleteDevice(Device device)
|
||||||
{
|
{
|
||||||
_devices.TryRemove(device.Id, out _);
|
|
||||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
await using (dbContext.ConfigureAwait(false))
|
await using (dbContext.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
@@ -204,19 +207,6 @@ namespace Jellyfin.Server.Implementations.Devices
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task UpdateDevice(Device device)
|
|
||||||
{
|
|
||||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
|
||||||
await using (dbContext.ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
dbContext.Devices.Update(device);
|
|
||||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
_devices[device.Id] = device;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool CanAccessDevice(User user, string deviceId)
|
public bool CanAccessDevice(User user, string deviceId)
|
||||||
{
|
{
|
||||||
@@ -235,11 +225,6 @@ namespace Jellyfin.Server.Implementations.Devices
|
|||||||
private DeviceInfo ToDeviceInfo(Device authInfo, DeviceOptions? options = null)
|
private DeviceInfo ToDeviceInfo(Device authInfo, DeviceOptions? options = null)
|
||||||
{
|
{
|
||||||
var caps = GetCapabilities(authInfo.DeviceId);
|
var caps = GetCapabilities(authInfo.DeviceId);
|
||||||
var user = _userManager.GetUserById(authInfo.UserId);
|
|
||||||
if (user is null)
|
|
||||||
{
|
|
||||||
throw new ResourceNotFoundException("User with UserId " + authInfo.UserId + " not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
return new DeviceInfo
|
return new DeviceInfo
|
||||||
{
|
{
|
||||||
@@ -247,7 +232,7 @@ namespace Jellyfin.Server.Implementations.Devices
|
|||||||
AppVersion = authInfo.AppVersion,
|
AppVersion = authInfo.AppVersion,
|
||||||
Id = authInfo.DeviceId,
|
Id = authInfo.DeviceId,
|
||||||
LastUserId = authInfo.UserId,
|
LastUserId = authInfo.UserId,
|
||||||
LastUserName = user.Username,
|
LastUserName = authInfo.User.Username,
|
||||||
Name = authInfo.DeviceName,
|
Name = authInfo.DeviceName,
|
||||||
DateLastActivity = authInfo.DateLastActivity,
|
DateLastActivity = authInfo.DateLastActivity,
|
||||||
IconUrl = caps.IconUrl,
|
IconUrl = caps.IconUrl,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using EFCoreSecondLevelCacheInterceptor;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@@ -15,13 +16,28 @@ public static class ServiceCollectionExtensions
|
|||||||
/// Adds the <see cref="IDbContextFactory{TContext}"/> interface to the service collection with second level caching enabled.
|
/// Adds the <see cref="IDbContextFactory{TContext}"/> interface to the service collection with second level caching enabled.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="serviceCollection">An instance of the <see cref="IServiceCollection"/> interface.</param>
|
/// <param name="serviceCollection">An instance of the <see cref="IServiceCollection"/> interface.</param>
|
||||||
|
/// <param name="disableSecondLevelCache">Whether second level cache disabled..</param>
|
||||||
/// <returns>The updated service collection.</returns>
|
/// <returns>The updated service collection.</returns>
|
||||||
public static IServiceCollection AddJellyfinDbContext(this IServiceCollection serviceCollection)
|
public static IServiceCollection AddJellyfinDbContext(this IServiceCollection serviceCollection, bool disableSecondLevelCache)
|
||||||
{
|
{
|
||||||
|
if (!disableSecondLevelCache)
|
||||||
|
{
|
||||||
|
serviceCollection.AddEFSecondLevelCache(options =>
|
||||||
|
options.UseMemoryCacheProvider()
|
||||||
|
.CacheAllQueries(CacheExpirationMode.Sliding, TimeSpan.FromMinutes(10))
|
||||||
|
.UseCacheKeyPrefix("EF_")
|
||||||
|
// Don't cache null values. Remove this optional setting if it's not necessary.
|
||||||
|
.SkipCachingResults(result => result.Value is null or EFTableRows { RowsCount: 0 }));
|
||||||
|
}
|
||||||
|
|
||||||
serviceCollection.AddPooledDbContextFactory<JellyfinDbContext>((serviceProvider, opt) =>
|
serviceCollection.AddPooledDbContextFactory<JellyfinDbContext>((serviceProvider, opt) =>
|
||||||
{
|
{
|
||||||
var applicationPaths = serviceProvider.GetRequiredService<IApplicationPaths>();
|
var applicationPaths = serviceProvider.GetRequiredService<IApplicationPaths>();
|
||||||
opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}");
|
var dbOpt = opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}");
|
||||||
|
if (!disableSecondLevelCache)
|
||||||
|
{
|
||||||
|
dbOpt.AddInterceptors(serviceProvider.GetRequiredService<SecondLevelCacheInterceptor>());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return serviceCollection;
|
return serviceCollection;
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AsyncKeyedLock" />
|
<PackageReference Include="AsyncKeyedLock" />
|
||||||
|
<PackageReference Include="EFCoreSecondLevelCacheInterceptor" />
|
||||||
<PackageReference Include="System.Linq.Async" />
|
<PackageReference Include="System.Linq.Async" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
|
||||||
|
|||||||
@@ -4,10 +4,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Queries;
|
|
||||||
using Jellyfin.Extensions;
|
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
using MediaBrowser.Controller.Devices;
|
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
@@ -20,18 +17,15 @@ namespace Jellyfin.Server.Implementations.Security
|
|||||||
{
|
{
|
||||||
private readonly IDbContextFactory<JellyfinDbContext> _jellyfinDbProvider;
|
private readonly IDbContextFactory<JellyfinDbContext> _jellyfinDbProvider;
|
||||||
private readonly IUserManager _userManager;
|
private readonly IUserManager _userManager;
|
||||||
private readonly IDeviceManager _deviceManager;
|
|
||||||
private readonly IServerApplicationHost _serverApplicationHost;
|
private readonly IServerApplicationHost _serverApplicationHost;
|
||||||
|
|
||||||
public AuthorizationContext(
|
public AuthorizationContext(
|
||||||
IDbContextFactory<JellyfinDbContext> jellyfinDb,
|
IDbContextFactory<JellyfinDbContext> jellyfinDb,
|
||||||
IUserManager userManager,
|
IUserManager userManager,
|
||||||
IDeviceManager deviceManager,
|
|
||||||
IServerApplicationHost serverApplicationHost)
|
IServerApplicationHost serverApplicationHost)
|
||||||
{
|
{
|
||||||
_jellyfinDbProvider = jellyfinDb;
|
_jellyfinDbProvider = jellyfinDb;
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_deviceManager = deviceManager;
|
|
||||||
_serverApplicationHost = serverApplicationHost;
|
_serverApplicationHost = serverApplicationHost;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,11 +121,7 @@ namespace Jellyfin.Server.Implementations.Security
|
|||||||
var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
await using (dbContext.ConfigureAwait(false))
|
await using (dbContext.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
var device = _deviceManager.GetDevices(
|
var device = await dbContext.Devices.FirstOrDefaultAsync(d => d.AccessToken == token).ConfigureAwait(false);
|
||||||
new DeviceQuery
|
|
||||||
{
|
|
||||||
AccessToken = token
|
|
||||||
}).Items.FirstOrDefault();
|
|
||||||
|
|
||||||
if (device is not null)
|
if (device is not null)
|
||||||
{
|
{
|
||||||
@@ -188,7 +178,8 @@ namespace Jellyfin.Server.Implementations.Security
|
|||||||
|
|
||||||
if (updateToken)
|
if (updateToken)
|
||||||
{
|
{
|
||||||
await _deviceManager.UpdateDevice(device).ConfigureAwait(false);
|
dbContext.Devices.Update(device);
|
||||||
|
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ public class TrickplayManager : ITrickplayManager
|
|||||||
var mediaPath = mediaSource.Path;
|
var mediaPath = mediaSource.Path;
|
||||||
if (!File.Exists(mediaPath))
|
if (!File.Exists(mediaPath))
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Media source {MediaSourceId} not found at {Path} for item {ItemID}", mediaSource.Id, mediaPath, video.Id);
|
_logger.LogWarning("Media not found at {Path} for item {ItemID}", mediaPath, video.Id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,10 +60,10 @@ public sealed class DeviceAccessHost : IHostedService
|
|||||||
|
|
||||||
private async Task UpdateDeviceAccess(User user)
|
private async Task UpdateDeviceAccess(User user)
|
||||||
{
|
{
|
||||||
var existing = _deviceManager.GetDevices(new DeviceQuery
|
var existing = (await _deviceManager.GetDevices(new DeviceQuery
|
||||||
{
|
{
|
||||||
UserId = user.Id
|
UserId = user.Id
|
||||||
}).Items;
|
}).ConfigureAwait(false)).Items;
|
||||||
|
|
||||||
foreach (var device in existing)
|
foreach (var device in existing)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -85,6 +85,6 @@ public static class WebHostBuilderExtensions
|
|||||||
logger.LogInformation("Kestrel listening to unix socket {SocketPath}", socketPath);
|
logger.LogInformation("Kestrel listening to unix socket {SocketPath}", socketPath);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.UseStartup(_ => new Startup(appHost));
|
.UseStartup(_ => new Startup(appHost, startupConfig));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,9 +55,8 @@ namespace Jellyfin.Server.Migrations.Routines
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Backing up {Library} to {BackupPath}", DbFilename, bakPath);
|
|
||||||
File.Copy(dbPath, bakPath);
|
File.Copy(dbPath, bakPath);
|
||||||
_logger.LogInformation("{Library} backed up to {BackupPath}", DbFilename, bakPath);
|
_logger.LogInformation("Library database backed up to {BackupPath}", bakPath);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -81,7 +80,7 @@ namespace Jellyfin.Server.Migrations.Routines
|
|||||||
{
|
{
|
||||||
IncludeItemTypes = [BaseItemKind.Audio],
|
IncludeItemTypes = [BaseItemKind.Audio],
|
||||||
StartIndex = startIndex,
|
StartIndex = startIndex,
|
||||||
Limit = 5000,
|
Limit = 100,
|
||||||
SkipDeserialization = true
|
SkipDeserialization = true
|
||||||
})
|
})
|
||||||
.Cast<Audio>()
|
.Cast<Audio>()
|
||||||
@@ -98,8 +97,7 @@ namespace Jellyfin.Server.Migrations.Routines
|
|||||||
}
|
}
|
||||||
|
|
||||||
_itemRepository.SaveItems(results, CancellationToken.None);
|
_itemRepository.SaveItems(results, CancellationToken.None);
|
||||||
startIndex += results.Count;
|
startIndex += 100;
|
||||||
_logger.LogInformation("Backfilled data for {UpdatedRecords} of {TotalRecords} audio records", startIndex, records);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,15 +40,18 @@ namespace Jellyfin.Server
|
|||||||
{
|
{
|
||||||
private readonly CoreAppHost _serverApplicationHost;
|
private readonly CoreAppHost _serverApplicationHost;
|
||||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||||
|
private readonly IConfiguration _startupConfig;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="Startup" /> class.
|
/// Initializes a new instance of the <see cref="Startup" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="appHost">The server application host.</param>
|
/// <param name="appHost">The server application host.</param>
|
||||||
public Startup(CoreAppHost appHost)
|
/// <param name="startupConfig">The server startupConfig.</param>
|
||||||
|
public Startup(CoreAppHost appHost, IConfiguration startupConfig)
|
||||||
{
|
{
|
||||||
_serverApplicationHost = appHost;
|
_serverApplicationHost = appHost;
|
||||||
_serverConfigurationManager = appHost.ConfigurationManager;
|
_serverConfigurationManager = appHost.ConfigurationManager;
|
||||||
|
_startupConfig = startupConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -67,7 +70,7 @@ namespace Jellyfin.Server
|
|||||||
// TODO remove once this is fixed upstream https://github.com/dotnet/aspnetcore/issues/34371
|
// TODO remove once this is fixed upstream https://github.com/dotnet/aspnetcore/issues/34371
|
||||||
services.AddSingleton<IActionResultExecutor<PhysicalFileResult>, SymlinkFollowingPhysicalFileResultExecutor>();
|
services.AddSingleton<IActionResultExecutor<PhysicalFileResult>, SymlinkFollowingPhysicalFileResultExecutor>();
|
||||||
services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration());
|
services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration());
|
||||||
services.AddJellyfinDbContext();
|
services.AddJellyfinDbContext(_startupConfig.GetSqliteSecondLevelCacheDisabled());
|
||||||
services.AddJellyfinApiSwagger();
|
services.AddJellyfinApiSwagger();
|
||||||
|
|
||||||
// configure custom legacy authentication
|
// configure custom legacy authentication
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Common</PackageId>
|
<PackageId>Jellyfin.Common</PackageId>
|
||||||
<VersionPrefix>10.9.10</VersionPrefix>
|
<VersionPrefix>10.10.0</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -44,28 +44,26 @@ namespace MediaBrowser.Controller.Devices
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">The identifier.</param>
|
/// <param name="id">The identifier.</param>
|
||||||
/// <returns>DeviceInfo.</returns>
|
/// <returns>DeviceInfo.</returns>
|
||||||
DeviceInfo GetDevice(string id);
|
Task<DeviceInfo> GetDevice(string id);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets devices based on the provided query.
|
/// Gets devices based on the provided query.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="query">The device query.</param>
|
/// <param name="query">The device query.</param>
|
||||||
/// <returns>A <see cref="Task{QueryResult}"/> representing the retrieval of the devices.</returns>
|
/// <returns>A <see cref="Task{QueryResult}"/> representing the retrieval of the devices.</returns>
|
||||||
QueryResult<Device> GetDevices(DeviceQuery query);
|
Task<QueryResult<Device>> GetDevices(DeviceQuery query);
|
||||||
|
|
||||||
QueryResult<DeviceInfo> GetDeviceInfos(DeviceQuery query);
|
Task<QueryResult<DeviceInfo>> GetDeviceInfos(DeviceQuery query);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the devices.
|
/// Gets the devices.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="userId">The user's id, or <c>null</c>.</param>
|
/// <param name="userId">The user's id, or <c>null</c>.</param>
|
||||||
/// <returns>IEnumerable<DeviceInfo>.</returns>
|
/// <returns>IEnumerable<DeviceInfo>.</returns>
|
||||||
QueryResult<DeviceInfo> GetDevicesForUser(Guid? userId);
|
Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId);
|
||||||
|
|
||||||
Task DeleteDevice(Device device);
|
Task DeleteDevice(Device device);
|
||||||
|
|
||||||
Task UpdateDevice(Device device);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determines whether this instance [can access device] the specified user identifier.
|
/// Determines whether this instance [can access device] the specified user identifier.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -76,6 +74,6 @@ namespace MediaBrowser.Controller.Devices
|
|||||||
|
|
||||||
Task UpdateDeviceOptions(string deviceId, string deviceName);
|
Task UpdateDeviceOptions(string deviceId, string deviceName);
|
||||||
|
|
||||||
DeviceOptions GetDeviceOptions(string deviceId);
|
Task<DeviceOptions> GetDeviceOptions(string deviceId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1949,15 +1949,14 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove from file system
|
// Remove it from the item
|
||||||
|
RemoveImage(info);
|
||||||
|
|
||||||
if (info.IsLocalFile)
|
if (info.IsLocalFile)
|
||||||
{
|
{
|
||||||
FileSystem.DeleteFile(info.Path);
|
FileSystem.DeleteFile(info.Path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove from item
|
|
||||||
RemoveImage(info);
|
|
||||||
|
|
||||||
await UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
|
await UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Security;
|
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -365,23 +364,15 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
|
|
||||||
if (IsFileProtocol)
|
if (IsFileProtocol)
|
||||||
{
|
{
|
||||||
IEnumerable<BaseItem> nonCachedChildren = [];
|
IEnumerable<BaseItem> nonCachedChildren;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
nonCachedChildren = GetNonCachedChildren(directoryService);
|
nonCachedChildren = GetNonCachedChildren(directoryService);
|
||||||
}
|
}
|
||||||
catch (IOException ex)
|
|
||||||
{
|
|
||||||
Logger.LogError(ex, "Error retrieving children from file system");
|
|
||||||
}
|
|
||||||
catch (SecurityException ex)
|
|
||||||
{
|
|
||||||
Logger.LogError(ex, "Error retrieving children from file system");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogError(ex, "Error retrieving children");
|
Logger.LogError(ex, "Error retrieving children folder");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,11 @@ namespace MediaBrowser.Controller.Extensions
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public const string SqliteCacheSizeKey = "sqlite:cacheSize";
|
public const string SqliteCacheSizeKey = "sqlite:cacheSize";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Disable second level cache of sqlite.
|
||||||
|
/// </summary>
|
||||||
|
public const string SqliteDisableSecondLevelCacheKey = "sqlite:disableSecondLevelCache";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a value indicating whether the application should host static web content from the <see cref="IConfiguration"/>.
|
/// Gets a value indicating whether the application should host static web content from the <see cref="IConfiguration"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -128,5 +133,15 @@ namespace MediaBrowser.Controller.Extensions
|
|||||||
/// <returns>The sqlite cache size.</returns>
|
/// <returns>The sqlite cache size.</returns>
|
||||||
public static int? GetSqliteCacheSize(this IConfiguration configuration)
|
public static int? GetSqliteCacheSize(this IConfiguration configuration)
|
||||||
=> configuration.GetValue<int?>(SqliteCacheSizeKey);
|
=> configuration.GetValue<int?>(SqliteCacheSizeKey);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether second level cache disabled from the <see cref="IConfiguration" />.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configuration">The configuration to read the setting from.</param>
|
||||||
|
/// <returns>Whether second level cache disabled.</returns>
|
||||||
|
public static bool GetSqliteSecondLevelCacheDisabled(this IConfiguration configuration)
|
||||||
|
{
|
||||||
|
return configuration.GetValue<bool>(SqliteDisableSecondLevelCacheKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using MediaBrowser.Model.IO;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Controller.IO;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Helper methods for file system management.
|
|
||||||
/// </summary>
|
|
||||||
public static class FileSystemHelper
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Deletes the file.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="fileSystem">The fileSystem.</param>
|
|
||||||
/// <param name="path">The path.</param>
|
|
||||||
/// <param name="logger">The logger.</param>
|
|
||||||
public static void DeleteFile(IFileSystem fileSystem, string path, ILogger logger)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
fileSystem.DeleteFile(path);
|
|
||||||
}
|
|
||||||
catch (UnauthorizedAccessException ex)
|
|
||||||
{
|
|
||||||
logger.LogError(ex, "Error deleting file {Path}", path);
|
|
||||||
}
|
|
||||||
catch (IOException ex)
|
|
||||||
{
|
|
||||||
logger.LogError(ex, "Error deleting file {Path}", path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Recursively delete empty folders.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="fileSystem">The fileSystem.</param>
|
|
||||||
/// <param name="path">The path.</param>
|
|
||||||
/// <param name="logger">The logger.</param>
|
|
||||||
public static void DeleteEmptyFolders(IFileSystem fileSystem, string path, ILogger logger)
|
|
||||||
{
|
|
||||||
foreach (var directory in fileSystem.GetDirectoryPaths(path))
|
|
||||||
{
|
|
||||||
DeleteEmptyFolders(fileSystem, directory, logger);
|
|
||||||
if (!fileSystem.GetFileSystemEntryPaths(directory).Any())
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Directory.Delete(directory, false);
|
|
||||||
}
|
|
||||||
catch (UnauthorizedAccessException ex)
|
|
||||||
{
|
|
||||||
logger.LogError(ex, "Error deleting directory {Path}", directory);
|
|
||||||
}
|
|
||||||
catch (IOException ex)
|
|
||||||
{
|
|
||||||
logger.LogError(ex, "Error deleting directory {Path}", directory);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -149,14 +149,6 @@ namespace MediaBrowser.Controller.Library
|
|||||||
/// <returns>Task.</returns>
|
/// <returns>Task.</returns>
|
||||||
Task ValidateMediaLibrary(IProgress<double> progress, CancellationToken cancellationToken);
|
Task ValidateMediaLibrary(IProgress<double> progress, CancellationToken cancellationToken);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reloads the root media folder.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
|
||||||
/// <param name="removeRoot">Is remove the library itself allowed.</param>
|
|
||||||
/// <returns>Task.</returns>
|
|
||||||
Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false);
|
|
||||||
|
|
||||||
Task UpdateImagesAsync(BaseItem item, bool forceUpdate = false);
|
Task UpdateImagesAsync(BaseItem item, bool forceUpdate = false);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Controller</PackageId>
|
<PackageId>Jellyfin.Controller</PackageId>
|
||||||
<VersionPrefix>10.9.10</VersionPrefix>
|
<VersionPrefix>10.10.0</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -1208,8 +1208,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
var subtitlePath = state.SubtitleStream.Path;
|
var subtitlePath = state.SubtitleStream.Path;
|
||||||
var subtitleExtension = Path.GetExtension(subtitlePath.AsSpan());
|
var subtitleExtension = Path.GetExtension(subtitlePath.AsSpan());
|
||||||
|
|
||||||
// dvdsub/vobsub graphical subtitles use .sub+.idx pairs
|
if (subtitleExtension.Equals(".sub", StringComparison.OrdinalIgnoreCase)
|
||||||
if (subtitleExtension.Equals(".sub", StringComparison.OrdinalIgnoreCase))
|
|| subtitleExtension.Equals(".sup", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var idxFile = Path.ChangeExtension(subtitlePath, ".idx");
|
var idxFile = Path.ChangeExtension(subtitlePath, ".idx");
|
||||||
if (File.Exists(idxFile))
|
if (File.Exists(idxFile))
|
||||||
@@ -1313,7 +1313,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|
|
||||||
// Apply aac_adtstoasc bitstream filter when media source is in mpegts.
|
// Apply aac_adtstoasc bitstream filter when media source is in mpegts.
|
||||||
if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase)
|
if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase)
|
||||||
&& (string.Equals(mediaSourceContainer, "ts", StringComparison.OrdinalIgnoreCase)
|
&& (string.Equals(mediaSourceContainer, "mpegts", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(mediaSourceContainer, "aac", StringComparison.OrdinalIgnoreCase)
|
|| string.Equals(mediaSourceContainer, "aac", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(mediaSourceContainer, "hls", StringComparison.OrdinalIgnoreCase)))
|
|| string.Equals(mediaSourceContainer, "hls", StringComparison.OrdinalIgnoreCase)))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ namespace MediaBrowser.Controller.Playlists
|
|||||||
return base.GetChildren(user, true, query);
|
return base.GetChildren(user, true, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IReadOnlyList<BaseItem> GetPlaylistItems(IEnumerable<BaseItem> inputItems, User user, DtoOptions options)
|
public static IReadOnlyList<BaseItem> GetPlaylistItems(MediaType playlistMediaType, IEnumerable<BaseItem> inputItems, User user, DtoOptions options)
|
||||||
{
|
{
|
||||||
if (user is not null)
|
if (user is not null)
|
||||||
{
|
{
|
||||||
@@ -177,14 +177,14 @@ namespace MediaBrowser.Controller.Playlists
|
|||||||
|
|
||||||
foreach (var item in inputItems)
|
foreach (var item in inputItems)
|
||||||
{
|
{
|
||||||
var playlistItems = GetPlaylistItems(item, user, options);
|
var playlistItems = GetPlaylistItems(item, user, playlistMediaType, options);
|
||||||
list.AddRange(playlistItems);
|
list.AddRange(playlistItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IEnumerable<BaseItem> GetPlaylistItems(BaseItem item, User user, DtoOptions options)
|
private static IEnumerable<BaseItem> GetPlaylistItems(BaseItem item, User user, MediaType mediaType, DtoOptions options)
|
||||||
{
|
{
|
||||||
if (item is MusicGenre musicGenre)
|
if (item is MusicGenre musicGenre)
|
||||||
{
|
{
|
||||||
@@ -216,7 +216,7 @@ namespace MediaBrowser.Controller.Playlists
|
|||||||
{
|
{
|
||||||
Recursive = true,
|
Recursive = true,
|
||||||
IsFolder = false,
|
IsFolder = false,
|
||||||
MediaTypes = [MediaType.Audio, MediaType.Video],
|
MediaTypes = [mediaType],
|
||||||
EnableTotalRecordCount = false,
|
EnableTotalRecordCount = false,
|
||||||
DtoOptions = options
|
DtoOptions = options
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,22 +28,6 @@ namespace MediaBrowser.Controller.Providers
|
|||||||
return _cache.GetOrAdd(path, static (p, fileSystem) => fileSystem.GetFileSystemEntries(p).ToArray(), _fileSystem);
|
return _cache.GetOrAdd(path, static (p, fileSystem) => fileSystem.GetFileSystemEntries(p).ToArray(), _fileSystem);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<FileSystemMetadata> GetDirectories(string path)
|
|
||||||
{
|
|
||||||
var list = new List<FileSystemMetadata>();
|
|
||||||
var items = GetFileSystemEntries(path);
|
|
||||||
for (var i = 0; i < items.Length; i++)
|
|
||||||
{
|
|
||||||
var item = items[i];
|
|
||||||
if (item.IsDirectory)
|
|
||||||
{
|
|
||||||
list.Add(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<FileSystemMetadata> GetFiles(string path)
|
public List<FileSystemMetadata> GetFiles(string path)
|
||||||
{
|
{
|
||||||
var list = new List<FileSystemMetadata>();
|
var list = new List<FileSystemMetadata>();
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ namespace MediaBrowser.Controller.Providers
|
|||||||
{
|
{
|
||||||
FileSystemMetadata[] GetFileSystemEntries(string path);
|
FileSystemMetadata[] GetFileSystemEntries(string path);
|
||||||
|
|
||||||
List<FileSystemMetadata> GetDirectories(string path);
|
|
||||||
|
|
||||||
List<FileSystemMetadata> GetFiles(string path);
|
List<FileSystemMetadata> GetFiles(string path);
|
||||||
|
|
||||||
FileSystemMetadata? GetFile(string path);
|
FileSystemMetadata? GetFile(string path);
|
||||||
|
|||||||
@@ -140,14 +140,6 @@ namespace MediaBrowser.Controller.Providers
|
|||||||
IEnumerable<IMetadataProvider<T>> GetMetadataProviders<T>(BaseItem item, LibraryOptions libraryOptions)
|
IEnumerable<IMetadataProvider<T>> GetMetadataProviders<T>(BaseItem item, LibraryOptions libraryOptions)
|
||||||
where T : BaseItem;
|
where T : BaseItem;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the metadata savers for the provided item.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="item">The item.</param>
|
|
||||||
/// <param name="libraryOptions">The library options.</param>
|
|
||||||
/// <returns>The metadata savers.</returns>
|
|
||||||
IEnumerable<IMetadataSaver> GetMetadataSavers(BaseItem item, LibraryOptions libraryOptions);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets all metadata plugins.
|
/// Gets all metadata plugins.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -38,26 +38,19 @@ namespace MediaBrowser.LocalMetadata.Images
|
|||||||
}
|
}
|
||||||
|
|
||||||
var parentPathFiles = directoryService.GetFiles(parentPath);
|
var parentPathFiles = directoryService.GetFiles(parentPath);
|
||||||
var nameWithoutExtension = Path.GetFileNameWithoutExtension(item.Path.AsSpan()).ToString();
|
|
||||||
|
|
||||||
var images = GetImageFilesFromFolder(nameWithoutExtension, parentPathFiles);
|
var nameWithoutExtension = Path.GetFileNameWithoutExtension(item.Path.AsSpan());
|
||||||
|
|
||||||
var metadataSubDir = directoryService.GetDirectories(parentPath).FirstOrDefault(d => d.Name.Equals("metadata", StringComparison.Ordinal));
|
return GetFilesFromParentFolder(nameWithoutExtension, parentPathFiles);
|
||||||
if (metadataSubDir is not null)
|
|
||||||
{
|
|
||||||
var files = directoryService.GetFiles(metadataSubDir.FullName);
|
|
||||||
images.AddRange(GetImageFilesFromFolder(nameWithoutExtension, files));
|
|
||||||
}
|
|
||||||
|
|
||||||
return images;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<LocalImageInfo> GetImageFilesFromFolder(ReadOnlySpan<char> filenameWithoutExtension, List<FileSystemMetadata> filePaths)
|
private List<LocalImageInfo> GetFilesFromParentFolder(ReadOnlySpan<char> filenameWithoutExtension, List<FileSystemMetadata> parentPathFiles)
|
||||||
{
|
{
|
||||||
var list = new List<LocalImageInfo>(1);
|
|
||||||
var thumbName = string.Concat(filenameWithoutExtension, "-thumb");
|
var thumbName = string.Concat(filenameWithoutExtension, "-thumb");
|
||||||
|
|
||||||
foreach (var i in filePaths)
|
var list = new List<LocalImageInfo>(1);
|
||||||
|
|
||||||
|
foreach (var i in parentPathFiles)
|
||||||
{
|
{
|
||||||
if (i.IsDirectory)
|
if (i.IsDirectory)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -89,8 +89,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
|||||||
string outputPath,
|
string outputPath,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var shouldExtractOneByOne = mediaSource.MediaAttachments.Any(a => !string.IsNullOrEmpty(a.FileName)
|
var shouldExtractOneByOne = mediaSource.MediaAttachments.Any(a => a.FileName.Contains('/', StringComparison.OrdinalIgnoreCase) || a.FileName.Contains('\\', StringComparison.OrdinalIgnoreCase));
|
||||||
&& (a.FileName.Contains('/', StringComparison.OrdinalIgnoreCase) || a.FileName.Contains('\\', StringComparison.OrdinalIgnoreCase)));
|
|
||||||
if (shouldExtractOneByOne)
|
if (shouldExtractOneByOne)
|
||||||
{
|
{
|
||||||
var attachmentIndexes = mediaSource.MediaAttachments.Select(a => a.Index);
|
var attachmentIndexes = mediaSource.MediaAttachments.Select(a => a.Index);
|
||||||
|
|||||||
@@ -168,8 +168,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
|
|
||||||
private readonly string _encoderPath;
|
private readonly string _encoderPath;
|
||||||
|
|
||||||
private readonly Version _minFFmpegMultiThreadedCli = new Version(7, 0);
|
|
||||||
|
|
||||||
public EncoderValidator(ILogger logger, string encoderPath)
|
public EncoderValidator(ILogger logger, string encoderPath)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@@ -479,7 +477,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CheckSupportedRuntimeKey(string keyDesc, Version? ffmpegVersion)
|
public bool CheckSupportedRuntimeKey(string keyDesc)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(keyDesc))
|
if (string.IsNullOrEmpty(keyDesc))
|
||||||
{
|
{
|
||||||
@@ -489,9 +487,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
string output;
|
string output;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// With multi-threaded cli support, FFmpeg 7 is less sensitive to keyboard input
|
output = GetProcessOutput(_encoderPath, "-hide_banner -f lavfi -i nullsrc=s=1x1:d=500 -f null -", true, "?");
|
||||||
var duration = ffmpegVersion >= _minFFmpegMultiThreadedCli ? 10000 : 1000;
|
|
||||||
output = GetProcessOutput(_encoderPath, $"-hide_banner -f lavfi -i nullsrc=s=1x1:d={duration} -f null -", true, "?");
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
|
|
||||||
_threads = EncodingHelper.GetNumberOfThreads(null, options, null);
|
_threads = EncodingHelper.GetNumberOfThreads(null, options, null);
|
||||||
|
|
||||||
_isPkeyPauseSupported = validator.CheckSupportedRuntimeKey("p pause transcoding", _ffmpegVersion);
|
_isPkeyPauseSupported = validator.CheckSupportedRuntimeKey("p pause transcoding");
|
||||||
|
|
||||||
// Check the Vaapi device vendor
|
// Check the Vaapi device vendor
|
||||||
if (OperatingSystem.IsLinux()
|
if (OperatingSystem.IsLinux()
|
||||||
@@ -1155,10 +1155,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
|
|
||||||
// Get all files from the BDMV/STREAMING directory
|
// Get all files from the BDMV/STREAMING directory
|
||||||
// Only return playable local .m2ts files
|
// Only return playable local .m2ts files
|
||||||
var files = _fileSystem.GetFiles(Path.Join(path, "BDMV", "STREAM")).ToList();
|
|
||||||
return validPlaybackFiles
|
return validPlaybackFiles
|
||||||
.Select(validFile => files.FirstOrDefault(f => Path.GetFileName(f.FullName.AsSpan()).Equals(validFile, StringComparison.OrdinalIgnoreCase))?.FullName)
|
.Select(f => _fileSystem.GetFileInfo(Path.Join(path, "BDMV", "STREAM", f)))
|
||||||
.Where(f => f is not null)
|
.Where(f => f.Exists)
|
||||||
|
.Select(f => f.FullName)
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1216,7 +1216,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
var duration = TimeSpan.FromTicks(mediaInfoResult.RunTimeTicks.Value).TotalSeconds;
|
var duration = TimeSpan.FromTicks(mediaInfoResult.RunTimeTicks.Value).TotalSeconds;
|
||||||
|
|
||||||
// Add file path stanza to concat configuration
|
// Add file path stanza to concat configuration
|
||||||
sw.WriteLine("file '{0}'", path.Replace("'", @"'\''", StringComparison.Ordinal));
|
sw.WriteLine("file '{0}'", path);
|
||||||
|
|
||||||
// Add duration stanza to concat configuration
|
// Add duration stanza to concat configuration
|
||||||
sw.WriteLine("duration {0}", duration);
|
sw.WriteLine("duration {0}", duration);
|
||||||
|
|||||||
@@ -280,8 +280,8 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||||||
splitFormat[i] = "mpeg";
|
splitFormat[i] = "mpeg";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle MPEG-TS container
|
// Handle MPEG-2 container
|
||||||
else if (string.Equals(splitFormat[i], "mpegts", StringComparison.OrdinalIgnoreCase))
|
else if (string.Equals(splitFormat[i], "mpeg", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
splitFormat[i] = "ts";
|
splitFormat[i] = "ts";
|
||||||
}
|
}
|
||||||
@@ -624,19 +624,15 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||||||
{
|
{
|
||||||
if (string.Equals(codec, "dvb_subtitle", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(codec, "dvb_subtitle", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
codec = "DVBSUB";
|
codec = "dvbsub";
|
||||||
}
|
}
|
||||||
else if (string.Equals(codec, "dvb_teletext", StringComparison.OrdinalIgnoreCase))
|
else if ((codec ?? string.Empty).Contains("PGS", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
codec = "DVBTXT";
|
codec = "PGSSUB";
|
||||||
}
|
}
|
||||||
else if (string.Equals(codec, "dvd_subtitle", StringComparison.OrdinalIgnoreCase))
|
else if ((codec ?? string.Empty).Contains("DVD", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
codec = "DVDSUB"; // .sub+.idx
|
codec = "DVDSUB";
|
||||||
}
|
|
||||||
else if (string.Equals(codec, "hdmv_pgs_subtitle", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
codec = "PGSSUB"; // .sup
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return codec;
|
return codec;
|
||||||
@@ -721,8 +717,6 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||||||
if (streamInfo.CodecType == CodecType.Audio)
|
if (streamInfo.CodecType == CodecType.Audio)
|
||||||
{
|
{
|
||||||
stream.Type = MediaStreamType.Audio;
|
stream.Type = MediaStreamType.Audio;
|
||||||
stream.LocalizedDefault = _localization.GetLocalizedString("Default");
|
|
||||||
stream.LocalizedExternal = _localization.GetLocalizedString("External");
|
|
||||||
|
|
||||||
stream.Channels = streamInfo.Channels;
|
stream.Channels = streamInfo.Channels;
|
||||||
|
|
||||||
@@ -785,10 +779,11 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||||||
&& !string.Equals(streamInfo.FieldOrder, "progressive", StringComparison.OrdinalIgnoreCase);
|
&& !string.Equals(streamInfo.FieldOrder, "progressive", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
if (isAudio
|
if (isAudio
|
||||||
|| string.Equals(stream.Codec, "bmp", StringComparison.OrdinalIgnoreCase)
|
&& (string.Equals(stream.Codec, "bmp", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase)
|
|| string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase)
|
|| string.Equals(stream.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(stream.Codec, "webp", StringComparison.OrdinalIgnoreCase))
|
|| string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(stream.Codec, "webp", StringComparison.OrdinalIgnoreCase)))
|
||||||
{
|
{
|
||||||
stream.Type = MediaStreamType.EmbeddedImage;
|
stream.Type = MediaStreamType.EmbeddedImage;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,8 +51,6 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
|
|||||||
o.PoolInitialFill = 1;
|
o.PoolInitialFill = 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
private readonly Version _maxFFmpegCkeyPauseSupported = new Version(6, 1);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="TranscodeManager"/> class.
|
/// Initializes a new instance of the <see cref="TranscodeManager"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -561,9 +559,7 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
|
|||||||
|
|
||||||
private void StartThrottler(StreamState state, TranscodingJob transcodingJob)
|
private void StartThrottler(StreamState state, TranscodingJob transcodingJob)
|
||||||
{
|
{
|
||||||
if (EnableThrottling(state)
|
if (EnableThrottling(state))
|
||||||
&& (_mediaEncoder.IsPkeyPauseSupported
|
|
||||||
|| _mediaEncoder.EncoderVersion <= _maxFFmpegCkeyPauseSupported))
|
|
||||||
{
|
{
|
||||||
transcodingJob.TranscodingThrottler = new TranscodingThrottler(transcodingJob, _loggerFactory.CreateLogger<TranscodingThrottler>(), _serverConfigurationManager, _fileSystem, _mediaEncoder);
|
transcodingJob.TranscodingThrottler = new TranscodingThrottler(transcodingJob, _loggerFactory.CreateLogger<TranscodingThrottler>(), _serverConfigurationManager, _fileSystem, _mediaEncoder);
|
||||||
transcodingJob.TranscodingThrottler.Start();
|
transcodingJob.TranscodingThrottler.Start();
|
||||||
|
|||||||
@@ -898,10 +898,8 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
var appliedVideoConditions = options.Profile.CodecProfiles
|
var appliedVideoConditions = options.Profile.CodecProfiles
|
||||||
.Where(i => i.Type == CodecType.Video &&
|
.Where(i => i.Type == CodecType.Video &&
|
||||||
i.ContainsAnyCodec(videoStream?.Codec, container) &&
|
i.ContainsAnyCodec(videoStream?.Codec, container) &&
|
||||||
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)))
|
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)));
|
||||||
// Reverse codec profiles for backward compatibility - first codec profile has higher priority
|
var isFirstAppliedCodecProfile = true;
|
||||||
.Reverse();
|
|
||||||
|
|
||||||
foreach (var i in appliedVideoConditions)
|
foreach (var i in appliedVideoConditions)
|
||||||
{
|
{
|
||||||
var transcodingVideoCodecs = ContainerProfile.SplitValue(videoCodec);
|
var transcodingVideoCodecs = ContainerProfile.SplitValue(videoCodec);
|
||||||
@@ -909,7 +907,8 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
{
|
{
|
||||||
if (i.ContainsAnyCodec(transcodingVideoCodec, container))
|
if (i.ContainsAnyCodec(transcodingVideoCodec, container))
|
||||||
{
|
{
|
||||||
ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingVideoCodec, true, true);
|
ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingVideoCodec, true, isFirstAppliedCodecProfile);
|
||||||
|
isFirstAppliedCodecProfile = false;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -931,10 +930,8 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
var appliedAudioConditions = options.Profile.CodecProfiles
|
var appliedAudioConditions = options.Profile.CodecProfiles
|
||||||
.Where(i => i.Type == CodecType.VideoAudio &&
|
.Where(i => i.Type == CodecType.VideoAudio &&
|
||||||
i.ContainsAnyCodec(audioStream?.Codec, container) &&
|
i.ContainsAnyCodec(audioStream?.Codec, container) &&
|
||||||
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio)))
|
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio)));
|
||||||
// Reverse codec profiles for backward compatibility - first codec profile has higher priority
|
isFirstAppliedCodecProfile = true;
|
||||||
.Reverse();
|
|
||||||
|
|
||||||
foreach (var codecProfile in appliedAudioConditions)
|
foreach (var codecProfile in appliedAudioConditions)
|
||||||
{
|
{
|
||||||
var transcodingAudioCodecs = ContainerProfile.SplitValue(audioCodec);
|
var transcodingAudioCodecs = ContainerProfile.SplitValue(audioCodec);
|
||||||
@@ -942,7 +939,8 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
{
|
{
|
||||||
if (codecProfile.ContainsAnyCodec(transcodingAudioCodec, container))
|
if (codecProfile.ContainsAnyCodec(transcodingAudioCodec, container))
|
||||||
{
|
{
|
||||||
ApplyTranscodingConditions(playlistItem, codecProfile.Conditions, transcodingAudioCodec, true, true);
|
ApplyTranscodingConditions(playlistItem, codecProfile.Conditions, transcodingAudioCodec, true, isFirstAppliedCodecProfile);
|
||||||
|
isFirstAppliedCodecProfile = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -267,14 +267,14 @@ namespace MediaBrowser.Model.Entities
|
|||||||
attributes.Add(StringHelper.FirstToUpper(fullLanguage ?? Language));
|
attributes.Add(StringHelper.FirstToUpper(fullLanguage ?? Language));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(Codec) && !string.Equals(Codec, "dca", StringComparison.OrdinalIgnoreCase) && !string.Equals(Codec, "dts", StringComparison.OrdinalIgnoreCase))
|
if (!string.IsNullOrEmpty(Profile) && !string.Equals(Profile, "lc", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
|
||||||
attributes.Add(AudioCodec.GetFriendlyName(Codec));
|
|
||||||
}
|
|
||||||
else if (!string.IsNullOrEmpty(Profile) && !string.Equals(Profile, "lc", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
attributes.Add(Profile);
|
attributes.Add(Profile);
|
||||||
}
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(Codec))
|
||||||
|
{
|
||||||
|
attributes.Add(AudioCodec.GetFriendlyName(Codec));
|
||||||
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(ChannelLayout))
|
if (!string.IsNullOrEmpty(ChannelLayout))
|
||||||
{
|
{
|
||||||
@@ -656,14 +656,14 @@ namespace MediaBrowser.Model.Entities
|
|||||||
{
|
{
|
||||||
string codec = format ?? string.Empty;
|
string codec = format ?? string.Empty;
|
||||||
|
|
||||||
// microdvd and dvdsub/vobsub share the ".sub" file extension, but it's text-based.
|
// sub = external .sub file
|
||||||
|
|
||||||
return codec.Contains("microdvd", StringComparison.OrdinalIgnoreCase)
|
return !codec.Contains("pgs", StringComparison.OrdinalIgnoreCase)
|
||||||
|| (!codec.Contains("pgs", StringComparison.OrdinalIgnoreCase)
|
&& !codec.Contains("dvd", StringComparison.OrdinalIgnoreCase)
|
||||||
&& !codec.Contains("dvdsub", StringComparison.OrdinalIgnoreCase)
|
&& !codec.Contains("dvbsub", StringComparison.OrdinalIgnoreCase)
|
||||||
&& !codec.Contains("dvbsub", StringComparison.OrdinalIgnoreCase)
|
&& !string.Equals(codec, "sub", StringComparison.OrdinalIgnoreCase)
|
||||||
&& !string.Equals(codec, "sup", StringComparison.OrdinalIgnoreCase)
|
&& !string.Equals(codec, "sup", StringComparison.OrdinalIgnoreCase)
|
||||||
&& !string.Equals(codec, "sub", StringComparison.OrdinalIgnoreCase));
|
&& !string.Equals(codec, "dvb_subtitle", StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool SupportsSubtitleConversionTo(string toCodec)
|
public bool SupportsSubtitleConversionTo(string toCodec)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Model</PackageId>
|
<PackageId>Jellyfin.Model</PackageId>
|
||||||
<VersionPrefix>10.9.10</VersionPrefix>
|
<VersionPrefix>10.10.0</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
@@ -33,7 +33,10 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.HttpOverrides" />
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||||
<PackageReference Include="MimeTypes">
|
<PackageReference Include="MimeTypes">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ using MediaBrowser.Common.Configuration;
|
|||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Entities.Audio;
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
using MediaBrowser.Controller.IO;
|
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Model.Configuration;
|
using MediaBrowser.Model.Configuration;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
@@ -101,8 +100,8 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
{
|
{
|
||||||
saveLocally = false;
|
saveLocally = false;
|
||||||
|
|
||||||
// If season is virtual under a physical series, save locally
|
// If season is virtual under a physical series, save locally if using compatible convention
|
||||||
if (item is Season season)
|
if (item is Season season && _config.Configuration.ImageSavingConvention == ImageSavingConvention.Compatible)
|
||||||
{
|
{
|
||||||
var series = season.Series;
|
var series = season.Series;
|
||||||
|
|
||||||
@@ -127,11 +126,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
|
|
||||||
var paths = GetSavePaths(item, type, imageIndex, mimeType, saveLocally);
|
var paths = GetSavePaths(item, type, imageIndex, mimeType, saveLocally);
|
||||||
|
|
||||||
string[] retryPaths = [];
|
var retryPaths = GetSavePaths(item, type, imageIndex, mimeType, false);
|
||||||
if (saveLocally)
|
|
||||||
{
|
|
||||||
retryPaths = GetSavePaths(item, type, imageIndex, mimeType, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there are more than one output paths, the stream will need to be seekable
|
// If there are more than one output paths, the stream will need to be seekable
|
||||||
if (paths.Length > 1 && !source.CanSeek)
|
if (paths.Length > 1 && !source.CanSeek)
|
||||||
@@ -188,29 +183,6 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
_fileSystem.DeleteFile(currentPath);
|
_fileSystem.DeleteFile(currentPath);
|
||||||
|
|
||||||
// Remove local episode metadata directory if it exists and is empty
|
|
||||||
var directory = Path.GetDirectoryName(currentPath);
|
|
||||||
if (item is Episode && directory.Equals("metadata", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
var parentDirectoryPath = Directory.GetParent(currentPath).FullName;
|
|
||||||
if (_fileSystem.DirectoryExists(parentDirectoryPath) && !_fileSystem.GetFiles(parentDirectoryPath).Any())
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Deleting empty local metadata folder {Folder}", parentDirectoryPath);
|
|
||||||
Directory.Delete(parentDirectoryPath);
|
|
||||||
}
|
|
||||||
catch (UnauthorizedAccessException ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error deleting directory {Path}", parentDirectoryPath);
|
|
||||||
}
|
|
||||||
catch (IOException ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error deleting directory {Path}", parentDirectoryPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (FileNotFoundException)
|
catch (FileNotFoundException)
|
||||||
{
|
{
|
||||||
@@ -402,47 +374,6 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Unable to determine image file extension from mime type {0}", mimeType));
|
throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Unable to determine image file extension from mime type {0}", mimeType));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(extension, ".jpeg", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
extension = ".jpg";
|
|
||||||
}
|
|
||||||
|
|
||||||
extension = extension.ToLowerInvariant();
|
|
||||||
|
|
||||||
if (type == ImageType.Primary && saveLocally)
|
|
||||||
{
|
|
||||||
if (season is not null && season.IndexNumber.HasValue)
|
|
||||||
{
|
|
||||||
var seriesFolder = season.SeriesPath;
|
|
||||||
|
|
||||||
var seasonMarker = season.IndexNumber.Value == 0
|
|
||||||
? "-specials"
|
|
||||||
: season.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
|
|
||||||
|
|
||||||
var imageFilename = "season" + seasonMarker + "-poster" + extension;
|
|
||||||
|
|
||||||
return Path.Combine(seriesFolder, imageFilename);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type == ImageType.Backdrop && saveLocally)
|
|
||||||
{
|
|
||||||
if (season is not null
|
|
||||||
&& season.IndexNumber.HasValue
|
|
||||||
&& (imageIndex is null || imageIndex == 0))
|
|
||||||
{
|
|
||||||
var seriesFolder = season.SeriesPath;
|
|
||||||
|
|
||||||
var seasonMarker = season.IndexNumber.Value == 0
|
|
||||||
? "-specials"
|
|
||||||
: season.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
|
|
||||||
|
|
||||||
var imageFilename = "season" + seasonMarker + "-fanart" + extension;
|
|
||||||
|
|
||||||
return Path.Combine(seriesFolder, imageFilename);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type == ImageType.Thumb && saveLocally)
|
if (type == ImageType.Thumb && saveLocally)
|
||||||
{
|
{
|
||||||
if (season is not null && season.IndexNumber.HasValue)
|
if (season is not null && season.IndexNumber.HasValue)
|
||||||
@@ -516,12 +447,20 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.Equals(extension, ".jpeg", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
extension = ".jpg";
|
||||||
|
}
|
||||||
|
|
||||||
|
extension = extension.ToLowerInvariant();
|
||||||
|
|
||||||
string path = null;
|
string path = null;
|
||||||
|
|
||||||
if (saveLocally)
|
if (saveLocally)
|
||||||
{
|
{
|
||||||
if (type == ImageType.Primary && item is Episode)
|
if (type == ImageType.Primary && item is Episode)
|
||||||
{
|
{
|
||||||
path = Path.Combine(Path.GetDirectoryName(item.Path), filename + "-thumb" + extension);
|
path = Path.Combine(Path.GetDirectoryName(item.Path), "metadata", filename + extension);
|
||||||
}
|
}
|
||||||
else if (item.IsInMixedFolder)
|
else if (item.IsInMixedFolder)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Entities.Audio;
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
using MediaBrowser.Controller.Entities.TV;
|
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.LiveTv;
|
using MediaBrowser.Controller.LiveTv;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
@@ -97,7 +96,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
public bool ValidateImages(BaseItem item, IEnumerable<IImageProvider> providers, ImageRefreshOptions refreshOptions)
|
public bool ValidateImages(BaseItem item, IEnumerable<IImageProvider> providers, ImageRefreshOptions refreshOptions)
|
||||||
{
|
{
|
||||||
var hasChanges = false;
|
var hasChanges = false;
|
||||||
var directoryService = refreshOptions?.DirectoryService;
|
IDirectoryService directoryService = refreshOptions?.DirectoryService;
|
||||||
|
|
||||||
if (item is not Photo)
|
if (item is not Photo)
|
||||||
{
|
{
|
||||||
@@ -159,7 +158,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only delete existing multi-images if new ones were added
|
// only delete existing multi-images if new ones were added
|
||||||
if (oldBackdropImages.Length > 0 && oldBackdropImages.Length < item.GetImages(ImageType.Backdrop).Count())
|
if (oldBackdropImages.Length > 0 && oldBackdropImages.Length < item.GetImages(ImageType.Backdrop).Count())
|
||||||
{
|
{
|
||||||
PruneImages(item, oldBackdropImages);
|
PruneImages(item, oldBackdropImages);
|
||||||
@@ -360,8 +359,10 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
|
|
||||||
private void PruneImages(BaseItem item, IReadOnlyList<ItemImageInfo> images)
|
private void PruneImages(BaseItem item, IReadOnlyList<ItemImageInfo> images)
|
||||||
{
|
{
|
||||||
foreach (var image in images)
|
for (var i = 0; i < images.Count; i++)
|
||||||
{
|
{
|
||||||
|
var image = images[i];
|
||||||
|
|
||||||
if (image.IsLocalFile)
|
if (image.IsLocalFile)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -370,7 +371,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
}
|
}
|
||||||
catch (FileNotFoundException)
|
catch (FileNotFoundException)
|
||||||
{
|
{
|
||||||
// Nothing to do, already gone
|
// nothing to do, already gone
|
||||||
}
|
}
|
||||||
catch (UnauthorizedAccessException ex)
|
catch (UnauthorizedAccessException ex)
|
||||||
{
|
{
|
||||||
@@ -380,16 +381,6 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
}
|
}
|
||||||
|
|
||||||
item.RemoveImages(images);
|
item.RemoveImages(images);
|
||||||
|
|
||||||
// Cleanup old metadata directory for episodes if empty
|
|
||||||
if (item is Episode)
|
|
||||||
{
|
|
||||||
var oldLocalMetadataDirectory = Path.Combine(item.ContainingFolderPath, "metadata");
|
|
||||||
if (_fileSystem.DirectoryExists(oldLocalMetadataDirectory) && !_fileSystem.GetFiles(oldLocalMetadataDirectory).Any())
|
|
||||||
{
|
|
||||||
Directory.Delete(oldLocalMetadataDirectory);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -422,10 +413,12 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
{
|
{
|
||||||
var changed = item.ValidateImages();
|
var changed = item.ValidateImages();
|
||||||
var foundImageTypes = new List<ImageType>();
|
var foundImageTypes = new List<ImageType>();
|
||||||
|
|
||||||
for (var i = 0; i < _singularImages.Length; i++)
|
for (var i = 0; i < _singularImages.Length; i++)
|
||||||
{
|
{
|
||||||
var type = _singularImages[i];
|
var type = _singularImages[i];
|
||||||
var image = GetFirstLocalImageInfoByType(images, type);
|
var image = GetFirstLocalImageInfoByType(images, type);
|
||||||
|
|
||||||
if (image is not null)
|
if (image is not null)
|
||||||
{
|
{
|
||||||
var currentImage = item.GetImageInfo(type, 0);
|
var currentImage = item.GetImageInfo(type, 0);
|
||||||
|
|||||||
@@ -92,6 +92,10 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var localImagesFailed = false;
|
||||||
|
|
||||||
|
var allImageProviders = ProviderManager.GetImageProviders(item, refreshOptions).ToList();
|
||||||
|
|
||||||
if (refreshOptions.RemoveOldMetadata && refreshOptions.ReplaceAllImages)
|
if (refreshOptions.RemoveOldMetadata && refreshOptions.ReplaceAllImages)
|
||||||
{
|
{
|
||||||
if (ImageProvider.RemoveImages(item))
|
if (ImageProvider.RemoveImages(item))
|
||||||
@@ -100,29 +104,19 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var localImagesFailed = false;
|
// Start by validating images
|
||||||
var allImageProviders = ProviderManager.GetImageProviders(item, refreshOptions).ToList();
|
try
|
||||||
|
|
||||||
// Only validate already registered images if we are replacing and saving locally
|
|
||||||
if (item.IsSaveLocalMetadataEnabled() && refreshOptions.ReplaceAllImages)
|
|
||||||
{
|
{
|
||||||
item.ValidateImages();
|
// Always validate images and check for new locally stored ones.
|
||||||
|
if (ImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptions))
|
||||||
|
{
|
||||||
|
updateType |= ItemUpdateType.ImageUpdate;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Run full image validation and register new local images
|
localImagesFailed = true;
|
||||||
try
|
Logger.LogError(ex, "Error validating images for {Item}", item.Path ?? item.Name ?? "Unknown name");
|
||||||
{
|
|
||||||
if (ImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptions))
|
|
||||||
{
|
|
||||||
updateType |= ItemUpdateType.ImageUpdate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
localImagesFailed = true;
|
|
||||||
Logger.LogError(ex, "Error validating images for {Item}", item.Path ?? item.Name ?? "Unknown name");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var metadataResult = new MetadataResult<TItemType>
|
var metadataResult = new MetadataResult<TItemType>
|
||||||
@@ -160,8 +154,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
|
|
||||||
id.IsAutomated = refreshOptions.IsAutomated;
|
id.IsAutomated = refreshOptions.IsAutomated;
|
||||||
|
|
||||||
var hasMetadataSavers = ProviderManager.GetMetadataSavers(item, libraryOptions).Any();
|
var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, ImageProvider, cancellationToken).ConfigureAwait(false);
|
||||||
var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, ImageProvider, hasMetadataSavers, cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
updateType |= result.UpdateType;
|
updateType |= result.UpdateType;
|
||||||
if (result.Failures > 0)
|
if (result.Failures > 0)
|
||||||
@@ -646,7 +639,6 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
MetadataRefreshOptions options,
|
MetadataRefreshOptions options,
|
||||||
ICollection<IMetadataProvider> providers,
|
ICollection<IMetadataProvider> providers,
|
||||||
ItemImageProvider imageService,
|
ItemImageProvider imageService,
|
||||||
bool isSavingMetadata,
|
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var refreshResult = new RefreshResult
|
var refreshResult = new RefreshResult
|
||||||
@@ -675,78 +667,71 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
};
|
};
|
||||||
temp.Item.Path = item.Path;
|
temp.Item.Path = item.Path;
|
||||||
temp.Item.Id = item.Id;
|
temp.Item.Id = item.Id;
|
||||||
temp.Item.PreferredMetadataCountryCode = item.PreferredMetadataCountryCode;
|
|
||||||
temp.Item.PreferredMetadataLanguage = item.PreferredMetadataLanguage;
|
|
||||||
|
|
||||||
var foundImageTypes = new List<ImageType>();
|
var foundImageTypes = new List<ImageType>();
|
||||||
|
foreach (var provider in providers.OfType<ILocalMetadataProvider<TItemType>>())
|
||||||
// Do not execute local providers if we are identifying or replacing with local metadata saving enabled
|
|
||||||
if (options.SearchResult is null && !(isSavingMetadata && options.ReplaceAllMetadata))
|
|
||||||
{
|
{
|
||||||
foreach (var provider in providers.OfType<ILocalMetadataProvider<TItemType>>())
|
var providerName = provider.GetType().Name;
|
||||||
|
Logger.LogDebug("Running {Provider} for {Item}", providerName, logName);
|
||||||
|
|
||||||
|
var itemInfo = new ItemInfo(item);
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var providerName = provider.GetType().Name;
|
var localItem = await provider.GetMetadata(itemInfo, options.DirectoryService, cancellationToken).ConfigureAwait(false);
|
||||||
Logger.LogDebug("Running {Provider} for {Item}", providerName, logName);
|
|
||||||
|
|
||||||
var itemInfo = new ItemInfo(item);
|
if (localItem.HasMetadata)
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
var localItem = await provider.GetMetadata(itemInfo, options.DirectoryService, cancellationToken).ConfigureAwait(false);
|
foreach (var remoteImage in localItem.RemoteImages)
|
||||||
|
|
||||||
if (localItem.HasMetadata)
|
|
||||||
{
|
{
|
||||||
foreach (var remoteImage in localItem.RemoteImages)
|
try
|
||||||
{
|
{
|
||||||
try
|
if (item.ImageInfos.Any(x => x.Type == remoteImage.Type)
|
||||||
|
&& !options.IsReplacingImage(remoteImage.Type))
|
||||||
{
|
{
|
||||||
if (item.ImageInfos.Any(x => x.Type == remoteImage.Type)
|
continue;
|
||||||
&& !options.IsReplacingImage(remoteImage.Type))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
await ProviderManager.SaveImage(item, remoteImage.Url, remoteImage.Type, null, cancellationToken).ConfigureAwait(false);
|
|
||||||
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
|
|
||||||
|
|
||||||
// remember imagetype that has just been downloaded
|
|
||||||
foundImageTypes.Add(remoteImage.Type);
|
|
||||||
}
|
}
|
||||||
catch (HttpRequestException ex)
|
|
||||||
{
|
|
||||||
Logger.LogError(ex, "Could not save {ImageType} image: {Url}", Enum.GetName(remoteImage.Type), remoteImage.Url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (foundImageTypes.Count > 0)
|
await ProviderManager.SaveImage(item, remoteImage.Url, remoteImage.Type, null, cancellationToken).ConfigureAwait(false);
|
||||||
{
|
|
||||||
imageService.UpdateReplaceImages(options, foundImageTypes);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (imageService.MergeImages(item, localItem.Images, options))
|
|
||||||
{
|
|
||||||
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
|
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
|
||||||
|
|
||||||
|
// remember imagetype that has just been downloaded
|
||||||
|
foundImageTypes.Add(remoteImage.Type);
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Could not save {ImageType} image: {Url}", Enum.GetName(remoteImage.Type), remoteImage.Url);
|
||||||
}
|
}
|
||||||
|
|
||||||
MergeData(localItem, temp, Array.Empty<MetadataField>(), false, true);
|
|
||||||
refreshResult.UpdateType |= ItemUpdateType.MetadataImport;
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.LogDebug("{Provider} returned no metadata for {Item}", providerName, logName);
|
if (foundImageTypes.Count > 0)
|
||||||
}
|
{
|
||||||
catch (OperationCanceledException)
|
imageService.UpdateReplaceImages(options, foundImageTypes);
|
||||||
{
|
}
|
||||||
throw;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogError(ex, "Error in {Provider}", provider.Name);
|
|
||||||
|
|
||||||
// If a local provider fails, consider that a failure
|
if (imageService.MergeImages(item, localItem.Images, options))
|
||||||
refreshResult.ErrorMessage = ex.Message;
|
{
|
||||||
|
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
MergeData(localItem, temp, Array.Empty<MetadataField>(), false, true);
|
||||||
|
refreshResult.UpdateType |= ItemUpdateType.MetadataImport;
|
||||||
|
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger.LogDebug("{Provider} returned no metadata for {Item}", providerName, logName);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Error in {Provider}", provider.Name);
|
||||||
|
|
||||||
|
// If a local provider fails, consider that a failure
|
||||||
|
refreshResult.ErrorMessage = ex.Message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -778,7 +763,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
var shouldReplace = options.MetadataRefreshMode > MetadataRefreshMode.ValidationOnly || options.ReplaceAllMetadata;
|
var shouldReplace = options.MetadataRefreshMode > MetadataRefreshMode.ValidationOnly || options.ReplaceAllMetadata;
|
||||||
MergeData(temp, metadata, item.LockedFields, shouldReplace, true);
|
MergeData(temp, metadata, item.LockedFields, shouldReplace, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -819,16 +804,19 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
{
|
{
|
||||||
var refreshResult = new RefreshResult();
|
var refreshResult = new RefreshResult();
|
||||||
|
|
||||||
if (id is not null)
|
var tmpDataMerged = false;
|
||||||
{
|
|
||||||
MergeNewData(temp.Item, id);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var provider in providers)
|
foreach (var provider in providers)
|
||||||
{
|
{
|
||||||
var providerName = provider.GetType().Name;
|
var providerName = provider.GetType().Name;
|
||||||
Logger.LogDebug("Running {Provider} for {Item}", providerName, logName);
|
Logger.LogDebug("Running {Provider} for {Item}", providerName, logName);
|
||||||
|
|
||||||
|
if (id is not null && !tmpDataMerged)
|
||||||
|
{
|
||||||
|
MergeNewData(temp.Item, id);
|
||||||
|
tmpDataMerged = true;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await provider.GetMetadata(id, cancellationToken).ConfigureAwait(false);
|
var result = await provider.GetMetadata(id, cancellationToken).ConfigureAwait(false);
|
||||||
@@ -1062,7 +1050,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
target.ProductionLocations = target.ProductionLocations.Concat(source.ProductionLocations).Distinct().ToArray();
|
target.Tags = target.ProductionLocations.Concat(source.ProductionLocations).Distinct().ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1092,7 +1080,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
target.RemoteTrailers = target.RemoteTrailers.Concat(source.RemoteTrailers).DistinctBy(t => t.Url).ToArray();
|
target.RemoteTrailers = target.RemoteTrailers.Concat(source.RemoteTrailers).Distinct().ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
MergeAlbumArtist(source, target, replaceData);
|
MergeAlbumArtist(source, target, replaceData);
|
||||||
|
|||||||
@@ -418,12 +418,6 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
return GetMetadataProvidersInternal<T>(item, libraryOptions, globalMetadataOptions, false, false);
|
return GetMetadataProvidersInternal<T>(item, libraryOptions, globalMetadataOptions, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public IEnumerable<IMetadataSaver> GetMetadataSavers(BaseItem item, LibraryOptions libraryOptions)
|
|
||||||
{
|
|
||||||
return _savers.Where(i => IsSaverEnabledForItem(i, item, libraryOptions, ItemUpdateType.MetadataEdit, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<IMetadataProvider<T>> GetMetadataProvidersInternal<T>(BaseItem item, LibraryOptions libraryOptions, MetadataOptions globalMetadataOptions, bool includeDisabled, bool forceEnableInternetMetadata)
|
private IEnumerable<IMetadataProvider<T>> GetMetadataProvidersInternal<T>(BaseItem item, LibraryOptions libraryOptions, MetadataOptions globalMetadataOptions, bool includeDisabled, bool forceEnableInternetMetadata)
|
||||||
where T : BaseItem
|
where T : BaseItem
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using Jellyfin.Extensions;
|
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Entities.Audio;
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
@@ -137,10 +136,6 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
if (!audio.IsLocked)
|
if (!audio.IsLocked)
|
||||||
{
|
{
|
||||||
await FetchDataFromTags(audio, mediaInfo, options, tryExtractEmbeddedLyrics).ConfigureAwait(false);
|
await FetchDataFromTags(audio, mediaInfo, options, tryExtractEmbeddedLyrics).ConfigureAwait(false);
|
||||||
if (tryExtractEmbeddedLyrics)
|
|
||||||
{
|
|
||||||
AddExternalLyrics(audio, mediaStreams, options);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
audio.HasLyrics = mediaStreams.Any(s => s.Type == MediaStreamType.Lyric);
|
audio.HasLyrics = mediaStreams.Any(s => s.Type == MediaStreamType.Lyric);
|
||||||
@@ -198,11 +193,11 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
}
|
}
|
||||||
|
|
||||||
tags ??= new TagLib.Id3v2.Tag();
|
tags ??= new TagLib.Id3v2.Tag();
|
||||||
tags.AlbumArtists = tags.AlbumArtists.Length == 0 ? mediaInfo.AlbumArtists : tags.AlbumArtists;
|
tags.AlbumArtists ??= mediaInfo.AlbumArtists;
|
||||||
tags.Album ??= mediaInfo.Album;
|
tags.Album ??= mediaInfo.Album;
|
||||||
tags.Title ??= mediaInfo.Name;
|
tags.Title ??= mediaInfo.Name;
|
||||||
tags.Year = tags.Year == 0U ? Convert.ToUInt32(mediaInfo.ProductionYear, CultureInfo.InvariantCulture) : tags.Year;
|
tags.Year = tags.Year == 0U ? Convert.ToUInt32(mediaInfo.ProductionYear, CultureInfo.InvariantCulture) : tags.Year;
|
||||||
tags.Performers = tags.Performers.Length == 0 ? mediaInfo.Artists : tags.Performers;
|
tags.Performers ??= mediaInfo.Artists;
|
||||||
tags.Genres ??= mediaInfo.Genres;
|
tags.Genres ??= mediaInfo.Genres;
|
||||||
tags.Track = tags.Track == 0U ? Convert.ToUInt32(mediaInfo.IndexNumber, CultureInfo.InvariantCulture) : tags.Track;
|
tags.Track = tags.Track == 0U ? Convert.ToUInt32(mediaInfo.IndexNumber, CultureInfo.InvariantCulture) : tags.Track;
|
||||||
tags.Disc = tags.Disc == 0U ? Convert.ToUInt32(mediaInfo.ParentIndexNumber, CultureInfo.InvariantCulture) : tags.Disc;
|
tags.Disc = tags.Disc == 0U ? Convert.ToUInt32(mediaInfo.ParentIndexNumber, CultureInfo.InvariantCulture) : tags.Disc;
|
||||||
@@ -374,10 +369,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
var externalLyricFiles = _lyricResolver.GetExternalStreams(audio, startIndex, options.DirectoryService, false);
|
var externalLyricFiles = _lyricResolver.GetExternalStreams(audio, startIndex, options.DirectoryService, false);
|
||||||
|
|
||||||
audio.LyricFiles = externalLyricFiles.Select(i => i.Path).Distinct().ToArray();
|
audio.LyricFiles = externalLyricFiles.Select(i => i.Path).Distinct().ToArray();
|
||||||
if (externalLyricFiles.Count > 0)
|
currentStreams.AddRange(externalLyricFiles);
|
||||||
{
|
|
||||||
currentStreams.Add(externalLyricFiles[0]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -358,10 +358,6 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
blurayVideoStream.BitRate = blurayVideoStream.BitRate.GetValueOrDefault() == 0 ? ffmpegVideoStream.BitRate : blurayVideoStream.BitRate;
|
blurayVideoStream.BitRate = blurayVideoStream.BitRate.GetValueOrDefault() == 0 ? ffmpegVideoStream.BitRate : blurayVideoStream.BitRate;
|
||||||
blurayVideoStream.Width = blurayVideoStream.Width.GetValueOrDefault() == 0 ? ffmpegVideoStream.Width : blurayVideoStream.Width;
|
blurayVideoStream.Width = blurayVideoStream.Width.GetValueOrDefault() == 0 ? ffmpegVideoStream.Width : blurayVideoStream.Width;
|
||||||
blurayVideoStream.Height = blurayVideoStream.Height.GetValueOrDefault() == 0 ? ffmpegVideoStream.Width : blurayVideoStream.Height;
|
blurayVideoStream.Height = blurayVideoStream.Height.GetValueOrDefault() == 0 ? ffmpegVideoStream.Width : blurayVideoStream.Height;
|
||||||
blurayVideoStream.ColorRange = ffmpegVideoStream.ColorRange;
|
|
||||||
blurayVideoStream.ColorSpace = ffmpegVideoStream.ColorSpace;
|
|
||||||
blurayVideoStream.ColorTransfer = ffmpegVideoStream.ColorTransfer;
|
|
||||||
blurayVideoStream.ColorPrimaries = ffmpegVideoStream.ColorPrimaries;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@@ -16,212 +18,182 @@ using MediaBrowser.Model.IO;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using PlaylistsNET.Content;
|
using PlaylistsNET.Content;
|
||||||
|
|
||||||
namespace MediaBrowser.Providers.Playlists;
|
namespace MediaBrowser.Providers.Playlists
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Local playlist provider.
|
|
||||||
/// </summary>
|
|
||||||
public class PlaylistItemsProvider : ILocalMetadataProvider<Playlist>,
|
|
||||||
IHasOrder,
|
|
||||||
IForcedProvider,
|
|
||||||
IHasItemChangeMonitor
|
|
||||||
{
|
{
|
||||||
private readonly IFileSystem _fileSystem;
|
public class PlaylistItemsProvider : ICustomMetadataProvider<Playlist>,
|
||||||
private readonly ILibraryManager _libraryManager;
|
IHasOrder,
|
||||||
private readonly ILogger<PlaylistItemsProvider> _logger;
|
IForcedProvider,
|
||||||
private readonly CollectionType[] _ignoredCollections = [CollectionType.livetv, CollectionType.boxsets, CollectionType.playlists];
|
IPreRefreshProvider,
|
||||||
|
IHasItemChangeMonitor
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="PlaylistItemsProvider"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="logger">Instance of the <see cref="ILogger{PlaylistItemsProvider}"/> interface.</param>
|
|
||||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
|
||||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
|
||||||
public PlaylistItemsProvider(ILogger<PlaylistItemsProvider> logger, ILibraryManager libraryManager, IFileSystem fileSystem)
|
|
||||||
{
|
{
|
||||||
_logger = logger;
|
private readonly IFileSystem _fileSystem;
|
||||||
_libraryManager = libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
_fileSystem = fileSystem;
|
private readonly ILogger<PlaylistItemsProvider> _logger;
|
||||||
}
|
private readonly CollectionType[] _ignoredCollections = [CollectionType.livetv, CollectionType.boxsets, CollectionType.playlists];
|
||||||
|
|
||||||
/// <inheritdoc />
|
public PlaylistItemsProvider(ILogger<PlaylistItemsProvider> logger, ILibraryManager libraryManager, IFileSystem fileSystem)
|
||||||
public string Name => "Playlist Item Provider";
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public int Order => 100;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Task<MetadataResult<Playlist>> GetMetadata(
|
|
||||||
ItemInfo info,
|
|
||||||
IDirectoryService directoryService,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var result = new MetadataResult<Playlist>()
|
|
||||||
{
|
{
|
||||||
Item = new Playlist
|
_logger = logger;
|
||||||
|
_libraryManager = libraryManager;
|
||||||
|
_fileSystem = fileSystem;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name => "Playlist Reader";
|
||||||
|
|
||||||
|
// Run last
|
||||||
|
public int Order => 100;
|
||||||
|
|
||||||
|
public Task<ItemUpdateType> FetchAsync(Playlist item, MetadataRefreshOptions options, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var path = item.Path;
|
||||||
|
if (!Playlist.IsPlaylistFile(path))
|
||||||
{
|
{
|
||||||
Path = info.Path
|
return Task.FromResult(ItemUpdateType.None);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
Fetch(result);
|
|
||||||
|
|
||||||
return Task.FromResult(result);
|
var extension = Path.GetExtension(path);
|
||||||
}
|
if (!Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return Task.FromResult(ItemUpdateType.None);
|
||||||
|
}
|
||||||
|
|
||||||
private void Fetch(MetadataResult<Playlist> result)
|
var items = GetItems(path, extension).ToArray();
|
||||||
{
|
|
||||||
var item = result.Item;
|
|
||||||
var path = item.Path;
|
|
||||||
if (!Playlist.IsPlaylistFile(path))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var extension = Path.GetExtension(path);
|
|
||||||
if (!Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var items = GetItems(path, extension).ToArray();
|
|
||||||
if (items.Length > 0)
|
|
||||||
{
|
|
||||||
result.HasMetadata = true;
|
|
||||||
item.LinkedChildren = items;
|
item.LinkedChildren = items;
|
||||||
|
|
||||||
|
return Task.FromResult(ItemUpdateType.MetadataImport);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
private IEnumerable<LinkedChild> GetItems(string path, string extension)
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<LinkedChild> GetItems(string path, string extension)
|
|
||||||
{
|
|
||||||
var libraryRoots = _libraryManager.GetUserRootFolder().Children
|
|
||||||
.OfType<CollectionFolder>()
|
|
||||||
.Where(f => f.CollectionType.HasValue && !_ignoredCollections.Contains(f.CollectionType.Value))
|
|
||||||
.SelectMany(f => f.PhysicalLocations)
|
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
using (var stream = File.OpenRead(path))
|
|
||||||
{
|
{
|
||||||
if (string.Equals(".wpl", extension, StringComparison.OrdinalIgnoreCase))
|
var libraryRoots = _libraryManager.GetUserRootFolder().Children
|
||||||
|
.OfType<CollectionFolder>()
|
||||||
|
.Where(f => f.CollectionType.HasValue && !_ignoredCollections.Contains(f.CollectionType.Value))
|
||||||
|
.SelectMany(f => f.PhysicalLocations)
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
using (var stream = File.OpenRead(path))
|
||||||
{
|
{
|
||||||
return GetWplItems(stream, path, libraryRoots);
|
if (string.Equals(".wpl", extension, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return GetWplItems(stream, path, libraryRoots);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(".zpl", extension, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return GetZplItems(stream, path, libraryRoots);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return GetM3uItems(stream, path, libraryRoots);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(".m3u8", extension, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return GetM3uItems(stream, path, libraryRoots);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(".pls", extension, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return GetPlsItems(stream, path, libraryRoots);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(".zpl", extension, StringComparison.OrdinalIgnoreCase))
|
return Enumerable.Empty<LinkedChild>();
|
||||||
{
|
|
||||||
return GetZplItems(stream, path, libraryRoots);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return GetM3uItems(stream, path, libraryRoots);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(".m3u8", extension, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return GetM3uItems(stream, path, libraryRoots);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(".pls", extension, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return GetPlsItems(stream, path, libraryRoots);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Enumerable.Empty<LinkedChild>();
|
private IEnumerable<LinkedChild> GetPlsItems(Stream stream, string playlistPath, List<string> libraryRoots)
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<LinkedChild> GetPlsItems(Stream stream, string playlistPath, List<string> libraryRoots)
|
|
||||||
{
|
|
||||||
var content = new PlsContent();
|
|
||||||
var playlist = content.GetFromStream(stream);
|
|
||||||
|
|
||||||
return playlist.PlaylistEntries
|
|
||||||
.Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
|
|
||||||
.Where(i => i is not null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<LinkedChild> GetM3uItems(Stream stream, string playlistPath, List<string> libraryRoots)
|
|
||||||
{
|
|
||||||
var content = new M3uContent();
|
|
||||||
var playlist = content.GetFromStream(stream);
|
|
||||||
|
|
||||||
return playlist.PlaylistEntries
|
|
||||||
.Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
|
|
||||||
.Where(i => i is not null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<LinkedChild> GetZplItems(Stream stream, string playlistPath, List<string> libraryRoots)
|
|
||||||
{
|
|
||||||
var content = new ZplContent();
|
|
||||||
var playlist = content.GetFromStream(stream);
|
|
||||||
|
|
||||||
return playlist.PlaylistEntries
|
|
||||||
.Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
|
|
||||||
.Where(i => i is not null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<LinkedChild> GetWplItems(Stream stream, string playlistPath, List<string> libraryRoots)
|
|
||||||
{
|
|
||||||
var content = new WplContent();
|
|
||||||
var playlist = content.GetFromStream(stream);
|
|
||||||
|
|
||||||
return playlist.PlaylistEntries
|
|
||||||
.Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
|
|
||||||
.Where(i => i is not null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private LinkedChild GetLinkedChild(string itemPath, string playlistPath, List<string> libraryRoots)
|
|
||||||
{
|
|
||||||
if (TryGetPlaylistItemPath(itemPath, playlistPath, libraryRoots, out var parsedPath))
|
|
||||||
{
|
{
|
||||||
return new LinkedChild
|
var content = new PlsContent();
|
||||||
{
|
var playlist = content.GetFromStream(stream);
|
||||||
Path = parsedPath,
|
|
||||||
Type = LinkedChildType.Manual
|
return playlist.PlaylistEntries
|
||||||
};
|
.Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
|
||||||
|
.Where(i => i is not null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
private IEnumerable<LinkedChild> GetM3uItems(Stream stream, string playlistPath, List<string> libraryRoots)
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryGetPlaylistItemPath(string itemPath, string playlistPath, List<string> libraryPaths, out string path)
|
|
||||||
{
|
|
||||||
path = null;
|
|
||||||
string pathToCheck = _fileSystem.MakeAbsolutePath(Path.GetDirectoryName(playlistPath), itemPath);
|
|
||||||
if (!File.Exists(pathToCheck))
|
|
||||||
{
|
{
|
||||||
|
var content = new M3uContent();
|
||||||
|
var playlist = content.GetFromStream(stream);
|
||||||
|
|
||||||
|
return playlist.PlaylistEntries
|
||||||
|
.Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
|
||||||
|
.Where(i => i is not null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<LinkedChild> GetZplItems(Stream stream, string playlistPath, List<string> libraryRoots)
|
||||||
|
{
|
||||||
|
var content = new ZplContent();
|
||||||
|
var playlist = content.GetFromStream(stream);
|
||||||
|
|
||||||
|
return playlist.PlaylistEntries
|
||||||
|
.Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
|
||||||
|
.Where(i => i is not null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<LinkedChild> GetWplItems(Stream stream, string playlistPath, List<string> libraryRoots)
|
||||||
|
{
|
||||||
|
var content = new WplContent();
|
||||||
|
var playlist = content.GetFromStream(stream);
|
||||||
|
|
||||||
|
return playlist.PlaylistEntries
|
||||||
|
.Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
|
||||||
|
.Where(i => i is not null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LinkedChild GetLinkedChild(string itemPath, string playlistPath, List<string> libraryRoots)
|
||||||
|
{
|
||||||
|
if (TryGetPlaylistItemPath(itemPath, playlistPath, libraryRoots, out var parsedPath))
|
||||||
|
{
|
||||||
|
return new LinkedChild
|
||||||
|
{
|
||||||
|
Path = parsedPath,
|
||||||
|
Type = LinkedChildType.Manual
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryGetPlaylistItemPath(string itemPath, string playlistPath, List<string> libraryPaths, out string path)
|
||||||
|
{
|
||||||
|
path = null;
|
||||||
|
string pathToCheck = _fileSystem.MakeAbsolutePath(Path.GetDirectoryName(playlistPath), itemPath);
|
||||||
|
if (!File.Exists(pathToCheck))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var libraryPath in libraryPaths)
|
||||||
|
{
|
||||||
|
if (pathToCheck.StartsWith(libraryPath, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
path = pathToCheck;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var libraryPath in libraryPaths)
|
public bool HasChanged(BaseItem item, IDirectoryService directoryService)
|
||||||
{
|
{
|
||||||
if (pathToCheck.StartsWith(libraryPath, StringComparison.OrdinalIgnoreCase))
|
var path = item.Path;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(path) && item.IsFileProtocol)
|
||||||
{
|
{
|
||||||
path = pathToCheck;
|
var file = directoryService.GetFile(path);
|
||||||
return true;
|
if (file is not null && file.LastWriteTimeUtc != item.DateModified)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Refreshing {Path} due to date modified timestamp change.", path);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public bool HasChanged(BaseItem item, IDirectoryService directoryService)
|
|
||||||
{
|
|
||||||
var path = item.Path;
|
|
||||||
if (!string.IsNullOrWhiteSpace(path) && item.IsFileProtocol)
|
|
||||||
{
|
|
||||||
var file = directoryService.GetFile(path);
|
|
||||||
if (file is not null && file.LastWriteTimeUtc != item.DateModified)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Refreshing {Path} due to date modified timestamp change.", path);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu
|
|||||||
// If we have a release ID but not a release group ID, lookup the release group
|
// If we have a release ID but not a release group ID, lookup the release group
|
||||||
if (!string.IsNullOrWhiteSpace(releaseId) && string.IsNullOrWhiteSpace(releaseGroupId))
|
if (!string.IsNullOrWhiteSpace(releaseId) && string.IsNullOrWhiteSpace(releaseGroupId))
|
||||||
{
|
{
|
||||||
var release = await _musicBrainzQuery.LookupReleaseAsync(new Guid(releaseId), Include.ReleaseGroups, cancellationToken).ConfigureAwait(false);
|
var release = await _musicBrainzQuery.LookupReleaseAsync(new Guid(releaseId), Include.Releases, cancellationToken).ConfigureAwait(false);
|
||||||
releaseGroupId = release.ReleaseGroup?.Id.ToString();
|
releaseGroupId = release.ReleaseGroup?.Id.ToString();
|
||||||
result.HasMetadata = true;
|
result.HasMetadata = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ namespace MediaBrowser.Providers.TV
|
|||||||
|
|
||||||
private void RemoveObsoleteSeasons(Series series)
|
private void RemoveObsoleteSeasons(Series series)
|
||||||
{
|
{
|
||||||
// TODO Legacy. It's not really "physical" seasons as any virtual seasons are always converted to non-virtual in CreateSeasonsAsync.
|
// TODO Legacy. It's not really "physical" seasons as any virtual seasons are always converted to non-virtual in UpdateAndCreateSeasonsAsync.
|
||||||
var physicalSeasonNumbers = new HashSet<int>();
|
var physicalSeasonNumbers = new HashSet<int>();
|
||||||
var virtualSeasons = new List<Season>();
|
var virtualSeasons = new List<Season>();
|
||||||
foreach (var existingSeason in series.Children.OfType<Season>())
|
foreach (var existingSeason in series.Children.OfType<Season>())
|
||||||
@@ -119,8 +119,7 @@ namespace MediaBrowser.Providers.TV
|
|||||||
virtualSeason,
|
virtualSeason,
|
||||||
new DeleteOptions
|
new DeleteOptions
|
||||||
{
|
{
|
||||||
// Internal metadata paths are removed regardless of this.
|
DeleteFileLocation = true
|
||||||
DeleteFileLocation = false
|
|
||||||
},
|
},
|
||||||
false);
|
false);
|
||||||
}
|
}
|
||||||
@@ -177,8 +176,7 @@ namespace MediaBrowser.Providers.TV
|
|||||||
episode,
|
episode,
|
||||||
new DeleteOptions
|
new DeleteOptions
|
||||||
{
|
{
|
||||||
// Internal metadata paths are removed regardless of this.
|
DeleteFileLocation = true
|
||||||
DeleteFileLocation = false
|
|
||||||
},
|
},
|
||||||
false);
|
false);
|
||||||
}
|
}
|
||||||
@@ -203,20 +201,11 @@ namespace MediaBrowser.Providers.TV
|
|||||||
foreach (var seasonNumber in uniqueSeasonNumbers)
|
foreach (var seasonNumber in uniqueSeasonNumbers)
|
||||||
{
|
{
|
||||||
// Null season numbers will have a 'dummy' season created because seasons are always required.
|
// Null season numbers will have a 'dummy' season created because seasons are always required.
|
||||||
var existingSeason = seasons.FirstOrDefault(i => i.IndexNumber == seasonNumber);
|
if (!seasons.Any(i => i.IndexNumber == seasonNumber))
|
||||||
if (existingSeason is null)
|
|
||||||
{
|
{
|
||||||
var seasonName = GetValidSeasonNameForSeries(series, null, seasonNumber);
|
var seasonName = GetValidSeasonNameForSeries(series, null, seasonNumber);
|
||||||
await CreateSeasonAsync(series, seasonName, seasonNumber, cancellationToken).ConfigureAwait(false);
|
var season = await CreateSeasonAsync(series, seasonName, seasonNumber, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
series.AddChild(season);
|
||||||
else if (existingSeason.IsVirtualItem)
|
|
||||||
{
|
|
||||||
var episodeCount = seriesChildren.OfType<Episode>().Count(e => e.ParentIndexNumber == seasonNumber && !e.IsMissingEpisode);
|
|
||||||
if (episodeCount > 0)
|
|
||||||
{
|
|
||||||
existingSeason.IsVirtualItem = false;
|
|
||||||
await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,7 +218,7 @@ namespace MediaBrowser.Providers.TV
|
|||||||
/// <param name="seasonNumber">The season number.</param>
|
/// <param name="seasonNumber">The season number.</param>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <returns>The newly created season.</returns>
|
/// <returns>The newly created season.</returns>
|
||||||
private async Task CreateSeasonAsync(
|
private async Task<Season> CreateSeasonAsync(
|
||||||
Series series,
|
Series series,
|
||||||
string? seasonName,
|
string? seasonName,
|
||||||
int? seasonNumber,
|
int? seasonNumber,
|
||||||
@@ -246,12 +235,14 @@ namespace MediaBrowser.Providers.TV
|
|||||||
typeof(Season)),
|
typeof(Season)),
|
||||||
IsVirtualItem = false,
|
IsVirtualItem = false,
|
||||||
SeriesId = series.Id,
|
SeriesId = series.Id,
|
||||||
SeriesName = series.Name,
|
SeriesName = series.Name
|
||||||
SeriesPresentationUniqueKey = series.GetPresentationUniqueKey()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
series.AddChild(season);
|
series.AddChild(season);
|
||||||
|
|
||||||
await season.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken).ConfigureAwait(false);
|
await season.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
return season;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetValidSeasonNameForSeries(Series series, string? seasonName, int? seasonNumber)
|
private string GetValidSeasonNameForSeries(Series series, string? seasonName, int? seasonNumber)
|
||||||
|
|||||||
@@ -519,9 +519,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
|||||||
if (reader.TryReadDateTimeExact(nfoConfiguration.ReleaseDateFormat, out var releaseDate))
|
if (reader.TryReadDateTimeExact(nfoConfiguration.ReleaseDateFormat, out var releaseDate))
|
||||||
{
|
{
|
||||||
item.PremiereDate = releaseDate;
|
item.PremiereDate = releaseDate;
|
||||||
|
item.ProductionYear = releaseDate.Year;
|
||||||
// Production year can already be set by the year tag
|
|
||||||
item.ProductionYear ??= releaseDate.Year;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -825,7 +825,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
|||||||
private string GetOutputTrailerUrl(string url)
|
private string GetOutputTrailerUrl(string url)
|
||||||
{
|
{
|
||||||
// This is what xbmc expects
|
// This is what xbmc expects
|
||||||
return url.Replace(YouTubeWatchUrl, "plugin://plugin.video.youtube/play/?video_id=", StringComparison.OrdinalIgnoreCase);
|
return url.Replace(YouTubeWatchUrl, "plugin://plugin.video.youtube/?action=play_video&videoid=", StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddImages(BaseItem item, XmlWriter writer, ILibraryManager libraryManager)
|
private void AddImages(BaseItem item, XmlWriter writer, ILibraryManager libraryManager)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user