mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-12-16 14:03:03 +03:00
Compare commits
176 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 | ||
|
|
98daf4aedb | ||
|
|
fcf56b73cb | ||
|
|
e8239a7ee2 | ||
|
|
84cebeae64 | ||
|
|
c0e2875818 | ||
|
|
411ba03bf0 | ||
|
|
b2e19c0306 | ||
|
|
a7891b3f2d | ||
|
|
e7bc86ebb8 | ||
|
|
7aa96dfc20 | ||
|
|
70d07b830d |
@@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "9.0.6",
|
||||
"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@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
|
||||
uses: github/codeql-action/init@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: +security-extended
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
|
||||
uses: github/codeql-action/autobuild@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
|
||||
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/"
|
||||
|
||||
@@ -61,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)
|
||||
@@ -196,8 +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.6" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.6" />
|
||||
<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.6" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.6" />
|
||||
<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.6.0" />
|
||||
<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" />
|
||||
@@ -75,16 +76,16 @@
|
||||
<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.6" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.6" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.6" />
|
||||
<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.26.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" />
|
||||
@@ -92,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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -2991,13 +3009,12 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
var path = Person.GetPath(person.Name);
|
||||
var info = Directory.CreateDirectory(path);
|
||||
var lastWriteTime = info.LastWriteTimeUtc;
|
||||
personEntity = new Person()
|
||||
{
|
||||
Name = person.Name,
|
||||
Id = GetItemByNameId<Person>(path),
|
||||
DateCreated = info.CreationTimeUtc,
|
||||
DateModified = lastWriteTime,
|
||||
DateModified = info.LastWriteTimeUtc,
|
||||
Path = path
|
||||
};
|
||||
|
||||
@@ -3043,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;
|
||||
}
|
||||
|
||||
@@ -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": "ব্যবহারকারীর ডেটা পরিষ্কারের কাজ"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -93,50 +93,50 @@
|
||||
"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",
|
||||
"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": "Actualitza els complements",
|
||||
"TaskRefreshPeopleDescription": "Actualitza les metadades dels actors i directors de la mediateca.",
|
||||
"TaskRefreshPeople": "Actualitza les persones",
|
||||
"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 els registres",
|
||||
"TaskRefreshLibraryDescription": "Escaneja la mediateca, a la cerca de fitxers nous i refresca les metadades.",
|
||||
"TaskRefreshLibrary": "Escaneja la mediateca",
|
||||
"TaskRefreshChapterImagesDescription": "Crea les miniatures dels vídeos que tinguin capítols.",
|
||||
"TaskRefreshChapterImages": "Extreu les imatges dels capítols",
|
||||
"TaskCleanCacheDescription": "Elimina la memòria cau no necessària per al servidor.",
|
||||
"TaskCleanCache": "Elimina la memòria cau",
|
||||
"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": "Mediateca",
|
||||
"TasksMaintenanceCategory": "Manteniment",
|
||||
"TaskCleanActivityLogDescription": "Eliminades les entrades del registre d'activitats més antigues que l'antiguitat configurada.",
|
||||
"TaskCleanActivityLog": "Buida 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 mediateca o fer d'altres canvis que impliquin modificacions a la base de dades pot millorar el rendiment.",
|
||||
"TaskOptimizeDatabase": "Optimitza 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 allargar-se molt en el temps.",
|
||||
"TaskKeyframeExtractor": "Extractor de fotogrames clau",
|
||||
"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": "Genera imatges de previsualització",
|
||||
"TaskRefreshTrickplayImagesDescription": "Crea imatges de previsualització per vídeos en les mediateques 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ó",
|
||||
"TaskCleanCollectionsAndPlaylists": "Neteja de les col·leccions i llistes de reproducció",
|
||||
"TaskAudioNormalization": "Estabilització de l'àudio",
|
||||
"TaskAudioNormalizationDescription": "Escaneja arxius per dades d'estabilització de l'àudio.",
|
||||
"TaskDownloadMissingLyricsDescription": "Baixa les lletres de les cançons",
|
||||
"TaskDownloadMissingLyrics": "Baixa les lletres que falten",
|
||||
"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 mediateca.",
|
||||
"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": "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": "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."
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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.)."
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -137,6 +137,6 @@
|
||||
"TaskMoveTrickplayImages": "Locatie trickplay-afbeeldingen migreren",
|
||||
"TaskMoveTrickplayImagesDescription": "Verplaatst bestaande trickplay-bestanden op basis van de bibliotheekinstellingen.",
|
||||
"TaskExtractMediaSegments": "Scannen op mediasegmenten",
|
||||
"CleanupUserDataTaskDescription": "Wist alle gebruikersgegevens (kijkstatus, favorieten, etc.) van media die al minstens 90 dagen niet meer aanwezig is.",
|
||||
"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 @@
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -138,5 +138,5 @@
|
||||
"TaskExtractMediaSegmentsDescription": "Извлекает или получает медиасегменты из плагинов MediaSegment.",
|
||||
"TaskMoveTrickplayImagesDescription": "Перемещает существующие файлы trickplay в соответствии с настройками медиатеки.",
|
||||
"CleanupUserDataTask": "Задача очистки пользовательских данных",
|
||||
"CleanupUserDataTaskDescription": "Очищает все пользовательские данные (состояние просмотра, статус избранного и т.д.) с носителей, на которых больше нет информации, по крайней мере, в течение 90 дней."
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -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": "பயனர் தரவை சுத்தம் செய்யும் பணி"
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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)!;
|
||||
@@ -296,8 +306,8 @@ public class BackupService : IBackupService
|
||||
.. typeof(JellyfinDbContext)
|
||||
.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
|
||||
.Where(e => e.PropertyType.IsAssignableTo(typeof(IQueryable)))
|
||||
.Select(e => (Type: e.PropertyType, dbContext.Model.FindEntityType(e.PropertyType.GetGenericArguments()[0])!.GetSchemaQualifiedTableName()!, ValueFactory: new Func<IAsyncEnumerable<object>>(() => GetValues((IQueryable)e.GetValue(dbContext)!, e.PropertyType)))),
|
||||
(Type: typeof(HistoryRow), SourceName: nameof(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);
|
||||
@@ -309,7 +319,7 @@ public class BackupService : IBackupService
|
||||
foreach (var entityType in entityTypes)
|
||||
{
|
||||
_logger.LogInformation("Begin backup of entity {Table}", entityType.SourceName);
|
||||
var zipEntry = zipArchive.CreateEntry($"Database\\{entityType.SourceName}.json");
|
||||
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('\\', '/');
|
||||
}
|
||||
|
||||
@@ -110,6 +110,20 @@ public sealed class BaseItemRepository
|
||||
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
|
||||
@@ -256,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;
|
||||
}
|
||||
@@ -275,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/>
|
||||
@@ -317,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 />
|
||||
@@ -468,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();
|
||||
@@ -540,7 +561,7 @@ public sealed class BaseItemRepository
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -567,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();
|
||||
@@ -655,7 +676,7 @@ public sealed class BaseItemRepository
|
||||
return null;
|
||||
}
|
||||
|
||||
return DeserialiseBaseItem(item);
|
||||
return DeserializeBaseItem(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -701,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();
|
||||
@@ -733,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)
|
||||
{
|
||||
@@ -807,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;
|
||||
@@ -867,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;
|
||||
@@ -981,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;
|
||||
}
|
||||
|
||||
@@ -1017,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)
|
||||
@@ -1026,7 +1047,7 @@ public sealed class BaseItemRepository
|
||||
}
|
||||
|
||||
var typeToSerialise = GetType(baseItemEntity.Type);
|
||||
return BaseItemRepository.DeserialiseBaseItem(
|
||||
return BaseItemRepository.DeserializeBaseItem(
|
||||
baseItemEntity,
|
||||
_logger,
|
||||
_appHost,
|
||||
@@ -1034,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>
|
||||
@@ -1042,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)
|
||||
{
|
||||
@@ -1060,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);
|
||||
@@ -1206,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);
|
||||
})
|
||||
];
|
||||
}
|
||||
@@ -1221,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);
|
||||
})
|
||||
];
|
||||
}
|
||||
@@ -1302,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
|
||||
@@ -2239,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)
|
||||
@@ -2250,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))));
|
||||
}
|
||||
|
||||
@@ -2261,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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -253,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;
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
123
Jellyfin.Server/Migrations/Routines/MigrateLibraryUserData.cs
Normal file
123
Jellyfin.Server/Migrations/Routines/MigrateLibraryUserData.cs
Normal file
@@ -0,0 +1,123 @@
|
||||
#pragma warning disable RS0030 // Do not use banned APIs
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Server.Implementations.Data;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using Jellyfin.Server.Implementations.Item;
|
||||
using Jellyfin.Server.ServerSetupApp;
|
||||
using MediaBrowser.Controller;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Server.Migrations.Routines;
|
||||
|
||||
[JellyfinMigration("2025-06-18T01:00:00", nameof(MigrateLibraryUserData))]
|
||||
[JellyfinMigrationBackup(JellyfinDb = true)]
|
||||
internal class MigrateLibraryUserData : IAsyncMigrationRoutine
|
||||
{
|
||||
private const string DbFilename = "library.db.old";
|
||||
|
||||
private readonly IStartupLogger _logger;
|
||||
private readonly IServerApplicationPaths _paths;
|
||||
private readonly IDbContextFactory<JellyfinDbContext> _provider;
|
||||
|
||||
public MigrateLibraryUserData(
|
||||
IStartupLogger<MigrateLibraryDb> startupLogger,
|
||||
IDbContextFactory<JellyfinDbContext> provider,
|
||||
IServerApplicationPaths paths)
|
||||
{
|
||||
_logger = startupLogger;
|
||||
_provider = provider;
|
||||
_paths = paths;
|
||||
}
|
||||
|
||||
public async Task PerformAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Migrating the userdata from library.db.old may take a while, do not stop Jellyfin.");
|
||||
|
||||
var dataPath = _paths.DataPath;
|
||||
var libraryDbPath = Path.Combine(dataPath, DbFilename);
|
||||
if (!File.Exists(libraryDbPath))
|
||||
{
|
||||
_logger.LogError("Cannot migrate userdata from {LibraryDb} as it does not exist. This migration expects the MigrateLibraryDb to run first.", libraryDbPath);
|
||||
return;
|
||||
}
|
||||
|
||||
var dbContext = await _provider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
{
|
||||
if (!await dbContext.BaseItems.AnyAsync(e => e.Id == BaseItemRepository.PlaceholderId, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
// the placeholder baseitem has been deleted by the librarydb migration so we need to readd it.
|
||||
await dbContext.BaseItems.AddAsync(
|
||||
new Database.Implementations.Entities.BaseItemEntity()
|
||||
{
|
||||
Id = BaseItemRepository.PlaceholderId,
|
||||
Type = "PLACEHOLDER",
|
||||
Name = "This is a placeholder item for UserData that has been detacted from its original item"
|
||||
},
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var users = dbContext.Users.AsNoTracking().ToArray();
|
||||
var userIdBlacklist = new HashSet<int>();
|
||||
using var connection = new SqliteConnection($"Filename={libraryDbPath};Mode=ReadOnly");
|
||||
var retentionDate = DateTime.UtcNow;
|
||||
|
||||
var queryResult = connection.Query(
|
||||
"""
|
||||
SELECT key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex FROM UserDatas
|
||||
|
||||
WHERE NOT EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.UserDataKey = UserDatas.key)
|
||||
""");
|
||||
|
||||
var importedUserData = new Dictionary<Guid, List<UserData>>();
|
||||
foreach (var entity in queryResult)
|
||||
{
|
||||
var userData = MigrateLibraryDb.GetUserData(users, entity, userIdBlacklist, _logger);
|
||||
if (userData is null)
|
||||
{
|
||||
var userDataId = entity.GetString(0);
|
||||
var internalUserId = entity.GetInt32(1);
|
||||
|
||||
if (!userIdBlacklist.Contains(internalUserId))
|
||||
{
|
||||
_logger.LogError("Was not able to migrate user data with key {0} because its id {InternalId} does not match any existing user.", userDataId, internalUserId);
|
||||
userIdBlacklist.Add(internalUserId);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var ogId = userData.ItemId;
|
||||
userData.ItemId = BaseItemRepository.PlaceholderId;
|
||||
userData.RetentionDate = retentionDate;
|
||||
if (!importedUserData.TryGetValue(ogId, out var importUserData))
|
||||
{
|
||||
importUserData = [];
|
||||
importedUserData[ogId] = importUserData;
|
||||
}
|
||||
|
||||
importUserData.Add(userData);
|
||||
}
|
||||
|
||||
foreach (var item in importedUserData)
|
||||
{
|
||||
await dbContext.UserData.Where(e => e.ItemId == item.Key).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
|
||||
dbContext.UserData.AddRange(item.Value.DistinctBy(e => e.CustomDataKey)); // old userdata can have fucked up duplicates
|
||||
}
|
||||
|
||||
_logger.LogInformation("Try saving {NewSaved} UserData entries.", dbContext.UserData.Local.Count);
|
||||
await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
74
Jellyfin.Server/Migrations/Routines/ReseedFolderFlag.cs
Normal file
74
Jellyfin.Server/Migrations/Routines/ReseedFolderFlag.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
#pragma warning disable RS0030 // Do not use banned APIs
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Server.Implementations.Data;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using Jellyfin.Server.ServerSetupApp;
|
||||
using MediaBrowser.Controller;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Server.Migrations.Routines;
|
||||
|
||||
[JellyfinMigration("2025-07-30T21:50:00", nameof(ReseedFolderFlag))]
|
||||
[JellyfinMigrationBackup(JellyfinDb = true)]
|
||||
internal class ReseedFolderFlag : IAsyncMigrationRoutine
|
||||
{
|
||||
private const string DbFilename = "library.db.old";
|
||||
|
||||
private readonly IStartupLogger _logger;
|
||||
private readonly IServerApplicationPaths _paths;
|
||||
private readonly IDbContextFactory<JellyfinDbContext> _provider;
|
||||
|
||||
public ReseedFolderFlag(
|
||||
IStartupLogger<MigrateLibraryDb> startupLogger,
|
||||
IDbContextFactory<JellyfinDbContext> provider,
|
||||
IServerApplicationPaths paths)
|
||||
{
|
||||
_logger = startupLogger;
|
||||
_provider = provider;
|
||||
_paths = paths;
|
||||
}
|
||||
|
||||
internal static bool RerunGuardFlag { get; set; } = false;
|
||||
|
||||
public async Task PerformAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (RerunGuardFlag)
|
||||
{
|
||||
_logger.LogInformation("Migration is skipped because it does not apply.");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Migrating the IsFolder flag from library.db.old may take a while, do not stop Jellyfin.");
|
||||
|
||||
var dataPath = _paths.DataPath;
|
||||
var libraryDbPath = Path.Combine(dataPath, DbFilename);
|
||||
if (!File.Exists(libraryDbPath))
|
||||
{
|
||||
_logger.LogError("Cannot migrate IsFolder flag from {LibraryDb} as it does not exist. This migration expects the MigrateLibraryDb to run first.", libraryDbPath);
|
||||
return;
|
||||
}
|
||||
|
||||
var dbContext = await _provider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
{
|
||||
using var connection = new SqliteConnection($"Filename={libraryDbPath};Mode=ReadOnly");
|
||||
var queryResult = connection.Query(
|
||||
"""
|
||||
SELECT guid FROM TypedBaseItems
|
||||
WHERE IsFolder = true
|
||||
""")
|
||||
.Select(entity => entity.GetGuid(0))
|
||||
.ToList();
|
||||
_logger.LogInformation("Migrating the IsFolder flag for {Count} items.", queryResult.Count);
|
||||
foreach (var id in queryResult)
|
||||
{
|
||||
await dbContext.BaseItems.Where(e => e.Id == id).ExecuteUpdateAsync(e => e.SetProperty(f => f.IsFolder, true), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,7 +82,7 @@ internal class CodeMigration(Type migrationType, JellyfinMigrationAttribute meta
|
||||
}
|
||||
}
|
||||
|
||||
private class NestedStartupLogger<TCategory> : StartupLogger<TCategory>, IStartupLogger<TCategory>
|
||||
private class NestedStartupLogger<TCategory> : StartupLogger<TCategory>
|
||||
{
|
||||
public NestedStartupLogger(ILogger logger, StartupLogTopic topic) : base(logger, topic)
|
||||
{
|
||||
|
||||
@@ -19,7 +19,6 @@ using MediaBrowser.Model.System;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
@@ -29,6 +28,8 @@ using Microsoft.Extensions.Primitives;
|
||||
using Morestachio;
|
||||
using Morestachio.Framework.IO.SingleStream;
|
||||
using Morestachio.Rendering;
|
||||
using Serilog;
|
||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||
|
||||
namespace Jellyfin.Server.ServerSetupApp;
|
||||
|
||||
@@ -143,8 +144,10 @@ public sealed class SetupServer : IDisposable
|
||||
var config = _configurationManager.GetNetworkConfiguration()!;
|
||||
_startupServer = Host.CreateDefaultBuilder(["hostBuilder:reloadConfigOnChange=false"])
|
||||
.UseConsoleLifetime()
|
||||
.UseSerilog()
|
||||
.ConfigureServices(serv =>
|
||||
{
|
||||
serv.AddSingleton(this);
|
||||
serv.AddHealthChecks()
|
||||
.AddCheck<SetupHealthcheck>("StartupCheck");
|
||||
serv.Configure<ForwardedHeadersOptions>(options =>
|
||||
|
||||
@@ -1423,23 +1423,14 @@ namespace MediaBrowser.Controller.Entities
|
||||
|
||||
public virtual bool RequiresRefresh()
|
||||
{
|
||||
if (string.IsNullOrEmpty(Path) || DateModified == default)
|
||||
if (string.IsNullOrEmpty(Path) || DateModified == DateTime.MinValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var info = FileSystem.GetFileSystemInfo(Path);
|
||||
if (info.Exists)
|
||||
{
|
||||
if (info.IsDirectory)
|
||||
{
|
||||
return info.LastWriteTimeUtc != DateModified;
|
||||
}
|
||||
|
||||
return info.LastWriteTimeUtc != DateModified;
|
||||
}
|
||||
|
||||
return false;
|
||||
return info.Exists && this.HasChanged(info.LastWriteTimeUtc);
|
||||
}
|
||||
|
||||
public virtual List<string> GetUserDataKeys()
|
||||
|
||||
@@ -114,5 +114,19 @@ namespace MediaBrowser.Controller.Entities
|
||||
source.DeepCopy(dest);
|
||||
return dest;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the item has changed.
|
||||
/// </summary>
|
||||
/// <param name="source">The source object.</param>
|
||||
/// <param name="asOf">The timestamp to detect changes as of.</param>
|
||||
/// <typeparam name="T">Source type.</typeparam>
|
||||
/// <returns>Whether the item has changed.</returns>
|
||||
public static bool HasChanged<T>(this T source, DateTime asOf)
|
||||
where T : BaseItem
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
return source.DateModified.Subtract(asOf).Duration().TotalSeconds > 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -695,7 +695,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
items = GetRecursiveChildren(user, query);
|
||||
}
|
||||
|
||||
return PostFilterAndSort(items, query, true);
|
||||
return PostFilterAndSort(items, query);
|
||||
}
|
||||
|
||||
if (this is not UserRootFolder
|
||||
@@ -959,10 +959,10 @@ namespace MediaBrowser.Controller.Entities
|
||||
items = GetChildren(user, true, childQuery).Where(filter);
|
||||
}
|
||||
|
||||
return PostFilterAndSort(items, query, true);
|
||||
return PostFilterAndSort(items, query);
|
||||
}
|
||||
|
||||
protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query, bool enableSorting)
|
||||
protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query)
|
||||
{
|
||||
var user = query.User;
|
||||
|
||||
@@ -995,7 +995,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
items = UserViewBuilder.FilterForAdjacency(items.ToList(), query.AdjacentTo.Value);
|
||||
}
|
||||
|
||||
return UserViewBuilder.SortAndPage(items, null, query, LibraryManager, enableSorting);
|
||||
return UserViewBuilder.SortAndPage(items, null, query, LibraryManager);
|
||||
}
|
||||
|
||||
private static IEnumerable<BaseItem> CollapseBoxSetItemsIfNeeded(
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Diacritics.Extensions;
|
||||
using Jellyfin.Data;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
@@ -373,8 +374,15 @@ namespace MediaBrowser.Controller.Entities
|
||||
.Where(i => i != other)
|
||||
.Select(e => Enum.Parse<UnratedItem>(e, true)).ToArray();
|
||||
|
||||
ExcludeInheritedTags = user.GetPreference(PreferenceKind.BlockedTags);
|
||||
IncludeInheritedTags = user.GetPreference(PreferenceKind.AllowedTags);
|
||||
ExcludeInheritedTags = user.GetPreference(PreferenceKind.BlockedTags)
|
||||
.Where(tag => !string.IsNullOrWhiteSpace(tag))
|
||||
.Select(tag => tag.RemoveDiacritics().ToLowerInvariant())
|
||||
.ToArray();
|
||||
|
||||
IncludeInheritedTags = user.GetPreference(PreferenceKind.AllowedTags)
|
||||
.Where(tag => !string.IsNullOrWhiteSpace(tag))
|
||||
.Select(tag => tag.RemoveDiacritics().ToLowerInvariant())
|
||||
.ToArray();
|
||||
|
||||
User = user;
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ namespace MediaBrowser.Controller.Entities.TV
|
||||
|
||||
var items = GetEpisodes(user, query.DtoOptions, true).Where(filter);
|
||||
|
||||
return PostFilterAndSort(items, query, false);
|
||||
return PostFilterAndSort(items, query);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -80,7 +80,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
PresetViews = query.PresetViews
|
||||
});
|
||||
|
||||
return UserViewBuilder.SortAndPage(result, null, query, LibraryManager, true);
|
||||
return UserViewBuilder.SortAndPage(result, null, query, LibraryManager);
|
||||
}
|
||||
|
||||
public override int GetChildCount(User user)
|
||||
|
||||
@@ -438,22 +438,18 @@ namespace MediaBrowser.Controller.Entities
|
||||
items = FilterForAdjacency(items.ToList(), query.AdjacentTo.Value);
|
||||
}
|
||||
|
||||
return SortAndPage(items, totalRecordLimit, query, libraryManager, true);
|
||||
return SortAndPage(items, totalRecordLimit, query, libraryManager);
|
||||
}
|
||||
|
||||
public static QueryResult<BaseItem> SortAndPage(
|
||||
IEnumerable<BaseItem> items,
|
||||
int? totalRecordLimit,
|
||||
InternalItemsQuery query,
|
||||
ILibraryManager libraryManager,
|
||||
bool enableSorting)
|
||||
ILibraryManager libraryManager)
|
||||
{
|
||||
if (enableSorting)
|
||||
if (query.OrderBy.Count > 0)
|
||||
{
|
||||
if (query.OrderBy.Count > 0)
|
||||
{
|
||||
items = libraryManager.Sort(items, query.User, query.OrderBy);
|
||||
}
|
||||
items = libraryManager.Sort(items, query.User, query.OrderBy);
|
||||
}
|
||||
|
||||
var itemsArray = totalRecordLimit.HasValue ? items.Take(totalRecordLimit.Value).ToArray() : items.ToArray();
|
||||
|
||||
@@ -33,6 +33,12 @@ namespace MediaBrowser.Controller.Library
|
||||
/// <value>The users ids.</value>
|
||||
IEnumerable<Guid> UsersIds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the user's username is valid.
|
||||
/// </summary>
|
||||
/// <param name="name">The user's username.</param>
|
||||
void ThrowIfInvalidUsername(string name);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the user manager and ensures that a user exists.
|
||||
/// </summary>
|
||||
|
||||
@@ -96,14 +96,33 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
|
||||
}
|
||||
}
|
||||
|
||||
private bool ShouldForceSequentialOperation()
|
||||
{
|
||||
// if the user either set the setting to 1 or it's unset and we have fewer than 4 cores it's better to run sequentially.
|
||||
var fanoutSetting = _serverConfigurationManager.Configuration.LibraryScanFanoutConcurrency;
|
||||
return fanoutSetting == 1 || (fanoutSetting <= 0 && Environment.ProcessorCount <= 3);
|
||||
}
|
||||
|
||||
private int CalculateScanConcurrencyLimit()
|
||||
{
|
||||
// when this is invoked, we already checked ShouldForceSequentialOperation for the sequential check.
|
||||
var fanoutConcurrency = _serverConfigurationManager.Configuration.LibraryScanFanoutConcurrency;
|
||||
if (fanoutConcurrency <= 0)
|
||||
{
|
||||
// in case the user did not set a limit manually, we can assume he has 3 or more cores as already checked by ShouldForceSequentialOperation.
|
||||
return Environment.ProcessorCount - 3;
|
||||
}
|
||||
|
||||
return fanoutConcurrency;
|
||||
}
|
||||
|
||||
private void Worker()
|
||||
{
|
||||
lock (_taskLock)
|
||||
{
|
||||
var fanoutConcurrency = _serverConfigurationManager.Configuration.LibraryScanFanoutConcurrency;
|
||||
var parallelism = (fanoutConcurrency > 0 ? fanoutConcurrency : Environment.ProcessorCount) - _taskRunners.Count;
|
||||
_logger.LogDebug("Spawn {NumberRunners} new runners.", parallelism);
|
||||
for (int i = 0; i < parallelism; i++)
|
||||
var operationFanout = Math.Max(0, CalculateScanConcurrencyLimit() - _taskRunners.Count);
|
||||
_logger.LogDebug("Spawn {NumberRunners} new runners.", operationFanout);
|
||||
for (int i = 0; i < operationFanout; i++)
|
||||
{
|
||||
var stopToken = new CancellationTokenSource();
|
||||
var combinedSource = CancellationTokenSource.CreateLinkedTokenSource(stopToken.Token, _hostApplicationLifetime.ApplicationStopping);
|
||||
@@ -223,7 +242,7 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
|
||||
};
|
||||
}).ToArray();
|
||||
|
||||
if (_serverConfigurationManager.Configuration.LibraryScanFanoutConcurrency == 1)
|
||||
if (ShouldForceSequentialOperation())
|
||||
{
|
||||
_logger.LogDebug("Process sequentially.");
|
||||
try
|
||||
|
||||
@@ -230,10 +230,10 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
var hwType = encodingOptions.HardwareAccelerationType;
|
||||
|
||||
// Only Intel has VA-API MJPEG encoder
|
||||
// Only enable VA-API MJPEG encoder on Intel iHD driver.
|
||||
// Legacy platforms supported ONLY by i965 do not support MJPEG encoder.
|
||||
if (hwType == HardwareAccelerationType.vaapi
|
||||
&& !(_mediaEncoder.IsVaapiDeviceInteliHD
|
||||
|| _mediaEncoder.IsVaapiDeviceInteli965))
|
||||
&& !_mediaEncoder.IsVaapiDeviceInteliHD)
|
||||
{
|
||||
return _defaultMjpegEncoder;
|
||||
}
|
||||
@@ -2390,6 +2390,12 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|| (requestHasSDR && videoStream.VideoRangeType == VideoRangeType.DOVIWithSDR)
|
||||
|| (requestHasHDR10 && videoStream.VideoRangeType == VideoRangeType.HDR10Plus)))
|
||||
{
|
||||
// If the video stream is in a static HDR format, don't allow copy if the client does not support HDR10 or HLG.
|
||||
if (videoStream.VideoRangeType is VideoRangeType.HDR10 or VideoRangeType.HLG)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check complicated cases where we need to remove dynamic metadata
|
||||
// Conservatively refuse to copy if the encoder can't remove dynamic metadata,
|
||||
// but a removal is required for compatability reasons.
|
||||
@@ -4442,6 +4448,13 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
var swapOutputWandH = doVppTranspose && swapWAndH;
|
||||
var hwScaleFilter = GetHwScaleFilter("vpp", "qsv", outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
|
||||
// d3d11va doesn't support dynamic pool size, use vpp filter ctx to relay
|
||||
// to prevent encoder async and bframes from exhausting the decoder pool.
|
||||
if (!string.IsNullOrEmpty(hwScaleFilter) && isD3d11vaDecoder)
|
||||
{
|
||||
hwScaleFilter += ":passthrough=0";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(hwScaleFilter) && doVppTranspose)
|
||||
{
|
||||
hwScaleFilter += $":transpose={transposeDir}";
|
||||
@@ -7138,7 +7151,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
inputModifier += " -async " + state.InputAudioSync;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(state.InputVideoSync))
|
||||
// The -fps_mode option cannot be applied to input
|
||||
if (!string.IsNullOrEmpty(state.InputVideoSync) && _mediaEncoder.EncoderVersion < new Version(5, 1))
|
||||
{
|
||||
inputModifier += GetVideoSyncOption(state.InputVideoSync, _mediaEncoder.EncoderVersion);
|
||||
}
|
||||
|
||||
@@ -235,11 +235,11 @@ namespace MediaBrowser.LocalMetadata.Savers
|
||||
{
|
||||
if (item is Person)
|
||||
{
|
||||
await writer.WriteElementStringAsync(null, "DeathDate", null, item.EndDate.Value.ToLocalTime().ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)).ConfigureAwait(false);
|
||||
await writer.WriteElementStringAsync(null, "DeathDate", null, item.EndDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)).ConfigureAwait(false);
|
||||
}
|
||||
else if (item is not Episode)
|
||||
{
|
||||
await writer.WriteElementStringAsync(null, "EndDate", null, item.EndDate.Value.ToLocalTime().ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)).ConfigureAwait(false);
|
||||
await writer.WriteElementStringAsync(null, "EndDate", null, item.EndDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -827,7 +827,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<string> ExtractVideoImagesOnIntervalAccelerated(
|
||||
public async Task<string> ExtractVideoImagesOnIntervalAccelerated(
|
||||
string inputFile,
|
||||
string container,
|
||||
MediaSourceInfo mediaSource,
|
||||
@@ -918,18 +918,34 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
inputArg = "-hwaccel_flags +low_priority " + inputArg;
|
||||
}
|
||||
|
||||
if (enableKeyFrameOnlyExtraction)
|
||||
{
|
||||
inputArg = "-skip_frame nokey " + inputArg;
|
||||
}
|
||||
|
||||
var filterParam = encodingHelper.GetVideoProcessingFilterParam(jobState, options, vidEncoder).Trim();
|
||||
if (string.IsNullOrWhiteSpace(filterParam))
|
||||
{
|
||||
throw new InvalidOperationException("EncodingHelper returned empty or invalid filter parameters.");
|
||||
}
|
||||
|
||||
return ExtractVideoImagesOnIntervalInternal(inputArg, filterParam, vidEncoder, threads, qualityScale, priority, cancellationToken);
|
||||
try
|
||||
{
|
||||
return await ExtractVideoImagesOnIntervalInternal(
|
||||
(enableKeyFrameOnlyExtraction ? "-skip_frame nokey " : string.Empty) + inputArg,
|
||||
filterParam,
|
||||
vidEncoder,
|
||||
threads,
|
||||
qualityScale,
|
||||
priority,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (FfmpegException ex)
|
||||
{
|
||||
if (!enableKeyFrameOnlyExtraction)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
_logger.LogWarning(ex, "I-frame trickplay extraction failed, will attempt standard way. Input: {InputFile}", inputFile);
|
||||
}
|
||||
|
||||
return await ExtractVideoImagesOnIntervalInternal(inputArg, filterParam, vidEncoder, threads, qualityScale, priority, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<string> ExtractVideoImagesOnIntervalInternal(
|
||||
@@ -1071,11 +1087,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
}
|
||||
}
|
||||
|
||||
var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
|
||||
|
||||
if (exitCode == -1)
|
||||
if (!ranToCompletion || processWrapper.ExitCode != 0)
|
||||
{
|
||||
_logger.LogError("ffmpeg image extraction failed for {ProcessDescription}", processDescription);
|
||||
// Cleanup temp folder here, because the targetDirectory is not returned and the cleanup for failed ffmpeg process is not possible for caller.
|
||||
// Ideally the ffmpeg should not write any files if it fails, but it seems like it is not guaranteed.
|
||||
try
|
||||
|
||||
@@ -489,10 +489,15 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
try
|
||||
{
|
||||
var subtitleStreams = mediaSource.MediaStreams
|
||||
.Where(stream => stream is { IsExtractableSubtitleStream: true, SupportsExternalStream: true, IsExternal: false });
|
||||
.Where(stream => stream is { IsExtractableSubtitleStream: true, SupportsExternalStream: true });
|
||||
|
||||
foreach (var subtitleStream in subtitleStreams)
|
||||
{
|
||||
if (subtitleStream.IsExternal && !subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitleFileExtension(subtitleStream));
|
||||
|
||||
var releaser = await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false);
|
||||
@@ -510,6 +515,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
if (extractableStreams.Count > 0)
|
||||
{
|
||||
await ExtractAllExtractableSubtitlesInternal(mediaSource, extractableStreams, cancellationToken).ConfigureAwait(false);
|
||||
await ExtractAllExtractableSubtitlesMKS(mediaSource, extractableStreams, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -522,6 +528,72 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExtractAllExtractableSubtitlesMKS(
|
||||
MediaSourceInfo mediaSource,
|
||||
List<MediaStream> subtitleStreams,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var mksFiles = new List<string>();
|
||||
|
||||
foreach (var subtitleStream in subtitleStreams)
|
||||
{
|
||||
if (string.IsNullOrEmpty(subtitleStream.Path) || !subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!mksFiles.Contains(subtitleStream.Path))
|
||||
{
|
||||
mksFiles.Add(subtitleStream.Path);
|
||||
}
|
||||
}
|
||||
|
||||
if (mksFiles.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (string mksFile in mksFiles)
|
||||
{
|
||||
var inputPath = _mediaEncoder.GetInputArgument(mksFile, mediaSource);
|
||||
var outputPaths = new List<string>();
|
||||
var args = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"-i {0} -copyts",
|
||||
inputPath);
|
||||
|
||||
foreach (var subtitleStream in subtitleStreams)
|
||||
{
|
||||
if (!subtitleStream.Path.Equals(mksFile, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitleFileExtension(subtitleStream));
|
||||
var outputCodec = IsCodecCopyable(subtitleStream.Codec) ? "copy" : "srt";
|
||||
var streamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream);
|
||||
|
||||
if (streamIndex == -1)
|
||||
{
|
||||
_logger.LogError("Cannot find subtitle stream index for {InputPath} ({Index}), skipping this stream", inputPath, subtitleStream.Index);
|
||||
continue;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new FileNotFoundException($"Calculated path ({outputPath}) is not valid."));
|
||||
|
||||
outputPaths.Add(outputPath);
|
||||
args += string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
" -map 0:{0} -an -vn -c:s {1} \"{2}\"",
|
||||
streamIndex,
|
||||
outputCodec,
|
||||
outputPath);
|
||||
}
|
||||
|
||||
await ExtractSubtitlesForFile(inputPath, args, outputPaths, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExtractAllExtractableSubtitlesInternal(
|
||||
MediaSourceInfo mediaSource,
|
||||
List<MediaStream> subtitleStreams,
|
||||
@@ -536,6 +608,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
|
||||
foreach (var subtitleStream in subtitleStreams)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(subtitleStream.Path) && subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogDebug("Subtitle {Index} for file {InputPath} is part in an MKS file. Skipping", inputPath, subtitleStream.Index);
|
||||
continue;
|
||||
}
|
||||
|
||||
var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitleFileExtension(subtitleStream));
|
||||
var outputCodec = IsCodecCopyable(subtitleStream.Codec) ? "copy" : "srt";
|
||||
var streamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream);
|
||||
@@ -557,6 +635,20 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
outputPath);
|
||||
}
|
||||
|
||||
if (outputPaths.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await ExtractSubtitlesForFile(inputPath, args, outputPaths, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task ExtractSubtitlesForFile(
|
||||
string inputPath,
|
||||
string args,
|
||||
List<string> outputPaths,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
int exitCode;
|
||||
|
||||
using (var process = new Process
|
||||
|
||||
@@ -569,7 +569,7 @@ namespace MediaBrowser.Model.Dto
|
||||
/// Gets or sets the trickplay manifest.
|
||||
/// </summary>
|
||||
/// <value>The trickplay manifest.</value>
|
||||
public Dictionary<string, Dictionary<int, TrickplayInfo>> Trickplay { get; set; }
|
||||
public Dictionary<string, Dictionary<int, TrickplayInfoDto>> Trickplay { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the location.
|
||||
|
||||
62
MediaBrowser.Model/Dto/TrickplayInfoDto.cs
Normal file
62
MediaBrowser.Model/Dto/TrickplayInfoDto.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
|
||||
namespace MediaBrowser.Model.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// The trickplay api model.
|
||||
/// </summary>
|
||||
public record TrickplayInfoDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TrickplayInfoDto"/> class.
|
||||
/// </summary>
|
||||
/// <param name="info">The trickplay info.</param>
|
||||
public TrickplayInfoDto(TrickplayInfo info)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(info);
|
||||
|
||||
Width = info.Width;
|
||||
Height = info.Height;
|
||||
TileWidth = info.TileWidth;
|
||||
TileHeight = info.TileHeight;
|
||||
ThumbnailCount = info.ThumbnailCount;
|
||||
Interval = info.Interval;
|
||||
Bandwidth = info.Bandwidth;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the width of an individual thumbnail.
|
||||
/// </summary>
|
||||
public int Width { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the height of an individual thumbnail.
|
||||
/// </summary>
|
||||
public int Height { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the amount of thumbnails per row.
|
||||
/// </summary>
|
||||
public int TileWidth { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the amount of thumbnails per column.
|
||||
/// </summary>
|
||||
public int TileHeight { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total amount of non-black thumbnails.
|
||||
/// </summary>
|
||||
public int ThumbnailCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the interval in milliseconds between each trickplay thumbnail.
|
||||
/// </summary>
|
||||
public int Interval { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the peak bandwidth usage in bits per second.
|
||||
/// </summary>
|
||||
public int Bandwidth { get; init; }
|
||||
}
|
||||
@@ -273,11 +273,28 @@ namespace MediaBrowser.Model.Entities
|
||||
// Do not display the language code in display titles if unset or set to a special code. Show it in all other cases (possibly expanded).
|
||||
if (!string.IsNullOrEmpty(Language) && !_specialCodes.Contains(Language, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Get full language string i.e. eng -> English.
|
||||
string fullLanguage = CultureInfo
|
||||
.GetCultures(CultureTypes.NeutralCultures)
|
||||
.FirstOrDefault(r => r.ThreeLetterISOLanguageName.Equals(Language, StringComparison.OrdinalIgnoreCase))
|
||||
?.DisplayName;
|
||||
// Get full language string i.e. eng -> English, zh-Hans -> Chinese (Simplified).
|
||||
var cultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
|
||||
CultureInfo match = null;
|
||||
if (Language.Contains('-', StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
match = cultures.FirstOrDefault(r =>
|
||||
r.Name.Equals(Language, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (match is null)
|
||||
{
|
||||
string baseLang = Language.AsSpan().LeftPart('-').ToString();
|
||||
match = cultures.FirstOrDefault(r =>
|
||||
r.TwoLetterISOLanguageName.Equals(baseLang, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
match = cultures.FirstOrDefault(r =>
|
||||
r.ThreeLetterISOLanguageName.Equals(Language, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
string fullLanguage = match?.DisplayName;
|
||||
attributes.Add(StringHelper.FirstToUpper(fullLanguage ?? Language));
|
||||
}
|
||||
|
||||
@@ -376,11 +393,28 @@ namespace MediaBrowser.Model.Entities
|
||||
|
||||
if (!string.IsNullOrEmpty(Language))
|
||||
{
|
||||
// Get full language string i.e. eng -> English.
|
||||
string fullLanguage = CultureInfo
|
||||
.GetCultures(CultureTypes.NeutralCultures)
|
||||
.FirstOrDefault(r => r.ThreeLetterISOLanguageName.Equals(Language, StringComparison.OrdinalIgnoreCase))
|
||||
?.DisplayName;
|
||||
// Get full language string i.e. eng -> English, zh-Hans -> Chinese (Simplified).
|
||||
var cultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
|
||||
CultureInfo match = null;
|
||||
if (Language.Contains('-', StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
match = cultures.FirstOrDefault(r =>
|
||||
r.Name.Equals(Language, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (match is null)
|
||||
{
|
||||
string baseLang = Language.AsSpan().LeftPart('-').ToString();
|
||||
match = cultures.FirstOrDefault(r =>
|
||||
r.TwoLetterISOLanguageName.Equals(baseLang, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
match = cultures.FirstOrDefault(r =>
|
||||
r.ThreeLetterISOLanguageName.Equals(Language, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
string fullLanguage = match?.DisplayName;
|
||||
attributes.Add(StringHelper.FirstToUpper(fullLanguage ?? Language));
|
||||
}
|
||||
else
|
||||
|
||||
@@ -8,21 +8,28 @@ public class LyricLineCue
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LyricLineCue"/> class.
|
||||
/// </summary>
|
||||
/// <param name="position">The start of the character index of the lyric.</param>
|
||||
/// <param name="position">The start character index of the cue.</param>
|
||||
/// <param name="endPosition">The end character index of the cue.</param>
|
||||
/// <param name="start">The start of the timestamp the lyric is synced to in ticks.</param>
|
||||
/// <param name="end">The end of the timestamp the lyric is synced to in ticks.</param>
|
||||
public LyricLineCue(int position, long start, long? end)
|
||||
public LyricLineCue(int position, int endPosition, long start, long? end)
|
||||
{
|
||||
Position = position;
|
||||
EndPosition = endPosition;
|
||||
Start = start;
|
||||
End = end;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the character index of the lyric.
|
||||
/// Gets the start character index of the cue.
|
||||
/// </summary>
|
||||
public int Position { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the end character index of the cue.
|
||||
/// </summary>
|
||||
public int EndPosition { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the timestamp the lyric is synced to in ticks.
|
||||
/// </summary>
|
||||
|
||||
@@ -56,6 +56,7 @@ namespace MediaBrowser.Model.Net
|
||||
".rec",
|
||||
".ts",
|
||||
".rmvb",
|
||||
".vob",
|
||||
".webm",
|
||||
".wmv",
|
||||
".wtv",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user