mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-03-01 07:52:53 +00:00
Compare commits
229 Commits
v10.11.0-r
...
v10.11.0-r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5cae44fdf7 | ||
|
|
c3cb5fd2f9 | ||
|
|
1262ac31dc | ||
|
|
0f5bb5cf76 | ||
|
|
ce78af2ed4 | ||
|
|
4b6fb6c4bb | ||
|
|
db7465e83d | ||
|
|
803e87ca5f | ||
|
|
9e36fa4263 | ||
|
|
a52a230778 | ||
|
|
b00e381109 | ||
|
|
b8fb8bd608 | ||
|
|
34c9adef80 | ||
|
|
c8d2f43660 | ||
|
|
ef733c5ace | ||
|
|
a1eb04dc0b | ||
|
|
711e649e35 | ||
|
|
1d408a1503 | ||
|
|
6391dd9570 | ||
|
|
2007815fa6 | ||
|
|
a5b4eca804 | ||
|
|
76d498ac9d | ||
|
|
90b4345cfd | ||
|
|
317192c23d | ||
|
|
dcb12a73fb | ||
|
|
b15abddfd7 | ||
|
|
cfde5af3b0 | ||
|
|
26a6cfaf65 | ||
|
|
8a8018f0de | ||
|
|
6f49782b7b | ||
|
|
536437bbe3 | ||
|
|
ba54cda774 | ||
|
|
e86315128d | ||
|
|
dfab2fb6e2 | ||
|
|
7785b51f57 | ||
|
|
a068f75623 | ||
|
|
1ed191c5b3 | ||
|
|
0e3fbb6abd | ||
|
|
583a861b32 | ||
|
|
3bcfe13652 | ||
|
|
f5a135a1db | ||
|
|
0cea853b45 | ||
|
|
663087b155 | ||
|
|
dddeea1f7b | ||
|
|
a148a4ad02 | ||
|
|
57d077d08e | ||
|
|
774be151aa | ||
|
|
7569ac65a8 | ||
|
|
4621a99c7c | ||
|
|
1e796e0b7a | ||
|
|
4da5483ef4 | ||
|
|
eea0872980 | ||
|
|
36c90ce2ce | ||
|
|
48e93dcbce | ||
|
|
6cee66119e | ||
|
|
c62a07405e | ||
|
|
7bd08ab290 | ||
|
|
088ef0d37a | ||
|
|
ba0f61ef2d | ||
|
|
c70f6bffcf | ||
|
|
21a6d6f0d6 | ||
|
|
aa77dfb92d | ||
|
|
2ad37fe021 | ||
|
|
fd5205a6eb | ||
|
|
60cfa65cdc | ||
|
|
e5139e1004 | ||
|
|
aa1abf8b94 | ||
|
|
742b5637fa | ||
|
|
25a362345d | ||
|
|
310a54f090 | ||
|
|
e9d92bdcb0 | ||
|
|
dc39a51475 | ||
|
|
c51f3a3342 | ||
|
|
7ece959f4e | ||
|
|
c96e828002 | ||
|
|
ab56ceaa16 | ||
|
|
4645633acf | ||
|
|
d6f93759ea | ||
|
|
bf3f37e3d0 | ||
|
|
982e0c9370 | ||
|
|
55e681b9a6 | ||
|
|
7ba77804c4 | ||
|
|
af6f5a8ed0 | ||
|
|
1162fcebf8 | ||
|
|
38d0367c42 | ||
|
|
7d3372018f | ||
|
|
8629831658 | ||
|
|
db55d983f8 | ||
|
|
4d5ba8d7a5 | ||
|
|
6d4169a449 | ||
|
|
8dcb0bfecb | ||
|
|
844d69ab64 | ||
|
|
5c36b44484 | ||
|
|
4e4d7e7764 | ||
|
|
4c268a3579 | ||
|
|
77bcd2f5f6 | ||
|
|
8406924471 | ||
|
|
67fd4ce187 | ||
|
|
b37b39773a | ||
|
|
6f98767aed | ||
|
|
643460f484 | ||
|
|
a4231bf428 | ||
|
|
9c817a97a9 | ||
|
|
f9c4c9b345 | ||
|
|
dde306b170 | ||
|
|
e2b61d951b | ||
|
|
9eff25bfed | ||
|
|
ff4484eb4a | ||
|
|
62b2adbf66 | ||
|
|
9ac8c2a2fa | ||
|
|
90e72fb687 | ||
|
|
630846798d | ||
|
|
9d5be19a27 | ||
|
|
6058ab50f8 | ||
|
|
e3b379052d | ||
|
|
0b6f4b2bd9 | ||
|
|
4f6db1bc22 | ||
|
|
8c8c71125c | ||
|
|
c6e568692e | ||
|
|
d5a76bdff8 | ||
|
|
ebdc756547 | ||
|
|
10d0cec7b9 | ||
|
|
10cc651790 | ||
|
|
7d18f3d6ed | ||
|
|
9b8c12d433 | ||
|
|
ba0eb87371 | ||
|
|
d561cef81f | ||
|
|
b528c1100f | ||
|
|
96c9f4fdad | ||
|
|
6d077fcf40 | ||
|
|
ab99b2bad3 | ||
|
|
db36be7a6b | ||
|
|
85f158e1dd | ||
|
|
e1365bd253 | ||
|
|
1ec66adc30 | ||
|
|
af0bcbc652 | ||
|
|
b2312466e1 | ||
|
|
cc7915c2e6 | ||
|
|
a537c66da1 | ||
|
|
a43adf42f3 | ||
|
|
6996c8a1de | ||
|
|
f976630003 | ||
|
|
965cf93419 | ||
|
|
70ea3f863a | ||
|
|
989aef18af | ||
|
|
ccb917b8df | ||
|
|
7cf6389ab5 | ||
|
|
2473b89a8d | ||
|
|
6575c69a4e | ||
|
|
66d594836c | ||
|
|
43028f735f | ||
|
|
e83b992eef | ||
|
|
8368d10d1b | ||
|
|
e8291fc856 | ||
|
|
308707476d | ||
|
|
e252589900 | ||
|
|
1220cac255 | ||
|
|
7218d82c21 | ||
|
|
a4524eb2ad | ||
|
|
553ba56389 | ||
|
|
afa2103d42 | ||
|
|
7256c9c89d | ||
|
|
f3cdaeaa12 | ||
|
|
368808eba4 | ||
|
|
0fc8ed6aeb | ||
|
|
f60281d8fd | ||
|
|
2936588c0f | ||
|
|
0e1be6ce30 | ||
|
|
4cd0a2ed8d | ||
|
|
aa05185917 | ||
|
|
2d9257b203 | ||
|
|
d1d9c8ed06 | ||
|
|
23c25289da | ||
|
|
aad6bca955 | ||
|
|
9f0f9a276f | ||
|
|
6016159860 | ||
|
|
6ffc044af1 | ||
|
|
c22f24319b | ||
|
|
1c4c9cf733 | ||
|
|
ea34a38f09 | ||
|
|
bbcfb2f421 | ||
|
|
0873fa8a86 | ||
|
|
9dc50b4ac6 | ||
|
|
617ab0d0ca | ||
|
|
dee9629037 | ||
|
|
31f3b5f6bb | ||
|
|
2ac6a7ba3f | ||
|
|
ece77779f8 | ||
|
|
c15c1f82a3 | ||
|
|
a15352b80c | ||
|
|
304b944152 | ||
|
|
e81c8ac6d1 | ||
|
|
97c1cb2f26 | ||
|
|
ac9d84f602 | ||
|
|
f3bf3c9853 | ||
|
|
644245bb7c | ||
|
|
a18c0007b4 | ||
|
|
c8a51160b4 | ||
|
|
4a0a45a045 | ||
|
|
91da1c035d | ||
|
|
6b5ce934b3 | ||
|
|
7174bb6a93 | ||
|
|
7037121bd0 | ||
|
|
7417da0e5c | ||
|
|
1e8bf1ce8d | ||
|
|
d4c3d24e52 | ||
|
|
d3ad2aec60 | ||
|
|
3554f068fb | ||
|
|
6dac1fde0a | ||
|
|
56fe4a158e | ||
|
|
c2332d340c | ||
|
|
d5b5c71baf | ||
|
|
7aee5b1e70 | ||
|
|
a8601b3797 | ||
|
|
1e9e4ffda9 | ||
|
|
d7faf9a327 | ||
|
|
bdb3adeb30 | ||
|
|
1f5cfb1e23 | ||
|
|
98daf4aedb | ||
|
|
fcf56b73cb | ||
|
|
e8239a7ee2 | ||
|
|
84cebeae64 | ||
|
|
c0e2875818 | ||
|
|
411ba03bf0 | ||
|
|
b2e19c0306 | ||
|
|
a7891b3f2d | ||
|
|
e7bc86ebb8 | ||
|
|
7aa96dfc20 | ||
|
|
70d07b830d |
@@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "9.0.5",
|
||||
"version": "9.0.7",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"dotnetRuntimeVersions": "9.0",
|
||||
"aspNetCoreRuntimeVersions": "9.0"
|
||||
},
|
||||
"ghcr.io/devcontainers-contrib/features/apt-packages:1": {
|
||||
"ghcr.io/devcontainers-extra/features/apt-packages:1": {
|
||||
"preserve_apt_list": false,
|
||||
"packages": [
|
||||
"libfontconfig1"
|
||||
|
||||
6
.github/workflows/ci-codeql-analysis.yml
vendored
6
.github/workflows/ci-codeql-analysis.yml
vendored
@@ -27,11 +27,11 @@ jobs:
|
||||
dotnet-version: '9.0.x'
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19
|
||||
uses: github/codeql-action/init@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: +security-extended
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19
|
||||
uses: github/codeql-action/autobuild@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19
|
||||
uses: github/codeql-action/analyze@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
|
||||
|
||||
2
.github/workflows/ci-tests.yml
vendored
2
.github/workflows/ci-tests.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
--verbosity minimal
|
||||
|
||||
- name: Merge code coverage results
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@c9576654e2fea2faa7b69e59550b3805bf6a9977 # v5.4.7
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@c1dd332d00304c5aa5d506aab698a5224a8fa24e # 5.4.11
|
||||
with:
|
||||
reports: "**/coverage.cobertura.xml"
|
||||
targetdir: "merged/"
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
- [cryptobank](https://github.com/cryptobank)
|
||||
- [cvium](https://github.com/cvium)
|
||||
- [dannymichel](https://github.com/dannymichel)
|
||||
- [darioackermann](https://github.com/darioackermann)
|
||||
- [DaveChild](https://github.com/DaveChild)
|
||||
- [DavidFair](https://github.com/DavidFair)
|
||||
- [Delgan](https://github.com/Delgan)
|
||||
@@ -60,6 +61,7 @@
|
||||
- [ikomhoog](https://github.com/ikomhoog)
|
||||
- [iwalton3](https://github.com/iwalton3)
|
||||
- [jftuga](https://github.com/jftuga)
|
||||
- [jkhsjdhjs](https://github.com/jkhsjdhjs)
|
||||
- [jmshrv](https://github.com/jmshrv)
|
||||
- [joern-h](https://github.com/joern-h)
|
||||
- [joshuaboniface](https://github.com/joshuaboniface)
|
||||
@@ -195,7 +197,12 @@
|
||||
- [Kenneth Cochran](https://github.com/kennethcochran)
|
||||
- [benedikt257](https://github.com/benedikt257)
|
||||
- [revam](https://github.com/revam)
|
||||
- [Jxiced](https://github.com/Jxiced)
|
||||
- [allesmi](https://github.com/allesmi)
|
||||
- [ThunderClapLP](https://github.com/ThunderClapLP)
|
||||
- [Shoham Peller](https://github.com/spellr)
|
||||
- [theshoeshiner](https://github.com/theshoeshiner)
|
||||
- [TokerX](https://github.com/TokerX)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
|
||||
@@ -9,12 +9,12 @@
|
||||
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
|
||||
<PackageVersion Include="AutoFixture" Version="4.18.1" />
|
||||
<PackageVersion Include="BDInfo" Version="0.8.0" />
|
||||
<PackageVersion Include="BitFaster.Caching" Version="2.5.3" />
|
||||
<PackageVersion Include="BitFaster.Caching" Version="2.5.4" />
|
||||
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.4.0-pre.1" />
|
||||
<PackageVersion Include="BlurHashSharp" Version="1.4.0-pre.1" />
|
||||
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
|
||||
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageVersion Include="Diacritics" Version="3.3.29" />
|
||||
<PackageVersion Include="Diacritics" Version="4.0.17" />
|
||||
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
|
||||
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
|
||||
<PackageVersion Include="FsCheck.Xunit" Version="3.3.0" />
|
||||
@@ -24,44 +24,45 @@
|
||||
<PackageVersion Include="Ignore" Version="0.2.1" />
|
||||
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
|
||||
<PackageVersion Include="libse" Version="4.0.12" />
|
||||
<PackageVersion Include="LrcParser" Version="2025.228.1" />
|
||||
<PackageVersion Include="LrcParser" Version="2025.623.0" />
|
||||
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageVersion Include="MimeTypes" Version="2.5.2" />
|
||||
<PackageVersion Include="Morestachio" Version="5.0.1.631" />
|
||||
<PackageVersion Include="Moq" Version="4.18.4" />
|
||||
<PackageVersion Include="NEbml" Version="0.12.0" />
|
||||
<PackageVersion Include="NEbml" Version="1.0.0.3" />
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageVersion Include="PlaylistsNET" Version="1.4.1" />
|
||||
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
|
||||
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.1" />
|
||||
<PackageVersion Include="prometheus-net" Version="8.2.1" />
|
||||
<PackageVersion Include="Polly" Version="8.5.2" />
|
||||
<PackageVersion Include="Polly" Version="8.6.2" />
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" />
|
||||
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
|
||||
<PackageVersion Include="Serilog.Expressions" Version="5.0.0" />
|
||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="9.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
@@ -69,21 +70,22 @@
|
||||
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
|
||||
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
|
||||
<PackageVersion Include="SharpFuzz" Version="2.2.0" />
|
||||
<PackageVersion Include="SkiaSharp" Version="3.119.0" />
|
||||
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="3.119.0" />
|
||||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.0" />
|
||||
<!-- Pinned to 3.116.1 because https://github.com/jellyfin/jellyfin/pull/14255 -->
|
||||
<PackageVersion Include="SkiaSharp" Version="3.116.1" />
|
||||
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="3.116.1" />
|
||||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.116.1" />
|
||||
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<PackageVersion Include="Svg.Skia" Version="3.0.3" />
|
||||
<PackageVersion Include="Svg.Skia" Version="3.0.4" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
|
||||
<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="9.0.5" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.5" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.5" />
|
||||
<PackageVersion Include="System.Linq.Async" Version="6.0.3" />
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.7" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.7" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.7" />
|
||||
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
||||
<PackageVersion Include="z440.atl.core" Version="6.24.0" />
|
||||
<PackageVersion Include="z440.atl.core" Version="7.2.0" />
|
||||
<PackageVersion Include="TMDbLib" Version="2.2.0" />
|
||||
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
|
||||
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
||||
@@ -91,4 +93,4 @@
|
||||
<PackageVersion Include="Xunit.SkippableFact" Version="1.5.23" />
|
||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -188,7 +188,8 @@ namespace Emby.Naming.Common
|
||||
"disk",
|
||||
"vol",
|
||||
"volume",
|
||||
"part"
|
||||
"part",
|
||||
"act"
|
||||
};
|
||||
|
||||
ArtistSubfolders = new[]
|
||||
@@ -571,6 +572,18 @@ namespace Emby.Naming.Common
|
||||
"trailer",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.Sample,
|
||||
ExtraRuleType.Filename,
|
||||
"sample",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.ThemeSong,
|
||||
ExtraRuleType.Filename,
|
||||
"theme",
|
||||
MediaType.Audio),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.Trailer,
|
||||
ExtraRuleType.Suffix,
|
||||
@@ -592,13 +605,7 @@ namespace Emby.Naming.Common
|
||||
new ExtraRule(
|
||||
ExtraType.Trailer,
|
||||
ExtraRuleType.Suffix,
|
||||
" trailer",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.Sample,
|
||||
ExtraRuleType.Filename,
|
||||
"sample",
|
||||
"- trailer",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
@@ -622,15 +629,9 @@ namespace Emby.Naming.Common
|
||||
new ExtraRule(
|
||||
ExtraType.Sample,
|
||||
ExtraRuleType.Suffix,
|
||||
" sample",
|
||||
"- sample",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.ThemeSong,
|
||||
ExtraRuleType.Filename,
|
||||
"theme",
|
||||
MediaType.Audio),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.Scene,
|
||||
ExtraRuleType.Suffix,
|
||||
|
||||
@@ -97,14 +97,18 @@ namespace Emby.Naming.ExternalFiles
|
||||
|
||||
if (culture is not null && pathInfo.Language is null)
|
||||
{
|
||||
pathInfo.Language = culture.ThreeLetterISOLanguageName;
|
||||
pathInfo.Language = culture.Name.Contains('-', StringComparison.OrdinalIgnoreCase)
|
||||
? culture.Name
|
||||
: culture.ThreeLetterISOLanguageName;
|
||||
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
else if (culture is not null && pathInfo.Language == "hin")
|
||||
{
|
||||
// Hindi language code "hi" collides with a hearing impaired flag - use as Hindi only if no other language is set
|
||||
pathInfo.IsHearingImpaired = true;
|
||||
pathInfo.Language = culture.ThreeLetterISOLanguageName;
|
||||
pathInfo.Language = culture.Name.Contains('-', StringComparison.OrdinalIgnoreCase)
|
||||
? culture.Name
|
||||
: culture.ThreeLetterISOLanguageName;
|
||||
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
else if (_namingOptions.MediaHearingImpairedFlags.Any(s => currentSliceWithoutSeparator.Equals(s, StringComparison.OrdinalIgnoreCase)))
|
||||
|
||||
@@ -22,67 +22,45 @@ namespace Emby.Naming.Video
|
||||
/// <returns>Returns <see cref="ExtraResult"/> object.</returns>
|
||||
public static ExtraResult GetExtraInfo(string path, NamingOptions namingOptions, string? libraryRoot = "")
|
||||
{
|
||||
var result = new ExtraResult();
|
||||
ExtraResult result = new ExtraResult();
|
||||
|
||||
for (var i = 0; i < namingOptions.VideoExtraRules.Length; i++)
|
||||
bool isAudioFile = AudioFileParser.IsAudioFile(path, namingOptions);
|
||||
bool isVideoFile = VideoResolver.IsVideoFile(path, namingOptions);
|
||||
|
||||
ReadOnlySpan<char> pathSpan = path.AsSpan();
|
||||
ReadOnlySpan<char> fileName = Path.GetFileName(pathSpan);
|
||||
ReadOnlySpan<char> fileNameWithoutExtension = Path.GetFileNameWithoutExtension(pathSpan);
|
||||
// Trim the digits from the end of the filename so we can recognize things like -trailer2
|
||||
ReadOnlySpan<char> trimmedFileNameWithoutExtension = fileNameWithoutExtension.TrimEnd(_digits);
|
||||
ReadOnlySpan<char> directoryName = Path.GetFileName(Path.GetDirectoryName(pathSpan));
|
||||
string fullDirectory = Path.GetDirectoryName(pathSpan).ToString();
|
||||
|
||||
foreach (ExtraRule rule in namingOptions.VideoExtraRules)
|
||||
{
|
||||
var rule = namingOptions.VideoExtraRules[i];
|
||||
if ((rule.MediaType == MediaType.Audio && !AudioFileParser.IsAudioFile(path, namingOptions))
|
||||
|| (rule.MediaType == MediaType.Video && !VideoResolver.IsVideoFile(path, namingOptions)))
|
||||
if ((rule.MediaType == MediaType.Audio && !isAudioFile)
|
||||
|| (rule.MediaType == MediaType.Video && !isVideoFile))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var pathSpan = path.AsSpan();
|
||||
if (rule.RuleType == ExtraRuleType.Filename)
|
||||
bool isMatch = rule.RuleType switch
|
||||
{
|
||||
var filename = Path.GetFileNameWithoutExtension(pathSpan);
|
||||
ExtraRuleType.Filename => fileNameWithoutExtension.Equals(rule.Token, StringComparison.OrdinalIgnoreCase),
|
||||
ExtraRuleType.Suffix => trimmedFileNameWithoutExtension.EndsWith(rule.Token, StringComparison.OrdinalIgnoreCase),
|
||||
ExtraRuleType.Regex => Regex.IsMatch(fileName, rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||
ExtraRuleType.DirectoryName => directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(fullDirectory, libraryRoot, StringComparison.OrdinalIgnoreCase),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if (filename.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
result.ExtraType = rule.ExtraType;
|
||||
result.Rule = rule;
|
||||
}
|
||||
}
|
||||
else if (rule.RuleType == ExtraRuleType.Suffix)
|
||||
if (!isMatch)
|
||||
{
|
||||
// Trim the digits from the end of the filename so we can recognize things like -trailer2
|
||||
var filename = Path.GetFileNameWithoutExtension(pathSpan).TrimEnd(_digits);
|
||||
|
||||
if (filename.EndsWith(rule.Token, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
result.ExtraType = rule.ExtraType;
|
||||
result.Rule = rule;
|
||||
}
|
||||
}
|
||||
else if (rule.RuleType == ExtraRuleType.Regex)
|
||||
{
|
||||
var filename = Path.GetFileName(path.AsSpan());
|
||||
|
||||
var isMatch = Regex.IsMatch(filename, rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
if (isMatch)
|
||||
{
|
||||
result.ExtraType = rule.ExtraType;
|
||||
result.Rule = rule;
|
||||
}
|
||||
}
|
||||
else if (rule.RuleType == ExtraRuleType.DirectoryName)
|
||||
{
|
||||
var directoryName = Path.GetFileName(Path.GetDirectoryName(pathSpan));
|
||||
string fullDirectory = Path.GetDirectoryName(pathSpan).ToString();
|
||||
if (directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(fullDirectory, libraryRoot, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
result.ExtraType = rule.ExtraType;
|
||||
result.Rule = rule;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (result.ExtraType is not null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
result.ExtraType = rule.ExtraType;
|
||||
result.Rule = rule;
|
||||
return result;
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -49,7 +49,7 @@ public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IH
|
||||
if (item.IsFileProtocol)
|
||||
{
|
||||
var file = directoryService.GetFile(item.Path);
|
||||
return file is not null && file.LastWriteTimeUtc != item.DateModified;
|
||||
return file is not null && item.HasChanged(file.LastWriteTimeUtc);
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -108,7 +108,7 @@ public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IH
|
||||
var dateTaken = image.ImageTag.DateTime;
|
||||
if (dateTaken.HasValue)
|
||||
{
|
||||
item.DateCreated = dateTaken.Value;
|
||||
item.DateCreated = dateTaken.Value.ToUniversalTime();
|
||||
item.PremiereDate = dateTaken.Value;
|
||||
item.ProductionYear = dateTaken.Value.Year;
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LibraryTaskScheduler;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.Lyrics;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
@@ -552,6 +553,7 @@ namespace Emby.Server.Implementations
|
||||
serviceCollection.AddSingleton<ISessionManager, SessionManager>();
|
||||
|
||||
serviceCollection.AddSingleton<ICollectionManager, CollectionManager>();
|
||||
serviceCollection.AddSingleton<ILimitedConcurrencyLibraryScheduler, LimitedConcurrencyLibraryScheduler>();
|
||||
|
||||
serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
|
||||
|
||||
@@ -650,6 +652,7 @@ namespace Emby.Server.Implementations
|
||||
CollectionFolder.ApplicationHost = this;
|
||||
Folder.UserViewManager = Resolve<IUserViewManager>();
|
||||
Folder.CollectionManager = Resolve<ICollectionManager>();
|
||||
Folder.LimitedConcurrencyLibraryScheduler = Resolve<ILimitedConcurrencyLibraryScheduler>();
|
||||
Episode.MediaEncoder = Resolve<IMediaEncoder>();
|
||||
UserView.TVSeriesManager = Resolve<ITVSeriesManager>();
|
||||
Video.RecordingsManager = Resolve<IRecordingsManager>();
|
||||
|
||||
@@ -1065,7 +1065,12 @@ namespace Emby.Server.Implementations.Dto
|
||||
|
||||
if (options.ContainsField(ItemFields.Trickplay))
|
||||
{
|
||||
dto.Trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult();
|
||||
var trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult();
|
||||
dto.Trickplay = trickplay.ToDictionary(
|
||||
mediaStream => mediaStream.Key,
|
||||
mediaStream => mediaStream.Value.ToDictionary(
|
||||
width => width.Key,
|
||||
width => new TrickplayInfoDto(width.Value)));
|
||||
}
|
||||
|
||||
dto.ExtraType = video.ExtraType;
|
||||
|
||||
@@ -57,7 +57,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
RemoteEndPoint = remoteEndPoint;
|
||||
|
||||
_jsonOptions = JsonDefaults.Options;
|
||||
LastActivityDate = DateTime.Now;
|
||||
LastActivityDate = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -43,13 +43,11 @@ namespace Emby.Server.Implementations.Images
|
||||
protected IImageProcessor ImageProcessor { get; set; }
|
||||
|
||||
protected virtual IReadOnlyCollection<ImageType> SupportedImages { get; }
|
||||
= new ImageType[] { ImageType.Primary };
|
||||
= [ImageType.Primary];
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "Dynamic Image Provider";
|
||||
|
||||
protected virtual int MaxImageAgeDays => 7;
|
||||
|
||||
public int Order => 0;
|
||||
|
||||
protected virtual bool Supports(BaseItem item) => true;
|
||||
@@ -292,8 +290,14 @@ namespace Emby.Server.Implementations.Images
|
||||
|
||||
protected virtual bool HasChangedByDate(BaseItem item, ItemImageInfo image)
|
||||
{
|
||||
var age = DateTime.UtcNow - image.DateModified;
|
||||
return age.TotalDays > MaxImageAgeDays;
|
||||
var path = image.Path;
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
var modificationDate = FileSystem.GetLastWriteTimeUtc(path);
|
||||
return image.DateModified != modificationDate;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected string CreateSingleImage(IEnumerable<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType)
|
||||
|
||||
@@ -38,7 +38,8 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
|
||||
// Don't ignore top level folders
|
||||
if (fileInfo.IsDirectory && parent is AggregateFolder)
|
||||
if (fileInfo.IsDirectory
|
||||
&& (parent is AggregateFolder || (parent?.IsTopParent ?? false)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -48,35 +49,21 @@ namespace Emby.Server.Implementations.Library
|
||||
return true;
|
||||
}
|
||||
|
||||
var filename = fileInfo.Name;
|
||||
if (parent is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fileInfo.IsDirectory)
|
||||
{
|
||||
if (parent is not null)
|
||||
{
|
||||
// Ignore extras for unsupported types
|
||||
if (_namingOptions.AllExtrasTypesFolderNames.ContainsKey(filename)
|
||||
&& parent is not AggregateFolder
|
||||
&& parent is not UserRootFolder)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (parent is not null)
|
||||
{
|
||||
// Don't resolve theme songs
|
||||
if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFileName, StringComparison.Ordinal)
|
||||
&& AudioFileParser.IsAudioFile(filename, _namingOptions))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Ignore extras for unsupported types
|
||||
return _namingOptions.AllExtrasTypesFolderNames.ContainsKey(fileInfo.Name)
|
||||
&& parent is not UserRootFolder;
|
||||
}
|
||||
|
||||
return false;
|
||||
// Don't resolve theme songs
|
||||
return Path.GetFileNameWithoutExtension(fileInfo.Name.AsSpan()).Equals(BaseItem.ThemeSongFileName, StringComparison.Ordinal)
|
||||
&& AudioFileParser.IsAudioFile(fileInfo.Name, _namingOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,19 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
|
||||
/// <returns>True if the file should be ignored.</returns>
|
||||
public static bool IsIgnored(FileSystemMetadata fileInfo, BaseItem? parent)
|
||||
{
|
||||
if (fileInfo.IsDirectory)
|
||||
{
|
||||
var dirIgnoreFile = FindIgnoreFile(new DirectoryInfo(fileInfo.FullName));
|
||||
if (dirIgnoreFile is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// ignore the directory only if the .ignore file is empty
|
||||
// evaluate individual files otherwise
|
||||
return string.IsNullOrWhiteSpace(GetFileContent(dirIgnoreFile));
|
||||
}
|
||||
|
||||
var parentDirPath = Path.GetDirectoryName(fileInfo.FullName);
|
||||
if (string.IsNullOrEmpty(parentDirPath))
|
||||
{
|
||||
@@ -55,13 +68,9 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
|
||||
return false;
|
||||
}
|
||||
|
||||
string ignoreFileString;
|
||||
using (var reader = ignoreFile.OpenText())
|
||||
{
|
||||
ignoreFileString = reader.ReadToEnd();
|
||||
}
|
||||
string ignoreFileString = GetFileContent(ignoreFile);
|
||||
|
||||
if (string.IsNullOrEmpty(ignoreFileString))
|
||||
if (string.IsNullOrWhiteSpace(ignoreFileString))
|
||||
{
|
||||
// Ignore directory if we just have the file
|
||||
return true;
|
||||
@@ -74,4 +83,12 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
|
||||
|
||||
return ignore.IsIgnored(fileInfo.FullName);
|
||||
}
|
||||
|
||||
private static string GetFileContent(FileInfo dirIgnoreFile)
|
||||
{
|
||||
using (var reader = dirIgnoreFile.OpenText())
|
||||
{
|
||||
return reader.ReadToEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
@@ -6,6 +7,7 @@ using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.MediaSegments;
|
||||
using MediaBrowser.Controller.Trickplay;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Library;
|
||||
|
||||
@@ -18,6 +20,7 @@ public class ExternalDataManager : IExternalDataManager
|
||||
private readonly IMediaSegmentManager _mediaSegmentManager;
|
||||
private readonly IPathManager _pathManager;
|
||||
private readonly ITrickplayManager _trickplayManager;
|
||||
private readonly ILogger<ExternalDataManager> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExternalDataManager"/> class.
|
||||
@@ -26,16 +29,19 @@ public class ExternalDataManager : IExternalDataManager
|
||||
/// <param name="mediaSegmentManager">The media segment manager.</param>
|
||||
/// <param name="pathManager">The path manager.</param>
|
||||
/// <param name="trickplayManager">The trickplay manager.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public ExternalDataManager(
|
||||
IKeyframeManager keyframeManager,
|
||||
IMediaSegmentManager mediaSegmentManager,
|
||||
IPathManager pathManager,
|
||||
ITrickplayManager trickplayManager)
|
||||
ITrickplayManager trickplayManager,
|
||||
ILogger<ExternalDataManager> logger)
|
||||
{
|
||||
_keyframeManager = keyframeManager;
|
||||
_mediaSegmentManager = mediaSegmentManager;
|
||||
_pathManager = pathManager;
|
||||
_trickplayManager = trickplayManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -47,7 +53,14 @@ public class ExternalDataManager : IExternalDataManager
|
||||
{
|
||||
foreach (var path in validPaths)
|
||||
{
|
||||
Directory.Delete(path, true);
|
||||
try
|
||||
{
|
||||
Directory.Delete(path, true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning("Unable to prune external item data at {Path}: {Exception}", path, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1954,7 +1954,7 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
try
|
||||
{
|
||||
return _fileSystem.GetLastWriteTimeUtc(image.Path) != image.DateModified;
|
||||
return image.DateModified.Subtract(_fileSystem.GetLastWriteTimeUtc(image.Path)).Duration().TotalSeconds > 1;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -1981,6 +1981,8 @@ namespace Emby.Server.Implementations.Library
|
||||
return;
|
||||
}
|
||||
|
||||
var anyChange = false;
|
||||
|
||||
foreach (var img in outdated)
|
||||
{
|
||||
var image = img;
|
||||
@@ -2012,6 +2014,7 @@ namespace Emby.Server.Implementations.Library
|
||||
try
|
||||
{
|
||||
size = _imageProcessor.GetImageDimensions(item, image);
|
||||
anyChange = image.Width != size.Width || image.Height != size.Height;
|
||||
image.Width = size.Width;
|
||||
image.Height = size.Height;
|
||||
}
|
||||
@@ -2019,23 +2022,29 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
_logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path);
|
||||
size = default;
|
||||
anyChange = image.Width != size.Width || image.Height != size.Height;
|
||||
image.Width = 0;
|
||||
image.Height = 0;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
image.BlurHash = _imageProcessor.GetImageBlurHash(image.Path, size);
|
||||
var blurhash = _imageProcessor.GetImageBlurHash(image.Path, size);
|
||||
anyChange = anyChange || !blurhash.Equals(image.BlurHash, StringComparison.Ordinal);
|
||||
image.BlurHash = blurhash;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Cannot compute blurhash for {ImagePath}", image.Path);
|
||||
anyChange = anyChange || !string.IsNullOrEmpty(image.BlurHash);
|
||||
image.BlurHash = string.Empty;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
image.DateModified = _fileSystem.GetLastWriteTimeUtc(image.Path);
|
||||
var modifiedDate = _fileSystem.GetLastWriteTimeUtc(image.Path);
|
||||
anyChange = anyChange || modifiedDate != image.DateModified;
|
||||
image.DateModified = modifiedDate;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -2043,20 +2052,28 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
_itemRepository.SaveImages(item);
|
||||
if (anyChange)
|
||||
{
|
||||
_itemRepository.SaveImages(item);
|
||||
}
|
||||
|
||||
RegisterItem(item);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
|
||||
{
|
||||
_itemRepository.SaveItems(items, cancellationToken);
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
item.DateLastSaved = DateTime.UtcNow;
|
||||
await RunMetadataSavers(item, updateReason).ConfigureAwait(false);
|
||||
|
||||
// Modify again, so saved value is after write time of externally saved metadata
|
||||
item.DateLastSaved = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
_itemRepository.SaveItems(items, cancellationToken);
|
||||
|
||||
if (ItemUpdated is not null)
|
||||
{
|
||||
foreach (var item in items)
|
||||
@@ -2097,8 +2114,6 @@ namespace Emby.Server.Implementations.Library
|
||||
await ProviderManager.SaveMetadataAsync(item, updateReason).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
item.DateLastSaved = DateTime.UtcNow;
|
||||
|
||||
await UpdateImagesAsync(item, updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -2384,12 +2399,13 @@ namespace Emby.Server.Implementations.Library
|
||||
isNew = true;
|
||||
}
|
||||
|
||||
var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval;
|
||||
var lastRefreshedUtc = item.DateLastRefreshed;
|
||||
var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval;
|
||||
|
||||
if (!refresh && !item.DisplayParentId.IsEmpty())
|
||||
{
|
||||
var displayParent = GetItemById(item.DisplayParentId);
|
||||
refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed;
|
||||
refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc;
|
||||
}
|
||||
|
||||
if (refresh)
|
||||
@@ -2447,12 +2463,13 @@ namespace Emby.Server.Implementations.Library
|
||||
isNew = true;
|
||||
}
|
||||
|
||||
var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval;
|
||||
var lastRefreshedUtc = item.DateLastRefreshed;
|
||||
var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval;
|
||||
|
||||
if (!refresh && !item.DisplayParentId.IsEmpty())
|
||||
{
|
||||
var displayParent = GetItemById(item.DisplayParentId);
|
||||
refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed;
|
||||
refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc;
|
||||
}
|
||||
|
||||
if (refresh)
|
||||
@@ -2522,12 +2539,13 @@ namespace Emby.Server.Implementations.Library
|
||||
item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval;
|
||||
var lastRefreshedUtc = item.DateLastRefreshed;
|
||||
var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval;
|
||||
|
||||
if (!refresh && !item.DisplayParentId.IsEmpty())
|
||||
{
|
||||
var displayParent = GetItemById(item.DisplayParentId);
|
||||
refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed;
|
||||
refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc;
|
||||
}
|
||||
|
||||
if (refresh)
|
||||
@@ -2987,21 +3005,28 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
if (personEntity is null)
|
||||
{
|
||||
var path = Person.GetPath(person.Name);
|
||||
var info = Directory.CreateDirectory(path);
|
||||
var lastWriteTime = info.LastWriteTimeUtc;
|
||||
personEntity = new Person()
|
||||
try
|
||||
{
|
||||
Name = person.Name,
|
||||
Id = GetItemByNameId<Person>(path),
|
||||
DateCreated = info.CreationTimeUtc,
|
||||
DateModified = lastWriteTime,
|
||||
Path = path
|
||||
};
|
||||
var path = Person.GetPath(person.Name);
|
||||
var info = Directory.CreateDirectory(path);
|
||||
personEntity = new Person()
|
||||
{
|
||||
Name = person.Name,
|
||||
Id = GetItemByNameId<Person>(path),
|
||||
DateCreated = info.CreationTimeUtc,
|
||||
DateModified = info.LastWriteTimeUtc,
|
||||
Path = path
|
||||
};
|
||||
|
||||
personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
|
||||
saveEntity = true;
|
||||
createEntity = true;
|
||||
personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
|
||||
saveEntity = true;
|
||||
createEntity = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to create person {Name}", person.Name);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var id in person.ProviderIds)
|
||||
@@ -3035,6 +3060,8 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
|
||||
await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
|
||||
personEntity.DateLastSaved = DateTime.UtcNow;
|
||||
|
||||
CreateItems([personEntity], null, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,7 +379,7 @@ namespace Emby.Server.Implementations.Library
|
||||
var culture = _localizationManager.FindLanguageInfo(language);
|
||||
if (culture is not null)
|
||||
{
|
||||
return culture.ThreeLetterISOLanguageNames;
|
||||
return culture.Name.Contains('-', StringComparison.OrdinalIgnoreCase) ? [culture.Name] : culture.ThreeLetterISOLanguageNames;
|
||||
}
|
||||
|
||||
return [language];
|
||||
|
||||
@@ -140,7 +140,7 @@ namespace Emby.Server.Implementations.Library
|
||||
if (fileCreationDate is not null)
|
||||
{
|
||||
var dateCreated = fileCreationDate;
|
||||
if (dateCreated.Equals(DateTime.MinValue))
|
||||
if (dateCreated == DateTime.MinValue)
|
||||
{
|
||||
dateCreated = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
@@ -462,7 +462,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
{
|
||||
var movie = (T)result.Items[0];
|
||||
movie.IsInMixedFolder = false;
|
||||
movie.Name = Path.GetFileName(movie.ContainingFolderPath);
|
||||
if (collectionType == CollectionType.movies || collectionType is null)
|
||||
{
|
||||
movie.Name = Path.GetFileName(movie.ContainingFolderPath);
|
||||
}
|
||||
|
||||
return movie;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +125,6 @@ public class CollectionPostScanTask : ILibraryPostScanTask
|
||||
boxSet = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions
|
||||
{
|
||||
Name = collectionName,
|
||||
IsLocked = true
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds).ConfigureAwait(false);
|
||||
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskExtractMediaSegments": "فحص مقاطع الوسائط",
|
||||
"TaskExtractMediaSegmentsDescription": "يستخرج مقاطع وسائط من إضافات MediaSegment المُفعّلة.",
|
||||
"TaskMoveTrickplayImages": "تغيير مكان صور المعاينة السريعة",
|
||||
"TaskMoveTrickplayImagesDescription": "تُنقل ملفات التشغيل السريع الحالية بناءً على إعدادات المكتبة."
|
||||
"TaskMoveTrickplayImagesDescription": "تُنقل ملفات التشغيل السريع الحالية بناءً على إعدادات المكتبة.",
|
||||
"CleanupUserDataTask": "مهمة تنظيف بيانات المستخدم",
|
||||
"CleanupUserDataTaskDescription": "مسح جميع بيانات المستخدم (حالة المشاهدة، والحالة المفضلة وما إلى ذلك) من الوسائط التي لم تعد موجودة لمدة 90 يومًا على الأقل."
|
||||
}
|
||||
|
||||
@@ -135,5 +135,7 @@
|
||||
"TaskDownloadMissingLyrics": "Спампаваць зніклыя тэксты песень",
|
||||
"TaskDownloadMissingLyricsDescription": "Спампоўвае тэксты для песень",
|
||||
"TaskExtractMediaSegments": "Сканіраванне медыя-сегмента",
|
||||
"TaskMoveTrickplayImages": "Перанесці месцазнаходжанне выявы Trickplay"
|
||||
"TaskMoveTrickplayImages": "Перанесці месцазнаходжанне выявы Trickplay",
|
||||
"CleanupUserDataTask": "Задача па ачыстцы дадзеных карыстальніка",
|
||||
"CleanupUserDataTaskDescription": "Ачысьціць усе дадзеныя карыстальніка (стан прагляду, абранае і г.д.) для медыяфайлаў, што адсутнічаюць больш за 90 дзён."
|
||||
}
|
||||
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskExtractMediaSegmentsDescription": "Изважда медиини сегменти от MediaSegment плъгини.",
|
||||
"TaskMoveTrickplayImages": "Мигриране на Локацията за Trickplay изображения",
|
||||
"TaskMoveTrickplayImagesDescription": "Премества съществуващите trickplay изображения спрямо настройките на библиотеката.",
|
||||
"TaskExtractMediaSegments": "Сканиране за сегменти"
|
||||
"TaskExtractMediaSegments": "Сканиране за сегменти",
|
||||
"CleanupUserDataTask": "Задача за почистване на потребителски данни",
|
||||
"CleanupUserDataTaskDescription": "Почиства всички потребителски данни (статус на гледане, любими и т.н.) от медия, която вече не е налична от поне 90 дни."
|
||||
}
|
||||
|
||||
@@ -6,29 +6,29 @@
|
||||
"Channels": "চ্যানেলসমূহ",
|
||||
"CameraImageUploadedFrom": "{0} থেকে একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে",
|
||||
"Books": "পুস্তকসমূহ",
|
||||
"AuthenticationSucceededWithUserName": "{0} অনুমোদন সফল",
|
||||
"AuthenticationSucceededWithUserName": "{0} সফলভাবে অথেন্টিকেট করেছেন",
|
||||
"Artists": "শিল্পীগণ",
|
||||
"Application": "অ্যাপ্লিকেশন",
|
||||
"Albums": "অ্যালবামসমূহ",
|
||||
"HeaderFavoriteEpisodes": "প্রিব পর্বগুলো",
|
||||
"HeaderFavoriteEpisodes": "প্রিয় পর্বগুলো",
|
||||
"HeaderFavoriteArtists": "প্রিয় শিল্পীরা",
|
||||
"HeaderFavoriteAlbums": "প্রিয় এলবামগুলো",
|
||||
"HeaderContinueWatching": "দেখতে থাকুন",
|
||||
"HeaderAlbumArtists": "অ্যালবাম শিল্পীবৃন্দ",
|
||||
"Genres": "শৈলীধারাসমূহ",
|
||||
"Genres": "জনরা",
|
||||
"Folders": "ফোল্ডারসমূহ",
|
||||
"Favorites": "পছন্দসমূহ",
|
||||
"FailedLoginAttemptWithUserName": "{0} লগিন করতে ব্যর্থ হয়েছে",
|
||||
"AppDeviceValues": "অ্যাপ: {0}, ডিভাইস: {0}",
|
||||
"AppDeviceValues": "অ্যাপ: {0}, ডিভাইস: {1}",
|
||||
"VersionNumber": "সংস্করণ {0}",
|
||||
"ValueSpecialEpisodeName": "বিশেষ পর্ব - {0}",
|
||||
"ValueHasBeenAddedToLibrary": "আপনার লাইব্রেরিতে {0} যোগ করা হয়েছে",
|
||||
"UserStoppedPlayingItemWithValues": "{2}তে {1} বাজানো শেষ করেছেন {0}",
|
||||
"UserStartedPlayingItemWithValues": "{2}তে {1} বাজাচ্ছেন {0}",
|
||||
"UserStoppedPlayingItemWithValues": "{2}তে {1} প্লে শেষ করেছেন {0}",
|
||||
"UserStartedPlayingItemWithValues": "{2}তে {1} প্লে করেছেন {0}",
|
||||
"UserPolicyUpdatedWithName": "{0} এর জন্য ব্যবহার নীতি আপডেট করা হয়েছে",
|
||||
"UserPasswordChangedWithName": "ব্যবহারকারী {0} এর পাসওয়ার্ড পরিবর্তিত হয়েছে",
|
||||
"UserOnlineFromDevice": "{0}, {1} থেকে অনলাইন",
|
||||
"UserOfflineFromDevice": "{0} {1} থেকে বিযুক্ত হয়ে গেছে",
|
||||
"UserOnlineFromDevice": "{0}, {1} থেকে অনলাইন আছে",
|
||||
"UserOfflineFromDevice": "{0} {1} থেকে বিচ্ছিন্ন হয়ে গেছে",
|
||||
"UserLockedOutWithName": "ব্যবহারকারী {0} ঢুকতে পারছে না",
|
||||
"UserDownloadingItemWithValues": "{0}, {1} ডাউনলোড করছে",
|
||||
"UserDeletedWithName": "ব্যবহারকারী {0}কে বাদ দেয়া হয়েছে",
|
||||
@@ -36,8 +36,8 @@
|
||||
"User": "ব্যবহারকারী",
|
||||
"TvShows": "টিভি শোগুলো",
|
||||
"System": "সিস্টেম",
|
||||
"Sync": "সমলয় স্থাপন",
|
||||
"SubtitleDownloadFailureFromForItem": "{2} থেকে {1} এর জন্য সাবটাইটেল ডাউনলোড ব্যর্থ",
|
||||
"Sync": "সমন্বয় করুন",
|
||||
"SubtitleDownloadFailureFromForItem": "{0} থেকে {1} এর জন্য সাবটাইটেল ডাউনলোড ব্যর্থ হয়েছে",
|
||||
"StartupEmbyServerIsLoading": "জেলিফিন সার্ভার লোড হচ্ছে। দয়া করে একটু পরে আবার চেষ্টা করুন।",
|
||||
"Songs": "সঙ্গীতসমূহ",
|
||||
"Shows": "টিভি পর্ব",
|
||||
@@ -46,18 +46,18 @@
|
||||
"ScheduledTaskFailedWithName": "{0} ব্যর্থ",
|
||||
"ProviderValue": "প্রদানকারী: {0}",
|
||||
"PluginUpdatedWithName": "{0} আপডেট করা হয়েছে",
|
||||
"PluginUninstalledWithName": "{0} বাদ দেয়া হয়েছে",
|
||||
"PluginInstalledWithName": "{0} ইন্সটল করা হয়েছে",
|
||||
"PluginUninstalledWithName": "{0} আনইন্সটল হয়েছে",
|
||||
"PluginInstalledWithName": "{0} ইন্সটল হয়েছে",
|
||||
"Plugin": "প্লাগিন",
|
||||
"Playlists": "প্লে লিস্ট সমূহ",
|
||||
"Photos": "চিত্রসমূহ",
|
||||
"NotificationOptionVideoPlaybackStopped": "ভিডিও চলা বন্ধ",
|
||||
"NotificationOptionVideoPlayback": "ভিডিও চলা শুরু হয়েছে",
|
||||
"Photos": "ছবিসমূহ",
|
||||
"NotificationOptionVideoPlaybackStopped": "ভিডিও বন্ধ হয়েছে",
|
||||
"NotificationOptionVideoPlayback": "ভিডিও শুরু হয়েছে",
|
||||
"NotificationOptionUserLockedOut": "ব্যবহারকারী ঢুকতে পারছে না",
|
||||
"NotificationOptionTaskFailed": "পরিকল্পিত কাজটি ব্যর্থ",
|
||||
"NotificationOptionServerRestartRequired": "সার্ভার রিস্টার্ট বাধ্যতামূলক",
|
||||
"NotificationOptionPluginUpdateInstalled": "প্লাগিন আপডেট ইন্সটল করা হয়েছে",
|
||||
"NotificationOptionPluginUninstalled": "প্লাগিন বাদ দেয়া হয়েছে",
|
||||
"NotificationOptionServerRestartRequired": "সার্ভার রিস্টার্ট করা লাগবে",
|
||||
"NotificationOptionPluginUpdateInstalled": "প্লাগিন আপডেট ইন্সটল হয়েছে",
|
||||
"NotificationOptionPluginUninstalled": "প্লাগিন আনইনষ্টল হয়েছে",
|
||||
"NotificationOptionPluginInstalled": "প্লাগিন ইন্সটল করা হয়েছে",
|
||||
"NotificationOptionPluginError": "প্লাগিন ব্যর্থ",
|
||||
"NotificationOptionNewLibraryContent": "নতুন কন্টেন্ট যোগ করা হয়েছে",
|
||||
@@ -76,8 +76,8 @@
|
||||
"Movies": "চলচ্চিত্রসমূহ",
|
||||
"MixedContent": "মিশ্র কন্টেন্ট",
|
||||
"MessageServerConfigurationUpdated": "সার্ভারের কনফিগারেশন আপডেট করা হয়েছে",
|
||||
"HeaderRecordingGroups": "রেকর্ডিং দল",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "সার্ভারের {0} কনফিগারেসনের অংশ আপডেট করা হয়েছে",
|
||||
"HeaderRecordingGroups": "রেকর্ডিং গ্রুপগুলো",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "সার্ভার কনফিগারেশন সেকশন {0} আপডেট করা হয়েছে",
|
||||
"MessageApplicationUpdatedTo": "জেলিফিন সার্ভার {0} তে আপডেট করা হয়েছে",
|
||||
"MessageApplicationUpdated": "জেলিফিন সার্ভার আপডেট করা হয়েছে",
|
||||
"Latest": "সর্বশেষ",
|
||||
@@ -85,51 +85,57 @@
|
||||
"LabelIpAddressValue": "আইপি এড্রেস: {0}",
|
||||
"ItemRemovedWithName": "{0} লাইব্রেরি থেকে বাদ দেয়া হয়েছে",
|
||||
"ItemAddedWithName": "{0} লাইব্রেরিতে যোগ করা হয়েছে",
|
||||
"Inherit": "থেকে পাওয়া",
|
||||
"Inherit": "মূল থেকে গ্রহণ করুন",
|
||||
"HomeVideos": "হোম ভিডিও",
|
||||
"HeaderNextUp": "এরপরে আসছে",
|
||||
"HeaderLiveTV": "লাইভ টিভি",
|
||||
"HeaderFavoriteSongs": "প্রিয় গানগুলো",
|
||||
"HeaderFavoriteShows": "প্রিয় শোগুলো",
|
||||
"TasksLibraryCategory": "গ্রন্থাগার",
|
||||
"TasksLibraryCategory": "লাইব্রেরি",
|
||||
"TasksMaintenanceCategory": "রক্ষণাবেক্ষণ",
|
||||
"TaskRefreshLibrary": "স্ক্যান মিডিয়া লাইব্রেরি",
|
||||
"TaskRefreshChapterImagesDescription": "অধ্যায়গুলিতে থাকা ভিডিওগুলির জন্য থাম্বনেইল তৈরি ।",
|
||||
"TaskRefreshChapterImages": "অধ্যায়ের চিত্রগুলি বের করুন",
|
||||
"TaskCleanCacheDescription": "সিস্টেমে আর প্রয়োজন নেই ক্যাশ, ফাইলগুলি মুছে ফেলুন।",
|
||||
"TaskRefreshChapterImagesDescription": "যেসব ভিডিওতে চ্যাপ্টার রয়েছে, তাদের জন্য থাম্বনেইল তৈরি করবে।",
|
||||
"TaskRefreshChapterImages": "চ্যাপ্টার ইমেজ বের করুন",
|
||||
"TaskCleanCacheDescription": "সিস্টেমের অপ্রয়োজনীয় ক্যাশ ফাইলগুলো মুছে ফেলবে।",
|
||||
"TaskCleanCache": "ক্লিন ক্যাশ ডিরেক্টরি",
|
||||
"TasksChannelsCategory": "ইন্টারনেট চ্যানেল",
|
||||
"TasksApplicationCategory": "আবেদন",
|
||||
"TasksApplicationCategory": "অ্যাপ্লিকেশন",
|
||||
"TaskDownloadMissingSubtitlesDescription": "মেটাডেটা কনফিগারেশনের উপর ভিত্তি করে অনুপস্থিত সাবটাইটেলগুলির জন্য ইন্টারনেট অনুসন্ধান করে।",
|
||||
"TaskDownloadMissingSubtitles": "অনুপস্থিত সাবটাইটেলগুলি ডাউনলোড করুন",
|
||||
"TaskRefreshChannelsDescription": "ইন্টারনেট চ্যানেল তথ্য রিফ্রেশ করুন।",
|
||||
"TaskRefreshChannels": "চ্যানেল রিফ্রেশ করুন",
|
||||
"TaskCleanTranscodeDescription": "এক দিনেরও বেশি পুরানো ট্রান্সকোড ফাইলগুলি মুছে ফেলুন।",
|
||||
"TaskCleanTranscodeDescription": "এক দিনেরও বেশি পুরানো ট্রান্সকোড ফাইলগুলি মুছে ফেলবে।",
|
||||
"TaskCleanTranscode": "ট্রান্সকোড ডিরেক্টরি ক্লিন করুন",
|
||||
"TaskUpdatePluginsDescription": "স্বয়ংক্রিয়ভাবে আপডেট কনফিগার করা প্লাগইনগুলির জন্য আপডেট ডাউনলোড এবং ইনস্টল করুন।",
|
||||
"TaskUpdatePlugins": "প্লাগইন আপডেট করুন",
|
||||
"TaskRefreshPeopleDescription": "আপনার মিডিয়া লাইব্রেরিতে অভিনেতা এবং পরিচালকদের জন্য মেটাডাটা আপডেট করুন।",
|
||||
"TaskRefreshPeople": "পিপল রিফ্রেশ করুন",
|
||||
"TaskCleanLogsDescription": "{0} দিনের বেশী পুরানো লগ ফাইলগুলি মুছে ফেলুন।",
|
||||
"TaskCleanLogs": "লগ ডিরেক্টরি ক্লিন করুন",
|
||||
"TaskRefreshLibraryDescription": "নতুন ফাইলের জন্য মিডিয়া লাইব্রেরি স্ক্যান এবং মেটাডাটা রিফ্রেশ করুন।",
|
||||
"TaskUpdatePlugins": "আপডেট প্লাগইন",
|
||||
"TaskRefreshPeopleDescription": "আপনার মিডিয়া লাইব্রেরিতে অভিনেতা এবং পরিচালকদের জন্য মেটাডাটা আপডেট করবে।",
|
||||
"TaskRefreshPeople": "ব্যক্তিদের তথ্য রিফ্রেশ",
|
||||
"TaskCleanLogsDescription": "{0} দিনের বেশী পুরানো লগ ফাইলগুলি মুছে ফেলবে।",
|
||||
"TaskCleanLogs": "ক্লিন লগ ডিরেক্টরি",
|
||||
"TaskRefreshLibraryDescription": "নতুন ফাইলের জন্য মিডিয়া লাইব্রেরি স্ক্যান এবং মেটাডাটা রিফ্রেশ করবে।",
|
||||
"Undefined": "অসঙ্গায়িত",
|
||||
"Forced": "জোরকরে",
|
||||
"TaskCleanActivityLogDescription": "নির্ধারিত সময়ের আগের কাজের হিসাব মুছে দিন খালি করুন.",
|
||||
"TaskCleanActivityLog": "কাজের ফাইল খালি করুন",
|
||||
"TaskCleanActivityLogDescription": "নির্ধারিত সময়ের আগের অ্যাক্টিভিটি লগ মুছে দিবে।",
|
||||
"TaskCleanActivityLog": "অ্যাক্টিভিটি লগ মুছুন",
|
||||
"Default": "ডিফল্ট",
|
||||
"HearingImpaired": "দুর্বল শ্রবণক্ষমতাধরদের জন্য",
|
||||
"HearingImpaired": "শ্রবণ প্রতিবন্ধী",
|
||||
"TaskOptimizeDatabaseDescription": "তথ্যভাণ্ডার সুবিন্যস্ত করে ও অব্যবহৃত জায়গা ছেড়ে দেয়। লাইব্রেরী স্ক্যান অথবা যেকোনো তথ্যভাণ্ডার পরিবর্তনের পর এই প্রক্রিয়া চালালে তথ্যভাণ্ডারের তথ্য প্রদান দ্রুততর হতে পারে।",
|
||||
"External": "বাহ্যিক",
|
||||
"TaskOptimizeDatabase": "তথ্যভাণ্ডার সুবিন্যাস",
|
||||
"TaskKeyframeExtractor": "কি-ফ্রেম নিষ্কাশক",
|
||||
"TaskKeyframeExtractorDescription": "ভিডিয়ো থেকে কি-ফ্রেম নিষ্কাশনের মাধ্যমে অধিকতর সঠিক HLS প্লে লিস্ট তৈরী করে। এই প্রক্রিয়া দীর্ঘ সময় ধরে চলতে পারে।",
|
||||
"TaskRefreshTrickplayImages": "ট্রিকপ্লে ইমেজ তৈরি করুন",
|
||||
"TaskRefreshTrickplayImages": "ট্রিকপ্লে ইমেজ তৈরি",
|
||||
"TaskRefreshTrickplayImagesDescription": "সক্ষম লাইব্রেরিতে ভিডিওর জন্য ট্রিকপ্লে প্রিভিউ তৈরি করে।",
|
||||
"TaskDownloadMissingLyricsDescription": "গানের লিরিক্স ডাউনলোড করে",
|
||||
"TaskCleanCollectionsAndPlaylists": "সংগ্রহ এবং প্লেলিস্ট পরিষ্কার করুন",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "সংগ্রহ এবং প্লেলিস্ট থেকে আইটেমগুলি সরিয়ে দেয় যা আর বিদ্যমান নেই।",
|
||||
"TaskCleanCollectionsAndPlaylists": "কালেকশন এবং প্লেলিস্ট পরিষ্কার করুন",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "কালেকশন এবং প্লেলিস্ট থেকে আইটেমগুলি সরিয়ে দেয় যা আর বিদ্যমান নেই।",
|
||||
"TaskExtractMediaSegments": "মিডিয়া সেগমেন্ট স্ক্যান",
|
||||
"TaskExtractMediaSegmentsDescription": "MediaSegment সক্ষম প্লাগইনগুলি থেকে মিডিয়া সেগমেন্টগুলি বের করে বা প্রাপ্ত করে।",
|
||||
"TaskDownloadMissingLyrics": "অনুপস্থিত গান ডাউনলোড করুন"
|
||||
"TaskExtractMediaSegmentsDescription": "মিডিয়া সেগমেন্ট সক্রিয় প্লাগইনগুলি থেকে মিডিয়া সেগমেন্টগুলি বের করে বা প্রাপ্ত করে।",
|
||||
"TaskDownloadMissingLyrics": "অনুপস্থিত গান ডাউনলোড করুন",
|
||||
"TaskMoveTrickplayImagesDescription": "লাইব্রেরির সেটিং অনুযায়ী বিদ্যমান ট্রিকপ্লে ফাইলগুলো সরিয়ে নেবে।",
|
||||
"TaskAudioNormalizationDescription": "অডিও নর্মালাইজেশন তথ্যের জন্য ফাইল স্ক্যান করবে।",
|
||||
"CleanupUserDataTaskDescription": "৯০ দিন বা তার বেশি সময় ধরে অনুপস্থিত মিডিয়া থেকে সকল ব্যবহারকারীর ডেটা (ওয়াচ স্টেট, ফেভারিট স্ট্যাটাস ইত্যাদি) মুছে ফেলবে।",
|
||||
"TaskMoveTrickplayImages": "ট্রিকপ্লে ইমেজের অবস্থান পরিবর্তন",
|
||||
"TaskAudioNormalization": "অডিও নর্মলাইজেশন",
|
||||
"CleanupUserDataTask": "ব্যবহারকারীর ডেটা পরিষ্কারের কাজ"
|
||||
}
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
"DeviceOnlineWithName": "{0} està connectat",
|
||||
"FailedLoginAttemptWithUserName": "Intent de connexió fallit des de {0}",
|
||||
"Favorites": "Preferits",
|
||||
"Folders": "Carpetes",
|
||||
"Folders": "Directoris",
|
||||
"Genres": "Gèneres",
|
||||
"HeaderAlbumArtists": "Artistes de l'àlbum",
|
||||
"HeaderContinueWatching": "Continua veient",
|
||||
"HeaderContinueWatching": "Continueu mirant",
|
||||
"HeaderFavoriteAlbums": "Àlbums preferits",
|
||||
"HeaderFavoriteArtists": "Artistes preferits",
|
||||
"HeaderFavoriteEpisodes": "Episodis preferits",
|
||||
@@ -24,11 +24,11 @@
|
||||
"HeaderFavoriteSongs": "Cançons preferides",
|
||||
"HeaderLiveTV": "TV en directe",
|
||||
"HeaderNextUp": "A continuació",
|
||||
"HeaderRecordingGroups": "Grups Musicals",
|
||||
"HeaderRecordingGroups": "Grups musicals",
|
||||
"HomeVideos": "Vídeos domèstics",
|
||||
"Inherit": "Heretat",
|
||||
"ItemAddedWithName": "{0} s'ha afegit a la biblioteca",
|
||||
"ItemRemovedWithName": "{0} s'ha eliminat de la biblioteca",
|
||||
"ItemAddedWithName": "{0} s'ha afegit a la mediateca",
|
||||
"ItemRemovedWithName": "{0} s'ha eliminat de la mediateca",
|
||||
"LabelIpAddressValue": "Adreça IP: {0}",
|
||||
"LabelRunningTimeValue": "Temps en marxa: {0}",
|
||||
"Latest": "Darrers",
|
||||
@@ -43,7 +43,7 @@
|
||||
"NameInstallFailed": "{0} instal·lació fallida",
|
||||
"NameSeasonNumber": "Temporada {0}",
|
||||
"NameSeasonUnknown": "Temporada desconeguda",
|
||||
"NewVersionIsAvailable": "Una nova versió del servidor de Jellyfin està disponible per a descarregar.",
|
||||
"NewVersionIsAvailable": "Hi ha disponible una versió nova del servidor de Jellyfin per a la descàrrega.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Actualització de l'aplicatiu disponible",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Actualització de l'aplicatiu instal·lada",
|
||||
"NotificationOptionAudioPlayback": "Reproducció d'àudio iniciada",
|
||||
@@ -64,7 +64,7 @@
|
||||
"Playlists": "Llistes de reproducció",
|
||||
"Plugin": "Complement",
|
||||
"PluginInstalledWithName": "{0} ha estat instal·lat",
|
||||
"PluginUninstalledWithName": "S'ha instalat {0}",
|
||||
"PluginUninstalledWithName": "S'ha instal·lat {0}",
|
||||
"PluginUpdatedWithName": "S'ha actualitzat {0}",
|
||||
"ProviderValue": "Proveïdor: {0}",
|
||||
"ScheduledTaskFailedWithName": "{0} ha fallat",
|
||||
@@ -72,10 +72,10 @@
|
||||
"ServerNameNeedsToBeRestarted": "S'ha de reiniciar {0}",
|
||||
"Shows": "Sèries",
|
||||
"Songs": "Cançons",
|
||||
"StartupEmbyServerIsLoading": "El servidor de Jellyfin s'està carregant. Proveu de nou en una estona.",
|
||||
"StartupEmbyServerIsLoading": "El servidor de Jellyfin s'està carregant. Proveu-ho de nou en una estona.",
|
||||
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
||||
"SubtitleDownloadFailureFromForItem": "Els subtítols per a {1} no s'han pogut baixar de {0}",
|
||||
"Sync": "Sincronitzar",
|
||||
"Sync": "Sincronitza",
|
||||
"System": "Sistema",
|
||||
"TvShows": "Sèries de TV",
|
||||
"User": "Usuari",
|
||||
@@ -89,52 +89,54 @@
|
||||
"UserPolicyUpdatedWithName": "La política d'usuari s'ha actualitzat per a {0}",
|
||||
"UserStartedPlayingItemWithValues": "{0} ha començat a reproduir {1} a {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} ha parat de reproduir {1} a {2}",
|
||||
"ValueHasBeenAddedToLibrary": "S'ha afegit {0} a la teva biblioteca",
|
||||
"ValueHasBeenAddedToLibrary": "S'ha afegit {0} a la mediateca",
|
||||
"ValueSpecialEpisodeName": "Especial - {0}",
|
||||
"VersionNumber": "Versió {0}",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Cerca a internet els subtítols que faltin a partir de la configuració de metadades.",
|
||||
"TaskDownloadMissingSubtitles": "Descarrega els subtítols que faltin",
|
||||
"TaskDownloadMissingSubtitles": "Descàrrega dels subtítols que faltin",
|
||||
"TaskRefreshChannelsDescription": "Actualitza la informació dels canals per internet.",
|
||||
"TaskRefreshChannels": "Actualitza els canals",
|
||||
"TaskCleanTranscodeDescription": "Elimina els arxius de transcodificacions que tinguin més d'un dia.",
|
||||
"TaskCleanTranscode": "Neteja les transcodificacions",
|
||||
"TaskUpdatePluginsDescription": "Actualitza els complements que estan configurats per a actualitzar-se automàticament.",
|
||||
"TaskUpdatePlugins": "Actualitza els complements",
|
||||
"TaskRefreshPeopleDescription": "Actualitza les metadades dels actors i directors de la teva biblioteca de mitjans.",
|
||||
"TaskRefreshPeople": "Actualitza les persones",
|
||||
"TaskCleanLogsDescription": "Esborra els logs que tinguin més de {0} dies.",
|
||||
"TaskCleanLogs": "Neteja els registres",
|
||||
"TaskRefreshLibraryDescription": "Escaneja la biblioteca de mitjans buscant fitxers nous i refresca les metadades.",
|
||||
"TaskRefreshLibrary": "Escaneja la biblioteca de mitjans",
|
||||
"TaskRefreshChapterImagesDescription": "Crea les miniatures dels vídeos que tinguin capítols.",
|
||||
"TaskRefreshChapterImages": "Extreure les imatges dels capítols",
|
||||
"TaskCleanCacheDescription": "Elimina la memòria cau no necessària per al servidor.",
|
||||
"TaskCleanCache": "Elimina la memòria cau",
|
||||
"TaskCleanTranscodeDescription": "Elimina els fitxers de transcodificacions que tinguin més d'un dia.",
|
||||
"TaskCleanTranscode": "Neteja de les transcodificacions",
|
||||
"TaskUpdatePluginsDescription": "Descarrega i instal·la els complements que estiguin configurats per a actualitzar-se automàticament.",
|
||||
"TaskUpdatePlugins": "Actualització dels complements",
|
||||
"TaskRefreshPeopleDescription": "Actualització de les metadades dels actors i directors de la mediateca.",
|
||||
"TaskRefreshPeople": "Actualització de les persones",
|
||||
"TaskCleanLogsDescription": "Esborra els registres que tinguin més de {0} dies.",
|
||||
"TaskCleanLogs": "Neteja dels registres",
|
||||
"TaskRefreshLibraryDescription": "Escaneja les mediateques, a la cerca de fitxers nous i refresca les metadades.",
|
||||
"TaskRefreshLibrary": "Escaneig de les mediateques",
|
||||
"TaskRefreshChapterImagesDescription": "Creació de les miniatures dels vídeos que tinguin capítols.",
|
||||
"TaskRefreshChapterImages": "Extracció de les imatges dels capítols",
|
||||
"TaskCleanCacheDescription": "Eliminació de la memòria cau no necessària per al servidor.",
|
||||
"TaskCleanCache": "Eliminació de la memòria cau",
|
||||
"TasksChannelsCategory": "Canals per internet",
|
||||
"TasksApplicationCategory": "Aplicatiu",
|
||||
"TasksLibraryCategory": "Biblioteca",
|
||||
"TasksLibraryCategory": "Mediateca",
|
||||
"TasksMaintenanceCategory": "Manteniment",
|
||||
"TaskCleanActivityLogDescription": "Eliminades les entrades del registre d'activitats més antigues que l'antiguitat configurada.",
|
||||
"TaskCleanActivityLog": "Buidar el registre d'activitat",
|
||||
"TaskCleanActivityLogDescription": "Eliminació de les entrades del registre d'activitats més antigues que l'antiguitat configurada.",
|
||||
"TaskCleanActivityLog": "Buidatge del registre d'activitat",
|
||||
"Undefined": "Indefinit",
|
||||
"Forced": "Forçat",
|
||||
"Default": "Per defecte",
|
||||
"TaskOptimizeDatabaseDescription": "Compacta la base de dades i trunca l'espai lliure. Executar aquesta tasca després d’escanejar la biblioteca o fer altres canvis que impliquin modificacions a la base de dades pot millorar el rendiment.",
|
||||
"TaskOptimizeDatabase": "Optimitzar la base de dades",
|
||||
"TaskKeyframeExtractorDescription": "Extreu fotogrames clau dels fitxers de vídeo per crear llistes de reproducció HLS més precises. Aquesta tasca pot durar molt de temps.",
|
||||
"TaskKeyframeExtractor": "Extractor de fotogrames clau",
|
||||
"TaskOptimizeDatabaseDescription": "Compacta la base de dades i trunca l'espai lliure. Executar aquesta tasca després d’escanejar la mediateca o fer d'altres canvis que impliquin modificacions a la base de dades pot millorar el rendiment.",
|
||||
"TaskOptimizeDatabase": "Optimització de la base de dades",
|
||||
"TaskKeyframeExtractorDescription": "Extracció de fotogrames clau dels fitxers de vídeo per a crear llistes de reproducció HLS més precises. Aquesta tasca pot allargar-se molt en el temps.",
|
||||
"TaskKeyframeExtractor": "Extracció de fotogrames clau",
|
||||
"External": "Extern",
|
||||
"HearingImpaired": "Discapacitat auditiva",
|
||||
"TaskRefreshTrickplayImages": "Generar miniatures de línia de temps",
|
||||
"TaskRefreshTrickplayImagesDescription": "Crear miniatures de línia de temps per vídeos en les biblioteques habilitades.",
|
||||
"TaskRefreshTrickplayImages": "Generació d'imatges de previsualització",
|
||||
"TaskRefreshTrickplayImagesDescription": "Creació d'imatges de previsualització per a vídeos en les mediateques habilitades.",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Esborra elements de col·leccions i llistes de reproducció que ja no existeixen.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Neteja les col·leccions i llistes de reproducció",
|
||||
"TaskAudioNormalization": "Estabilització d'Àudio",
|
||||
"TaskAudioNormalizationDescription": "Escaneja arxius per dades d'estabilització d'àudio.",
|
||||
"TaskDownloadMissingLyricsDescription": "Baixar les lletres de les cançons",
|
||||
"TaskDownloadMissingLyrics": "Baixar les lletres que falten",
|
||||
"TaskCleanCollectionsAndPlaylists": "Neteja de les col·leccions i llistes de reproducció",
|
||||
"TaskAudioNormalization": "Estabilització de l'àudio",
|
||||
"TaskAudioNormalizationDescription": "Escaneja els fitxer per a obtenir dades de normalització de l'àudio.",
|
||||
"TaskDownloadMissingLyricsDescription": "Descàrrega de les lletres de les cançons",
|
||||
"TaskDownloadMissingLyrics": "Descàrrega de les lletres que faltin",
|
||||
"TaskExtractMediaSegments": "Escaneig de segments multimèdia",
|
||||
"TaskExtractMediaSegmentsDescription": "Extreu o obté segments multimèdia usant els connectors MediaSegment activats.",
|
||||
"TaskMoveTrickplayImages": "Migra la ubicació de la imatge de Trickplay",
|
||||
"TaskMoveTrickplayImagesDescription": "Mou els fitxers trickplay existents segons la configuració de la biblioteca."
|
||||
"TaskMoveTrickplayImages": "Migració de la ubicació de la imatge de previsualització",
|
||||
"TaskMoveTrickplayImagesDescription": "Mou els fitxers existents d'imatges de previsualització segons la configuració de la mediateca.",
|
||||
"CleanupUserDataTaskDescription": "Neteja totes les dades d'usuari (estat de la visualització, estat dels preferits, etc.) del contingut multimèdia que no ha estat present durant almenys 90 dies.",
|
||||
"CleanupUserDataTask": "Tasca de neteja de dades d'usuari"
|
||||
}
|
||||
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskExtractMediaSegments": "Skenování segmentů médií",
|
||||
"TaskExtractMediaSegmentsDescription": "Extrahuje či získá segmenty médií pomocí zásuvných modulů MediaSegment.",
|
||||
"TaskMoveTrickplayImages": "Přesunout úložiště obrázků Trickplay",
|
||||
"TaskMoveTrickplayImagesDescription": "Přesune existující soubory Trickplay podle nastavení knihovny."
|
||||
"TaskMoveTrickplayImagesDescription": "Přesune existující soubory Trickplay podle nastavení knihovny.",
|
||||
"CleanupUserDataTaskDescription": "Odstraní všechna uživatelská data (stav zhlédnutí, oblíbené atd.) z médií, které již neexistují více než 90 dní.",
|
||||
"CleanupUserDataTask": "Pročistit uživatelská data"
|
||||
}
|
||||
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskExtractMediaSegments": "Scan for mediesegmenter",
|
||||
"TaskMoveTrickplayImages": "Migrer billedelokationer for trickplay-billeder",
|
||||
"TaskMoveTrickplayImagesDescription": "Flyt eksisterende trickplay-billeder jævnfør biblioteksindstillinger.",
|
||||
"TaskExtractMediaSegmentsDescription": "Udtrækker eller henter mediesegmenter fra plugins som understøtter MediaSegment."
|
||||
"TaskExtractMediaSegmentsDescription": "Udtrækker eller henter mediesegmenter fra plugins som understøtter MediaSegment.",
|
||||
"CleanupUserDataTask": "Brugerdata oprydningsopgave",
|
||||
"CleanupUserDataTaskDescription": "Rydder alle brugerdata (eks. visning- og favoritstatus) fra medier, der har været utilgængelige i mindst 90 dage."
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
"UserStartedPlayingItemWithValues": "{0} hat die Wiedergabe von {1} auf {2} gestartet",
|
||||
"UserStoppedPlayingItemWithValues": "{0} hat die Wiedergabe von {1} auf {2} beendet",
|
||||
"ValueHasBeenAddedToLibrary": "{0} wurde deiner Bibliothek hinzugefügt",
|
||||
"ValueSpecialEpisodeName": "Extra - {0}",
|
||||
"ValueSpecialEpisodeName": "Extra – {0}",
|
||||
"VersionNumber": "Version {0}",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Sucht im Internet basierend auf den Metadaten-Einstellungen nach fehlenden Untertiteln.",
|
||||
"TaskDownloadMissingSubtitles": "Fehlende Untertitel herunterladen",
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskExtractMediaSegments": "Mediensegmente scannen",
|
||||
"TaskExtractMediaSegmentsDescription": "Extrahiert oder empfängt Mediensegmente von Plugins die Mediensegmente nutzen.",
|
||||
"TaskMoveTrickplayImages": "Verzeichnis für Trickplay-Bilder migrieren",
|
||||
"TaskMoveTrickplayImagesDescription": "Trickplay-Bilder werden entsprechend der Bibliothekseinstellungen verschoben."
|
||||
"TaskMoveTrickplayImagesDescription": "Trickplay-Bilder werden entsprechend der Bibliothekseinstellungen verschoben.",
|
||||
"CleanupUserDataTask": "Aufgabe zur Bereinigung von Benutzerdaten",
|
||||
"CleanupUserDataTaskDescription": "Löscht alle Benutzerdaten (Anschaustatus, Favoritenstatus, usw.) von Medien, die seit mindestens 90 Tagen nicht mehr vorhanden sind."
|
||||
}
|
||||
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskExtractMediaSegments": "Media Segment Scan",
|
||||
"TaskExtractMediaSegmentsDescription": "Extracts or obtains media segments from MediaSegment enabled plugins.",
|
||||
"TaskMoveTrickplayImages": "Migrate Trickplay Image Location",
|
||||
"TaskMoveTrickplayImagesDescription": "Moves existing trickplay files according to the library settings."
|
||||
"TaskMoveTrickplayImagesDescription": "Moves existing trickplay files according to the library settings.",
|
||||
"CleanupUserDataTask": "User data cleanup task",
|
||||
"CleanupUserDataTaskDescription": "Cleans all user data (Watch state, favourite status etc) from media that is no longer present for at least 90 days."
|
||||
}
|
||||
|
||||
@@ -135,5 +135,7 @@
|
||||
"TaskExtractMediaSegments": "Media Segment Scan",
|
||||
"TaskExtractMediaSegmentsDescription": "Extracts or obtains media segments from MediaSegment enabled plugins.",
|
||||
"TaskMoveTrickplayImages": "Migrate Trickplay Image Location",
|
||||
"TaskMoveTrickplayImagesDescription": "Moves existing trickplay files according to the library settings."
|
||||
"TaskMoveTrickplayImagesDescription": "Moves existing trickplay files according to the library settings.",
|
||||
"CleanupUserDataTask": "User data cleanup task",
|
||||
"CleanupUserDataTaskDescription": "Cleans all user data (Watch state, favorite status etc) from media that is no longer present for at least 90 days."
|
||||
}
|
||||
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskMoveTrickplayImagesDescription": "Mueve archivos de trickplay existentes según la configuración de la biblioteca.",
|
||||
"TaskExtractMediaSegments": "Escaneo de segmentos de medios",
|
||||
"TaskExtractMediaSegmentsDescription": "Extrae u obtiene segmentos de medios de plugins habilitados para MediaSegment.",
|
||||
"TaskMoveTrickplayImages": "Migrar la ubicación de la imagen de Trickplay"
|
||||
"TaskMoveTrickplayImages": "Migrar la ubicación de la imagen de Trickplay",
|
||||
"CleanupUserDataTask": "Tarea de limpieza de datos del usuario",
|
||||
"CleanupUserDataTaskDescription": "Limpia todos los datos del usuario (estado de visualización, favoritos, etc.) de los medios que ya no están disponibles desde hace al menos 90 días."
|
||||
}
|
||||
|
||||
@@ -135,5 +135,7 @@
|
||||
"TaskExtractMediaSegmentsDescription": "Media segmentuak atera edo lortzen ditu MediaSegment gaituta duten pluginetik.",
|
||||
"TaskMoveTrickplayImages": "Aldatu Trickplay irudien kokalekua",
|
||||
"TaskMoveTrickplayImagesDescription": "Lehendik dauden trickplay fitxategiak liburutegiaren ezarpenen arabera mugitzen dira.",
|
||||
"TaskAudioNormalizationDescription": "Audio normalizazio datuak lortzeko fitxategiak eskaneatzen ditu."
|
||||
"TaskAudioNormalizationDescription": "Audio normalizazio datuak lortzeko fitxategiak eskaneatzen ditu.",
|
||||
"CleanupUserDataTaskDescription": "Gutxienez 90 egunez dagoeneko existitzen ez den multimediatik erabiltzaile-datu guztiak (ikusteko egoera, gogokoen egoera, etab.) garbitzen ditu.",
|
||||
"CleanupUserDataTask": "Erabiltzaileen datuak garbitzeko zeregina"
|
||||
}
|
||||
|
||||
@@ -135,5 +135,7 @@
|
||||
"TaskDownloadMissingLyricsDescription": "Ladataan sanoituksia",
|
||||
"TaskExtractMediaSegmentsDescription": "Poimii tai hankkii mediasegmenttejä MediaSegment-yhteensopivista laajennuksista.",
|
||||
"TaskMoveTrickplayImages": "Siirrä Trickplay-kuvien sijainti",
|
||||
"TaskMoveTrickplayImagesDescription": "Siirtää olemassa olevia trickplay-tiedostoja kirjaston asetusten mukaan."
|
||||
"TaskMoveTrickplayImagesDescription": "Siirtää olemassa olevia trickplay-tiedostoja kirjaston asetusten mukaan.",
|
||||
"CleanupUserDataTask": "Käyttäjätietojen puhdistustehtävä",
|
||||
"CleanupUserDataTaskDescription": "Puhdistaa kaikki käyttäjätiedot (katselutila, suosikit ym.) medioista, joita ei ole ollut saatavilla yli 90 päivään."
|
||||
}
|
||||
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskMoveTrickplayImagesDescription": "Déplace les fichiers trickplay existants en fonction des paramètres de la bibliothèque.",
|
||||
"TaskDownloadMissingLyrics": "Télécharger les paroles des chansons manquantes",
|
||||
"TaskMoveTrickplayImages": "Changer l'emplacement des images Trickplay",
|
||||
"TaskExtractMediaSegmentsDescription": "Extrait ou obtient des segments de média à partir des plugins compatibles avec MediaSegment."
|
||||
"TaskExtractMediaSegmentsDescription": "Extrait ou obtient des segments de média à partir des plugins compatibles avec MediaSegment.",
|
||||
"CleanupUserDataTaskDescription": "Nettoie toutes les données utilisateur (état de la montre, statut favori, etc.) des supports qui ne sont plus présents depuis au moins 90 jours.",
|
||||
"CleanupUserDataTask": "Tâche de nettoyage des données utilisateur"
|
||||
}
|
||||
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskExtractMediaSegments": "Analyse des segments de média",
|
||||
"TaskMoveTrickplayImages": "Changer l'emplacement des images Trickplay",
|
||||
"TaskExtractMediaSegmentsDescription": "Extrait ou obtient des segments de média à partir des plugins compatibles avec MediaSegment.",
|
||||
"TaskMoveTrickplayImagesDescription": "Déplace les fichiers trickplay existants en fonction des paramètres de la bibliothèque."
|
||||
"TaskMoveTrickplayImagesDescription": "Déplace les fichiers trickplay existants en fonction des paramètres de la bibliothèque.",
|
||||
"CleanupUserDataTaskDescription": "Nettoie toutes les données utilisateur (état de la montre, statut favori, etc.) des supports qui ne sont plus présents depuis au moins 90 jours.",
|
||||
"CleanupUserDataTask": "Tâche de nettoyage des données utilisateur"
|
||||
}
|
||||
|
||||
@@ -135,5 +135,7 @@
|
||||
"TaskUpdatePlugins": "Nuashonraigh Breiseáin",
|
||||
"TaskCleanTranscodeDescription": "Scriostar comhaid traschódaithe níos mó ná lá amháin d'aois.",
|
||||
"TaskCleanTranscode": "Eolaire Transcode Glan",
|
||||
"TaskDownloadMissingSubtitles": "Íosluchtaigh fotheidil ar iarraidh"
|
||||
"TaskDownloadMissingSubtitles": "Íosluchtaigh fotheidil ar iarraidh",
|
||||
"CleanupUserDataTask": "Tasc glantacháin sonraí úsáideora",
|
||||
"CleanupUserDataTaskDescription": "Glanann sé gach sonraí úsáideora (stádas faire, stádas is fearr leat srl.) ó mheáin nach bhfuil i láthair a thuilleadh ar feadh 90 lá ar a laghad."
|
||||
}
|
||||
|
||||
@@ -135,5 +135,6 @@
|
||||
"TaskMoveTrickplayImages": "Migrar a localización da imaxe de Trickplay",
|
||||
"TaskMoveTrickplayImagesDescription": "Move os ficheiros de reprodución con trickplay existentes segundo a configuración da biblioteca.",
|
||||
"TaskRefreshTrickplayImages": "Xerar imaxes de Trickplay",
|
||||
"TaskAudioNormalizationDescription": "Analiza ficheiros para obter datos de normalización de audio."
|
||||
"TaskAudioNormalizationDescription": "Analiza ficheiros para obter datos de normalización de audio.",
|
||||
"CleanupUserDataTask": "Tarefa de limpeza de datos do usuario"
|
||||
}
|
||||
|
||||
@@ -32,8 +32,8 @@
|
||||
"LabelIpAddressValue": "Ip כתובת: {0}",
|
||||
"LabelRunningTimeValue": "משך צפייה: {0}",
|
||||
"Latest": "אחרון",
|
||||
"MessageApplicationUpdated": "שרת ג'ליפין עודכן",
|
||||
"MessageApplicationUpdatedTo": "שרת ג'ליפין עודכן לגרסה {0}",
|
||||
"MessageApplicationUpdated": "שרת Jellyfin עודכן",
|
||||
"MessageApplicationUpdatedTo": "שרת Jellyfin עודכן לגרסה {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "סעיף הגדרת השרת {0} עודכן",
|
||||
"MessageServerConfigurationUpdated": "תצורת השרת עודכנה",
|
||||
"MixedContent": "תוכן מעורב",
|
||||
@@ -43,7 +43,7 @@
|
||||
"NameInstallFailed": "התקנת {0} נכשלה",
|
||||
"NameSeasonNumber": "עונה {0}",
|
||||
"NameSeasonUnknown": "עונה לא ידועה",
|
||||
"NewVersionIsAvailable": "גרסה חדשה של שרת ג'ליפין זמינה להורדה.",
|
||||
"NewVersionIsAvailable": "גרסה חדשה של שרת Jellyfin זמינה להורדה.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "קיים עדכון זמין ליישום",
|
||||
"NotificationOptionApplicationUpdateInstalled": "עדכון ליישום הותקן",
|
||||
"NotificationOptionAudioPlayback": "ניגון שמע החל",
|
||||
@@ -72,7 +72,7 @@
|
||||
"ServerNameNeedsToBeRestarted": "{0} דורש הפעלה מחדש",
|
||||
"Shows": "סדרות",
|
||||
"Songs": "שירים",
|
||||
"StartupEmbyServerIsLoading": "שרת ג'ליפין טוען. נא לנסות שוב בקרוב.",
|
||||
"StartupEmbyServerIsLoading": "שרת Jellyfin בתהליך טעינה. נא לנסות שוב בקרוב.",
|
||||
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
||||
"SubtitleDownloadFailureFromForItem": "הורדת כתוביות מ־{0} עבור {1} נכשלה",
|
||||
"Sync": "סנכרון",
|
||||
@@ -100,14 +100,14 @@
|
||||
"TasksLibraryCategory": "ספרייה",
|
||||
"TasksMaintenanceCategory": "תחזוקה",
|
||||
"TaskUpdatePlugins": "עדכן תוספים",
|
||||
"TaskRefreshPeopleDescription": "מעדכן מטא נתונים עבור שחקנים ובמאים בספריית המדיה שלך.",
|
||||
"TaskRefreshPeopleDescription": "מעדכן מטא-דאטה עבור שחקנים ובמאים בספריית המדיה שלך.",
|
||||
"TaskRefreshPeople": "רענן אנשים",
|
||||
"TaskCleanLogsDescription": "מוחק קבצי יומן בני יותר מ- {0} ימים.",
|
||||
"TaskCleanLogs": "ניקוי תיקיית יומן",
|
||||
"TaskRefreshLibraryDescription": "סורק את ספריית המדיה שלך אחר קבצים חדשים ומרענן מטא נתונים.",
|
||||
"TaskRefreshLibraryDescription": "סורק את ספריית המדיה שלך אחר קבצים חדשים ומרענן מטא-דאטה.",
|
||||
"TaskRefreshChapterImagesDescription": "יוצר תמונות ממוזערות לסרטונים שיש להם פרקים.",
|
||||
"TasksChannelsCategory": "ערוצי אינטרנט",
|
||||
"TaskDownloadMissingSubtitlesDescription": "חפש באינטרנט עבור הכתוביות החסרות בהתבסס על המטה-דיאטה.",
|
||||
"TaskDownloadMissingSubtitlesDescription": "חפש באינטרנט כתוביות חסרות בהתבסס על המטא-דאטה.",
|
||||
"TaskDownloadMissingSubtitles": "הורד כתוביות חסרות",
|
||||
"TaskRefreshChannelsDescription": "רענן פרטי ערוץ אינטרנטי.",
|
||||
"TaskRefreshChannels": "רענן ערוץ",
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskMoveTrickplayImages": "העברת מיקום של תמונות Trickplay",
|
||||
"TaskExtractMediaSegments": "סריקת מדיה",
|
||||
"TaskExtractMediaSegmentsDescription": "מחלץ חלקי מדיה מתוספים המאפשרים זאת.",
|
||||
"TaskMoveTrickplayImagesDescription": "הזזת קבצי Trickplay קיימים בהתאם להגדרות הספרייה."
|
||||
"TaskMoveTrickplayImagesDescription": "הזזת קבצי Trickplay קיימים בהתאם להגדרות הספרייה.",
|
||||
"CleanupUserDataTaskDescription": "ניקוי כל המידע של המשתמש (מצב צפייה, מועדפים וכו) ממדיה שאינה קיימת מעל 90 יום.",
|
||||
"CleanupUserDataTask": "משימת ניקוי מידע משתמש"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"Albums": "Albumok",
|
||||
"AppDeviceValues": "Program: {0}, eszköz: {1}",
|
||||
"AppDeviceValues": "Program: {0}, Eszköz: {1}",
|
||||
"Application": "Alkalmazás",
|
||||
"Artists": "Előadók",
|
||||
"AuthenticationSucceededWithUserName": "{0} sikeresen hitelesítve",
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskDownloadMissingLyricsDescription": "Zenék szövegének letöltése",
|
||||
"TaskMoveTrickplayImages": "Trickplay képek helyének átköltöztetése",
|
||||
"TaskMoveTrickplayImagesDescription": "A médiatár-beállításoknak megfelelően áthelyezi a meglévő trickplay fájlokat.",
|
||||
"TaskExtractMediaSegmentsDescription": "Kinyeri vagy megszerzi a médiaszegmenseket a MediaSegment támogatással rendelkező bővítményekből."
|
||||
"TaskExtractMediaSegmentsDescription": "Kinyeri vagy megszerzi a médiaszegmenseket a MediaSegment támogatással rendelkező bővítményekből.",
|
||||
"CleanupUserDataTaskDescription": "Legalább 90 napja nem elérhető médiákhoz kapcsolódó összes felhasználói adat (pl. megtekintési állapot, kedvencek) törlése.",
|
||||
"CleanupUserDataTask": "Felhasználói adatok tisztítása feladat"
|
||||
}
|
||||
|
||||
@@ -129,5 +129,13 @@
|
||||
"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."
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Menghapus item dari koleksi dan daftar putar yang sudah tidak ada.",
|
||||
"TaskDownloadMissingLyricsDescription": "Unduh lirik untuk lagu",
|
||||
"TaskExtractMediaSegmentsDescription": "Mengekstrak atau memperoleh segmen media dari plugin yang mendukung MediaSegment.",
|
||||
"TaskMoveTrickplayImagesDescription": "Memindahkan file trickplay yang sudah ada sesuai dengan pengaturan pustaka.",
|
||||
"CleanupUserDataTaskDescription": "Membersihkan semua data pengguna (status tontonan, status favorit, dll.) dari media yang sudah tidak ada selama setidaknya 90 hari.",
|
||||
"TaskExtractMediaSegments": "Scan Segmen media",
|
||||
"TaskMoveTrickplayImages": "Migrasikan Lokasi Gambar Trickplay",
|
||||
"TaskDownloadMissingLyrics": "Unduh Lirik yang Hilang",
|
||||
"CleanupUserDataTask": "Tugas Pembersihan Data Pengguna"
|
||||
}
|
||||
|
||||
@@ -131,5 +131,8 @@
|
||||
"TaskCleanCollectionsAndPlaylists": "Hreinsa söfn og spilunarlista",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Fjarlægir hluti úr söfnum og spilalistum sem eru ekki lengur til.",
|
||||
"TaskDownloadMissingLyricsDescription": "Sækja söngtexta fyrir lög",
|
||||
"TaskDownloadMissingLyrics": "Sækja söngtexta sem vantar"
|
||||
"TaskDownloadMissingLyrics": "Sækja söngtexta sem vantar",
|
||||
"TaskExtractMediaSegments": "Skönnun efnishluta",
|
||||
"CleanupUserDataTask": "Hreinsun notendagagna",
|
||||
"CleanupUserDataTaskDescription": "Hreinsar öll notendagögn (spilunarstöðu, uppáhöld o.s.frv.) um gögn sem hafa ekki verið til staðar í að lámarki 90 daga."
|
||||
}
|
||||
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskMoveTrickplayImages": "Sposta le immagini Trickplay",
|
||||
"TaskMoveTrickplayImagesDescription": "Sposta le immagini Trickplay esistenti secondo la configurazione della libreria.",
|
||||
"TaskExtractMediaSegmentsDescription": "Estrae o ottiene segmenti multimediali dai plugin abilitati MediaSegment.",
|
||||
"TaskExtractMediaSegments": "Scansiona Segmento Media"
|
||||
"TaskExtractMediaSegments": "Scansiona Segmento Media",
|
||||
"CleanupUserDataTask": "Task di pulizia dei dati utente",
|
||||
"CleanupUserDataTaskDescription": "Pulisce tutti i dati utente (stato di visione, status preferiti, ecc.) dai contenuti non più presenti da almeno 90 giorni."
|
||||
}
|
||||
|
||||
@@ -135,5 +135,7 @@
|
||||
"TaskMoveTrickplayImages": "Trickplayの画像を移動",
|
||||
"TaskMoveTrickplayImagesDescription": "ライブラリ設定によりTrickplayのファイルを移動。",
|
||||
"TaskDownloadMissingLyrics": "失われた歌詞をダウンロード",
|
||||
"TaskExtractMediaSegmentsDescription": "MediaSegment 対応プラグインからメディア セグメントを抽出または取得します。"
|
||||
"TaskExtractMediaSegmentsDescription": "MediaSegment 対応プラグインからメディア セグメントを抽出または取得します。",
|
||||
"CleanupUserDataTask": "ユーザーデータのクリーンアップタスク",
|
||||
"CleanupUserDataTaskDescription": "90日以上存在しないメディアに対して、視聴状態やお気に入り状態などのユーザーデータをすべて削除します。"
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"DeviceOfflineWithName": "{0} ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ",
|
||||
"DeviceOnlineWithName": "{0} ಸಂಪರ್ಕಗೊಂಡಿದೆ",
|
||||
"External": "ಹೊರಗಿನ",
|
||||
"FailedLoginAttemptWithUserName": "{0} ರಿಂದ ವಿಫಲ ಲಾಗಿನ್ ಪ್ರಯತ್ನ",
|
||||
"FailedLoginAttemptWithUserName": "ವಿಫಲ ಲಾಗಿನ್ ಪ್ರಯತ್ನ ಸಂಖ್ಯೆ {0}",
|
||||
"Favorites": "ಮೆಚ್ಚಿನವುಗಳು",
|
||||
"Folders": "ಫೋಲ್ಡರ್ಗಳು",
|
||||
"Forced": "ಬಲವಂತವಾಗಿ",
|
||||
@@ -123,5 +123,13 @@
|
||||
"TaskUpdatePlugins": "ಪ್ಲಗಿನ್ಗಳನ್ನು ನವೀಕರಿಸಿ",
|
||||
"TaskCleanTranscode": "ಟ್ರಾನ್ಸ್ಕೋಡ್ ಡೈರೆಕ್ಟರಿಯನ್ನು ಸ್ವಚ್ಛಗೊಳಿಸಿ",
|
||||
"TaskRefreshChannels": "ಚಾನಲ್ಗಳನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡಿ",
|
||||
"TaskRefreshChannelsDescription": "ಇಂಟರ್ನೆಟ್ ಚಾನಲ್ ಮಾಹಿತಿಯನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ."
|
||||
"TaskRefreshChannelsDescription": "ಇಂಟರ್ನೆಟ್ ಚಾನಲ್ ಮಾಹಿತಿಯನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ.",
|
||||
"TaskAudioNormalizationDescription": "ಧ್ವನಿ ಸಾಮಾನ್ಯೀಕರಣ ಮಾಹಿತಿಗಾಗಿ ಕಡತಗಳನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡುತ್ತದೆ.",
|
||||
"TaskDownloadMissingLyricsDescription": "ಹಾಡುಗಳಿಗೆ ಸಾಹಿತ್ಯ ಪಡೆಯಿರಿ",
|
||||
"TaskExtractMediaSegments": "ಮಾಧ್ಯಮ ವಿಭಾಗದ ಹುಡುಕು",
|
||||
"TaskDownloadMissingLyrics": "ಇಲ್ಲದ ಸಾಹಿತ್ಯವನ್ನು ಪಡೆಯಿರಿ",
|
||||
"TaskAudioNormalization": "ಧ್ವನಿ ಸಾಮಾನ್ಯೀಕರಣ",
|
||||
"TaskRefreshTrickplayImages": "ಟ್ರಿಕ್ಪ್ಲೇ ಚಿತ್ರಗಳನ್ನು ರಚಿಸಿ",
|
||||
"TaskCleanCollectionsAndPlaylists": "ಸಂಗ್ರಹಗಳು ಮತ್ತು ಪ್ಲೇಪಟ್ಟಿಗಳನ್ನು ಸ್ವಚ್ಛಗೊಳಿಸಿ",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "ಇಲ್ಲದ ಸಂಗ್ರಹಗಳು ಮತ್ತು ಪ್ಲೇಪಟ್ಟಿಗಳಿಂದ ವಸ್ತುಗಳನ್ನು ತೆಗೆದುಹಾಕುತ್ತದೆ."
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"CameraImageUploadedFrom": "Nauja nuotrauka įkelta iš kameros {0}",
|
||||
"Channels": "Kanalai",
|
||||
"ChapterNameValue": "Scena{0}",
|
||||
"Collections": "Kolekcijos",
|
||||
"Collections": "Rinkiniai",
|
||||
"DeviceOfflineWithName": "{0} buvo atjungtas",
|
||||
"DeviceOnlineWithName": "{0} prisijungęs",
|
||||
"FailedLoginAttemptWithUserName": "Nesėkmingas {0} bandymas prisijungti",
|
||||
@@ -17,18 +17,18 @@
|
||||
"Genres": "Žanrai",
|
||||
"HeaderAlbumArtists": "Albumo atlikėjai",
|
||||
"HeaderContinueWatching": "Žiūrėti toliau",
|
||||
"HeaderFavoriteAlbums": "Mėgstami Albumai",
|
||||
"HeaderFavoriteArtists": "Mėgstami Atlikėjai",
|
||||
"HeaderFavoriteAlbums": "Mėgstami albumai",
|
||||
"HeaderFavoriteArtists": "Mėgstami atlikėjai",
|
||||
"HeaderFavoriteEpisodes": "Mėgstamiausios serijos",
|
||||
"HeaderFavoriteShows": "Mėgstamiausios TV Laidos",
|
||||
"HeaderFavoriteSongs": "Mėgstamos Dainos",
|
||||
"HeaderLiveTV": "Tiesioginė TV",
|
||||
"HeaderNextUp": "Toliau eilėje",
|
||||
"HeaderNextUp": "Toliau",
|
||||
"HeaderRecordingGroups": "Įrašų grupės",
|
||||
"HomeVideos": "Namų vaizdo įrašai",
|
||||
"Inherit": "Paveldėti",
|
||||
"ItemAddedWithName": "{0} - buvo įkeltas į mediateką",
|
||||
"ItemRemovedWithName": "{0} - buvo pašalinta iš mediatekos",
|
||||
"ItemAddedWithName": "{0} - buvo įkeltas į biblioteką",
|
||||
"ItemRemovedWithName": "{0} - buvo pašalinta iš bibliotekos",
|
||||
"LabelIpAddressValue": "IP adresas: {0}",
|
||||
"LabelRunningTimeValue": "Trukmė: {0}",
|
||||
"Latest": "Naujausi",
|
||||
@@ -36,7 +36,7 @@
|
||||
"MessageApplicationUpdatedTo": "\"Jellyfin Server\" buvo atnaujinta iki {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Serverio nustatymai (skyrius {0}) buvo atnaujinti",
|
||||
"MessageServerConfigurationUpdated": "Serverio nustatymai buvo atnaujinti",
|
||||
"MixedContent": "Mixed content",
|
||||
"MixedContent": "Mišrus turinys",
|
||||
"Movies": "Filmai",
|
||||
"Music": "Muzika",
|
||||
"MusicVideos": "Muzikiniai vaizdo įrašai",
|
||||
@@ -53,21 +53,21 @@
|
||||
"NotificationOptionNewLibraryContent": "Naujas turinys įkeltas",
|
||||
"NotificationOptionPluginError": "Įskiepio klaida",
|
||||
"NotificationOptionPluginInstalled": "Įskiepis įdiegtas",
|
||||
"NotificationOptionPluginUninstalled": "Įskiepis pašalintas",
|
||||
"NotificationOptionPluginUninstalled": "Įskiepis išdiegtas",
|
||||
"NotificationOptionPluginUpdateInstalled": "Įskiepio atnaujinimas įdiegtas",
|
||||
"NotificationOptionServerRestartRequired": "Reikalingas serverio perleidimas",
|
||||
"NotificationOptionTaskFailed": "Suplanuotos užduoties klaida",
|
||||
"NotificationOptionUserLockedOut": "Vartotojas užblokuotas",
|
||||
"NotificationOptionUserLockedOut": "Naudotojas užblokuotas",
|
||||
"NotificationOptionVideoPlayback": "Vaizdo įrašo atkūrimas pradėtas",
|
||||
"NotificationOptionVideoPlaybackStopped": "Vaizdo įrašo atkūrimas sustabdytas",
|
||||
"Photos": "Nuotraukos",
|
||||
"Playlists": "Grojaraštis",
|
||||
"Plugin": "Plugin",
|
||||
"Playlists": "Grojaraščiai",
|
||||
"Plugin": "Įskiepis",
|
||||
"PluginInstalledWithName": "{0} buvo įdiegtas",
|
||||
"PluginUninstalledWithName": "{0} buvo pašalintas",
|
||||
"PluginUpdatedWithName": "{0} buvo atnaujintas",
|
||||
"ProviderValue": "Provider: {0}",
|
||||
"ScheduledTaskFailedWithName": "{0} klaida",
|
||||
"ProviderValue": "Paslaugos tiekėjas: {0}",
|
||||
"ScheduledTaskFailedWithName": "{0} nepavyko",
|
||||
"ScheduledTaskStartedWithName": "{0} paleista",
|
||||
"ServerNameNeedsToBeRestarted": "{0} reikia iš naujo paleisti",
|
||||
"Shows": "Laidos",
|
||||
@@ -76,65 +76,67 @@
|
||||
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
||||
"SubtitleDownloadFailureFromForItem": "{1} subtitrai buvo nesėkmingai parsiųsti iš {0}",
|
||||
"Sync": "Sinchronizuoti",
|
||||
"System": "System",
|
||||
"TvShows": "TV Serialai",
|
||||
"User": "User",
|
||||
"UserCreatedWithName": "Vartotojas {0} buvo sukurtas",
|
||||
"UserDeletedWithName": "Vartotojas {0} ištrintas",
|
||||
"System": "Sistema",
|
||||
"TvShows": "TV laidos",
|
||||
"User": "Naudotojas",
|
||||
"UserCreatedWithName": "Buvo sukurtas {0} naudotojas",
|
||||
"UserDeletedWithName": "Naudotojas {0} ištrintas",
|
||||
"UserDownloadingItemWithValues": "{0} siunčiasi {1}",
|
||||
"UserLockedOutWithName": "Vartotojas {0} užblokuotas",
|
||||
"UserLockedOutWithName": "Naudotojas {0} užblokuotas",
|
||||
"UserOfflineFromDevice": "{0} buvo atjungtas nuo {1}",
|
||||
"UserOnlineFromDevice": "{0} prisijungęs iš {1}",
|
||||
"UserPasswordChangedWithName": "Slaptažodis pakeistas vartotojui {0}",
|
||||
"UserPolicyUpdatedWithName": "Vartotojo {0} teisės buvo pakeistos",
|
||||
"UserPasswordChangedWithName": "Slaptažodis pakeistas naudotojui {0}",
|
||||
"UserPolicyUpdatedWithName": "Naudotojo {0} teisės buvo pakeistos",
|
||||
"UserStartedPlayingItemWithValues": "{0} leidžia {1} į {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} baigė leisti {1} į {2}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} pridėtas į mediateką",
|
||||
"ValueSpecialEpisodeName": "Ypatinga - {0}",
|
||||
"VersionNumber": "Version {0}",
|
||||
"TaskUpdatePluginsDescription": "Atsisiųsti ir įdiegti atnaujinimus priedams kuriem yra nustatytas automatiškas atnaujinimas.",
|
||||
"TaskUpdatePlugins": "Atnaujinti Priedus",
|
||||
"ValueSpecialEpisodeName": "Ypatingų - {0}",
|
||||
"VersionNumber": "Versija {0}",
|
||||
"TaskUpdatePluginsDescription": "Atsisiunčia ir įdiegia įskiepių, kurie sukonfigūruoti atnaujinti automatiškai, naujinius.",
|
||||
"TaskUpdatePlugins": "Atnaujinti įskieius",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Ieško trūkstamų subtitrų internete remiantis metaduomenų konfigūracija.",
|
||||
"TaskCleanTranscodeDescription": "Ištrina dienos senumo perkodavimo failus.",
|
||||
"TaskCleanTranscode": "Išvalyti Perkodavimo Direktorija",
|
||||
"TaskRefreshLibraryDescription": "Ieškoti naujų failų jūsų mediatekoje ir atnaujina metaduomenis.",
|
||||
"TaskRefreshLibrary": "Skenuoti Mediateka",
|
||||
"TaskCleanTranscode": "Išvalyti perkodavimo katalogą",
|
||||
"TaskRefreshLibraryDescription": "Skenuoja medijos biblioteką, ieškodamas naujų failų, ir atnaujina metaduomenis.",
|
||||
"TaskRefreshLibrary": "Skenuoti medijos biblioteką",
|
||||
"TaskDownloadMissingSubtitles": "Atsisiųsti trūkstamus subtitrus",
|
||||
"TaskRefreshChannelsDescription": "Atnaujina internetinių kanalų informaciją.",
|
||||
"TaskRefreshChannels": "Atnaujinti kanalus",
|
||||
"TaskRefreshPeopleDescription": "Atnaujina metaduomenis apie aktorius ir režisierius jūsų mediatekoje.",
|
||||
"TaskRefreshPeople": "Atnaujinti Žmones",
|
||||
"TaskRefreshPeopleDescription": "Atnaujina metaduomenis apie aktorius ir režisierius jūsų medijos bibliotekoje.",
|
||||
"TaskRefreshPeople": "Atnaujinti žmones",
|
||||
"TaskCleanLogsDescription": "Ištrina žurnalo failus kurie yra senesni nei {0} dienos.",
|
||||
"TaskCleanLogs": "Išvalyti Žurnalą",
|
||||
"TaskRefreshChapterImagesDescription": "Sukuria miniatiūras vaizdo įrašam, kurie turi scenas.",
|
||||
"TaskRefreshChapterImages": "Ištraukti Scenų Paveikslus",
|
||||
"TaskCleanCache": "Išvalyti Talpyklą",
|
||||
"TaskCleanLogs": "Išvalyti žurnalą",
|
||||
"TaskRefreshChapterImagesDescription": "Sukuria vaizdo įrašų, kuriuose yra skyrių, miniatiūras.",
|
||||
"TaskRefreshChapterImages": "Ištraukti skyrių vaizdus",
|
||||
"TaskCleanCache": "Išvalyti talpyklą",
|
||||
"TaskCleanCacheDescription": "Ištrina talpyklos failus, kurių daugiau nereikia sistemai.",
|
||||
"TasksChannelsCategory": "Internetiniai Kanalai",
|
||||
"TasksChannelsCategory": "Internetiniai kanalai",
|
||||
"TasksApplicationCategory": "Programa",
|
||||
"TasksLibraryCategory": "Mediateka",
|
||||
"TasksLibraryCategory": "Biblioteka",
|
||||
"TasksMaintenanceCategory": "Priežiūra",
|
||||
"TaskCleanActivityLog": "Išvalyti veiklos žurnalą",
|
||||
"Undefined": "Neapibrėžtas",
|
||||
"Forced": "Priverstas",
|
||||
"Forced": "Priverstinis",
|
||||
"Default": "Numatytas",
|
||||
"TaskCleanActivityLogDescription": "Ištrina veiklos žuranlo įrašus, kurie yra senesni nei nustatytas amžius.",
|
||||
"TaskCleanActivityLogDescription": "Ištrina senesnius nei nustatytas amžius veiklos žurnalo įrašus.",
|
||||
"TaskOptimizeDatabase": "Optimizuoti duomenų bazę",
|
||||
"TaskKeyframeExtractorDescription": "Iš vaizdo įrašo paruošia reikšminius kadrus, kad būtų sukuriamas tikslenis HLS grojaraštis. Šios užduoties vykdymas gali ilgai užtrukti.",
|
||||
"TaskKeyframeExtractor": "Pagrindinių kadrų išgavėjas",
|
||||
"TaskKeyframeExtractor": "Reikšminių kadrų (KeyFrame) išgavėjas",
|
||||
"TaskOptimizeDatabaseDescription": "Suspaudžia duomenų bazę ir atlaisvina vietą. Paleidžiant šią užduotį, po bibliotekos skenavimo arba kitų veiksmų kurie galimai modifikuoja duomenų bazę, gali pagerinti greitaveiką.",
|
||||
"External": "Išorinis",
|
||||
"HearingImpaired": "Su klausos sutrikimais",
|
||||
"TaskRefreshTrickplayImages": "Generuoti Trickplay atvaizdus",
|
||||
"TaskRefreshTrickplayImagesDescription": "Sukuria trickplay peržiūras vaizdo įrašams įgalintose bibliotekose.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Išvalo duomenis kolekcijose ir grojaraščiuose",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Pašalina neegzistuojančius elementus iš kolekcijų ir grojaraščių.",
|
||||
"TaskAudioNormalization": "Garso Normalizavimas",
|
||||
"TaskAudioNormalizationDescription": "Skenuoti garso normalizavimo informacijos failuose.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Išvalo duomenis rinkiniuose ir grojaraščiuose",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Pašalina neegzistuojančius elementus iš rinkinių ir grojaraščių.",
|
||||
"TaskAudioNormalization": "Garso normalizavimas",
|
||||
"TaskAudioNormalizationDescription": "Skenuoja failus, ieškant garso normalizavimo duomenų.",
|
||||
"TaskExtractMediaSegments": "Medijos segmentų nuskaitymas",
|
||||
"TaskDownloadMissingLyrics": "Parsisiųsti trūkstamus dainų tekstus",
|
||||
"TaskExtractMediaSegmentsDescription": "Ištraukia arba gauna medijos segmentus iš MediaSegment ijungtų papildinių.",
|
||||
"TaskExtractMediaSegmentsDescription": "Ištraukia arba gauna medijos segmentus iš MediaSegment ijungtų įskiepių.",
|
||||
"TaskMoveTrickplayImages": "Pakeisti Trickplay vaizdų vietą",
|
||||
"TaskMoveTrickplayImagesDescription": "Perkelia egzistuojančius trickplay failus pagal bibliotekos nustatymus.",
|
||||
"TaskDownloadMissingLyricsDescription": "Parsisiųsti dainų žodžius"
|
||||
"TaskDownloadMissingLyricsDescription": "Parsisiųsti dainų žodžius",
|
||||
"CleanupUserDataTask": "Naudotojo duomenų valymo užduotis",
|
||||
"CleanupUserDataTaskDescription": "Iš medijos, kurios nebėra bent 90 dienų, išvalo visus naudotojo duomenis (žiūrėjimo būseną, mėgstamiausią būseną ir t. t.)."
|
||||
}
|
||||
|
||||
@@ -135,5 +135,7 @@
|
||||
"TaskMoveTrickplayImages": "Trickplay attēlu pārvietošana",
|
||||
"TaskMoveTrickplayImagesDescription": "Pārvieto esošos trickplay failus atbilstoši bibliotēkas iestatījumiem.",
|
||||
"TaskDownloadMissingLyrics": "Lejupielādēt trūkstošos vārdus",
|
||||
"TaskDownloadMissingLyricsDescription": "Lejupielādēt vārdus dziesmām"
|
||||
"TaskDownloadMissingLyricsDescription": "Lejupielādēt vārdus dziesmām",
|
||||
"CleanupUserDataTask": "Lietotāju datu tīrīšanas uzdevums",
|
||||
"CleanupUserDataTaskDescription": "Notīra visus lietotāja datus (skatīšanās stāvokļus, favorītu statusi utt.) no medijiem, kas vairs nav pieejami vismaz 90 dienas."
|
||||
}
|
||||
|
||||
@@ -1,14 +1,141 @@
|
||||
{
|
||||
"Books": "Номууд",
|
||||
"HeaderNextUp": "Дараах",
|
||||
"HeaderNextUp": "Дараа нь",
|
||||
"HeaderContinueWatching": "Үргэлжлүүлэн үзэх",
|
||||
"Songs": "Дуунууд",
|
||||
"Playlists": "Тоглуулах жагсаалт",
|
||||
"Movies": "Кино",
|
||||
"Latest": "Сүүлийн үеийн",
|
||||
"Genres": "Төрөл зүйл",
|
||||
"Genres": "Төрлүүд",
|
||||
"Favorites": "Дуртай",
|
||||
"Collections": "Багц",
|
||||
"Artists": "Зураачуд",
|
||||
"Albums": "Цомгууд"
|
||||
"Artists": "Уран бүтээлчид",
|
||||
"Albums": "Цомгууд",
|
||||
"TaskExtractMediaSegments": "Медиа сегмент шалга",
|
||||
"TaskExtractMediaSegmentsDescription": "MediaSegment идэвхжүүлсэн залгаасуудаас медиа сегментүүдийг задлах эсвэл олж авах.",
|
||||
"TaskMoveTrickplayImages": "Трикплэй зургуудын байршлыг шилжүүлэх",
|
||||
"TaskMoveTrickplayImagesDescription": "Одоогоор байгаа трикплэй файлуудыг сангийн тохиргоонд тохируулан шилжүүлнэ.",
|
||||
"TaskDownloadMissingLyrics": "Алга болсон дууны үгийг татаж авах",
|
||||
"TaskDownloadMissingLyricsDescription": "Дууны үгийг татаж авах",
|
||||
"TaskOptimizeDatabase": "Датабаазыг сайжруулах",
|
||||
"TaskKeyframeExtractor": "Түлхүүр кадр гаргагч",
|
||||
"TaskCleanCache": "Кэш санг цэвэрлэх",
|
||||
"NewVersionIsAvailable": "Jellyfin Server-н шинэ хувилбар татаж авахад нээлттэй боллоо.",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Server-н {0}-р хэсгийн тохиргоо шинэчлэгдлээ",
|
||||
"NotificationOptionAudioPlaybackStopped": "Дууг зогсоов",
|
||||
"NotificationOptionNewLibraryContent": "Шинэ агуулга орлоо",
|
||||
"NotificationOptionServerRestartRequired": "Server-г дахин асаана уу",
|
||||
"NotificationOptionVideoPlaybackStopped": "Бичлэгийг зогсоов",
|
||||
"UserPasswordChangedWithName": "Хэрэглэгч {0}-н нууц үгийг өөрчиллөө",
|
||||
"TaskCleanCollectionsAndPlaylists": "Цуглуулга ба тоглуулах жагсаалтыг цэвэрлэх",
|
||||
"ScheduledTaskFailedWithName": "{0} амжилтгүй",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin Server ачааллаж байна. Хэсэг хугацааны дараа дахин оролдоно уу.",
|
||||
"TaskCleanActivityLog": "Үйл ажиллагааны бүртгэлийг цэвэрлэх",
|
||||
"SubtitleDownloadFailureFromForItem": "{0}-г {1}-д зориулсан хадмал орчуулгыг татаж авч чадсангүй",
|
||||
"TaskRefreshLibraryDescription": "Таны медиа санг шинэ файлуудын хувьд шалгаж, мета мэдээллийг шинэчилнэ.",
|
||||
"UserOfflineFromDevice": "{0}-г {1}-с салгалаа",
|
||||
"ValueHasBeenAddedToLibrary": "{0}-г медиа сан руу нэмэгдлээ",
|
||||
"TaskRefreshPeopleDescription": "Таны медиа санд байгаа жүжигчид болон найруулагчдын мета мэдээллийг шинэчилнэ.",
|
||||
"TaskCleanTranscodeDescription": "Нэг өдрөөс илүү настай транскодлох файлуудыг устгана.",
|
||||
"TaskRefreshChannelsDescription": "Интернет сувгуудын мэдээллийг шинэчлэх.",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Мета мэдээллийн тохиргоонд үндэслэн интернетээс алга болсон дэд гарчгийг хайна.",
|
||||
"TaskOptimizeDatabaseDescription": "Мэдээллийн сантайг шахаж, чөлөөтэй зайг багасгана. Санг шалгаж, мэдээллийн сантай холбоотой өөрчлөлт хийхийн дараа энэ үйлдлийг гүйцэтгэх нь гүйцэтгэлийг сайжруулах боломжтой.",
|
||||
"TaskKeyframeExtractorDescription": "Видео файлуудаас түлхүүр кадруудыг гаргаж, илүү нарийвчилсан HLS тоглуулах жагсаалт үүсгэнэ. Энэ үйлдэл удаан хугацаанд үргэлжлэх боломжтой.",
|
||||
"NotificationOptionAudioPlayback": "Дууг тоглууллаа",
|
||||
"TaskRefreshTrickplayImages": "Трикплэй зургуудыг үүсгэх",
|
||||
"TaskUpdatePlugins": "Plugin-уудыг шинэчлэх",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Одоо байхгүй болсон зүйлсийг цуглуулга ба тоглуулах жагсаалтаас устгана.",
|
||||
"TaskAudioNormalization": "Аудиог хэвшүүлэх",
|
||||
"TaskAudioNormalizationDescription": "Файлуудаас дууны хэвийн хэмжээсийн мэдээллийг шалгана.",
|
||||
"TaskRefreshTrickplayImagesDescription": "Идэвхжсэн сангуудад байгаа видеонуудын трикплэй урьдчилсан харагдацыг үүсгэнэ.",
|
||||
"TaskUpdatePluginsDescription": "Автомат шинэчлэлд тохируулсан залгаасуудын шинэчлэлтийг татаж авч суулгана.",
|
||||
"TaskCleanTranscode": "Транскодлох санг цэвэрлэх",
|
||||
"TaskRefreshChannels": "Сувгуудыг шинэчлэх",
|
||||
"TaskDownloadMissingSubtitles": "Алга болсон хадмал орчуулгыг татах",
|
||||
"External": "Гадны",
|
||||
"HeaderFavoriteArtists": "Дуртай уран бүтээлчид",
|
||||
"HeaderFavoriteEpisodes": "Дуртай ангиуд",
|
||||
"HeaderFavoriteShows": "Дуртай нэвтрүүлэг",
|
||||
"HeaderFavoriteSongs": "Дуртай дуу",
|
||||
"AppDeviceValues": "Aпп: {0}, Төхөөрөмж: {1}",
|
||||
"Application": "Aпп",
|
||||
"AuthenticationSucceededWithUserName": "{0} амжилттай нэвтэрлээ",
|
||||
"CameraImageUploadedFrom": "{0}-с шинэ зураг байршуулагдлаа",
|
||||
"Channels": "Сувгууд",
|
||||
"ChapterNameValue": "{0}-р бүлэг",
|
||||
"Default": "Өгөгдмөл",
|
||||
"DeviceOfflineWithName": "{0}-н холболт саллаа",
|
||||
"DeviceOnlineWithName": "{0} холбогдлоо",
|
||||
"FailedLoginAttemptWithUserName": "{0}-н нэвтрэх оролдлого амжилтгүй",
|
||||
"Folders": "Хавтаснууд",
|
||||
"Forced": "Хүчээр",
|
||||
"HeaderAlbumArtists": "Цомгийн уран бүтээлчид",
|
||||
"HeaderFavoriteAlbums": "Дуртай цомгууд",
|
||||
"HeaderLiveTV": "Шууд",
|
||||
"HeaderRecordingGroups": "Бичлэгийн бүлгүүд",
|
||||
"HearingImpaired": "Сонсголын бэрхшээлтэй",
|
||||
"HomeVideos": "Үндсэн дүрсүүд",
|
||||
"Inherit": "Уламжлах",
|
||||
"ItemAddedWithName": "{0}-г санд нэмлээ",
|
||||
"ItemRemovedWithName": "{0}-с сангаас хаслаа",
|
||||
"LabelIpAddressValue": "IP хаяг: {0}",
|
||||
"LabelRunningTimeValue": "Үргэлжлэх хугацаа: {0}",
|
||||
"MessageApplicationUpdated": "Jellyfin Server шинэчлэгдлээ",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin Server {0} болж шинэчлэгдлээ",
|
||||
"MessageServerConfigurationUpdated": "Server-н тохиргоо шинэчлэгдлээ",
|
||||
"MixedContent": "Холимог агуулга",
|
||||
"Music": "Дуу",
|
||||
"MusicVideos": "Дууны клип",
|
||||
"NameInstallFailed": "{0} суулгахад алдаа гарлаа",
|
||||
"NameSeasonNumber": "{0}-р улирал",
|
||||
"NameSeasonUnknown": "Улирал олдсонгүй",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Апп шинэчлэлт бий болсон байна",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Апп-н шинэчлэлийг суулгалаа",
|
||||
"NotificationOptionCameraImageUploaded": "Камерын зураг орууллаа",
|
||||
"NotificationOptionInstallationFailed": "Суулгалт амжилтгүй",
|
||||
"NotificationOptionPluginError": "Plugin-д алдаа гарлаа",
|
||||
"NotificationOptionPluginInstalled": "Plugin-г суулгалаа",
|
||||
"NotificationOptionPluginUninstalled": "Plugin-г устгалаа",
|
||||
"NotificationOptionPluginUpdateInstalled": "Plugin-ны шинэчлэн суулгалаа",
|
||||
"NotificationOptionTaskFailed": "Товолсон ажил амжилтгүй",
|
||||
"NotificationOptionUserLockedOut": "Хэрэглэгчийг түгжив",
|
||||
"NotificationOptionVideoPlayback": "Бичлэгийг тоглуулж эхлэв",
|
||||
"Photos": "Зургууд",
|
||||
"Plugin": "Plugin",
|
||||
"PluginInstalledWithName": "{0}-г суулгалаа",
|
||||
"PluginUninstalledWithName": "{0}-г устгалаа",
|
||||
"PluginUpdatedWithName": "{0}-г шинэчиллээ",
|
||||
"ProviderValue": "Нийлүүлэгч: {0}",
|
||||
"ScheduledTaskStartedWithName": "{0}-г эхлүүлэв",
|
||||
"ServerNameNeedsToBeRestarted": "{0}-г дахин асаана уу",
|
||||
"Shows": "Нэвтрүүлгүүд",
|
||||
"Sync": "Дахин",
|
||||
"System": "Систем",
|
||||
"TvShows": "ТВ нэвтрүүлгүүд",
|
||||
"Undefined": "Танисангүй",
|
||||
"User": "Хэрэглэгч",
|
||||
"UserCreatedWithName": "Хэрэглэгч {0}-г үүсгэлээ",
|
||||
"UserDeletedWithName": "Хэрэглэгч {0}-г устгалаа",
|
||||
"UserDownloadingItemWithValues": "{0} нь {1}-г татаж байна",
|
||||
"UserLockedOutWithName": "Хэрэглэгч {0}-г түгжлээ",
|
||||
"UserOnlineFromDevice": "{0} нь {1}-тэй холбоотой байна",
|
||||
"UserPolicyUpdatedWithName": "Хэрэглэгчийн журмыг {0}-д зориулан шинэчиллээ",
|
||||
"UserStartedPlayingItemWithValues": "{0}-г {2} дээр {1}-г тоглуулж байна",
|
||||
"UserStoppedPlayingItemWithValues": "{0}-г {2} дээр {1}-г тоглуулж дуусгалаа",
|
||||
"ValueSpecialEpisodeName": "Тусгай - {0}",
|
||||
"VersionNumber": "Хувилбар {0}",
|
||||
"TasksMaintenanceCategory": "Засвар",
|
||||
"TasksLibraryCategory": "Сан",
|
||||
"TasksApplicationCategory": "Апп",
|
||||
"TasksChannelsCategory": "Интернет сувгууд",
|
||||
"TaskCleanActivityLogDescription": "Тохируулсан хугацаанаас хуучин үйл ажиллагааны бүртгэлийн бичлэгүүдийг устгана.",
|
||||
"TaskCleanLogs": "Бүртгэлийн санг цэвэрлэх",
|
||||
"TaskCleanLogsDescription": "{0} өдрөөс илүү настай бүртгэлийн файлуудыг устгана.",
|
||||
"TaskRefreshPeople": "Хүмүүсийг шинэчлэх",
|
||||
"TaskCleanCacheDescription": "Системд хэрэггүй болсон кэш файлуудыг устгана.",
|
||||
"TaskRefreshChapterImages": "Бүлгийн зураг авах",
|
||||
"TaskRefreshChapterImagesDescription": "Бүлгүүдтэй видеонуудын хуудсан зураг үүсгэнэ.",
|
||||
"TaskRefreshLibrary": "Медиа санг шалгах",
|
||||
"CleanupUserDataTask": "Хэрэглэгчийн өгөгдлийн цэвэрлэгээний үүрэг",
|
||||
"CleanupUserDataTaskDescription": "Хугацаа нь 90 хоногоос дээш хугацаанд байхгүй болсон медианаас бүх хэрэглэгчийн өгөгдлийг (үзсэн төлөв, дуртай жагсаалт гэх мэт) цэвэрлэнэ."
|
||||
}
|
||||
|
||||
@@ -130,5 +130,7 @@
|
||||
"TaskExtractMediaSegments": "मिडिया विभाग तपासणी",
|
||||
"TaskMoveTrickplayImages": "ट्रिकप्ले प्रतिमेचे स्थान स्थलांतर करा",
|
||||
"TaskDownloadMissingLyrics": "उपलब्ध नसलेली गीतपट्टी (Lyrics) डाउनलोड करा",
|
||||
"TaskAudioNormalization": "ऑडिओ सामान्यीकरण"
|
||||
"TaskAudioNormalization": "ऑडिओ सामान्यीकरण",
|
||||
"TaskAudioNormalizationDescription": "ऑडिओ सामान्यीकरणाचा डाटा स्कॅन करतो.",
|
||||
"TaskDownloadMissingLyricsDescription": "गाण्यांची गीतपट्टी (Lyrics) डाउनलोड करतो"
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"Albums": "Album",
|
||||
"AppDeviceValues": "Apl: {0}, Peranti: {1}",
|
||||
"AppDeviceValues": "Aplikasi: {0}, Peranti: {1}",
|
||||
"Application": "Aplikasi",
|
||||
"Artists": "Artis-artis",
|
||||
"Artists": "Artis",
|
||||
"AuthenticationSucceededWithUserName": "{0} berjaya disahkan",
|
||||
"Books": "Buku-buku",
|
||||
"Books": "Buku",
|
||||
"CameraImageUploadedFrom": "Gambar baharu telah dimuat naik melalui {0}",
|
||||
"Channels": "Saluran",
|
||||
"ChapterNameValue": "Bab {0}",
|
||||
@@ -99,7 +99,7 @@
|
||||
"TasksMaintenanceCategory": "Penyelenggaraan",
|
||||
"Undefined": "Tidak ditentukan",
|
||||
"Forced": "Dipaksa",
|
||||
"Default": "Lalai",
|
||||
"Default": "Default",
|
||||
"TaskCleanCache": "Bersihkan Direktori Cache",
|
||||
"TaskCleanActivityLogDescription": "Padamkan entri log aktiviti yang lebih tua daripada usia yang dikonfigurasi.",
|
||||
"TaskRefreshPeople": "Segarkan Orang",
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskCleanCollectionsAndPlaylists": "Bersihkan koleksi dan senarai audio video",
|
||||
"TaskAudioNormalization": "Normalisasi Audio",
|
||||
"TaskAudioNormalizationDescription": "Mengimbas fail-fail untuk data normalisasi audio.",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Mengalih keluar item daripada koleksi dan senarai audio video yang tidak wujud lagi."
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Mengalih keluar item daripada koleksi dan senarai audio video yang tidak wujud lagi.",
|
||||
"CleanupUserDataTaskDescription": "Membersihkan semua data pengguna (keadaan tontonan, status kegemaran, dan sebagainya) daripada media yang tidak lagi wujud sekurang-kurangnya selama 90 hari.",
|
||||
"CleanupUserDataTask": "Tugas pembersihan data pengguna"
|
||||
}
|
||||
|
||||
@@ -135,6 +135,6 @@
|
||||
"TaskDownloadMissingLyricsDescription": "Last ned sangtekster",
|
||||
"TaskExtractMediaSegments": "Skann mediasegment",
|
||||
"TaskMoveTrickplayImages": "Migrer bildeplassering for Trickplay",
|
||||
"TaskMoveTrickplayImagesDescription": "Flytter eksisterende Trickplay-filer i henhold til bibliotekseinstillingene.",
|
||||
"TaskMoveTrickplayImagesDescription": "Flytter eksisterende Trickplay-filer i henhold til biblioteksinstillingene.",
|
||||
"TaskExtractMediaSegmentsDescription": "Trekker ut eller henter mediasegmenter fra plugins som støtter MediaSegment."
|
||||
}
|
||||
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskExtractMediaSegmentsDescription": "Verkrijgt mediasegmenten vanuit plug-ins met MediaSegment-ondersteuning.",
|
||||
"TaskMoveTrickplayImages": "Locatie trickplay-afbeeldingen migreren",
|
||||
"TaskMoveTrickplayImagesDescription": "Verplaatst bestaande trickplay-bestanden op basis van de bibliotheekinstellingen.",
|
||||
"TaskExtractMediaSegments": "Scannen op mediasegmenten"
|
||||
"TaskExtractMediaSegments": "Scannen op mediasegmenten",
|
||||
"CleanupUserDataTaskDescription": "Wist alle gebruikersgegevens (kijkstatus, favorieten, etc.) van media die al minstens 90 dagen niet meer aanwezig zijn.",
|
||||
"CleanupUserDataTask": "Opruimtaak gebruikersdata"
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"Genres": "Sjangrar",
|
||||
"Folders": "Mapper",
|
||||
"Favorites": "Favorittar",
|
||||
"FailedLoginAttemptWithUserName": "Mislukka påloggingsforsøk frå {0}",
|
||||
"FailedLoginAttemptWithUserName": "https://betpro-dealers.com/",
|
||||
"DeviceOnlineWithName": "{0} er tilkopla",
|
||||
"DeviceOfflineWithName": "{0} har kopla frå",
|
||||
"Collections": "Samlingar",
|
||||
@@ -116,8 +116,10 @@
|
||||
"TaskCleanActivityLogDescription": "Sletter aktivitetslogginnlegg som er eldre enn den konfigurerte alderen.",
|
||||
"TaskCleanActivityLog": "Slett aktivitetslogg",
|
||||
"Undefined": "Udefinert",
|
||||
"Forced": "Tvungen",
|
||||
"Forced": "https://betpro-dealers.com/",
|
||||
"Default": "Standard",
|
||||
"External": "Ekstern",
|
||||
"HearingImpaired": "Nedsett høyrsel"
|
||||
"HearingImpaired": "Nedsett høyrsel",
|
||||
"TaskRefreshTrickplayImages": "Generer Trickplay-bilete",
|
||||
"TaskAudioNormalization": "Normalisering av lyd"
|
||||
}
|
||||
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskExtractMediaSegments": "Skanowanie segmentów mediów",
|
||||
"TaskMoveTrickplayImages": "Migruj lokalizację obrazu Trickplay",
|
||||
"TaskExtractMediaSegmentsDescription": "Wyodrębnia lub pobiera segmenty mediów z wtyczek obsługujących MediaSegment.",
|
||||
"TaskMoveTrickplayImagesDescription": "Przenosi istniejące pliki Trickplay zgodnie z ustawieniami biblioteki."
|
||||
"TaskMoveTrickplayImagesDescription": "Przenosi istniejące pliki Trickplay zgodnie z ustawieniami biblioteki.",
|
||||
"CleanupUserDataTaskDescription": "Usuwa wszystkie dane użytkownika (stan oglądanych, status ulubionych itp.) z mediów, które nie są dostępne od co najmniej 90 dni.",
|
||||
"CleanupUserDataTask": "Zadanie czyszczenia danych użytkownika"
|
||||
}
|
||||
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskMoveTrickplayImagesDescription": "Move os arquivos do trickplay de acordo com as configurações da biblioteca.",
|
||||
"TaskExtractMediaSegments": "Varredura do segmento de mídia",
|
||||
"TaskExtractMediaSegmentsDescription": "Extrai ou obtém segmentos de mídia de plug-ins habilitados para MediaSegment.",
|
||||
"TaskMoveTrickplayImages": "Migrar o local da imagem do Trickplay"
|
||||
"TaskMoveTrickplayImages": "Migrar o local da imagem do Trickplay",
|
||||
"CleanupUserDataTask": "Tarefa de limpeza de dados do usuário",
|
||||
"CleanupUserDataTaskDescription": "Limpa todos os dados do usuário (estado de visualização, status de favorito, etc.) de mídias que não estão presentes por pelo menos 90 dias."
|
||||
}
|
||||
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskMoveTrickplayImages": "Migrar a localização da imagem do Trickplay",
|
||||
"TaskDownloadMissingLyricsDescription": "Transferir letra para músicas",
|
||||
"TaskExtractMediaSegmentsDescription": "Extrai ou obtém segmentos de multimédia a partir de plugins com suporte para MediaSegment.",
|
||||
"TaskMoveTrickplayImagesDescription": "Move os ficheiros trickplay existentes de acordo com as definições da mediateca."
|
||||
"TaskMoveTrickplayImagesDescription": "Move os ficheiros trickplay existentes de acordo com as definições da mediateca.",
|
||||
"CleanupUserDataTaskDescription": "Apaga todos os dados de utilizador (estados de reprodução, favoritos, etc) de arquivos média não presentes há 90 dias ou mais.",
|
||||
"CleanupUserDataTask": "Limpeza de dados de utilizador"
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
"TaskCleanTranscodeDescription": "Șterge fișierele de transcodare mai vechi de o zi.",
|
||||
"TaskCleanTranscode": "Curățați directorul de transcodare",
|
||||
"TaskUpdatePluginsDescription": "Descarcă și instalează actualizări pentru extensiile care sunt configurate să se actualizeze automat.",
|
||||
"TaskUpdatePlugins": "Actualizați Extensile",
|
||||
"TaskUpdatePlugins": "Actualizați Extensiile",
|
||||
"TaskRefreshPeopleDescription": "Actualizează metadatele pentru actori și regizori din biblioteca media.",
|
||||
"TaskRefreshPeople": "Actualizează Persoanele",
|
||||
"TaskCleanLogsDescription": "Șterge fișierele jurnal care au mai mult de {0} zile.",
|
||||
@@ -135,5 +135,7 @@
|
||||
"TaskExtractMediaSegmentsDescription": "Extrage sau obține segmentele media de la pluginurile MediaSegment activate.",
|
||||
"TaskMoveTrickplayImages": "Migrează locația imaginii Trickplay",
|
||||
"TaskDownloadMissingLyrics": "Descarcă versurile lipsă",
|
||||
"TaskDownloadMissingLyricsDescription": "Descarcă versuri pentru melodii"
|
||||
"TaskDownloadMissingLyricsDescription": "Descarcă versuri pentru melodii",
|
||||
"CleanupUserDataTask": "Sarcina de curatare a datelor utilizatorului",
|
||||
"CleanupUserDataTaskDescription": "Sterge toate datele utilizatorului (starea vizionarii, starea favoritelor etc.) de pe suporturile media care nu mai sunt prezente timp de cel puțin 90 de zile."
|
||||
}
|
||||
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskMoveTrickplayImages": "Перенесение местоположения изображений Trickplay",
|
||||
"TaskExtractMediaSegments": "Сканирование медиасегментов",
|
||||
"TaskExtractMediaSegmentsDescription": "Извлекает или получает медиасегменты из плагинов MediaSegment.",
|
||||
"TaskMoveTrickplayImagesDescription": "Перемещает существующие файлы trickplay в соответствии с настройками медиатеки."
|
||||
"TaskMoveTrickplayImagesDescription": "Перемещает существующие файлы trickplay в соответствии с настройками медиатеки.",
|
||||
"CleanupUserDataTask": "Задача очистки пользовательских данных",
|
||||
"CleanupUserDataTaskDescription": "Очищает все пользовательские данные (состояние просмотра, статус избранного и т.д.) с медиа, отсутствующих по меньшей мере в течение 90 дней."
|
||||
}
|
||||
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskMoveTrickplayImages": "Presunúť umiestnenie obrázkov Trickplay",
|
||||
"TaskMoveTrickplayImagesDescription": "Presunie existujúce súbory Trickplay podľa nastavení knižnice.",
|
||||
"TaskDownloadMissingLyrics": "Stiahnuť chýbajúce texty piesní",
|
||||
"TaskDownloadMissingLyricsDescription": "Stiahne texty pre piesne"
|
||||
"TaskDownloadMissingLyricsDescription": "Stiahne texty pre piesne",
|
||||
"CleanupUserDataTask": "Prečistiť používateľské dáta",
|
||||
"CleanupUserDataTaskDescription": "Vyčistí všetky dáta používateľa (stav sledovania, stav obľúbených atď.) z médií, ktoré už neexistujú aspoň 90 dní."
|
||||
}
|
||||
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskCleanCollectionsAndPlaylists": "Počisti zbirke in sezname predvajanja",
|
||||
"TaskAudioNormalization": "Normalizacija zvoka",
|
||||
"TaskAudioNormalizationDescription": "Pregled datotek za podatke o normalizaciji zvoka.",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Odstrani elemente iz zbirk in seznamov predvajanja, ki ne obstajajo več."
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Odstrani elemente iz zbirk in seznamov predvajanja, ki ne obstajajo več.",
|
||||
"CleanupUserDataTask": "Čiščenje uporabniških podatkov",
|
||||
"CleanupUserDataTaskDescription": "Izbriše vse uporabniške podatke (stanje ogleda, priljubljene itd.) za vsebine, ki že več kot 90 dni niso na voljo."
|
||||
}
|
||||
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskExtractMediaSegments": "Skanning av mediesegment",
|
||||
"TaskExtractMediaSegmentsDescription": "Extraherar eller hämtar ut mediesegmen från tillägg som stöder MediaSegment.",
|
||||
"TaskMoveTrickplayImages": "Migrera platsen för Trickplay-bilder",
|
||||
"TaskMoveTrickplayImagesDescription": "Flyttar befintliga trickplay-filer enligt bibliotekets inställningar."
|
||||
"TaskMoveTrickplayImagesDescription": "Flyttar befintliga trickplay-filer enligt bibliotekets inställningar.",
|
||||
"CleanupUserDataTaskDescription": "Tar bort all användardata (såsom vad du sett, favoriter med mera) för media som inte funnits på enheten på minst 90 dagar.",
|
||||
"CleanupUserDataTask": "Uppgift för rensning av användardata"
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"Inherit": "மரபுரிமையாகப் பெறு",
|
||||
"HeaderRecordingGroups": "பதிவு குழுக்கள்",
|
||||
"Folders": "கோப்புறைகள்",
|
||||
"FailedLoginAttemptWithUserName": "{0} இன் உள்நுழைவு முயற்சி தோல்வியடைந்தது",
|
||||
"FailedLoginAttemptWithUserName": "{0} இலிருந்து உள்நுழைவு முயற்சி தோல்வியடைந்தது",
|
||||
"DeviceOnlineWithName": "{0} இணைக்கப்பட்டது",
|
||||
"DeviceOfflineWithName": "{0} துண்டிக்கப்பட்டது",
|
||||
"Collections": "தொகுப்புகள்",
|
||||
@@ -133,5 +133,9 @@
|
||||
"TaskDownloadMissingLyrics": "விடுபட்ட பாடல் வரிகளைப் பதிவிறக்கவும்",
|
||||
"TaskDownloadMissingLyricsDescription": "பாடல்களுக்கான வரிகளைப் பதிவிறக்குகிறது",
|
||||
"TaskMoveTrickplayImages": "ட்ரிக்பிளே பட இருப்பிடத்தை நகர்த்து",
|
||||
"TaskMoveTrickplayImagesDescription": "நூலக அமைப்புகளுக்கு ஏற்ப ஏற்கனவே உள்ள ட்ரிக்பிளே கோப்புகளை நகர்த்துகிறது."
|
||||
"TaskMoveTrickplayImagesDescription": "நூலக அமைப்புகளுக்கு ஏற்ப ஏற்கனவே உள்ள ட்ரிக்பிளே கோப்புகளை நகர்த்துகிறது.",
|
||||
"TaskExtractMediaSegments": "மீடியா பிரிவு ஸ்கேன்",
|
||||
"TaskExtractMediaSegmentsDescription": "மீடியாசெக்மென்ட் இயக்கப்பட்ட செருகுநிரல்களிலிருந்து மீடியா பிரிவுகளைப் பிரித்தெடுக்கிறது அல்லது பெறுகிறது.",
|
||||
"CleanupUserDataTaskDescription": "குறைந்தது 90 நாட்களுக்கு இல்லாத மீடியாவிலிருந்து அனைத்து பயனர் தரவையும் (கண்காணிப்பு நிலை, பிடித்த நிலை போன்றவை) சுத்தம் செய்கிறது.",
|
||||
"CleanupUserDataTask": "பயனர் தரவை சுத்தம் செய்யும் பணி"
|
||||
}
|
||||
|
||||
@@ -58,11 +58,11 @@
|
||||
"DeviceOnlineWithName": "{0} เชื่อมต่อสำเร็จแล้ว",
|
||||
"DeviceOfflineWithName": "{0} ยกเลิกการเชื่อมต่อแล้ว",
|
||||
"Collections": "คอลเลกชัน",
|
||||
"ChapterNameValue": "บท {0}",
|
||||
"ChapterNameValue": "บทที่ {0}",
|
||||
"Channels": "ช่อง",
|
||||
"CameraImageUploadedFrom": "ภาพถ่ายใหม่ได้ถูกอัปโหลดมาจาก {0}",
|
||||
"Books": "หนังสือ",
|
||||
"AuthenticationSucceededWithUserName": "{0} ยืนยันตัวสำเร็จแล้ว",
|
||||
"AuthenticationSucceededWithUserName": "{0} ยืนยันตัวตนสำเร็จแล้ว",
|
||||
"Artists": "ศิลปิน",
|
||||
"Application": "แอปพลิเคชัน",
|
||||
"AppDeviceValues": "แอป: {0}, อุปกรณ์: {1}",
|
||||
@@ -132,5 +132,8 @@
|
||||
"TaskAudioNormalizationDescription": "สแกนไฟล์เพื่อค้นหาข้อมูลการปรับระดับเสียงให้สม่ำเสมอ",
|
||||
"TaskCleanCollectionsAndPlaylists": "จัดระเบียบคอลเลกชันและเพลย์ลิสต์",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "ลบรายการออกจากคอลเลกชันและเพลย์ลิสต์ที่ไม่มีแล้ว",
|
||||
"TaskExtractMediaSegments": "การสแกนส่วนของสื่อมีเดีย"
|
||||
"TaskExtractMediaSegments": "การสแกนส่วนของสื่อมีเดีย",
|
||||
"TaskMoveTrickplayImagesDescription": "ย้ายไฟล์ Trickplay ตามการตั้งค่าของไลบรารี",
|
||||
"TaskExtractMediaSegmentsDescription": "แยกหรือดึงส่วนของสื่อจากปลั๊กอินที่เปิดใช้งาน MediaSegment",
|
||||
"TaskMoveTrickplayImages": "ย้ายตำแหน่งเก็บภาพตัวอย่าง Trickplay"
|
||||
}
|
||||
|
||||
@@ -98,8 +98,8 @@
|
||||
"TasksLibraryCategory": "Kütüphane",
|
||||
"TasksMaintenanceCategory": "Bakım",
|
||||
"TaskRefreshPeopleDescription": "Medya kütüphanenizdeki videoların oyuncu ve yönetmen bilgilerini günceller.",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Meta veri yapılandırmasına dayalı olarak eksik altyazılar için internette arama yapar.",
|
||||
"TaskDownloadMissingSubtitles": "Eksik altyazıları indir",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Meta veri yapılandırmasına dayalı olarak eksik alt yazılar için internette arama yapar.",
|
||||
"TaskDownloadMissingSubtitles": "Eksik alt yazıları indir",
|
||||
"TaskRefreshChannelsDescription": "Internet kanal bilgilerini yenile.",
|
||||
"TaskRefreshChannels": "Kanalları Yenile",
|
||||
"TaskCleanTranscodeDescription": "Bir günden daha eski kod dönüştürme dosyalarını siler.",
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskMoveTrickplayImagesDescription": "Mevcut trickplay dosyalarını kütüphane ayarlarına göre taşır.",
|
||||
"TaskDownloadMissingLyrics": "Eksik şarkı sözlerini indir",
|
||||
"TaskDownloadMissingLyricsDescription": "Şarkı sözlerini indirir",
|
||||
"TaskExtractMediaSegmentsDescription": "MediaSegment özelliği etkin olan eklentilerden medya segmentlerini çıkarır veya alır."
|
||||
"TaskExtractMediaSegmentsDescription": "MediaSegment özelliği etkin olan eklentilerden medya segmentlerini çıkarır veya alır.",
|
||||
"CleanupUserDataTask": "Kullanıcı verisi temizleme görevi",
|
||||
"CleanupUserDataTaskDescription": "En az 90 gün boyunca artık mevcut olmayan medyadaki tüm kullanıcı verilerini (İzleme durumu, favori durumu vb.) temizler."
|
||||
}
|
||||
|
||||
@@ -135,5 +135,7 @@
|
||||
"TaskMoveTrickplayImagesDescription": "Переміщує наявні Trickplay-зображення відповідно до налаштувань медіатеки.",
|
||||
"TaskExtractMediaSegments": "Сканування медіа-сегментів",
|
||||
"TaskMoveTrickplayImages": "Змінити місце розташування Trickplay-зображень",
|
||||
"TaskExtractMediaSegmentsDescription": "Витягує або отримує медіа-сегменти з плагінів з підтримкою MediaSegment."
|
||||
"TaskExtractMediaSegmentsDescription": "Витягує або отримує медіа-сегменти з плагінів з підтримкою MediaSegment.",
|
||||
"CleanupUserDataTask": "Завдання очищення даних користувача",
|
||||
"CleanupUserDataTaskDescription": "Очищає всі дані користувача (стан перегляду, статус обраного тощо) з медіа, які перестали бути доступними щонайменше 90 днів тому."
|
||||
}
|
||||
|
||||
@@ -135,5 +135,7 @@
|
||||
"TaskExtractMediaSegmentsDescription": "Trích xuất hoặc lấy các phân đoạn phương tiện từ các plugin hỗ trợ MediaSegment.",
|
||||
"TaskMoveTrickplayImages": "Di chuyển vị trí hình ảnh Trickplay",
|
||||
"TaskMoveTrickplayImagesDescription": "Di chuyển các tập tin trickplay hiện có theo cài đặt thư viện.",
|
||||
"TaskExtractMediaSegments": "Quét Phân Đoạn Phương Tiện"
|
||||
"TaskExtractMediaSegments": "Quét Phân Đoạn Phương Tiện",
|
||||
"CleanupUserDataTask": "Tác vụ dọn dẹp dữ liệu người dùng",
|
||||
"CleanupUserDataTaskDescription": "Làm sạch tất cả dữ liệu người dùng (trạng thái xem, trạng thái yêu thích, v.v.) từ phương tiện không còn có mặt trong ít nhất 90 ngày."
|
||||
}
|
||||
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskMoveTrickplayImages": "迁移进度条预览图的存储位置",
|
||||
"TaskExtractMediaSegments": "媒体分段扫描",
|
||||
"TaskExtractMediaSegmentsDescription": "从支持 MediaSegment 的插件中提取或获取媒体分段。",
|
||||
"TaskMoveTrickplayImagesDescription": "根据媒体库设置移动现有的进度条预览图文件。"
|
||||
"TaskMoveTrickplayImagesDescription": "根据媒体库设置移动现有的进度条预览图文件。",
|
||||
"CleanupUserDataTask": "用户数据清理任务",
|
||||
"CleanupUserDataTaskDescription": "清理已被删除超过90天的媒体中的所有用户数据(观看状态、收藏夹状态等)。"
|
||||
}
|
||||
|
||||
@@ -136,5 +136,6 @@
|
||||
"TaskAudioNormalizationDescription": "掃描檔案裏的音訊同等化資料。",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "從資料庫及播放清單中移除已不存在的項目。",
|
||||
"TaskMoveTrickplayImagesDescription": "根據媒體庫設定移動現有的 Trickplay 檔案。",
|
||||
"TaskMoveTrickplayImages": "轉移 Trickplay 影像位置"
|
||||
"TaskMoveTrickplayImages": "轉移 Trickplay 影像位置",
|
||||
"CleanupUserDataTask": "用戶資料清理工作"
|
||||
}
|
||||
|
||||
@@ -5,23 +5,23 @@
|
||||
"Artists": "藝人",
|
||||
"AuthenticationSucceededWithUserName": "成功授權 {0}",
|
||||
"Books": "書籍",
|
||||
"CameraImageUploadedFrom": "已從 {0} 成功上傳一張相片",
|
||||
"CameraImageUploadedFrom": "已從 {0} 成功上傳一張照片",
|
||||
"Channels": "頻道",
|
||||
"ChapterNameValue": "章節 {0}",
|
||||
"Collections": "系列作",
|
||||
"DeviceOfflineWithName": "{0} 已中斷連接",
|
||||
"DeviceOnlineWithName": "{0} 已連接",
|
||||
"FailedLoginAttemptWithUserName": "來自使用者 {0} 的登入失敗嘗試",
|
||||
"FailedLoginAttemptWithUserName": "來自 {0} 的登入失敗嘗試",
|
||||
"Favorites": "我的最愛",
|
||||
"Folders": "資料夾",
|
||||
"Genres": "風格",
|
||||
"HeaderAlbumArtists": "專輯演出者",
|
||||
"HeaderContinueWatching": "繼續觀看",
|
||||
"HeaderFavoriteAlbums": "最愛專輯",
|
||||
"HeaderFavoriteArtists": "最愛藝人",
|
||||
"HeaderFavoriteEpisodes": "最愛劇集",
|
||||
"HeaderFavoriteShows": "最愛節目",
|
||||
"HeaderFavoriteSongs": "最愛歌曲",
|
||||
"HeaderFavoriteArtists": "最愛的藝人",
|
||||
"HeaderFavoriteEpisodes": "最愛的劇集",
|
||||
"HeaderFavoriteShows": "最愛的節目",
|
||||
"HeaderFavoriteSongs": "最愛的歌曲",
|
||||
"HeaderLiveTV": "電視直播",
|
||||
"HeaderNextUp": "接下來",
|
||||
"HomeVideos": "家庭影片",
|
||||
@@ -135,5 +135,7 @@
|
||||
"TaskExtractMediaSegments": "掃描媒體片段",
|
||||
"TaskExtractMediaSegmentsDescription": "從使用媒體片段的擴充功能取得媒體片段。",
|
||||
"TaskMoveTrickplayImages": "遷移快轉縮圖位置",
|
||||
"TaskMoveTrickplayImagesDescription": "根據媒體庫的設定遷移快轉縮圖的檔案。"
|
||||
"TaskMoveTrickplayImagesDescription": "根據媒體庫的設定遷移快轉縮圖的檔案。",
|
||||
"CleanupUserDataTask": "用戶資料清理工作",
|
||||
"CleanupUserDataTaskDescription": "從用戶資料中清除已被刪除超過 90 天的媒體的相關資料。"
|
||||
}
|
||||
|
||||
@@ -128,7 +128,8 @@ namespace Emby.Server.Implementations.Localization
|
||||
}
|
||||
|
||||
string name = parts[3];
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
string displayname = parts[3];
|
||||
if (string.IsNullOrWhiteSpace(displayname))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -138,6 +139,10 @@ namespace Emby.Server.Implementations.Localization
|
||||
{
|
||||
continue;
|
||||
}
|
||||
else if (twoCharName.Contains('-', StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
name = twoCharName;
|
||||
}
|
||||
|
||||
string[] threeLetterNames;
|
||||
if (string.IsNullOrWhiteSpace(parts[1]))
|
||||
@@ -153,7 +158,7 @@ namespace Emby.Server.Implementations.Localization
|
||||
iso6392BtoTdict.TryAdd(parts[1], parts[0]);
|
||||
}
|
||||
|
||||
list.Add(new CultureDto(name, name, twoCharName, threeLetterNames));
|
||||
list.Add(new CultureDto(name, displayname, twoCharName, threeLetterNames));
|
||||
}
|
||||
|
||||
_cultures = list;
|
||||
|
||||
@@ -311,8 +311,8 @@ nia|||Nias|nias
|
||||
nic|||Niger-Kordofanian languages|nigéro-kordofaniennes, langues
|
||||
niu|||Niuean|niué
|
||||
nld|dut|nl|Dutch; Flemish|néerlandais; flamand
|
||||
nno||nn|Norwegian Nynorsk; Nynorsk, Norwegian|norvégien nynorsk; nynorsk, norvégien
|
||||
nob||nb|Bokmål, Norwegian; Norwegian Bokmål|norvégien bokmål
|
||||
nno||nn|Norwegian (Nynorsk)|norvégien (nynorsk)
|
||||
nob||nb|Norwegian (Bokmal)|norvégien (bokmål)
|
||||
nog|||Nogai|nogaï; nogay
|
||||
non|||Norse, Old|norrois, vieux
|
||||
nor||no|Norwegian|norvégien
|
||||
@@ -373,7 +373,7 @@ sam|||Samaritan Aramaic|samaritain
|
||||
san||sa|Sanskrit|sanskrit
|
||||
sas|||Sasak|sasak
|
||||
sat|||Santali|santal
|
||||
scc|srp|sr|Serbian|serbe
|
||||
srp||sr|Serbian|serbe
|
||||
scn|||Sicilian|sicilien
|
||||
sco|||Scots|écossais
|
||||
sel|||Selkup|selkoupe
|
||||
@@ -391,10 +391,10 @@ slv||sl|Slovenian|slovène
|
||||
sma|||Southern Sami|sami du Sud
|
||||
sme||se|Northern Sami|sami du Nord
|
||||
smi|||Sami languages|sames, langues
|
||||
smj|||Lule Sami|sami de Lule
|
||||
smn|||Inari Sami|sami d'Inari
|
||||
smj|||Sami (Lule)|sami de Lule
|
||||
smn|||Sami (Inari)|sami d'Inari
|
||||
smo||sm|Samoan|samoan
|
||||
sms|||Skolt Sami|sami skolt
|
||||
sms|||Sami (Skolt)|sami skolt
|
||||
sna||sn|Shona|shona
|
||||
snd||sd|Sindhi|sindhi
|
||||
snk|||Soninke|soninké
|
||||
@@ -483,9 +483,12 @@ zen|||Zenaga|zenaga
|
||||
zgh|||Standard Moroccan Tamazight|amazighe standard marocain
|
||||
zha||za|Zhuang; Chuang|zhuang; chuang
|
||||
zho|chi|zh|Chinese|chinois
|
||||
zho|chi|ze|Chinese; Bilingual|chinois
|
||||
zho|chi|zh-tw|Chinese; Traditional|chinois
|
||||
zho|chi|zh-hk|Chinese; Hong Kong|chinois
|
||||
zho|chi|ze|Chinese (Bilingual)|chinois
|
||||
zho|chi|zh-cn|Chinese (Simplified)|chinois
|
||||
zho|chi|zh-hans|Chinese (Simplified)|chinois
|
||||
zho|chi|zh-tw|Chinese (Traditional)|chinois
|
||||
zho|chi|zh-hant|Chinese (Traditional)|chinois
|
||||
zho|chi|zh-hk|Chinese (Hong Kong)|chinois
|
||||
znd|||Zande languages|zandé, langues
|
||||
zul||zu|Zulu|zoulou
|
||||
zun|||Zuni|zuni
|
||||
|
||||
@@ -423,7 +423,7 @@ namespace Emby.Server.Implementations.Plugins
|
||||
Overview = packageInfo.Overview,
|
||||
Owner = packageInfo.Owner,
|
||||
TargetAbi = versionInfo.TargetAbi ?? string.Empty,
|
||||
Timestamp = string.IsNullOrEmpty(versionInfo.Timestamp) ? DateTime.MinValue : DateTime.Parse(versionInfo.Timestamp, CultureInfo.InvariantCulture),
|
||||
Timestamp = string.IsNullOrEmpty(versionInfo.Timestamp) ? DateTime.MinValue : DateTime.Parse(versionInfo.Timestamp, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal),
|
||||
Version = versionInfo.Version,
|
||||
Status = status == PluginStatus.Disabled ? PluginStatus.Disabled : PluginStatus.Active, // Keep disabled state.
|
||||
AutoUpdate = true,
|
||||
|
||||
@@ -76,81 +76,98 @@ public partial class AudioNormalizationTask : IScheduledTask
|
||||
/// <inheritdoc />
|
||||
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var library in _libraryManager.RootFolder.Children)
|
||||
{
|
||||
var libraryOptions = _libraryManager.GetLibraryOptions(library);
|
||||
if (!libraryOptions.EnableLUFSScan)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var numComplete = 0;
|
||||
var libraries = _libraryManager.RootFolder.Children.Where(library => _libraryManager.GetLibraryOptions(library).EnableLUFSScan).ToArray();
|
||||
double percent = 0;
|
||||
|
||||
// Album gain
|
||||
var albums = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = [BaseItemKind.MusicAlbum],
|
||||
Parent = library,
|
||||
Recursive = true
|
||||
});
|
||||
foreach (var library in libraries)
|
||||
{
|
||||
var albums = _libraryManager.GetItemList(new InternalItemsQuery { IncludeItemTypes = [BaseItemKind.MusicAlbum], Parent = library, Recursive = true });
|
||||
|
||||
double nextPercent = numComplete + 1;
|
||||
nextPercent /= libraries.Length;
|
||||
nextPercent -= percent;
|
||||
// Split the progress for this single library into two halves: album gain and track gain.
|
||||
// The first half will be for album gain, the second half for track gain.
|
||||
nextPercent /= 2;
|
||||
var albumComplete = 0;
|
||||
|
||||
foreach (var a in albums)
|
||||
{
|
||||
if (a.NormalizationGain.HasValue || a.LUFS.HasValue)
|
||||
if (!a.NormalizationGain.HasValue && !a.LUFS.HasValue)
|
||||
{
|
||||
continue;
|
||||
// Album gain
|
||||
var albumTracks = ((MusicAlbum)a).Tracks.Where(x => x.IsFileProtocol).ToList();
|
||||
|
||||
// Skip albums that don't have multiple tracks, album gain is useless here
|
||||
if (albumTracks.Count > 1)
|
||||
{
|
||||
_logger.LogInformation("Calculating LUFS for album: {Album} with id: {Id}", a.Name, a.Id);
|
||||
var tempDir = _applicationPaths.TempDirectory;
|
||||
Directory.CreateDirectory(tempDir);
|
||||
var tempFile = Path.Join(tempDir, 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);
|
||||
try
|
||||
{
|
||||
a.LUFS = await CalculateLUFSAsync(
|
||||
string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile),
|
||||
OperatingSystem.IsWindows(), // Wait for process to exit on Windows before we try deleting the concat file
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(tempFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Skip albums that don't have multiple tracks, album gain is useless here
|
||||
var albumTracks = ((MusicAlbum)a).Tracks.Where(x => x.IsFileProtocol).ToList();
|
||||
if (albumTracks.Count <= 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// Update sub-progress for album gain
|
||||
albumComplete++;
|
||||
double albumPercent = albumComplete;
|
||||
albumPercent /= albums.Count;
|
||||
|
||||
_logger.LogInformation("Calculating LUFS for album: {Album} with id: {Id}", a.Name, a.Id);
|
||||
var tempDir = _applicationPaths.TempDirectory;
|
||||
Directory.CreateDirectory(tempDir);
|
||||
var tempFile = Path.Join(tempDir, 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);
|
||||
try
|
||||
{
|
||||
a.LUFS = await CalculateLUFSAsync(
|
||||
string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile),
|
||||
OperatingSystem.IsWindows(), // Wait for process to exit on Windows before we try deleting the concat file
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(tempFile);
|
||||
}
|
||||
progress.Report(100 * (percent + (albumPercent * nextPercent)));
|
||||
}
|
||||
|
||||
// Update progress to start at the track gain percent calculation
|
||||
percent += nextPercent;
|
||||
|
||||
_itemRepository.SaveItems(albums, cancellationToken);
|
||||
|
||||
// Track gain
|
||||
var tracks = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
MediaTypes = [MediaType.Audio],
|
||||
IncludeItemTypes = [BaseItemKind.Audio],
|
||||
Parent = library,
|
||||
Recursive = true
|
||||
});
|
||||
var tracks = _libraryManager.GetItemList(new InternalItemsQuery { MediaTypes = [MediaType.Audio], IncludeItemTypes = [BaseItemKind.Audio], Parent = library, Recursive = true });
|
||||
|
||||
var tracksComplete = 0;
|
||||
foreach (var t in tracks)
|
||||
{
|
||||
if (t.NormalizationGain.HasValue || t.LUFS.HasValue || !t.IsFileProtocol)
|
||||
if (!t.NormalizationGain.HasValue && !t.LUFS.HasValue && t.IsFileProtocol)
|
||||
{
|
||||
continue;
|
||||
t.LUFS = await CalculateLUFSAsync(
|
||||
string.Format(CultureInfo.InvariantCulture, "-i \"{0}\"", t.Path.Replace("\"", "\\\"", StringComparison.Ordinal)),
|
||||
false,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
t.LUFS = await CalculateLUFSAsync(
|
||||
string.Format(CultureInfo.InvariantCulture, "-i \"{0}\"", t.Path.Replace("\"", "\\\"", StringComparison.Ordinal)),
|
||||
false,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
// Update sub-progress for track gain
|
||||
tracksComplete++;
|
||||
double trackPercent = tracksComplete;
|
||||
trackPercent /= tracks.Count;
|
||||
|
||||
progress.Report(100 * (percent + (trackPercent * nextPercent)));
|
||||
}
|
||||
|
||||
_itemRepository.SaveItems(tracks, cancellationToken);
|
||||
|
||||
// Update progress
|
||||
numComplete++;
|
||||
percent = numComplete;
|
||||
percent /= libraries.Length;
|
||||
|
||||
progress.Report(100 * percent);
|
||||
}
|
||||
|
||||
progress.Report(100.0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
#pragma warning disable RS0030 // Do not use banned APIs
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using Jellyfin.Server.Implementations.Item;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// Task to clean up any detached userdata from the database.
|
||||
/// </summary>
|
||||
public class CleanupUserDataTask : IScheduledTask
|
||||
{
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
|
||||
private readonly ILogger<CleanupUserDataTask> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CleanupUserDataTask"/> class.
|
||||
/// </summary>
|
||||
/// <param name="localization">The localisation Provider.</param>
|
||||
/// <param name="dbProvider">The DB context factory.</param>
|
||||
/// <param name="logger">A logger.</param>
|
||||
public CleanupUserDataTask(ILocalizationManager localization, IDbContextFactory<JellyfinDbContext> dbProvider, ILogger<CleanupUserDataTask> logger)
|
||||
{
|
||||
_localization = localization;
|
||||
_dbProvider = dbProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => _localization.GetLocalizedString("CleanupUserDataTask");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => _localization.GetLocalizedString("CleanupUserDataTaskDescription");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Key => nameof(CleanupUserDataTask);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
const int LimitDays = 90;
|
||||
var userDataDate = DateTime.UtcNow.AddDays(LimitDays * -1);
|
||||
var dbContext = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
{
|
||||
var detachedUserData = dbContext.UserData.Where(e => e.ItemId == BaseItemRepository.PlaceholderId);
|
||||
_logger.LogInformation("There are {NoDetached} detached UserData entries.", detachedUserData.Count());
|
||||
|
||||
detachedUserData = detachedUserData.Where(e => e.RetentionDate < userDataDate);
|
||||
|
||||
_logger.LogInformation("{NoDetached} are older then {Limit} days.", detachedUserData.Count(), LimitDays);
|
||||
|
||||
await detachedUserData.ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
progress.Report(100);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
@@ -54,12 +54,12 @@ public class RefreshMediaLibraryTask : IScheduledTask
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
progress.Report(0);
|
||||
|
||||
return ((LibraryManager)_libraryManager).ValidateMediaLibraryInternal(progress, cancellationToken);
|
||||
await ((LibraryManager)_libraryManager).ValidateMediaLibraryInternal(progress, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -456,7 +456,7 @@ namespace Emby.Server.Implementations.Session
|
||||
|
||||
var nowPlayingQueue = info.NowPlayingQueue;
|
||||
|
||||
if (nowPlayingQueue?.Length > 0)
|
||||
if (nowPlayingQueue?.Length > 0 && !nowPlayingQueue.SequenceEqual(session.NowPlayingQueue))
|
||||
{
|
||||
session.NowPlayingQueue = nowPlayingQueue;
|
||||
|
||||
@@ -474,6 +474,7 @@ namespace Emby.Server.Implementations.Session
|
||||
private void RemoveNowPlayingItem(SessionInfo session)
|
||||
{
|
||||
session.NowPlayingItem = null;
|
||||
session.FullNowPlayingItem = null;
|
||||
session.PlayState = new PlayerStateInfo();
|
||||
|
||||
if (!string.IsNullOrEmpty(session.DeviceId))
|
||||
|
||||
@@ -5,6 +5,8 @@ using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
|
||||
using MediaBrowser.Controller.Session;
|
||||
@@ -44,6 +46,7 @@ namespace Emby.Server.Implementations.Session
|
||||
private readonly Lock _webSocketsLock = new();
|
||||
|
||||
private readonly ISessionManager _sessionManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly ILogger<SessionWebSocketListener> _logger;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
|
||||
@@ -57,14 +60,17 @@ namespace Emby.Server.Implementations.Session
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="sessionManager">The session manager.</param>
|
||||
/// <param name="userManager">The user manager.</param>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
public SessionWebSocketListener(
|
||||
ILogger<SessionWebSocketListener> logger,
|
||||
ISessionManager sessionManager,
|
||||
IUserManager userManager,
|
||||
ILoggerFactory loggerFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_sessionManager = sessionManager;
|
||||
_userManager = userManager;
|
||||
_loggerFactory = loggerFactory;
|
||||
_keepAlive = new System.Timers.Timer(TimeSpan.FromSeconds(WebSocketLostTimeout * IntervalFactor))
|
||||
{
|
||||
@@ -107,33 +113,9 @@ namespace Emby.Server.Implementations.Session
|
||||
/// <inheritdoc />
|
||||
public async Task ProcessWebSocketConnectedAsync(IWebSocketConnection connection, HttpContext httpContext)
|
||||
{
|
||||
var session = await GetSession(httpContext, connection.RemoteEndPoint?.ToString()).ConfigureAwait(false);
|
||||
if (session is not null)
|
||||
{
|
||||
EnsureController(session, connection);
|
||||
await KeepAliveWebSocket(connection).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Unable to determine session based on query string: {0}", httpContext.Request.QueryString);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<SessionInfo?> GetSession(HttpContext httpContext, string? remoteEndpoint)
|
||||
{
|
||||
if (!httpContext.User.Identity?.IsAuthenticated ?? false)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var deviceId = httpContext.User.GetDeviceId();
|
||||
if (httpContext.Request.Query.TryGetValue("deviceId", out var queryDeviceId))
|
||||
{
|
||||
deviceId = queryDeviceId;
|
||||
}
|
||||
|
||||
return await _sessionManager.GetSessionByAuthenticationToken(httpContext.User.GetToken(), deviceId, remoteEndpoint)
|
||||
.ConfigureAwait(false);
|
||||
var session = await RequestHelpers.GetSession(_sessionManager, _userManager, httpContext).ConfigureAwait(false);
|
||||
EnsureController(session, connection);
|
||||
await KeepAliveWebSocket(connection).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void EnsureController(SessionInfo session, IWebSocketConnection connection)
|
||||
|
||||
@@ -5,7 +5,6 @@ using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.Sorting;
|
||||
using MediaBrowser.Model.Querying;
|
||||
|
||||
namespace Emby.Server.Implementations.Sorting
|
||||
{
|
||||
|
||||
@@ -85,7 +85,10 @@ public class SystemManager : ISystemManager
|
||||
/// <inheritdoc/>
|
||||
public SystemStorageInfo GetSystemStorageInfo()
|
||||
{
|
||||
var virtualFolderInfos = _libraryManager.GetVirtualFolders().Select(e => new LibraryStorageInfo()
|
||||
var virtualFolderInfos = _libraryManager
|
||||
.GetVirtualFolders()
|
||||
.Where(e => !string.IsNullOrWhiteSpace(e.ItemId)) // this should not be null but for some users it is.
|
||||
.Select(e => new LibraryStorageInfo()
|
||||
{
|
||||
Id = Guid.Parse(e.ItemId),
|
||||
Name = e.Name,
|
||||
|
||||
@@ -46,6 +46,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
|
||||
private readonly Version _minFFmpegFlacInMp4 = new Version(6, 0);
|
||||
private readonly Version _minFFmpegX265BframeInFmp4 = new Version(7, 0, 1);
|
||||
private readonly Version _minFFmpegHlsSegmentOptions = new Version(5, 0);
|
||||
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IUserManager _userManager;
|
||||
@@ -1606,6 +1607,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
var segmentFormat = string.Empty;
|
||||
var segmentContainer = outputExtension.TrimStart('.');
|
||||
var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions, segmentContainer);
|
||||
var hlsArguments = $"-hls_playlist_type {(isEventPlaylist ? "event" : "vod")} -hls_list_size 0";
|
||||
|
||||
if (string.Equals(segmentContainer, "ts", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -1621,6 +1623,11 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
false => " -hls_fmp4_init_filename \"" + outputFileNameWithoutExtension + "-1" + outputExtension + "\""
|
||||
};
|
||||
|
||||
var useLegacySegmentOption = _mediaEncoder.EncoderVersion < _minFFmpegHlsSegmentOptions;
|
||||
|
||||
// fMP4 needs this flag to write the audio packet DTS/PTS including the initial delay into MOOF::TRAF::TFDT
|
||||
hlsArguments += $" {(useLegacySegmentOption ? "-hls_ts_options" : "-hls_segment_options")} movflags=+frag_discont";
|
||||
|
||||
segmentFormat = "fmp4" + outputFmp4HeaderArg;
|
||||
}
|
||||
else
|
||||
@@ -1642,8 +1649,6 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
Path.GetFileNameWithoutExtension(outputPath));
|
||||
}
|
||||
|
||||
var hlsArguments = $"-hls_playlist_type {(isEventPlaylist ? "event" : "vod")} -hls_list_size 0";
|
||||
|
||||
return string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -hls_segment_type {8} -start_number {9}{10} -hls_segment_filename \"{11}\" {12} -y \"{13}\"",
|
||||
|
||||
@@ -158,7 +158,10 @@ public class ItemUpdateController : BaseJellyfinApiController
|
||||
ParentalRatingOptions = _localizationManager.GetParentalRatings().ToList(),
|
||||
ExternalIdInfos = _providerManager.GetExternalIdInfos(item).ToArray(),
|
||||
Countries = _localizationManager.GetCountries().ToArray(),
|
||||
Cultures = _localizationManager.GetCultures().ToArray()
|
||||
Cultures = _localizationManager.GetCultures()
|
||||
.DistinctBy(c => c.DisplayName, StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(c => c.DisplayName)
|
||||
.ToArray()
|
||||
};
|
||||
|
||||
if (!item.IsVirtualItem
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Common.Api;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
@@ -34,7 +36,14 @@ public class LocalizationController : BaseJellyfinApiController
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<IEnumerable<CultureDto>> GetCultures()
|
||||
{
|
||||
return Ok(_localization.GetCultures());
|
||||
var allCultures = _localization.GetCultures();
|
||||
|
||||
var distinctCultures = allCultures
|
||||
.DistinctBy(c => c.DisplayName, StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(c => c.DisplayName)
|
||||
.AsEnumerable();
|
||||
|
||||
return Ok(distinctCultures);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
@@ -131,16 +132,16 @@ public class StartupController : BaseJellyfinApiController
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> UpdateStartupUser([FromBody] StartupUserDto startupUserDto)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(startupUserDto.Name);
|
||||
_userManager.ThrowIfInvalidUsername(startupUserDto.Name);
|
||||
|
||||
var user = _userManager.Users.First();
|
||||
if (string.IsNullOrWhiteSpace(startupUserDto.Password))
|
||||
{
|
||||
return BadRequest("Password must not be empty");
|
||||
}
|
||||
|
||||
if (startupUserDto.Name is not null)
|
||||
{
|
||||
user.Username = startupUserDto.Name;
|
||||
}
|
||||
user.Username = startupUserDto.Name;
|
||||
|
||||
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
|
||||
|
||||
|
||||
@@ -32,17 +32,67 @@ public static class FileStreamResponseHelpers
|
||||
HttpContext httpContext,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var requestMessage = new HttpRequestMessage(HttpMethod.Get, new Uri(state.MediaPath));
|
||||
|
||||
// Forward User-Agent if provided
|
||||
if (state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent))
|
||||
{
|
||||
httpClient.DefaultRequestHeaders.Add(HeaderNames.UserAgent, useragent);
|
||||
// Clear default and add specific one if exists, otherwise HttpClient default might be used
|
||||
requestMessage.Headers.UserAgent.Clear();
|
||||
requestMessage.Headers.TryAddWithoutValidation(HeaderNames.UserAgent, useragent);
|
||||
}
|
||||
|
||||
// Can't dispose the response as it's required up the call chain.
|
||||
var response = await httpClient.GetAsync(new Uri(state.MediaPath), HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
|
||||
var contentType = response.Content.Headers.ContentType?.ToString() ?? MediaTypeNames.Text.Plain;
|
||||
// Forward Range header if present in the client request
|
||||
if (httpContext.Request.Headers.TryGetValue(HeaderNames.Range, out var rangeValue))
|
||||
{
|
||||
var rangeString = rangeValue.ToString();
|
||||
if (!string.IsNullOrEmpty(rangeString))
|
||||
{
|
||||
requestMessage.Headers.Range = System.Net.Http.Headers.RangeHeaderValue.Parse(rangeString);
|
||||
}
|
||||
}
|
||||
|
||||
httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none";
|
||||
// Send the request to the upstream server
|
||||
// Use ResponseHeadersRead to avoid downloading the whole content immediately
|
||||
var response = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Check if the upstream server supports range requests and acted upon our Range header
|
||||
bool upstreamSupportsRange = response.StatusCode == System.Net.HttpStatusCode.PartialContent;
|
||||
string acceptRangesValue = "none";
|
||||
if (response.Headers.TryGetValues(HeaderNames.AcceptRanges, out var acceptRangesHeaders))
|
||||
{
|
||||
// Prefer upstream server's Accept-Ranges header if available
|
||||
acceptRangesValue = string.Join(", ", acceptRangesHeaders);
|
||||
upstreamSupportsRange |= acceptRangesValue.Contains("bytes", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
else if (upstreamSupportsRange) // If we got 206 but no Accept-Ranges header, assume bytes
|
||||
{
|
||||
acceptRangesValue = "bytes";
|
||||
}
|
||||
|
||||
// Set Accept-Ranges header for the client based on upstream support
|
||||
httpContext.Response.Headers[HeaderNames.AcceptRanges] = acceptRangesValue;
|
||||
|
||||
// Set Content-Range header if upstream provided it (implies partial content)
|
||||
if (response.Content.Headers.ContentRange is not null)
|
||||
{
|
||||
httpContext.Response.Headers[HeaderNames.ContentRange] = response.Content.Headers.ContentRange.ToString();
|
||||
}
|
||||
|
||||
// Set Content-Length header. For partial content, this is the length of the partial segment.
|
||||
if (response.Content.Headers.ContentLength.HasValue)
|
||||
{
|
||||
httpContext.Response.ContentLength = response.Content.Headers.ContentLength.Value;
|
||||
}
|
||||
|
||||
// Set Content-Type header
|
||||
var contentType = response.Content.Headers.ContentType?.ToString() ?? MediaTypeNames.Application.Octet; // Use a more generic default
|
||||
|
||||
// Set the status code for the client response (e.g., 200 OK or 206 Partial Content)
|
||||
httpContext.Response.StatusCode = (int)response.StatusCode;
|
||||
|
||||
// Return the stream from the upstream server
|
||||
// IMPORTANT: Do not dispose the response stream here, FileStreamResult will handle it.
|
||||
return new FileStreamResult(await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false), contentType);
|
||||
}
|
||||
|
||||
|
||||
@@ -111,7 +111,16 @@ public static class RequestHelpers
|
||||
return user.EnableUserPreferenceAccess;
|
||||
}
|
||||
|
||||
internal static async Task<SessionInfo> GetSession(ISessionManager sessionManager, IUserManager userManager, HttpContext httpContext, Guid? userId = null)
|
||||
/// <summary>
|
||||
/// Get the session based on http request.
|
||||
/// </summary>
|
||||
/// <param name="sessionManager">The session manager.</param>
|
||||
/// <param name="userManager">The user manager.</param>
|
||||
/// <param name="httpContext">The http context.</param>
|
||||
/// <param name="userId">The optional userid.</param>
|
||||
/// <returns>The session.</returns>
|
||||
/// <exception cref="ResourceNotFoundException">Session not found.</exception>
|
||||
public static async Task<SessionInfo> GetSession(ISessionManager sessionManager, IUserManager userManager, HttpContext httpContext, Guid? userId = null)
|
||||
{
|
||||
userId ??= httpContext.User.GetUserId();
|
||||
User? user = null;
|
||||
|
||||
@@ -139,7 +139,7 @@ public static class ServiceCollectionExtensions
|
||||
serviceCollection.AddPooledDbContextFactory<JellyfinDbContext>((serviceProvider, opt) =>
|
||||
{
|
||||
var provider = serviceProvider.GetRequiredService<IJellyfinDatabaseProvider>();
|
||||
provider.Initialise(opt);
|
||||
provider.Initialise(opt, efCoreConfiguration);
|
||||
var lockingBehavior = serviceProvider.GetRequiredService<IEntityFrameworkCoreLockingBehavior>();
|
||||
lockingBehavior.Initialise(opt);
|
||||
});
|
||||
|
||||
@@ -39,7 +39,7 @@ public class BackupService : IBackupService
|
||||
ReferenceHandler = ReferenceHandler.IgnoreCycles,
|
||||
};
|
||||
|
||||
private readonly Version _backupEngineVersion = Version.Parse("0.1.0");
|
||||
private readonly Version _backupEngineVersion = Version.Parse("0.2.0");
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BackupService"/> class.
|
||||
@@ -120,26 +120,29 @@ public class BackupService : IBackupService
|
||||
|
||||
void CopyDirectory(string source, string target)
|
||||
{
|
||||
source = Path.GetFullPath(source);
|
||||
Directory.CreateDirectory(source);
|
||||
|
||||
var fullSourcePath = NormalizePathSeparator(Path.GetFullPath(source) + Path.DirectorySeparatorChar);
|
||||
var fullTargetRoot = Path.GetFullPath(target) + Path.DirectorySeparatorChar;
|
||||
foreach (var item in zipArchive.Entries)
|
||||
{
|
||||
var sanitizedSourcePath = Path.GetFullPath(item.FullName.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar);
|
||||
if (!sanitizedSourcePath.StartsWith(target, StringComparison.Ordinal))
|
||||
var sourcePath = NormalizePathSeparator(Path.GetFullPath(item.FullName));
|
||||
var targetPath = Path.GetFullPath(Path.Combine(target, Path.GetRelativePath(source, item.FullName)));
|
||||
|
||||
if (!sourcePath.StartsWith(fullSourcePath, StringComparison.Ordinal)
|
||||
|| !targetPath.StartsWith(fullTargetRoot, StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var targetPath = Path.Combine(source, sanitizedSourcePath[target.Length..].Trim('/'));
|
||||
_logger.LogInformation("Restore and override {File}", targetPath);
|
||||
item.ExtractToFile(targetPath);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!);
|
||||
item.ExtractToFile(targetPath, overwrite: true);
|
||||
}
|
||||
}
|
||||
|
||||
CopyDirectory(_applicationPaths.ConfigurationDirectoryPath, "Config/");
|
||||
CopyDirectory(_applicationPaths.DataPath, "Data/");
|
||||
CopyDirectory(_applicationPaths.RootFolderPath, "Root/");
|
||||
CopyDirectory("Config", _applicationPaths.ConfigurationDirectoryPath);
|
||||
CopyDirectory("Data", _applicationPaths.DataPath);
|
||||
CopyDirectory("Root", _applicationPaths.RootFolderPath);
|
||||
|
||||
if (manifest.Options.Database)
|
||||
{
|
||||
@@ -148,7 +151,7 @@ public class BackupService : IBackupService
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
{
|
||||
// restore migration history manually
|
||||
var historyEntry = zipArchive.GetEntry($"Database\\{nameof(HistoryRow)}.json");
|
||||
var historyEntry = zipArchive.GetEntry(NormalizePathSeparator(Path.Combine("Database", $"{nameof(HistoryRow)}.json")));
|
||||
if (historyEntry is null)
|
||||
{
|
||||
_logger.LogInformation("No backup of the history table in archive. This is required for Jellyfin operation");
|
||||
@@ -165,6 +168,13 @@ public class BackupService : IBackupService
|
||||
|
||||
var historyRepository = dbContext.GetService<IHistoryRepository>();
|
||||
await historyRepository.CreateIfNotExistsAsync().ConfigureAwait(false);
|
||||
|
||||
foreach (var item in await historyRepository.GetAppliedMigrationsAsync(CancellationToken.None).ConfigureAwait(false))
|
||||
{
|
||||
var insertScript = historyRepository.GetDeleteScript(item.MigrationId);
|
||||
await dbContext.Database.ExecuteSqlRawAsync(insertScript).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
foreach (var item in historyEntries)
|
||||
{
|
||||
var insertScript = historyRepository.GetInsertScript(item);
|
||||
@@ -186,7 +196,7 @@ public class BackupService : IBackupService
|
||||
{
|
||||
_logger.LogInformation("Read backup of {Table}", entityType.Type.Name);
|
||||
|
||||
var zipEntry = zipArchive.GetEntry($"Database\\{entityType.Type.Name}.json");
|
||||
var zipEntry = zipArchive.GetEntry(NormalizePathSeparator(Path.Combine("Database", $"{entityType.Type.Name}.json")));
|
||||
if (zipEntry is null)
|
||||
{
|
||||
_logger.LogInformation("No backup of expected table {Table} is present in backup. Continue anyway.", entityType.Type.Name);
|
||||
@@ -198,7 +208,7 @@ public class BackupService : IBackupService
|
||||
{
|
||||
_logger.LogInformation("Restore backup of {Table}", entityType.Type.Name);
|
||||
var records = 0;
|
||||
await foreach (var item in JsonSerializer.DeserializeAsyncEnumerable<JsonObject>(zipEntryStream, _serializerSettings).ConfigureAwait(false)!)
|
||||
await foreach (var item in JsonSerializer.DeserializeAsyncEnumerable<JsonObject>(zipEntryStream, _serializerSettings).ConfigureAwait(false))
|
||||
{
|
||||
var entity = item.Deserialize(entityType.Type.PropertyType.GetGenericArguments()[0]);
|
||||
if (entity is null)
|
||||
@@ -281,7 +291,7 @@ public class BackupService : IBackupService
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
{
|
||||
dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||
static IAsyncEnumerable<object> GetValues(IQueryable dbSet, Type type)
|
||||
static IAsyncEnumerable<object> GetValues(IQueryable dbSet)
|
||||
{
|
||||
var method = dbSet.GetType().GetMethod(nameof(DbSet<object>.AsAsyncEnumerable))!;
|
||||
var enumerable = method.Invoke(dbSet, null)!;
|
||||
@@ -292,12 +302,12 @@ public class BackupService : IBackupService
|
||||
var historyRepository = dbContext.GetService<IHistoryRepository>();
|
||||
var migrations = await historyRepository.GetAppliedMigrationsAsync().ConfigureAwait(false);
|
||||
|
||||
ICollection<(Type Type, Func<IAsyncEnumerable<object>> ValueFactory)> entityTypes = [
|
||||
ICollection<(Type Type, string SourceName, Func<IAsyncEnumerable<object>> ValueFactory)> entityTypes = [
|
||||
.. typeof(JellyfinDbContext)
|
||||
.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
|
||||
.Where(e => e.PropertyType.IsAssignableTo(typeof(IQueryable)))
|
||||
.Select(e => (Type: e.PropertyType, ValueFactory: new Func<IAsyncEnumerable<object>>(() => GetValues((IQueryable)e.GetValue(dbContext)!, e.PropertyType)))),
|
||||
(Type: typeof(HistoryRow), ValueFactory: new Func<IAsyncEnumerable<object>>(() => migrations.ToAsyncEnumerable()))
|
||||
.Select(e => (Type: e.PropertyType, dbContext.Model.FindEntityType(e.PropertyType.GetGenericArguments()[0])!.GetSchemaQualifiedTableName()!, ValueFactory: new Func<IAsyncEnumerable<object>>(() => GetValues((IQueryable)e.GetValue(dbContext)!)))),
|
||||
(Type: typeof(HistoryRow), SourceName: nameof(HistoryRow), ValueFactory: () => migrations.ToAsyncEnumerable())
|
||||
];
|
||||
manifest.DatabaseTables = entityTypes.Select(e => e.Type.Name).ToArray();
|
||||
var transaction = await dbContext.Database.BeginTransactionAsync().ConfigureAwait(false);
|
||||
@@ -308,8 +318,8 @@ public class BackupService : IBackupService
|
||||
|
||||
foreach (var entityType in entityTypes)
|
||||
{
|
||||
_logger.LogInformation("Begin backup of entity {Table}", entityType.Type.Name);
|
||||
var zipEntry = zipArchive.CreateEntry($"Database\\{entityType.Type.Name}.json");
|
||||
_logger.LogInformation("Begin backup of entity {Table}", entityType.SourceName);
|
||||
var zipEntry = zipArchive.CreateEntry(NormalizePathSeparator(Path.Combine("Database", $"{entityType.SourceName}.json")));
|
||||
var entities = 0;
|
||||
var zipEntryStream = zipEntry.Open();
|
||||
await using (zipEntryStream.ConfigureAwait(false))
|
||||
@@ -347,7 +357,7 @@ public class BackupService : IBackupService
|
||||
foreach (var item in Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.xml", SearchOption.TopDirectoryOnly)
|
||||
.Union(Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.json", SearchOption.TopDirectoryOnly)))
|
||||
{
|
||||
zipArchive.CreateEntryFromFile(item, Path.Combine("Config", Path.GetFileName(item)));
|
||||
zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine("Config", Path.GetFileName(item))));
|
||||
}
|
||||
|
||||
void CopyDirectory(string source, string target, string filter = "*")
|
||||
@@ -361,7 +371,7 @@ public class BackupService : IBackupService
|
||||
|
||||
foreach (var item in Directory.EnumerateFiles(source, filter, SearchOption.AllDirectories))
|
||||
{
|
||||
zipArchive.CreateEntryFromFile(item, Path.Combine(target, item[..source.Length].Trim('\\')));
|
||||
zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine(target, Path.GetRelativePath(source, item))));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -509,4 +519,14 @@ public class BackupService : IBackupService
|
||||
Database = options.Database
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Windows is able to handle '/' as a path seperator in zip files
|
||||
/// but linux isn't able to handle '\' as a path seperator in zip files,
|
||||
/// So normalize to '/'.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to normalize.</param>
|
||||
/// <returns>The normalized path. </returns>
|
||||
private static string NormalizePathSeparator(string path)
|
||||
=> path.Replace('\\', '/');
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
@@ -53,6 +54,11 @@ namespace Jellyfin.Server.Implementations.Item;
|
||||
public sealed class BaseItemRepository
|
||||
: IItemRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the placeholder id for UserData detached items.
|
||||
/// </summary>
|
||||
public static readonly Guid PlaceholderId = Guid.Parse("00000000-0000-0000-0000-000000000001");
|
||||
|
||||
/// <summary>
|
||||
/// This holds all the types in the running assemblies
|
||||
/// so that we can de-serialize properly when we don't have strong types.
|
||||
@@ -95,13 +101,35 @@ public sealed class BaseItemRepository
|
||||
/// <inheritdoc />
|
||||
public void DeleteItem(Guid id)
|
||||
{
|
||||
if (id.IsEmpty())
|
||||
if (id.IsEmpty() || id.Equals(PlaceholderId))
|
||||
{
|
||||
throw new ArgumentException("Guid can't be empty", nameof(id));
|
||||
throw new ArgumentException("Guid can't be empty or the placeholder id.", nameof(id));
|
||||
}
|
||||
|
||||
using var context = _dbProvider.CreateDbContext();
|
||||
using var transaction = context.Database.BeginTransaction();
|
||||
|
||||
var date = (DateTime?)DateTime.UtcNow;
|
||||
|
||||
// Remove any UserData entries for the placeholder item that would conflict with the UserData
|
||||
// being detached from the item being deleted. This is necessary because, during an update,
|
||||
// UserData may be reattached to a new entry, but some entries can be left behind.
|
||||
// Ensures there are no duplicate UserId/CustomDataKey combinations for the placeholder.
|
||||
context.UserData
|
||||
.Join(
|
||||
context.UserData.Where(e => e.ItemId == id),
|
||||
placeholder => new { placeholder.UserId, placeholder.CustomDataKey },
|
||||
userData => new { userData.UserId, userData.CustomDataKey },
|
||||
(placeholder, userData) => placeholder)
|
||||
.Where(e => e.ItemId == PlaceholderId)
|
||||
.ExecuteDelete();
|
||||
|
||||
// Detach all user watch data
|
||||
context.UserData.Where(e => e.ItemId == id)
|
||||
.ExecuteUpdate(e => e
|
||||
.SetProperty(f => f.RetentionDate, date)
|
||||
.SetProperty(f => f.ItemId, PlaceholderId));
|
||||
|
||||
context.AncestorIds.Where(e => e.ItemId == id || e.ParentItemId == id).ExecuteDelete();
|
||||
context.AttachmentStreamInfos.Where(e => e.ItemId == id).ExecuteDelete();
|
||||
context.BaseItemImageInfos.Where(e => e.ItemId == id).ExecuteDelete();
|
||||
@@ -144,7 +172,7 @@ public sealed class BaseItemRepository
|
||||
PrepareFilterQuery(filter);
|
||||
|
||||
using var context = _dbProvider.CreateDbContext();
|
||||
return ApplyQueryFilter(context.BaseItems.AsNoTracking(), context, filter).Select(e => e.Id).ToArray();
|
||||
return ApplyQueryFilter(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderId)), context, filter).Select(e => e.Id).ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -242,7 +270,7 @@ public sealed class BaseItemRepository
|
||||
dbQuery = ApplyGroupingFilter(dbQuery, filter);
|
||||
dbQuery = ApplyQueryPaging(dbQuery, filter);
|
||||
|
||||
result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserialiseBaseItem(w, filter.SkipDeserialization)).ToArray();
|
||||
result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
|
||||
result.StartIndex = filter.StartIndex ?? 0;
|
||||
return result;
|
||||
}
|
||||
@@ -261,7 +289,7 @@ public sealed class BaseItemRepository
|
||||
dbQuery = ApplyGroupingFilter(dbQuery, filter);
|
||||
dbQuery = ApplyQueryPaging(dbQuery, filter);
|
||||
|
||||
return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserialiseBaseItem(w, filter.SkipDeserialization)).ToArray();
|
||||
return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -303,7 +331,7 @@ public sealed class BaseItemRepository
|
||||
mainquery = ApplyGroupingFilter(mainquery, filter);
|
||||
mainquery = ApplyQueryPaging(mainquery, filter);
|
||||
|
||||
return mainquery.AsEnumerable().Where(e => e is not null).Select(w => DeserialiseBaseItem(w, filter.SkipDeserialization)).ToArray();
|
||||
return mainquery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -319,7 +347,7 @@ public sealed class BaseItemRepository
|
||||
.Where(i => filter.TopParentIds.Contains(i.TopParentId!.Value))
|
||||
.Where(i => i.Type == _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode])
|
||||
.Join(
|
||||
context.UserData.AsNoTracking(),
|
||||
context.UserData.AsNoTracking().Where(e => e.ItemId != EF.Constant(PlaceholderId)),
|
||||
i => new { UserId = filter.User.Id, ItemId = i.Id },
|
||||
u => new { UserId = u.UserId, ItemId = u.ItemId },
|
||||
(entity, data) => new { Item = entity, UserData = data })
|
||||
@@ -454,6 +482,13 @@ public sealed class BaseItemRepository
|
||||
|
||||
var images = item.ImageInfos.Select(e => Map(item.Id, e));
|
||||
using var context = _dbProvider.CreateDbContext();
|
||||
|
||||
if (!context.BaseItems.Any(bi => bi.Id == item.Id))
|
||||
{
|
||||
_logger.LogWarning("Unable to save ImageInfo for non existing BaseItem");
|
||||
return;
|
||||
}
|
||||
|
||||
context.BaseItemImageInfos.Where(e => e.ItemId == item.Id).ExecuteDelete();
|
||||
context.BaseItemImageInfos.AddRange(images);
|
||||
context.SaveChanges();
|
||||
@@ -472,7 +507,7 @@ public sealed class BaseItemRepository
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var tuples = new List<(BaseItemDto Item, List<Guid>? AncestorIds, BaseItemDto TopParent, IEnumerable<string> UserDataKey, List<string> InheritedTags)>();
|
||||
foreach (var item in items.GroupBy(e => e.Id).Select(e => e.Last()))
|
||||
foreach (var item in items.GroupBy(e => e.Id).Select(e => e.Last()).Where(e => e.Id != PlaceholderId))
|
||||
{
|
||||
var ancestorIds = item.SupportsAncestors ?
|
||||
item.GetAncestorIds().Distinct().ToList() :
|
||||
@@ -491,6 +526,7 @@ public sealed class BaseItemRepository
|
||||
|
||||
var ids = tuples.Select(f => f.Item.Id).ToArray();
|
||||
var existingItems = context.BaseItems.Where(e => ids.Contains(e.Id)).Select(f => f.Id).ToArray();
|
||||
var newItems = tuples.Where(e => !existingItems.Contains(e.Item.Id)).ToArray();
|
||||
|
||||
foreach (var item in tuples)
|
||||
{
|
||||
@@ -511,8 +547,21 @@ public sealed class BaseItemRepository
|
||||
|
||||
context.SaveChanges();
|
||||
|
||||
foreach (var item in newItems)
|
||||
{
|
||||
// reattach old userData entries
|
||||
var userKeys = item.UserDataKey.ToArray();
|
||||
var retentionDate = (DateTime?)null;
|
||||
context.UserData
|
||||
.Where(e => e.ItemId == PlaceholderId)
|
||||
.Where(e => userKeys.Contains(e.CustomDataKey))
|
||||
.ExecuteUpdate(e => e
|
||||
.SetProperty(f => f.ItemId, item.Item.Id)
|
||||
.SetProperty(f => f.RetentionDate, retentionDate));
|
||||
}
|
||||
|
||||
var itemValueMaps = tuples
|
||||
.Select(e => (Item: e.Item, Values: GetItemValuesToSave(e.Item, e.InheritedTags)))
|
||||
.Select(e => (e.Item, Values: GetItemValuesToSave(e.Item, e.InheritedTags)))
|
||||
.ToArray();
|
||||
var allListedItemValues = itemValueMaps
|
||||
.SelectMany(f => f.Values)
|
||||
@@ -539,7 +588,7 @@ public sealed class BaseItemRepository
|
||||
|
||||
var itemValuesStore = existingValues.Concat(missingItemValues).ToArray();
|
||||
var valueMap = itemValueMaps
|
||||
.Select(f => (Item: f.Item, Values: f.Values.Select(e => itemValuesStore.First(g => g.Value == e.Value && g.Type == e.MagicNumber)).ToArray()))
|
||||
.Select(f => (f.Item, Values: f.Values.Select(e => itemValuesStore.First(g => g.Value == e.Value && g.Type == e.MagicNumber)).DistinctBy(e => e.ItemValueId).ToArray()))
|
||||
.ToArray();
|
||||
|
||||
var mappedValues = context.ItemValuesMap.Where(e => ids.Contains(e.ItemId)).ToList();
|
||||
@@ -627,7 +676,7 @@ public sealed class BaseItemRepository
|
||||
return null;
|
||||
}
|
||||
|
||||
return DeserialiseBaseItem(item);
|
||||
return DeserializeBaseItem(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -673,12 +722,12 @@ public sealed class BaseItemRepository
|
||||
dto.TotalBitrate = entity.TotalBitrate;
|
||||
dto.ExternalId = entity.ExternalId;
|
||||
dto.Size = entity.Size;
|
||||
dto.Genres = entity.Genres?.Split('|') ?? [];
|
||||
dto.DateCreated = entity.DateCreated.GetValueOrDefault();
|
||||
dto.DateModified = entity.DateModified.GetValueOrDefault();
|
||||
dto.Genres = string.IsNullOrWhiteSpace(entity.Genres) ? [] : entity.Genres.Split('|');
|
||||
dto.DateCreated = entity.DateCreated ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
|
||||
dto.DateModified = entity.DateModified ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
|
||||
dto.ChannelId = entity.ChannelId ?? Guid.Empty;
|
||||
dto.DateLastRefreshed = entity.DateLastRefreshed.GetValueOrDefault();
|
||||
dto.DateLastSaved = entity.DateLastSaved.GetValueOrDefault();
|
||||
dto.DateLastRefreshed = entity.DateLastRefreshed ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
|
||||
dto.DateLastSaved = entity.DateLastSaved ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
|
||||
dto.OwnerId = string.IsNullOrWhiteSpace(entity.OwnerId) ? Guid.Empty : (Guid.TryParse(entity.OwnerId, out var ownerId) ? ownerId : Guid.Empty);
|
||||
dto.Width = entity.Width.GetValueOrDefault();
|
||||
dto.Height = entity.Height.GetValueOrDefault();
|
||||
@@ -705,7 +754,7 @@ public sealed class BaseItemRepository
|
||||
dto.ExtraIds = string.IsNullOrWhiteSpace(entity.ExtraIds) ? [] : entity.ExtraIds.Split('|').Select(e => Guid.Parse(e)).ToArray();
|
||||
dto.ProductionLocations = entity.ProductionLocations?.Split('|') ?? [];
|
||||
dto.Studios = entity.Studios?.Split('|') ?? [];
|
||||
dto.Tags = entity.Tags?.Split('|') ?? [];
|
||||
dto.Tags = string.IsNullOrWhiteSpace(entity.Tags) ? [] : entity.Tags.Split('|');
|
||||
|
||||
if (dto is IHasProgramAttributes hasProgramAttributes)
|
||||
{
|
||||
@@ -779,7 +828,7 @@ public sealed class BaseItemRepository
|
||||
|
||||
if (dto is Folder folder)
|
||||
{
|
||||
folder.DateLastMediaAdded = entity.DateLastMediaAdded;
|
||||
folder.DateLastMediaAdded = entity.DateLastMediaAdded ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
return dto;
|
||||
@@ -839,11 +888,11 @@ public sealed class BaseItemRepository
|
||||
entity.ExternalId = dto.ExternalId;
|
||||
entity.Size = dto.Size;
|
||||
entity.Genres = string.Join('|', dto.Genres);
|
||||
entity.DateCreated = dto.DateCreated;
|
||||
entity.DateModified = dto.DateModified;
|
||||
entity.DateCreated = dto.DateCreated == DateTime.MinValue ? null : dto.DateCreated;
|
||||
entity.DateModified = dto.DateModified == DateTime.MinValue ? null : dto.DateModified;
|
||||
entity.ChannelId = dto.ChannelId;
|
||||
entity.DateLastRefreshed = dto.DateLastRefreshed;
|
||||
entity.DateLastSaved = dto.DateLastSaved;
|
||||
entity.DateLastRefreshed = dto.DateLastRefreshed == DateTime.MinValue ? null : dto.DateLastRefreshed;
|
||||
entity.DateLastSaved = dto.DateLastSaved == DateTime.MinValue ? null : dto.DateLastSaved;
|
||||
entity.OwnerId = dto.OwnerId.ToString();
|
||||
entity.Width = dto.Width;
|
||||
entity.Height = dto.Height;
|
||||
@@ -953,7 +1002,7 @@ public sealed class BaseItemRepository
|
||||
|
||||
if (dto is Folder folder)
|
||||
{
|
||||
entity.DateLastMediaAdded = folder.DateLastMediaAdded;
|
||||
entity.DateLastMediaAdded = folder.DateLastMediaAdded == DateTime.MinValue ? null : folder.DateLastMediaAdded;
|
||||
entity.IsFolder = folder.IsFolder;
|
||||
}
|
||||
|
||||
@@ -989,7 +1038,7 @@ public sealed class BaseItemRepository
|
||||
return type.GetCustomAttribute<RequiresSourceSerialisationAttribute>() == null;
|
||||
}
|
||||
|
||||
private BaseItemDto DeserialiseBaseItem(BaseItemEntity baseItemEntity, bool skipDeserialization = false)
|
||||
private BaseItemDto DeserializeBaseItem(BaseItemEntity baseItemEntity, bool skipDeserialization = false)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(baseItemEntity, nameof(baseItemEntity));
|
||||
if (_serverConfigurationManager?.Configuration is null)
|
||||
@@ -998,7 +1047,7 @@ public sealed class BaseItemRepository
|
||||
}
|
||||
|
||||
var typeToSerialise = GetType(baseItemEntity.Type);
|
||||
return BaseItemRepository.DeserialiseBaseItem(
|
||||
return BaseItemRepository.DeserializeBaseItem(
|
||||
baseItemEntity,
|
||||
_logger,
|
||||
_appHost,
|
||||
@@ -1006,7 +1055,7 @@ public sealed class BaseItemRepository
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialises a BaseItemEntity and sets all properties.
|
||||
/// Deserializes a BaseItemEntity and sets all properties.
|
||||
/// </summary>
|
||||
/// <param name="baseItemEntity">The DB entity.</param>
|
||||
/// <param name="logger">Logger.</param>
|
||||
@@ -1014,9 +1063,9 @@ public sealed class BaseItemRepository
|
||||
/// <param name="skipDeserialization">If only mapping should be processed.</param>
|
||||
/// <returns>A mapped BaseItem.</returns>
|
||||
/// <exception cref="InvalidOperationException">Will be thrown if an invalid serialisation is requested.</exception>
|
||||
public static BaseItemDto DeserialiseBaseItem(BaseItemEntity baseItemEntity, ILogger logger, IServerApplicationHost? appHost, bool skipDeserialization = false)
|
||||
public static BaseItemDto DeserializeBaseItem(BaseItemEntity baseItemEntity, ILogger logger, IServerApplicationHost? appHost, bool skipDeserialization = false)
|
||||
{
|
||||
var type = GetType(baseItemEntity.Type) ?? throw new InvalidOperationException("Cannot deserialise unknown type.");
|
||||
var type = GetType(baseItemEntity.Type) ?? throw new InvalidOperationException("Cannot deserialize unknown type.");
|
||||
BaseItemDto? dto = null;
|
||||
if (TypeRequiresDeserialization(type) && baseItemEntity.Data is not null && !skipDeserialization)
|
||||
{
|
||||
@@ -1032,7 +1081,7 @@ public sealed class BaseItemRepository
|
||||
|
||||
if (dto is null)
|
||||
{
|
||||
dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deserialise unknown type.");
|
||||
dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deserialize unknown type.");
|
||||
}
|
||||
|
||||
return Map(baseItemEntity, dto, appHost);
|
||||
@@ -1049,7 +1098,7 @@ public sealed class BaseItemRepository
|
||||
|
||||
using var context = _dbProvider.CreateDbContext();
|
||||
|
||||
var innerQueryFilter = TranslateQuery(context.BaseItems, context, new InternalItemsQuery(filter.User)
|
||||
var innerQueryFilter = TranslateQuery(context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)), context, new InternalItemsQuery(filter.User)
|
||||
{
|
||||
ExcludeItemTypes = filter.ExcludeItemTypes,
|
||||
IncludeItemTypes = filter.IncludeItemTypes,
|
||||
@@ -1138,7 +1187,7 @@ public sealed class BaseItemRepository
|
||||
IsPlayed = filter.IsPlayed
|
||||
};
|
||||
|
||||
itemCountQuery = TranslateQuery(context.BaseItems.AsNoTracking(), context, typeSubQuery)
|
||||
itemCountQuery = TranslateQuery(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderId)), context, typeSubQuery)
|
||||
.Where(e => e.ItemValues!.Any(f => itemValueTypes!.Contains(f.ItemValue.Type)));
|
||||
|
||||
var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series];
|
||||
@@ -1178,7 +1227,7 @@ public sealed class BaseItemRepository
|
||||
.Where(e => e is not null)
|
||||
.Select(e =>
|
||||
{
|
||||
return (DeserialiseBaseItem(e.item, filter.SkipDeserialization), e.itemCount);
|
||||
return (DeserializeBaseItem(e.item, filter.SkipDeserialization), e.itemCount);
|
||||
})
|
||||
];
|
||||
}
|
||||
@@ -1193,7 +1242,7 @@ public sealed class BaseItemRepository
|
||||
.Where(e => e is not null)
|
||||
.Select<BaseItemEntity, (BaseItemDto, ItemCounts?)>(e =>
|
||||
{
|
||||
return (DeserialiseBaseItem(e, filter.SkipDeserialization), null);
|
||||
return (DeserializeBaseItem(e, filter.SkipDeserialization), null);
|
||||
})
|
||||
];
|
||||
}
|
||||
@@ -1274,7 +1323,7 @@ public sealed class BaseItemRepository
|
||||
{
|
||||
Path = appHost?.ExpandVirtualPath(e.Path) ?? e.Path,
|
||||
BlurHash = e.Blurhash is null ? null : Encoding.UTF8.GetString(e.Blurhash),
|
||||
DateModified = e.DateModified,
|
||||
DateModified = e.DateModified ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc),
|
||||
Height = e.Height,
|
||||
Width = e.Width,
|
||||
Type = (ImageType)e.ImageType
|
||||
@@ -1814,7 +1863,7 @@ public sealed class BaseItemRepository
|
||||
// We should probably figure this out for all folders, but for right now, this is the only place where we need it
|
||||
if (filter.IncludeItemTypes.Length == 1 && filter.IncludeItemTypes[0] == BaseItemKind.Series)
|
||||
{
|
||||
baseQuery = baseQuery.Where(e => context.BaseItems
|
||||
baseQuery = baseQuery.Where(e => context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId))
|
||||
.Where(e => e.IsFolder == false && e.IsVirtualItem == false)
|
||||
.Where(f => f.UserData!.FirstOrDefault(e => e.UserId == filter.User!.Id && e.Played)!.Played)
|
||||
.Any(f => f.SeriesPresentationUniqueKey == e.PresentationUniqueKey) == filter.IsPlayed);
|
||||
@@ -2064,7 +2113,7 @@ public sealed class BaseItemRepository
|
||||
if (filter.HasDeadParentId.HasValue && filter.HasDeadParentId.Value)
|
||||
{
|
||||
baseQuery = baseQuery
|
||||
.Where(e => e.ParentId.HasValue && !context.BaseItems.Any(f => f.Id == e.ParentId.Value));
|
||||
.Where(e => e.ParentId.HasValue && !context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)).Any(f => f.Id == e.ParentId.Value));
|
||||
}
|
||||
|
||||
if (filter.IsDeadArtist.HasValue && filter.IsDeadArtist.Value)
|
||||
@@ -2145,17 +2194,19 @@ public sealed class BaseItemRepository
|
||||
if (filter.ExcludeItemIds.Length > 0)
|
||||
{
|
||||
baseQuery = baseQuery
|
||||
.Where(e => !filter.ItemIds.Contains(e.Id));
|
||||
.Where(e => !filter.ExcludeItemIds.Contains(e.Id));
|
||||
}
|
||||
|
||||
if (filter.ExcludeProviderIds is not null && filter.ExcludeProviderIds.Count > 0)
|
||||
{
|
||||
baseQuery = baseQuery.Where(e => !e.Provider!.All(f => !filter.ExcludeProviderIds.All(w => f.ProviderId == w.Key && f.ProviderValue == w.Value)));
|
||||
var exclude = filter.ExcludeProviderIds.Select(e => $"{e.Key}:{e.Value}").ToArray();
|
||||
baseQuery = baseQuery.Where(e => e.Provider!.Select(f => f.ProviderId + ":" + f.ProviderValue)!.All(f => !exclude.Contains(f)));
|
||||
}
|
||||
|
||||
if (filter.HasAnyProviderId is not null && filter.HasAnyProviderId.Count > 0)
|
||||
{
|
||||
baseQuery = baseQuery.Where(e => e.Provider!.Any(f => !filter.HasAnyProviderId.Any(w => f.ProviderId == w.Key && f.ProviderValue == w.Value)));
|
||||
var include = filter.HasAnyProviderId.Select(e => $"{e.Key}:{e.Value}").ToArray();
|
||||
baseQuery = baseQuery.Where(e => e.Provider!.Select(f => f.ProviderId + ":" + f.ProviderValue)!.Any(f => include.Contains(f)));
|
||||
}
|
||||
|
||||
if (filter.HasImdbId.HasValue)
|
||||
@@ -2197,7 +2248,7 @@ public sealed class BaseItemRepository
|
||||
if (!string.IsNullOrWhiteSpace(filter.AncestorWithPresentationUniqueKey))
|
||||
{
|
||||
baseQuery = baseQuery
|
||||
.Where(e => context.BaseItems.Where(f => f.PresentationUniqueKey == filter.AncestorWithPresentationUniqueKey).Any(f => f.Children!.Any(w => w.ItemId == e.Id)));
|
||||
.Where(e => context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)).Where(f => f.PresentationUniqueKey == filter.AncestorWithPresentationUniqueKey).Any(f => f.Children!.Any(w => w.ItemId == e.Id)));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.SeriesPresentationUniqueKey))
|
||||
@@ -2209,8 +2260,8 @@ public sealed class BaseItemRepository
|
||||
if (filter.ExcludeInheritedTags.Length > 0)
|
||||
{
|
||||
baseQuery = baseQuery
|
||||
.Where(e => !e.ItemValues!.Where(w => w.ItemValue.Type == ItemValueType.InheritedTags)
|
||||
.Any(f => filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue)));
|
||||
.Where(e => !e.ItemValues!.Where(w => w.ItemValue.Type == ItemValueType.InheritedTags || w.ItemValue.Type == ItemValueType.Tags)
|
||||
.Any(f => filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue)));
|
||||
}
|
||||
|
||||
if (filter.IncludeInheritedTags.Length > 0)
|
||||
@@ -2220,10 +2271,10 @@ public sealed class BaseItemRepository
|
||||
if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Episode)
|
||||
{
|
||||
baseQuery = baseQuery
|
||||
.Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags)
|
||||
.Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags || f.ItemValue.Type == ItemValueType.Tags)
|
||||
.Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))
|
||||
||
|
||||
(e.ParentId.HasValue && context.ItemValuesMap.Where(w => w.ItemId == e.ParentId.Value)!.Where(w => w.ItemValue.Type == ItemValueType.InheritedTags)
|
||||
(e.ParentId.HasValue && context.ItemValuesMap.Where(w => w.ItemId == e.ParentId.Value && (w.ItemValue.Type == ItemValueType.InheritedTags || w.ItemValue.Type == ItemValueType.Tags))
|
||||
.Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))));
|
||||
}
|
||||
|
||||
@@ -2231,17 +2282,16 @@ public sealed class BaseItemRepository
|
||||
else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist)
|
||||
{
|
||||
baseQuery = baseQuery
|
||||
.Where(e =>
|
||||
e.Parents!
|
||||
.Any(f =>
|
||||
f.ParentItem.ItemValues!.Any(w => w.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(w.ItemValue.CleanValue))
|
||||
|| e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\"")));
|
||||
.Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags || f.ItemValue.Type == ItemValueType.Tags)
|
||||
.Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))
|
||||
|| e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\""));
|
||||
// d ^^ this is stupid it hate this.
|
||||
}
|
||||
else
|
||||
{
|
||||
baseQuery = baseQuery
|
||||
.Where(e => e.Parents!.Any(f => f.ParentItem.ItemValues!.Any(w => w.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(w.ItemValue.CleanValue))));
|
||||
.Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags || f.ItemValue.Type == ItemValueType.Tags)
|
||||
.Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2324,4 +2374,14 @@ public sealed class BaseItemRepository
|
||||
|
||||
return baseQuery;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<bool> ItemExistsAsync(Guid id)
|
||||
{
|
||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
{
|
||||
return await dbContext.BaseItems.AnyAsync(f => f.Id == id).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,16 @@ public class MediaAttachmentRepository(IDbContextFactory<JellyfinDbContext> dbPr
|
||||
{
|
||||
using var context = dbProvider.CreateDbContext();
|
||||
using var transaction = context.Database.BeginTransaction();
|
||||
|
||||
// Users may replace a media with a version that includes attachments to one without them.
|
||||
// So when saving attachments is triggered by a library scan, we always unconditionally
|
||||
// clear the old ones, and then add the new ones if given.
|
||||
context.AttachmentStreamInfos.Where(e => e.ItemId.Equals(id)).ExecuteDelete();
|
||||
context.AttachmentStreamInfos.AddRange(attachments.Select(e => Map(e, id)));
|
||||
if (attachments.Any())
|
||||
{
|
||||
context.AttachmentStreamInfos.AddRange(attachments.Select(e => Map(e, id)));
|
||||
}
|
||||
|
||||
context.SaveChanges();
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ public class MediaSegmentManager : IMediaSegmentManager
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task RunSegmentPluginProviders(BaseItem baseItem, LibraryOptions libraryOptions, bool overwrite, CancellationToken cancellationToken)
|
||||
public async Task RunSegmentPluginProviders(BaseItem baseItem, LibraryOptions libraryOptions, bool forceOverwrite, CancellationToken cancellationToken)
|
||||
{
|
||||
var providers = _segmentProviders
|
||||
.Where(e => !libraryOptions.DisabledMediaSegmentProviders.Contains(GetProviderId(e.Name)))
|
||||
@@ -70,18 +70,13 @@ public class MediaSegmentManager : IMediaSegmentManager
|
||||
|
||||
using var db = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!overwrite && (await db.MediaSegments.AnyAsync(e => e.ItemId.Equals(baseItem.Id), cancellationToken).ConfigureAwait(false)))
|
||||
{
|
||||
_logger.LogDebug("Skip {MediaPath} as it already contains media segments", baseItem.Path);
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Start media segment extraction for {MediaPath} with {CountProviders} providers enabled", baseItem.Path, providers.Count);
|
||||
|
||||
await db.MediaSegments.Where(e => e.ItemId.Equals(baseItem.Id)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// no need to recreate the request object every time.
|
||||
var requestItem = new MediaSegmentGenerationRequest() { ItemId = baseItem.Id };
|
||||
if (forceOverwrite)
|
||||
{
|
||||
// delete all existing media segments if forceOverwrite is set.
|
||||
await db.MediaSegments.Where(e => e.ItemId.Equals(baseItem.Id)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
foreach (var provider in providers)
|
||||
{
|
||||
@@ -91,15 +86,56 @@ public class MediaSegmentManager : IMediaSegmentManager
|
||||
continue;
|
||||
}
|
||||
|
||||
IQueryable<MediaSegment> existingSegments;
|
||||
if (forceOverwrite)
|
||||
{
|
||||
existingSegments = Array.Empty<MediaSegment>().AsQueryable();
|
||||
}
|
||||
else
|
||||
{
|
||||
existingSegments = db.MediaSegments.Where(e => e.ItemId.Equals(baseItem.Id) && e.SegmentProviderId == GetProviderId(provider.Name));
|
||||
}
|
||||
|
||||
var requestItem = new MediaSegmentGenerationRequest()
|
||||
{
|
||||
ItemId = baseItem.Id,
|
||||
ExistingSegments = existingSegments.Select(e => Map(e)).ToArray()
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var segments = await provider.GetMediaSegments(requestItem, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (segments.Count == 0)
|
||||
|
||||
if (!forceOverwrite)
|
||||
{
|
||||
var existingSegmentsList = existingSegments.ToArray(); // Cannot use requestItem's list, as the provider might tamper with its items.
|
||||
if (segments.Count == requestItem.ExistingSegments.Count && segments.All(e => existingSegmentsList.Any(f =>
|
||||
{
|
||||
return
|
||||
e.StartTicks == f.StartTicks &&
|
||||
e.EndTicks == f.EndTicks &&
|
||||
e.Type == f.Type;
|
||||
})))
|
||||
{
|
||||
_logger.LogDebug("Media Segment provider {ProviderName} did not modify any segments for {MediaPath}", provider.Name, baseItem.Path);
|
||||
continue;
|
||||
}
|
||||
|
||||
// delete existing media segments that were re-generated.
|
||||
await existingSegments.ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (segments.Count == 0 && !requestItem.ExistingSegments.Any())
|
||||
{
|
||||
_logger.LogDebug("Media Segment provider {ProviderName} did not find any segments for {MediaPath}", provider.Name, baseItem.Path);
|
||||
continue;
|
||||
}
|
||||
else if (segments.Count == 0 && requestItem.ExistingSegments.Any())
|
||||
{
|
||||
_logger.LogDebug("Media Segment provider {ProviderName} deleted all segments for {MediaPath}", provider.Name, baseItem.Path);
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Media Segment provider {ProviderName} found {CountSegments} for {MediaPath}", provider.Name, segments.Count, baseItem.Path);
|
||||
var providerId = GetProviderId(provider.Name);
|
||||
|
||||
@@ -744,7 +744,8 @@ namespace Jellyfin.Server.Implementations.Users
|
||||
_users[user.Id] = user;
|
||||
}
|
||||
|
||||
internal static void ThrowIfInvalidUsername(string name)
|
||||
/// <inheritdoc/>
|
||||
public void ThrowIfInvalidUsername(string name)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(name) && ValidUsernameRegex().IsMatch(name))
|
||||
{
|
||||
|
||||
@@ -116,26 +116,7 @@ namespace Jellyfin.Server.Extensions
|
||||
.AddTransient<ICorsPolicyProvider, CorsPolicyProvider>()
|
||||
.Configure<ForwardedHeadersOptions>(options =>
|
||||
{
|
||||
// https://github.com/dotnet/aspnetcore/blob/master/src/Middleware/HttpOverrides/src/ForwardedHeadersMiddleware.cs
|
||||
// Enable debug logging on Microsoft.AspNetCore.HttpOverrides.ForwardedHeadersMiddleware to help investigate issues.
|
||||
|
||||
if (config.KnownProxies.Length == 0)
|
||||
{
|
||||
options.ForwardedHeaders = ForwardedHeaders.None;
|
||||
options.KnownNetworks.Clear();
|
||||
options.KnownProxies.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost;
|
||||
AddProxyAddresses(config, config.KnownProxies, options);
|
||||
}
|
||||
|
||||
// Only set forward limit if we have some known proxies or some known networks.
|
||||
if (options.KnownProxies.Count != 0 || options.KnownNetworks.Count != 0)
|
||||
{
|
||||
options.ForwardLimit = null;
|
||||
}
|
||||
ConfigureForwardHeaders(config, options);
|
||||
})
|
||||
.AddMvc(opts =>
|
||||
{
|
||||
@@ -183,6 +164,30 @@ namespace Jellyfin.Server.Extensions
|
||||
return mvcBuilder.AddControllersAsServices();
|
||||
}
|
||||
|
||||
internal static void ConfigureForwardHeaders(NetworkConfiguration config, ForwardedHeadersOptions options)
|
||||
{
|
||||
// https://github.com/dotnet/aspnetcore/blob/master/src/Middleware/HttpOverrides/src/ForwardedHeadersMiddleware.cs
|
||||
// Enable debug logging on Microsoft.AspNetCore.HttpOverrides.ForwardedHeadersMiddleware to help investigate issues.
|
||||
|
||||
if (config.KnownProxies.Length == 0)
|
||||
{
|
||||
options.ForwardedHeaders = ForwardedHeaders.None;
|
||||
options.KnownNetworks.Clear();
|
||||
options.KnownProxies.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost;
|
||||
AddProxyAddresses(config, config.KnownProxies, options);
|
||||
}
|
||||
|
||||
// Only set forward limit if we have some known proxies or some known networks.
|
||||
if (options.KnownProxies.Count != 0 || options.KnownNetworks.Count != 0)
|
||||
{
|
||||
options.ForwardLimit = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds Swagger to the service collection.
|
||||
/// </summary>
|
||||
@@ -248,7 +253,7 @@ namespace Jellyfin.Server.Extensions
|
||||
c.AddSwaggerTypeMappings();
|
||||
|
||||
c.SchemaFilter<IgnoreEnumSchemaFilter>();
|
||||
c.OperationFilter<RetryOnTemporarlyUnavailableFilter>();
|
||||
c.OperationFilter<RetryOnTemporarilyUnavailableFilter>();
|
||||
c.OperationFilter<SecurityRequirementsOperationFilter>();
|
||||
c.OperationFilter<FileResponseFilter>();
|
||||
c.OperationFilter<FileRequestFilter>();
|
||||
|
||||
@@ -6,13 +6,13 @@ using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Jellyfin.Server.Filters;
|
||||
|
||||
internal class RetryOnTemporarlyUnavailableFilter : IOperationFilter
|
||||
internal class RetryOnTemporarilyUnavailableFilter : IOperationFilter
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
operation.Responses.Add("503", new OpenApiResponse()
|
||||
{
|
||||
Description = "The server is currently starting or is temporarly not available.",
|
||||
Description = "The server is currently starting or is temporarily not available.",
|
||||
Headers = new Dictionary<string, OpenApiHeader>()
|
||||
{
|
||||
{
|
||||
@@ -53,6 +53,7 @@
|
||||
<PackageReference Include="prometheus-net.AspNetCore" />
|
||||
<PackageReference Include="Serilog.AspNetCore" />
|
||||
<PackageReference Include="Serilog.Enrichers.Thread" />
|
||||
<PackageReference Include="Serilog.Expressions" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" />
|
||||
<PackageReference Include="Serilog.Sinks.Async" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" />
|
||||
|
||||
@@ -17,6 +17,7 @@ using MediaBrowser.Model.Configuration;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Server.Migrations;
|
||||
@@ -47,7 +48,7 @@ internal class JellyfinMigrationService
|
||||
public JellyfinMigrationService(
|
||||
IDbContextFactory<JellyfinDbContext> dbContextFactory,
|
||||
ILoggerFactory loggerFactory,
|
||||
IStartupLogger startupLogger,
|
||||
IStartupLogger<JellyfinMigrationService> startupLogger,
|
||||
IApplicationPaths applicationPaths,
|
||||
IBackupService? backupService = null,
|
||||
IJellyfinDatabaseProvider? jellyfinDatabaseProvider = null)
|
||||
@@ -105,6 +106,13 @@ internal class JellyfinMigrationService
|
||||
var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
{
|
||||
var databaseCreator = dbContext.Database.GetService<IDatabaseCreator>() as IRelationalDatabaseCreator
|
||||
?? throw new InvalidOperationException("Jellyfin does only support relational databases.");
|
||||
if (!await databaseCreator.ExistsAsync().ConfigureAwait(false))
|
||||
{
|
||||
await databaseCreator.CreateAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var historyRepository = dbContext.GetService<IHistoryRepository>();
|
||||
|
||||
await historyRepository.CreateIfNotExistsAsync().ConfigureAwait(false);
|
||||
|
||||
168
Jellyfin.Server/Migrations/Routines/FixDates.cs
Normal file
168
Jellyfin.Server/Migrations/Routines/FixDates.cs
Normal file
@@ -0,0 +1,168 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using Jellyfin.Server.ServerSetupApp;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Server.Migrations.Routines;
|
||||
|
||||
/// <summary>
|
||||
/// Migration to fix dates saved in the database to always be UTC.
|
||||
/// </summary>
|
||||
[JellyfinMigration("2025-06-20T18:00:00", nameof(FixDates))]
|
||||
public class FixDates : IAsyncMigrationRoutine
|
||||
{
|
||||
private const int PageSize = 5000;
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FixDates"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="startupLogger">The startup logger for Startup UI integration.</param>
|
||||
/// <param name="dbProvider">Instance of the <see cref="IDbContextFactory{JellyfinDbContext}"/> interface.</param>
|
||||
public FixDates(
|
||||
ILogger<FixDates> logger,
|
||||
IStartupLogger<FixDates> startupLogger,
|
||||
IDbContextFactory<JellyfinDbContext> dbProvider)
|
||||
{
|
||||
_logger = startupLogger.With(logger);
|
||||
_dbProvider = dbProvider;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task PerformAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!TimeZoneInfo.Local.Equals(TimeZoneInfo.Utc))
|
||||
{
|
||||
using var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
await FixBaseItemsAsync(context, sw, cancellationToken).ConfigureAwait(false);
|
||||
sw.Reset();
|
||||
await FixChaptersAsync(context, sw, cancellationToken).ConfigureAwait(false);
|
||||
sw.Reset();
|
||||
await FixBaseItemImageInfos(context, sw, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task FixBaseItemsAsync(JellyfinDbContext context, Stopwatch sw, CancellationToken cancellationToken)
|
||||
{
|
||||
int itemCount = 0;
|
||||
|
||||
var baseQuery = context.BaseItems.OrderBy(e => e.Id);
|
||||
var records = baseQuery.Count();
|
||||
_logger.LogInformation("Fixing dates for {Count} BaseItems.", records);
|
||||
|
||||
sw.Start();
|
||||
await foreach (var result in context.BaseItems.OrderBy(e => e.Id)
|
||||
.WithPartitionProgress(
|
||||
(partition) =>
|
||||
_logger.LogInformation(
|
||||
"Processing BaseItems batch {BatchNumber} ({ProcessedSoFar}/{TotalRecords}) - Time: {ElapsedTime}",
|
||||
partition + 1,
|
||||
Math.Min((partition + 1) * PageSize, records),
|
||||
records,
|
||||
sw.Elapsed))
|
||||
.PartitionEagerAsync(PageSize, cancellationToken)
|
||||
.WithCancellation(cancellationToken)
|
||||
.ConfigureAwait(false))
|
||||
{
|
||||
result.DateCreated = ToUniversalTime(result.DateCreated);
|
||||
result.DateLastMediaAdded = ToUniversalTime(result.DateLastMediaAdded);
|
||||
result.DateLastRefreshed = ToUniversalTime(result.DateLastRefreshed);
|
||||
result.DateLastSaved = ToUniversalTime(result.DateLastSaved);
|
||||
result.DateModified = ToUniversalTime(result.DateModified);
|
||||
itemCount++;
|
||||
}
|
||||
|
||||
var saveCount = await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||
_logger.LogInformation("BaseItems: Processed {ItemCount} items, saved {SaveCount} changes in {ElapsedTime}", itemCount, saveCount, sw.Elapsed);
|
||||
}
|
||||
|
||||
private async Task FixChaptersAsync(JellyfinDbContext context, Stopwatch sw, CancellationToken cancellationToken)
|
||||
{
|
||||
int itemCount = 0;
|
||||
|
||||
var baseQuery = context.Chapters;
|
||||
var records = baseQuery.Count();
|
||||
_logger.LogInformation("Fixing dates for {Count} Chapters.", records);
|
||||
|
||||
sw.Start();
|
||||
await foreach (var result in context.Chapters.OrderBy(e => e.ItemId)
|
||||
.WithPartitionProgress(
|
||||
(partition) =>
|
||||
_logger.LogInformation(
|
||||
"Processing Chapter batch {BatchNumber} ({ProcessedSoFar}/{TotalRecords}) - Time: {ElapsedTime}",
|
||||
partition + 1,
|
||||
Math.Min((partition + 1) * PageSize, records),
|
||||
records,
|
||||
sw.Elapsed))
|
||||
.PartitionEagerAsync(PageSize, cancellationToken)
|
||||
.WithCancellation(cancellationToken)
|
||||
.ConfigureAwait(false))
|
||||
{
|
||||
result.ImageDateModified = ToUniversalTime(result.ImageDateModified, true);
|
||||
itemCount++;
|
||||
}
|
||||
|
||||
var saveCount = await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||
_logger.LogInformation("Chapters: Processed {ItemCount} items, saved {SaveCount} changes in {ElapsedTime}", itemCount, saveCount, sw.Elapsed);
|
||||
}
|
||||
|
||||
private async Task FixBaseItemImageInfos(JellyfinDbContext context, Stopwatch sw, CancellationToken cancellationToken)
|
||||
{
|
||||
int itemCount = 0;
|
||||
|
||||
var baseQuery = context.BaseItemImageInfos;
|
||||
var records = baseQuery.Count();
|
||||
_logger.LogInformation("Fixing dates for {Count} BaseItemImageInfos.", records);
|
||||
|
||||
sw.Start();
|
||||
await foreach (var result in context.BaseItemImageInfos.OrderBy(e => e.Id)
|
||||
.WithPartitionProgress(
|
||||
(partition) =>
|
||||
_logger.LogInformation(
|
||||
"Processing BaseItemImageInfos batch {BatchNumber} ({ProcessedSoFar}/{TotalRecords}) - Time: {ElapsedTime}",
|
||||
partition + 1,
|
||||
Math.Min((partition + 1) * PageSize, records),
|
||||
records,
|
||||
sw.Elapsed))
|
||||
.PartitionEagerAsync(PageSize, cancellationToken)
|
||||
.WithCancellation(cancellationToken)
|
||||
.ConfigureAwait(false))
|
||||
{
|
||||
result.DateModified = ToUniversalTime(result.DateModified);
|
||||
itemCount++;
|
||||
}
|
||||
|
||||
var saveCount = await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||
_logger.LogInformation("BaseItemImageInfos: Processed {ItemCount} items, saved {SaveCount} changes in {ElapsedTime}", itemCount, saveCount, sw.Elapsed);
|
||||
}
|
||||
|
||||
private DateTime? ToUniversalTime(DateTime? dateTime, bool isUTC = false)
|
||||
{
|
||||
if (dateTime is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (dateTime.Value.Year == 1 && dateTime.Value.Month == 1 && dateTime.Value.Day == 1)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (dateTime.Value.Kind == DateTimeKind.Utc || isUTC)
|
||||
{
|
||||
return dateTime.Value;
|
||||
}
|
||||
|
||||
return dateTime.Value.ToUniversalTime();
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ public class MigrateKeyframeData : IDatabaseMigrationRoutine
|
||||
/// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
|
||||
/// <param name="dbProvider">The EFCore db factory.</param>
|
||||
public MigrateKeyframeData(
|
||||
IStartupLogger startupLogger,
|
||||
IStartupLogger<MigrateKeyframeData> startupLogger,
|
||||
IApplicationPaths appPaths,
|
||||
IDbContextFactory<JellyfinDbContext> dbProvider)
|
||||
{
|
||||
|
||||
@@ -48,7 +48,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
/// <param name="paths">The server application paths.</param>
|
||||
/// <param name="jellyfinDatabaseProvider">The database provider for special access.</param>
|
||||
public MigrateLibraryDb(
|
||||
IStartupLogger startupLogger,
|
||||
IStartupLogger<MigrateLibraryDb> startupLogger,
|
||||
IDbContextFactory<JellyfinDbContext> provider,
|
||||
IServerApplicationPaths paths,
|
||||
IJellyfinDatabaseProvider jellyfinDatabaseProvider)
|
||||
@@ -90,11 +90,14 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
operation.JellyfinDbContext.AncestorIds.ExecuteDelete();
|
||||
}
|
||||
|
||||
// notify the other migration to just silently abort because the fix has been applied here already.
|
||||
ReseedFolderFlag.RerunGuardFlag = true;
|
||||
|
||||
var legacyBaseItemWithUserKeys = new Dictionary<string, BaseItemEntity>();
|
||||
connection.Open();
|
||||
|
||||
var baseItemIds = new HashSet<Guid>();
|
||||
using (var operation = GetPreparedDbContext("moving TypedBaseItem"))
|
||||
using (var operation = GetPreparedDbContext("Moving TypedBaseItem"))
|
||||
{
|
||||
const string typedBaseItemsQuery =
|
||||
"""
|
||||
@@ -105,7 +108,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
Audio, ExternalServiceId, IsInMixedFolder, DateLastSaved, LockedFields, Studios, Tags, TrailerTypes, OriginalTitle, PrimaryVersionId,
|
||||
DateLastMediaAdded, Album, LUFS, NormalizationGain, CriticRating, IsVirtualItem, SeriesName, UserDataKey, SeasonName, SeasonId, SeriesId,
|
||||
PresentationUniqueKey, InheritedParentalRatingValue, ExternalSeriesId, Tagline, ProviderIds, Images, ProductionLocations, ExtraIds, TotalBitrate,
|
||||
ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId, MediaType, SortName, CleanName, UnratedType FROM TypedBaseItems
|
||||
ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId, MediaType, SortName, CleanName, UnratedType, IsFolder FROM TypedBaseItems
|
||||
""";
|
||||
using (new TrackedMigrationStep("Loading TypedBaseItems", _logger))
|
||||
{
|
||||
@@ -121,13 +124,13 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
}
|
||||
}
|
||||
|
||||
using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.BaseItems.Local.Count} BaseItem entries", _logger))
|
||||
using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.BaseItems.Local.Count} BaseItem entries", _logger))
|
||||
{
|
||||
operation.JellyfinDbContext.SaveChanges();
|
||||
}
|
||||
}
|
||||
|
||||
using (var operation = GetPreparedDbContext("moving ItemValues"))
|
||||
using (var operation = GetPreparedDbContext("Moving ItemValues"))
|
||||
{
|
||||
// do not migrate inherited types as they are now properly mapped in search and lookup.
|
||||
const string itemValueQuery =
|
||||
@@ -138,7 +141,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
|
||||
// EFCores local lookup sucks. We cannot use context.ItemValues.Local here because its just super slow.
|
||||
var localItems = new Dictionary<(int Type, string Value), (Database.Implementations.Entities.ItemValue ItemValue, List<Guid> ItemIds)>();
|
||||
using (new TrackedMigrationStep("loading ItemValues", _logger))
|
||||
using (new TrackedMigrationStep("Loading ItemValues", _logger))
|
||||
{
|
||||
foreach (SqliteDataReader dto in connection.Query(itemValueQuery))
|
||||
{
|
||||
@@ -166,13 +169,13 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
}
|
||||
}
|
||||
|
||||
using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.ItemValues.Local.Count} ItemValues entries", _logger))
|
||||
using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.ItemValues.Local.Count} ItemValues entries", _logger))
|
||||
{
|
||||
operation.JellyfinDbContext.SaveChanges();
|
||||
}
|
||||
}
|
||||
|
||||
using (var operation = GetPreparedDbContext("moving UserData"))
|
||||
using (var operation = GetPreparedDbContext("Moving UserData"))
|
||||
{
|
||||
var queryResult = connection.Query(
|
||||
"""
|
||||
@@ -181,14 +184,14 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.UserDataKey = UserDatas.key)
|
||||
""");
|
||||
|
||||
using (new TrackedMigrationStep("loading UserData", _logger))
|
||||
using (new TrackedMigrationStep("Loading UserData", _logger))
|
||||
{
|
||||
var users = operation.JellyfinDbContext.Users.AsNoTracking().ToImmutableArray();
|
||||
var users = operation.JellyfinDbContext.Users.AsNoTracking().ToArray();
|
||||
var userIdBlacklist = new HashSet<int>();
|
||||
|
||||
foreach (var entity in queryResult)
|
||||
{
|
||||
var userData = GetUserData(users, entity, userIdBlacklist);
|
||||
var userData = GetUserData(users, entity, userIdBlacklist, _logger);
|
||||
if (userData is null)
|
||||
{
|
||||
var userDataId = entity.GetString(0);
|
||||
@@ -212,19 +215,17 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
userData.ItemId = refItem.Id;
|
||||
operation.JellyfinDbContext.UserData.Add(userData);
|
||||
}
|
||||
|
||||
users.Clear();
|
||||
}
|
||||
|
||||
legacyBaseItemWithUserKeys.Clear();
|
||||
|
||||
using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.UserData.Local.Count} UserData entries", _logger))
|
||||
using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.UserData.Local.Count} UserData entries", _logger))
|
||||
{
|
||||
operation.JellyfinDbContext.SaveChanges();
|
||||
}
|
||||
}
|
||||
|
||||
using (var operation = GetPreparedDbContext("moving MediaStreamInfos"))
|
||||
using (var operation = GetPreparedDbContext("Moving MediaStreamInfos"))
|
||||
{
|
||||
const string mediaStreamQuery =
|
||||
"""
|
||||
@@ -237,7 +238,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = MediaStreams.ItemId)
|
||||
""";
|
||||
|
||||
using (new TrackedMigrationStep("loading MediaStreamInfos", _logger))
|
||||
using (new TrackedMigrationStep("Loading MediaStreamInfos", _logger))
|
||||
{
|
||||
foreach (SqliteDataReader dto in connection.Query(mediaStreamQuery))
|
||||
{
|
||||
@@ -245,13 +246,13 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
}
|
||||
}
|
||||
|
||||
using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.MediaStreamInfos.Local.Count} MediaStreamInfos entries", _logger))
|
||||
using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.MediaStreamInfos.Local.Count} MediaStreamInfos entries", _logger))
|
||||
{
|
||||
operation.JellyfinDbContext.SaveChanges();
|
||||
}
|
||||
}
|
||||
|
||||
using (var operation = GetPreparedDbContext("moving AttachmentStreamInfos"))
|
||||
using (var operation = GetPreparedDbContext("Moving AttachmentStreamInfos"))
|
||||
{
|
||||
const string mediaAttachmentQuery =
|
||||
"""
|
||||
@@ -260,7 +261,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = mediaattachments.ItemId)
|
||||
""";
|
||||
|
||||
using (new TrackedMigrationStep("loading AttachmentStreamInfos", _logger))
|
||||
using (new TrackedMigrationStep("Loading AttachmentStreamInfos", _logger))
|
||||
{
|
||||
foreach (SqliteDataReader dto in connection.Query(mediaAttachmentQuery))
|
||||
{
|
||||
@@ -268,13 +269,13 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
}
|
||||
}
|
||||
|
||||
using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.AttachmentStreamInfos.Local.Count} AttachmentStreamInfos entries", _logger))
|
||||
using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.AttachmentStreamInfos.Local.Count} AttachmentStreamInfos entries", _logger))
|
||||
{
|
||||
operation.JellyfinDbContext.SaveChanges();
|
||||
}
|
||||
}
|
||||
|
||||
using (var operation = GetPreparedDbContext("moving People"))
|
||||
using (var operation = GetPreparedDbContext("Moving People"))
|
||||
{
|
||||
const string personsQuery =
|
||||
"""
|
||||
@@ -284,14 +285,14 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
|
||||
var peopleCache = new Dictionary<string, (People Person, List<PeopleBaseItemMap> Items)>();
|
||||
|
||||
using (new TrackedMigrationStep("loading People", _logger))
|
||||
using (new TrackedMigrationStep("Loading People", _logger))
|
||||
{
|
||||
foreach (SqliteDataReader reader in connection.Query(personsQuery))
|
||||
{
|
||||
var itemId = reader.GetGuid(0);
|
||||
if (!baseItemIds.Contains(itemId))
|
||||
{
|
||||
_logger.LogError("Dont save person {0} because its not in use by any BaseItem", reader.GetString(1));
|
||||
_logger.LogError("Not saving person {0} because it's not in use by any BaseItem", reader.GetString(1));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -330,13 +331,13 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
peopleCache.Clear();
|
||||
}
|
||||
|
||||
using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.Peoples.Local.Count} People entries and {operation.JellyfinDbContext.PeopleBaseItemMap.Local.Count} maps", _logger))
|
||||
using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.Peoples.Local.Count} People entries and {operation.JellyfinDbContext.PeopleBaseItemMap.Local.Count} maps", _logger))
|
||||
{
|
||||
operation.JellyfinDbContext.SaveChanges();
|
||||
}
|
||||
}
|
||||
|
||||
using (var operation = GetPreparedDbContext("moving Chapters"))
|
||||
using (var operation = GetPreparedDbContext("Moving Chapters"))
|
||||
{
|
||||
const string chapterQuery =
|
||||
"""
|
||||
@@ -344,7 +345,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = Chapters2.ItemId)
|
||||
""";
|
||||
|
||||
using (new TrackedMigrationStep("loading Chapters", _logger))
|
||||
using (new TrackedMigrationStep("Loading Chapters", _logger))
|
||||
{
|
||||
foreach (SqliteDataReader dto in connection.Query(chapterQuery))
|
||||
{
|
||||
@@ -353,13 +354,13 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
}
|
||||
}
|
||||
|
||||
using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.Chapters.Local.Count} Chapters entries", _logger))
|
||||
using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.Chapters.Local.Count} Chapters entries", _logger))
|
||||
{
|
||||
operation.JellyfinDbContext.SaveChanges();
|
||||
}
|
||||
}
|
||||
|
||||
using (var operation = GetPreparedDbContext("moving AncestorIds"))
|
||||
using (var operation = GetPreparedDbContext("Moving AncestorIds"))
|
||||
{
|
||||
const string ancestorIdsQuery =
|
||||
"""
|
||||
@@ -370,7 +371,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = AncestorIds.AncestorId)
|
||||
""";
|
||||
|
||||
using (new TrackedMigrationStep("loading AncestorIds", _logger))
|
||||
using (new TrackedMigrationStep("Loading AncestorIds", _logger))
|
||||
{
|
||||
foreach (SqliteDataReader dto in connection.Query(ancestorIdsQuery))
|
||||
{
|
||||
@@ -379,7 +380,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
}
|
||||
}
|
||||
|
||||
using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.AncestorIds.Local.Count} AncestorId entries", _logger))
|
||||
using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.AncestorIds.Local.Count} AncestorId entries", _logger))
|
||||
{
|
||||
operation.JellyfinDbContext.SaveChanges();
|
||||
}
|
||||
@@ -404,19 +405,20 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
return new DatabaseMigrationStep(dbContext, operationName, _logger);
|
||||
}
|
||||
|
||||
private UserData? GetUserData(ImmutableArray<User> users, SqliteDataReader dto, HashSet<int> userIdBlacklist)
|
||||
internal static UserData? GetUserData(User[] users, SqliteDataReader dto, HashSet<int> userIdBlacklist, ILogger logger)
|
||||
{
|
||||
var internalUserId = dto.GetInt32(1);
|
||||
var user = users.FirstOrDefault(e => e.InternalId == internalUserId);
|
||||
if (userIdBlacklist.Contains(internalUserId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var user = users.FirstOrDefault(e => e.InternalId == internalUserId);
|
||||
if (user is null)
|
||||
{
|
||||
if (userIdBlacklist.Contains(internalUserId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
userIdBlacklist.Add(internalUserId);
|
||||
|
||||
_logger.LogError("Tried to find user with index '{Idx}' but there are only '{MaxIdx}' users.", internalUserId, users.Length);
|
||||
logger.LogError("Tried to find user with index '{Idx}' but there are only '{MaxIdx}' users.", internalUserId, users.Length);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1168,7 +1170,12 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
entity.UnratedType = unratedType;
|
||||
}
|
||||
|
||||
var baseItem = BaseItemRepository.DeserialiseBaseItem(entity, _logger, null, false);
|
||||
if (reader.TryGetBoolean(index++, out var isFolder))
|
||||
{
|
||||
entity.IsFolder = isFolder;
|
||||
}
|
||||
|
||||
var baseItem = BaseItemRepository.DeserializeBaseItem(entity, _logger, null, false);
|
||||
var dataKeys = baseItem.GetUserDataKeys();
|
||||
userDataKeys.AddRange(dataKeys);
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ public class MigrateLibraryDbCompatibilityCheck : IAsyncMigrationRoutine
|
||||
/// </summary>
|
||||
/// <param name="startupLogger">The startup logger.</param>
|
||||
/// <param name="paths">The Path service.</param>
|
||||
public MigrateLibraryDbCompatibilityCheck(IStartupLogger startupLogger, IServerApplicationPaths paths)
|
||||
public MigrateLibraryDbCompatibilityCheck(IStartupLogger<MigrateLibraryDbCompatibilityCheck> startupLogger, IServerApplicationPaths paths)
|
||||
{
|
||||
_logger = startupLogger;
|
||||
_paths = paths;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user