mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-05-13 04:06:31 +01:00
Compare commits
288 Commits
renovate/s
...
renovate/m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8435dfb779 | ||
|
|
e9942c3857 | ||
|
|
04ecf77d97 | ||
|
|
b9ee9b0660 | ||
|
|
27a3ccb7e4 | ||
|
|
a9865367d8 | ||
|
|
c169184e01 | ||
|
|
5bcea608c5 | ||
|
|
22bf421be6 | ||
|
|
6d3a7b6f69 | ||
|
|
406aaabefd | ||
|
|
ef64f19eac | ||
|
|
2800ae3b8a | ||
|
|
7ead333f6e | ||
|
|
9be1246749 | ||
|
|
2229851689 | ||
|
|
21f12a1ad0 | ||
|
|
be0f0b9761 | ||
|
|
f24709f11c | ||
|
|
4f238ca9b3 | ||
|
|
42870986a8 | ||
|
|
82120732ca | ||
|
|
9a3dfec151 | ||
|
|
02835c6144 | ||
|
|
6d6dee9492 | ||
|
|
fc251265d9 | ||
|
|
5cb6ac521a | ||
|
|
4dd19b990b | ||
|
|
efa502a65f | ||
|
|
b7b405dc83 | ||
|
|
149649a6cf | ||
|
|
169745fddb | ||
|
|
c4c521719e | ||
|
|
e9cad048e9 | ||
|
|
de64a69c7a | ||
|
|
c328dbef10 | ||
|
|
8437866ffa | ||
|
|
b84bd34c67 | ||
|
|
82b51a60a5 | ||
|
|
5cdfb9bfac | ||
|
|
6c59f9a03d | ||
|
|
e1e18e8da0 | ||
|
|
63a078b720 | ||
|
|
d636b82e83 | ||
|
|
bc074b5283 | ||
|
|
3876a0ad3d | ||
|
|
a629080c89 | ||
|
|
2fbb821581 | ||
|
|
33ed52b8ee | ||
|
|
d1ab428476 | ||
|
|
142b89eab5 | ||
|
|
1bbbc1c823 | ||
|
|
a8f361f8c0 | ||
|
|
c0593281ff | ||
|
|
d648aba881 | ||
|
|
44d5954205 | ||
|
|
10c42d70ca | ||
|
|
da88a06ede | ||
|
|
842a5efdcf | ||
|
|
e84fd95bcc | ||
|
|
28546f535c | ||
|
|
e50aeb914a | ||
|
|
087e745e7a | ||
|
|
c8c890b9e5 | ||
|
|
4b8be6bc91 | ||
|
|
ec054f6a34 | ||
|
|
d3e6079d38 | ||
|
|
9ade1fb8f6 | ||
|
|
8a43b1c784 | ||
|
|
4178e0ebaf | ||
|
|
064fd8c5c0 | ||
|
|
7be1350205 | ||
|
|
b227f3e85b | ||
|
|
fd6badf096 | ||
|
|
7e4727fff8 | ||
|
|
6f2e42c20c | ||
|
|
d4f91ab5ca | ||
|
|
0d58c773f9 | ||
|
|
6be96100c7 | ||
|
|
57c0fcd674 | ||
|
|
6ea2f05497 | ||
|
|
ec04313317 | ||
|
|
d87e9f9622 | ||
|
|
10de1ce8fe | ||
|
|
5d5ae271a5 | ||
|
|
d707a9dba1 | ||
|
|
d359d2f7a8 | ||
|
|
ba268cc3fb | ||
|
|
dcba6c3659 | ||
|
|
57821e4cde | ||
|
|
b7d0301099 | ||
|
|
2365cea626 | ||
|
|
fa65a392b0 | ||
|
|
ec990be12a | ||
|
|
0f6bab03eb | ||
|
|
88cad2ad1a | ||
|
|
d20c775daf | ||
|
|
b8e25b49b3 | ||
|
|
622947e374 | ||
|
|
6e22075a63 | ||
|
|
d68d0fa962 | ||
|
|
00b08c0b32 | ||
|
|
0183127d2a | ||
|
|
d9ced0d639 | ||
|
|
f5f75ed2e1 | ||
|
|
df6f706c2f | ||
|
|
3aed429120 | ||
|
|
f9012b6411 | ||
|
|
6293e7a3c9 | ||
|
|
9404fa2b27 | ||
|
|
19b756a507 | ||
|
|
4e94c3e28b | ||
|
|
127d924c5b | ||
|
|
8150a51238 | ||
|
|
fd80a9d916 | ||
|
|
e75f7f1b28 | ||
|
|
f7bfad8673 | ||
|
|
3690d0bf86 | ||
|
|
4e257364b6 | ||
|
|
58e7ff7f9d | ||
|
|
68ab585894 | ||
|
|
105492ac28 | ||
|
|
bb12b122c3 | ||
|
|
6d15f693b5 | ||
|
|
e12fbe08a4 | ||
|
|
e9af1588f2 | ||
|
|
b733857da2 | ||
|
|
44a5c6b3dd | ||
|
|
035d8f06cc | ||
|
|
c59d3bb21a | ||
|
|
9962fbbe2e | ||
|
|
070e2b2d0c | ||
|
|
755ef1f942 | ||
|
|
a1f3da1819 | ||
|
|
d19449e6a5 | ||
|
|
fc866a64e0 | ||
|
|
9f5f18d2db | ||
|
|
8044156df5 | ||
|
|
f9a7cd7457 | ||
|
|
1dd8541ed5 | ||
|
|
d86a4d6815 | ||
|
|
857e730168 | ||
|
|
1691940c3f | ||
|
|
44957300ab | ||
|
|
c483619928 | ||
|
|
b717754ed8 | ||
|
|
f806ae4018 | ||
|
|
8a1ad14faf | ||
|
|
e71bb7e904 | ||
|
|
31e8e197cf | ||
|
|
5b4882c102 | ||
|
|
60e01e1f22 | ||
|
|
e0f50f504a | ||
|
|
d8bbb4dfe8 | ||
|
|
3ef7ada736 | ||
|
|
24a0df9a39 | ||
|
|
e44821e8f6 | ||
|
|
99ad70fbc8 | ||
|
|
6fdfc6a61b | ||
|
|
d5f4c624e3 | ||
|
|
a12736a0ce | ||
|
|
3a4dff8cc4 | ||
|
|
4fe3abdc0e | ||
|
|
1219c5ec3b | ||
|
|
2147f57df5 | ||
|
|
f793acc1aa | ||
|
|
aa96ff42e6 | ||
|
|
e065015d6d | ||
|
|
ebc15d3e27 | ||
|
|
4b48cad6f7 | ||
|
|
27d54c5b1c | ||
|
|
d218303b93 | ||
|
|
bcd5c33ecd | ||
|
|
352b6c91f8 | ||
|
|
a8a029de73 | ||
|
|
ba722b4517 | ||
|
|
1d8bdcc411 | ||
|
|
e6e7f2a692 | ||
|
|
5882006ee7 | ||
|
|
ea1c1d0468 | ||
|
|
077fa89717 | ||
|
|
268f23f39a | ||
|
|
744c5539d8 | ||
|
|
f5b2e0b8f9 | ||
|
|
59c360aea7 | ||
|
|
3da726463d | ||
|
|
8c0898738d | ||
|
|
d5fb6f99ef | ||
|
|
3d4e4c4572 | ||
|
|
11e16df596 | ||
|
|
febfd7f94a | ||
|
|
8271568677 | ||
|
|
b22c8882d6 | ||
|
|
63c4fc297a | ||
|
|
e70eaf8bc1 | ||
|
|
116a036d56 | ||
|
|
8ee4f951fe | ||
|
|
457c53da6f | ||
|
|
46ffe0af9c | ||
|
|
826e21ecc8 | ||
|
|
885b45838c | ||
|
|
bd6bf6ee3c | ||
|
|
2d0d497961 | ||
|
|
4bd9dbe910 | ||
|
|
b7da5c1860 | ||
|
|
9aa69eded9 | ||
|
|
b5f5b02787 | ||
|
|
41d2070008 | ||
|
|
61ff36d761 | ||
|
|
100d6bb38c | ||
|
|
66c11231b2 | ||
|
|
34c1e45bc2 | ||
|
|
ea0641b659 | ||
|
|
d63b2b2657 | ||
|
|
6ce5f9dfd5 | ||
|
|
ed43ad0968 | ||
|
|
27396bffc6 | ||
|
|
d156e04c9a | ||
|
|
5541653f73 | ||
|
|
ae5420d4ae | ||
|
|
0f1a6fe4c2 | ||
|
|
97340edf02 | ||
|
|
c4c3e9ea4d | ||
|
|
561e78efb4 | ||
|
|
ff0a64ecb9 | ||
|
|
679664ca28 | ||
|
|
b0eec00e1c | ||
|
|
e49d71707c | ||
|
|
3a090a5716 | ||
|
|
f96c399e62 | ||
|
|
0f75518287 | ||
|
|
893188ab28 | ||
|
|
de32e2eb6f | ||
|
|
84962cbc94 | ||
|
|
ba356638e8 | ||
|
|
3439d3c017 | ||
|
|
837c7d4ed3 | ||
|
|
4ce03ffa21 | ||
|
|
50cabcd99d | ||
|
|
9730aaac57 | ||
|
|
340bcafd3d | ||
|
|
f5c9a4a476 | ||
|
|
edec464306 | ||
|
|
5dcec831f3 | ||
|
|
737abe6f3a | ||
|
|
edc6caf255 | ||
|
|
0a99a78ddc | ||
|
|
71594b4a9a | ||
|
|
bb6c3b4eec | ||
|
|
2420ece5fe | ||
|
|
00dd84035e | ||
|
|
f5d966fcc3 | ||
|
|
98d7c8d59f | ||
|
|
268d88a5fb | ||
|
|
8ddc35a1ce | ||
|
|
46ad25f47d | ||
|
|
0c46004cd9 | ||
|
|
a0346fe5b7 | ||
|
|
aedd2b04a2 | ||
|
|
98b561d62c | ||
|
|
d6a8fa1485 | ||
|
|
042385599f | ||
|
|
09a729effe | ||
|
|
2789532aa8 | ||
|
|
694db80d4c | ||
|
|
a650148dfd | ||
|
|
17e8759a52 | ||
|
|
327ace1d30 | ||
|
|
95a301dc43 | ||
|
|
342846e4fc | ||
|
|
99440f8432 | ||
|
|
2086ac7dd2 | ||
|
|
4a1012fd22 | ||
|
|
89427af41c | ||
|
|
5996c4afce | ||
|
|
dfa78590c2 | ||
|
|
912a963a2b | ||
|
|
f260585917 | ||
|
|
22d8a00716 | ||
|
|
c350fd0f40 | ||
|
|
139d23ddc2 | ||
|
|
cc2ccd1bf3 | ||
|
|
1491494bcb | ||
|
|
0b77f97048 | ||
|
|
d3d4d37e82 | ||
|
|
1c1447362e | ||
|
|
a014cb538e | ||
|
|
378ba937b6 |
6
.github/workflows/ci-codeql-analysis.yml
vendored
6
.github/workflows/ci-codeql-analysis.yml
vendored
@@ -32,13 +32,13 @@ jobs:
|
||||
dotnet-version: '10.0.x'
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||
uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: +security-extended
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||
uses: github/codeql-action/autobuild@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||
uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
|
||||
|
||||
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@ee3806a36b8b2eb9594cb3e5fae045af7e5ead10 # v5.5.6
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@049f7ec958c672fd31d5cc1cb01622dc8d2e23ab # v5.5.10
|
||||
with:
|
||||
reports: "**/coverage.cobertura.xml"
|
||||
targetdir: "merged/"
|
||||
|
||||
@@ -118,6 +118,7 @@
|
||||
- [Phlogi](https://github.com/Phlogi)
|
||||
- [pjeanjean](https://github.com/pjeanjean)
|
||||
- [ploughpuff](https://github.com/ploughpuff)
|
||||
- [poytiis](https://github.com/poytiis)
|
||||
- [pR0Ps](https://github.com/pR0Ps)
|
||||
- [PrplHaz4](https://github.com/PrplHaz4)
|
||||
- [RazeLighter777](https://github.com/RazeLighter777)
|
||||
@@ -228,6 +229,8 @@
|
||||
- [MarcoCoreDuo](https://github.com/MarcoCoreDuo)
|
||||
- [LiHRaM](https://github.com/LiHRaM)
|
||||
- [MSalman5230](https://github.com/MSalman5230)
|
||||
- [dwandw](https://github.com/dwandw)
|
||||
- [Lampan-git](https://github.com/Lampan-git)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<PackageVersion Include="BlurHashSharp" Version="1.4.0-pre.1" />
|
||||
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
|
||||
<PackageVersion Include="coverlet.collector" Version="10.0.0" />
|
||||
<PackageVersion Include="Diacritics" Version="4.1.4" />
|
||||
<PackageVersion Include="Diacritics" Version="4.1.8" />
|
||||
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
|
||||
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
|
||||
<PackageVersion Include="FsCheck.Xunit.v3" Version="3.3.3" />
|
||||
@@ -26,30 +26,30 @@
|
||||
<PackageVersion Include="libse" Version="4.0.12" />
|
||||
<PackageVersion Include="LrcParser" Version="2025.623.0" />
|
||||
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.8" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.8" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="5.3.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.3.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.7" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.5.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.8" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.8" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.8" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.8" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.8" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
|
||||
<PackageVersion Include="MimeTypes" Version="2.5.2" />
|
||||
<PackageVersion Include="Morestachio" Version="5.0.1.631" />
|
||||
<PackageVersion Include="Morestachio" Version="5.0.1.670" />
|
||||
<PackageVersion Include="Moq" Version="4.18.4" />
|
||||
<PackageVersion Include="NEbml" Version="1.1.0.5" />
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
@@ -77,7 +77,7 @@
|
||||
<PackageVersion Include="Svg.Skia" Version="3.4.1" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="10.1.7" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.7" />
|
||||
<PackageVersion Include="System.Text.Json" Version="10.0.7" />
|
||||
<PackageVersion Include="System.Text.Json" Version="10.0.8" />
|
||||
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
||||
<PackageVersion Include="z440.atl.core" Version="7.13.0" />
|
||||
<PackageVersion Include="TMDbLib" Version="3.0.0" />
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#pragma warning disable CA1815
|
||||
|
||||
namespace Emby.Naming.AudioBook
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -379,6 +379,14 @@ namespace Emby.Naming.Common
|
||||
IsNamed = true
|
||||
},
|
||||
|
||||
// "Name - 101.mkv", "Name - 101 [720p].mkv", "Name - 101 (2020).mkv"
|
||||
// Handles absolute episode numbers with hyphen delimiter (common in anime)
|
||||
// Without brackets (bracketed version handled above)
|
||||
new EpisodeExpression(@".*[\\\/](?<seriesname>[^\\\/]+?)[\s_]+-[\s_]+(?<epnumber>[0-9]+)[\s_]*(?:\[.*?\]|\(.*?\))*[\s_]*(?:\.\w+)?$")
|
||||
{
|
||||
IsNamed = true
|
||||
},
|
||||
|
||||
// /server/anything_102.mp4
|
||||
// /server/james.corden.2017.04.20.anne.hathaway.720p.hdtv.x264-crooks.mkv
|
||||
// /server/anything_1996.11.14.mp4
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Naming</PackageId>
|
||||
<VersionPrefix>10.12.0</VersionPrefix>
|
||||
<VersionPrefix>12.0.0</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -12,10 +12,10 @@ namespace Emby.Naming.TV
|
||||
{
|
||||
private static readonly Regex CleanNameRegex = new(@"[ ._\-\[\]]", RegexOptions.Compiled);
|
||||
|
||||
[GeneratedRegex(@"^\s*((?<seasonnumber>(?>\d+))(?:st|nd|rd|th|\.)*(?!\s*[Ee]\d+))\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<rightpart>.*)$", RegexOptions.IgnoreCase)]
|
||||
[GeneratedRegex(@"^\s*((?<seasonnumber>(?>\d+))(?:st|nd|rd|th|\.)*(?!\s*[Ee]\d+))\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul|érie|éria|erie|eria)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<rightpart>.*)$", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex ProcessPre();
|
||||
|
||||
[GeneratedRegex(@"^\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<seasonnumber>\d+?)(?=\d{3,4}p|[^\d]|$)(?!\s*[Ee]\d)(?<rightpart>.*)$", RegexOptions.IgnoreCase)]
|
||||
[GeneratedRegex(@"^\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul|érie|éria|erie|eria)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<seasonnumber>\d+?)(?=\d{3,4}p|[^\d]|$)(?!\s*[Ee]\d)(?<rightpart>.*)$", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex ProcessPost();
|
||||
|
||||
[GeneratedRegex(@"[sS](\d{1,4})(?!\d|[eE]\d)(?=\.|_|-|\[|\]|\s|$)", RegexOptions.None)]
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#pragma warning disable CA1815
|
||||
|
||||
namespace Emby.Naming.Video
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -90,6 +90,7 @@ namespace Emby.Server.Implementations.AppBase
|
||||
CreateAndCheckMarker(ProgramDataPath, "data");
|
||||
CreateAndCheckMarker(CachePath, "cache");
|
||||
CreateAndCheckMarker(DataPath, "data");
|
||||
CreateCacheDirTag(CachePath);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -100,6 +101,26 @@ namespace Emby.Server.Implementations.AppBase
|
||||
CheckOrCreateMarker(path, $".jellyfin-{markerName}", recursive);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a CACHEDIR.TAG file in the specified directory per the Cache Directory Tagging specification.
|
||||
/// This signals to backup tools (e.g. Restic, Borg) that the directory contains cached data
|
||||
/// and can be excluded from backups.
|
||||
/// </summary>
|
||||
/// <param name="path">The cache directory path.</param>
|
||||
internal static void CreateCacheDirTag(string path)
|
||||
{
|
||||
var tagPath = Path.Combine(path, "CACHEDIR.TAG");
|
||||
if (!File.Exists(tagPath))
|
||||
{
|
||||
File.WriteAllText(
|
||||
tagPath,
|
||||
"Signature: 8a477f597d28d172789f06886806bc55\n"
|
||||
+ "# This file is a cache directory tag created by Jellyfin.\n"
|
||||
+ "# For information about cache directory tags, see:\n"
|
||||
+ "#\thttps://bford.info/cachedir/\n");
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetMarkers(string path, bool recursive = false)
|
||||
{
|
||||
return Directory.EnumerateFiles(path, ".jellyfin-*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);
|
||||
|
||||
@@ -228,6 +228,7 @@ namespace Emby.Server.Implementations.AppBase
|
||||
Logger.LogInformation("Setting cache path: {Path}", cachePath);
|
||||
((BaseApplicationPaths)CommonApplicationPaths).CachePath = cachePath;
|
||||
CommonApplicationPaths.CreateAndCheckMarker(((BaseApplicationPaths)CommonApplicationPaths).CachePath, "cache");
|
||||
BaseApplicationPaths.CreateCacheDirTag(cachePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -166,8 +166,6 @@ namespace Emby.Server.Implementations
|
||||
ConfigurationManager.Configuration,
|
||||
ApplicationPaths.PluginsPath,
|
||||
ApplicationVersion);
|
||||
|
||||
_disposableParts.Add(_pluginManager);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -507,7 +505,13 @@ namespace Emby.Server.Implementations
|
||||
|
||||
serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
|
||||
|
||||
serviceCollection.AddSingleton<IItemRepository, BaseItemRepository>();
|
||||
serviceCollection.AddSingleton<BaseItemRepository>();
|
||||
serviceCollection.AddSingleton<IItemRepository>(sp => sp.GetRequiredService<BaseItemRepository>());
|
||||
serviceCollection.AddSingleton<IItemQueryHelpers>(sp => sp.GetRequiredService<BaseItemRepository>());
|
||||
serviceCollection.AddSingleton<IItemPersistenceService, ItemPersistenceService>();
|
||||
serviceCollection.AddSingleton<INextUpService, NextUpService>();
|
||||
serviceCollection.AddSingleton<IItemCountService, ItemCountService>();
|
||||
serviceCollection.AddSingleton<ILinkedChildrenService, LinkedChildrenService>();
|
||||
serviceCollection.AddSingleton<IPeopleRepository, PeopleRepository>();
|
||||
serviceCollection.AddSingleton<IChapterRepository, ChapterRepository>();
|
||||
serviceCollection.AddSingleton<IMediaAttachmentRepository, MediaAttachmentRepository>();
|
||||
@@ -530,6 +534,7 @@ namespace Emby.Server.Implementations
|
||||
serviceCollection.AddSingleton<IMusicManager, MusicManager>();
|
||||
|
||||
serviceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>();
|
||||
serviceCollection.AddSingleton<DotIgnoreIgnoreRule>();
|
||||
|
||||
serviceCollection.AddSingleton<ISearchEngine, SearchEngine>();
|
||||
|
||||
@@ -641,6 +646,7 @@ namespace Emby.Server.Implementations
|
||||
BaseItem.ConfigurationManager = ConfigurationManager;
|
||||
BaseItem.FileSystem = Resolve<IFileSystem>();
|
||||
BaseItem.ItemRepository = Resolve<IItemRepository>();
|
||||
BaseItem.ItemCountService = Resolve<IItemCountService>();
|
||||
BaseItem.LibraryManager = Resolve<ILibraryManager>();
|
||||
BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
|
||||
BaseItem.Logger = Resolve<ILogger<BaseItem>>();
|
||||
@@ -1006,6 +1012,8 @@ namespace Emby.Server.Implementations
|
||||
}
|
||||
|
||||
_disposableParts.Clear();
|
||||
|
||||
_pluginManager?.Dispose();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Chapters;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
@@ -128,7 +129,7 @@ public class ChapterManager : IChapterManager
|
||||
|
||||
var averageChapterDuration = GetAverageDurationBetweenChapters(chapters);
|
||||
var threshold = TimeSpan.FromSeconds(1).Ticks;
|
||||
if (averageChapterDuration < threshold)
|
||||
if (chapters.Count >= 2 && averageChapterDuration < threshold)
|
||||
{
|
||||
_logger.LogInformation("Skipping chapter image extraction for {Video} as the average chapter duration {AverageDuration} was lower than the minimum threshold {Threshold}", video.Name, averageChapterDuration, threshold);
|
||||
extractImages = false;
|
||||
@@ -232,12 +233,22 @@ public class ChapterManager : IChapterManager
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SaveChapters(Video video, IReadOnlyList<ChapterInfo> chapters)
|
||||
public bool Supports(BaseItem item)
|
||||
=> item is Video or Audio;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SaveChapters(BaseItem item, IReadOnlyList<ChapterInfo> chapters)
|
||||
{
|
||||
// Remove any chapters that are outside of the runtime of the video
|
||||
var validChapters = chapters.Where(c => c.StartPositionTicks < video.RunTimeTicks).ToList();
|
||||
_chapterRepository.SaveChapters(video.Id, validChapters);
|
||||
}
|
||||
if (!Supports(item))
|
||||
{
|
||||
_logger.LogWarning("Attempted to save chapters for unsupported item type {Type}: {Name} ({Id})", item.GetType().Name, item.Name, item.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove any chapters that are outside of the runtime of the item
|
||||
var validChapters = chapters.Where(c => c.StartPositionTicks < item.RunTimeTicks).ToList();
|
||||
_chapterRepository.SaveChapters(item.Id, validChapters);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ChapterInfo? GetChapter(Guid baseItemId, int index)
|
||||
|
||||
@@ -272,7 +272,7 @@ namespace Emby.Server.Implementations.Collections
|
||||
{
|
||||
var childItem = _libraryManager.GetItemById(guidId);
|
||||
|
||||
var child = collection.LinkedChildren.FirstOrDefault(i => (i.ItemId.HasValue && i.ItemId.Value.Equals(guidId)) || (childItem is not null && string.Equals(childItem.Path, i.Path, StringComparison.OrdinalIgnoreCase)));
|
||||
var child = collection.LinkedChildren.FirstOrDefault(i => i.ItemId.HasValue && i.ItemId.Value.Equals(guidId));
|
||||
|
||||
if (child is null)
|
||||
{
|
||||
@@ -342,7 +342,7 @@ namespace Emby.Server.Implementations.Collections
|
||||
// this is kind of a performance hack because only Video has alternate versions that should be in a box set?
|
||||
if (item is Video video)
|
||||
{
|
||||
foreach (var childId in video.GetLocalAlternateVersionIds())
|
||||
foreach (var childId in _libraryManager.GetLocalAlternateVersionIds(video))
|
||||
{
|
||||
if (!results.ContainsKey(childId))
|
||||
{
|
||||
|
||||
@@ -5,10 +5,12 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -35,7 +37,11 @@ public class CleanDatabaseScheduledTask : ILibraryPostScanTask
|
||||
|
||||
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false);
|
||||
var deadItemsProgress = new Progress<double>(val => progress.Report(val * 0.8));
|
||||
await CleanDeadItems(cancellationToken, deadItemsProgress).ConfigureAwait(false);
|
||||
|
||||
var playlistProgress = new Progress<double>(val => progress.Report(80 + (val * 0.2)));
|
||||
await CleanOrphanedFilePlaylistsAsync(cancellationToken, playlistProgress).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress)
|
||||
@@ -116,4 +122,32 @@ public class CleanDatabaseScheduledTask : ILibraryPostScanTask
|
||||
|
||||
progress.Report(100);
|
||||
}
|
||||
|
||||
private async Task CleanOrphanedFilePlaylistsAsync(CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
var playlists = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = [BaseItemKind.Playlist],
|
||||
Recursive = true
|
||||
}).OfType<Playlist>().ToList();
|
||||
|
||||
var numComplete = 0;
|
||||
var numItems = Math.Max(playlists.Count, 1);
|
||||
|
||||
foreach (var playlist in playlists)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (playlist.IsFile && !File.Exists(playlist.Path))
|
||||
{
|
||||
_logger.LogInformation("Removing file-based playlist {Name} because source file {Path} no longer exists", playlist.Name, playlist.Path);
|
||||
_libraryManager.DeleteItem(playlist, new DeleteOptions { DeleteFileLocation = false });
|
||||
}
|
||||
|
||||
numComplete++;
|
||||
progress.Report((double)numComplete / numItems * 100);
|
||||
}
|
||||
|
||||
progress.Report(100);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,17 +153,102 @@ namespace Emby.Server.Implementations.Dto
|
||||
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User? user = null, BaseItem? owner = null)
|
||||
public IReadOnlyList<BaseItemDto> GetBaseItemDtos(
|
||||
IReadOnlyList<BaseItem> items,
|
||||
DtoOptions options,
|
||||
User? user = null,
|
||||
BaseItem? owner = null,
|
||||
bool skipVisibilityCheck = false)
|
||||
{
|
||||
var accessibleItems = user is null ? items : items.Where(x => x.IsVisible(user)).ToList();
|
||||
var accessibleItems = skipVisibilityCheck || user is null ? items : items.Where(x => x.IsVisible(user)).ToList();
|
||||
var returnItems = new BaseItemDto[accessibleItems.Count];
|
||||
List<(BaseItem, BaseItemDto)>? programTuples = null;
|
||||
List<(BaseItemDto, LiveTvChannel)>? channelTuples = null;
|
||||
|
||||
// Batch-fetch user data for all items
|
||||
Dictionary<Guid, UserItemData>? userDataBatch = null;
|
||||
if (user is not null && options.EnableUserData)
|
||||
{
|
||||
userDataBatch = _userDataRepository.GetUserDataBatch(accessibleItems, user);
|
||||
}
|
||||
|
||||
// Pre-compute collection folders once to avoid N+1 queries in CanDelete
|
||||
List<Folder>? allCollectionFolders = null;
|
||||
if (user is not null && options.ContainsField(ItemFields.CanDelete))
|
||||
{
|
||||
allCollectionFolders = _libraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList();
|
||||
}
|
||||
|
||||
// Batch-fetch child counts for all folders to avoid N+1 queries
|
||||
Dictionary<Guid, int>? childCountBatch = null;
|
||||
if (options.ContainsField(ItemFields.ChildCount))
|
||||
{
|
||||
var folderIds = accessibleItems.OfType<Folder>().Select(f => f.Id).ToList();
|
||||
if (folderIds.Count > 0)
|
||||
{
|
||||
childCountBatch = _libraryManager.GetChildCountBatch(folderIds, user?.Id);
|
||||
}
|
||||
}
|
||||
|
||||
// Batch-fetch played/total counts for all folders to avoid N+1 queries
|
||||
Dictionary<Guid, (int Played, int Total)>? playedCountBatch = null;
|
||||
if (user is not null && options.EnableUserData)
|
||||
{
|
||||
var folderIds = accessibleItems.OfType<Folder>()
|
||||
.Where(f => f.SupportsUserDataFromChildren && (f.SupportsPlayedStatus || options.ContainsField(ItemFields.RecursiveItemCount)))
|
||||
.Select(f => f.Id).ToList();
|
||||
if (folderIds.Count > 0)
|
||||
{
|
||||
playedCountBatch = _libraryManager.GetPlayedAndTotalCountBatch(folderIds, user);
|
||||
}
|
||||
}
|
||||
|
||||
// Batch-fetch MusicArtist lookups across all items to avoid N+1 queries.
|
||||
IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch = null;
|
||||
var artistNames = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var item in accessibleItems)
|
||||
{
|
||||
if (item is IHasArtist hasArtist)
|
||||
{
|
||||
foreach (var name in hasArtist.Artists)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
artistNames.Add(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (item is IHasAlbumArtist hasAlbumArtist)
|
||||
{
|
||||
foreach (var name in hasAlbumArtist.AlbumArtists)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
artistNames.Add(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (artistNames.Count > 0)
|
||||
{
|
||||
artistsBatch = _libraryManager.GetArtists(artistNames.ToArray());
|
||||
}
|
||||
|
||||
for (int index = 0; index < accessibleItems.Count; index++)
|
||||
{
|
||||
var item = accessibleItems[index];
|
||||
var dto = GetBaseItemDtoInternal(item, options, user, owner);
|
||||
var dto = GetBaseItemDtoInternal(
|
||||
item,
|
||||
options,
|
||||
user,
|
||||
owner,
|
||||
userDataBatch?.GetValueOrDefault(item.Id),
|
||||
allCollectionFolders,
|
||||
childCountBatch,
|
||||
playedCountBatch,
|
||||
artistsBatch);
|
||||
|
||||
if (item is LiveTvChannel tvChannel)
|
||||
{
|
||||
@@ -197,7 +282,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
|
||||
public BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User? user = null, BaseItem? owner = null)
|
||||
{
|
||||
var dto = GetBaseItemDtoInternal(item, options, user, owner);
|
||||
var dto = GetBaseItemDtoInternal(item, options, user, owner, null);
|
||||
if (item is LiveTvChannel tvChannel)
|
||||
{
|
||||
LivetvManager.AddChannelInfo(new[] { (dto, tvChannel) }, options, user);
|
||||
@@ -215,7 +300,16 @@ namespace Emby.Server.Implementations.Dto
|
||||
return dto;
|
||||
}
|
||||
|
||||
private BaseItemDto GetBaseItemDtoInternal(BaseItem item, DtoOptions options, User? user = null, BaseItem? owner = null)
|
||||
private BaseItemDto GetBaseItemDtoInternal(
|
||||
BaseItem item,
|
||||
DtoOptions options,
|
||||
User? user = null,
|
||||
BaseItem? owner = null,
|
||||
UserItemData? userData = null,
|
||||
List<Folder>? allCollectionFolders = null,
|
||||
Dictionary<Guid, int>? childCountBatch = null,
|
||||
Dictionary<Guid, (int Played, int Total)>? playedCountBatch = null,
|
||||
IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch = null)
|
||||
{
|
||||
var dto = new BaseItemDto
|
||||
{
|
||||
@@ -252,7 +346,14 @@ namespace Emby.Server.Implementations.Dto
|
||||
|
||||
if (user is not null)
|
||||
{
|
||||
AttachUserSpecificInfo(dto, item, user, options);
|
||||
AttachUserSpecificInfo(
|
||||
dto,
|
||||
item,
|
||||
user,
|
||||
options,
|
||||
userData,
|
||||
childCountBatch,
|
||||
playedCountBatch);
|
||||
}
|
||||
|
||||
if (item is IHasMediaSources
|
||||
@@ -268,13 +369,15 @@ namespace Emby.Server.Implementations.Dto
|
||||
AttachStudios(dto, item);
|
||||
}
|
||||
|
||||
AttachBasicFields(dto, item, owner, options);
|
||||
AttachBasicFields(dto, item, owner, options, artistsBatch);
|
||||
|
||||
if (options.ContainsField(ItemFields.CanDelete))
|
||||
{
|
||||
dto.CanDelete = user is null
|
||||
? item.CanDelete()
|
||||
: item.CanDelete(user);
|
||||
: allCollectionFolders is not null
|
||||
? item.CanDelete(user, allCollectionFolders)
|
||||
: item.CanDelete(user);
|
||||
}
|
||||
|
||||
if (options.ContainsField(ItemFields.CanDownload))
|
||||
@@ -378,37 +481,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
return;
|
||||
}
|
||||
|
||||
var query = new InternalItemsQuery(user)
|
||||
{
|
||||
Recursive = true,
|
||||
DtoOptions = new DtoOptions(false) { EnableImages = false },
|
||||
IncludeItemTypes = relatedItemKinds
|
||||
};
|
||||
|
||||
switch (dto.Type)
|
||||
{
|
||||
case BaseItemKind.Genre:
|
||||
case BaseItemKind.MusicGenre:
|
||||
query.GenreIds = [dto.Id];
|
||||
break;
|
||||
case BaseItemKind.MusicArtist:
|
||||
query.ArtistIds = [dto.Id];
|
||||
break;
|
||||
case BaseItemKind.Person:
|
||||
query.PersonIds = [dto.Id];
|
||||
break;
|
||||
case BaseItemKind.Studio:
|
||||
query.StudioIds = [dto.Id];
|
||||
break;
|
||||
case BaseItemKind.Year
|
||||
when int.TryParse(dto.Name, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year):
|
||||
query.Years = [year];
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
var counts = _libraryManager.GetItemCounts(query);
|
||||
var counts = _libraryManager.GetItemCountsForNameItem(dto.Type, dto.Id, relatedItemKinds, user);
|
||||
|
||||
dto.AlbumCount = counts.AlbumCount;
|
||||
dto.ArtistCount = counts.ArtistCount;
|
||||
@@ -458,7 +531,14 @@ namespace Emby.Server.Implementations.Dto
|
||||
/// <summary>
|
||||
/// Attaches the user specific info.
|
||||
/// </summary>
|
||||
private void AttachUserSpecificInfo(BaseItemDto dto, BaseItem item, User user, DtoOptions options)
|
||||
private void AttachUserSpecificInfo(
|
||||
BaseItemDto dto,
|
||||
BaseItem item,
|
||||
User user,
|
||||
DtoOptions options,
|
||||
UserItemData? userData = null,
|
||||
Dictionary<Guid, int>? childCountBatch = null,
|
||||
Dictionary<Guid, (int Played, int Total)>? playedCountBatch = null)
|
||||
{
|
||||
if (item.IsFolder)
|
||||
{
|
||||
@@ -466,7 +546,19 @@ namespace Emby.Server.Implementations.Dto
|
||||
|
||||
if (options.EnableUserData)
|
||||
{
|
||||
dto.UserData = _userDataRepository.GetUserDataDto(item, dto, user, options);
|
||||
if (userData is not null)
|
||||
{
|
||||
// Use pre-fetched user data
|
||||
dto.UserData = GetUserItemDataDto(userData, item.Id);
|
||||
(int Played, int Total)? precomputed = playedCountBatch is not null
|
||||
&& playedCountBatch.TryGetValue(item.Id, out var counts) ? counts : null;
|
||||
item.FillUserDataDtoValues(dto.UserData, userData, dto, user, options, precomputed);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fall back to individual fetch
|
||||
dto.UserData = _userDataRepository.GetUserDataDto(item, dto, user, options);
|
||||
}
|
||||
}
|
||||
|
||||
if (!dto.ChildCount.HasValue && item.SourceType == SourceType.Library)
|
||||
@@ -485,7 +577,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
|
||||
if (options.ContainsField(ItemFields.ChildCount))
|
||||
{
|
||||
dto.ChildCount ??= GetChildCount(folder, user);
|
||||
dto.ChildCount ??= GetChildCount(folder, user, childCountBatch);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,7 +595,17 @@ namespace Emby.Server.Implementations.Dto
|
||||
{
|
||||
if (options.EnableUserData)
|
||||
{
|
||||
dto.UserData = _userDataRepository.GetUserDataDto(item, user);
|
||||
if (userData is not null)
|
||||
{
|
||||
// Use pre-fetched user data
|
||||
dto.UserData = GetUserItemDataDto(userData, item.Id);
|
||||
item.FillUserDataDtoValues(dto.UserData, userData, dto, user, options);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fall back to individual fetch
|
||||
dto.UserData = _userDataRepository.GetUserDataDto(item, user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -513,7 +615,25 @@ namespace Emby.Server.Implementations.Dto
|
||||
}
|
||||
}
|
||||
|
||||
private static int GetChildCount(Folder folder, User user)
|
||||
private static UserItemDataDto GetUserItemDataDto(UserItemData data, Guid itemId)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(data);
|
||||
|
||||
return new UserItemDataDto
|
||||
{
|
||||
IsFavorite = data.IsFavorite,
|
||||
Likes = data.Likes,
|
||||
PlaybackPositionTicks = data.PlaybackPositionTicks,
|
||||
PlayCount = data.PlayCount,
|
||||
Rating = data.Rating,
|
||||
Played = data.Played,
|
||||
LastPlayedDate = data.LastPlayedDate,
|
||||
ItemId = itemId,
|
||||
Key = data.Key
|
||||
};
|
||||
}
|
||||
|
||||
private static int GetChildCount(Folder folder, User user, Dictionary<Guid, int>? childCountBatch)
|
||||
{
|
||||
// Right now this is too slow to calculate for top level folders on a per-user basis
|
||||
// Just return something so that apps that are expecting a value won't think the folders are empty
|
||||
@@ -522,6 +642,13 @@ namespace Emby.Server.Implementations.Dto
|
||||
return Random.Shared.Next(1, 10);
|
||||
}
|
||||
|
||||
// Use pre-fetched batch data if available
|
||||
if (childCountBatch is not null && childCountBatch.TryGetValue(folder.Id, out var count))
|
||||
{
|
||||
return count;
|
||||
}
|
||||
|
||||
// Fall back to individual query for special cases (Series, Season, etc.)
|
||||
return folder.GetChildCount(user);
|
||||
}
|
||||
|
||||
@@ -815,7 +942,8 @@ namespace Emby.Server.Implementations.Dto
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="owner">The owner.</param>
|
||||
/// <param name="options">The options.</param>
|
||||
private void AttachBasicFields(BaseItemDto dto, BaseItem item, BaseItem? owner, DtoOptions options)
|
||||
/// <param name="artistsBatch">Optional pre-fetched artist lookup shared across a batch of items.</param>
|
||||
private void AttachBasicFields(BaseItemDto dto, BaseItem item, BaseItem? owner, DtoOptions options, IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch = null)
|
||||
{
|
||||
if (options.ContainsField(ItemFields.DateCreated))
|
||||
{
|
||||
@@ -939,6 +1067,8 @@ namespace Emby.Server.Implementations.Dto
|
||||
dto.OriginalTitle = item.OriginalTitle;
|
||||
}
|
||||
|
||||
dto.OriginalLanguage = item.OriginalLanguage;
|
||||
|
||||
if (options.ContainsField(ItemFields.ParentId))
|
||||
{
|
||||
dto.ParentId = item.DisplayParentId;
|
||||
@@ -1060,7 +1190,8 @@ namespace Emby.Server.Implementations.Dto
|
||||
|
||||
// Include artists that are not in the database yet, e.g., just added via metadata editor
|
||||
// var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList();
|
||||
var artistsLookup = _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpace(e))]);
|
||||
var artistsLookup = artistsBatch
|
||||
?? _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpace(e))]);
|
||||
|
||||
dto.ArtistItems = hasArtist.Artists
|
||||
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||
@@ -1094,7 +1225,8 @@ namespace Emby.Server.Implementations.Dto
|
||||
// })
|
||||
// .ToList();
|
||||
|
||||
var albumArtistsLookup = _libraryManager.GetArtists([.. hasAlbumArtist.AlbumArtists.Where(e => !string.IsNullOrWhiteSpace(e))]);
|
||||
var albumArtistsLookup = artistsBatch
|
||||
?? _libraryManager.GetArtists([.. hasAlbumArtist.AlbumArtists.Where(e => !string.IsNullOrWhiteSpace(e))]);
|
||||
|
||||
dto.AlbumArtists = hasAlbumArtist.AlbumArtists
|
||||
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||
@@ -1132,11 +1264,6 @@ namespace Emby.Server.Implementations.Dto
|
||||
}
|
||||
}
|
||||
|
||||
if (options.ContainsField(ItemFields.Chapters))
|
||||
{
|
||||
dto.Chapters = _chapterManager.GetChapters(item.Id).ToList();
|
||||
}
|
||||
|
||||
if (options.ContainsField(ItemFields.Trickplay))
|
||||
{
|
||||
var trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult();
|
||||
@@ -1150,6 +1277,11 @@ namespace Emby.Server.Implementations.Dto
|
||||
dto.ExtraType = video.ExtraType;
|
||||
}
|
||||
|
||||
if (options.ContainsField(ItemFields.Chapters))
|
||||
{
|
||||
dto.Chapters = _chapterManager.GetChapters(item.Id).ToList();
|
||||
}
|
||||
|
||||
if (options.ContainsField(ItemFields.MediaStreams))
|
||||
{
|
||||
// Add VideoInfo
|
||||
|
||||
@@ -21,6 +21,7 @@ namespace Emby.Server.Implementations.IO
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IServerConfigurationManager _configurationManager;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly DotIgnoreIgnoreRule _dotIgnoreIgnoreRule;
|
||||
|
||||
/// <summary>
|
||||
/// The file system watchers.
|
||||
@@ -47,17 +48,20 @@ namespace Emby.Server.Implementations.IO
|
||||
/// <param name="configurationManager">The configuration manager.</param>
|
||||
/// <param name="fileSystem">The filesystem.</param>
|
||||
/// <param name="appLifetime">The <see cref="IHostApplicationLifetime"/>.</param>
|
||||
/// <param name="dotIgnoreIgnoreRule">The .ignore rule handler.</param>
|
||||
public LibraryMonitor(
|
||||
ILogger<LibraryMonitor> logger,
|
||||
ILibraryManager libraryManager,
|
||||
IServerConfigurationManager configurationManager,
|
||||
IFileSystem fileSystem,
|
||||
IHostApplicationLifetime appLifetime)
|
||||
IHostApplicationLifetime appLifetime,
|
||||
DotIgnoreIgnoreRule dotIgnoreIgnoreRule)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_logger = logger;
|
||||
_configurationManager = configurationManager;
|
||||
_fileSystem = fileSystem;
|
||||
_dotIgnoreIgnoreRule = dotIgnoreIgnoreRule;
|
||||
|
||||
appLifetime.ApplicationStarted.Register(Start);
|
||||
appLifetime.ApplicationStopping.Register(Stop);
|
||||
@@ -354,7 +358,7 @@ namespace Emby.Server.Implementations.IO
|
||||
}
|
||||
|
||||
var fileInfo = _fileSystem.GetFileSystemInfo(path);
|
||||
if (DotIgnoreIgnoreRule.IsIgnored(fileInfo, null))
|
||||
if (_dotIgnoreIgnoreRule.ShouldIgnore(fileInfo, null))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -586,6 +586,12 @@ namespace Emby.Server.Implementations.IO
|
||||
/// <inheritdoc />
|
||||
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, string searchPattern, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
|
||||
{
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
_logger.LogWarning("Directory does not exist: {Path}", path);
|
||||
return [];
|
||||
}
|
||||
|
||||
var enumerationOptions = GetEnumerationOptions(recursive);
|
||||
|
||||
// On linux and macOS the search pattern is case-sensitive
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using BitFaster.Caching.Lru;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
@@ -15,22 +17,36 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
|
||||
{
|
||||
private static readonly bool IsWindows = OperatingSystem.IsWindows();
|
||||
|
||||
private static FileInfo? FindIgnoreFile(DirectoryInfo directory)
|
||||
{
|
||||
for (var current = directory; current is not null; current = current.Parent)
|
||||
{
|
||||
var ignorePath = Path.Join(current.FullName, ".ignore");
|
||||
if (File.Exists(ignorePath))
|
||||
{
|
||||
return new FileInfo(ignorePath);
|
||||
}
|
||||
}
|
||||
private readonly FastConcurrentLru<string, IgnoreFileCacheEntry> _directoryCache;
|
||||
private readonly FastConcurrentLru<string, ParsedIgnoreCacheEntry> _rulesCache;
|
||||
|
||||
return null;
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DotIgnoreIgnoreRule"/> class.
|
||||
/// </summary>
|
||||
public DotIgnoreIgnoreRule()
|
||||
{
|
||||
var cacheSize = Math.Max(100, Environment.ProcessorCount * 100);
|
||||
_directoryCache = new FastConcurrentLru<string, IgnoreFileCacheEntry>(
|
||||
Environment.ProcessorCount,
|
||||
cacheSize,
|
||||
StringComparer.Ordinal);
|
||||
_rulesCache = new FastConcurrentLru<string, ParsedIgnoreCacheEntry>(
|
||||
Environment.ProcessorCount,
|
||||
Math.Max(32, cacheSize / 4),
|
||||
StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent) => IsIgnored(fileInfo, parent);
|
||||
public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent) => IsIgnoredInternal(fileInfo, parent);
|
||||
|
||||
/// <summary>
|
||||
/// Clears the directory lookup cache. The parsed rules cache is not cleared
|
||||
/// as it validates file modification time on each access.
|
||||
/// </summary>
|
||||
public void ClearDirectoryCache()
|
||||
{
|
||||
_directoryCache.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether or not the file is ignored.
|
||||
@@ -38,40 +54,38 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
|
||||
/// <param name="fileInfo">The file information.</param>
|
||||
/// <param name="parent">The parent BaseItem.</param>
|
||||
/// <returns>True if the file should be ignored.</returns>
|
||||
public static bool IsIgnored(FileSystemMetadata fileInfo, BaseItem? parent)
|
||||
public bool IsIgnoredInternal(FileSystemMetadata fileInfo, BaseItem? parent)
|
||||
{
|
||||
var searchDirectory = fileInfo.IsDirectory
|
||||
? new DirectoryInfo(fileInfo.FullName)
|
||||
: new DirectoryInfo(Path.GetDirectoryName(fileInfo.FullName) ?? string.Empty);
|
||||
? fileInfo.FullName
|
||||
: Path.GetDirectoryName(fileInfo.FullName);
|
||||
|
||||
if (string.IsNullOrEmpty(searchDirectory.FullName))
|
||||
if (string.IsNullOrEmpty(searchDirectory))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var ignoreFile = FindIgnoreFile(searchDirectory);
|
||||
var ignoreFile = FindIgnoreFileCached(searchDirectory);
|
||||
if (ignoreFile is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fast path in case the ignore files isn't a symlink and is empty
|
||||
if (ignoreFile.LinkTarget is null && ignoreFile.Length == 0)
|
||||
var parsedEntry = GetParsedRules(ignoreFile);
|
||||
if (parsedEntry is null)
|
||||
{
|
||||
// File was deleted after we cached the path - clear the directory cache entry and return false
|
||||
_directoryCache.TryRemove(searchDirectory, out _);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Empty file means ignore everything
|
||||
if (parsedEntry.IsEmpty)
|
||||
{
|
||||
// Ignore directory if we just have the file
|
||||
return true;
|
||||
}
|
||||
|
||||
var content = GetFileContent(ignoreFile);
|
||||
return string.IsNullOrWhiteSpace(content)
|
||||
|| CheckIgnoreRules(fileInfo.FullName, content, fileInfo.IsDirectory);
|
||||
}
|
||||
|
||||
private static bool CheckIgnoreRules(string path, string ignoreFileContent, bool isDirectory)
|
||||
{
|
||||
// If file has content, base ignoring off the content .gitignore-style rules
|
||||
var rules = ignoreFileContent.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
return CheckIgnoreRules(path, rules, isDirectory);
|
||||
return parsedEntry.Rules.IsIgnored(GetPathToCheck(fileInfo.FullName, fileInfo.IsDirectory));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -117,8 +131,8 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
|
||||
return true;
|
||||
}
|
||||
|
||||
// Mitigate the problem of the Ignore library not handling Windows paths correctly.
|
||||
// See https://github.com/jellyfin/jellyfin/issues/15484
|
||||
// Mitigate the problem of the Ignore library not handling Windows paths correctly.
|
||||
// See https://github.com/jellyfin/jellyfin/issues/15484
|
||||
var pathToCheck = normalizePath ? path.NormalizePath('/') : path;
|
||||
|
||||
// Add trailing slash for directories to match "folder/"
|
||||
@@ -130,11 +144,196 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
|
||||
return ignore.IsIgnored(pathToCheck);
|
||||
}
|
||||
|
||||
private static string GetFileContent(FileInfo ignoreFile)
|
||||
private FileInfo? FindIgnoreFileCached(string directory)
|
||||
{
|
||||
ignoreFile = FileSystemHelper.ResolveLinkTarget(ignoreFile, returnFinalTarget: true) ?? ignoreFile;
|
||||
return ignoreFile.Exists
|
||||
? File.ReadAllText(ignoreFile.FullName)
|
||||
: string.Empty;
|
||||
// Check if we have a cached result for this directory
|
||||
if (_directoryCache.TryGet(directory, out var cached))
|
||||
{
|
||||
return cached.IgnoreFileDirectory is null
|
||||
? null
|
||||
: new FileInfo(Path.Join(cached.IgnoreFileDirectory, ".ignore"));
|
||||
}
|
||||
|
||||
DirectoryInfo startDir;
|
||||
try
|
||||
{
|
||||
startDir = new DirectoryInfo(directory);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Walk up the directory tree to find .ignore file using DirectoryInfo.Parent
|
||||
var checkedDirs = new List<string> { directory };
|
||||
|
||||
for (var current = startDir; current is not null; current = current.Parent)
|
||||
{
|
||||
var currentPath = current.FullName;
|
||||
|
||||
// Check if this intermediate directory is cached
|
||||
if (current != startDir && _directoryCache.TryGet(currentPath, out var parentCached))
|
||||
{
|
||||
// Cache the result for all directories we checked
|
||||
var entry = new IgnoreFileCacheEntry(parentCached.IgnoreFileDirectory);
|
||||
foreach (var dir in checkedDirs)
|
||||
{
|
||||
_directoryCache.AddOrUpdate(dir, entry);
|
||||
}
|
||||
|
||||
return parentCached.IgnoreFileDirectory is null
|
||||
? null
|
||||
: new FileInfo(Path.Join(parentCached.IgnoreFileDirectory, ".ignore"));
|
||||
}
|
||||
|
||||
var ignoreFile = new FileInfo(Path.Join(currentPath, ".ignore"));
|
||||
if (ignoreFile.Exists)
|
||||
{
|
||||
// Cache for all directories we checked
|
||||
var entry = new IgnoreFileCacheEntry(currentPath);
|
||||
foreach (var dir in checkedDirs)
|
||||
{
|
||||
_directoryCache.AddOrUpdate(dir, entry);
|
||||
}
|
||||
|
||||
return ignoreFile;
|
||||
}
|
||||
|
||||
if (current != startDir)
|
||||
{
|
||||
checkedDirs.Add(currentPath);
|
||||
}
|
||||
}
|
||||
|
||||
// No .ignore file found - cache null result for all directories
|
||||
var nullEntry = new IgnoreFileCacheEntry((string?)null);
|
||||
foreach (var dir in checkedDirs)
|
||||
{
|
||||
_directoryCache.AddOrUpdate(dir, nullEntry);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private ParsedIgnoreCacheEntry? GetParsedRules(FileInfo ignoreFile)
|
||||
{
|
||||
if (!ignoreFile.Exists)
|
||||
{
|
||||
_rulesCache.TryRemove(ignoreFile.FullName, out _);
|
||||
return null;
|
||||
}
|
||||
|
||||
var lastModified = ignoreFile.LastWriteTimeUtc;
|
||||
var fileLength = ignoreFile.Length;
|
||||
var key = ignoreFile.FullName;
|
||||
|
||||
// Check cache
|
||||
if (_rulesCache.TryGet(key, out var cached))
|
||||
{
|
||||
if (cached.FileLastModified == lastModified && cached.FileLength == fileLength)
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Stale - need to reparse
|
||||
_rulesCache.TryRemove(key, out _);
|
||||
}
|
||||
|
||||
// Parse the file
|
||||
var parsedEntry = ParseIgnoreFile(ignoreFile, lastModified, fileLength);
|
||||
_rulesCache.AddOrUpdate(key, parsedEntry);
|
||||
return parsedEntry;
|
||||
}
|
||||
|
||||
private static ParsedIgnoreCacheEntry ParseIgnoreFile(FileInfo ignoreFile, DateTime lastModified, long fileLength)
|
||||
{
|
||||
if (ignoreFile.LinkTarget is null && fileLength == 0)
|
||||
{
|
||||
return new ParsedIgnoreCacheEntry
|
||||
{
|
||||
Rules = new Ignore.Ignore(),
|
||||
FileLastModified = lastModified,
|
||||
FileLength = fileLength,
|
||||
IsEmpty = true
|
||||
};
|
||||
}
|
||||
|
||||
// Resolve symlinks
|
||||
var resolvedFile = FileSystemHelper.ResolveLinkTarget(ignoreFile, returnFinalTarget: true) ?? ignoreFile;
|
||||
if (!resolvedFile.Exists)
|
||||
{
|
||||
return new ParsedIgnoreCacheEntry
|
||||
{
|
||||
Rules = new Ignore.Ignore(),
|
||||
FileLastModified = lastModified,
|
||||
FileLength = fileLength,
|
||||
IsEmpty = true
|
||||
};
|
||||
}
|
||||
|
||||
var content = File.ReadAllText(resolvedFile.FullName);
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
{
|
||||
return new ParsedIgnoreCacheEntry
|
||||
{
|
||||
Rules = new Ignore.Ignore(),
|
||||
FileLastModified = lastModified,
|
||||
FileLength = fileLength,
|
||||
IsEmpty = true
|
||||
};
|
||||
}
|
||||
|
||||
var rules = content.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
var ignore = new Ignore.Ignore();
|
||||
var validRulesAdded = 0;
|
||||
|
||||
foreach (var rule in rules)
|
||||
{
|
||||
try
|
||||
{
|
||||
ignore.Add(rule);
|
||||
validRulesAdded++;
|
||||
}
|
||||
catch (RegexParseException)
|
||||
{
|
||||
// Ignore invalid patterns
|
||||
}
|
||||
}
|
||||
|
||||
// No valid rules means treat as empty (ignore all)
|
||||
return new ParsedIgnoreCacheEntry
|
||||
{
|
||||
Rules = ignore,
|
||||
FileLastModified = lastModified,
|
||||
FileLength = fileLength,
|
||||
IsEmpty = validRulesAdded == 0
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetPathToCheck(string path, bool isDirectory)
|
||||
{
|
||||
// Normalize Windows paths
|
||||
var pathToCheck = IsWindows ? path.NormalizePath('/') : path;
|
||||
|
||||
// Add trailing slash for directories to match "folder/"
|
||||
if (isDirectory)
|
||||
{
|
||||
pathToCheck = string.Concat(pathToCheck.AsSpan().TrimEnd('/'), "/");
|
||||
}
|
||||
|
||||
return pathToCheck;
|
||||
}
|
||||
|
||||
private readonly record struct IgnoreFileCacheEntry(string? IgnoreFileDirectory);
|
||||
|
||||
private sealed class ParsedIgnoreCacheEntry
|
||||
{
|
||||
public required Ignore.Ignore Rules { get; init; }
|
||||
|
||||
public required DateTime FileLastModified { get; init; }
|
||||
|
||||
public required long FileLength { get; init; }
|
||||
|
||||
public required bool IsEmpty { get; init; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,18 +30,17 @@ using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.MediaSegments;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
using MediaBrowser.Controller.Sorting;
|
||||
using MediaBrowser.Controller.Trickplay;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Drawing;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
@@ -77,12 +76,17 @@ namespace Emby.Server.Implementations.Library
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IItemRepository _itemRepository;
|
||||
private readonly IItemPersistenceService _persistenceService;
|
||||
private readonly INextUpService _nextUpService;
|
||||
private readonly IItemCountService _countService;
|
||||
private readonly ILinkedChildrenService _linkedChildrenService;
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
private readonly NamingOptions _namingOptions;
|
||||
private readonly IPeopleRepository _peopleRepository;
|
||||
private readonly ExtraResolver _extraResolver;
|
||||
private readonly IPathManager _pathManager;
|
||||
private readonly FastConcurrentLru<Guid, BaseItem> _cache;
|
||||
private readonly DotIgnoreIgnoreRule _dotIgnoreIgnoreRule;
|
||||
|
||||
/// <summary>
|
||||
/// The _root folder sync lock.
|
||||
@@ -115,11 +119,16 @@ namespace Emby.Server.Implementations.Library
|
||||
/// <param name="userViewManagerFactory">The user view manager.</param>
|
||||
/// <param name="mediaEncoder">The media encoder.</param>
|
||||
/// <param name="itemRepository">The item repository.</param>
|
||||
/// <param name="persistenceService">The item persistence service.</param>
|
||||
/// <param name="nextUpService">The next up service.</param>
|
||||
/// <param name="countService">The item count service.</param>
|
||||
/// <param name="linkedChildrenService">The linked children service.</param>
|
||||
/// <param name="imageProcessor">The image processor.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
/// <param name="directoryService">The directory service.</param>
|
||||
/// <param name="peopleRepository">The people repository.</param>
|
||||
/// <param name="pathManager">The path manager.</param>
|
||||
/// <param name="dotIgnoreIgnoreRule">The .ignore rule handler.</param>
|
||||
public LibraryManager(
|
||||
IServerApplicationHost appHost,
|
||||
ILoggerFactory loggerFactory,
|
||||
@@ -133,11 +142,16 @@ namespace Emby.Server.Implementations.Library
|
||||
Lazy<IUserViewManager> userViewManagerFactory,
|
||||
IMediaEncoder mediaEncoder,
|
||||
IItemRepository itemRepository,
|
||||
IItemPersistenceService persistenceService,
|
||||
INextUpService nextUpService,
|
||||
IItemCountService countService,
|
||||
ILinkedChildrenService linkedChildrenService,
|
||||
IImageProcessor imageProcessor,
|
||||
NamingOptions namingOptions,
|
||||
IDirectoryService directoryService,
|
||||
IPeopleRepository peopleRepository,
|
||||
IPathManager pathManager)
|
||||
IPathManager pathManager,
|
||||
DotIgnoreIgnoreRule dotIgnoreIgnoreRule)
|
||||
{
|
||||
_appHost = appHost;
|
||||
_logger = loggerFactory.CreateLogger<LibraryManager>();
|
||||
@@ -151,6 +165,10 @@ namespace Emby.Server.Implementations.Library
|
||||
_userViewManagerFactory = userViewManagerFactory;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_itemRepository = itemRepository;
|
||||
_persistenceService = persistenceService;
|
||||
_nextUpService = nextUpService;
|
||||
_countService = countService;
|
||||
_linkedChildrenService = linkedChildrenService;
|
||||
_imageProcessor = imageProcessor;
|
||||
|
||||
_cache = new FastConcurrentLru<Guid, BaseItem>(_configurationManager.Configuration.CacheSize);
|
||||
@@ -158,6 +176,7 @@ namespace Emby.Server.Implementations.Library
|
||||
_namingOptions = namingOptions;
|
||||
_peopleRepository = peopleRepository;
|
||||
_pathManager = pathManager;
|
||||
_dotIgnoreIgnoreRule = dotIgnoreIgnoreRule;
|
||||
_extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService);
|
||||
|
||||
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
|
||||
@@ -327,9 +346,17 @@ namespace Emby.Server.Implementations.Library
|
||||
DeleteItem(item, options, parent, notifyParentItem);
|
||||
}
|
||||
|
||||
public void DeleteItemsUnsafeFast(IEnumerable<BaseItem> items)
|
||||
public void DeleteItemsUnsafeFast(IReadOnlyCollection<BaseItem> items, bool deleteSourceFiles = false)
|
||||
{
|
||||
var pathMaps = items.Select(e => (Item: e, InternalPath: GetInternalMetadataPaths(e), DeletePaths: e.GetDeletePaths())).ToArray();
|
||||
if (items.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var pathMaps = items.Select(e =>
|
||||
(Item: e,
|
||||
InternalPath: GetInternalMetadataPaths(e),
|
||||
DeletePaths: deleteSourceFiles ? e.GetDeletePaths() : [])).ToArray();
|
||||
|
||||
foreach (var (item, internalPaths, pathsToDelete) in pathMaps)
|
||||
{
|
||||
@@ -363,7 +390,7 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
_itemRepository.DeleteItem([.. pathMaps.Select(f => f.Item.Id)]);
|
||||
_persistenceService.DeleteItem([.. pathMaps.Select(f => f.Item.Id)]);
|
||||
}
|
||||
|
||||
public void DeleteItem(BaseItem item, DeleteOptions options, BaseItem parent, bool notifyParentItem)
|
||||
@@ -406,6 +433,99 @@ namespace Emby.Server.Implementations.Library
|
||||
item.Id);
|
||||
}
|
||||
|
||||
// If deleting a primary version video, clear PrimaryVersionId from alternate versions
|
||||
// OwnerId check: items with OwnerId set are alternate versions or extras, not primaries
|
||||
if (item is Video video && !video.PrimaryVersionId.HasValue && video.OwnerId.IsEmpty())
|
||||
{
|
||||
var localAlternateIds = GetLocalAlternateVersionIds(video).ToHashSet();
|
||||
var allAlternateVersions = localAlternateIds
|
||||
.Concat(GetLinkedAlternateVersions(video).Select(v => v.Id))
|
||||
.Distinct()
|
||||
.Select(id => GetItemById(id))
|
||||
.OfType<Video>()
|
||||
.ToList();
|
||||
|
||||
// Partition alternates by whether their files still exist on disk
|
||||
var alternateVersions = new List<Video>();
|
||||
var missingAlternates = new List<Video>();
|
||||
foreach (var alt in allAlternateVersions)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(alt.Path) && !_fileSystem.FileExists(alt.Path))
|
||||
{
|
||||
missingAlternates.Add(alt);
|
||||
}
|
||||
else
|
||||
{
|
||||
alternateVersions.Add(alt);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete alternates whose files no longer exist to avoid ghost items.
|
||||
// Clear PrimaryVersionId first so DeleteItem doesn't try to update the primary being deleted.
|
||||
foreach (var missing in missingAlternates)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Deleting missing alternate version {Name} ({Path})",
|
||||
missing.Name ?? "Unknown name",
|
||||
missing.Path ?? string.Empty);
|
||||
missing.SetPrimaryVersionId(null);
|
||||
missing.OwnerId = Guid.Empty;
|
||||
missing.LocalAlternateVersions = [];
|
||||
missing.LinkedAlternateVersions = [];
|
||||
DeleteItem(missing, new DeleteOptions { DeleteFileLocation = false }, false);
|
||||
}
|
||||
|
||||
if (alternateVersions.Count > 0)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Clearing PrimaryVersionId from {Count} alternate versions of {Name}",
|
||||
alternateVersions.Count,
|
||||
item.Name ?? "Unknown name");
|
||||
|
||||
// Promote the first alternate version to be the new primary
|
||||
var newPrimary = alternateVersions[0];
|
||||
newPrimary.SetPrimaryVersionId(null);
|
||||
newPrimary.OwnerId = Guid.Empty;
|
||||
|
||||
// Transfer alternate version arrays from old primary to new primary
|
||||
// so UpdateToRepositoryAsync creates correct LinkedChildren entries
|
||||
newPrimary.LocalAlternateVersions = video.LocalAlternateVersions
|
||||
.Where(p => !string.Equals(p, newPrimary.Path, StringComparison.OrdinalIgnoreCase))
|
||||
.ToArray();
|
||||
newPrimary.LinkedAlternateVersions = video.LinkedAlternateVersions
|
||||
.Where(lc => !lc.ItemId.HasValue || !lc.ItemId.Value.Equals(newPrimary.Id))
|
||||
.ToArray();
|
||||
|
||||
newPrimary.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
|
||||
|
||||
// Re-route playlist/collection references from deleted primary to new primary
|
||||
RerouteLinkedChildReferencesAsync(video.Id, newPrimary.Id).GetAwaiter().GetResult();
|
||||
|
||||
// Update remaining alternates to point to new primary
|
||||
foreach (var alternate in alternateVersions.Skip(1))
|
||||
{
|
||||
alternate.SetPrimaryVersionId(newPrimary.Id);
|
||||
// Only set OwnerId for local alternates; linked alternates are independent items
|
||||
alternate.OwnerId = localAlternateIds.Contains(alternate.Id) ? newPrimary.Id : Guid.Empty;
|
||||
alternate.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (item is Video alternateVideo && alternateVideo.PrimaryVersionId.HasValue)
|
||||
{
|
||||
// If deleting an alternate version, re-route references to its primary
|
||||
RerouteLinkedChildReferencesAsync(alternateVideo.Id, alternateVideo.PrimaryVersionId.Value).GetAwaiter().GetResult();
|
||||
|
||||
// Remove deleted alternate from primary's LinkedAlternateVersions
|
||||
if (GetItemById(alternateVideo.PrimaryVersionId.Value) is Video primaryVideo)
|
||||
{
|
||||
primaryVideo.LinkedAlternateVersions = primaryVideo.LinkedAlternateVersions
|
||||
.Where(lc => !lc.ItemId.HasValue || !lc.ItemId.Value.Equals(alternateVideo.Id))
|
||||
.ToArray();
|
||||
primaryVideo.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
var children = item.IsFolder
|
||||
? ((Folder)item).GetRecursiveChildren(false)
|
||||
: [];
|
||||
@@ -450,7 +570,7 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
item.SetParent(null);
|
||||
|
||||
_itemRepository.DeleteItem([item.Id, .. children.Select(f => f.Id)]);
|
||||
_persistenceService.DeleteItem([item.Id, .. children.Select(f => f.Id)]);
|
||||
_cache.TryRemove(item.Id, out _);
|
||||
foreach (var child in children)
|
||||
{
|
||||
@@ -576,6 +696,9 @@ namespace Emby.Server.Implementations.Library
|
||||
// Trickplay
|
||||
list.Add(_pathManager.GetTrickplayDirectory(video));
|
||||
|
||||
// Chapter Images
|
||||
list.Add(_pathManager.GetChapterImageFolderPath(video));
|
||||
|
||||
// Subtitles and attachments
|
||||
foreach (var mediaSource in item.GetMediaSources(false))
|
||||
{
|
||||
@@ -657,8 +780,63 @@ namespace Emby.Server.Implementations.Library
|
||||
return key.GetMD5();
|
||||
}
|
||||
|
||||
public BaseItem? ResolvePath(FileSystemMetadata fileInfo, Folder? parent = null, IDirectoryService? directoryService = null)
|
||||
=> ResolvePath(fileInfo, directoryService ?? new DirectoryService(_fileSystem), null, parent);
|
||||
public BaseItem? ResolvePath(
|
||||
FileSystemMetadata fileInfo,
|
||||
Folder? parent = null,
|
||||
IDirectoryService? directoryService = null,
|
||||
CollectionType? collectionType = null)
|
||||
=> ResolvePath(fileInfo, directoryService ?? new DirectoryService(_fileSystem), null, parent, collectionType);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Video? ResolveAlternateVersion(string path, Type expectedVideoType, Folder? parent, CollectionType? collectionType)
|
||||
{
|
||||
// Clean up any existing item saved with wrong type (e.g. Video instead of Movie).
|
||||
// This happens when items were previously resolved without proper type context
|
||||
// in mixed-content libraries where collectionType is null.
|
||||
var expectedId = GetNewItemId(path, expectedVideoType);
|
||||
if (expectedVideoType != typeof(Video))
|
||||
{
|
||||
var wrongTypeId = GetNewItemId(path, typeof(Video));
|
||||
if (!wrongTypeId.Equals(expectedId) && GetItemById(wrongTypeId) is Video wrongTypeItem)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Removing alternate version with wrong type {WrongType}, expected {ExpectedType}: {Path}",
|
||||
wrongTypeItem.GetType().Name,
|
||||
expectedVideoType.Name,
|
||||
path);
|
||||
DeleteItem(wrongTypeItem, new DeleteOptions { DeleteFileLocation = false });
|
||||
}
|
||||
}
|
||||
|
||||
var resolved = ResolvePath(
|
||||
_fileSystem.GetFileSystemInfo(path),
|
||||
parent,
|
||||
collectionType: collectionType) as Video;
|
||||
|
||||
if (resolved is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ensure the alternate version has the same concrete type as the primary video.
|
||||
// ResolvePath may return a generic Video for files in mixed-content libraries
|
||||
// where collectionType is null, even though the primary is a Movie/Episode/etc.
|
||||
if (resolved.GetType() != expectedVideoType)
|
||||
{
|
||||
if (Activator.CreateInstance(expectedVideoType) is Video correctVideo)
|
||||
{
|
||||
correctVideo.Path = resolved.Path;
|
||||
correctVideo.Name = resolved.Name;
|
||||
correctVideo.VideoType = resolved.VideoType;
|
||||
correctVideo.ProductionYear = resolved.ProductionYear;
|
||||
correctVideo.ExtraType = resolved.ExtraType;
|
||||
resolved = correctVideo;
|
||||
}
|
||||
}
|
||||
|
||||
resolved.Id = expectedId;
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private BaseItem? ResolvePath(
|
||||
FileSystemMetadata fileInfo,
|
||||
@@ -1041,7 +1219,7 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
public IReadOnlyDictionary<string, MusicArtist[]> GetArtists(IReadOnlyList<string> names)
|
||||
{
|
||||
return _itemRepository.FindArtists(names);
|
||||
return _linkedChildrenService.FindArtists(names);
|
||||
}
|
||||
|
||||
public MusicArtist GetArtist(string name, DtoOptions options)
|
||||
@@ -1131,6 +1309,7 @@ namespace Emby.Server.Implementations.Library
|
||||
public async Task ValidateMediaLibraryInternal(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
IsScanRunning = true;
|
||||
ClearIgnoreRuleCache();
|
||||
LibraryMonitor.Stop();
|
||||
|
||||
try
|
||||
@@ -1139,6 +1318,7 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
finally
|
||||
{
|
||||
ClearIgnoreRuleCache();
|
||||
LibraryMonitor.Start();
|
||||
IsScanRunning = false;
|
||||
}
|
||||
@@ -1146,6 +1326,7 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
public async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false)
|
||||
{
|
||||
ClearIgnoreRuleCache();
|
||||
RootFolder.Children = null;
|
||||
await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -1186,8 +1367,16 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
if (toDelete.Count > 0)
|
||||
{
|
||||
_itemRepository.DeleteItem(toDelete.ToArray());
|
||||
_persistenceService.DeleteItem(toDelete.ToArray());
|
||||
}
|
||||
|
||||
ClearIgnoreRuleCache();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ClearIgnoreRuleCache()
|
||||
{
|
||||
_dotIgnoreIgnoreRule.ClearDirectoryCache();
|
||||
}
|
||||
|
||||
private async Task PerformLibraryValidation(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
@@ -1262,7 +1451,7 @@ namespace Emby.Server.Implementations.Library
|
||||
progress.Report(percent * 100);
|
||||
}
|
||||
|
||||
_itemRepository.UpdateInheritedValues();
|
||||
_persistenceService.UpdateInheritedValues();
|
||||
|
||||
progress.Report(100);
|
||||
}
|
||||
@@ -1421,14 +1610,7 @@ namespace Emby.Server.Implementations.Library
|
||||
AddUserToQuery(query, query.User, allowExternalContent);
|
||||
}
|
||||
|
||||
var itemList = _itemRepository.GetItemList(query);
|
||||
var user = query.User;
|
||||
if (user is not null)
|
||||
{
|
||||
return itemList.Where(i => i.IsVisible(user)).ToList();
|
||||
}
|
||||
|
||||
return itemList;
|
||||
return _itemRepository.GetItemList(query);
|
||||
}
|
||||
|
||||
public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query)
|
||||
@@ -1452,7 +1634,7 @@ namespace Emby.Server.Implementations.Library
|
||||
AddUserToQuery(query, query.User);
|
||||
}
|
||||
|
||||
return _itemRepository.GetCount(query);
|
||||
return _countService.GetCount(query);
|
||||
}
|
||||
|
||||
public ItemCounts GetItemCounts(InternalItemsQuery query)
|
||||
@@ -1471,7 +1653,30 @@ namespace Emby.Server.Implementations.Library
|
||||
AddUserToQuery(query, query.User);
|
||||
}
|
||||
|
||||
return _itemRepository.GetItemCounts(query);
|
||||
return _countService.GetItemCounts(query);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ItemCounts GetItemCountsForNameItem(BaseItemKind kind, Guid id, BaseItemKind[] relatedItemKinds, User? user)
|
||||
{
|
||||
var query = new InternalItemsQuery(user);
|
||||
if (user is not null)
|
||||
{
|
||||
AddUserToQuery(query, user);
|
||||
}
|
||||
|
||||
return _countService.GetItemCountsForNameItem(kind, id, relatedItemKinds, query);
|
||||
}
|
||||
|
||||
public Dictionary<Guid, int> GetChildCountBatch(IReadOnlyList<Guid> parentIds, Guid? userId)
|
||||
{
|
||||
return _countService.GetChildCountBatch(parentIds, userId);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Dictionary<Guid, (int Played, int Total)> GetPlayedAndTotalCountBatch(IReadOnlyList<Guid> folderIds, User user)
|
||||
{
|
||||
return _countService.GetPlayedAndTotalCountBatch(folderIds, user);
|
||||
}
|
||||
|
||||
public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents)
|
||||
@@ -1516,7 +1721,17 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
return _itemRepository.GetNextUpSeriesKeys(query, dateCutoff);
|
||||
return _nextUpService.GetNextUpSeriesKeys(query, dateCutoff);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyDictionary<string, MediaBrowser.Controller.Persistence.NextUpEpisodeBatchResult> GetNextUpEpisodesBatch(
|
||||
InternalItemsQuery query,
|
||||
IReadOnlyList<string> seriesKeys,
|
||||
bool includeSpecials,
|
||||
bool includeWatchedForRewatching)
|
||||
{
|
||||
return _nextUpService.GetNextUpEpisodesBatch(query, seriesKeys, includeSpecials, includeWatchedForRewatching);
|
||||
}
|
||||
|
||||
public QueryResult<BaseItem> QueryItems(InternalItemsQuery query)
|
||||
@@ -1683,6 +1898,25 @@ namespace Emby.Server.Implementations.Library
|
||||
query.TopParentIds = [Guid.NewGuid()];
|
||||
}
|
||||
}
|
||||
else if (parents.Count == 1 && parents.First() is Folder folder
|
||||
&& (folder is Playlist || folder is BoxSet)
|
||||
&& folder.LinkedChildren.Length > 0)
|
||||
{
|
||||
// Playlists and BoxSets store their contents in LinkedChildren and never
|
||||
// populate AncestorIds for those items, so a recursive AncestorIds query
|
||||
// would return zero rows. Resolve to the linked child IDs up front and
|
||||
// route through the existing indexed ItemIds filter.
|
||||
query.ItemIds = folder.LinkedChildren
|
||||
.Where(lc => lc.ItemId.HasValue && !lc.ItemId.Value.IsEmpty())
|
||||
.Select(lc => lc.ItemId!.Value)
|
||||
.ToArray();
|
||||
|
||||
// Empty linked-children should still return empty rather than scanning everything.
|
||||
if (query.ItemIds.Length == 0)
|
||||
{
|
||||
query.ItemIds = [Guid.NewGuid()];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// We need to be able to query from any arbitrary ancestor up the tree
|
||||
@@ -1700,6 +1934,11 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
private void AddUserToQuery(InternalItemsQuery query, User user, bool allowExternalContent = true)
|
||||
{
|
||||
if (query.User is null)
|
||||
{
|
||||
query.SetUser(user);
|
||||
}
|
||||
|
||||
if (query.AncestorIds.Length == 0 &&
|
||||
query.ParentId.IsEmpty() &&
|
||||
query.ChannelIds.Count == 0 &&
|
||||
@@ -1725,6 +1964,15 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void ConfigureUserAccess(InternalItemsQuery query, User user)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(query);
|
||||
ArgumentNullException.ThrowIfNull(user);
|
||||
|
||||
AddUserToQuery(query, user);
|
||||
}
|
||||
|
||||
private IEnumerable<Guid> GetTopParentIdsForQuery(BaseItem item, User? user)
|
||||
{
|
||||
if (item is UserView view)
|
||||
@@ -1889,6 +2137,44 @@ namespace Emby.Server.Implementations.Library
|
||||
return video;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<Guid> GetLocalAlternateVersionIds(Video video)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(video);
|
||||
|
||||
var linkedIds = _linkedChildrenService.GetLinkedChildrenIds(video.Id, (int)MediaBrowser.Controller.Entities.LinkedChildType.LocalAlternateVersion);
|
||||
if (linkedIds.Count > 0)
|
||||
{
|
||||
return linkedIds;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<Video> GetLinkedAlternateVersions(Video video)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(video);
|
||||
|
||||
var linkedIds = _linkedChildrenService.GetLinkedChildrenIds(video.Id, (int)MediaBrowser.Controller.Entities.LinkedChildType.LinkedAlternateVersion);
|
||||
if (linkedIds.Count > 0)
|
||||
{
|
||||
return linkedIds
|
||||
.Select(id => GetItemById(id))
|
||||
.Where(i => i is not null)
|
||||
.OfType<Video>()
|
||||
.OrderBy(i => i.SortName);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void UpsertLinkedChild(Guid parentId, Guid childId, MediaBrowser.Controller.Entities.LinkedChildType childType)
|
||||
{
|
||||
_linkedChildrenService.UpsertLinkedChild(parentId, childId, childType);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<ItemSortBy> sortBy, SortOrder sortOrder)
|
||||
{
|
||||
@@ -1993,9 +2279,44 @@ namespace Emby.Server.Implementations.Library
|
||||
/// <inheritdoc />
|
||||
public void CreateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken)
|
||||
{
|
||||
_itemRepository.SaveItems(items, cancellationToken);
|
||||
|
||||
// Resolve and add any local alternate version items that don't exist yet
|
||||
// This ensures they exist in the database when LinkedChildren are processed
|
||||
var allItems = new List<BaseItem>(items);
|
||||
var parentFolder = parent as Folder;
|
||||
var parentCollectionType = parent is not null ? GetTopFolderContentType(parent) : null;
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (item is Video video && video.LocalAlternateVersions.Length > 0)
|
||||
{
|
||||
var videoType = video.GetType();
|
||||
foreach (var path in video.LocalAlternateVersions)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use the primary video's type for ID calculation to ensure consistency
|
||||
var altId = GetNewItemId(path, videoType);
|
||||
if (GetItemById(altId) is null && !allItems.Any(i => i.Id.Equals(altId)))
|
||||
{
|
||||
// Alternate version doesn't exist, resolve and create it
|
||||
// ensuring it has the same type as the primary video
|
||||
var altVideo = ResolveAlternateVersion(path, videoType, parentFolder, parentCollectionType);
|
||||
if (altVideo is not null)
|
||||
{
|
||||
altVideo.OwnerId = video.Id;
|
||||
altVideo.SetPrimaryVersionId(video.Id);
|
||||
allItems.Add(altVideo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_persistenceService.SaveItems(allItems, cancellationToken);
|
||||
|
||||
foreach (var item in allItems)
|
||||
{
|
||||
RegisterItem(item);
|
||||
}
|
||||
@@ -2144,7 +2465,7 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
item.ValidateImages();
|
||||
|
||||
await _itemRepository.SaveImagesAsync(item).ConfigureAwait(false);
|
||||
await _persistenceService.SaveImagesAsync(item).ConfigureAwait(false);
|
||||
|
||||
RegisterItem(item);
|
||||
}
|
||||
@@ -2161,7 +2482,50 @@ namespace Emby.Server.Implementations.Library
|
||||
item.DateLastSaved = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
_itemRepository.SaveItems(items, cancellationToken);
|
||||
// Resolve and add any local alternate version items that don't exist yet
|
||||
// This ensures they exist in the database when LinkedChildren are processed
|
||||
var allItems = new List<BaseItem>(items);
|
||||
var parentFolder = parent as Folder;
|
||||
var parentCollectionType = GetTopFolderContentType(parent);
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (item is Video video && video.LocalAlternateVersions.Length > 0)
|
||||
{
|
||||
var videoType = video.GetType();
|
||||
foreach (var path in video.LocalAlternateVersions)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use the primary video's type for ID calculation to ensure consistency
|
||||
var altId = GetNewItemId(path, videoType);
|
||||
if (GetItemById(altId) is null && !allItems.Any(i => i.Id.Equals(altId)))
|
||||
{
|
||||
// Alternate version doesn't exist, resolve and create it
|
||||
// ensuring it has the same type as the primary video
|
||||
var altVideo = ResolveAlternateVersion(path, videoType, parentFolder, parentCollectionType);
|
||||
if (altVideo is not null)
|
||||
{
|
||||
altVideo.OwnerId = video.Id;
|
||||
altVideo.SetPrimaryVersionId(video.Id);
|
||||
allItems.Add(altVideo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_persistenceService.SaveItems(allItems, cancellationToken);
|
||||
|
||||
foreach (var item in allItems)
|
||||
{
|
||||
if (!items.Contains(item))
|
||||
{
|
||||
RegisterItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (parent is Folder folder)
|
||||
{
|
||||
@@ -2205,7 +2569,7 @@ namespace Emby.Server.Implementations.Library
|
||||
/// <inheritdoc />
|
||||
public async Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken)
|
||||
{
|
||||
await _itemRepository.ReattachUserDataAsync(item, cancellationToken).ConfigureAwait(false);
|
||||
await _persistenceService.ReattachUserDataAsync(item, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason)
|
||||
@@ -2833,8 +3197,9 @@ namespace Emby.Server.Implementations.Library
|
||||
public IEnumerable<BaseItem> FindExtras(BaseItem owner, IReadOnlyList<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
|
||||
{
|
||||
// Apply .ignore rules
|
||||
var filtered = fileSystemChildren.Where(c => !DotIgnoreIgnoreRule.IsIgnored(c, owner)).ToList();
|
||||
var ownerVideoInfo = VideoResolver.Resolve(owner.Path, owner.IsFolder, _namingOptions, libraryRoot: owner.ContainingFolderPath);
|
||||
var filtered = fileSystemChildren.Where(c => !_dotIgnoreIgnoreRule.ShouldIgnore(c, owner)).ToList();
|
||||
var isFolder = owner.IsFolder || (owner is Video video && (video.VideoType == VideoType.BluRay || video.VideoType == VideoType.Dvd));
|
||||
var ownerVideoInfo = VideoResolver.Resolve(owner.Path, isFolder, _namingOptions, libraryRoot: owner.ContainingFolderPath);
|
||||
if (ownerVideoInfo is null)
|
||||
{
|
||||
yield break;
|
||||
@@ -2896,10 +3261,16 @@ namespace Emby.Server.Implementations.Library
|
||||
extra.ExtraType = extraType;
|
||||
}
|
||||
|
||||
extra.ParentId = Guid.Empty;
|
||||
extra.OwnerId = owner.Id;
|
||||
extra.IsInMixedFolder = isInMixedFolder;
|
||||
return extra;
|
||||
// Only return items that are actual extras (have ExtraType set)
|
||||
// Note: OwnerId and ParentId are set by RefreshExtras, not here,
|
||||
// so that RefreshExtras can detect when they need updating and set ForceSave.
|
||||
if (extra.ExtraType is not null)
|
||||
{
|
||||
extra.IsInMixedFolder = isInMixedFolder;
|
||||
return extra;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2918,7 +3289,7 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
public IReadOnlyList<PersonInfo> GetPeople(InternalPeopleQuery query)
|
||||
{
|
||||
return _peopleRepository.GetPeople(query);
|
||||
return _peopleRepository.GetPeople(query).Items;
|
||||
}
|
||||
|
||||
public IReadOnlyList<PersonInfo> GetPeople(BaseItem item)
|
||||
@@ -2939,24 +3310,33 @@ namespace Emby.Server.Implementations.Library
|
||||
return [];
|
||||
}
|
||||
|
||||
public IReadOnlyList<Person> GetPeopleItems(InternalPeopleQuery query)
|
||||
public QueryResult<BaseItem> GetPeopleItems(InternalPeopleQuery query)
|
||||
{
|
||||
return _peopleRepository.GetPeopleNames(query)
|
||||
.Select(i =>
|
||||
var queryResult = _peopleRepository.GetPeople(query);
|
||||
var baseItems = queryResult.Items.Select(i =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return GetPerson(i.Name);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "error retrieving BaseItem for person: {0}", i.Name);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.Where(i => i is not null)
|
||||
.Where(i => query.User is null || i!.IsVisible(query.User))
|
||||
.OfType<BaseItem>()
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
|
||||
return new QueryResult<BaseItem>
|
||||
{
|
||||
try
|
||||
{
|
||||
return GetPerson(i);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting person");
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.Where(i => i is not null)
|
||||
.Where(i => query.User is null || i!.IsVisible(query.User))
|
||||
.ToList()!; // null values are filtered out
|
||||
StartIndex = queryResult.StartIndex,
|
||||
TotalRecordCount = queryResult.TotalRecordCount,
|
||||
Items = baseItems,
|
||||
};
|
||||
}
|
||||
|
||||
public IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery query)
|
||||
@@ -3385,5 +3765,40 @@ namespace Emby.Server.Implementations.Library
|
||||
_fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path));
|
||||
RemoveContentTypeOverrides(path);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task RerouteLinkedChildReferencesAsync(Guid fromChildId, Guid toChildId)
|
||||
{
|
||||
var affectedParentIds = _linkedChildrenService.RerouteLinkedChildren(fromChildId, toChildId);
|
||||
|
||||
// Update in-memory LinkedChildren and re-save metadata (NFO) for affected parents
|
||||
foreach (var parentId in affectedParentIds)
|
||||
{
|
||||
if (GetItemById(parentId) is Folder parent)
|
||||
{
|
||||
foreach (var lc in parent.LinkedChildren)
|
||||
{
|
||||
if (lc.ItemId.HasValue && lc.ItemId.Value.Equals(fromChildId))
|
||||
{
|
||||
lc.ItemId = toChildId;
|
||||
}
|
||||
}
|
||||
|
||||
await RunMetadataSavers(parent, ItemUpdateType.MetadataEdit).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public QueryFiltersLegacy GetQueryFiltersLegacy(InternalItemsQuery query)
|
||||
{
|
||||
if (query.User is not null)
|
||||
{
|
||||
AddUserToQuery(query, query.User);
|
||||
}
|
||||
|
||||
SetTopParentOrAncestorIds(query);
|
||||
return _itemRepository.GetQueryFiltersLegacy(query);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
@@ -423,7 +424,7 @@ namespace Emby.Server.Implementations.Library
|
||||
MediaStreamSelector.SetSubtitleStreamScores(source.MediaStreams, preferredSubs, user.SubtitleMode, audioLanguage);
|
||||
}
|
||||
|
||||
private void SetDefaultAudioStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection)
|
||||
private void SetDefaultAudioStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection, string originalLanguage)
|
||||
{
|
||||
if (userData is not null && userData.AudioStreamIndex.HasValue && user.RememberAudioSelections && allowRememberingSelection)
|
||||
{
|
||||
@@ -437,7 +438,42 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
var preferredAudio = NormalizeLanguage(user.AudioLanguagePreference);
|
||||
if (string.Equals(user.AudioLanguagePreference, "OriginalLanguage", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
originalLanguage = !string.IsNullOrWhiteSpace(originalLanguage)
|
||||
? originalLanguage.Split(',').FirstOrDefault()
|
||||
: null;
|
||||
|
||||
if (user.PlayDefaultAudioTrack)
|
||||
{
|
||||
source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(
|
||||
source.MediaStreams,
|
||||
NormalizeLanguage(originalLanguage),
|
||||
user.PlayDefaultAudioTrack);
|
||||
return;
|
||||
}
|
||||
|
||||
var originalIndex = source.MediaStreams.FindIndex(i => i.Type == MediaStreamType.Audio && i.IsOriginal);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(originalLanguage) && originalIndex != -1)
|
||||
{
|
||||
var mediaLanguageOriginal = source.MediaStreams[originalIndex].Language;
|
||||
if (NormalizeLanguage(mediaLanguageOriginal).Contains(NormalizeLanguage(originalLanguage).FirstOrDefault()))
|
||||
{
|
||||
source.DefaultAudioStreamIndex = originalIndex;
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (originalIndex != -1)
|
||||
{
|
||||
source.DefaultAudioStreamIndex = originalIndex;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var preferredAudio = string.Equals(user.AudioLanguagePreference, "OriginalLanguage", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(originalLanguage)
|
||||
? NormalizeLanguage(originalLanguage)
|
||||
: NormalizeLanguage(user.AudioLanguagePreference);
|
||||
|
||||
source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack);
|
||||
if (user.PlayDefaultAudioTrack)
|
||||
@@ -462,7 +498,19 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
var allowRememberingSelection = item is null || item.EnableRememberingTrackSelections;
|
||||
|
||||
SetDefaultAudioStreamIndex(source, userData, user, allowRememberingSelection);
|
||||
var originalLanguage = item?.OriginalLanguage ?? item switch
|
||||
{
|
||||
Episode episode => episode.Series.OriginalLanguage,
|
||||
Video video => video.GetOwner() switch
|
||||
{
|
||||
Episode ownerEpisode => ownerEpisode.OriginalLanguage ?? ownerEpisode.Series.OriginalLanguage,
|
||||
BaseItem owner => owner.OriginalLanguage,
|
||||
null => null
|
||||
},
|
||||
_ => null
|
||||
};
|
||||
|
||||
SetDefaultAudioStreamIndex(source, userData, user, allowRememberingSelection, originalLanguage);
|
||||
SetDefaultSubtitleStreamIndex(source, userData, user, allowRememberingSelection);
|
||||
}
|
||||
else if (mediaType == MediaType.Audio)
|
||||
|
||||
@@ -70,6 +70,16 @@ namespace Emby.Server.Implementations.Library
|
||||
return match ? imdbId.ToString() : null;
|
||||
}
|
||||
|
||||
// Allow tmdb as an alias for tmdbid
|
||||
if (attribute.Equals("tmdbid", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var tmdbValue = str.GetAttributeValue("tmdb");
|
||||
if (tmdbValue is not null)
|
||||
{
|
||||
return tmdbValue;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Library;
|
||||
|
||||
@@ -14,18 +15,22 @@ namespace Emby.Server.Implementations.Library;
|
||||
/// </summary>
|
||||
public class PathManager : IPathManager
|
||||
{
|
||||
private readonly ILogger<PathManager> _logger;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PathManager"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="config">The server configuration manager.</param>
|
||||
/// <param name="appPaths">The application paths.</param>
|
||||
public PathManager(
|
||||
ILogger<PathManager> logger,
|
||||
IServerConfigurationManager config,
|
||||
IApplicationPaths appPaths)
|
||||
{
|
||||
_logger = logger;
|
||||
_config = config;
|
||||
_appPaths = appPaths;
|
||||
}
|
||||
@@ -35,31 +40,43 @@ public class PathManager : IPathManager
|
||||
private string AttachmentCachePath => Path.Combine(_appPaths.DataPath, "attachments");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetAttachmentPath(string mediaSourceId, string fileName)
|
||||
public string? GetAttachmentPath(string mediaSourceId, string fileName)
|
||||
{
|
||||
return Path.Combine(GetAttachmentFolderPath(mediaSourceId), fileName);
|
||||
var folder = GetAttachmentFolderPath(mediaSourceId);
|
||||
return folder is null ? null : Path.Combine(folder, fileName);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetAttachmentFolderPath(string mediaSourceId)
|
||||
public string? GetAttachmentFolderPath(string mediaSourceId)
|
||||
{
|
||||
var id = Guid.Parse(mediaSourceId).ToString("D", CultureInfo.InvariantCulture).AsSpan();
|
||||
if (!Guid.TryParse(mediaSourceId, out var parsed))
|
||||
{
|
||||
_logger.LogDebug("MediaSource Id '{MediaSourceId}' is not a GUID; no on-disk attachment folder.", mediaSourceId);
|
||||
return null;
|
||||
}
|
||||
|
||||
var id = parsed.ToString("D", CultureInfo.InvariantCulture).AsSpan();
|
||||
return Path.Join(AttachmentCachePath, id[..2], id);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetSubtitleFolderPath(string mediaSourceId)
|
||||
public string? GetSubtitleFolderPath(string mediaSourceId)
|
||||
{
|
||||
var id = Guid.Parse(mediaSourceId).ToString("D", CultureInfo.InvariantCulture).AsSpan();
|
||||
if (!Guid.TryParse(mediaSourceId, out var parsed))
|
||||
{
|
||||
_logger.LogDebug("MediaSource Id '{MediaSourceId}' is not a GUID; no on-disk subtitle folder.", mediaSourceId);
|
||||
return null;
|
||||
}
|
||||
|
||||
var id = parsed.ToString("D", CultureInfo.InvariantCulture).AsSpan();
|
||||
return Path.Join(SubtitleCachePath, id[..2], id);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetSubtitlePath(string mediaSourceId, int streamIndex, string extension)
|
||||
public string? GetSubtitlePath(string mediaSourceId, int streamIndex, string extension)
|
||||
{
|
||||
return Path.Combine(GetSubtitleFolderPath(mediaSourceId), streamIndex.ToString(CultureInfo.InvariantCulture) + extension);
|
||||
var folder = GetSubtitleFolderPath(mediaSourceId);
|
||||
return folder is null ? null : Path.Combine(folder, streamIndex.ToString(CultureInfo.InvariantCulture) + extension);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -90,12 +107,23 @@ public class PathManager : IPathManager
|
||||
public IReadOnlyList<string> GetExtractedDataPaths(BaseItem item)
|
||||
{
|
||||
var mediaSourceId = item.Id.ToString("N", CultureInfo.InvariantCulture);
|
||||
return [
|
||||
GetAttachmentFolderPath(mediaSourceId),
|
||||
GetSubtitleFolderPath(mediaSourceId),
|
||||
GetTrickplayDirectory(item, false),
|
||||
GetTrickplayDirectory(item, true),
|
||||
GetChapterImageFolderPath(item)
|
||||
];
|
||||
List<string> paths = [];
|
||||
var attachmentFolder = GetAttachmentFolderPath(mediaSourceId);
|
||||
if (attachmentFolder is not null)
|
||||
{
|
||||
paths.Add(attachmentFolder);
|
||||
}
|
||||
|
||||
var subtitleFolder = GetSubtitleFolderPath(mediaSourceId);
|
||||
if (subtitleFolder is not null)
|
||||
{
|
||||
paths.Add(subtitleFolder);
|
||||
}
|
||||
|
||||
paths.Add(GetTrickplayDirectory(item, false));
|
||||
paths.Add(GetTrickplayDirectory(item, true));
|
||||
paths.Add(GetChapterImageFolderPath(item));
|
||||
|
||||
return paths;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
|
||||
{
|
||||
public class BookResolver : ItemResolver<Book>
|
||||
{
|
||||
private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".pdf", ".m4b", ".m4a", ".aac", ".flac", ".mp3", ".opus" };
|
||||
private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".pdf" };
|
||||
|
||||
protected override Book Resolve(ItemResolveArgs args)
|
||||
{
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Emby.Naming.Common;
|
||||
using Emby.Server.Implementations.Library;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@@ -81,10 +83,34 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
episode.ParentIndexNumber = 1;
|
||||
}
|
||||
|
||||
SetProviderIdFromPath(episode, args.Path);
|
||||
|
||||
return episode;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets provider ids from the episode file name.
|
||||
/// </summary>
|
||||
/// <param name="item">The episode.</param>
|
||||
/// <param name="path">The episode file path.</param>
|
||||
private static void SetProviderIdFromPath(Episode item, string path)
|
||||
{
|
||||
var justName = Path.GetFileNameWithoutExtension(path.AsSpan());
|
||||
|
||||
var imdbId = justName.GetAttributeValue("imdbid");
|
||||
item.TrySetProviderId(MetadataProvider.Imdb, imdbId);
|
||||
|
||||
var tvdbId = justName.GetAttributeValue("tvdbid");
|
||||
item.TrySetProviderId(MetadataProvider.Tvdb, tvdbId);
|
||||
|
||||
var tvmazeId = justName.GetAttributeValue("tvmazeid");
|
||||
item.TrySetProviderId(MetadataProvider.TvMaze, tvmazeId);
|
||||
|
||||
var tmdbId = justName.GetAttributeValue("tmdbid");
|
||||
item.TrySetProviderId(MetadataProvider.Tmdb, tmdbId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Emby.Naming.Common;
|
||||
using Emby.Naming.TV;
|
||||
using Emby.Server.Implementations.Library;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -77,6 +82,14 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
var hasAnyVideo = Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories)
|
||||
.Any(file => _namingOptions.VideoFileExtensions.Contains(Path.GetExtension(file)));
|
||||
|
||||
if (!hasAnyVideo)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (season.IndexNumber.HasValue && string.IsNullOrEmpty(season.Name))
|
||||
@@ -91,10 +104,31 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
args.LibraryOptions.PreferredMetadataLanguage);
|
||||
}
|
||||
|
||||
SetProviderIdFromPath(season, path);
|
||||
|
||||
return season;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets provider ids from the season folder name.
|
||||
/// </summary>
|
||||
/// <param name="item">The season.</param>
|
||||
/// <param name="path">The season folder path.</param>
|
||||
private static void SetProviderIdFromPath(Season item, string path)
|
||||
{
|
||||
var justName = Path.GetFileName(path.AsSpan());
|
||||
|
||||
var tvdbId = justName.GetAttributeValue("tvdbid");
|
||||
item.TrySetProviderId(MetadataProvider.Tvdb, tvdbId);
|
||||
|
||||
var tvmazeId = justName.GetAttributeValue("tvmazeid");
|
||||
item.TrySetProviderId(MetadataProvider.TvMaze, tvmazeId);
|
||||
|
||||
var tmdbId = justName.GetAttributeValue("tmdbid");
|
||||
item.TrySetProviderId(MetadataProvider.Tmdb, tmdbId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,53 +177,74 @@ namespace Emby.Server.Implementations.Library
|
||||
};
|
||||
}
|
||||
|
||||
private UserItemData? GetUserData(User user, Guid itemId, List<string> keys)
|
||||
/// <inheritdoc />
|
||||
public Dictionary<Guid, UserItemData> GetUserDataBatch(IReadOnlyList<BaseItem> items, User user)
|
||||
{
|
||||
var cacheKey = GetCacheKey(user.InternalId, itemId);
|
||||
var result = new Dictionary<Guid, UserItemData>(items.Count);
|
||||
var itemsNeedingQuery = new List<(BaseItem Item, List<string> Keys)>();
|
||||
|
||||
if (_cache.TryGet(cacheKey, out var data))
|
||||
foreach (var item in items)
|
||||
{
|
||||
return data;
|
||||
}
|
||||
|
||||
data = GetUserDataInternal(user.Id, itemId, keys);
|
||||
|
||||
if (data is null)
|
||||
{
|
||||
return new UserItemData()
|
||||
var cacheKey = GetCacheKey(user.InternalId, item.Id);
|
||||
if (_cache.TryGet(cacheKey, out var cachedData))
|
||||
{
|
||||
Key = keys[0],
|
||||
};
|
||||
}
|
||||
|
||||
return _cache.GetOrAdd(cacheKey, _ => data);
|
||||
}
|
||||
|
||||
private UserItemData? GetUserDataInternal(Guid userId, Guid itemId, List<string> keys)
|
||||
{
|
||||
if (keys.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using var context = _repository.CreateDbContext();
|
||||
var userData = context.UserData.AsNoTracking().Where(e => e.ItemId == itemId && keys.Contains(e.CustomDataKey) && e.UserId.Equals(userId)).ToArray();
|
||||
|
||||
if (userData.Length > 0)
|
||||
{
|
||||
var directDataReference = userData.FirstOrDefault(e => e.CustomDataKey == itemId.ToString("N"));
|
||||
if (directDataReference is not null)
|
||||
{
|
||||
return Map(directDataReference);
|
||||
result[item.Id] = cachedData;
|
||||
}
|
||||
else
|
||||
{
|
||||
var userData = item.UserData?.Where(e => e.UserId.Equals(user.Id)).Select(Map).FirstOrDefault();
|
||||
if (userData is not null)
|
||||
{
|
||||
result[item.Id] = userData;
|
||||
_cache.AddOrUpdate(cacheKey, userData);
|
||||
}
|
||||
else
|
||||
{
|
||||
var keys = item.GetUserDataKeys();
|
||||
itemsNeedingQuery.Add((item, keys));
|
||||
}
|
||||
}
|
||||
|
||||
return Map(userData.First());
|
||||
}
|
||||
|
||||
return new UserItemData
|
||||
if (itemsNeedingQuery.Count == 0)
|
||||
{
|
||||
Key = keys.Last()!
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
// Build a single query for all missing items
|
||||
var allItemIds = itemsNeedingQuery.Select(x => x.Item.Id).ToList();
|
||||
var allKeys = itemsNeedingQuery.SelectMany(x => x.Keys).Distinct().ToList();
|
||||
if (allKeys.Count > 0)
|
||||
{
|
||||
using var context = _repository.CreateDbContext();
|
||||
var userDataArray = context.UserData
|
||||
.AsNoTracking()
|
||||
.Where(e => e.UserId.Equals(user.Id))
|
||||
.WhereOneOrMany(allItemIds, e => e.ItemId)
|
||||
.WhereOneOrMany(allKeys, e => e.CustomDataKey)
|
||||
.ToArray();
|
||||
|
||||
var userDataByItem = userDataArray.GroupBy(e => e.ItemId).ToDictionary(g => g.Key, g => g.ToArray());
|
||||
foreach (var (item, keys) in itemsNeedingQuery)
|
||||
{
|
||||
UserItemData userData;
|
||||
if (userDataByItem.TryGetValue(item.Id, out var itemUserData) && itemUserData.Length > 0)
|
||||
{
|
||||
var directDataReference = itemUserData.FirstOrDefault(e => e.CustomDataKey == item.Id.ToString("N"));
|
||||
userData = directDataReference is not null ? Map(directDataReference) : Map(itemUserData.First());
|
||||
}
|
||||
else
|
||||
{
|
||||
userData = new UserItemData { Key = keys.Count > 0 ? keys[0] : string.Empty };
|
||||
}
|
||||
|
||||
result[item.Id] = userData;
|
||||
var cacheKey = GetCacheKey(user.InternalId, item.Id);
|
||||
_cache.AddOrUpdate(cacheKey, userData);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -59,8 +59,8 @@ namespace Emby.Server.Implementations.Library
|
||||
var collectionFolder = folder as ICollectionFolder;
|
||||
var folderViewType = collectionFolder?.CollectionType;
|
||||
|
||||
// Playlist library requires special handling because the folder only references user playlists
|
||||
if (folderViewType == CollectionType.playlists)
|
||||
// Playlist and BoxSet libraries require special handling because the folder only references linked items
|
||||
if (folderViewType == CollectionType.playlists || folderViewType == CollectionType.boxsets)
|
||||
{
|
||||
var items = folder.GetItemList(new InternalItemsQuery(user)
|
||||
{
|
||||
@@ -138,7 +138,7 @@ namespace Emby.Server.Implementations.Library
|
||||
list = list.Where(i => !user.GetPreferenceValues<Guid>(PreferenceKind.MyMediaExcludes).Contains(i.Id)).ToList();
|
||||
}
|
||||
|
||||
var sorted = _libraryManager.Sort(list, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList();
|
||||
var sorted = _libraryManager.Sort(list, user, [ItemSortBy.SortName], SortOrder.Ascending).ToList();
|
||||
var orders = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews);
|
||||
|
||||
return list
|
||||
@@ -205,7 +205,7 @@ namespace Emby.Server.Implementations.Library
|
||||
var libraryItems = GetItemsForLatestItems(request.User, request, options);
|
||||
|
||||
var list = new List<Tuple<BaseItem, List<BaseItem>>>();
|
||||
|
||||
var containerIndexMap = new Dictionary<Guid, int>();
|
||||
foreach (var item in libraryItems)
|
||||
{
|
||||
// Only grab the index container for media
|
||||
@@ -213,20 +213,16 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
if (container is null)
|
||||
{
|
||||
list.Add(new Tuple<BaseItem, List<BaseItem>>(null, new List<BaseItem> { item }));
|
||||
list.Add(new Tuple<BaseItem, List<BaseItem>>(null!, new List<BaseItem> { item }));
|
||||
}
|
||||
else if (containerIndexMap.TryGetValue(container.Id, out var existingIndex))
|
||||
{
|
||||
list[existingIndex].Item2.Add(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
var current = list.FirstOrDefault(i => i.Item1 is not null && i.Item1.Id.Equals(container.Id));
|
||||
|
||||
if (current is not null)
|
||||
{
|
||||
current.Item2.Add(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
list.Add(new Tuple<BaseItem, List<BaseItem>>(container, new List<BaseItem> { item }));
|
||||
}
|
||||
containerIndexMap[container.Id] = list.Count;
|
||||
list.Add(new Tuple<BaseItem, List<BaseItem>>(container, new List<BaseItem> { item }));
|
||||
}
|
||||
|
||||
if (list.Count >= request.Limit)
|
||||
@@ -255,7 +251,7 @@ namespace Emby.Server.Implementations.Library
|
||||
return _channelManager.GetLatestChannelItemsInternal(
|
||||
new InternalItemsQuery(user)
|
||||
{
|
||||
ChannelIds = new[] { parentId },
|
||||
ChannelIds = [parentId],
|
||||
IsPlayed = request.IsPlayed,
|
||||
StartIndex = request.StartIndex,
|
||||
Limit = request.Limit,
|
||||
@@ -301,11 +297,11 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
if (hasCollectionType.All(i => i.CollectionType == CollectionType.movies))
|
||||
{
|
||||
includeItemTypes = new[] { BaseItemKind.Movie };
|
||||
includeItemTypes = [BaseItemKind.Movie];
|
||||
}
|
||||
else if (hasCollectionType.All(i => i.CollectionType == CollectionType.tvshows))
|
||||
{
|
||||
includeItemTypes = new[] { BaseItemKind.Episode };
|
||||
includeItemTypes = [BaseItemKind.Episode];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -344,29 +340,29 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
|
||||
var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Length == 0
|
||||
? new[]
|
||||
{
|
||||
?
|
||||
[
|
||||
BaseItemKind.Person,
|
||||
BaseItemKind.Studio,
|
||||
BaseItemKind.Year,
|
||||
BaseItemKind.MusicGenre,
|
||||
BaseItemKind.Genre
|
||||
}
|
||||
]
|
||||
: Array.Empty<BaseItemKind>();
|
||||
|
||||
var query = new InternalItemsQuery(user)
|
||||
{
|
||||
IncludeItemTypes = includeItemTypes,
|
||||
OrderBy = new[]
|
||||
{
|
||||
OrderBy =
|
||||
[
|
||||
(ItemSortBy.DateCreated, SortOrder.Descending),
|
||||
(ItemSortBy.SortName, SortOrder.Descending),
|
||||
(ItemSortBy.ProductionYear, SortOrder.Descending)
|
||||
},
|
||||
],
|
||||
IsFolder = includeItemTypes.Length == 0 ? false : null,
|
||||
ExcludeItemTypes = excludeItemTypes,
|
||||
IsVirtualItem = false,
|
||||
Limit = limit * 5,
|
||||
Limit = limit * 2,
|
||||
IsPlayed = isPlayed,
|
||||
DtoOptions = options,
|
||||
MediaTypes = mediaTypes
|
||||
@@ -394,6 +390,12 @@ namespace Emby.Server.Implementations.Library
|
||||
query.Limit = limit;
|
||||
return _libraryManager.GetLatestItemList(query, parents, CollectionType.music);
|
||||
}
|
||||
|
||||
if (collectionType == CollectionType.movies)
|
||||
{
|
||||
query.Limit = limit;
|
||||
return _libraryManager.GetLatestItemList(query, parents, CollectionType.movies);
|
||||
}
|
||||
}
|
||||
|
||||
return _libraryManager.GetItemList(query, parents);
|
||||
|
||||
@@ -55,25 +55,35 @@ public class ArtistsValidator
|
||||
IncludeItemTypes = [BaseItemKind.MusicArtist]
|
||||
}).ToHashSet();
|
||||
|
||||
var existingArtists = _libraryManager.GetArtists(names);
|
||||
|
||||
var numComplete = 0;
|
||||
var count = names.Count;
|
||||
var refreshed = 0;
|
||||
|
||||
foreach (var name in names)
|
||||
{
|
||||
try
|
||||
{
|
||||
var item = _libraryManager.GetArtist(name);
|
||||
MusicArtist? item = null;
|
||||
if (existingArtists.TryGetValue(name, out var artists) && artists.Length > 0)
|
||||
{
|
||||
item = artists.OrderBy(i => i.IsAccessedByName ? 1 : 0).First();
|
||||
}
|
||||
|
||||
// Fall back to GetArtist if not found (creates new item if needed)
|
||||
item ??= _libraryManager.GetArtist(name);
|
||||
var isNew = !existingArtistIds.Contains(item.Id);
|
||||
var neverRefreshed = item.DateLastRefreshed == default;
|
||||
|
||||
if (isNew || neverRefreshed)
|
||||
{
|
||||
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||
refreshed++;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Don't clutter the log
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -89,31 +99,24 @@ public class ArtistsValidator
|
||||
progress.Report(percent);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Refreshed metadata for {RefreshedCount} new artists out of {TotalCount} total", refreshed, count);
|
||||
|
||||
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = [BaseItemKind.MusicArtist],
|
||||
IsDeadArtist = true,
|
||||
IsLocked = false
|
||||
}).Cast<MusicArtist>().ToList();
|
||||
}).Cast<MusicArtist>()
|
||||
.Where(item => item.IsAccessedByName)
|
||||
.ToList();
|
||||
|
||||
foreach (var item in deadEntities)
|
||||
{
|
||||
if (!item.IsAccessedByName)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
|
||||
|
||||
_libraryManager.DeleteItem(
|
||||
item,
|
||||
new DeleteOptions
|
||||
{
|
||||
DeleteFileLocation = false
|
||||
},
|
||||
false);
|
||||
}
|
||||
|
||||
_libraryManager.DeleteItemsUnsafeFast(deadEntities, deleteSourceFiles: true);
|
||||
|
||||
progress.Report(100);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ public class CollectionPostScanTask : ILibraryPostScanTask
|
||||
|
||||
foreach (var m in movies)
|
||||
{
|
||||
if (m is Movie movie && !string.IsNullOrEmpty(movie.CollectionName))
|
||||
if (m is Movie movie && !string.IsNullOrEmpty(movie.CollectionName) && !movie.PrimaryVersionId.HasValue)
|
||||
{
|
||||
if (collectionNameMoviesMap.TryGetValue(movie.CollectionName, out var movieList))
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
@@ -48,17 +49,40 @@ public class GenresValidator
|
||||
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
var names = _itemRepo.GetGenreNames();
|
||||
var existingGenreIds = _libraryManager.GetItemIds(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = [BaseItemKind.Genre]
|
||||
}).ToHashSet();
|
||||
|
||||
var existingGenres = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = [BaseItemKind.Genre]
|
||||
}).Cast<Genre>()
|
||||
.GroupBy(g => g.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var numComplete = 0;
|
||||
var count = names.Count;
|
||||
var refreshed = 0;
|
||||
|
||||
foreach (var name in names)
|
||||
{
|
||||
try
|
||||
{
|
||||
var item = _libraryManager.GetGenre(name);
|
||||
Genre? item = null;
|
||||
if (existingGenres.TryGetValue(name, out var existingGenre))
|
||||
{
|
||||
item = existingGenre;
|
||||
}
|
||||
|
||||
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||
// Fall back to GetGenre if not found (creates new item if needed)
|
||||
item ??= _libraryManager.GetGenre(name);
|
||||
|
||||
if (!existingGenreIds.Contains(item.Id))
|
||||
{
|
||||
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||
refreshed++;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -78,6 +102,8 @@ public class GenresValidator
|
||||
progress.Report(percent);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Refreshed metadata for {RefreshedCount} new genres out of {TotalCount} total", refreshed, count);
|
||||
|
||||
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = [BaseItemKind.Genre, BaseItemKind.MusicGenre],
|
||||
@@ -88,16 +114,10 @@ public class GenresValidator
|
||||
foreach (var item in deadEntities)
|
||||
{
|
||||
_logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
|
||||
|
||||
_libraryManager.DeleteItem(
|
||||
item,
|
||||
new DeleteOptions
|
||||
{
|
||||
DeleteFileLocation = false
|
||||
},
|
||||
false);
|
||||
}
|
||||
|
||||
_libraryManager.DeleteItemsUnsafeFast(deadEntities, deleteSourceFiles: true);
|
||||
|
||||
progress.Report(100);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -45,17 +48,25 @@ public class MusicGenresValidator
|
||||
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
var names = _itemRepo.GetMusicGenreNames();
|
||||
var existingMusicGenreIds = _libraryManager.GetItemIds(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = [BaseItemKind.MusicGenre]
|
||||
}).ToHashSet();
|
||||
|
||||
var numComplete = 0;
|
||||
var count = names.Count;
|
||||
var refreshed = 0;
|
||||
|
||||
foreach (var name in names)
|
||||
{
|
||||
try
|
||||
{
|
||||
var item = _libraryManager.GetMusicGenre(name);
|
||||
|
||||
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||
if (!existingMusicGenreIds.Contains(item.Id))
|
||||
{
|
||||
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||
refreshed++;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -75,6 +86,8 @@ public class MusicGenresValidator
|
||||
progress.Report(percent);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Refreshed metadata for {RefreshedCount} new music genres out of {TotalCount} total", refreshed, count);
|
||||
|
||||
progress.Report(100);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ public class PeopleValidator
|
||||
var i = 0;
|
||||
foreach (var item in deadEntities.Chunk(500))
|
||||
{
|
||||
_libraryManager.DeleteItemsUnsafeFast(item);
|
||||
_libraryManager.DeleteItemsUnsafeFast(item, true);
|
||||
subProgress.Report(100f / deadEntities.Count * (i++ * 100));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
@@ -49,17 +50,40 @@ public class StudiosValidator
|
||||
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
var names = _itemRepo.GetStudioNames();
|
||||
var existingStudioIds = _libraryManager.GetItemIds(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = [BaseItemKind.Studio]
|
||||
}).ToHashSet();
|
||||
|
||||
var existingStudios = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = [BaseItemKind.Studio]
|
||||
}).Cast<Studio>()
|
||||
.GroupBy(s => s.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var numComplete = 0;
|
||||
var count = names.Count;
|
||||
var refreshed = 0;
|
||||
|
||||
foreach (var name in names)
|
||||
{
|
||||
try
|
||||
{
|
||||
var item = _libraryManager.GetStudio(name);
|
||||
Studio? item = null;
|
||||
if (existingStudios.TryGetValue(name, out var existingStudio))
|
||||
{
|
||||
item = existingStudio;
|
||||
}
|
||||
|
||||
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||
// Fall back to GetStudio if not found (creates new item if needed)
|
||||
item ??= _libraryManager.GetStudio(name);
|
||||
|
||||
if (!existingStudioIds.Contains(item.Id))
|
||||
{
|
||||
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||
refreshed++;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -79,6 +103,8 @@ public class StudiosValidator
|
||||
progress.Report(percent);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Refreshed metadata for {RefreshedCount} new studios out of {TotalCount} total", refreshed, count);
|
||||
|
||||
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = [BaseItemKind.Studio],
|
||||
@@ -89,16 +115,10 @@ public class StudiosValidator
|
||||
foreach (var item in deadEntities)
|
||||
{
|
||||
_logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
|
||||
|
||||
_libraryManager.DeleteItem(
|
||||
item,
|
||||
new DeleteOptions
|
||||
{
|
||||
DeleteFileLocation = false
|
||||
},
|
||||
false);
|
||||
}
|
||||
|
||||
_libraryManager.DeleteItemsUnsafeFast(deadEntities, deleteSourceFiles: true);
|
||||
|
||||
progress.Report(100);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,8 +128,6 @@
|
||||
"TaskRefreshTrickplayImagesDescription": "Skep fopspeel voorskou vir videos in aangeskakelde media versameling.",
|
||||
"TaskAudioNormalizationDescription": "Skandeer lêers vir oudio-normaliseringsdata.",
|
||||
"TaskAudioNormalization": "Odio Normalisering",
|
||||
"TaskCleanCollectionsAndPlaylists": "Maak versamelings en snitlyste skoon",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Verwyder items uit versamelings en snitlyste wat nie meer bestaan nie.",
|
||||
"TaskDownloadMissingLyrics": "Laai tekorte lirieke af",
|
||||
"TaskDownloadMissingLyricsDescription": "Laai lirieke af vir liedjies",
|
||||
"TaskExtractMediaSegments": "Media Segment Skandeer",
|
||||
|
||||
@@ -1,141 +1,140 @@
|
||||
{
|
||||
"Albums": "ألبومات",
|
||||
"AppDeviceValues": "تطبيق: {0}, جهاز: {1}",
|
||||
"Application": "تطبيق",
|
||||
"Artists": "فنانون",
|
||||
"AuthenticationSucceededWithUserName": "نجحت عملية التوثيق بـ {0}",
|
||||
"Albums": "الألبومات",
|
||||
"AppDeviceValues": "التطبيق: {0}، الجهاز: {1}",
|
||||
"Application": "التطبيق",
|
||||
"Artists": "الفنانون",
|
||||
"AuthenticationSucceededWithUserName": "تمت مصادقة {0} بنجاح",
|
||||
"Books": "الكتب",
|
||||
"CameraImageUploadedFrom": "رُفعت صورة الكاميرا الجديدة من {0}",
|
||||
"CameraImageUploadedFrom": "تم رفع صورة كاميرا جديدة من {0}",
|
||||
"Channels": "القنوات",
|
||||
"ChapterNameValue": "الفصل {0}",
|
||||
"Collections": "مجموعات",
|
||||
"DeviceOfflineWithName": "قُطِع الاتصال ب{0}",
|
||||
"Collections": "المجموعات",
|
||||
"DeviceOfflineWithName": "انقطع اتصال {0}",
|
||||
"DeviceOnlineWithName": "{0} متصل",
|
||||
"FailedLoginAttemptWithUserName": "محاولة تسجيل الدخول فاشلة من {0}",
|
||||
"FailedLoginAttemptWithUserName": "محاولة تسجيل دخول فاشلة من {0}",
|
||||
"Favorites": "المفضلة",
|
||||
"Folders": "المجلدات",
|
||||
"Genres": "التصنيفات",
|
||||
"HeaderAlbumArtists": "فناني الألبوم",
|
||||
"Genres": "الأنواع",
|
||||
"HeaderAlbumArtists": "فنانو الألبوم",
|
||||
"HeaderContinueWatching": "متابعة المشاهدة",
|
||||
"HeaderFavoriteAlbums": "الألبومات المفضلة",
|
||||
"HeaderFavoriteArtists": "الفنانون المفضلون",
|
||||
"HeaderFavoriteEpisodes": "الحلقات المفضلة",
|
||||
"HeaderFavoriteShows": "المسلسلات المفضلة",
|
||||
"HeaderFavoriteSongs": "الأغاني المفضلة",
|
||||
"HeaderLiveTV": "التلفاز المباشر",
|
||||
"HeaderLiveTV": "البث التلفزيوني المباشر",
|
||||
"HeaderNextUp": "التالي",
|
||||
"HeaderRecordingGroups": "مجموعات التسجيل",
|
||||
"HomeVideos": "الفيديوهات الشخصية",
|
||||
"Inherit": "توريث",
|
||||
"ItemAddedWithName": "أُضيف {0} للمكتبة",
|
||||
"ItemRemovedWithName": "أُزيل {0} من المكتبة",
|
||||
"LabelIpAddressValue": "عنوان الآي بي: {0}",
|
||||
"HomeVideos": "فيديوهات منزلية",
|
||||
"Inherit": "وراثة",
|
||||
"ItemAddedWithName": "تمت إضافة {0} إلى المكتبة",
|
||||
"ItemRemovedWithName": "تمت إزالة {0} من المكتبة",
|
||||
"LabelIpAddressValue": "عنوان IP: {0}",
|
||||
"LabelRunningTimeValue": "مدة التشغيل: {0}",
|
||||
"Latest": "الأحدث",
|
||||
"MessageApplicationUpdated": "حُدث خادم Jellyfin",
|
||||
"MessageApplicationUpdatedTo": "حُدث خادم Jellyfin إلى {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "حُدثت إعدادات الخادم في قسم {0}",
|
||||
"MessageServerConfigurationUpdated": "حُدثت إعدادات الخادم",
|
||||
"MessageApplicationUpdated": "تم تحديث خادم Jellyfin",
|
||||
"MessageApplicationUpdatedTo": "تم تحديث خادم Jellyfin إلى {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "تم تحديث قسم إعدادات الخادم {0}",
|
||||
"MessageServerConfigurationUpdated": "تم تحديث إعدادات الخادم",
|
||||
"MixedContent": "محتوى مختلط",
|
||||
"Movies": "الأفلام",
|
||||
"Music": "الموسيقى",
|
||||
"MusicVideos": "الفيديوهات الموسيقية",
|
||||
"NameInstallFailed": "فشل تثبيت {0}",
|
||||
"NameSeasonNumber": "الموسم {0}",
|
||||
"NameSeasonUnknown": "الموسم غير معروف",
|
||||
"NewVersionIsAvailable": "نسخة جديدة من خادم Jellyfin متوفرة للتحميل.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "يوجد تحديث للتطبيق",
|
||||
"NotificationOptionApplicationUpdateInstalled": "نُصب تحديث التطبيق",
|
||||
"NotificationOptionAudioPlayback": "بدأ تشغيل المقطع الصوتي",
|
||||
"NotificationOptionAudioPlaybackStopped": "أُوقف تشغيل المقطع الصوتي",
|
||||
"NotificationOptionCameraImageUploaded": "رُفعت صورة الكاميرا",
|
||||
"NotificationOptionInstallationFailed": "فشل في التثبيت",
|
||||
"NotificationOptionNewLibraryContent": "أُضيف محتوى جديدا",
|
||||
"NotificationOptionPluginError": "فشل في الملحق",
|
||||
"NotificationOptionPluginInstalled": "ثُبتت الملحق",
|
||||
"NameSeasonUnknown": "موسم غير معروف",
|
||||
"NewVersionIsAvailable": "يتوفر إصدار جديد من خادم Jellyfin للتنزيل.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "تحديث التطبيق متاح",
|
||||
"NotificationOptionApplicationUpdateInstalled": "تم تثبيت تحديث التطبيق",
|
||||
"NotificationOptionAudioPlayback": "بدأ تشغيل الصوت",
|
||||
"NotificationOptionAudioPlaybackStopped": "توقف تشغيل الصوت",
|
||||
"NotificationOptionCameraImageUploaded": "تم رفع صورة كاميرا",
|
||||
"NotificationOptionInstallationFailed": "فشل التثبيت",
|
||||
"NotificationOptionNewLibraryContent": "تمت إضافة محتوى جديد",
|
||||
"NotificationOptionPluginError": "خطأ في الملحق",
|
||||
"NotificationOptionPluginInstalled": "تم تثبيت الملحق",
|
||||
"NotificationOptionPluginUninstalled": "تمت إزالة الملحق",
|
||||
"NotificationOptionPluginUpdateInstalled": "تم تثبيت تحديثات الملحق",
|
||||
"NotificationOptionServerRestartRequired": "يجب إعادة تشغيل الخادم",
|
||||
"NotificationOptionTaskFailed": "فشل في المهمة المجدولة",
|
||||
"NotificationOptionUserLockedOut": "تم إقفال حساب المستخدم",
|
||||
"NotificationOptionPluginUpdateInstalled": "تم تحديث الملحق",
|
||||
"NotificationOptionServerRestartRequired": "مطلوب إعادة تشغيل الخادم",
|
||||
"NotificationOptionTaskFailed": "فشل المهمة المجدولة",
|
||||
"NotificationOptionUserLockedOut": "تم قفل حساب المستخدم",
|
||||
"NotificationOptionVideoPlayback": "بدأ تشغيل الفيديو",
|
||||
"NotificationOptionVideoPlaybackStopped": "تم إيقاف تشغيل الفيديو",
|
||||
"NotificationOptionVideoPlaybackStopped": "توقف تشغيل الفيديو",
|
||||
"Photos": "الصور",
|
||||
"Playlists": "قوائم التشغيل",
|
||||
"Plugin": "الملحق",
|
||||
"PluginInstalledWithName": "تم تثبيت {0}",
|
||||
"PluginUninstalledWithName": "تمت إزالة {0}",
|
||||
"PluginUpdatedWithName": "تم تحديث {0}",
|
||||
"ProviderValue": "المزود: {0}",
|
||||
"ScheduledTaskFailedWithName": "فشلت العملية {0}",
|
||||
"ScheduledTaskStartedWithName": "تم بدء العملية {0}",
|
||||
"ServerNameNeedsToBeRestarted": "يحتاج {0} لإعادة التشغيل",
|
||||
"Shows": "العروض",
|
||||
"ProviderValue": "المزوّد: {0}",
|
||||
"ScheduledTaskFailedWithName": "فشلت {0}",
|
||||
"ScheduledTaskStartedWithName": "بدأت {0}",
|
||||
"ServerNameNeedsToBeRestarted": "يحتاج {0} إلى إعادة التشغيل",
|
||||
"Shows": "المسلسلات",
|
||||
"Songs": "الأغاني",
|
||||
"StartupEmbyServerIsLoading": "يتم تحميل خادم Jellyfin . الرجاء المحاولة بعد قليل.",
|
||||
"SubtitleDownloadFailureFromForItem": "فشل تحميل الترجمات من {0} ل {1}",
|
||||
"StartupEmbyServerIsLoading": "يتم الآن تحميل خادم Jellyfin. يرجى المحاولة مرة أخرى بعد قليل.",
|
||||
"SubtitleDownloadFailureFromForItem": "فشل تنزيل الترجمات من {0} لـ {1}",
|
||||
"Sync": "مزامنة",
|
||||
"System": "النظام",
|
||||
"TvShows": "البرامج التلفزيونية",
|
||||
"User": "المستخدم",
|
||||
"UserCreatedWithName": "تم إنشاء المستخدم {0}",
|
||||
"UserDeletedWithName": "تم حذف المستخدم {0}",
|
||||
"UserDownloadingItemWithValues": "يقوم {0} بتنزيل {1}",
|
||||
"UserLockedOutWithName": "تم منع المستخدم {0} من الدخول",
|
||||
"UserOfflineFromDevice": "تم قطع اتصال {0} من {1}",
|
||||
"UserOnlineFromDevice": "{0} متصل عبر {1}",
|
||||
"UserPasswordChangedWithName": "تم تغيير كلمة السر للمستخدم {0}",
|
||||
"UserPolicyUpdatedWithName": "تم تحديث سياسة المستخدم {0}",
|
||||
"UserStartedPlayingItemWithValues": "قام {0} ببدء تشغيل {1} على {2}",
|
||||
"UserStoppedPlayingItemWithValues": "قام {0} بإيقاف تشغيل {1} على {2}",
|
||||
"ValueHasBeenAddedToLibrary": "تمت اضافت {0} إلى مكتبة الوسائط",
|
||||
"ValueSpecialEpisodeName": "حلقة خاصه - {0}",
|
||||
"UserDownloadingItemWithValues": "{0} يقوم بتنزيل {1}",
|
||||
"UserLockedOutWithName": "تم قفل حساب المستخدم {0}",
|
||||
"UserOfflineFromDevice": "انقطع اتصال {0} من {1}",
|
||||
"UserOnlineFromDevice": "{0} متصل من {1}",
|
||||
"UserPasswordChangedWithName": "تم تغيير كلمة المرور للمستخدم {0}",
|
||||
"UserPolicyUpdatedWithName": "تم تحديث سياسة المستخدم لـ {0}",
|
||||
"UserStartedPlayingItemWithValues": "{0} يقوم بتشغيل {1} على {2}",
|
||||
"UserStoppedPlayingItemWithValues": "أنهى {0} تشغيل {1} على {2}",
|
||||
"ValueHasBeenAddedToLibrary": "تمت إضافة {0} إلى مكتبة المحتوى الخاصة بك",
|
||||
"ValueSpecialEpisodeName": "خاص - {0}",
|
||||
"VersionNumber": "الإصدار {0}",
|
||||
"TaskCleanCacheDescription": "يحذف الملفات المؤقتة التي لم يعد النظام بحاجة إليها.",
|
||||
"TaskCleanCache": "حذف الملفات المؤقتة",
|
||||
"TaskCleanCacheDescription": "يحذف ملفات ذاكرة التخزين المؤقت التي لم يعد النظام بحاجة إليها.",
|
||||
"TaskCleanCache": "تنظيف مجلد ذاكرة التخزين المؤقت",
|
||||
"TasksChannelsCategory": "قنوات الإنترنت",
|
||||
"TasksLibraryCategory": "مكتبة",
|
||||
"TasksMaintenanceCategory": "صيانة",
|
||||
"TaskRefreshLibraryDescription": "يفحص مكتبة الوسائط الخاصة بك باحثا عن ملفات جديدة، ومن ثم يُحدث البيانات الوصفية.",
|
||||
"TaskRefreshLibrary": "افحص مكتبة الوسائط",
|
||||
"TaskRefreshChapterImagesDescription": "يُنشئ صور مصغرة لمقاطع الفيديو التي تحتوي على فصول.",
|
||||
"TaskRefreshChapterImages": "استخراج صور الفصل",
|
||||
"TasksApplicationCategory": "تطبيق",
|
||||
"TaskDownloadMissingSubtitlesDescription": "يبحث في الإنترنت على الترجمات الناقصة استنادا على البيانات الوصفية.",
|
||||
"TaskDownloadMissingSubtitles": "تحميل الترجمات الناقصة",
|
||||
"TaskRefreshChannelsDescription": "يحدث معلومات قنوات الإنترنت.",
|
||||
"TaskRefreshChannels": "إعادة تحديث القنوات",
|
||||
"TaskCleanTranscodeDescription": "يحذف ملفات الترميز الأقدم من يوم واحد.",
|
||||
"TaskCleanTranscode": "حذف ما بمجلد الترميز",
|
||||
"TaskUpdatePluginsDescription": "تحميل وتثبيت الإضافات التي تم تفعيل التحديث التلقائي لها.",
|
||||
"TaskUpdatePlugins": "تحديث الإضافات",
|
||||
"TaskRefreshPeopleDescription": "يقوم بتحديث البيانات الوصفية للممثلين والمخرجين في مكتبة الوسائط الخاصة بك.",
|
||||
"TaskRefreshPeople": "إعادة تحميل الأشخاص",
|
||||
"TaskCleanLogsDescription": "يحذف السجلات الأقدم من {0} يوم.",
|
||||
"TaskCleanLogs": "حذف مسار السجل",
|
||||
"TaskCleanActivityLogDescription": "يحذف سجل الأنشطة الأقدم من الوقت الذي تم تحديده.",
|
||||
"TaskCleanActivityLog": "حذف سجل الأنشطة",
|
||||
"Default": "افتراضي",
|
||||
"Undefined": "غير معرف",
|
||||
"Forced": "ملحقة",
|
||||
"TaskOptimizeDatabaseDescription": "يضغط قاعدة البيانات ويقتطع المساحة الحرة. تشغيل هذه المهمة بعد فحص المكتبة أو إجراء تغييرات أخرى تتضمن تعديلات في قاعدة البيانات قد تؤدي إلى تحسين الأداء.",
|
||||
"TasksLibraryCategory": "المكتبة",
|
||||
"TasksMaintenanceCategory": "الصيانة",
|
||||
"TaskRefreshLibraryDescription": "يفحص مكتبة المحتوى الخاصة بك بحثاً عن ملفات جديدة ويحدّث البيانات الوصفية.",
|
||||
"TaskRefreshLibrary": "فحص مكتبة المحتوى",
|
||||
"TaskRefreshChapterImagesDescription": "ينشئ صوراً مصغرة للفيديوهات التي تحتوي على فصول.",
|
||||
"TaskRefreshChapterImages": "استخراج صور الفصول",
|
||||
"TasksApplicationCategory": "التطبيق",
|
||||
"TaskDownloadMissingSubtitlesDescription": "يبحث في الإنترنت عن الترجمات المفقودة بناءً على إعدادات البيانات الوصفية.",
|
||||
"TaskDownloadMissingSubtitles": "تنزيل الترجمات المفقودة",
|
||||
"TaskRefreshChannelsDescription": "يحدّث معلومات قنوات الإنترنت.",
|
||||
"TaskRefreshChannels": "تحديث القنوات",
|
||||
"TaskCleanTranscodeDescription": "يحذف ملفات تحويل الترميز التي مر عليها أكثر من يوم واحد.",
|
||||
"TaskCleanTranscode": "تنظيف مجلد تحويل الترميز",
|
||||
"TaskUpdatePluginsDescription": "ينزّل ويثبّت التحديثات للملحقات المهيأة للتحديث التلقائي.",
|
||||
"TaskUpdatePlugins": "تحديث الملحقات",
|
||||
"TaskRefreshPeopleDescription": "يحدّث البيانات الوصفية للممثلين والمخرجين في مكتبة المحتوى الخاصة بك.",
|
||||
"TaskRefreshPeople": "تحديث الأشخاص",
|
||||
"TaskCleanLogsDescription": "يحذف ملفات السجل التي يزيد عمرها عن {0} أيام.",
|
||||
"TaskCleanLogs": "تنظيف مجلد السجلات",
|
||||
"TaskCleanActivityLogDescription": "يحذف إدخالات سجل النشاط الأقدم من العمر المحدد.",
|
||||
"TaskCleanActivityLog": "تنظيف سجل النشاط",
|
||||
"Default": "الافتراضي",
|
||||
"Undefined": "غير محدد",
|
||||
"Forced": "إجباري",
|
||||
"TaskOptimizeDatabaseDescription": "يضغط قاعدة البيانات ويقلل المساحة الحرة. قد يؤدي تشغيل هذه المهمة بعد فحص المكتبة أو إجراء تغييرات أخرى تتضمن تعديلات على قاعدة البيانات إلى تحسين الأداء.",
|
||||
"TaskOptimizeDatabase": "تحسين قاعدة البيانات",
|
||||
"TaskKeyframeExtractorDescription": "يستخرج الإطارات الرئيسية من ملفات الفيديو لكي ينشئ قوائم تشغيل بث HTTP المباشر. قد تستمر هذه العملية لوقت طويل.",
|
||||
"TaskKeyframeExtractor": "مستخرج الإطار الرئيسي",
|
||||
"TaskKeyframeExtractorDescription": "يستخرج الإطارات الرئيسية من ملفات الفيديو لإنشاء قوائم تشغيل HLS أكثر دقة. قد يستغرق تشغيل هذه المهمة وقتاً طويلاً.",
|
||||
"TaskKeyframeExtractor": "مستخرج الإطارات الرئيسية",
|
||||
"External": "خارجي",
|
||||
"HearingImpaired": "ضعاف السمع",
|
||||
"TaskRefreshTrickplayImages": "توليد صور المعاينة السريعة",
|
||||
"TaskRefreshTrickplayImagesDescription": "يُولّد معاينات تنقل سريع لمقاطع الفيديو ضمن المكتبات المفعّلة.",
|
||||
"TaskCleanCollectionsAndPlaylists": "حذف المجموعات وقوائم التشغيل",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "حذف عناصر من المجموعات وقوائم التشغيل التي لم تعد موجودة.",
|
||||
"TaskAudioNormalization": "تسوية الصوت",
|
||||
"TaskAudioNormalizationDescription": "مسح الملفات لتطبيع بيانات الصوت.",
|
||||
"TaskDownloadMissingLyrics": "تنزيل عبارات القصيدة",
|
||||
"TaskDownloadMissingLyricsDescription": "كلمات",
|
||||
"TaskExtractMediaSegments": "فحص مقاطع الوسائط",
|
||||
"TaskExtractMediaSegmentsDescription": "يستخرج مقاطع وسائط من إضافات MediaSegment المُفعّلة.",
|
||||
"TaskMoveTrickplayImages": "تغيير مكان صور المعاينة السريعة",
|
||||
"TaskMoveTrickplayImagesDescription": "تُنقل ملفات التشغيل السريع الحالية بناءً على إعدادات المكتبة.",
|
||||
"HearingImpaired": "لضعاف السمع",
|
||||
"TaskRefreshTrickplayImages": "إنشاء صور معاينات التنقل (Trickplay)",
|
||||
"TaskRefreshTrickplayImagesDescription": "ينشئ صور معاينات التنقل السريع للفيديوهات في المكتبات المفعّلة.",
|
||||
"TaskAudioNormalization": "تطبيع الصوت",
|
||||
"TaskAudioNormalizationDescription": "يفحص الملفات لجمع بيانات تطبيع الصوت.",
|
||||
"TaskDownloadMissingLyrics": "تنزيل الكلمات المفقودة",
|
||||
"TaskDownloadMissingLyricsDescription": "ينزّل الكلمات للأغاني.",
|
||||
"TaskExtractMediaSegments": "فحص مقاطع المحتوى",
|
||||
"TaskExtractMediaSegmentsDescription": "يستخرج أو يحصل على مقاطع المحتوى من الملحقات المفعّلة لمقاطع المحتوى (MediaSegment).",
|
||||
"TaskMoveTrickplayImages": "نقل موقع صور معاينات التنقل",
|
||||
"TaskMoveTrickplayImagesDescription": "ينقل ملفات معاينات التنقل الحالية وفقاً لإعدادات المكتبة.",
|
||||
"CleanupUserDataTask": "مهمة تنظيف بيانات المستخدم",
|
||||
"CleanupUserDataTaskDescription": "مسح جميع بيانات المستخدم (حالة المشاهدة، والحالة المفضلة وما إلى ذلك) من الوسائط التي لم تعد موجودة لمدة 90 يومًا على الأقل."
|
||||
"CleanupUserDataTaskDescription": "ينظف جميع بيانات المستخدم (مثل حالة المشاهدة وحالة المفضلة وغيرها) للمحتوى الذي لم يعد موجوداً لمدة 90 يوماً على الأقل.",
|
||||
"Original": "فريد"
|
||||
}
|
||||
|
||||
1
Emby.Server.Implementations/Localization/Core/ar_SA.json
Normal file
1
Emby.Server.Implementations/Localization/Core/ar_SA.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -126,8 +126,6 @@
|
||||
"TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных плэй-лістоў HLS. Гэта задача можа выконвацца доўга.",
|
||||
"TaskRefreshTrickplayImages": "Стварыць выявы Trickplay",
|
||||
"TaskRefreshTrickplayImagesDescription": "Стварае перадпрагляды відэаролікаў для Trickplay у падключаных бібліятэках.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Ачысціць калекцыі і плэй-лісты",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Выдаляе элементы з калекцый і плэй-лістоў, якія больш не існуюць.",
|
||||
"TaskAudioNormalizationDescription": "Скануе файлы на прадмет нармалізацыі гуку.",
|
||||
"TaskAudioNormalization": "Нармалізацыя гуку",
|
||||
"TaskExtractMediaSegmentsDescription": "Выдае або атрымлівае медыясегменты з убудоў з падтрымкай MediaSegment.",
|
||||
|
||||
@@ -128,8 +128,6 @@
|
||||
"TaskRefreshTrickplayImagesDescription": "Създава прегледи на Trickplay за видеа в активирани библиотеки.",
|
||||
"TaskDownloadMissingLyrics": "Свали липсващи текстове",
|
||||
"TaskDownloadMissingLyricsDescription": "Свали текстове за песни",
|
||||
"TaskCleanCollectionsAndPlaylists": "Изчисти колекциите и плейлистите",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Премахни несъществуващи файлове в колекциите и плейлистите.",
|
||||
"TaskAudioNormalization": "Нормализиране на звука",
|
||||
"TaskAudioNormalizationDescription": "Сканирай файловете за нормализация на звука.",
|
||||
"TaskExtractMediaSegmentsDescription": "Изважда медиини сегменти от MediaSegment плъгини.",
|
||||
|
||||
@@ -127,8 +127,6 @@
|
||||
"TaskRefreshTrickplayImages": "ট্রিকপ্লে ইমেজ তৈরি",
|
||||
"TaskRefreshTrickplayImagesDescription": "সক্ষম লাইব্রেরিতে ভিডিওর জন্য ট্রিকপ্লে প্রিভিউ তৈরি করে।",
|
||||
"TaskDownloadMissingLyricsDescription": "গানের জন্য লিরিকস ডাউনলোড করুন",
|
||||
"TaskCleanCollectionsAndPlaylists": "কালেকশন এবং প্লেলিস্ট পরিষ্কার করুন",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "কালেকশন এবং প্লেলিস্ট থেকে আইটেমগুলি সরিয়ে দেয় যা আর বিদ্যমান নেই।",
|
||||
"TaskExtractMediaSegments": "মিডিয়া সেগমেন্ট স্ক্যান",
|
||||
"TaskExtractMediaSegmentsDescription": "মিডিয়া সেগমেন্ট সক্ষম প্লাগইনগুলি থেকে মিডিয়া সেগমেন্ট বের করে বা অর্জন করে।",
|
||||
"TaskDownloadMissingLyrics": "অনুপস্থিত গান ডাউনলোড করুন",
|
||||
|
||||
@@ -130,8 +130,6 @@
|
||||
"TaskOptimizeDatabaseDescription": "Komprimira bazu podataka i čisti slobodan prostor. Pokretanje ovog zadatka nakon skeniranja biblioteke ili izvođenja drugih promjena koje podrazumijevaju izmjene baze podataka može poboljšati performanse.",
|
||||
"TaskKeyframeExtractor": "Izvađač ključnih sličica",
|
||||
"TaskKeyframeExtractorDescription": "Izvlači ključne okvire iz video datoteka kako bi kreirao preciznije HLS playliste. Ovaj zadatak može trajati dugo.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Očistite kolekcije i playliste",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Uklanja stavke iz kolekcija i playlista koje više ne postoje.",
|
||||
"TaskExtractMediaSegments": "Analiza medijskog segmenta",
|
||||
"TaskExtractMediaSegmentsDescription": "Izvlači ili dobija medijske segmente iz dodataka koji podržavaju MediaSegment.",
|
||||
"TaskMoveTrickplayImages": "Migracija lokacije slike Trickplay",
|
||||
|
||||
@@ -126,8 +126,6 @@
|
||||
"HearingImpaired": "Discapacitat auditiva",
|
||||
"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 de les col·leccions i llistes de reproducció",
|
||||
"TaskAudioNormalization": "Estabilització de l'àudio",
|
||||
"TaskAudioNormalizationDescription": "Escaneja els fitxer per a obtenir dades de normalització de l'àudio.",
|
||||
"TaskDownloadMissingLyricsDescription": "Descàrrega de les lletres de les cançons",
|
||||
|
||||
@@ -126,8 +126,6 @@
|
||||
"HearingImpaired": "Sluchově postižení",
|
||||
"TaskRefreshTrickplayImages": "Generovat obrázky pro Trickplay",
|
||||
"TaskRefreshTrickplayImagesDescription": "Obrázky Trickplay se používají k zobrazení náhledů u videí v knihovnách, kde je to povoleno.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Pročistit kolekce a seznamy přehrávání",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Odstraní neexistující položky z kolekcí a seznamů přehrávání.",
|
||||
"TaskAudioNormalization": "Normalizace zvuku",
|
||||
"TaskAudioNormalizationDescription": "Skenovat soubory za účelem normalizace zvuku.",
|
||||
"TaskDownloadMissingLyrics": "Stáhnout chybějící texty k písni",
|
||||
@@ -137,5 +135,6 @@
|
||||
"TaskMoveTrickplayImages": "Přesunout úložiště obrázků Trickplay",
|
||||
"TaskMoveTrickplayImagesDescription": "Přesune existující soubory Trickplay podle nastavení knihovny.",
|
||||
"CleanupUserDataTaskDescription": "Odstraní všechna uživatelská data (stav zhlédnutí, oblíbené atd.) z médií, které již neexistují více než 90 dní.",
|
||||
"CleanupUserDataTask": "Pročistit uživatelská data"
|
||||
"CleanupUserDataTask": "Pročistit uživatelská data",
|
||||
"Original": "Originál"
|
||||
}
|
||||
|
||||
@@ -130,7 +130,5 @@
|
||||
"TaskRefreshTrickplayImagesDescription": "Creu rhagolygon Trickplay ar gyfer fideos mewn llyfrgelloedd gweithredol.",
|
||||
"TaskDownloadMissingLyrics": "Lawrlwytho geiriau coll",
|
||||
"TaskDownloadMissingLyricsDescription": "Lawrlwytho geiriau caneuon",
|
||||
"TaskCleanCollectionsAndPlaylists": "Glanhau casgliadau a rhestrau chwarae",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Dileu eitemau o gasgliadau a rhestrau chwarae sydd ddim yn bodoli bellach.",
|
||||
"TaskExtractMediaSegments": "Sganio Darnau Cyfryngau"
|
||||
}
|
||||
|
||||
@@ -126,8 +126,6 @@
|
||||
"HearingImpaired": "Hørehæmmet",
|
||||
"TaskRefreshTrickplayImages": "Generer trickplay-billeder",
|
||||
"TaskRefreshTrickplayImagesDescription": "Laver trickplay-billeder for videoer i aktiverede biblioteker.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Ryd op i samlinger og afspilningslister",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Fjerner elementer fra samlinger og afspilningslister der ikke eksisterer længere.",
|
||||
"TaskAudioNormalizationDescription": "Skanner filer for data vedrørende lydnormalisering.",
|
||||
"TaskAudioNormalization": "Lydnormalisering",
|
||||
"TaskDownloadMissingLyricsDescription": "Søger på internettet efter manglende sangtekster baseret på metadata-konfigurationen",
|
||||
|
||||
@@ -126,8 +126,6 @@
|
||||
"HearingImpaired": "Hörgeschädigt",
|
||||
"TaskRefreshTrickplayImages": "Trickplay-Bilder generieren",
|
||||
"TaskRefreshTrickplayImagesDescription": "Erstellt ein Trickplay-Vorschauen für Videos in aktivierten Bibliotheken.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Sammlungen und Playlisten aufräumen",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Löscht nicht mehr vorhandene Einträge aus den Sammlungen und Playlisten.",
|
||||
"TaskAudioNormalization": "Audio Normalisierung",
|
||||
"TaskAudioNormalizationDescription": "Durchsucht Dateien nach Audionormalisierungsdaten.",
|
||||
"TaskDownloadMissingLyricsDescription": "Lädt Songtexte herunter",
|
||||
@@ -137,5 +135,6 @@
|
||||
"TaskMoveTrickplayImages": "Verzeichnis für Trickplay-Bilder migrieren",
|
||||
"TaskMoveTrickplayImagesDescription": "Trickplay-Bilder werden entsprechend der Bibliothekseinstellungen verschoben.",
|
||||
"CleanupUserDataTask": "Aufgabe zur Bereinigung von Benutzerdaten",
|
||||
"CleanupUserDataTaskDescription": "Löscht alle Benutzerdaten (Abspielstatus, Favoritenstatus, usw.) von Medien, die seit mindestens 90 Tagen nicht mehr vorhanden sind."
|
||||
"CleanupUserDataTaskDescription": "Löscht alle Benutzerdaten (Abspielstatus, Favoritenstatus, usw.) von Medien, die seit mindestens 90 Tagen nicht mehr vorhanden sind.",
|
||||
"Original": "Original"
|
||||
}
|
||||
|
||||
@@ -128,8 +128,6 @@
|
||||
"TaskRefreshTrickplayImagesDescription": "Δημιουργεί προεπισκοπήσεις trickplay για βίντεο σε ενεργοποιημένες βιβλιοθήκες.",
|
||||
"TaskAudioNormalization": "Ομοιομορφία ήχου",
|
||||
"TaskAudioNormalizationDescription": "Ανίχνευση αρχείων για δεδομένα ομοιομορφίας ήχου.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Καθαρισμός συλλογών και λιστών αναπαραγωγής",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Αφαιρούνται στοιχεία από τις συλλογές και τις λίστες αναπαραγωγής που δεν υπάρχουν πλέον.",
|
||||
"TaskMoveTrickplayImages": "Αλλαγή τοποθεσίας εικόνων Trickplay",
|
||||
"TaskDownloadMissingLyrics": "Λήψη στίχων που λείπουν",
|
||||
"TaskMoveTrickplayImagesDescription": "Μετακινεί τα υπάρχοντα αρχεία trickplay σύμφωνα με τις ρυθμίσεις της βιβλιοθήκης.",
|
||||
|
||||
@@ -126,8 +126,6 @@
|
||||
"HearingImpaired": "Hearing Impaired",
|
||||
"TaskRefreshTrickplayImages": "Generate Trickplay Images",
|
||||
"TaskRefreshTrickplayImagesDescription": "Creates trickplay previews for videos in enabled libraries.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Clean up collections and playlists",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Removes items from collections and playlists that no longer exist.",
|
||||
"TaskAudioNormalization": "Audio Normalisation",
|
||||
"TaskAudioNormalizationDescription": "Scans files for audio normalisation data.",
|
||||
"TaskDownloadMissingLyrics": "Download missing lyrics",
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
"NotificationOptionUserLockedOut": "User locked out",
|
||||
"NotificationOptionVideoPlayback": "Video playback started",
|
||||
"NotificationOptionVideoPlaybackStopped": "Video playback stopped",
|
||||
"Original": "Original",
|
||||
"Photos": "Photos",
|
||||
"Playlists": "Playlists",
|
||||
"Plugin": "Plugin",
|
||||
@@ -130,8 +131,6 @@
|
||||
"TaskOptimizeDatabaseDescription": "Compacts database and truncates free space. Running this task after scanning the library or doing other changes that imply database modifications might improve performance.",
|
||||
"TaskKeyframeExtractor": "Keyframe Extractor",
|
||||
"TaskKeyframeExtractorDescription": "Extracts keyframes from video files to create more precise HLS playlists. This task may run for a long time.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Clean up collections and playlists",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Removes items from collections and playlists that no longer exist.",
|
||||
"TaskExtractMediaSegments": "Media Segment Scan",
|
||||
"TaskExtractMediaSegmentsDescription": "Extracts or obtains media segments from MediaSegment enabled plugins.",
|
||||
"TaskMoveTrickplayImages": "Migrate Trickplay Image Location",
|
||||
|
||||
@@ -128,8 +128,6 @@
|
||||
"TaskRefreshTrickplayImagesDescription": "Crea vistas previas de reproducción engañosa para videos en bibliotecas habilitadas.",
|
||||
"TaskAudioNormalization": "Normalización de audio",
|
||||
"TaskAudioNormalizationDescription": "Escanea archivos en busca de datos de normalización de audio.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Limpiar colecciones y listas de reproducción",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Elimina elementos de colecciones y listas de reproducción que ya no existen.",
|
||||
"TaskDownloadMissingLyrics": "Descargar letra faltante",
|
||||
"TaskDownloadMissingLyricsDescription": "Descarga letras de canciones",
|
||||
"TaskExtractMediaSegments": "Escanear Segmentos de Media",
|
||||
|
||||
@@ -128,8 +128,6 @@
|
||||
"TaskRefreshTrickplayImages": "Generar imágenes de la barra de reproducción",
|
||||
"TaskAudioNormalization": "Normalización de audio",
|
||||
"TaskAudioNormalizationDescription": "Analiza los archivos para normalizar el audio.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Limpieza de colecciones y listas de reproducción",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Quita elementos que ya no existen de colecciones y listas de reproducción.",
|
||||
"TaskDownloadMissingLyrics": "descargar letras que faltan",
|
||||
"TaskDownloadMissingLyricsDescription": "Descargar letras de canciones",
|
||||
"TaskExtractMediaSegments": "Escaneo de segmentos de medios",
|
||||
|
||||
@@ -126,8 +126,6 @@
|
||||
"HearingImpaired": "Discapacidad Auditiva",
|
||||
"TaskRefreshTrickplayImages": "Generar miniaturas de línea de tiempo",
|
||||
"TaskRefreshTrickplayImagesDescription": "Crear miniaturas de tiempo para videos en las librerías habilitadas.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Limpiar colecciones y listas de reproducción",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Elimina elementos de colecciones y listas de reproducción que ya no existen.",
|
||||
"TaskAudioNormalization": "Normalización de audio",
|
||||
"TaskAudioNormalizationDescription": "Escanear archivos para obtener datos de normalización.",
|
||||
"TaskDownloadMissingLyricsDescription": "Descargar letras para las canciones",
|
||||
@@ -137,5 +135,6 @@
|
||||
"TaskExtractMediaSegmentsDescription": "Extrae u obtiene segmentos de medios de plugins habilitados para MediaSegment.",
|
||||
"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."
|
||||
"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.",
|
||||
"Original": "Original"
|
||||
}
|
||||
|
||||
@@ -127,9 +127,7 @@
|
||||
"TaskRefreshTrickplayImagesDescription": "Crea previsualizaciones para la barra de reproducción en las bibliotecas habilitadas.",
|
||||
"TaskRefreshTrickplayImages": "Generar imágenes de la barra de reproducción",
|
||||
"TaskAudioNormalization": "Normalización de audio",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Quita elementos que ya no existen de colecciones y listas de reproducción.",
|
||||
"TaskAudioNormalizationDescription": "Analiza los archivos para normalizar el audio.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Limpieza de colecciones y listas de reproducción",
|
||||
"TaskDownloadMissingLyrics": "Descargar letra faltante",
|
||||
"TaskDownloadMissingLyricsDescription": "Descarga letras de canciones",
|
||||
"TaskExtractMediaSegmentsDescription": "Extrae u obtiene segmentos de medios de complementos habilitados para MediaSegment.",
|
||||
|
||||
@@ -41,8 +41,6 @@
|
||||
"TaskKeyframeExtractorDescription": "Extrae Fotogramas Clave de los archivos de vídeo para crear Listas de Reproducción HLS más precisas. Esta tarea puede durar mucho tiempo.",
|
||||
"TaskAudioNormalization": "Normalización de audio",
|
||||
"TaskAudioNormalizationDescription": "Escanear archivos para la normalización de data.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Limpiar colecciones y listas de reproducción",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Remover elementos de colecciones y listas de reproducción que no existen.",
|
||||
"TvShows": "Series de TV",
|
||||
"UserStartedPlayingItemWithValues": "{0} está reproduciendo {1} en {2}",
|
||||
"TaskRefreshChannels": "Actualizar canales",
|
||||
|
||||
@@ -128,8 +128,6 @@
|
||||
"TaskRefreshTrickplayImagesDescription": "Loob trickplay eelvaated videotele lubatud meediakogudes.",
|
||||
"TaskAudioNormalization": "Normaliseeri helitugevus",
|
||||
"TaskAudioNormalizationDescription": "Otsib failidest helitugevuse normaliseerimise teavet.",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Eemaldab kogumikest ja esitusloenditest üksused, mida enam ei eksisteeri.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Puhasta kogumikud ja esitusloendid",
|
||||
"TaskDownloadMissingLyrics": "Hangi puuduvad laulusõnad",
|
||||
"TaskDownloadMissingLyricsDescription": "Laulusõnade allalaadimine",
|
||||
"TaskMoveTrickplayImagesDescription": "Liigutab trickplay pildid meediakogu sätete kohaselt.",
|
||||
|
||||
@@ -130,8 +130,6 @@
|
||||
"TaskDownloadMissingLyrics": "Deskargatu falta diren letrak",
|
||||
"TaskDownloadMissingLyricsDescription": "Deskargatu abestientzako letrak",
|
||||
"TaskExtractMediaSegments": "Multimedia segmentuen eskaneoa",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Jada existitzen ez diren bildumak eta erreprodukzio-zerrendak kentzen ditu.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Garbitu bildumak eta erreprodukzio-zerrendak",
|
||||
"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.",
|
||||
|
||||
@@ -126,8 +126,6 @@
|
||||
"HearingImpaired": "مشکل شنوایی",
|
||||
"TaskRefreshTrickplayImages": "تولید تصاویر Trickplay",
|
||||
"TaskRefreshTrickplayImagesDescription": "تولید پیشنمایش های trickplay برای ویدیو های فعال شده در کتابخانه.",
|
||||
"TaskCleanCollectionsAndPlaylists": "پاکسازی مجموعه ها و لیست پخش",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "موارد را از مجموعه ها و لیست پخش هایی که دیگر وجود ندارند حذف میکند.",
|
||||
"TaskAudioNormalizationDescription": "بررسی فایل برای دادههای نرمال کردن صدا.",
|
||||
"TaskDownloadMissingLyrics": "دانلود متنهای ناموجود",
|
||||
"TaskDownloadMissingLyricsDescription": "دانلود متن شعرها",
|
||||
|
||||
@@ -126,8 +126,6 @@
|
||||
"HearingImpaired": "Kuulorajoitteinen",
|
||||
"TaskRefreshTrickplayImages": "Luo Trickplay-kuvat",
|
||||
"TaskRefreshTrickplayImagesDescription": "Luo Trickplay-esikatselut käytössä olevien kirjastojen videoista.",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Poistaa kohteet kokoelmista ja soittolistoista joita ei ole enää olemassa.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Puhdista kokoelmat ja soittolistat",
|
||||
"TaskAudioNormalization": "Äänenvoimakkuuden normalisointi",
|
||||
"TaskAudioNormalizationDescription": "Etsii tiedostoista äänenvoimakkuuden normalisointitietoja.",
|
||||
"TaskDownloadMissingLyrics": "Lataa puuttuva lyriikka",
|
||||
|
||||
@@ -126,8 +126,6 @@
|
||||
"HearingImpaired": "Malentendants",
|
||||
"TaskRefreshTrickplayImages": "Générer des images Trickplay",
|
||||
"TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Nettoyer les collections et les listes de lecture",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Supprime les éléments des collections et des listes de lecture qui n'existent plus.",
|
||||
"TaskAudioNormalization": "Normalisation audio",
|
||||
"TaskAudioNormalizationDescription": "Analyse les fichiers à la recherche de données de normalisation audio.",
|
||||
"TaskExtractMediaSegments": "Analyse des segments de média",
|
||||
|
||||
@@ -126,8 +126,6 @@
|
||||
"HearingImpaired": "Malentendants",
|
||||
"TaskRefreshTrickplayImages": "Générer des images Trickplay",
|
||||
"TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Nettoyer les collections et les listes de lecture",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Supprime les éléments des collections et des listes de lecture qui n'existent plus.",
|
||||
"TaskAudioNormalization": "Normalisation audio",
|
||||
"TaskAudioNormalizationDescription": "Analyse les fichiers à la recherche de données de normalisation audio.",
|
||||
"TaskDownloadMissingLyricsDescription": "Téléchargement des paroles des chansons",
|
||||
|
||||
@@ -29,12 +29,10 @@
|
||||
"TaskRefreshChannelsDescription": "Athnuachan eolas faoi chainéil idirlín.",
|
||||
"TaskOptimizeDatabase": "Bunachar sonraí a bharrfheabhsú",
|
||||
"TaskKeyframeExtractorDescription": "Baintear eochairfhrámaí as comhaid físe chun seinmliostaí HLS níos cruinne a chruthú. Féadfaidh an tasc seo a bheith ar siúl ar feadh i bhfad.",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Baintear míreanna as bailiúcháin agus seinmliostaí nach ann dóibh a thuilleadh.",
|
||||
"TaskDownloadMissingLyricsDescription": "Íosluchtaigh liricí do na hamhráin",
|
||||
"TaskUpdatePluginsDescription": "Íoslódálann agus suiteálann nuashonruithe do bhreiseáin atá cumraithe le nuashonrú go huathoibríoch.",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Déanann sé cuardach ar an idirlíon le haghaidh fotheidil atá ar iarraidh bunaithe ar chumraíocht meiteashonraí.",
|
||||
"TaskExtractMediaSegmentsDescription": "Sliocht nó faigheann codanna meán ó bhreiseáin chumasaithe MediaSegment.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Glan suas bailiúcháin agus seinmliostaí",
|
||||
"TaskOptimizeDatabaseDescription": "Comhdhlúthaíonn bunachar sonraí agus gearrtar spás saor in aisce. Má ritheann tú an tasc seo tar éis scanadh a dhéanamh ar an leabharlann nó athruithe eile a dhéanamh a thugann le tuiscint gur cheart go bhfeabhsófaí an fheidhmíocht.",
|
||||
"TaskMoveTrickplayImagesDescription": "Bogtar comhaid trickplay atá ann cheana de réir socruithe na leabharlainne.",
|
||||
"AppDeviceValues": "Aip: {0}, Gléas: {1}",
|
||||
@@ -137,5 +135,6 @@
|
||||
"TaskCleanTranscode": "Eolaire Transcode Glan",
|
||||
"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."
|
||||
"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.",
|
||||
"Original": "Bunaidh"
|
||||
}
|
||||
|
||||
@@ -128,8 +128,6 @@
|
||||
"TaskRefreshTrickplayImagesDescription": "Crea miniaturas de previsualización para os vídeos nas bibliotecas habilitadas.",
|
||||
"TaskDownloadMissingLyrics": "Descargar letras que faltan",
|
||||
"TaskDownloadMissingLyricsDescription": "Descarga as letras das cancións",
|
||||
"TaskCleanCollectionsAndPlaylists": "Limpar coleccións e listas de reprodución",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Quita ítems que xa non existen das coleccións e listas de reprodución.",
|
||||
"TaskExtractMediaSegmentsDescription": "Procura segmentos de medios cos plugins habilitados.",
|
||||
"TaskExtractMediaSegments": "Escaneo de segmentos de medios",
|
||||
"TaskMoveTrickplayImages": "Migrar as miniaturas de previsualización a outra ubicación",
|
||||
|
||||
@@ -127,9 +127,7 @@
|
||||
"TaskRefreshTrickplayImages": "יצירת תמונות Trickplay",
|
||||
"TaskRefreshTrickplayImagesDescription": "יוצר תמונות Trickplay לסרטונים בספריות הפעילות.",
|
||||
"TaskAudioNormalization": "נרמול שמע",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "מנקה פריטים לא קיימים מאוספים ורשימות השמעה.",
|
||||
"TaskAudioNormalizationDescription": "מחפש קבצי נורמליזציה של שמע.",
|
||||
"TaskCleanCollectionsAndPlaylists": "מנקה אוספים ורשימות השמעה",
|
||||
"TaskDownloadMissingLyrics": "הורדת מילים חסרות",
|
||||
"TaskDownloadMissingLyricsDescription": "הורדת מילים לשירים",
|
||||
"TaskMoveTrickplayImages": "העברת מיקום של תמונות Trickplay",
|
||||
|
||||
@@ -134,7 +134,5 @@
|
||||
"TaskExtractMediaSegmentsDescription": "मीडियासेगमेंट सक्षम प्लगइन्स से मीडिया सेगमेंट निकालता है या प्राप्त करता है।",
|
||||
"TaskMoveTrickplayImages": "ट्रिकप्ले छवि स्थान माइग्रेट करें",
|
||||
"TaskMoveTrickplayImagesDescription": "लाइब्रेरी सेटिंग्स के अनुसार मौजूदा ट्रिकप्ले फ़ाइलों को स्थानांतरित करता है।",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "संग्रहों और प्लेलिस्टों से उन आइटमों को हटाता है जो अब मौजूद नहीं हैं।",
|
||||
"TaskCleanCollectionsAndPlaylists": "संग्रह और प्लेलिस्ट साफ़ करें",
|
||||
"CleanupUserDataTask": "यूज़र डेटा सफाई कार्य"
|
||||
}
|
||||
|
||||
@@ -128,8 +128,6 @@
|
||||
"TaskRefreshTrickplayImagesDescription": "Stvara preglede brzog pregledavanja za videa u aktiviranim bibliotekama.",
|
||||
"TaskAudioNormalization": "Normalizacija zvuka",
|
||||
"TaskAudioNormalizationDescription": "Skenira datoteke u potrazi za podacima o normalizaciji zvuka.",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Uklanja stavke iz zbirki i popisa za reprodukciju koje više ne postoje.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Očisti zbirke i popise za reprodukciju",
|
||||
"TaskExtractMediaSegments": "Skeniranje dijelova medija",
|
||||
"TaskDownloadMissingLyrics": "Preuzmi tekstove koji nedostaju",
|
||||
"TaskDownloadMissingLyricsDescription": "Preuzmi tekstove pjesama",
|
||||
@@ -137,5 +135,6 @@
|
||||
"TaskMoveTrickplayImages": "Premjesti mjesto slika brzog pregledavanja",
|
||||
"TaskMoveTrickplayImagesDescription": "Premješta postojeće datoteke brzog pregledavanja u postavke biblioteke.",
|
||||
"CleanupUserDataTask": "Zadatak čišćenja korisničkih podataka",
|
||||
"CleanupUserDataTaskDescription": "Briše sve korisničke podatke (stanje gledanja, status favorita itd.) s medija koji više nisu prisutni najmanje 90 dana."
|
||||
"CleanupUserDataTaskDescription": "Briše sve korisničke podatke (stanje gledanja, status favorita itd.) s medija koji više nisu prisutni najmanje 90 dana.",
|
||||
"Original": "Original"
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"MixedContent": "Vegyes tartalom",
|
||||
"Movies": "Filmek",
|
||||
"Music": "Zenék",
|
||||
"MusicVideos": "Zenei videóklipek",
|
||||
"MusicVideos": "Zenei videók",
|
||||
"NameInstallFailed": "{0} sikertelen telepítés",
|
||||
"NameSeasonNumber": "{0}. évad",
|
||||
"NameSeasonUnknown": "Ismeretlen évad",
|
||||
@@ -127,9 +127,7 @@
|
||||
"TaskRefreshTrickplayImages": "Trickplay képek előállítása",
|
||||
"TaskRefreshTrickplayImagesDescription": "Trickplay előnézetet készít az engedélyezett könyvtárakban lévő videókhoz.",
|
||||
"TaskAudioNormalization": "Hangerő-normalizálás",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Nem létező elemek törlése a gyűjteményekből és lejátszási listákról.",
|
||||
"TaskAudioNormalizationDescription": "Hangerő-normalizálási adatok keresése.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Gyűjtemények és lejátszási listák optimalizálása",
|
||||
"TaskExtractMediaSegments": "Médiaszegmens felismerése",
|
||||
"TaskDownloadMissingLyrics": "Hiányzó szöveg letöltése",
|
||||
"TaskDownloadMissingLyricsDescription": "Zenék szövegének letöltése",
|
||||
@@ -137,5 +135,6 @@
|
||||
"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.",
|
||||
"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"
|
||||
"CleanupUserDataTask": "Felhasználói adatok tisztítása feladat",
|
||||
"Original": "Eredeti"
|
||||
}
|
||||
|
||||
@@ -128,8 +128,6 @@
|
||||
"TaskRefreshTrickplayImagesDescription": "Buat pratinjau trickplay untuk video di perpustakaan yang diaktifkan.",
|
||||
"TaskAudioNormalizationDescription": "Pindai file untuk data normalisasi audio.",
|
||||
"TaskAudioNormalization": "Normalisasi Audio",
|
||||
"TaskCleanCollectionsAndPlaylists": "Bersihkan koleksi dan daftar putar",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Menghapus item dari koleksi dan daftar putar yang sudah tidak ada.",
|
||||
"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.",
|
||||
|
||||
@@ -128,8 +128,6 @@
|
||||
"TaskRefreshTrickplayImages": "Búa til hraðspilunarmyndir",
|
||||
"TaskAudioNormalization": "Hljóðstöðlun",
|
||||
"TaskAudioNormalizationDescription": "Leitar að hljóðstöðlunargögnum í skrám.",
|
||||
"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",
|
||||
"TaskExtractMediaSegments": "Skönnun efnishluta",
|
||||
|
||||
@@ -126,8 +126,6 @@
|
||||
"HearingImpaired": "Non udenti",
|
||||
"TaskRefreshTrickplayImages": "Genera immagini Trickplay",
|
||||
"TaskRefreshTrickplayImagesDescription": "Crea anteprime trickplay per i video nelle librerie abilitate.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Ripulisci le collezioni e le scalette",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Rimuove gli elementi dalle collezioni e dalle scalette che non esistono più.",
|
||||
"TaskAudioNormalization": "Normalizzazione dell'audio",
|
||||
"TaskAudioNormalizationDescription": "Scansiona i file alla ricerca dei dati per la normalizzazione dell'audio.",
|
||||
"TaskDownloadMissingLyricsDescription": "Scarica testi per le canzoni",
|
||||
@@ -137,5 +135,6 @@
|
||||
"TaskExtractMediaSegmentsDescription": "Estrae o ottiene segmenti multimediali dai plugin abilitati MediaSegment.",
|
||||
"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."
|
||||
"CleanupUserDataTaskDescription": "Pulisce tutti i dati utente (stato di visione, status preferiti, ecc.) dai contenuti non più presenti da almeno 90 giorni.",
|
||||
"Original": "Originale"
|
||||
}
|
||||
|
||||
@@ -126,10 +126,8 @@
|
||||
"HearingImpaired": "聴覚障害の方",
|
||||
"TaskRefreshTrickplayImages": "トリックプレー画像を生成",
|
||||
"TaskRefreshTrickplayImagesDescription": "有効なライブラリ内のビデオをもとにトリックプレーのプレビューを生成します。",
|
||||
"TaskCleanCollectionsAndPlaylists": "コレクションとプレイリストをクリーンアップ",
|
||||
"TaskAudioNormalization": "音声の正規化",
|
||||
"TaskAudioNormalizationDescription": "音声の正規化データのためにファイルをスキャンします。",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "在しなくなったコレクションやプレイリストからアイテムを削除します。",
|
||||
"TaskDownloadMissingLyricsDescription": "歌詞をダウンロード",
|
||||
"TaskExtractMediaSegments": "メディアセグメントを読み取る",
|
||||
"TaskMoveTrickplayImages": "Trickplayの画像を移動",
|
||||
|
||||
@@ -130,8 +130,6 @@
|
||||
"TaskAudioNormalizationDescription": "აანალიზებს ფაილებს აუდიოს ნორმალიზაციისთვის.",
|
||||
"TaskDownloadMissingLyrics": "მიუწვდომელი ლირიკების ჩამოტვირთვა",
|
||||
"TaskDownloadMissingLyricsDescription": "ჩამოტვირთავს ამჟამად ბიბლიოთეკაში არარსებულ ლირიკებს სიმღერებისთვის",
|
||||
"TaskCleanCollectionsAndPlaylists": "კოლექციების და დასაკრავი სიების გასუფთავება",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "შლის არარსებულ ერთეულებს კოლექციებიდან და დასაკრავი სიებიდან.",
|
||||
"TaskExtractMediaSegments": "მედია სეგმენტების სკანირება",
|
||||
"TaskExtractMediaSegmentsDescription": "მედია სეგმენტების სკანირება მხარდაჭერილი მოდულებისთვის.",
|
||||
"TaskMoveTrickplayImages": "Trickplay სურათების მიგრაცია",
|
||||
|
||||
@@ -127,7 +127,5 @@
|
||||
"TaskUpdatePluginsDescription": "ទាញយក និងដំឡើងបច្ចុប្បន្នភាពសម្រាប់Plugins ដែលត្រូវបាន Config ដើម្បីធ្វើបច្ចុប្បន្នភាពដោយស្វ័យប្រវត្តិ.",
|
||||
"TaskCleanTranscodeDescription": "លុបឯកសារ Transcode ដែលលើសពីមួយថ្ងៃ.",
|
||||
"TaskDownloadMissingSubtitlesDescription": "ស្វែងរកតាមអ៊ីនធឺណិត សម្រាប់សាប់ថាយថល ដែលបាត់ដោយផ្អែកលើ metadata.",
|
||||
"TaskOptimizeDatabase": "ធ្វើឱ្យ Database ប្រសើរឡើង",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "លុបរបស់របរចេញពីបណ្តុំ និងបញ្ជីចាក់ដែលលែងមាន.",
|
||||
"TaskCleanCollectionsAndPlaylists": "សម្អាតបណ្តុំ និងបញ្ជីចាក់"
|
||||
"TaskOptimizeDatabase": "ធ្វើឱ្យ Database ប្រសើរឡើង"
|
||||
}
|
||||
|
||||
@@ -129,7 +129,5 @@
|
||||
"TaskExtractMediaSegments": "ಮಾಧ್ಯಮ ವಿಭಾಗದ ಹುಡುಕು",
|
||||
"TaskDownloadMissingLyrics": "ಇಲ್ಲದ ಸಾಹಿತ್ಯವನ್ನು ಪಡೆಯಿರಿ",
|
||||
"TaskAudioNormalization": "ಧ್ವನಿ ಸಾಮಾನ್ಯೀಕರಣ",
|
||||
"TaskRefreshTrickplayImages": "ಟ್ರಿಕ್ಪ್ಲೇ ಚಿತ್ರಗಳನ್ನು ರಚಿಸಿ",
|
||||
"TaskCleanCollectionsAndPlaylists": "ಸಂಗ್ರಹಗಳು ಮತ್ತು ಪ್ಲೇಪಟ್ಟಿಗಳನ್ನು ಸ್ವಚ್ಛಗೊಳಿಸಿ",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "ಇಲ್ಲದ ಸಂಗ್ರಹಗಳು ಮತ್ತು ಪ್ಲೇಪಟ್ಟಿಗಳಿಂದ ವಸ್ತುಗಳನ್ನು ತೆಗೆದುಹಾಕುತ್ತದೆ."
|
||||
"TaskRefreshTrickplayImages": "ಟ್ರಿಕ್ಪ್ಲೇ ಚಿತ್ರಗಳನ್ನು ರಚಿಸಿ"
|
||||
}
|
||||
|
||||
@@ -124,12 +124,10 @@
|
||||
"TaskKeyframeExtractor": "키프레임 추출",
|
||||
"External": "외부",
|
||||
"HearingImpaired": "청각 장애",
|
||||
"TaskCleanCollectionsAndPlaylists": "컬렉션과 재생목록 정리",
|
||||
"TaskAudioNormalization": "오디오의 볼륨 수준을 일정하게 조정",
|
||||
"TaskAudioNormalizationDescription": "오디오의 볼륨 수준을 일정하게 조정하기 위해 파일을 스캔합니다.",
|
||||
"TaskRefreshTrickplayImages": "비디오 탐색용 미리보기 썸네일 생성",
|
||||
"TaskRefreshTrickplayImagesDescription": "활성화된 라이브러리에서 비디오의 트릭플레이 미리보기를 생성합니다.",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "더 이상 존재하지 않는 컬렉션 및 재생 목록에서 항목을 제거합니다.",
|
||||
"TaskExtractMediaSegments": "미디어 세그먼트 스캔",
|
||||
"TaskExtractMediaSegmentsDescription": "MediaSegment를 지원하는 플러그인에서 미디어 세그먼트를 추출하거나 가져옵니다.",
|
||||
"TaskMoveTrickplayImages": "트릭플레이 이미지 위치 마이그레이션",
|
||||
|
||||
@@ -128,9 +128,7 @@
|
||||
"TaskOptimizeDatabaseDescription": "Y hwra kesstrotha ha berrhe efander rydh. Martesen y hwra gwellhe gwryth mar kwre'ta an oberen ma wosa ty dhe arhwilas an lyverva, po neb chanj aral neb a brof chanjyansow selvanylyon.",
|
||||
"TaskAudioNormalizationDescription": "Y hwra arhwilas restrennow rag manylyon normalheans klewans.",
|
||||
"TaskRefreshLibraryDescription": "Y hwra arhwilas dha lyverva media rag restrennow nowydh ha disegha metamanylyon.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Glanhe kuntellow ha rolyow-gwari",
|
||||
"TaskKeyframeExtractor": "Estennell Framalhwedh",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Y hwra dilea taklow a-dhyworth kuntellow ha rolyow-gwari na vos na moy.",
|
||||
"TaskKeyframeExtractorDescription": "Y hwra kuntel framyowalhwedh a-dhyworth restrennow gwydhyowyow rag gul rolyow-gwari HLS moy poran. Martesen y hwra an oberen ma ow ponya rag termyn hir.",
|
||||
"TaskExtractMediaSegments": "Arhwilas Rann Media",
|
||||
"TaskExtractMediaSegmentsDescription": "Kavos rannow media a-dhyworth ystynansow gallosegys MediaSegment.",
|
||||
|
||||
@@ -104,8 +104,6 @@
|
||||
"TaskDownloadMissingSubtitles": "Fehlend Ënnertitelen eroflueden",
|
||||
"TaskOptimizeDatabase": "Datebank optiméieren",
|
||||
"TaskKeyframeExtractor": "Schlësselbild Extrakter",
|
||||
"TaskCleanCollectionsAndPlaylists": "Sammlungen a Playlisten botzen",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Ewechhuele vun Elementer aus Sammlungen a Playlisten, déi net méi existéieren.",
|
||||
"TaskExtractMediaSegments": "Mediesegment-Scan",
|
||||
"NewVersionIsAvailable": "Nei Versioun fir Jellyfin Server ass verfügbar.",
|
||||
"CameraImageUploadedFrom": "En neit Kamera Bild gouf vu {0} eropgelueden",
|
||||
|
||||
@@ -126,8 +126,6 @@
|
||||
"HearingImpaired": "Su klausos sutrikimais",
|
||||
"TaskRefreshTrickplayImages": "Generuoti Trickplay atvaizdus",
|
||||
"TaskRefreshTrickplayImagesDescription": "Sukuria trickplay peržiūras vaizdo įrašams įgalintose bibliotekose.",
|
||||
"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",
|
||||
|
||||
@@ -127,9 +127,7 @@
|
||||
"TaskRefreshTrickplayImages": "Ģenerēt partīšanas attēlus",
|
||||
"TaskRefreshTrickplayImagesDescription": "Izveido priekšskatījumus videoklipu pārtīšanai iespējotajās bibliotēkās.",
|
||||
"TaskAudioNormalization": "Audio normalizācija",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Noņem vairs neeksistējošus vienumus no kolekcijām un atskaņošanas sarakstiem.",
|
||||
"TaskAudioNormalizationDescription": "Skanē failus priekš audio normālizācijas informācijas.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Notīrīt kolekcijas un atskaņošanas sarakstus",
|
||||
"TaskExtractMediaSegments": "Multivides segmenta skenēšana",
|
||||
"TaskExtractMediaSegmentsDescription": "Izvelk vai iegūst multivides segmentus no MediaSegment iespējotiem spraudņiem.",
|
||||
"TaskMoveTrickplayImages": "Trickplay attēlu pārvietošana",
|
||||
@@ -137,5 +135,6 @@
|
||||
"TaskDownloadMissingLyrics": "Lejupielādēt trūkstošos vārdus",
|
||||
"TaskDownloadMissingLyricsDescription": "Lejupielādēt vārdus dziesmām",
|
||||
"CleanupUserDataTask": "Lietotāju datu tīrīšanas uzdevums",
|
||||
"CleanupUserDataTaskDescription": "Notīra visus lietotāja datus (skatīšanās stāvokļus, favorītu statusi utt.) no medijiem, kas vairs nav pieejami vismaz 90 dienas."
|
||||
"CleanupUserDataTaskDescription": "Notīra visus lietotāja datus (skatīšanās stāvokļus, favorītu statusi utt.) no medijiem, kas vairs nav pieejami vismaz 90 dienas.",
|
||||
"Original": "Oriģināls"
|
||||
}
|
||||
|
||||
@@ -124,13 +124,15 @@
|
||||
"TaskCleanActivityLog": "Избриши Лог на Активности",
|
||||
"External": "Надворешен",
|
||||
"HearingImpaired": "Оштетен слух",
|
||||
"TaskCleanCollectionsAndPlaylists": "Исчисти ги колекциите и плејлистите",
|
||||
"TaskAudioNormalizationDescription": "Скенирање датотеки за податоци за нормализација на звукот.",
|
||||
"TaskDownloadMissingLyrics": "Преземи стихови кои недостасуваат",
|
||||
"TaskDownloadMissingLyricsDescription": "Преземи стихови/текстови за песни",
|
||||
"TaskRefreshTrickplayImages": "Генерирај слики за прегледување (Trickplay)",
|
||||
"TaskAudioNormalization": "Нормализација на звукот",
|
||||
"TaskRefreshTrickplayImagesDescription": "Креира трикплеј прегледи за видеа во овозможените библиотеки.",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Отстранува ставки од колекциите и плејлистите што веќе не постојат.",
|
||||
"TaskExtractMediaSegments": "Скенирање на сегменти на содржина"
|
||||
"TaskExtractMediaSegments": "Скенирање на сегменти на содржина",
|
||||
"TaskMoveTrickplayImages": "Мигрирај ја локацијата на сликата од Trickplay",
|
||||
"TaskMoveTrickplayImagesDescription": "Ги преместува постоечките датотеки за трикплеј според поставките на библиотеката.",
|
||||
"CleanupUserDataTask": "Задача за чистење на кориснички податоци",
|
||||
"CleanupUserDataTaskDescription": "Ги чисти сите кориснички податоци (состојба на гледање, статус на омилени итн.) од медиуми што повеќе не се присутни најмалку 90 дена."
|
||||
}
|
||||
|
||||
@@ -124,8 +124,6 @@
|
||||
"External": "പുറമേയുള്ള",
|
||||
"TaskKeyframeExtractorDescription": "കൂടുതൽ കൃത്യമായ HLS പ്ലേലിസ്റ്റുകൾ സൃഷ്ടിക്കുന്നതിന് വീഡിയോ ഫയലുകളിൽ നിന്ന് കീഫ്രെയിമുകൾ എക്സ്ട്രാക്റ്റ് ചെയ്യുന്നു. ഈ പ്രവർത്തനം പൂർത്തിയാവാൻ കുറച്ചധികം സമയം എടുത്തേക്കാം.",
|
||||
"TaskKeyframeExtractor": "കീഫ്രെയിം എക്സ്ട്രാക്റ്റർ",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "നിലവിലില്ലാത്ത ശേഖരങ്ങളിൽ നിന്നും പ്ലേലിസ്റ്റുകളിൽ നിന്നും ഇനങ്ങൾ നീക്കംചെയ്യുന്നു.",
|
||||
"TaskCleanCollectionsAndPlaylists": "ശേഖരങ്ങളും പ്ലേലിസ്റ്റുകളും വൃത്തിയാക്കുക",
|
||||
"TaskAudioNormalization": "സാധാരണ ശബ്ദ നിലയിലെത്തിലെത്തിക്കുക",
|
||||
"TaskAudioNormalizationDescription": "സാധാരണ ശബ്ദ നിലയിലെത്തിലെത്തിക്കുന്ന ഡാറ്റയ്ക്കായി ഫയലുകൾ സ്കാൻ ചെയ്യുക.",
|
||||
"TaskRefreshTrickplayImages": "ട്രിക്ക് പ്ലേ ചിത്രങ്ങൾ സൃഷ്ടിക്കുക",
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"NotificationOptionServerRestartRequired": "Server-г дахин асаана уу",
|
||||
"NotificationOptionVideoPlaybackStopped": "Бичлэгийг зогсоов",
|
||||
"UserPasswordChangedWithName": "Хэрэглэгч {0}-н нууц үгийг өөрчиллөө",
|
||||
"TaskCleanCollectionsAndPlaylists": "Цуглуулга ба тоглуулах жагсаалтыг цэвэрлэх",
|
||||
"ScheduledTaskFailedWithName": "{0} амжилтгүй",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin Server ачааллаж байна. Хэсэг хугацааны дараа дахин оролдоно уу.",
|
||||
"TaskCleanActivityLog": "Үйл ажиллагааны бүртгэлийг цэвэрлэх",
|
||||
@@ -44,7 +43,6 @@
|
||||
"NotificationOptionAudioPlayback": "Дууг тоглууллаа",
|
||||
"TaskRefreshTrickplayImages": "Трикплэй зургуудыг үүсгэх",
|
||||
"TaskUpdatePlugins": "Plugin-уудыг шинэчлэх",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Одоо байхгүй болсон зүйлсийг цуглуулга ба тоглуулах жагсаалтаас устгана.",
|
||||
"TaskAudioNormalization": "Аудиог хэвшүүлэх",
|
||||
"TaskAudioNormalizationDescription": "Файлуудаас дууны хэвийн хэмжээсийн мэдээллийг шалгана.",
|
||||
"TaskRefreshTrickplayImagesDescription": "Идэвхжсэн сангуудад байгаа видеонуудын трикплэй урьдчилсан харагдацыг үүсгэнэ.",
|
||||
|
||||
@@ -126,7 +126,6 @@
|
||||
"HearingImpaired": "कर्णबधीर",
|
||||
"TaskRefreshTrickplayImages": "ट्रिकप्ले प्रतिमा तयार करा",
|
||||
"TaskRefreshTrickplayImagesDescription": "सक्षम लायब्ररीमधील व्हिडिओंसाठी ट्रिकप्ले पूर्वावलोकन तयार करते.",
|
||||
"TaskCleanCollectionsAndPlaylists": "संग्रह आणि प्लेलिस्ट व्यवस्थित करा",
|
||||
"TaskExtractMediaSegments": "मिडिया विभाग तपासणी",
|
||||
"TaskMoveTrickplayImages": "ट्रिकप्ले प्रतिमेचे स्थान स्थलांतर करा",
|
||||
"TaskDownloadMissingLyrics": "उपलब्ध नसलेली गीतपट्टी (Lyrics) डाउनलोड करा",
|
||||
@@ -135,7 +134,6 @@
|
||||
"TaskDownloadMissingLyricsDescription": "गाण्यांची गीतपट्टी (Lyrics) डाउनलोड करतो",
|
||||
"TaskExtractMediaSegmentsDescription": "सक्रिय असलेल्या प्लगिनमधून मीडिया विभाग प्राप्त करते.",
|
||||
"TaskMoveTrickplayImagesDescription": "लायब्ररीच्या सेटिंग्जप्रमाणे आधीपासून अस्तित्वात असलेल्या ट्रिकप्ले फाइल्सचे स्थान बदलते.",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "जे संग्रह आणि प्लेलिस्ट आता अस्तित्वात नाहीत, त्यांमधील घटक हटवते.",
|
||||
"CleanupUserDataTask": "वापरकर्ता डेटाची स्वच्छता प्रक्रिया",
|
||||
"CleanupUserDataTaskDescription": "९० दिवसांहून अधिक काळ अनुपस्थित असलेल्या माध्यमांवरील सर्व वापरकर्ता माहिती (जसे पाहण्याची स्थिती, आवडी इ.) हटवते."
|
||||
}
|
||||
|
||||
@@ -132,10 +132,8 @@
|
||||
"TaskDownloadMissingLyrics": "Muat turun lirik yang hilang",
|
||||
"TaskDownloadMissingLyricsDescription": "Memuat turun lirik-lirik untuk lagu-lagu",
|
||||
"TaskMoveTrickplayImages": "Alih Lokasi Imej Trickplay",
|
||||
"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.",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -128,8 +128,6 @@
|
||||
"TaskOptimizeDatabase": "Ottimiżża d-database",
|
||||
"TaskKeyframeExtractor": "Estrattur ta' Keyframes",
|
||||
"TaskKeyframeExtractorDescription": "Jiġbed il-keyframes mill-fajls tal-videos biex jagħmel playlists HLS aktar preċiżi. Dan it-task jista' jdum żmien twil biex ilesti.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Naddaf il-kollezzjonijiet u l-playlists",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Ineħħi oġġetti minn kollezzjonijiet u playlists li m'għadhomx jeżistu.",
|
||||
"TaskDownloadMissingLyrics": "Niżżel il-lirika nieqsa",
|
||||
"TaskDownloadMissingLyricsDescription": "Iniżżel il-lirika għal-kanzunetti",
|
||||
"TaskExtractMediaSegments": "Scan tas-Sezzjoni tal-Midja",
|
||||
|
||||
@@ -122,10 +122,8 @@
|
||||
"AppDeviceValues": "အက်ပ်- {0}၊ စက်- {1}",
|
||||
"External": "ပြင်ပ",
|
||||
"TaskKeyframeExtractorDescription": "ပိုမိုတိကျသည့် အိတ်ချ်အယ်လ်အက်စ် အစဉ်လိုက်ပြသမှုများ ဖန်တီးနိုင်ရန်အတွက် ဗီဒီယိုဖိုင်များမှ ကီးဖရိန်များကို ထုတ်နှုတ်ယူမည် ဖြစ်သည်။ ဤလုပ်ဆောင်မှုသည် အချိန်ကြာရှည်နိုင်သည်။",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "စုစည်းမှုများနှင့် အစဉ်လိုက်ပြသမှုများမှ မရှိတော့သည်များကို ဖယ်ရှားမည်။",
|
||||
"TaskRefreshTrickplayImages": "ထရစ်ခ်ပလေး ပုံများကို ထုတ်မည်",
|
||||
"TaskKeyframeExtractor": "ကီးဖရိန်များကို ထုတ်နုတ်ခြင်း",
|
||||
"TaskCleanCollectionsAndPlaylists": "စုစည်းမှုများနှင့် အစဉ်လိုက်ပြသမှုများကို ရှင်းလင်းမည်",
|
||||
"HearingImpaired": "အကြားအာရုံ ချို့တဲ့သူ",
|
||||
"TaskDownloadMissingLyrics": "ကျန်နေသောသီချင်းစာသားများအား ဒေါင်းလုတ်ဆွဲပါ",
|
||||
"TaskDownloadMissingLyricsDescription": "သီချင်းများအတွက် သီချင်းစာသား ဒေါင်းလုတ်ဆွဲပါ"
|
||||
|
||||
@@ -126,10 +126,8 @@
|
||||
"HearingImpaired": "Hørselshemmet",
|
||||
"TaskRefreshTrickplayImages": "Generer Trickplay bilder",
|
||||
"TaskRefreshTrickplayImagesDescription": "Oppretter trickplay-forhåndsvisninger for videoer i aktiverte biblioteker.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Rydd kolleksjoner og spillelister",
|
||||
"TaskAudioNormalization": "Lydnormalisering",
|
||||
"TaskAudioNormalizationDescription": "Skan filer for lydnormaliserende data.",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Fjerner elementer fra kolleksjoner og spillelister som ikke lengere finnes.",
|
||||
"TaskDownloadMissingLyrics": "Last ned manglende tekster",
|
||||
"TaskDownloadMissingLyricsDescription": "Last ned sangtekster",
|
||||
"TaskExtractMediaSegments": "Skann mediasegment",
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
"Genres": "विधाहरू",
|
||||
"Folders": "फोल्डरहरू",
|
||||
"Favorites": "मनपर्ने",
|
||||
"FailedLoginAttemptWithUserName": "{0}को लग इन प्रयास असफल",
|
||||
"FailedLoginAttemptWithUserName": "असफल लग इन प्रयास {0} देखि",
|
||||
"DeviceOnlineWithName": "{0}को साथ जडित",
|
||||
"DeviceOfflineWithName": "{0}बाट विच्छेदन भयो",
|
||||
"Collections": "संग्रह",
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"Favorites": "Favorieten",
|
||||
"Folders": "Mappen",
|
||||
"HeaderAlbumArtists": "Albumartiesten",
|
||||
"HeaderContinueWatching": "Verder kijken",
|
||||
"HeaderContinueWatching": "Verderkijken",
|
||||
"HeaderFavoriteAlbums": "Favoriete albums",
|
||||
"HeaderFavoriteArtists": "Favoriete artiesten",
|
||||
"HeaderFavoriteEpisodes": "Favoriete afleveringen",
|
||||
@@ -124,8 +124,6 @@
|
||||
"HearingImpaired": "Slechthorenden",
|
||||
"TaskRefreshTrickplayImages": "Trickplay-afbeeldingen genereren",
|
||||
"TaskRefreshTrickplayImagesDescription": "Creëert trickplay-voorvertoningen voor video's in bibliotheken waarvoor dit is ingeschakeld.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Collecties en afspeellijsten opruimen",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Verwijdert niet langer bestaande items uit collecties en afspeellijsten.",
|
||||
"TaskAudioNormalization": "Geluidsnormalisatie",
|
||||
"TaskAudioNormalizationDescription": "Scant bestanden op gegevens voor geluidsnormalisatie.",
|
||||
"TaskDownloadMissingLyrics": "Ontbrekende liedteksten downloaden",
|
||||
@@ -137,5 +135,6 @@
|
||||
"CleanupUserDataTaskDescription": "Wist alle gebruikersgegevens (kijkstatus, favorieten, etc.) van media die al minstens 90 dagen niet meer aanwezig zijn.",
|
||||
"CleanupUserDataTask": "Opruimtaak gebruikersdata",
|
||||
"Albums": "Albums",
|
||||
"Genres": "Genres"
|
||||
"Genres": "Genres",
|
||||
"Original": "Oorspronkelijk"
|
||||
}
|
||||
|
||||
@@ -131,8 +131,6 @@
|
||||
"TaskDownloadMissingLyrics": "ਅਧੂਰੇ ਬੋਲ ਡਾਊਨਲੋਡ ਕਰੋ",
|
||||
"TaskDownloadMissingLyricsDescription": "ਗੀਤਾਂ ਲਈ ਡਾਊਨਲੋਡ ਕਿਤੇ ਬੋਲ",
|
||||
"TaskKeyframeExtractor": "ਕੀ-ਫ੍ਰੇਮ ਐਕਸਟ੍ਰੈਕਟਰ",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "ਕਲੈਕਸ਼ਨਾਂ ਅਤੇ ਪਲੇਲਿਸਟਾਂ ਵਿੱਚੋਂ ਉਹ ਆਈਟਮ ਹਟਾਉਂਦਾ ਹੈ ਜੋ ਹੁਣ ਮੌਜੂਦ ਨਹੀਂ ਹਨ।",
|
||||
"TaskCleanCollectionsAndPlaylists": "ਕਲੈਕਸ਼ਨਾਂ ਅਤੇ ਪਲੇਲਿਸਟਾਂ ਨੂੰ ਸਾਫ ਕਰੋ",
|
||||
"TaskAudioNormalization": "ਆਵਾਜ਼ ਸਧਾਰਣੀਕਰਨ",
|
||||
"TaskRefreshTrickplayImagesDescription": "ਵੀਡੀਓ ਲਈ ਟ੍ਰਿਕਪਲੇ ਪ੍ਰੀਵਿਊ ਬਣਾਉਂਦਾ ਹੈ (ਜੇ ਲਾਇਬ੍ਰੇਰੀ ਵਿੱਚ ਚੁਣਿਆ ਗਿਆ ਹੈ)।",
|
||||
"TaskKeyframeExtractorDescription": "ਕੀ-ਫ੍ਰੇਮਜ਼ ਨੂੰ ਵੀਡੀਓ ਫਾਈਲਾਂ ਵਿੱਚੋਂ ਨਿਕਾਲਦਾ ਹੈ ਤਾਂ ਜੋ ਹੋਰ ਜ਼ਿਆਦਾ ਸਟਿਕ ਹੋਣ ਵਾਲੀਆਂ HLS ਪਲੇਲਿਸਟਾਂ ਬਣਾਈਆਂ ਜਾ ਸਕਣ। ਇਹ ਕੰਮ ਲੰਬੇ ਸਮੇਂ ਤੱਕ ਚੱਲ ਸਕਦਾ ਹੈ।",
|
||||
|
||||
@@ -126,8 +126,6 @@
|
||||
"HearingImpaired": "Niedosłyszący",
|
||||
"TaskRefreshTrickplayImages": "Generuj obrazy Trickplay",
|
||||
"TaskRefreshTrickplayImagesDescription": "Tworzy podglądy Trickplay dla filmów we włączonych bibliotekach.",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Usuwa elementy z kolekcji i list odtwarzania, które już nie istnieją.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Oczyść kolekcje i listy odtwarzania",
|
||||
"TaskAudioNormalization": "Normalizacja dźwięku",
|
||||
"TaskAudioNormalizationDescription": "Skanuje pliki w poszukiwaniu danych normalizacji dźwięku.",
|
||||
"TaskDownloadMissingLyrics": "Pobierz brakujące słowa",
|
||||
@@ -137,5 +135,6 @@
|
||||
"TaskExtractMediaSegmentsDescription": "Wyodrębnia lub pobiera segmenty mediów z wtyczek obsługujących MediaSegment.",
|
||||
"TaskMoveTrickplayImagesDescription": "Przenosi istniejące pliki Trickplay zgodnie z ustawieniami biblioteki.",
|
||||
"CleanupUserDataTaskDescription": "Usuwa wszystkie dane użytkownika (stan oglądanych, status ulubionych itp.) z mediów, które nie są dostępne od co najmniej 90 dni.",
|
||||
"CleanupUserDataTask": "Zadanie czyszczenia danych użytkownika"
|
||||
"CleanupUserDataTask": "Zadanie czyszczenia danych użytkownika",
|
||||
"Original": "Oryginalny"
|
||||
}
|
||||
|
||||
@@ -126,8 +126,6 @@
|
||||
"HearingImpaired": "Deficiência Auditiva",
|
||||
"TaskRefreshTrickplayImages": "Gerar imagens Trickplay",
|
||||
"TaskRefreshTrickplayImagesDescription": "Cria prévias Trickplay para vídeos em bibliotecas em que o recurso está habilitado.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Limpe coleções e playlists",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e playlists que não existem mais.",
|
||||
"TaskAudioNormalization": "Normalização de áudio",
|
||||
"TaskAudioNormalizationDescription": "Examina os ficheiros em busca de dados de normalização de áudio.",
|
||||
"TaskDownloadMissingLyricsDescription": "Baixar letras para músicas",
|
||||
|
||||
@@ -126,8 +126,6 @@
|
||||
"HearingImpaired": "Surdo",
|
||||
"TaskRefreshTrickplayImages": "Gerar imagens de trickplay",
|
||||
"TaskRefreshTrickplayImagesDescription": "Cria pré-visualizações de trickplay para vídeos nas bibliotecas ativadas.",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e listas de reprodução que já não existem.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução",
|
||||
"TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.",
|
||||
"TaskAudioNormalization": "Normalização de áudio",
|
||||
"TaskExtractMediaSegments": "Analisar segmentos de multimédia",
|
||||
@@ -137,5 +135,6 @@
|
||||
"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.",
|
||||
"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"
|
||||
"CleanupUserDataTask": "Limpeza de dados de utilizador",
|
||||
"Original": "Original"
|
||||
}
|
||||
|
||||
@@ -126,8 +126,6 @@
|
||||
"TaskKeyframeExtractorDescription": "Retira frames chave do video para criar listas HLS precisas. Esta tarefa pode correr durante algum tempo.",
|
||||
"TaskRefreshTrickplayImages": "Gerar imagens de trickplay",
|
||||
"TaskRefreshTrickplayImagesDescription": "Cria pré-visualizações de trickplay para vídeos nas bibliotecas ativadas.",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e listas de reprodução que já não existem.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução",
|
||||
"TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.",
|
||||
"TaskAudioNormalization": "Normalização de áudio",
|
||||
"TaskDownloadMissingLyrics": "Transferir letra em falta",
|
||||
@@ -137,5 +135,6 @@
|
||||
"TaskExtractMediaSegmentsDescription": "Extrai ou obtém segmentos de multimédia a partir de plugins com suporte para MediaSegment.",
|
||||
"TaskMoveTrickplayImages": "Migrar a localização da imagem do Trickplay",
|
||||
"CleanupUserDataTask": "Task de limpeza de dados do usuário",
|
||||
"CleanupUserDataTaskDescription": "Remove todos os dados do usuário (progresso, favoritos etc) de mídias que não estão presentes há pelo menos 90 dias."
|
||||
"CleanupUserDataTaskDescription": "Remove todos os dados do usuário (progresso, favoritos etc) de mídias que não estão presentes há pelo menos 90 dias.",
|
||||
"Original": "Original"
|
||||
}
|
||||
|
||||
@@ -128,8 +128,6 @@
|
||||
"TaskRefreshTrickplayImagesDescription": "Generează previzualizările trickplay pentru videourile din librăriile selectate.",
|
||||
"TaskAudioNormalizationDescription": "Scanează fișiere pentru date necesare normalizării sunetului.",
|
||||
"TaskAudioNormalization": "Normalizare sunet",
|
||||
"TaskCleanCollectionsAndPlaylists": "Curăță colecțiile și listele de redare",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Elimină elementele care nu mai există din colecții și liste de redare.",
|
||||
"TaskExtractMediaSegments": "Scanează segmentele media",
|
||||
"TaskMoveTrickplayImagesDescription": "Mută fișierele trickplay existente conform setărilor librăriei.",
|
||||
"TaskExtractMediaSegmentsDescription": "Extrage sau obține segmentele media de la pluginurile MediaSegment activate.",
|
||||
|
||||
@@ -126,8 +126,6 @@
|
||||
"HearingImpaired": "Для слабослышащих",
|
||||
"TaskRefreshTrickplayImages": "Сгенерировать изображения для Trickplay",
|
||||
"TaskRefreshTrickplayImagesDescription": "Создает предпросмотры для Trickplay для видео в библиотеках, где эта функция включена.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Очистка коллекций и списков воспроизведения",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Удаляет элементы из коллекций и списков воспроизведения, которые больше не существуют.",
|
||||
"TaskAudioNormalization": "Нормализация звука",
|
||||
"TaskAudioNormalizationDescription": "Сканирует файлы на наличие данных о нормализации звука.",
|
||||
"TaskDownloadMissingLyrics": "Загрузить недостающий текст",
|
||||
|
||||
@@ -126,8 +126,6 @@
|
||||
"HearingImpaired": "Sluchovo postihnutí",
|
||||
"TaskRefreshTrickplayImages": "Generovanie obrázkov Trickplay",
|
||||
"TaskRefreshTrickplayImagesDescription": "Vytvára trickplay náhľady pre videá v povolených knižniciach.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Vyčistiť kolekcie a playlisty",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Odstráni položky z kolekcií a playlistov, ktoré už neexistujú.",
|
||||
"TaskAudioNormalization": "Normalizácia zvuku",
|
||||
"TaskAudioNormalizationDescription": "Skenovať súbory za účelom normalizácie zvuku.",
|
||||
"TaskExtractMediaSegments": "Skenovanie segmentov médií",
|
||||
|
||||
@@ -132,10 +132,8 @@
|
||||
"TaskMoveTrickplayImages": "Preseli lokacijo Trickplay slik",
|
||||
"TaskDownloadMissingLyrics": "Prenesi manjkajoča besedila pesmi",
|
||||
"TaskDownloadMissingLyricsDescription": "Prenesi besedila za pesmi",
|
||||
"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č.",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -132,8 +132,6 @@
|
||||
"TaskMoveTrickplayImagesDescription": "Zhvendos skedarët ekzistues të trickplay sipas cilësimeve të bibliotekës.",
|
||||
"TaskDownloadMissingLyrics": "Shkarko tekstet e këngëve që mungojnë",
|
||||
"TaskDownloadMissingLyricsDescription": "Shkarkon tekstet e këngëve",
|
||||
"TaskCleanCollectionsAndPlaylists": "Pastron koleksionet dhe listat e këngëve",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Heq elementet nga koleksionet dhe listat e këngëve që nuk ekzistojnë më.",
|
||||
"TaskAudioNormalization": "Normalizimi i audios",
|
||||
"TaskAudioNormalizationDescription": "Skannon skedarët për të dhëna të normalizimit të audios.",
|
||||
"CleanupUserDataTaskDescription": "Pastron të gjitha të dhënat e përdorueseve (gjendja e shikimit, statusi i të preferuarave etj.) nga mediat që nuk janë më të pranishme për të paktën 90 ditë.",
|
||||
|
||||
@@ -125,12 +125,10 @@
|
||||
"TaskKeyframeExtractor": "Екстрактор кључних сличица",
|
||||
"HearingImpaired": "ослабљен слух",
|
||||
"TaskAudioNormalization": "Нормализација звука",
|
||||
"TaskCleanCollectionsAndPlaylists": "Очистите колекције и плејлисте",
|
||||
"TaskAudioNormalizationDescription": "Скенира датотеке за податке о нормализацији звука.",
|
||||
"TaskRefreshTrickplayImages": "Направи сличице за визуелно премотавање",
|
||||
"TaskRefreshTrickplayImagesDescription": "Прављење сличица које помажу код визуелног премотавања видео-снимака.",
|
||||
"TaskDownloadMissingLyrics": "Преузми стихове који недостају",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Уклања ставке које више не постоје из колекција и плејлиста.",
|
||||
"TaskExtractMediaSegments": "Скенирај сегменте медија",
|
||||
"TaskExtractMediaSegmentsDescription": "Извлачи или добавља сегменте медија у додацима који раде са MediaSegment-ом.",
|
||||
"TaskMoveTrickplayImagesDescription": "Премешта постојеће сличице за визуелно премотавање сходно подешавањима библиотеке.",
|
||||
|
||||
@@ -126,9 +126,7 @@
|
||||
"HearingImpaired": "Hörselskadad",
|
||||
"TaskRefreshTrickplayImages": "Generera Trickplay-bilder",
|
||||
"TaskRefreshTrickplayImagesDescription": "Skapar trickplay-förhandsvisningar för videor i aktiverade bibliotek.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Rensa upp samlingar och spellistor",
|
||||
"TaskAudioNormalization": "Ljudnormalisering",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Tar bort objekt från samlingar och spellistor som inte längre finns.",
|
||||
"TaskAudioNormalizationDescription": "Skannar filer för ljudnormaliseringsdata.",
|
||||
"TaskDownloadMissingLyrics": "Ladda ner saknad låttext",
|
||||
"TaskDownloadMissingLyricsDescription": "Laddar ner låttexter",
|
||||
@@ -137,5 +135,6 @@
|
||||
"TaskMoveTrickplayImages": "Migrera platsen för Trickplay-bilder",
|
||||
"TaskMoveTrickplayImagesDescription": "Flyttar befintliga trickplay-filer enligt bibliotekets inställningar.",
|
||||
"CleanupUserDataTaskDescription": "Tar bort all användardata (såsom vad du sett, favoriter med mera) för media som inte funnits på enheten på minst 90 dagar.",
|
||||
"CleanupUserDataTask": "Uppgift för rensning av användardata"
|
||||
"CleanupUserDataTask": "Uppgift för rensning av användardata",
|
||||
"Original": "Original"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user