mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-12-12 20:13:01 +03:00
Compare commits
280 Commits
feature/en
...
v10.9.10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24d482b36b | ||
|
|
fff4477a93 | ||
|
|
9810d22d96 | ||
|
|
5027e3cd53 | ||
|
|
9645955629 | ||
|
|
078ee1f2de | ||
|
|
236c7649dd | ||
|
|
c1e52df0b7 | ||
|
|
be949af59e | ||
|
|
122da8f447 | ||
|
|
486c7fa51e | ||
|
|
6709c80f0a | ||
|
|
3f3145600c | ||
|
|
bc613b8344 | ||
|
|
0eb5897100 | ||
|
|
ee0094d889 | ||
|
|
7051a18be0 | ||
|
|
fce3a5d241 | ||
|
|
900acc03aa | ||
|
|
a475a7d50a | ||
|
|
b7bc0e1c96 | ||
|
|
3c79d7a3f3 | ||
|
|
1739962f52 | ||
|
|
7f0f93eb4a | ||
|
|
71c13057f4 | ||
|
|
7f12677dc3 | ||
|
|
4f2b1736ab | ||
|
|
c05049e54e | ||
|
|
dd5f6406a2 | ||
|
|
79c0a7d7f0 | ||
|
|
3c6485f0a1 | ||
|
|
8a8b2c4380 | ||
|
|
7403428864 | ||
|
|
235da65a75 | ||
|
|
26eab7aa2e | ||
|
|
d235378133 | ||
|
|
5a62c7a146 | ||
|
|
4afa6db108 | ||
|
|
f7a90b6383 | ||
|
|
b8e2d8e11a | ||
|
|
c1f7ccbca4 | ||
|
|
5bab02fa54 | ||
|
|
f2fa0b9025 | ||
|
|
9c0edd2905 | ||
|
|
62deebc04c | ||
|
|
478d8b07bf | ||
|
|
c9b6ebd94f | ||
|
|
30fc089dd5 | ||
|
|
25f02658f0 | ||
|
|
2266a00337 | ||
|
|
afeff31dca | ||
|
|
476dc01f4d | ||
|
|
b81b674ae1 | ||
|
|
15eb7a25b9 | ||
|
|
aadd57bc48 | ||
|
|
cbbe5db813 | ||
|
|
4601097d3e | ||
|
|
10cd9a7f79 | ||
|
|
6cf98d4930 | ||
|
|
34a65980e3 | ||
|
|
6010bc01c3 | ||
|
|
a00f9e1a10 | ||
|
|
85078d8f10 | ||
|
|
1606b6c0f6 | ||
|
|
f097aad01e | ||
|
|
bf53f1ae38 | ||
|
|
31237f778a | ||
|
|
55d245a77b | ||
|
|
9f35f56eaf | ||
|
|
f2a5ccf102 | ||
|
|
2b78980747 | ||
|
|
a89678074e | ||
|
|
d813f83b4a | ||
|
|
37b7e953f7 | ||
|
|
08b64c5502 | ||
|
|
23a660e917 | ||
|
|
78eb9b2f78 | ||
|
|
d90f504ca7 | ||
|
|
56104d3042 | ||
|
|
fcec1fcc4d | ||
|
|
1a14902da8 | ||
|
|
34bdf8bf78 | ||
|
|
7ff3f6af6c | ||
|
|
bd8b0c4c03 | ||
|
|
0c560a313a | ||
|
|
8882bb495c | ||
|
|
e4078f984a | ||
|
|
8e5a2f565c | ||
|
|
8b442a7749 | ||
|
|
b63f7a2bc0 | ||
|
|
f9e7d5229e | ||
|
|
b24d05bff7 | ||
|
|
fd009fc71b | ||
|
|
302eea1cb7 | ||
|
|
b116a2742e | ||
|
|
99a04e23d9 | ||
|
|
19a89d5a60 | ||
|
|
feb20c131a | ||
|
|
ec82023265 | ||
|
|
e4f3f0b3b6 | ||
|
|
28274d4c75 | ||
|
|
b6595e4efc | ||
|
|
4046ef1c13 | ||
|
|
b25d6d1e48 | ||
|
|
cf59140276 | ||
|
|
cc4563a477 | ||
|
|
0d984b5162 | ||
|
|
279cba008b | ||
|
|
2b5d458456 | ||
|
|
f41efb3b2c | ||
|
|
0155293c64 | ||
|
|
b78efd6b1e | ||
|
|
bfcc09db8a | ||
|
|
a46c17e19f | ||
|
|
b0bb22b650 | ||
|
|
0c039145e5 | ||
|
|
2a3c904a9f | ||
|
|
7cbdb6708b | ||
|
|
7058db2b04 | ||
|
|
8f7df590cd | ||
|
|
0f67a5ba2f | ||
|
|
19fb00b5b7 | ||
|
|
8683253c6d | ||
|
|
918a36d564 | ||
|
|
57ae0b5796 | ||
|
|
262f7dd98f | ||
|
|
4714b3af67 | ||
|
|
0359035000 | ||
|
|
b14edb8876 | ||
|
|
47c5e0c2c7 | ||
|
|
8709d94783 | ||
|
|
23b1251393 | ||
|
|
484aea1cdb | ||
|
|
d1c00ba4ed | ||
|
|
b1a5fe2f55 | ||
|
|
d7ff6d023c | ||
|
|
253e95dcba | ||
|
|
c7ce1aa4c7 | ||
|
|
3d87885577 | ||
|
|
a7e2271845 | ||
|
|
2a02abee46 | ||
|
|
ab4315742f | ||
|
|
2ddb15c784 | ||
|
|
95c7d997c1 | ||
|
|
e2c909f50f | ||
|
|
a53ea029fa | ||
|
|
869dab2ba2 | ||
|
|
d2be2ee480 | ||
|
|
bc8ef94f0d | ||
|
|
ed1b880359 | ||
|
|
7221e7ca68 | ||
|
|
0392daa103 | ||
|
|
d602b6dbc5 | ||
|
|
b8a0cf6a9e | ||
|
|
26419c64f5 | ||
|
|
a71e2d9f0a | ||
|
|
cfe67ff17d | ||
|
|
78e3ee15f9 | ||
|
|
2cb74e3dd0 | ||
|
|
8e979bdb4b | ||
|
|
e099fd6141 | ||
|
|
35962bcc42 | ||
|
|
ae584beaac | ||
|
|
563033786f | ||
|
|
f8c7f36a34 | ||
|
|
4746c88633 | ||
|
|
b7d6bedbbb | ||
|
|
8db79c05dd | ||
|
|
8fa7ff647a | ||
|
|
d0336cd67e | ||
|
|
cfab4eb2fc | ||
|
|
5f1c5009d3 | ||
|
|
97d7151289 | ||
|
|
475fa36ea3 | ||
|
|
e8d1ee0934 | ||
|
|
d07ec4ad0f | ||
|
|
684dfedbcc | ||
|
|
730b01fb14 | ||
|
|
45e8872cc0 | ||
|
|
cc2c00d764 | ||
|
|
402a5e2c9f | ||
|
|
bcf884ccfa | ||
|
|
2eece01acc | ||
|
|
ef985896e2 | ||
|
|
5e7514243c | ||
|
|
b9c0fc69e8 | ||
|
|
4a344bebc0 | ||
|
|
b2d54b82fa | ||
|
|
e7b1162cb3 | ||
|
|
d89e5a0074 | ||
|
|
4a54e5ddeb | ||
|
|
d9232e05f1 | ||
|
|
52be8be28f | ||
|
|
ab6c2424db | ||
|
|
eb437e7163 | ||
|
|
2ddf2a7866 | ||
|
|
60232ce9be | ||
|
|
952995f796 | ||
|
|
933a285bf5 | ||
|
|
f8da69f8e5 | ||
|
|
d5e29bfce3 | ||
|
|
ab36c4c011 | ||
|
|
c6e29647fc | ||
|
|
95a6291c34 | ||
|
|
fa2bff30f6 | ||
|
|
58041e1f9d | ||
|
|
447f73caf4 | ||
|
|
9145be6bfc | ||
|
|
86129589ef | ||
|
|
f5a8fca22f | ||
|
|
2a612611b8 | ||
|
|
0b64426cf2 | ||
|
|
f3bf9bcdc8 | ||
|
|
06a5ddda5e | ||
|
|
8a5a93ee80 | ||
|
|
6777f47e0e | ||
|
|
cf04e1d8e5 | ||
|
|
d608f1e3cc | ||
|
|
86f5c93434 | ||
|
|
4fcbeef5e6 | ||
|
|
7d983ae0dd | ||
|
|
a2ab34ef4c | ||
|
|
77abafca8e | ||
|
|
e67eb48540 | ||
|
|
37d7e8f5bf | ||
|
|
e6eef8bece | ||
|
|
52cfd9f261 | ||
|
|
9a9e8e2648 | ||
|
|
2cebd5e05f | ||
|
|
7fa72260ca | ||
|
|
b19b346670 | ||
|
|
99de0ca45f | ||
|
|
c106b399d7 | ||
|
|
c274062e87 | ||
|
|
53de8c0805 | ||
|
|
76854b5eff | ||
|
|
5200633574 | ||
|
|
832e27a8fb | ||
|
|
2da06bc0b1 | ||
|
|
46c748d888 | ||
|
|
430d450828 | ||
|
|
02937873b1 | ||
|
|
d303ca56e3 | ||
|
|
c647143e53 | ||
|
|
dd0ab8ed56 | ||
|
|
18e6c1ef7d | ||
|
|
dec2032e13 | ||
|
|
287e06d6dc | ||
|
|
dc93cc13b5 | ||
|
|
26714e2c62 | ||
|
|
c6c48a2b47 | ||
|
|
f8b67ec44c | ||
|
|
ddd5c302b4 | ||
|
|
9b98638b2b | ||
|
|
2ca8ce6f60 | ||
|
|
56a158e5c9 | ||
|
|
9b65d243a8 | ||
|
|
c45dd5d6fb | ||
|
|
af4b732080 | ||
|
|
1cdf0f5cc4 | ||
|
|
20a1da1855 | ||
|
|
8aee50020b | ||
|
|
c1615419b9 | ||
|
|
15489eeae3 | ||
|
|
80c9589885 | ||
|
|
a5d60c4521 | ||
|
|
2cb052a119 | ||
|
|
0756174b13 | ||
|
|
3f760e6685 | ||
|
|
d5dc4435d9 | ||
|
|
48228430c0 | ||
|
|
f396a95f05 | ||
|
|
717afcdc82 | ||
|
|
25c50bcc5d | ||
|
|
f77a5d0c5c | ||
|
|
6689d837d6 | ||
|
|
c1907354e8 | ||
|
|
efba619acb | ||
|
|
7d271547c6 | ||
|
|
327f92bb2e |
@@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "8.0.6",
|
||||
"version": "8.0.7",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
|
||||
7
.github/ISSUE_TEMPLATE/issue report.yml
vendored
7
.github/ISSUE_TEMPLATE/issue report.yml
vendored
@@ -38,11 +38,10 @@ body:
|
||||
label: Jellyfin Version
|
||||
description: What version of Jellyfin are you running?
|
||||
options:
|
||||
- 10.9.0
|
||||
- 10.8.13
|
||||
- 10.8.12 or older (please specify)
|
||||
- Weekly unstable (please specify)
|
||||
- Master branch
|
||||
- 10.8.12
|
||||
- 10.8.11 or older (please specify)
|
||||
- Unstable (master branch)
|
||||
validations:
|
||||
required: true
|
||||
- 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'
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@f079b8493333aace61c81488f8bd40919487bd9f # v3.25.7
|
||||
uses: github/codeql-action/init@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: +security-extended
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@f079b8493333aace61c81488f8bd40919487bd9f # v3.25.7
|
||||
uses: github/codeql-action/autobuild@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@f079b8493333aace61c81488f8bd40919487bd9f # v3.25.7
|
||||
uses: github/codeql-action/analyze@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
|
||||
|
||||
2
.github/workflows/ci-tests.yml
vendored
2
.github/workflows/ci-tests.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
--verbosity minimal
|
||||
|
||||
- name: Merge code coverage results
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@fa728091745cdd279fddda1e0e80fb29265d0977 # 5.3.5
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@6b06171d1a131e7fd85121120a1c00c1ed03e033 # 5.3.0
|
||||
with:
|
||||
reports: "**/coverage.cobertura.xml"
|
||||
targetdir: "merged/"
|
||||
|
||||
2
.github/workflows/pull-request-conflict.yml
vendored
2
.github/workflows/pull-request-conflict.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
if: ${{ github.repository == 'jellyfin/jellyfin' }}
|
||||
steps:
|
||||
- name: Apply label
|
||||
uses: eps1lon/actions-label-merge-conflict@1b1b1fcde06a9b3d089f3464c96417961dde1168 # v3.0.2
|
||||
uses: eps1lon/actions-label-merge-conflict@6d74047dcef155976a15e4a124dde2c7fe0c5522 # v3.0.1
|
||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
|
||||
with:
|
||||
dirtyLabel: 'merge conflict'
|
||||
|
||||
@@ -16,33 +16,33 @@
|
||||
<PackageVersion Include="Diacritics" Version="3.3.29" />
|
||||
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
|
||||
<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="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.2" />
|
||||
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
|
||||
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.7" />
|
||||
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
|
||||
<PackageVersion Include="libse" Version="4.0.5" />
|
||||
<PackageVersion Include="libse" Version="4.0.7" />
|
||||
<PackageVersion Include="LrcParser" Version="2023.524.0" />
|
||||
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.6" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.6" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.7" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.7" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.6" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.6" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.6" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.6" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.7" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.7" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.7" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.7" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" 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.Binder" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" 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" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||
<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.DotNetRuntime" Version="4.4.0" />
|
||||
<PackageVersion Include="prometheus-net" Version="8.2.1" />
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.1" />
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.2" />
|
||||
<PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
|
||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Console" Version="5.0.1" />
|
||||
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.2" />
|
||||
<PackageVersion Include="Serilog.Sinks.Async" Version="2.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
|
||||
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
|
||||
<PackageVersion Include="SharpFuzz" Version="2.1.1" />
|
||||
@@ -73,19 +73,19 @@
|
||||
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<PackageVersion Include="Svg.Skia" Version="1.0.0.18" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.6.2" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
||||
<PackageVersion Include="System.Globalization" Version="4.3.0" />
|
||||
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="8.0.0" />
|
||||
<PackageVersion Include="System.Text.Json" Version="8.0.3" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="8.0.0" />
|
||||
<PackageVersion Include="System.Text.Json" Version="8.0.4" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="8.0.1" />
|
||||
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
||||
<PackageVersion Include="TMDbLib" Version="2.2.0" />
|
||||
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
|
||||
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.8" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.1" />
|
||||
<PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
|
||||
<PackageVersion Include="xunit" Version="2.7.1" />
|
||||
<PackageVersion Include="xunit" Version="2.8.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Naming</PackageId>
|
||||
<VersionPrefix>10.10.0</VersionPrefix>
|
||||
<VersionPrefix>10.9.10</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -107,7 +107,7 @@ namespace Emby.Naming.ExternalFiles
|
||||
pathInfo.Language = culture.ThreeLetterISOLanguageName;
|
||||
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
else if (_namingOptions.MediaHearingImpairedFlags.Any(s => currentSliceWithoutSeparator.Contains(s, StringComparison.OrdinalIgnoreCase)))
|
||||
else if (_namingOptions.MediaHearingImpairedFlags.Any(s => currentSliceWithoutSeparator.Equals(s, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
pathInfo.IsHearingImpaired = true;
|
||||
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
@@ -19,8 +19,7 @@ namespace Emby.Server.Implementations
|
||||
{ FfmpegAnalyzeDurationKey, "200M" },
|
||||
{ PlaylistsAllowDuplicatesKey, bool.FalseString },
|
||||
{ BindToUnixSocketKey, bool.FalseString },
|
||||
{ SqliteCacheSizeKey, "20000" },
|
||||
{ SqliteDisableSecondLevelCacheKey, bool.FalseString }
|
||||
{ SqliteCacheSizeKey, "20000" }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using Jellyfin.Extensions;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -13,6 +14,8 @@ namespace Emby.Server.Implementations.Data
|
||||
public abstract class BaseSqliteRepository : IDisposable
|
||||
{
|
||||
private bool _disposed = false;
|
||||
private SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1);
|
||||
private SqliteConnection _writeConnection;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BaseSqliteRepository"/> class.
|
||||
@@ -98,9 +101,55 @@ namespace Emby.Server.Implementations.Data
|
||||
}
|
||||
}
|
||||
|
||||
protected SqliteConnection GetConnection()
|
||||
protected ManagedConnection GetConnection(bool readOnly = false)
|
||||
{
|
||||
var connection = new SqliteConnection($"Filename={DbFilePath}");
|
||||
if (!readOnly)
|
||||
{
|
||||
_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();
|
||||
|
||||
if (CacheSize.HasValue)
|
||||
@@ -135,17 +184,17 @@ namespace Emby.Server.Implementations.Data
|
||||
|
||||
connection.Execute("PRAGMA temp_store=" + (int)TempStore);
|
||||
|
||||
return connection;
|
||||
return new ManagedConnection(connection, null);
|
||||
}
|
||||
|
||||
public SqliteCommand PrepareStatement(SqliteConnection connection, string sql)
|
||||
public SqliteCommand PrepareStatement(ManagedConnection connection, string sql)
|
||||
{
|
||||
var command = connection.CreateCommand();
|
||||
command.CommandText = sql;
|
||||
return command;
|
||||
}
|
||||
|
||||
protected bool TableExists(SqliteConnection connection, string name)
|
||||
protected bool TableExists(ManagedConnection connection, string name)
|
||||
{
|
||||
using var statement = PrepareStatement(connection, "select DISTINCT tbl_name from sqlite_master");
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
@@ -159,7 +208,7 @@ namespace Emby.Server.Implementations.Data
|
||||
return false;
|
||||
}
|
||||
|
||||
protected List<string> GetColumnNames(SqliteConnection connection, string table)
|
||||
protected List<string> GetColumnNames(ManagedConnection connection, string table)
|
||||
{
|
||||
var columnNames = new List<string>();
|
||||
|
||||
@@ -174,7 +223,7 @@ namespace Emby.Server.Implementations.Data
|
||||
return columnNames;
|
||||
}
|
||||
|
||||
protected void AddColumn(SqliteConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
|
||||
protected void AddColumn(ManagedConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
|
||||
{
|
||||
if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -207,6 +256,24 @@ namespace Emby.Server.Implementations.Data
|
||||
return;
|
||||
}
|
||||
|
||||
if (dispose)
|
||||
{
|
||||
_writeLock.Wait();
|
||||
try
|
||||
{
|
||||
_writeConnection.Dispose();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_writeLock.Release();
|
||||
}
|
||||
|
||||
_writeLock.Dispose();
|
||||
}
|
||||
|
||||
_writeConnection = null;
|
||||
_writeLock = null;
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
62
Emby.Server.Implementations/Data/ManagedConnection.cs
Normal file
62
Emby.Server.Implementations/Data/ManagedConnection.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
#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();
|
||||
}
|
||||
|
||||
private void SaveItemsInTransaction(SqliteConnection db, IEnumerable<(BaseItem Item, List<Guid> AncestorIds, BaseItem TopParent, string UserDataKey, List<string> InheritedTags)> tuples)
|
||||
private void SaveItemsInTransaction(ManagedConnection db, IEnumerable<(BaseItem Item, List<Guid> AncestorIds, BaseItem TopParent, string UserDataKey, List<string> InheritedTags)> tuples)
|
||||
{
|
||||
using (var saveItemStatement = PrepareStatement(db, SaveItemCommandText))
|
||||
using (var deleteAncestorsStatement = PrepareStatement(db, "delete from AncestorIds where ItemId=@ItemId"))
|
||||
@@ -1261,7 +1261,7 @@ namespace Emby.Server.Implementations.Data
|
||||
|
||||
CheckDisposed();
|
||||
|
||||
using (var connection = GetConnection())
|
||||
using (var connection = GetConnection(true))
|
||||
using (var statement = PrepareStatement(connection, _retrieveItemColumnsSelectQuery))
|
||||
{
|
||||
statement.TryBind("@guid", id);
|
||||
@@ -1887,7 +1887,7 @@ namespace Emby.Server.Implementations.Data
|
||||
CheckDisposed();
|
||||
|
||||
var chapters = new List<ChapterInfo>();
|
||||
using (var connection = GetConnection())
|
||||
using (var connection = GetConnection(true))
|
||||
using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc"))
|
||||
{
|
||||
statement.TryBind("@ItemId", item.Id);
|
||||
@@ -1906,7 +1906,7 @@ namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
CheckDisposed();
|
||||
|
||||
using (var connection = GetConnection())
|
||||
using (var connection = GetConnection(true))
|
||||
using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId and ChapterIndex=@ChapterIndex"))
|
||||
{
|
||||
statement.TryBind("@ItemId", item.Id);
|
||||
@@ -1980,7 +1980,7 @@ namespace Emby.Server.Implementations.Data
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
private void InsertChapters(Guid idBlob, IReadOnlyList<ChapterInfo> chapters, SqliteConnection db)
|
||||
private void InsertChapters(Guid idBlob, IReadOnlyList<ChapterInfo> chapters, ManagedConnection db)
|
||||
{
|
||||
var startIndex = 0;
|
||||
var limit = 100;
|
||||
@@ -2469,7 +2469,7 @@ namespace Emby.Server.Implementations.Data
|
||||
var commandText = commandTextBuilder.ToString();
|
||||
|
||||
using (new QueryTimeLogger(Logger, commandText))
|
||||
using (var connection = GetConnection())
|
||||
using (var connection = GetConnection(true))
|
||||
using (var statement = PrepareStatement(connection, commandText))
|
||||
{
|
||||
if (EnableJoinUserData(query))
|
||||
@@ -2537,7 +2537,7 @@ namespace Emby.Server.Implementations.Data
|
||||
var commandText = commandTextBuilder.ToString();
|
||||
var items = new List<BaseItem>();
|
||||
using (new QueryTimeLogger(Logger, commandText))
|
||||
using (var connection = GetConnection())
|
||||
using (var connection = GetConnection(true))
|
||||
using (var statement = PrepareStatement(connection, commandText))
|
||||
{
|
||||
if (EnableJoinUserData(query))
|
||||
@@ -2745,7 +2745,7 @@ namespace Emby.Server.Implementations.Data
|
||||
|
||||
var list = new List<BaseItem>();
|
||||
var result = new QueryResult<BaseItem>();
|
||||
using var connection = GetConnection();
|
||||
using var connection = GetConnection(true);
|
||||
using var transaction = connection.BeginTransaction();
|
||||
if (!isReturningZeroItems)
|
||||
{
|
||||
@@ -2927,7 +2927,7 @@ namespace Emby.Server.Implementations.Data
|
||||
var commandText = commandTextBuilder.ToString();
|
||||
var list = new List<Guid>();
|
||||
using (new QueryTimeLogger(Logger, commandText))
|
||||
using (var connection = GetConnection())
|
||||
using (var connection = GetConnection(true))
|
||||
using (var statement = PrepareStatement(connection, commandText))
|
||||
{
|
||||
if (EnableJoinUserData(query))
|
||||
@@ -4476,7 +4476,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
private void ExecuteWithSingleParam(SqliteConnection db, string query, Guid value)
|
||||
private void ExecuteWithSingleParam(ManagedConnection db, string query, Guid value)
|
||||
{
|
||||
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>();
|
||||
using (var connection = GetConnection())
|
||||
using (var connection = GetConnection(true))
|
||||
using (var statement = PrepareStatement(connection, commandText.ToString()))
|
||||
{
|
||||
// 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>();
|
||||
using (var connection = GetConnection())
|
||||
using (var connection = GetConnection(true))
|
||||
using (var statement = PrepareStatement(connection, commandText.ToString()))
|
||||
{
|
||||
// Run this again to bind the params
|
||||
@@ -4632,7 +4632,7 @@ AND Type = @InternalPersonType)");
|
||||
return whereClauses;
|
||||
}
|
||||
|
||||
private void UpdateAncestors(Guid itemId, List<Guid> ancestorIds, SqliteConnection db, SqliteCommand deleteAncestorsStatement)
|
||||
private void UpdateAncestors(Guid itemId, List<Guid> ancestorIds, ManagedConnection db, SqliteCommand deleteAncestorsStatement)
|
||||
{
|
||||
if (itemId.IsEmpty())
|
||||
{
|
||||
@@ -4787,7 +4787,7 @@ AND Type = @InternalPersonType)");
|
||||
|
||||
var list = new List<string>();
|
||||
using (new QueryTimeLogger(Logger, commandText))
|
||||
using (var connection = GetConnection())
|
||||
using (var connection = GetConnection(true))
|
||||
using (var statement = PrepareStatement(connection, commandText))
|
||||
{
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
@@ -4987,8 +4987,8 @@ AND Type = @InternalPersonType)");
|
||||
var list = new List<(BaseItem, ItemCounts)>();
|
||||
var result = new QueryResult<(BaseItem, ItemCounts)>();
|
||||
using (new QueryTimeLogger(Logger, commandText))
|
||||
using (var connection = GetConnection())
|
||||
using (var transaction = connection.BeginTransaction(deferred: true))
|
||||
using (var connection = GetConnection(true))
|
||||
using (var transaction = connection.BeginTransaction())
|
||||
{
|
||||
if (!isReturningZeroItems)
|
||||
{
|
||||
@@ -5148,7 +5148,7 @@ AND Type = @InternalPersonType)");
|
||||
return list;
|
||||
}
|
||||
|
||||
private void UpdateItemValues(Guid itemId, List<(int MagicNumber, string Value)> values, SqliteConnection db)
|
||||
private void UpdateItemValues(Guid itemId, List<(int MagicNumber, string Value)> values, ManagedConnection db)
|
||||
{
|
||||
if (itemId.IsEmpty())
|
||||
{
|
||||
@@ -5167,7 +5167,7 @@ AND Type = @InternalPersonType)");
|
||||
InsertItemValues(itemId, values, db);
|
||||
}
|
||||
|
||||
private void InsertItemValues(Guid id, List<(int MagicNumber, string Value)> values, SqliteConnection db)
|
||||
private void InsertItemValues(Guid id, List<(int MagicNumber, string Value)> values, ManagedConnection db)
|
||||
{
|
||||
const int Limit = 100;
|
||||
var startIndex = 0;
|
||||
@@ -5239,7 +5239,7 @@ AND Type = @InternalPersonType)");
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
private void InsertPeople(Guid id, List<PersonInfo> people, SqliteConnection db)
|
||||
private void InsertPeople(Guid id, List<PersonInfo> people, ManagedConnection db)
|
||||
{
|
||||
const int Limit = 100;
|
||||
var startIndex = 0;
|
||||
@@ -5335,7 +5335,7 @@ AND Type = @InternalPersonType)");
|
||||
|
||||
cmdText += " order by StreamIndex ASC";
|
||||
|
||||
using (var connection = GetConnection())
|
||||
using (var connection = GetConnection(true))
|
||||
{
|
||||
var list = new List<MediaStream>();
|
||||
|
||||
@@ -5388,7 +5388,7 @@ AND Type = @InternalPersonType)");
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
private void InsertMediaStreams(Guid id, IReadOnlyList<MediaStream> streams, SqliteConnection db)
|
||||
private void InsertMediaStreams(Guid id, IReadOnlyList<MediaStream> streams, ManagedConnection db)
|
||||
{
|
||||
const int Limit = 10;
|
||||
var startIndex = 0;
|
||||
@@ -5694,13 +5694,17 @@ AND Type = @InternalPersonType)");
|
||||
|
||||
item.IsHearingImpaired = reader.TryGetBoolean(43, out var result) && result;
|
||||
|
||||
if (item.Type == MediaStreamType.Subtitle)
|
||||
if (item.Type is MediaStreamType.Audio or MediaStreamType.Subtitle)
|
||||
{
|
||||
item.LocalizedUndefined = _localization.GetLocalizedString("Undefined");
|
||||
item.LocalizedDefault = _localization.GetLocalizedString("Default");
|
||||
item.LocalizedForced = _localization.GetLocalizedString("Forced");
|
||||
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;
|
||||
@@ -5722,7 +5726,7 @@ AND Type = @InternalPersonType)");
|
||||
cmdText += " order by AttachmentIndex ASC";
|
||||
|
||||
var list = new List<MediaAttachment>();
|
||||
using (var connection = GetConnection())
|
||||
using (var connection = GetConnection(true))
|
||||
using (var statement = PrepareStatement(connection, cmdText))
|
||||
{
|
||||
statement.TryBind("@ItemId", query.ItemId);
|
||||
@@ -5772,7 +5776,7 @@ AND Type = @InternalPersonType)");
|
||||
private void InsertMediaAttachments(
|
||||
Guid id,
|
||||
IReadOnlyList<MediaAttachment> attachments,
|
||||
SqliteConnection db,
|
||||
ManagedConnection db,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
const int InsertAtOnce = 10;
|
||||
|
||||
@@ -86,7 +86,7 @@ namespace Emby.Server.Implementations.Data
|
||||
}
|
||||
}
|
||||
|
||||
private void ImportUserIds(SqliteConnection db, IEnumerable<User> users)
|
||||
private void ImportUserIds(ManagedConnection db, IEnumerable<User> users)
|
||||
{
|
||||
var userIdsWithUserData = GetAllUserIdsWithUserData(db);
|
||||
|
||||
@@ -107,7 +107,7 @@ namespace Emby.Server.Implementations.Data
|
||||
}
|
||||
}
|
||||
|
||||
private List<Guid> GetAllUserIdsWithUserData(SqliteConnection db)
|
||||
private List<Guid> GetAllUserIdsWithUserData(ManagedConnection db)
|
||||
{
|
||||
var list = new List<Guid>();
|
||||
|
||||
@@ -176,7 +176,7 @@ namespace Emby.Server.Implementations.Data
|
||||
}
|
||||
}
|
||||
|
||||
private static void SaveUserData(SqliteConnection db, long internalUserId, string key, UserItemData userData)
|
||||
private static void SaveUserData(ManagedConnection 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)"))
|
||||
{
|
||||
@@ -267,7 +267,7 @@ namespace Emby.Server.Implementations.Data
|
||||
|
||||
ArgumentException.ThrowIfNullOrEmpty(key);
|
||||
|
||||
using (var connection = GetConnection())
|
||||
using (var connection = GetConnection(true))
|
||||
{
|
||||
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);
|
||||
|
||||
if (info.Exists &&
|
||||
((info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) != isHidden)
|
||||
(info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden != isHidden)
|
||||
{
|
||||
if (isHidden)
|
||||
{
|
||||
@@ -417,8 +417,8 @@ namespace Emby.Server.Implementations.IO
|
||||
return;
|
||||
}
|
||||
|
||||
if (((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) == readOnly
|
||||
&& ((info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) == isHidden)
|
||||
if ((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly == readOnly
|
||||
&& (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden == isHidden)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1029,7 +1029,7 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false)
|
||||
public async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false)
|
||||
{
|
||||
await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -1884,7 +1884,7 @@ namespace Emby.Server.Implementations.Library
|
||||
try
|
||||
{
|
||||
var index = item.GetImageIndex(img);
|
||||
image = await ConvertImageToLocal(item, img, index, removeOnFailure: true).ConfigureAwait(false);
|
||||
image = await ConvertImageToLocal(item, img, index, true).ConfigureAwait(false);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
IndexNumber = seasonParserResult.SeasonNumber,
|
||||
SeriesId = series.Id,
|
||||
SeriesName = series.Name,
|
||||
Path = seasonParserResult.IsSeasonFolder ? path : args.Parent.Path
|
||||
Path = seasonParserResult.IsSeasonFolder ? path : null
|
||||
};
|
||||
|
||||
if (!season.IndexNumber.HasValue || !seasonParserResult.IsSeasonFolder)
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
{
|
||||
"Albums": "аальбомқәа"
|
||||
}
|
||||
{}
|
||||
|
||||
@@ -127,7 +127,5 @@
|
||||
"TaskRefreshTrickplayImages": "Стварыце выявы Trickplay",
|
||||
"TaskRefreshTrickplayImagesDescription": "Стварае прагляд відэаролікаў для Trickplay у падключаных бібліятэках.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Ачысціце калекцыі і спісы прайгравання",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Выдаляе элементы з калекцый і спісаў прайгравання, якія больш не існуюць.",
|
||||
"TaskAudioNormalizationDescription": "Сканіруе файлы на прадмет нармалізацыі гуку.",
|
||||
"TaskAudioNormalization": "Нармалізацыя гуку"
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Выдаляе элементы з калекцый і спісаў прайгравання, якія больш не існуюць."
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"HeaderFavoriteEpisodes": "Oblíbené epizody",
|
||||
"HeaderFavoriteShows": "Oblíbené seriály",
|
||||
"HeaderFavoriteSongs": "Oblíbená hudba",
|
||||
"HeaderLiveTV": "TV vysílání",
|
||||
"HeaderLiveTV": "Živý přenos",
|
||||
"HeaderNextUp": "Další díly",
|
||||
"HeaderRecordingGroups": "Skupiny nahrávek",
|
||||
"HomeVideos": "Domácí videa",
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"Genres": "Genrer",
|
||||
"HeaderAlbumArtists": "Albumkunstnere",
|
||||
"HeaderContinueWatching": "Fortsæt afspilning",
|
||||
"HeaderFavoriteAlbums": "Favoritalbum",
|
||||
"HeaderFavoriteAlbums": "Favoritalbummer",
|
||||
"HeaderFavoriteArtists": "Favoritkunstnere",
|
||||
"HeaderFavoriteEpisodes": "Yndlingsafsnit",
|
||||
"HeaderFavoriteShows": "Yndlingsserier",
|
||||
@@ -87,21 +87,21 @@
|
||||
"UserOnlineFromDevice": "{0} er online fra {1}",
|
||||
"UserPasswordChangedWithName": "Adgangskode er ændret for brugeren {0}",
|
||||
"UserPolicyUpdatedWithName": "Brugerpolitikken er blevet opdateret for {0}",
|
||||
"UserStartedPlayingItemWithValues": "{0} har påbegyndt afspilning af {1} på {2}",
|
||||
"UserStartedPlayingItemWithValues": "{0} har påbegyndt afspilning af {1}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} har afsluttet afspilning af {1} på {2}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek",
|
||||
"ValueSpecialEpisodeName": "Special - {0}",
|
||||
"VersionNumber": "Version {0}",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata-konfigurationen.",
|
||||
"TaskDownloadMissingSubtitles": "Hent manglende undertekster",
|
||||
"TaskUpdatePluginsDescription": "Henter og installerer opdateringer for plugins, som er konfigurerede til at blive opdateret automatisk.",
|
||||
"TaskUpdatePluginsDescription": "Henter og installerer opdateringer for plugins, som er indstillet til at blive opdateret automatisk.",
|
||||
"TaskUpdatePlugins": "Opdater Plugins",
|
||||
"TaskCleanLogsDescription": "Sletter log-filer som er mere end {0} dage gamle.",
|
||||
"TaskCleanLogs": "Ryd Log-mappe",
|
||||
"TaskRefreshLibraryDescription": "Scanner dit mediebibliotek for nye filer og opdateret metadata.",
|
||||
"TaskRefreshLibrary": "Scan Mediebibliotek",
|
||||
"TaskCleanCacheDescription": "Sletter cache-filer som systemet ikke længere bruger.",
|
||||
"TaskCleanCache": "Ryd cache-mappe",
|
||||
"TaskCleanCache": "Ryd Cache-mappe",
|
||||
"TasksChannelsCategory": "Internetkanaler",
|
||||
"TasksApplicationCategory": "Applikation",
|
||||
"TasksLibraryCategory": "Bibliotek",
|
||||
@@ -128,7 +128,5 @@
|
||||
"TaskRefreshTrickplayImages": "Generér Trickplay Billeder",
|
||||
"TaskRefreshTrickplayImagesDescription": "Laver trickplay forhåndsvisninger for videoer i aktiverede biblioteker.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Ryd op i samlinger og afspilningslister",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Fjerner elementer fra samlinger og afspilningslister der ikke eksisterer længere.",
|
||||
"TaskAudioNormalizationDescription": "Skanner filer for data vedrørende audio-normalisering.",
|
||||
"TaskAudioNormalization": "Audio-normalisering"
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Fjerner enheder fra samlinger og afspilningslister der ikke eksisterer længere."
|
||||
}
|
||||
|
||||
@@ -126,9 +126,5 @@
|
||||
"External": "Εξωτερικό",
|
||||
"HearingImpaired": "Με προβλήματα ακοής",
|
||||
"TaskRefreshTrickplayImages": "Δημιουργήστε εικόνες Trickplay",
|
||||
"TaskRefreshTrickplayImagesDescription": "Δημιουργεί προεπισκοπήσεις trickplay για βίντεο σε ενεργοποιημένες βιβλιοθήκες.",
|
||||
"TaskAudioNormalization": "Ομοιομορφία ήχου",
|
||||
"TaskAudioNormalizationDescription": "Ανίχνευση αρχείων για δεδομένα ομοιομορφίας ήχου.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Καθαρισμός συλλογών και λιστών αναπαραγωγής",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Αφαιρούνται στοιχεία από τις συλλογές και τις λίστες αναπαραγωγής που δεν υπάρχουν πλέον."
|
||||
"TaskRefreshTrickplayImagesDescription": "Δημιουργεί προεπισκοπήσεις trickplay για βίντεο σε ενεργοποιημένες βιβλιοθήκες."
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"Collections": "Colecciones",
|
||||
"DeviceOfflineWithName": "{0} se ha desconectado",
|
||||
"DeviceOnlineWithName": "{0} está conectado",
|
||||
"FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión de {0}",
|
||||
"FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión desde {0}",
|
||||
"Favorites": "Favoritos",
|
||||
"Folders": "Carpetas",
|
||||
"Genres": "Géneros",
|
||||
@@ -124,11 +124,5 @@
|
||||
"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",
|
||||
"External": "Externo",
|
||||
"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."
|
||||
"HearingImpaired": "Discapacidad Auditiva"
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"Collections": "Colecciones",
|
||||
"DeviceOfflineWithName": "{0} se ha desconectado",
|
||||
"DeviceOnlineWithName": "{0} está conectado",
|
||||
"FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión de {0}",
|
||||
"FailedLoginAttemptWithUserName": "Error al intentar iniciar sesión desde {0}",
|
||||
"Favorites": "Favoritos",
|
||||
"Folders": "Carpetas",
|
||||
"Genres": "Géneros",
|
||||
|
||||
@@ -12,118 +12,14 @@
|
||||
"Application": "Aplicación",
|
||||
"AppDeviceValues": "App: {0}, Dispositivo: {1}",
|
||||
"HeaderContinueWatching": "Continuar Viendo",
|
||||
"HeaderAlbumArtists": "Artistas del álbum",
|
||||
"HeaderAlbumArtists": "Artistas del Álbum",
|
||||
"Genres": "Géneros",
|
||||
"Folders": "Carpetas",
|
||||
"Favorites": "Favoritos",
|
||||
"FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido desde {0}",
|
||||
"FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido de {0}",
|
||||
"HeaderFavoriteSongs": "Canciones Favoritas",
|
||||
"HeaderFavoriteEpisodes": "Episodios Favoritos",
|
||||
"HeaderFavoriteArtists": "Artistas Favoritos",
|
||||
"External": "Externo",
|
||||
"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."
|
||||
"Default": "Predeterminado"
|
||||
}
|
||||
|
||||
@@ -125,7 +125,5 @@
|
||||
"TaskKeyframeExtractorDescription": "Eraldab videofailidest võtmekaadreid, et luua täpsemaid HLS-i esitusloendeid. See ülesanne võib kesta pikka aega.",
|
||||
"TaskKeyframeExtractor": "Võtmekaadri ekstraktor",
|
||||
"TaskRefreshTrickplayImages": "Loo eelvaate pildid",
|
||||
"TaskRefreshTrickplayImagesDescription": "Loob eelvaated videotele, kus lubatud.",
|
||||
"TaskAudioNormalization": "Heli Normaliseerimine",
|
||||
"TaskAudioNormalizationDescription": "Skaneerib faile heli normaliseerimise andmete jaoks."
|
||||
"TaskRefreshTrickplayImagesDescription": "Loob eelvaated videotele, kus lubatud."
|
||||
}
|
||||
|
||||
@@ -127,7 +127,5 @@
|
||||
"TaskRefreshTrickplayImages": "Luo Trickplay-kuvat",
|
||||
"TaskRefreshTrickplayImagesDescription": "Luo Trickplay-esikatselut käytössä olevien kirjastojen videoista.",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Poistaa kohteet kokoelmista ja soittolistoista joita ei ole enää olemassa.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Puhdista kokoelmat ja soittolistat",
|
||||
"TaskAudioNormalization": "Äänenvoimakkuuden normalisointi",
|
||||
"TaskAudioNormalizationDescription": "Etsii tiedostoista äänenvoimakkuuden normalisointitietoja."
|
||||
"TaskCleanCollectionsAndPlaylists": "Puhdista kokoelmat ja soittolistat"
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"Collections": "Collections",
|
||||
"DeviceOfflineWithName": "{0} s'est déconnecté",
|
||||
"DeviceOnlineWithName": "{0} est connecté",
|
||||
"FailedLoginAttemptWithUserName": "Tentative de connexion échouée par {0}",
|
||||
"FailedLoginAttemptWithUserName": "Tentative de connexion échoué par {0}",
|
||||
"Favorites": "Favoris",
|
||||
"Folders": "Dossiers",
|
||||
"Genres": "Genres",
|
||||
@@ -39,7 +39,7 @@
|
||||
"MixedContent": "Contenu mixte",
|
||||
"Movies": "Films",
|
||||
"Music": "Musique",
|
||||
"MusicVideos": "Vidéoclips",
|
||||
"MusicVideos": "Vidéos musicales",
|
||||
"NameInstallFailed": "échec d'installation de {0}",
|
||||
"NameSeasonNumber": "Saison {0}",
|
||||
"NameSeasonUnknown": "Saison Inconnue",
|
||||
@@ -128,7 +128,5 @@
|
||||
"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.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Nettoyer les collections et les 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."
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Supprimer les liens inexistants des collections et des listes de lecture"
|
||||
}
|
||||
|
||||
@@ -126,9 +126,5 @@
|
||||
"External": "חיצוני",
|
||||
"HearingImpaired": "לקוי שמיעה",
|
||||
"TaskRefreshTrickplayImages": "יצירת תמונות המחשה",
|
||||
"TaskRefreshTrickplayImagesDescription": "יוצר תמונות המחשה לסרטונים שפעילים בספריות.",
|
||||
"TaskAudioNormalization": "נרמול שמע",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "מנקה פריטים לא קיימים מאוספים ורשימות השמעה.",
|
||||
"TaskAudioNormalizationDescription": "מחפש קבצי נורמליזציה של שמע.",
|
||||
"TaskCleanCollectionsAndPlaylists": "מנקה אוספים ורשימות השמעה"
|
||||
"TaskRefreshTrickplayImagesDescription": "יוצר תמונות המחשה לסרטונים שפעילים בספריות."
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
"Movies": "Film",
|
||||
"MessageServerConfigurationUpdated": "Konfigurasi server telah diperbarui",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Bagian konfigurasi server {0} telah diperbarui",
|
||||
"FailedLoginAttemptWithUserName": "Gagal upaya login dari {0}",
|
||||
"FailedLoginAttemptWithUserName": "Gagal melakukan login dari {0}",
|
||||
"CameraImageUploadedFrom": "Sebuah gambar kamera baru telah diunggah dari {0}",
|
||||
"DeviceOfflineWithName": "{0} telah terputus",
|
||||
"DeviceOnlineWithName": "{0} telah terhubung",
|
||||
@@ -125,9 +125,5 @@
|
||||
"External": "Luar",
|
||||
"HearingImpaired": "Gangguan Pendengaran",
|
||||
"TaskRefreshTrickplayImages": "Hasilkan Gambar Trickplay",
|
||||
"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."
|
||||
"TaskRefreshTrickplayImagesDescription": "Buat pratinjau trickplay untuk video di perpustakaan yang diaktifkan."
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
"UserDeletedWithName": "L'utente {0} è stato rimosso",
|
||||
"UserDownloadingItemWithValues": "{0} sta scaricando {1}",
|
||||
"UserLockedOutWithName": "L'utente {0} è stato bloccato",
|
||||
"UserOfflineFromDevice": "{0} si è disconnesso da {1}",
|
||||
"UserOfflineFromDevice": "{0} si è disconnesso su {1}",
|
||||
"UserOnlineFromDevice": "{0} è online su {1}",
|
||||
"UserPasswordChangedWithName": "La password è stata cambiata per l'utente {0}",
|
||||
"UserPolicyUpdatedWithName": "La policy dell'utente è stata aggiornata per {0}",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"Collections": "Collecties",
|
||||
"DeviceOfflineWithName": "Verbinding met {0} is verbroken",
|
||||
"DeviceOnlineWithName": "{0} is verbonden",
|
||||
"FailedLoginAttemptWithUserName": "Mislukte aanmeldpoging van {0}",
|
||||
"FailedLoginAttemptWithUserName": "Mislukte inlogpoging van {0}",
|
||||
"Favorites": "Favorieten",
|
||||
"Folders": "Mappen",
|
||||
"Genres": "Genres",
|
||||
@@ -124,7 +124,7 @@
|
||||
"TaskKeyframeExtractorDescription": "Haalt keyframes uit videobestanden om preciezere HLS-afspeellijsten te maken. Deze taak kan lang duren.",
|
||||
"TaskKeyframeExtractor": "Keyframes uitpakken",
|
||||
"External": "Extern",
|
||||
"HearingImpaired": "Slechthorenden",
|
||||
"HearingImpaired": "Slechthorend",
|
||||
"TaskRefreshTrickplayImages": "Trickplay-afbeeldingen genereren",
|
||||
"TaskRefreshTrickplayImagesDescription": "Creëert trickplay-voorvertoningen voor video's in bibliotheken waarvoor dit is ingeschakeld.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Collecties en afspeellijsten opruimen",
|
||||
|
||||
@@ -118,6 +118,5 @@
|
||||
"Undefined": "Udefinert",
|
||||
"Forced": "Tvungen",
|
||||
"Default": "Standard",
|
||||
"External": "Ekstern",
|
||||
"HearingImpaired": "Nedsett høyrsel"
|
||||
"External": "Ekstern"
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"Collections": "Kolekcje",
|
||||
"DeviceOfflineWithName": "{0} został rozłączony",
|
||||
"DeviceOnlineWithName": "{0} połączył się",
|
||||
"FailedLoginAttemptWithUserName": "Nieudana próba logowania przez {0}",
|
||||
"FailedLoginAttemptWithUserName": "Próba logowania przez {0} zakończona niepowodzeniem",
|
||||
"Favorites": "Ulubione",
|
||||
"Folders": "Foldery",
|
||||
"Genres": "Gatunki",
|
||||
@@ -98,8 +98,8 @@
|
||||
"TaskRefreshChannels": "Odśwież kanały",
|
||||
"TaskCleanTranscodeDescription": "Usuwa transkodowane pliki starsze niż 1 dzień.",
|
||||
"TaskCleanTranscode": "Wyczyść folder transkodowania",
|
||||
"TaskUpdatePluginsDescription": "Pobiera i instaluje aktualizacje wtyczek, które są skonfigurowane do automatycznej aktualizacji.",
|
||||
"TaskUpdatePlugins": "Aktualizuj wtyczki",
|
||||
"TaskUpdatePluginsDescription": "Pobiera i instaluje aktualizacje dla pluginów, które są skonfigurowane do automatycznej aktualizacji.",
|
||||
"TaskUpdatePlugins": "Aktualizuj pluginy",
|
||||
"TaskRefreshPeopleDescription": "Odświeża metadane o aktorów i reżyserów w Twojej bibliotece mediów.",
|
||||
"TaskRefreshPeople": "Odśwież obsadę",
|
||||
"TaskCleanLogsDescription": "Kasuje pliki logów starsze niż {0} dni.",
|
||||
|
||||
@@ -130,5 +130,5 @@
|
||||
"TaskCleanCollectionsAndPlaylists": "Limpe coleções e playlists",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e playlists que não existem mais.",
|
||||
"TaskAudioNormalization": "Normalização de áudio",
|
||||
"TaskAudioNormalizationDescription": "Examina os ficheiros em busca de dados de normalização de áudio."
|
||||
"TaskAudioNormalizationDescription": "Verifica arquivos em busca de dados de normalização de áudio."
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"Collections": "Коллекции",
|
||||
"DeviceOfflineWithName": "{0} - отключено",
|
||||
"DeviceOnlineWithName": "{0} - подключено",
|
||||
"FailedLoginAttemptWithUserName": "Неудачная попытка входа с {0}",
|
||||
"FailedLoginAttemptWithUserName": "{0} - попытка входа неудачна",
|
||||
"Favorites": "Избранное",
|
||||
"Folders": "Папки",
|
||||
"Genres": "Жанры",
|
||||
@@ -128,7 +128,5 @@
|
||||
"TaskRefreshTrickplayImages": "Сгенерировать изображения для Trickplay",
|
||||
"TaskRefreshTrickplayImagesDescription": "Создает предпросмотры для Trickplay для видео в библиотеках, где эта функция включена.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Очистка коллекций и списков воспроизведения",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Удаляет элементы из коллекций и списков воспроизведения, которые больше не существуют.",
|
||||
"TaskAudioNormalization": "Нормализация звука",
|
||||
"TaskAudioNormalizationDescription": "Сканирует файлы на наличие данных о нормализации звука."
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Удаляет элементы из коллекций и списков воспроизведения, которые больше не существуют."
|
||||
}
|
||||
|
||||
@@ -127,8 +127,5 @@
|
||||
"HearingImpaired": "Hörselskadad",
|
||||
"TaskRefreshTrickplayImages": "Generera Trickplay-bilder",
|
||||
"TaskRefreshTrickplayImagesDescription": "Skapar trickplay-förhandsvisningar för videor i aktiverade bibliotek.",
|
||||
"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."
|
||||
"TaskCleanCollectionsAndPlaylists": "Rensa samlingar och spellistor"
|
||||
}
|
||||
|
||||
@@ -125,9 +125,5 @@
|
||||
"External": "வெளி",
|
||||
"HearingImpaired": "செவித்திறன் குறைபாடுடையவர்",
|
||||
"TaskRefreshTrickplayImages": "முன்னோட்ட படங்களை உருவாக்கு",
|
||||
"TaskRefreshTrickplayImagesDescription": "செயல்பாட்டில் உள்ள தொகுப்புகளுக்கு முன்னோட்ட படங்களை உருவாக்கும்.",
|
||||
"TaskCleanCollectionsAndPlaylists": "சேகரிப்புகள் மற்றும் பிளேலிஸ்ட்களை சுத்தம் செய்யவும்",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "சேகரிப்புகள் மற்றும் பிளேலிஸ்ட்களில் இருந்து உருப்படிகளை நீக்குகிறது.",
|
||||
"TaskAudioNormalization": "ஆடியோ இயல்பாக்கம்",
|
||||
"TaskAudioNormalizationDescription": "ஆடியோ இயல்பாக்குதல் தரவுக்காக கோப்புகளை ஸ்கேன் செய்கிறது."
|
||||
"TaskRefreshTrickplayImagesDescription": "செயல்பாட்டில் உள்ள தொகுப்புகளுக்கு முன்னோட்ட படங்களை உருவாக்கும்."
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"Collections": "Koleksiyonlar",
|
||||
"DeviceOfflineWithName": "{0} bağlantısı kesildi",
|
||||
"DeviceOnlineWithName": "{0} bağlı",
|
||||
"FailedLoginAttemptWithUserName": "{0} kullanıcısının başarısız oturum açma girişimi",
|
||||
"FailedLoginAttemptWithUserName": "{0} kullanıcısının giriş denemesi başarısız oldu",
|
||||
"Favorites": "Favoriler",
|
||||
"Folders": "Klasörler",
|
||||
"Genres": "Türler",
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
"HeaderFavoriteEpisodes": "Tập Phim Yêu Thích",
|
||||
"HeaderFavoriteArtists": "Nghệ Sĩ Yêu Thích",
|
||||
"HeaderFavoriteAlbums": "Album Ưa Thích",
|
||||
"FailedLoginAttemptWithUserName": "Nỗ lực đăng nhập không thành công từ {0}",
|
||||
"FailedLoginAttemptWithUserName": "Đăng nhập không thành công thử từ {0}",
|
||||
"DeviceOnlineWithName": "{0} đã kết nối",
|
||||
"DeviceOfflineWithName": "{0} đã ngắt kết nối",
|
||||
"ChapterNameValue": "Phân Cảnh {0}",
|
||||
@@ -127,7 +127,5 @@
|
||||
"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.",
|
||||
"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.",
|
||||
"TaskAudioNormalization": "Chuẩn Hóa Âm Thanh",
|
||||
"TaskAudioNormalizationDescription": "Quét tập tin để tìm dữ liệu chuẩn hóa âm thanh."
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"Collections": "合集",
|
||||
"DeviceOfflineWithName": "{0} 已断开",
|
||||
"DeviceOnlineWithName": "{0} 已连接",
|
||||
"FailedLoginAttemptWithUserName": "来自 {0} 的登录尝试失败",
|
||||
"FailedLoginAttemptWithUserName": "从 {0} 尝试登录失败",
|
||||
"Favorites": "我的最爱",
|
||||
"Folders": "文件夹",
|
||||
"Genres": "类型",
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
Exempt,0
|
||||
G,0
|
||||
7+,7
|
||||
PG,15
|
||||
M,15
|
||||
MA,15
|
||||
MA15+,15
|
||||
MA 15+,15
|
||||
PG,16
|
||||
16+,16
|
||||
R,18
|
||||
R18+,18
|
||||
|
||||
|
@@ -170,8 +170,13 @@ namespace Emby.Server.Implementations.Playlists
|
||||
private List<Playlist> GetUserPlaylists(Guid userId)
|
||||
{
|
||||
var user = _userManager.GetUserById(userId);
|
||||
var playlistsFolder = GetPlaylistsFolder(userId);
|
||||
if (playlistsFolder is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return GetPlaylistsFolder(userId).GetChildren(user, true).OfType<Playlist>().ToList();
|
||||
return playlistsFolder.GetChildren(user, true).OfType<Playlist>().ToList();
|
||||
}
|
||||
|
||||
private static string GetTargetPath(string path)
|
||||
@@ -184,11 +189,11 @@ namespace Emby.Server.Implementations.Playlists
|
||||
return path;
|
||||
}
|
||||
|
||||
private IReadOnlyList<BaseItem> GetPlaylistItems(IEnumerable<Guid> itemIds, MediaType playlistMediaType, User user, DtoOptions options)
|
||||
private IReadOnlyList<BaseItem> GetPlaylistItems(IEnumerable<Guid> itemIds, User user, DtoOptions options)
|
||||
{
|
||||
var items = itemIds.Select(_libraryManager.GetItemById).Where(i => i is not null);
|
||||
|
||||
return Playlist.GetPlaylistItems(playlistMediaType, items, user, options);
|
||||
return Playlist.GetPlaylistItems(items, user, options);
|
||||
}
|
||||
|
||||
public Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId)
|
||||
@@ -208,7 +213,7 @@ namespace Emby.Server.Implementations.Playlists
|
||||
?? throw new ArgumentException("No Playlist exists with Id " + playlistId);
|
||||
|
||||
// Retrieve all the items to be added to the playlist
|
||||
var newItems = GetPlaylistItems(newItemIds, playlist.MediaType, user, options)
|
||||
var newItems = GetPlaylistItems(newItemIds, user, options)
|
||||
.Where(i => i.SupportsAddingToPlaylist);
|
||||
|
||||
// Filter out duplicate items, if necessary
|
||||
|
||||
@@ -106,13 +106,20 @@ public partial class AudioNormalizationTask : IScheduledTask
|
||||
continue;
|
||||
}
|
||||
|
||||
var tempFile = Path.Join(_configurationManager.GetTranscodePath(), Guid.NewGuid() + ".concat");
|
||||
_logger.LogInformation("Calculating LUFS for album: {Album} with id: {Id}", a.Name, a.Id);
|
||||
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)));
|
||||
await File.WriteAllLinesAsync(tempFile, inputLines, cancellationToken).ConfigureAwait(false);
|
||||
a.LUFS = await CalculateLUFSAsync(
|
||||
string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
File.Delete(tempFile);
|
||||
try
|
||||
{
|
||||
a.LUFS = await CalculateLUFSAsync(
|
||||
string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(tempFile);
|
||||
}
|
||||
}
|
||||
|
||||
_itemRepository.SaveItems(albums, cancellationToken);
|
||||
|
||||
@@ -127,15 +127,8 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask
|
||||
{
|
||||
_logger.LogDebug("Updating {FolderName}", folder.Name);
|
||||
folder.LinkedChildren = folder.LinkedChildren.Except(itemsToRemove).ToArray();
|
||||
_providerManager.SaveMetadataAsync(folder, ItemUpdateType.MetadataEdit);
|
||||
folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken);
|
||||
|
||||
_providerManager.QueueRefresh(
|
||||
folder.Id,
|
||||
new MetadataRefreshOptions(new DirectoryService(_fileSystem))
|
||||
{
|
||||
ForceSave = true
|
||||
},
|
||||
RefreshPriority.High);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
@@ -133,53 +134,14 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
DeleteFile(file.FullName);
|
||||
FileSystemHelper.DeleteFile(_fileSystem, file.FullName, _logger);
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
DeleteEmptyFolders(directory);
|
||||
FileSystemHelper.DeleteEmptyFolders(_fileSystem, directory, _logger);
|
||||
|
||||
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.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
@@ -113,53 +113,14 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
DeleteFile(file.FullName);
|
||||
FileSystemHelper.DeleteFile(_fileSystem, file.FullName, _logger);
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
DeleteEmptyFolders(directory);
|
||||
FileSystemHelper.DeleteEmptyFolders(_fileSystem, directory, _logger);
|
||||
|
||||
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);
|
||||
|
||||
var activityDate = DateTime.UtcNow;
|
||||
var session = await GetSessionInfo(appName, appVersion, deviceId, deviceName, remoteEndPoint, user).ConfigureAwait(false);
|
||||
var session = GetSessionInfo(appName, appVersion, deviceId, deviceName, remoteEndPoint, user);
|
||||
var lastActivityDate = session.LastActivityDate;
|
||||
session.LastActivityDate = activityDate;
|
||||
|
||||
@@ -435,7 +435,7 @@ namespace Emby.Server.Implementations.Session
|
||||
/// <param name="remoteEndPoint">The remote end point.</param>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <returns>SessionInfo.</returns>
|
||||
private async Task<SessionInfo> GetSessionInfo(
|
||||
private SessionInfo GetSessionInfo(
|
||||
string appName,
|
||||
string appVersion,
|
||||
string deviceId,
|
||||
@@ -453,7 +453,7 @@ namespace Emby.Server.Implementations.Session
|
||||
|
||||
if (!_activeConnections.TryGetValue(key, out var sessionInfo))
|
||||
{
|
||||
sessionInfo = await CreateSession(key, appName, appVersion, deviceId, deviceName, remoteEndPoint, user).ConfigureAwait(false);
|
||||
sessionInfo = CreateSession(key, appName, appVersion, deviceId, deviceName, remoteEndPoint, user);
|
||||
_activeConnections[key] = sessionInfo;
|
||||
}
|
||||
|
||||
@@ -478,7 +478,7 @@ namespace Emby.Server.Implementations.Session
|
||||
return sessionInfo;
|
||||
}
|
||||
|
||||
private async Task<SessionInfo> CreateSession(
|
||||
private SessionInfo CreateSession(
|
||||
string key,
|
||||
string appName,
|
||||
string appVersion,
|
||||
@@ -508,7 +508,7 @@ namespace Emby.Server.Implementations.Session
|
||||
deviceName = "Network Device";
|
||||
}
|
||||
|
||||
var deviceOptions = await _deviceManager.GetDeviceOptions(deviceId).ConfigureAwait(false);
|
||||
var deviceOptions = _deviceManager.GetDeviceOptions(deviceId);
|
||||
if (string.IsNullOrEmpty(deviceOptions.CustomName))
|
||||
{
|
||||
sessionInfo.DeviceName = deviceName;
|
||||
@@ -1297,7 +1297,7 @@ namespace Emby.Server.Implementations.Session
|
||||
return new[] { item };
|
||||
}
|
||||
|
||||
private IEnumerable<BaseItem> TranslateItemForInstantMix(Guid id, User user)
|
||||
private List<BaseItem> TranslateItemForInstantMix(Guid id, User user)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(id);
|
||||
|
||||
@@ -1307,7 +1307,7 @@ namespace Emby.Server.Implementations.Session
|
||||
return new List<BaseItem>();
|
||||
}
|
||||
|
||||
return _musicManager.GetInstantMixFromItem(item, user, new DtoOptions(false) { EnableImages = false });
|
||||
return _musicManager.GetInstantMixFromItem(item, user, new DtoOptions(false) { EnableImages = false }).ToList();
|
||||
}
|
||||
|
||||
/// <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.
|
||||
ArgumentException.ThrowIfNullOrEmpty(deviceId);
|
||||
|
||||
var existing = (await _deviceManager.GetDevices(
|
||||
var existing = _deviceManager.GetDevices(
|
||||
new DeviceQuery
|
||||
{
|
||||
DeviceId = deviceId,
|
||||
UserId = user.Id
|
||||
}).ConfigureAwait(false)).Items;
|
||||
}).Items;
|
||||
|
||||
foreach (var auth in existing)
|
||||
{
|
||||
@@ -1553,12 +1553,12 @@ namespace Emby.Server.Implementations.Session
|
||||
|
||||
ArgumentException.ThrowIfNullOrEmpty(accessToken);
|
||||
|
||||
var existing = (await _deviceManager.GetDevices(
|
||||
var existing = _deviceManager.GetDevices(
|
||||
new DeviceQuery
|
||||
{
|
||||
Limit = 1,
|
||||
AccessToken = accessToken
|
||||
}).ConfigureAwait(false)).Items;
|
||||
}).Items;
|
||||
|
||||
if (existing.Count > 0)
|
||||
{
|
||||
@@ -1597,10 +1597,10 @@ namespace Emby.Server.Implementations.Session
|
||||
{
|
||||
CheckDisposed();
|
||||
|
||||
var existing = await _deviceManager.GetDevices(new DeviceQuery
|
||||
var existing = _deviceManager.GetDevices(new DeviceQuery
|
||||
{
|
||||
UserId = userId
|
||||
}).ConfigureAwait(false);
|
||||
});
|
||||
|
||||
foreach (var info in existing.Items)
|
||||
{
|
||||
@@ -1787,11 +1787,11 @@ namespace Emby.Server.Implementations.Session
|
||||
/// <inheritdoc />
|
||||
public async Task<SessionInfo> GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint)
|
||||
{
|
||||
var items = (await _deviceManager.GetDevices(new DeviceQuery
|
||||
var items = _deviceManager.GetDevices(new DeviceQuery
|
||||
{
|
||||
AccessToken = token,
|
||||
Limit = 1
|
||||
}).ConfigureAwait(false)).Items;
|
||||
}).Items;
|
||||
|
||||
if (items.Count == 0)
|
||||
{
|
||||
|
||||
@@ -47,10 +47,10 @@ public class DevicesController : BaseJellyfinApiController
|
||||
/// <returns>An <see cref="OkResult"/> containing the list of devices.</returns>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<QueryResult<DeviceInfo>>> GetDevices([FromQuery] Guid? userId)
|
||||
public ActionResult<QueryResult<DeviceInfo>> GetDevices([FromQuery] Guid? userId)
|
||||
{
|
||||
userId = RequestHelpers.GetUserId(User, userId);
|
||||
return await _deviceManager.GetDevicesForUser(userId).ConfigureAwait(false);
|
||||
return _deviceManager.GetDevicesForUser(userId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -63,9 +63,9 @@ public class DevicesController : BaseJellyfinApiController
|
||||
[HttpGet("Info")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<DeviceInfo>> GetDeviceInfo([FromQuery, Required] string id)
|
||||
public ActionResult<DeviceInfo> GetDeviceInfo([FromQuery, Required] string id)
|
||||
{
|
||||
var deviceInfo = await _deviceManager.GetDevice(id).ConfigureAwait(false);
|
||||
var deviceInfo = _deviceManager.GetDevice(id);
|
||||
if (deviceInfo is null)
|
||||
{
|
||||
return NotFound();
|
||||
@@ -84,9 +84,9 @@ public class DevicesController : BaseJellyfinApiController
|
||||
[HttpGet("Options")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<DeviceOptions>> GetDeviceOptions([FromQuery, Required] string id)
|
||||
public ActionResult<DeviceOptions> GetDeviceOptions([FromQuery, Required] string id)
|
||||
{
|
||||
var deviceInfo = await _deviceManager.GetDeviceOptions(id).ConfigureAwait(false);
|
||||
var deviceInfo = _deviceManager.GetDeviceOptions(id);
|
||||
if (deviceInfo is null)
|
||||
{
|
||||
return NotFound();
|
||||
@@ -124,13 +124,13 @@ public class DevicesController : BaseJellyfinApiController
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult> DeleteDevice([FromQuery, Required] string id)
|
||||
{
|
||||
var existingDevice = await _deviceManager.GetDevice(id).ConfigureAwait(false);
|
||||
var existingDevice = _deviceManager.GetDevice(id);
|
||||
if (existingDevice is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var sessions = await _deviceManager.GetDevices(new DeviceQuery { DeviceId = id }).ConfigureAwait(false);
|
||||
var sessions = _deviceManager.GetDevices(new DeviceQuery { DeviceId = id });
|
||||
|
||||
foreach (var session in sessions.Items)
|
||||
{
|
||||
|
||||
@@ -2089,6 +2089,8 @@ public class ImageController : BaseJellyfinApiController
|
||||
Response.Headers.Append(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateImageModified).TotalSeconds).ToString(CultureInfo.InvariantCulture));
|
||||
Response.Headers.Append(HeaderNames.Vary, HeaderNames.Accept);
|
||||
|
||||
Response.Headers.ContentDisposition = "attachment";
|
||||
|
||||
if (disableCaching)
|
||||
{
|
||||
Response.Headers.Append(HeaderNames.CacheControl, "no-cache, no-store, must-revalidate");
|
||||
|
||||
@@ -80,7 +80,8 @@ public class ItemRefreshController : BaseJellyfinApiController
|
||||
|| imageRefreshMode == MetadataRefreshMode.FullRefresh
|
||||
|| replaceAllImages
|
||||
|| replaceAllMetadata,
|
||||
IsAutomated = false
|
||||
IsAutomated = false,
|
||||
RemoveOldMetadata = replaceAllMetadata
|
||||
};
|
||||
|
||||
_providerManager.QueueRefresh(item.Id, refreshOptions, RefreshPriority.High);
|
||||
|
||||
@@ -180,7 +180,21 @@ public class LibraryStructureController : BaseJellyfinApiController
|
||||
// No need to start if scanning the library because it will handle it
|
||||
if (refreshLibrary)
|
||||
{
|
||||
await _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None).ConfigureAwait(false);
|
||||
await _libraryManager.ValidateTopLibraryFolders(CancellationToken.None, true).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
|
||||
{
|
||||
|
||||
@@ -233,6 +233,8 @@ public class PluginsController : BaseJellyfinApiController
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
Response.Headers.ContentDisposition = "attachment";
|
||||
|
||||
imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath);
|
||||
return PhysicalFile(imagePath, MimeTypes.GetMimeType(imagePath));
|
||||
}
|
||||
|
||||
@@ -95,6 +95,7 @@ public class TrickplayController : BaseJellyfinApiController
|
||||
var path = _trickplayManager.GetTrickplayTilePath(item, width, index);
|
||||
if (System.IO.File.Exists(path))
|
||||
{
|
||||
Response.Headers.ContentDisposition = "attachment";
|
||||
return PhysicalFile(path, MediaTypeNames.Image.Jpeg);
|
||||
}
|
||||
|
||||
|
||||
@@ -154,6 +154,11 @@ public static class StreamingHelpers
|
||||
// Some channels from HDHomerun will experience A/V sync issues
|
||||
streamingRequest.SegmentContainer = "ts";
|
||||
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);
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Data</PackageId>
|
||||
<VersionPrefix>10.10.0</VersionPrefix>
|
||||
<VersionPrefix>10.9.10</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -27,6 +27,8 @@ namespace Jellyfin.Server.Implementations.Devices
|
||||
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly ConcurrentDictionary<string, ClientCapabilities> _capabilitiesMap = new();
|
||||
private readonly ConcurrentDictionary<int, Device> _devices;
|
||||
private readonly ConcurrentDictionary<string, DeviceOptions> _deviceOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DeviceManager"/> class.
|
||||
@@ -37,6 +39,23 @@ namespace Jellyfin.Server.Implementations.Devices
|
||||
{
|
||||
_dbProvider = dbProvider;
|
||||
_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 />
|
||||
@@ -66,6 +85,8 @@ namespace Jellyfin.Server.Implementations.Devices
|
||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_deviceOptions[deviceId] = deviceOptions;
|
||||
|
||||
DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs<Tuple<string, DeviceOptions>>(new Tuple<string, DeviceOptions>(deviceId, deviceOptions)));
|
||||
}
|
||||
|
||||
@@ -76,25 +97,17 @@ namespace Jellyfin.Server.Implementations.Devices
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
{
|
||||
dbContext.Devices.Add(device);
|
||||
|
||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
_devices.TryAdd(device.Id, device);
|
||||
}
|
||||
|
||||
return device;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<DeviceOptions> GetDeviceOptions(string deviceId)
|
||||
public DeviceOptions GetDeviceOptions(string deviceId)
|
||||
{
|
||||
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);
|
||||
}
|
||||
_deviceOptions.TryGetValue(deviceId, out var deviceOptions);
|
||||
|
||||
return deviceOptions ?? new DeviceOptions(deviceId);
|
||||
}
|
||||
@@ -108,57 +121,43 @@ namespace Jellyfin.Server.Implementations.Devices
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<DeviceInfo?> GetDevice(string id)
|
||||
public DeviceInfo? GetDevice(string id)
|
||||
{
|
||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||
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 device = _devices.Values.Where(d => d.DeviceId == id).OrderByDescending(d => d.DateLastActivity).FirstOrDefault();
|
||||
_deviceOptions.TryGetValue(id, out var deviceOption);
|
||||
|
||||
var deviceInfo = device is null ? null : ToDeviceInfo(device.Device, device.Options);
|
||||
|
||||
return deviceInfo;
|
||||
}
|
||||
var deviceInfo = device is null ? null : ToDeviceInfo(device, deviceOption);
|
||||
return deviceInfo;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<QueryResult<Device>> GetDevices(DeviceQuery query)
|
||||
public QueryResult<Device> GetDevices(DeviceQuery query)
|
||||
{
|
||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
IEnumerable<Device> devices = _devices.Values
|
||||
.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)
|
||||
.OrderBy(d => d.Id)
|
||||
.ToList();
|
||||
var count = devices.Count();
|
||||
|
||||
if (query.Skip.HasValue)
|
||||
{
|
||||
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);
|
||||
|
||||
var count = await devices.CountAsync().ConfigureAwait(false);
|
||||
|
||||
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));
|
||||
devices = devices.Skip(query.Skip.Value);
|
||||
}
|
||||
|
||||
if (query.Limit.HasValue)
|
||||
{
|
||||
devices = devices.Take(query.Limit.Value);
|
||||
}
|
||||
|
||||
return new QueryResult<Device>(query.Skip, count, devices.ToList());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<QueryResult<DeviceInfo>> GetDeviceInfos(DeviceQuery query)
|
||||
public QueryResult<DeviceInfo> GetDeviceInfos(DeviceQuery query)
|
||||
{
|
||||
var devices = await GetDevices(query).ConfigureAwait(false);
|
||||
var devices = GetDevices(query);
|
||||
|
||||
return new QueryResult<DeviceInfo>(
|
||||
devices.StartIndex,
|
||||
@@ -167,38 +166,36 @@ namespace Jellyfin.Server.Implementations.Devices
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId)
|
||||
public QueryResult<DeviceInfo> GetDevicesForUser(Guid? userId)
|
||||
{
|
||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
IEnumerable<Device> devices = _devices.Values
|
||||
.OrderByDescending(d => d.DateLastActivity)
|
||||
.ThenBy(d => d.DeviceId);
|
||||
|
||||
if (!userId.IsNullOrEmpty())
|
||||
{
|
||||
var sessions = dbContext.Devices
|
||||
.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())
|
||||
var user = _userManager.GetUserById(userId.Value);
|
||||
if (user is null)
|
||||
{
|
||||
var user = _userManager.GetUserById(userId.Value);
|
||||
if (user is null)
|
||||
{
|
||||
throw new ResourceNotFoundException();
|
||||
}
|
||||
|
||||
sessions = sessions.Where(i => CanAccessDevice(user, i.Device.DeviceId));
|
||||
throw new ResourceNotFoundException();
|
||||
}
|
||||
|
||||
var array = await sessions.Select(device => ToDeviceInfo(device.Device, device.Options)).ToArrayAsync().ConfigureAwait(false);
|
||||
|
||||
return new QueryResult<DeviceInfo>(array);
|
||||
devices = devices.Where(i => CanAccessDevice(user, i.DeviceId));
|
||||
}
|
||||
|
||||
var array = devices.Select(device =>
|
||||
{
|
||||
_deviceOptions.TryGetValue(device.DeviceId, out var option);
|
||||
return ToDeviceInfo(device, option);
|
||||
}).ToArray();
|
||||
|
||||
return new QueryResult<DeviceInfo>(array);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteDevice(Device device)
|
||||
{
|
||||
_devices.TryRemove(device.Id, out _);
|
||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
{
|
||||
@@ -207,6 +204,19 @@ 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 />
|
||||
public bool CanAccessDevice(User user, string deviceId)
|
||||
{
|
||||
@@ -225,6 +235,11 @@ namespace Jellyfin.Server.Implementations.Devices
|
||||
private DeviceInfo ToDeviceInfo(Device authInfo, DeviceOptions? options = null)
|
||||
{
|
||||
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
|
||||
{
|
||||
@@ -232,7 +247,7 @@ namespace Jellyfin.Server.Implementations.Devices
|
||||
AppVersion = authInfo.AppVersion,
|
||||
Id = authInfo.DeviceId,
|
||||
LastUserId = authInfo.UserId,
|
||||
LastUserName = authInfo.User.Username,
|
||||
LastUserName = user.Username,
|
||||
Name = authInfo.DeviceName,
|
||||
DateLastActivity = authInfo.DateLastActivity,
|
||||
IconUrl = caps.IconUrl,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using EFCoreSecondLevelCacheInterceptor;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -16,28 +15,13 @@ public static class ServiceCollectionExtensions
|
||||
/// Adds the <see cref="IDbContextFactory{TContext}"/> interface to the service collection with second level caching enabled.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
public static IServiceCollection AddJellyfinDbContext(this IServiceCollection serviceCollection, bool disableSecondLevelCache)
|
||||
public static IServiceCollection AddJellyfinDbContext(this IServiceCollection serviceCollection)
|
||||
{
|
||||
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) =>
|
||||
{
|
||||
var applicationPaths = serviceProvider.GetRequiredService<IApplicationPaths>();
|
||||
var dbOpt = opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}");
|
||||
if (!disableSecondLevelCache)
|
||||
{
|
||||
dbOpt.AddInterceptors(serviceProvider.GetRequiredService<SecondLevelCacheInterceptor>());
|
||||
}
|
||||
opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}");
|
||||
});
|
||||
|
||||
return serviceCollection;
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AsyncKeyedLock" />
|
||||
<PackageReference Include="EFCoreSecondLevelCacheInterceptor" />
|
||||
<PackageReference Include="System.Linq.Async" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
|
||||
|
||||
@@ -4,7 +4,10 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Queries;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@@ -17,15 +20,18 @@ namespace Jellyfin.Server.Implementations.Security
|
||||
{
|
||||
private readonly IDbContextFactory<JellyfinDbContext> _jellyfinDbProvider;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IDeviceManager _deviceManager;
|
||||
private readonly IServerApplicationHost _serverApplicationHost;
|
||||
|
||||
public AuthorizationContext(
|
||||
IDbContextFactory<JellyfinDbContext> jellyfinDb,
|
||||
IUserManager userManager,
|
||||
IDeviceManager deviceManager,
|
||||
IServerApplicationHost serverApplicationHost)
|
||||
{
|
||||
_jellyfinDbProvider = jellyfinDb;
|
||||
_userManager = userManager;
|
||||
_deviceManager = deviceManager;
|
||||
_serverApplicationHost = serverApplicationHost;
|
||||
}
|
||||
|
||||
@@ -121,7 +127,11 @@ namespace Jellyfin.Server.Implementations.Security
|
||||
var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
{
|
||||
var device = await dbContext.Devices.FirstOrDefaultAsync(d => d.AccessToken == token).ConfigureAwait(false);
|
||||
var device = _deviceManager.GetDevices(
|
||||
new DeviceQuery
|
||||
{
|
||||
AccessToken = token
|
||||
}).Items.FirstOrDefault();
|
||||
|
||||
if (device is not null)
|
||||
{
|
||||
@@ -178,8 +188,7 @@ namespace Jellyfin.Server.Implementations.Security
|
||||
|
||||
if (updateToken)
|
||||
{
|
||||
dbContext.Devices.Update(device);
|
||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
await _deviceManager.UpdateDevice(device).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -130,7 +130,7 @@ public class TrickplayManager : ITrickplayManager
|
||||
var mediaPath = mediaSource.Path;
|
||||
if (!File.Exists(mediaPath))
|
||||
{
|
||||
_logger.LogWarning("Media not found at {Path} for item {ItemID}", mediaPath, video.Id);
|
||||
_logger.LogWarning("Media source {MediaSourceId} not found at {Path} for item {ItemID}", mediaSource.Id, mediaPath, video.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -60,10 +60,10 @@ public sealed class DeviceAccessHost : IHostedService
|
||||
|
||||
private async Task UpdateDeviceAccess(User user)
|
||||
{
|
||||
var existing = (await _deviceManager.GetDevices(new DeviceQuery
|
||||
var existing = _deviceManager.GetDevices(new DeviceQuery
|
||||
{
|
||||
UserId = user.Id
|
||||
}).ConfigureAwait(false)).Items;
|
||||
}).Items;
|
||||
|
||||
foreach (var device in existing)
|
||||
{
|
||||
|
||||
@@ -85,6 +85,6 @@ public static class WebHostBuilderExtensions
|
||||
logger.LogInformation("Kestrel listening to unix socket {SocketPath}", socketPath);
|
||||
}
|
||||
})
|
||||
.UseStartup(_ => new Startup(appHost, startupConfig));
|
||||
.UseStartup(_ => new Startup(appHost));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,8 +55,9 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Backing up {Library} to {BackupPath}", DbFilename, bakPath);
|
||||
File.Copy(dbPath, bakPath);
|
||||
_logger.LogInformation("Library database backed up to {BackupPath}", bakPath);
|
||||
_logger.LogInformation("{Library} backed up to {BackupPath}", DbFilename, bakPath);
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -80,7 +81,7 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
{
|
||||
IncludeItemTypes = [BaseItemKind.Audio],
|
||||
StartIndex = startIndex,
|
||||
Limit = 100,
|
||||
Limit = 5000,
|
||||
SkipDeserialization = true
|
||||
})
|
||||
.Cast<Audio>()
|
||||
@@ -97,7 +98,8 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
}
|
||||
|
||||
_itemRepository.SaveItems(results, CancellationToken.None);
|
||||
startIndex += 100;
|
||||
startIndex += results.Count;
|
||||
_logger.LogInformation("Backfilled data for {UpdatedRecords} of {TotalRecords} audio records", startIndex, records);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,18 +40,15 @@ namespace Jellyfin.Server
|
||||
{
|
||||
private readonly CoreAppHost _serverApplicationHost;
|
||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||
private readonly IConfiguration _startupConfig;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Startup" /> class.
|
||||
/// </summary>
|
||||
/// <param name="appHost">The server application host.</param>
|
||||
/// <param name="startupConfig">The server startupConfig.</param>
|
||||
public Startup(CoreAppHost appHost, IConfiguration startupConfig)
|
||||
public Startup(CoreAppHost appHost)
|
||||
{
|
||||
_serverApplicationHost = appHost;
|
||||
_serverConfigurationManager = appHost.ConfigurationManager;
|
||||
_startupConfig = startupConfig;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -70,7 +67,7 @@ namespace Jellyfin.Server
|
||||
// TODO remove once this is fixed upstream https://github.com/dotnet/aspnetcore/issues/34371
|
||||
services.AddSingleton<IActionResultExecutor<PhysicalFileResult>, SymlinkFollowingPhysicalFileResultExecutor>();
|
||||
services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration());
|
||||
services.AddJellyfinDbContext(_startupConfig.GetSqliteSecondLevelCacheDisabled());
|
||||
services.AddJellyfinDbContext();
|
||||
services.AddJellyfinApiSwagger();
|
||||
|
||||
// configure custom legacy authentication
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Common</PackageId>
|
||||
<VersionPrefix>10.10.0</VersionPrefix>
|
||||
<VersionPrefix>10.9.10</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -44,26 +44,28 @@ namespace MediaBrowser.Controller.Devices
|
||||
/// </summary>
|
||||
/// <param name="id">The identifier.</param>
|
||||
/// <returns>DeviceInfo.</returns>
|
||||
Task<DeviceInfo> GetDevice(string id);
|
||||
DeviceInfo GetDevice(string id);
|
||||
|
||||
/// <summary>
|
||||
/// Gets devices based on the provided query.
|
||||
/// </summary>
|
||||
/// <param name="query">The device query.</param>
|
||||
/// <returns>A <see cref="Task{QueryResult}"/> representing the retrieval of the devices.</returns>
|
||||
Task<QueryResult<Device>> GetDevices(DeviceQuery query);
|
||||
QueryResult<Device> GetDevices(DeviceQuery query);
|
||||
|
||||
Task<QueryResult<DeviceInfo>> GetDeviceInfos(DeviceQuery query);
|
||||
QueryResult<DeviceInfo> GetDeviceInfos(DeviceQuery query);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the devices.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user's id, or <c>null</c>.</param>
|
||||
/// <returns>IEnumerable<DeviceInfo>.</returns>
|
||||
Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId);
|
||||
QueryResult<DeviceInfo> GetDevicesForUser(Guid? userId);
|
||||
|
||||
Task DeleteDevice(Device device);
|
||||
|
||||
Task UpdateDevice(Device device);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this instance [can access device] the specified user identifier.
|
||||
/// </summary>
|
||||
@@ -74,6 +76,6 @@ namespace MediaBrowser.Controller.Devices
|
||||
|
||||
Task UpdateDeviceOptions(string deviceId, string deviceName);
|
||||
|
||||
Task<DeviceOptions> GetDeviceOptions(string deviceId);
|
||||
DeviceOptions GetDeviceOptions(string deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1949,14 +1949,15 @@ namespace MediaBrowser.Controller.Entities
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove it from the item
|
||||
RemoveImage(info);
|
||||
|
||||
// Remove from file system
|
||||
if (info.IsLocalFile)
|
||||
{
|
||||
FileSystem.DeleteFile(info.Path);
|
||||
}
|
||||
|
||||
// Remove from item
|
||||
RemoveImage(info);
|
||||
|
||||
await UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -364,15 +365,23 @@ namespace MediaBrowser.Controller.Entities
|
||||
|
||||
if (IsFileProtocol)
|
||||
{
|
||||
IEnumerable<BaseItem> nonCachedChildren;
|
||||
IEnumerable<BaseItem> nonCachedChildren = [];
|
||||
|
||||
try
|
||||
{
|
||||
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)
|
||||
{
|
||||
Logger.LogError(ex, "Error retrieving children folder");
|
||||
Logger.LogError(ex, "Error retrieving children");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -64,11 +64,6 @@ namespace MediaBrowser.Controller.Extensions
|
||||
/// </summary>
|
||||
public const string SqliteCacheSizeKey = "sqlite:cacheSize";
|
||||
|
||||
/// <summary>
|
||||
/// Disable second level cache of sqlite.
|
||||
/// </summary>
|
||||
public const string SqliteDisableSecondLevelCacheKey = "sqlite:disableSecondLevelCache";
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the application should host static web content from the <see cref="IConfiguration"/>.
|
||||
/// </summary>
|
||||
@@ -133,15 +128,5 @@ namespace MediaBrowser.Controller.Extensions
|
||||
/// <returns>The sqlite cache size.</returns>
|
||||
public static int? GetSqliteCacheSize(this IConfiguration configuration)
|
||||
=> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
64
MediaBrowser.Controller/IO/FileSystemHelper.cs
Normal file
64
MediaBrowser.Controller/IO/FileSystemHelper.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
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,6 +149,14 @@ namespace MediaBrowser.Controller.Library
|
||||
/// <returns>Task.</returns>
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Controller</PackageId>
|
||||
<VersionPrefix>10.10.0</VersionPrefix>
|
||||
<VersionPrefix>10.9.10</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -1208,8 +1208,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
var subtitlePath = state.SubtitleStream.Path;
|
||||
var subtitleExtension = Path.GetExtension(subtitlePath.AsSpan());
|
||||
|
||||
if (subtitleExtension.Equals(".sub", StringComparison.OrdinalIgnoreCase)
|
||||
|| subtitleExtension.Equals(".sup", StringComparison.OrdinalIgnoreCase))
|
||||
// dvdsub/vobsub graphical subtitles use .sub+.idx pairs
|
||||
if (subtitleExtension.Equals(".sub", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var idxFile = Path.ChangeExtension(subtitlePath, ".idx");
|
||||
if (File.Exists(idxFile))
|
||||
@@ -1313,7 +1313,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
// Apply aac_adtstoasc bitstream filter when media source is in mpegts.
|
||||
if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase)
|
||||
&& (string.Equals(mediaSourceContainer, "mpegts", StringComparison.OrdinalIgnoreCase)
|
||||
&& (string.Equals(mediaSourceContainer, "ts", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(mediaSourceContainer, "aac", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(mediaSourceContainer, "hls", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
|
||||
@@ -166,7 +166,7 @@ namespace MediaBrowser.Controller.Playlists
|
||||
return base.GetChildren(user, true, query);
|
||||
}
|
||||
|
||||
public static IReadOnlyList<BaseItem> GetPlaylistItems(MediaType playlistMediaType, IEnumerable<BaseItem> inputItems, User user, DtoOptions options)
|
||||
public static IReadOnlyList<BaseItem> GetPlaylistItems(IEnumerable<BaseItem> inputItems, User user, DtoOptions options)
|
||||
{
|
||||
if (user is not null)
|
||||
{
|
||||
@@ -177,14 +177,14 @@ namespace MediaBrowser.Controller.Playlists
|
||||
|
||||
foreach (var item in inputItems)
|
||||
{
|
||||
var playlistItems = GetPlaylistItems(item, user, playlistMediaType, options);
|
||||
var playlistItems = GetPlaylistItems(item, user, options);
|
||||
list.AddRange(playlistItems);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private static IEnumerable<BaseItem> GetPlaylistItems(BaseItem item, User user, MediaType mediaType, DtoOptions options)
|
||||
private static IEnumerable<BaseItem> GetPlaylistItems(BaseItem item, User user, DtoOptions options)
|
||||
{
|
||||
if (item is MusicGenre musicGenre)
|
||||
{
|
||||
@@ -216,7 +216,7 @@ namespace MediaBrowser.Controller.Playlists
|
||||
{
|
||||
Recursive = true,
|
||||
IsFolder = false,
|
||||
MediaTypes = [mediaType],
|
||||
MediaTypes = [MediaType.Audio, MediaType.Video],
|
||||
EnableTotalRecordCount = false,
|
||||
DtoOptions = options
|
||||
};
|
||||
|
||||
@@ -28,6 +28,22 @@ namespace MediaBrowser.Controller.Providers
|
||||
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)
|
||||
{
|
||||
var list = new List<FileSystemMetadata>();
|
||||
|
||||
@@ -9,6 +9,8 @@ namespace MediaBrowser.Controller.Providers
|
||||
{
|
||||
FileSystemMetadata[] GetFileSystemEntries(string path);
|
||||
|
||||
List<FileSystemMetadata> GetDirectories(string path);
|
||||
|
||||
List<FileSystemMetadata> GetFiles(string path);
|
||||
|
||||
FileSystemMetadata? GetFile(string path);
|
||||
|
||||
@@ -140,6 +140,14 @@ namespace MediaBrowser.Controller.Providers
|
||||
IEnumerable<IMetadataProvider<T>> GetMetadataProviders<T>(BaseItem item, LibraryOptions libraryOptions)
|
||||
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>
|
||||
/// Gets all metadata plugins.
|
||||
/// </summary>
|
||||
|
||||
@@ -38,19 +38,26 @@ namespace MediaBrowser.LocalMetadata.Images
|
||||
}
|
||||
|
||||
var parentPathFiles = directoryService.GetFiles(parentPath);
|
||||
var nameWithoutExtension = Path.GetFileNameWithoutExtension(item.Path.AsSpan()).ToString();
|
||||
|
||||
var nameWithoutExtension = Path.GetFileNameWithoutExtension(item.Path.AsSpan());
|
||||
var images = GetImageFilesFromFolder(nameWithoutExtension, parentPathFiles);
|
||||
|
||||
return GetFilesFromParentFolder(nameWithoutExtension, parentPathFiles);
|
||||
var metadataSubDir = directoryService.GetDirectories(parentPath).FirstOrDefault(d => d.Name.Equals("metadata", StringComparison.Ordinal));
|
||||
if (metadataSubDir is not null)
|
||||
{
|
||||
var files = directoryService.GetFiles(metadataSubDir.FullName);
|
||||
images.AddRange(GetImageFilesFromFolder(nameWithoutExtension, files));
|
||||
}
|
||||
|
||||
return images;
|
||||
}
|
||||
|
||||
private List<LocalImageInfo> GetFilesFromParentFolder(ReadOnlySpan<char> filenameWithoutExtension, List<FileSystemMetadata> parentPathFiles)
|
||||
private List<LocalImageInfo> GetImageFilesFromFolder(ReadOnlySpan<char> filenameWithoutExtension, List<FileSystemMetadata> filePaths)
|
||||
{
|
||||
var list = new List<LocalImageInfo>(1);
|
||||
var thumbName = string.Concat(filenameWithoutExtension, "-thumb");
|
||||
|
||||
var list = new List<LocalImageInfo>(1);
|
||||
|
||||
foreach (var i in parentPathFiles)
|
||||
foreach (var i in filePaths)
|
||||
{
|
||||
if (i.IsDirectory)
|
||||
{
|
||||
|
||||
@@ -89,7 +89,8 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||
string outputPath,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var shouldExtractOneByOne = mediaSource.MediaAttachments.Any(a => a.FileName.Contains('/', StringComparison.OrdinalIgnoreCase) || a.FileName.Contains('\\', StringComparison.OrdinalIgnoreCase));
|
||||
var shouldExtractOneByOne = mediaSource.MediaAttachments.Any(a => !string.IsNullOrEmpty(a.FileName)
|
||||
&& (a.FileName.Contains('/', StringComparison.OrdinalIgnoreCase) || a.FileName.Contains('\\', StringComparison.OrdinalIgnoreCase)));
|
||||
if (shouldExtractOneByOne)
|
||||
{
|
||||
var attachmentIndexes = mediaSource.MediaAttachments.Select(a => a.Index);
|
||||
|
||||
@@ -168,6 +168,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
private readonly string _encoderPath;
|
||||
|
||||
private readonly Version _minFFmpegMultiThreadedCli = new Version(7, 0);
|
||||
|
||||
public EncoderValidator(ILogger logger, string encoderPath)
|
||||
{
|
||||
_logger = logger;
|
||||
@@ -477,7 +479,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool CheckSupportedRuntimeKey(string keyDesc)
|
||||
public bool CheckSupportedRuntimeKey(string keyDesc, Version? ffmpegVersion)
|
||||
{
|
||||
if (string.IsNullOrEmpty(keyDesc))
|
||||
{
|
||||
@@ -487,7 +489,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
string output;
|
||||
try
|
||||
{
|
||||
output = GetProcessOutput(_encoderPath, "-hide_banner -f lavfi -i nullsrc=s=1x1:d=500 -f null -", true, "?");
|
||||
// With multi-threaded cli support, FFmpeg 7 is less sensitive to keyboard input
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -193,7 +193,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
_threads = EncodingHelper.GetNumberOfThreads(null, options, null);
|
||||
|
||||
_isPkeyPauseSupported = validator.CheckSupportedRuntimeKey("p pause transcoding");
|
||||
_isPkeyPauseSupported = validator.CheckSupportedRuntimeKey("p pause transcoding", _ffmpegVersion);
|
||||
|
||||
// Check the Vaapi device vendor
|
||||
if (OperatingSystem.IsLinux()
|
||||
@@ -1155,10 +1155,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
// Get all files from the BDMV/STREAMING directory
|
||||
// Only return playable local .m2ts files
|
||||
var files = _fileSystem.GetFiles(Path.Join(path, "BDMV", "STREAM")).ToList();
|
||||
return validPlaybackFiles
|
||||
.Select(f => _fileSystem.GetFileInfo(Path.Join(path, "BDMV", "STREAM", f)))
|
||||
.Where(f => f.Exists)
|
||||
.Select(f => f.FullName)
|
||||
.Select(validFile => files.FirstOrDefault(f => Path.GetFileName(f.FullName.AsSpan()).Equals(validFile, StringComparison.OrdinalIgnoreCase))?.FullName)
|
||||
.Where(f => f is not null)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
@@ -1216,7 +1216,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
var duration = TimeSpan.FromTicks(mediaInfoResult.RunTimeTicks.Value).TotalSeconds;
|
||||
|
||||
// Add file path stanza to concat configuration
|
||||
sw.WriteLine("file '{0}'", path);
|
||||
sw.WriteLine("file '{0}'", path.Replace("'", @"'\''", StringComparison.Ordinal));
|
||||
|
||||
// Add duration stanza to concat configuration
|
||||
sw.WriteLine("duration {0}", duration);
|
||||
|
||||
@@ -280,8 +280,8 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
splitFormat[i] = "mpeg";
|
||||
}
|
||||
|
||||
// Handle MPEG-2 container
|
||||
else if (string.Equals(splitFormat[i], "mpeg", StringComparison.OrdinalIgnoreCase))
|
||||
// Handle MPEG-TS container
|
||||
else if (string.Equals(splitFormat[i], "mpegts", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
splitFormat[i] = "ts";
|
||||
}
|
||||
@@ -624,15 +624,19 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
{
|
||||
if (string.Equals(codec, "dvb_subtitle", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
codec = "dvbsub";
|
||||
codec = "DVBSUB";
|
||||
}
|
||||
else if ((codec ?? string.Empty).Contains("PGS", StringComparison.OrdinalIgnoreCase))
|
||||
else if (string.Equals(codec, "dvb_teletext", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
codec = "PGSSUB";
|
||||
codec = "DVBTXT";
|
||||
}
|
||||
else if ((codec ?? string.Empty).Contains("DVD", StringComparison.OrdinalIgnoreCase))
|
||||
else if (string.Equals(codec, "dvd_subtitle", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
codec = "DVDSUB";
|
||||
codec = "DVDSUB"; // .sub+.idx
|
||||
}
|
||||
else if (string.Equals(codec, "hdmv_pgs_subtitle", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
codec = "PGSSUB"; // .sup
|
||||
}
|
||||
|
||||
return codec;
|
||||
@@ -717,6 +721,8 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
if (streamInfo.CodecType == CodecType.Audio)
|
||||
{
|
||||
stream.Type = MediaStreamType.Audio;
|
||||
stream.LocalizedDefault = _localization.GetLocalizedString("Default");
|
||||
stream.LocalizedExternal = _localization.GetLocalizedString("External");
|
||||
|
||||
stream.Channels = streamInfo.Channels;
|
||||
|
||||
@@ -779,11 +785,10 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
&& !string.Equals(streamInfo.FieldOrder, "progressive", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (isAudio
|
||||
&& (string.Equals(stream.Codec, "bmp", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(stream.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(stream.Codec, "webp", StringComparison.OrdinalIgnoreCase)))
|
||||
|| string.Equals(stream.Codec, "bmp", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(stream.Codec, "webp", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream.Type = MediaStreamType.EmbeddedImage;
|
||||
}
|
||||
|
||||
@@ -51,6 +51,8 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
|
||||
o.PoolInitialFill = 1;
|
||||
});
|
||||
|
||||
private readonly Version _maxFFmpegCkeyPauseSupported = new Version(6, 1);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TranscodeManager"/> class.
|
||||
/// </summary>
|
||||
@@ -559,7 +561,9 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
|
||||
|
||||
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.Start();
|
||||
|
||||
@@ -898,8 +898,10 @@ namespace MediaBrowser.Model.Dlna
|
||||
var appliedVideoConditions = options.Profile.CodecProfiles
|
||||
.Where(i => i.Type == CodecType.Video &&
|
||||
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)));
|
||||
var isFirstAppliedCodecProfile = true;
|
||||
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
|
||||
.Reverse();
|
||||
|
||||
foreach (var i in appliedVideoConditions)
|
||||
{
|
||||
var transcodingVideoCodecs = ContainerProfile.SplitValue(videoCodec);
|
||||
@@ -907,8 +909,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
{
|
||||
if (i.ContainsAnyCodec(transcodingVideoCodec, container))
|
||||
{
|
||||
ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingVideoCodec, true, isFirstAppliedCodecProfile);
|
||||
isFirstAppliedCodecProfile = false;
|
||||
ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingVideoCodec, true, true);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -930,8 +931,10 @@ namespace MediaBrowser.Model.Dlna
|
||||
var appliedAudioConditions = options.Profile.CodecProfiles
|
||||
.Where(i => i.Type == CodecType.VideoAudio &&
|
||||
i.ContainsAnyCodec(audioStream?.Codec, container) &&
|
||||
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio)));
|
||||
isFirstAppliedCodecProfile = true;
|
||||
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
|
||||
.Reverse();
|
||||
|
||||
foreach (var codecProfile in appliedAudioConditions)
|
||||
{
|
||||
var transcodingAudioCodecs = ContainerProfile.SplitValue(audioCodec);
|
||||
@@ -939,8 +942,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
{
|
||||
if (codecProfile.ContainsAnyCodec(transcodingAudioCodec, container))
|
||||
{
|
||||
ApplyTranscodingConditions(playlistItem, codecProfile.Conditions, transcodingAudioCodec, true, isFirstAppliedCodecProfile);
|
||||
isFirstAppliedCodecProfile = false;
|
||||
ApplyTranscodingConditions(playlistItem, codecProfile.Conditions, transcodingAudioCodec, true, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,14 +267,14 @@ namespace MediaBrowser.Model.Entities
|
||||
attributes.Add(StringHelper.FirstToUpper(fullLanguage ?? Language));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(Profile) && !string.Equals(Profile, "lc", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
attributes.Add(Profile);
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(Codec))
|
||||
if (!string.IsNullOrEmpty(Codec) && !string.Equals(Codec, "dca", StringComparison.OrdinalIgnoreCase) && !string.Equals(Codec, "dts", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
attributes.Add(AudioCodec.GetFriendlyName(Codec));
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(Profile) && !string.Equals(Profile, "lc", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
attributes.Add(Profile);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(ChannelLayout))
|
||||
{
|
||||
@@ -656,14 +656,14 @@ namespace MediaBrowser.Model.Entities
|
||||
{
|
||||
string codec = format ?? string.Empty;
|
||||
|
||||
// sub = external .sub file
|
||||
// microdvd and dvdsub/vobsub share the ".sub" file extension, but it's text-based.
|
||||
|
||||
return !codec.Contains("pgs", StringComparison.OrdinalIgnoreCase)
|
||||
&& !codec.Contains("dvd", StringComparison.OrdinalIgnoreCase)
|
||||
&& !codec.Contains("dvbsub", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(codec, "sub", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(codec, "sup", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(codec, "dvb_subtitle", StringComparison.OrdinalIgnoreCase);
|
||||
return codec.Contains("microdvd", StringComparison.OrdinalIgnoreCase)
|
||||
|| (!codec.Contains("pgs", StringComparison.OrdinalIgnoreCase)
|
||||
&& !codec.Contains("dvdsub", StringComparison.OrdinalIgnoreCase)
|
||||
&& !codec.Contains("dvbsub", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(codec, "sup", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(codec, "sub", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
public bool SupportsSubtitleConversionTo(string toCodec)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Model</PackageId>
|
||||
<VersionPrefix>10.10.0</VersionPrefix>
|
||||
<VersionPrefix>10.9.10</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
@@ -33,10 +33,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.HttpOverrides" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<PackageReference Include="MimeTypes">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
||||
@@ -14,6 +14,7 @@ using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Entities;
|
||||
@@ -100,8 +101,8 @@ namespace MediaBrowser.Providers.Manager
|
||||
{
|
||||
saveLocally = false;
|
||||
|
||||
// If season is virtual under a physical series, save locally if using compatible convention
|
||||
if (item is Season season && _config.Configuration.ImageSavingConvention == ImageSavingConvention.Compatible)
|
||||
// If season is virtual under a physical series, save locally
|
||||
if (item is Season season)
|
||||
{
|
||||
var series = season.Series;
|
||||
|
||||
@@ -126,7 +127,11 @@ namespace MediaBrowser.Providers.Manager
|
||||
|
||||
var paths = GetSavePaths(item, type, imageIndex, mimeType, saveLocally);
|
||||
|
||||
var retryPaths = GetSavePaths(item, type, imageIndex, mimeType, false);
|
||||
string[] retryPaths = [];
|
||||
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 (paths.Length > 1 && !source.CanSeek)
|
||||
@@ -183,6 +188,29 @@ namespace MediaBrowser.Providers.Manager
|
||||
try
|
||||
{
|
||||
_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)
|
||||
{
|
||||
@@ -374,6 +402,47 @@ namespace MediaBrowser.Providers.Manager
|
||||
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 (season is not null && season.IndexNumber.HasValue)
|
||||
@@ -447,20 +516,12 @@ namespace MediaBrowser.Providers.Manager
|
||||
break;
|
||||
}
|
||||
|
||||
if (string.Equals(extension, ".jpeg", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
extension = ".jpg";
|
||||
}
|
||||
|
||||
extension = extension.ToLowerInvariant();
|
||||
|
||||
string path = null;
|
||||
|
||||
if (saveLocally)
|
||||
{
|
||||
if (type == ImageType.Primary && item is Episode)
|
||||
{
|
||||
path = Path.Combine(Path.GetDirectoryName(item.Path), "metadata", filename + extension);
|
||||
path = Path.Combine(Path.GetDirectoryName(item.Path), filename + "-thumb" + extension);
|
||||
}
|
||||
else if (item.IsInMixedFolder)
|
||||
{
|
||||
|
||||
@@ -10,6 +10,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
@@ -96,7 +97,7 @@ namespace MediaBrowser.Providers.Manager
|
||||
public bool ValidateImages(BaseItem item, IEnumerable<IImageProvider> providers, ImageRefreshOptions refreshOptions)
|
||||
{
|
||||
var hasChanges = false;
|
||||
IDirectoryService directoryService = refreshOptions?.DirectoryService;
|
||||
var directoryService = refreshOptions?.DirectoryService;
|
||||
|
||||
if (item is not Photo)
|
||||
{
|
||||
@@ -158,7 +159,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())
|
||||
{
|
||||
PruneImages(item, oldBackdropImages);
|
||||
@@ -359,10 +360,8 @@ namespace MediaBrowser.Providers.Manager
|
||||
|
||||
private void PruneImages(BaseItem item, IReadOnlyList<ItemImageInfo> images)
|
||||
{
|
||||
for (var i = 0; i < images.Count; i++)
|
||||
foreach (var image in images)
|
||||
{
|
||||
var image = images[i];
|
||||
|
||||
if (image.IsLocalFile)
|
||||
{
|
||||
try
|
||||
@@ -371,7 +370,7 @@ namespace MediaBrowser.Providers.Manager
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
// nothing to do, already gone
|
||||
// Nothing to do, already gone
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
@@ -381,6 +380,16 @@ namespace MediaBrowser.Providers.Manager
|
||||
}
|
||||
|
||||
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>
|
||||
@@ -413,12 +422,10 @@ namespace MediaBrowser.Providers.Manager
|
||||
{
|
||||
var changed = item.ValidateImages();
|
||||
var foundImageTypes = new List<ImageType>();
|
||||
|
||||
for (var i = 0; i < _singularImages.Length; i++)
|
||||
{
|
||||
var type = _singularImages[i];
|
||||
var image = GetFirstLocalImageInfoByType(images, type);
|
||||
|
||||
if (image is not null)
|
||||
{
|
||||
var currentImage = item.GetImageInfo(type, 0);
|
||||
|
||||
@@ -92,10 +92,6 @@ namespace MediaBrowser.Providers.Manager
|
||||
}
|
||||
}
|
||||
|
||||
var localImagesFailed = false;
|
||||
|
||||
var allImageProviders = ProviderManager.GetImageProviders(item, refreshOptions).ToList();
|
||||
|
||||
if (refreshOptions.RemoveOldMetadata && refreshOptions.ReplaceAllImages)
|
||||
{
|
||||
if (ImageProvider.RemoveImages(item))
|
||||
@@ -104,19 +100,29 @@ namespace MediaBrowser.Providers.Manager
|
||||
}
|
||||
}
|
||||
|
||||
// Start by validating images
|
||||
try
|
||||
var localImagesFailed = false;
|
||||
var allImageProviders = ProviderManager.GetImageProviders(item, refreshOptions).ToList();
|
||||
|
||||
// Only validate already registered images if we are replacing and saving locally
|
||||
if (item.IsSaveLocalMetadataEnabled() && refreshOptions.ReplaceAllImages)
|
||||
{
|
||||
// Always validate images and check for new locally stored ones.
|
||||
if (ImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptions))
|
||||
{
|
||||
updateType |= ItemUpdateType.ImageUpdate;
|
||||
}
|
||||
item.ValidateImages();
|
||||
}
|
||||
catch (Exception ex)
|
||||
else
|
||||
{
|
||||
localImagesFailed = true;
|
||||
Logger.LogError(ex, "Error validating images for {Item}", item.Path ?? item.Name ?? "Unknown name");
|
||||
// Run full image validation and register new local images
|
||||
try
|
||||
{
|
||||
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>
|
||||
@@ -154,7 +160,8 @@ namespace MediaBrowser.Providers.Manager
|
||||
|
||||
id.IsAutomated = refreshOptions.IsAutomated;
|
||||
|
||||
var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, ImageProvider, cancellationToken).ConfigureAwait(false);
|
||||
var hasMetadataSavers = ProviderManager.GetMetadataSavers(item, libraryOptions).Any();
|
||||
var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, ImageProvider, hasMetadataSavers, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
updateType |= result.UpdateType;
|
||||
if (result.Failures > 0)
|
||||
@@ -639,6 +646,7 @@ namespace MediaBrowser.Providers.Manager
|
||||
MetadataRefreshOptions options,
|
||||
ICollection<IMetadataProvider> providers,
|
||||
ItemImageProvider imageService,
|
||||
bool isSavingMetadata,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var refreshResult = new RefreshResult
|
||||
@@ -667,71 +675,78 @@ namespace MediaBrowser.Providers.Manager
|
||||
};
|
||||
temp.Item.Path = item.Path;
|
||||
temp.Item.Id = item.Id;
|
||||
temp.Item.PreferredMetadataCountryCode = item.PreferredMetadataCountryCode;
|
||||
temp.Item.PreferredMetadataLanguage = item.PreferredMetadataLanguage;
|
||||
|
||||
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))
|
||||
{
|
||||
var providerName = provider.GetType().Name;
|
||||
Logger.LogDebug("Running {Provider} for {Item}", providerName, logName);
|
||||
|
||||
var itemInfo = new ItemInfo(item);
|
||||
|
||||
try
|
||||
foreach (var provider in providers.OfType<ILocalMetadataProvider<TItemType>>())
|
||||
{
|
||||
var localItem = await provider.GetMetadata(itemInfo, options.DirectoryService, cancellationToken).ConfigureAwait(false);
|
||||
var providerName = provider.GetType().Name;
|
||||
Logger.LogDebug("Running {Provider} for {Item}", providerName, logName);
|
||||
|
||||
if (localItem.HasMetadata)
|
||||
var itemInfo = new ItemInfo(item);
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var remoteImage in localItem.RemoteImages)
|
||||
var localItem = await provider.GetMetadata(itemInfo, options.DirectoryService, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (localItem.HasMetadata)
|
||||
{
|
||||
try
|
||||
foreach (var remoteImage in localItem.RemoteImages)
|
||||
{
|
||||
if (item.ImageInfos.Any(x => x.Type == remoteImage.Type)
|
||||
&& !options.IsReplacingImage(remoteImage.Type))
|
||||
try
|
||||
{
|
||||
continue;
|
||||
if (item.ImageInfos.Any(x => x.Type == remoteImage.Type)
|
||||
&& !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);
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
if (foundImageTypes.Count > 0)
|
||||
{
|
||||
Logger.LogError(ex, "Could not save {ImageType} image: {Url}", Enum.GetName(remoteImage.Type), remoteImage.Url);
|
||||
imageService.UpdateReplaceImages(options, foundImageTypes);
|
||||
}
|
||||
|
||||
if (imageService.MergeImages(item, localItem.Images, options))
|
||||
{
|
||||
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
|
||||
}
|
||||
|
||||
MergeData(localItem, temp, Array.Empty<MetadataField>(), false, true);
|
||||
refreshResult.UpdateType |= ItemUpdateType.MetadataImport;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (foundImageTypes.Count > 0)
|
||||
{
|
||||
imageService.UpdateReplaceImages(options, foundImageTypes);
|
||||
}
|
||||
|
||||
if (imageService.MergeImages(item, localItem.Images, options))
|
||||
{
|
||||
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);
|
||||
|
||||
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;
|
||||
// If a local provider fails, consider that a failure
|
||||
refreshResult.ErrorMessage = ex.Message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -763,7 +778,7 @@ namespace MediaBrowser.Providers.Manager
|
||||
else
|
||||
{
|
||||
var shouldReplace = options.MetadataRefreshMode > MetadataRefreshMode.ValidationOnly || options.ReplaceAllMetadata;
|
||||
MergeData(temp, metadata, item.LockedFields, shouldReplace, false);
|
||||
MergeData(temp, metadata, item.LockedFields, shouldReplace, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -804,19 +819,16 @@ namespace MediaBrowser.Providers.Manager
|
||||
{
|
||||
var refreshResult = new RefreshResult();
|
||||
|
||||
var tmpDataMerged = false;
|
||||
if (id is not null)
|
||||
{
|
||||
MergeNewData(temp.Item, id);
|
||||
}
|
||||
|
||||
foreach (var provider in providers)
|
||||
{
|
||||
var providerName = provider.GetType().Name;
|
||||
Logger.LogDebug("Running {Provider} for {Item}", providerName, logName);
|
||||
|
||||
if (id is not null && !tmpDataMerged)
|
||||
{
|
||||
MergeNewData(temp.Item, id);
|
||||
tmpDataMerged = true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = await provider.GetMetadata(id, cancellationToken).ConfigureAwait(false);
|
||||
@@ -1050,7 +1062,7 @@ namespace MediaBrowser.Providers.Manager
|
||||
}
|
||||
else
|
||||
{
|
||||
target.Tags = target.ProductionLocations.Concat(source.ProductionLocations).Distinct().ToArray();
|
||||
target.ProductionLocations = target.ProductionLocations.Concat(source.ProductionLocations).Distinct().ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1080,7 +1092,7 @@ namespace MediaBrowser.Providers.Manager
|
||||
}
|
||||
else
|
||||
{
|
||||
target.RemoteTrailers = target.RemoteTrailers.Concat(source.RemoteTrailers).Distinct().ToArray();
|
||||
target.RemoteTrailers = target.RemoteTrailers.Concat(source.RemoteTrailers).DistinctBy(t => t.Url).ToArray();
|
||||
}
|
||||
|
||||
MergeAlbumArtist(source, target, replaceData);
|
||||
|
||||
@@ -418,6 +418,12 @@ namespace MediaBrowser.Providers.Manager
|
||||
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)
|
||||
where T : BaseItem
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@@ -136,6 +137,10 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
if (!audio.IsLocked)
|
||||
{
|
||||
await FetchDataFromTags(audio, mediaInfo, options, tryExtractEmbeddedLyrics).ConfigureAwait(false);
|
||||
if (tryExtractEmbeddedLyrics)
|
||||
{
|
||||
AddExternalLyrics(audio, mediaStreams, options);
|
||||
}
|
||||
}
|
||||
|
||||
audio.HasLyrics = mediaStreams.Any(s => s.Type == MediaStreamType.Lyric);
|
||||
@@ -193,11 +198,11 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
}
|
||||
|
||||
tags ??= new TagLib.Id3v2.Tag();
|
||||
tags.AlbumArtists ??= mediaInfo.AlbumArtists;
|
||||
tags.AlbumArtists = tags.AlbumArtists.Length == 0 ? mediaInfo.AlbumArtists : tags.AlbumArtists;
|
||||
tags.Album ??= mediaInfo.Album;
|
||||
tags.Title ??= mediaInfo.Name;
|
||||
tags.Year = tags.Year == 0U ? Convert.ToUInt32(mediaInfo.ProductionYear, CultureInfo.InvariantCulture) : tags.Year;
|
||||
tags.Performers ??= mediaInfo.Artists;
|
||||
tags.Performers = tags.Performers.Length == 0 ? mediaInfo.Artists : tags.Performers;
|
||||
tags.Genres ??= mediaInfo.Genres;
|
||||
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;
|
||||
@@ -369,7 +374,10 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
var externalLyricFiles = _lyricResolver.GetExternalStreams(audio, startIndex, options.DirectoryService, false);
|
||||
|
||||
audio.LyricFiles = externalLyricFiles.Select(i => i.Path).Distinct().ToArray();
|
||||
currentStreams.AddRange(externalLyricFiles);
|
||||
if (externalLyricFiles.Count > 0)
|
||||
{
|
||||
currentStreams.Add(externalLyricFiles[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,6 +358,10 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
blurayVideoStream.BitRate = blurayVideoStream.BitRate.GetValueOrDefault() == 0 ? ffmpegVideoStream.BitRate : blurayVideoStream.BitRate;
|
||||
blurayVideoStream.Width = blurayVideoStream.Width.GetValueOrDefault() == 0 ? ffmpegVideoStream.Width : blurayVideoStream.Width;
|
||||
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,7 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
@@ -18,182 +16,212 @@ using MediaBrowser.Model.IO;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PlaylistsNET.Content;
|
||||
|
||||
namespace MediaBrowser.Providers.Playlists
|
||||
namespace MediaBrowser.Providers.Playlists;
|
||||
|
||||
/// <summary>
|
||||
/// Local playlist provider.
|
||||
/// </summary>
|
||||
public class PlaylistItemsProvider : ILocalMetadataProvider<Playlist>,
|
||||
IHasOrder,
|
||||
IForcedProvider,
|
||||
IHasItemChangeMonitor
|
||||
{
|
||||
public class PlaylistItemsProvider : ICustomMetadataProvider<Playlist>,
|
||||
IHasOrder,
|
||||
IForcedProvider,
|
||||
IPreRefreshProvider,
|
||||
IHasItemChangeMonitor
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILogger<PlaylistItemsProvider> _logger;
|
||||
private readonly CollectionType[] _ignoredCollections = [CollectionType.livetv, CollectionType.boxsets, CollectionType.playlists];
|
||||
|
||||
/// <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)
|
||||
{
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILogger<PlaylistItemsProvider> _logger;
|
||||
private readonly CollectionType[] _ignoredCollections = [CollectionType.livetv, CollectionType.boxsets, CollectionType.playlists];
|
||||
_logger = logger;
|
||||
_libraryManager = libraryManager;
|
||||
_fileSystem = fileSystem;
|
||||
}
|
||||
|
||||
public PlaylistItemsProvider(ILogger<PlaylistItemsProvider> logger, ILibraryManager libraryManager, IFileSystem fileSystem)
|
||||
/// <inheritdoc />
|
||||
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>()
|
||||
{
|
||||
_logger = logger;
|
||||
_libraryManager = libraryManager;
|
||||
_fileSystem = fileSystem;
|
||||
Item = new Playlist
|
||||
{
|
||||
Path = info.Path
|
||||
}
|
||||
};
|
||||
Fetch(result);
|
||||
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
private void Fetch(MetadataResult<Playlist> result)
|
||||
{
|
||||
var item = result.Item;
|
||||
var path = item.Path;
|
||||
if (!Playlist.IsPlaylistFile(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
public string Name => "Playlist Reader";
|
||||
|
||||
// Run last
|
||||
public int Order => 100;
|
||||
|
||||
public Task<ItemUpdateType> FetchAsync(Playlist item, MetadataRefreshOptions options, CancellationToken cancellationToken)
|
||||
var extension = Path.GetExtension(path);
|
||||
if (!Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var path = item.Path;
|
||||
if (!Playlist.IsPlaylistFile(path))
|
||||
{
|
||||
return Task.FromResult(ItemUpdateType.None);
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(path);
|
||||
if (!Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Task.FromResult(ItemUpdateType.None);
|
||||
}
|
||||
|
||||
var items = GetItems(path, extension).ToArray();
|
||||
return;
|
||||
}
|
||||
|
||||
var items = GetItems(path, extension).ToArray();
|
||||
if (items.Length > 0)
|
||||
{
|
||||
result.HasMetadata = true;
|
||||
item.LinkedChildren = items;
|
||||
|
||||
return Task.FromResult(ItemUpdateType.MetadataImport);
|
||||
}
|
||||
|
||||
private IEnumerable<LinkedChild> GetItems(string path, string extension)
|
||||
return;
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
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))
|
||||
{
|
||||
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);
|
||||
}
|
||||
return GetWplItems(stream, path, libraryRoots);
|
||||
}
|
||||
|
||||
return Enumerable.Empty<LinkedChild>();
|
||||
}
|
||||
|
||||
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))
|
||||
if (string.Equals(".zpl", extension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new LinkedChild
|
||||
{
|
||||
Path = parsedPath,
|
||||
Type = LinkedChildType.Manual
|
||||
};
|
||||
return GetZplItems(stream, path, libraryRoots);
|
||||
}
|
||||
|
||||
return null;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryGetPlaylistItemPath(string itemPath, string playlistPath, List<string> libraryPaths, out string path)
|
||||
return Enumerable.Empty<LinkedChild>();
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
path = null;
|
||||
string pathToCheck = _fileSystem.MakeAbsolutePath(Path.GetDirectoryName(playlistPath), itemPath);
|
||||
if (!File.Exists(pathToCheck))
|
||||
return new LinkedChild
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Path = parsedPath,
|
||||
Type = LinkedChildType.Manual
|
||||
};
|
||||
}
|
||||
|
||||
foreach (var libraryPath in libraryPaths)
|
||||
{
|
||||
if (pathToCheck.StartsWith(libraryPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
path = pathToCheck;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
public bool HasChanged(BaseItem item, IDirectoryService directoryService)
|
||||
foreach (var libraryPath in libraryPaths)
|
||||
{
|
||||
var path = item.Path;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(path) && item.IsFileProtocol)
|
||||
if (pathToCheck.StartsWith(libraryPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
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;
|
||||
}
|
||||
path = pathToCheck;
|
||||
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 (!string.IsNullOrWhiteSpace(releaseId) && string.IsNullOrWhiteSpace(releaseGroupId))
|
||||
{
|
||||
var release = await _musicBrainzQuery.LookupReleaseAsync(new Guid(releaseId), Include.Releases, cancellationToken).ConfigureAwait(false);
|
||||
var release = await _musicBrainzQuery.LookupReleaseAsync(new Guid(releaseId), Include.ReleaseGroups, cancellationToken).ConfigureAwait(false);
|
||||
releaseGroupId = release.ReleaseGroup?.Id.ToString();
|
||||
result.HasMetadata = true;
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ namespace MediaBrowser.Providers.TV
|
||||
|
||||
private void RemoveObsoleteSeasons(Series series)
|
||||
{
|
||||
// TODO Legacy. It's not really "physical" seasons as any virtual seasons are always converted to non-virtual in UpdateAndCreateSeasonsAsync.
|
||||
// TODO Legacy. It's not really "physical" seasons as any virtual seasons are always converted to non-virtual in CreateSeasonsAsync.
|
||||
var physicalSeasonNumbers = new HashSet<int>();
|
||||
var virtualSeasons = new List<Season>();
|
||||
foreach (var existingSeason in series.Children.OfType<Season>())
|
||||
@@ -119,7 +119,8 @@ namespace MediaBrowser.Providers.TV
|
||||
virtualSeason,
|
||||
new DeleteOptions
|
||||
{
|
||||
DeleteFileLocation = true
|
||||
// Internal metadata paths are removed regardless of this.
|
||||
DeleteFileLocation = false
|
||||
},
|
||||
false);
|
||||
}
|
||||
@@ -176,7 +177,8 @@ namespace MediaBrowser.Providers.TV
|
||||
episode,
|
||||
new DeleteOptions
|
||||
{
|
||||
DeleteFileLocation = true
|
||||
// Internal metadata paths are removed regardless of this.
|
||||
DeleteFileLocation = false
|
||||
},
|
||||
false);
|
||||
}
|
||||
@@ -201,11 +203,20 @@ namespace MediaBrowser.Providers.TV
|
||||
foreach (var seasonNumber in uniqueSeasonNumbers)
|
||||
{
|
||||
// Null season numbers will have a 'dummy' season created because seasons are always required.
|
||||
if (!seasons.Any(i => i.IndexNumber == seasonNumber))
|
||||
var existingSeason = seasons.FirstOrDefault(i => i.IndexNumber == seasonNumber);
|
||||
if (existingSeason is null)
|
||||
{
|
||||
var seasonName = GetValidSeasonNameForSeries(series, null, seasonNumber);
|
||||
var season = await CreateSeasonAsync(series, seasonName, seasonNumber, cancellationToken).ConfigureAwait(false);
|
||||
series.AddChild(season);
|
||||
await CreateSeasonAsync(series, seasonName, seasonNumber, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -218,7 +229,7 @@ namespace MediaBrowser.Providers.TV
|
||||
/// <param name="seasonNumber">The season number.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The newly created season.</returns>
|
||||
private async Task<Season> CreateSeasonAsync(
|
||||
private async Task CreateSeasonAsync(
|
||||
Series series,
|
||||
string? seasonName,
|
||||
int? seasonNumber,
|
||||
@@ -235,14 +246,12 @@ namespace MediaBrowser.Providers.TV
|
||||
typeof(Season)),
|
||||
IsVirtualItem = false,
|
||||
SeriesId = series.Id,
|
||||
SeriesName = series.Name
|
||||
SeriesName = series.Name,
|
||||
SeriesPresentationUniqueKey = series.GetPresentationUniqueKey()
|
||||
};
|
||||
|
||||
series.AddChild(season);
|
||||
|
||||
await season.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return season;
|
||||
}
|
||||
|
||||
private string GetValidSeasonNameForSeries(Series series, string? seasonName, int? seasonNumber)
|
||||
|
||||
@@ -519,7 +519,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
||||
if (reader.TryReadDateTimeExact(nfoConfiguration.ReleaseDateFormat, out var releaseDate))
|
||||
{
|
||||
item.PremiereDate = releaseDate;
|
||||
item.ProductionYear = releaseDate.Year;
|
||||
|
||||
// Production year can already be set by the year tag
|
||||
item.ProductionYear ??= releaseDate.Year;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
@@ -825,7 +825,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
||||
private string GetOutputTrailerUrl(string url)
|
||||
{
|
||||
// This is what xbmc expects
|
||||
return url.Replace(YouTubeWatchUrl, "plugin://plugin.video.youtube/?action=play_video&videoid=", StringComparison.OrdinalIgnoreCase);
|
||||
return url.Replace(YouTubeWatchUrl, "plugin://plugin.video.youtube/play/?video_id=", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private void AddImages(BaseItem item, XmlWriter writer, ILibraryManager libraryManager)
|
||||
|
||||
@@ -45,27 +45,24 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
||||
|
||||
internal static IEnumerable<string> GetMovieSavePaths(ItemInfo item)
|
||||
{
|
||||
var path = item.ContainingFolderPath;
|
||||
if (item.VideoType == VideoType.Dvd && !item.IsPlaceHolder)
|
||||
{
|
||||
var path = item.ContainingFolderPath;
|
||||
|
||||
yield return Path.Combine(path, "VIDEO_TS", "VIDEO_TS.nfo");
|
||||
}
|
||||
|
||||
// only allow movie object to read movie.nfo, not owned videos (which will be itemtype video, not movie)
|
||||
if (!item.IsInMixedFolder && item.ItemType == typeof(Movie))
|
||||
{
|
||||
yield return Path.Combine(path, "movie.nfo");
|
||||
}
|
||||
|
||||
if (!item.IsPlaceHolder && (item.VideoType == VideoType.Dvd || item.VideoType == VideoType.BluRay))
|
||||
{
|
||||
var path = item.ContainingFolderPath;
|
||||
|
||||
yield return Path.Combine(path, Path.GetFileName(path) + ".nfo");
|
||||
}
|
||||
else
|
||||
{
|
||||
// only allow movie object to read movie.nfo, not owned videos (which will be itemtype video, not movie)
|
||||
if (!item.IsInMixedFolder && item.ItemType == typeof(Movie))
|
||||
{
|
||||
yield return Path.Combine(item.ContainingFolderPath, "movie.nfo");
|
||||
}
|
||||
|
||||
yield return Path.ChangeExtension(item.Path, ".nfo");
|
||||
}
|
||||
}
|
||||
|
||||
14
README.md
14
README.md
@@ -153,20 +153,20 @@ API documentation can be viewed at `http://localhost:8096/api-docs/swagger/index
|
||||
|
||||
As Jellyfin will run on a container on a github hosted server, JF needs to handle some things differently.
|
||||
|
||||
**NOTE:** Depending on the selected configuration (if you just click 'create codespace' it will create a default configuration one) it might take 20-30 seconds to load all extensions and prepare the environment while VS Code is already open. Just give it some time and wait until you see `Downloading .NET version(s) 7.0.15~x64 ...... Done!` in the output tab.
|
||||
**NOTE:** Depending on the selected configuration (if you just click 'create codespace' it will create a default configuration one) it might take 20-30 secounds to load all extensions and prepare the enviorment while vscode is already open. Just give it some time and wait until you see `Downloading .NET version(s) 7.0.15~x64 ...... Done!` in the output tab.
|
||||
|
||||
**NOTE:** If you want to access the JF instance from outside, like with a WebClient on another PC, remember to set the "ports" in the lower VS Code window to public.
|
||||
**NOTE:** If you want to access the JF instance from outside, like with a WebClient on another PC, remember to set the "ports" in the lower VsCode window to public.
|
||||
|
||||
**NOTE:** When first opening the server instance with any WebUI, you will be sent to the login instead of the setup page. Refresh the login page once and you should be redirected to the Setup.
|
||||
**NOTE:** When first opening the server instance with any WebUI, you will be send to the login instead of the setup page. Refresh the login page once and you should be redirected to the Setup.
|
||||
|
||||
There are two configurations for you to choose from.
|
||||
There are two configurations for you to chose from.
|
||||
#### Default - Development Jellyfin Server
|
||||
This creates a container that has everything to run and debug the Jellyfin Media server but does not setup anything else. Each time you create a new container you have to run through the whole setup again. There is also no ffmpeg, webclient or media preloaded. Use the `.NET Launch (nowebclient)` launch config to start the server.
|
||||
This creates a container that has everything to run and debug the Jellyfin Media server but does not setup anything else. Each time you create a new container you have to run though the whole setup again. There is also no ffmpeg, webclient or media preloaded. Use the `.NET Launch (nowebclient)` lunch config to start the server.
|
||||
|
||||
> Keep in mind that as this has no web client you have to connect to it via an external client. This can be just another codespace container running the WebUI. vuejs does not work from the get-go as it does not support the setup steps.
|
||||
> Keep in mind that as this has no web client you have to connect to it via an extenal client. This can be just another codespace container running the WebUI. vuejs does not work from the getgo as it does not support the setup steps.
|
||||
|
||||
#### Development Jellyfin Server ffmpeg
|
||||
this extends the default server with a default installation of ffmpeg6 though the means described here: https://jellyfin.org/docs/general/installation/linux#repository-manual
|
||||
this extens the default server with an default installation of ffmpeg6 though the means described here: https://jellyfin.org/docs/general/installation/linux#repository-manual
|
||||
If you want to install a specific ffmpeg version, follow the comments embedded in the `.devcontainer/Dev - Server Ffmpeg/install.ffmpeg.sh` file.
|
||||
|
||||
Use the `ghcs .NET Launch (nowebclient, ffmpeg)` launch config to run with the jellyfin-ffmpeg enabled.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Reflection;
|
||||
|
||||
[assembly: AssemblyVersion("10.10.0")]
|
||||
[assembly: AssemblyFileVersion("10.10.0")]
|
||||
[assembly: AssemblyVersion("10.9.10")]
|
||||
[assembly: AssemblyFileVersion("10.9.10")]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user