Compare commits

...

91 Commits

Author SHA1 Message Date
Joshua M. Boniface 5cae44fdf7 Merge pull request #12718 from IceStormNG/extract-mks-subtitles
Extract subtitles of MKS aux files if they're extractable.
2025-08-03 17:31:13 -04:00
Joshua M. Boniface c3cb5fd2f9 Merge pull request #14021 from sususu98/enhancement/strmSeek
Enhanced HTTP Range request support for. strm file
2025-08-03 17:30:45 -04:00
Joshua M. Boniface 1262ac31dc Merge pull request #14410 from dyphire/language
Further refinement of BCP 47 language labeling support
2025-08-03 17:29:40 -04:00
Joshua M. Boniface 0f5bb5cf76 Merge pull request #14540 from TokerX/issue-8641
Improve extra rule resolution and file handling
2025-08-03 17:29:04 -04:00
Joshua M. Boniface ce78af2ed4 Merge pull request #13604 from Jxiced/master
Prevent whitespaces in username during wizard setup
2025-08-03 17:27:50 -04:00
Joshua M. Boniface 4b6fb6c4bb Merge branch 'master' into master 2025-08-03 17:27:17 -04:00
Niels van Velzen db7465e83d Merge pull request #14567 from Shadowghost/skip-image-failure
Don't fail image saving on missing BaseItem
2025-08-03 18:47:50 +02:00
Shadowghost 803e87ca5f Don't fail image saving on missing BaseItem 2025-08-02 22:10:39 +02:00
Bond-009 9e36fa4263 Merge pull request #14553 from nyanmisaka/fix-fmp4-ignore-audio-delay
Add extra movflags to fMP4 to take initial audio delay into account
2025-08-01 10:31:00 +02:00
Bond-009 a52a230778 Merge pull request #14467 from jkhsjdhjs/keyframe-only-trickplay-extraction-fallback
Add fallback for keyframe-only trickplay extraction
2025-08-01 10:30:24 +02:00
Niels van Velzen b00e381109 Merge pull request #14554 from JPVenson/feature/FixIsFolderMigration
Also migrate IsFolder
2025-07-31 16:07:48 +02:00
Hestadgard b8fb8bd608 Translated using Weblate (Norwegian Nynorsk)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/nn/
2025-07-31 07:52:58 +00:00
nomener 34c9adef80 Translated using Weblate (German)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/de/
2025-07-31 07:25:36 +00:00
JPVenson c8d2f43660 Add logging 2025-07-30 20:14:24 +00:00
JPVenson ef733c5ace use guid instead 2025-07-30 20:10:26 +00:00
JPVenson a1eb04dc0b Add full migration for IsFolder flag 2025-07-30 19:58:56 +00:00
JPVenson 711e649e35 Also migrate IsFolder 2025-07-30 19:41:34 +00:00
Hestadgard 1d408a1503 Translated using Weblate (Norwegian Nynorsk)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/nn/
2025-07-30 17:29:30 +00:00
Hestadgard 6391dd9570 Translated using Weblate (Norwegian Bokmål)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/nb_NO/
2025-07-30 17:29:29 +00:00
dyphire 2007815fa6 Further refinement of BCP 47 language labeling support 2025-07-30 22:15:37 +08:00
nyanmisaka a5b4eca804 Add extra movflags to fMP4 to take initial audio delay into account
Signed-off-by: nyanmisaka <nst799610810@gmail.com>
2025-07-30 18:00:14 +08:00
Bond-009 76d498ac9d Merge pull request #14530 from jellyfin/renovate/ci-deps
Update CI dependencies
2025-07-30 10:55:40 +02:00
dikson804 90b4345cfd Translated using Weblate (Chinese (Traditional Han script, Hong Kong))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant_HK/
2025-07-30 05:11:20 +00:00
renovate[bot] 317192c23d Update CI dependencies 2025-07-30 03:59:51 +00:00
Ghouri dcb12a73fb Translated using Weblate (Norwegian Nynorsk)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/nn/
2025-07-29 08:13:35 +00:00
Hossain Rizbi b15abddfd7 Translated using Weblate (Bengali)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/bn/
2025-07-28 21:19:19 +00:00
Hossain Rizbi cfde5af3b0 Translated using Weblate (Bengali)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/bn/
2025-07-28 20:57:07 +00:00
Troj@ 26a6cfaf65 Translated using Weblate (Belarusian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/be/
2025-07-28 16:45:43 +00:00
theguymadmax 8a8018f0de Preserve interpunct (·) (#14543) 2025-07-27 20:09:50 -06:00
evan314159 6f49782b7b fix file modification date comparisons (#14503) 2025-07-27 20:08:06 -06:00
theguymadmax 536437bbe3 Fix allow and block queries (#14482) 2025-07-27 19:28:04 -06:00
Shane Powell ba54cda774 Add progress reporting to AudioNormalizationTask. (#14306) 2025-07-27 19:27:24 -06:00
Piyush Rungta e86315128d Ignore directory if empty .ignore file is present (#14536) 2025-07-27 19:22:12 -06:00
Bas dfab2fb6e2 Translated using Weblate (Dutch)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/nl/
2025-07-27 18:34:20 +00:00
Sven Cazier 7785b51f57 Enhance extra rules for video and audio file naming; update tests for new naming conventions 2025-07-26 23:24:58 +02:00
Tim Eisele a068f75623 Set DateLastSaved after running metadata savers (#14531) 2025-07-25 08:20:19 -06:00
Gargotaire 1ed191c5b3 Translated using Weblate (Catalan)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ca/
2025-07-24 20:32:31 +00:00
Gargotaire 0e3fbb6abd Translated using Weblate (Catalan)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ca/
2025-07-24 17:27:34 +00:00
Gargotaire 583a861b32 Translated using Weblate (Catalan)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ca/
2025-07-24 02:15:06 +00:00
Zhelyan Radoev 3bcfe13652 Translated using Weblate (Bulgarian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/bg/
2025-07-24 02:15:06 +00:00
Bond-009 f5a135a1db Merge pull request #14521 from jellyfin/renovate/ci-deps
Update github/codeql-action action to v3.29.4
2025-07-23 18:11:07 +02:00
renovate[bot] 0cea853b45 Update github/codeql-action action to v3.29.4 2025-07-23 15:32:24 +00:00
jkhsjdhjs 663087b155 Fix trickplay extraction ffmpeg error-handling (#14493) 2025-07-22 17:13:38 -06:00
Dan Watson dddeea1f7b Fix issue with EncodedRecorder streams not showing up under "Active Recordings" (#14357) 2025-07-22 17:13:01 -06:00
renovate[bot] a148a4ad02 Update dependency Svg.Skia to 3.0.4 (#14515) 2025-07-22 17:10:46 -06:00
JPVenson 57d077d08e Supress transaction (#14514) 2025-07-22 17:09:31 -06:00
Mylan1173 774be151aa Translated using Weblate (Hungarian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hu/
2025-07-22 12:19:37 +00:00
Bond-009 7569ac65a8 Merge pull request #14508 from jellyfin/renovate/ci-deps
Update github/codeql-action action to v3.29.3
2025-07-22 10:33:32 +02:00
zag 4621a99c7c Translated using Weblate (Malay)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ms/
2025-07-22 08:21:51 +00:00
renovate[bot] 1e796e0b7a Update github/codeql-action action to v3.29.3 2025-07-21 21:06:53 +00:00
Jacob Rasmussen 4da5483ef4 Translated using Weblate (Danish)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/da/
2025-07-21 08:07:29 +00:00
theguymadmax eea0872980 Fix Serbian ISO 639-2 codes (#14476) 2025-07-20 20:01:31 -06:00
Cody Robibero 36c90ce2ce Clean up and fix backup/restore (#14489) 2025-07-20 20:01:13 -06:00
Niels van Velzen 48e93dcbce Use RequestHelpers.GetSession in SessionWebSocketListener (#14494) 2025-07-20 20:00:47 -06:00
Gargotaire 6cee66119e Translated using Weblate (Catalan)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ca/
2025-07-19 22:22:01 +00:00
JPVenson c62a07405e improve userdata migration (#14488) 2025-07-19 11:34:51 -06:00
renovate[bot] 7bd08ab290 Update dependency NEbml to v1 (#14490) 2025-07-19 11:34:33 -06:00
renovate[bot] 088ef0d37a Update dependency z440.atl.core to 7.2.0 (#14496) 2025-07-19 11:34:11 -06:00
SaddFox ba0f61ef2d Translated using Weblate (Slovenian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sl/
2025-07-18 21:18:23 +00:00
SaddFox c70f6bffcf Translated using Weblate (Slovenian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sl/
2025-07-18 21:18:09 +00:00
JPVenson 21a6d6f0d6 Delete old migrations on restore (#14486) 2025-07-17 17:19:41 -06:00
queukat aa77dfb92d Drawing: make SkiaEncoder more robust when reading image dimensions (#14481) 2025-07-17 17:19:33 -06:00
Stephan Sundermann 2ad37fe021 Ensure UserData stays unique on delete (#14475) 2025-07-17 17:19:26 -06:00
renovate[bot] fd5205a6eb Update danielpalme/ReportGenerator-GitHub-Action action to v5.4.9 (#14485)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-17 09:20:39 +02:00
EinarGisla 60cfa65cdc Translated using Weblate (Icelandic)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/is/
2025-07-16 22:41:15 +00:00
madman38 e5139e1004 Translated using Weblate (Turkish)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/tr/
2025-07-16 14:41:36 +00:00
renovate[bot] aa1abf8b94 Update dependency Diacritics to 4.0.17 (#14477) 2025-07-15 06:58:00 -06:00
sharanchius 742b5637fa Translated using Weblate (Lithuanian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lt/
2025-07-15 01:50:30 +00:00
Nyanmisaka 25a362345d Fix refreshing the library cannot delete old attachments (#14461) 2025-07-14 18:40:37 -06:00
JPVenson 310a54f090 Allow loading of Database options for DB provider (#14466) 2025-07-14 18:39:43 -06:00
renovate[bot] e9d92bdcb0 Update dependency z440.atl.core to 7.1.0 (#14469)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-14 18:37:22 -06:00
FJOX.win dc39a51475 vob file support (#14471) 2025-07-14 18:36:54 -06:00
JPVenson c51f3a3342 Use IDatabaseCreator instead of relying on History repository (#14465) 2025-07-14 18:36:36 -06:00
sharanchius 7ece959f4e Translated using Weblate (Lithuanian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lt/
2025-07-14 15:23:01 +00:00
sharanchius c96e828002 Translated using Weblate (Lithuanian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lt/
2025-07-14 13:43:39 +00:00
felix920506 ab56ceaa16 Translated using Weblate (Chinese (Traditional Han script))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant/
2025-07-14 09:38:42 +00:00
sharanchius 4645633acf Translated using Weblate (Lithuanian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lt/
2025-07-14 09:38:42 +00:00
jkhsjdhjs d6f93759ea Add myself to CONTRIBUTORS.md
I have contributed previously, but forgot to add myself last time.
2025-07-14 00:09:46 +02:00
jkhsjdhjs bf3f37e3d0 Add fallback for keyframe-only trickplay extraction
Keyframe-only trickplay image extraction can fail for some media
files. The current behavior is to skip the media file and try again
on the next run, which will fail again.

This adds a fallback to regular non-keyframe-only extraction for
failed runs, so the extraction can complete.
2025-07-14 00:09:43 +02:00
dyphire f9c4c9b345 Add additional chinese languages 2025-07-01 16:47:13 +08:00
Carsten Braun 98daf4aedb Use string.IsNullOrEmpty instead of regular null check. 2025-06-07 21:51:08 +02:00
Carsten Braun fcf56b73cb When subtitle is embedded in the main video file, the path will be null. 2025-06-07 21:51:08 +02:00
Carsten Braun e8239a7ee2 Do not attempt to extract internal subtitles if there are only MKS subtitles. 2025-06-07 21:51:08 +02:00
Carsten Braun 84cebeae64 Skip early if subtitle is in MKS to avoid unnecessary function calls. 2025-06-07 21:51:08 +02:00
Carsten Braun c0e2875818 If subtitles are part of an MKS, it is not an error. Just log for debug purpose and continue. 2025-06-07 21:51:08 +02:00
Carsten Braun 411ba03bf0 Fixed formatting 2025-06-07 21:51:08 +02:00
Carsten Braun b2e19c0306 Also extract subtitles of MKS aux files if they're extractable. 2025-06-07 21:51:08 +02:00
sususu98 a7891b3f2d Enhanced HTTP Range request support for. strm file
Forward the Range, Accept-Ranges, and Content-
Range headers, improve User-Agent handling,
and adjust the default Content-Type.
2025-04-29 17:08:50 +08:00
Jxiced e7bc86ebb8 Move throw into interface to use in wizard, check for null and invalid username. 2025-02-23 22:16:35 +00:00
Jxiced 7aa96dfc20 Update contributors. 2025-02-22 22:34:41 +00:00
Jxiced 70d07b830d Prevent whitespaces in username during wizard setup. 2025-02-21 21:19:20 +00:00
75 changed files with 2744 additions and 417 deletions
+3 -3
View File
@@ -27,11 +27,11 @@ jobs:
dotnet-version: '9.0.x'
- name: Initialize CodeQL
uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
uses: github/codeql-action/init@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
uses: github/codeql-action/autobuild@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
uses: github/codeql-action/analyze@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
--verbosity minimal
- name: Merge code coverage results
uses: danielpalme/ReportGenerator-GitHub-Action@4c0f60daf67483745c34efdeadd4c4e78a19991e # v5.4.8
uses: danielpalme/ReportGenerator-GitHub-Action@c1dd332d00304c5aa5d506aab698a5224a8fa24e # 5.4.11
with:
reports: "**/coverage.cobertura.xml"
targetdir: "merged/"
+4
View File
@@ -61,6 +61,7 @@
- [ikomhoog](https://github.com/ikomhoog)
- [iwalton3](https://github.com/iwalton3)
- [jftuga](https://github.com/jftuga)
- [jkhsjdhjs](https://github.com/jkhsjdhjs)
- [jmshrv](https://github.com/jmshrv)
- [joern-h](https://github.com/joern-h)
- [joshuaboniface](https://github.com/joshuaboniface)
@@ -196,9 +197,12 @@
- [Kenneth Cochran](https://github.com/kennethcochran)
- [benedikt257](https://github.com/benedikt257)
- [revam](https://github.com/revam)
- [Jxiced](https://github.com/Jxiced)
- [allesmi](https://github.com/allesmi)
- [ThunderClapLP](https://github.com/ThunderClapLP)
- [Shoham Peller](https://github.com/spellr)
- [theshoeshiner](https://github.com/theshoeshiner)
- [TokerX](https://github.com/TokerX)
# Emby Contributors
+4 -4
View File
@@ -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="6.0.4" />
<PackageVersion Include="Diacritics" Version="4.0.14" />
<PackageVersion Include="Diacritics" Version="4.0.17" />
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
<PackageVersion Include="FsCheck.Xunit" Version="3.3.0" />
@@ -53,7 +53,7 @@
<PackageVersion Include="MimeTypes" Version="2.5.2" />
<PackageVersion Include="Morestachio" Version="5.0.1.631" />
<PackageVersion Include="Moq" Version="4.18.4" />
<PackageVersion Include="NEbml" Version="0.12.0" />
<PackageVersion Include="NEbml" Version="1.0.0.3" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="PlaylistsNET" Version="1.4.1" />
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
@@ -76,7 +76,7 @@
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.116.1" />
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<PackageVersion Include="Svg.Skia" Version="3.0.3" />
<PackageVersion Include="Svg.Skia" Version="3.0.4" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageVersion Include="System.Globalization" Version="4.3.0" />
@@ -85,7 +85,7 @@
<PackageVersion Include="System.Text.Json" Version="9.0.7" />
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.7" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
<PackageVersion Include="z440.atl.core" Version="7.0.0" />
<PackageVersion Include="z440.atl.core" Version="7.2.0" />
<PackageVersion Include="TMDbLib" Version="2.2.0" />
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
+14 -14
View File
@@ -572,6 +572,18 @@ namespace Emby.Naming.Common
"trailer",
MediaType.Video),
new ExtraRule(
ExtraType.Sample,
ExtraRuleType.Filename,
"sample",
MediaType.Video),
new ExtraRule(
ExtraType.ThemeSong,
ExtraRuleType.Filename,
"theme",
MediaType.Audio),
new ExtraRule(
ExtraType.Trailer,
ExtraRuleType.Suffix,
@@ -593,13 +605,7 @@ namespace Emby.Naming.Common
new ExtraRule(
ExtraType.Trailer,
ExtraRuleType.Suffix,
" trailer",
MediaType.Video),
new ExtraRule(
ExtraType.Sample,
ExtraRuleType.Filename,
"sample",
"- trailer",
MediaType.Video),
new ExtraRule(
@@ -623,15 +629,9 @@ namespace Emby.Naming.Common
new ExtraRule(
ExtraType.Sample,
ExtraRuleType.Suffix,
" sample",
"- sample",
MediaType.Video),
new ExtraRule(
ExtraType.ThemeSong,
ExtraRuleType.Filename,
"theme",
MediaType.Audio),
new ExtraRule(
ExtraType.Scene,
ExtraRuleType.Suffix,
@@ -97,14 +97,18 @@ namespace Emby.Naming.ExternalFiles
if (culture is not null && pathInfo.Language is null)
{
pathInfo.Language = culture.ThreeLetterISOLanguageName;
pathInfo.Language = culture.Name.Contains('-', StringComparison.OrdinalIgnoreCase)
? culture.Name
: culture.ThreeLetterISOLanguageName;
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
}
else if (culture is not null && pathInfo.Language == "hin")
{
// Hindi language code "hi" collides with a hearing impaired flag - use as Hindi only if no other language is set
pathInfo.IsHearingImpaired = true;
pathInfo.Language = culture.ThreeLetterISOLanguageName;
pathInfo.Language = culture.Name.Contains('-', StringComparison.OrdinalIgnoreCase)
? culture.Name
: culture.ThreeLetterISOLanguageName;
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
}
else if (_namingOptions.MediaHearingImpairedFlags.Any(s => currentSliceWithoutSeparator.Equals(s, StringComparison.OrdinalIgnoreCase)))
+28 -50
View File
@@ -22,67 +22,45 @@ namespace Emby.Naming.Video
/// <returns>Returns <see cref="ExtraResult"/> object.</returns>
public static ExtraResult GetExtraInfo(string path, NamingOptions namingOptions, string? libraryRoot = "")
{
var result = new ExtraResult();
ExtraResult result = new ExtraResult();
for (var i = 0; i < namingOptions.VideoExtraRules.Length; i++)
bool isAudioFile = AudioFileParser.IsAudioFile(path, namingOptions);
bool isVideoFile = VideoResolver.IsVideoFile(path, namingOptions);
ReadOnlySpan<char> pathSpan = path.AsSpan();
ReadOnlySpan<char> fileName = Path.GetFileName(pathSpan);
ReadOnlySpan<char> fileNameWithoutExtension = Path.GetFileNameWithoutExtension(pathSpan);
// Trim the digits from the end of the filename so we can recognize things like -trailer2
ReadOnlySpan<char> trimmedFileNameWithoutExtension = fileNameWithoutExtension.TrimEnd(_digits);
ReadOnlySpan<char> directoryName = Path.GetFileName(Path.GetDirectoryName(pathSpan));
string fullDirectory = Path.GetDirectoryName(pathSpan).ToString();
foreach (ExtraRule rule in namingOptions.VideoExtraRules)
{
var rule = namingOptions.VideoExtraRules[i];
if ((rule.MediaType == MediaType.Audio && !AudioFileParser.IsAudioFile(path, namingOptions))
|| (rule.MediaType == MediaType.Video && !VideoResolver.IsVideoFile(path, namingOptions)))
if ((rule.MediaType == MediaType.Audio && !isAudioFile)
|| (rule.MediaType == MediaType.Video && !isVideoFile))
{
continue;
}
var pathSpan = path.AsSpan();
if (rule.RuleType == ExtraRuleType.Filename)
bool isMatch = rule.RuleType switch
{
var filename = Path.GetFileNameWithoutExtension(pathSpan);
ExtraRuleType.Filename => fileNameWithoutExtension.Equals(rule.Token, StringComparison.OrdinalIgnoreCase),
ExtraRuleType.Suffix => trimmedFileNameWithoutExtension.EndsWith(rule.Token, StringComparison.OrdinalIgnoreCase),
ExtraRuleType.Regex => Regex.IsMatch(fileName, rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled),
ExtraRuleType.DirectoryName => directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase)
&& !string.Equals(fullDirectory, libraryRoot, StringComparison.OrdinalIgnoreCase),
_ => false,
};
if (filename.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
}
}
else if (rule.RuleType == ExtraRuleType.Suffix)
if (!isMatch)
{
// Trim the digits from the end of the filename so we can recognize things like -trailer2
var filename = Path.GetFileNameWithoutExtension(pathSpan).TrimEnd(_digits);
if (filename.EndsWith(rule.Token, StringComparison.OrdinalIgnoreCase))
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
}
}
else if (rule.RuleType == ExtraRuleType.Regex)
{
var filename = Path.GetFileName(path.AsSpan());
var isMatch = Regex.IsMatch(filename, rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled);
if (isMatch)
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
}
}
else if (rule.RuleType == ExtraRuleType.DirectoryName)
{
var directoryName = Path.GetFileName(Path.GetDirectoryName(pathSpan));
string fullDirectory = Path.GetDirectoryName(pathSpan).ToString();
if (directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase)
&& !string.Equals(fullDirectory, libraryRoot, StringComparison.OrdinalIgnoreCase))
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
}
continue;
}
if (result.ExtraType is not null)
{
return result;
}
result.ExtraType = rule.ExtraType;
result.Rule = rule;
return result;
}
return result;
+1 -1
View File
@@ -49,7 +49,7 @@ public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IH
if (item.IsFileProtocol)
{
var file = directoryService.GetFile(item.Path);
return file is not null && file.LastWriteTimeUtc != item.DateModified;
return file is not null && item.HasChanged(file.LastWriteTimeUtc);
}
return false;
@@ -38,7 +38,8 @@ namespace Emby.Server.Implementations.Library
}
// Don't ignore top level folders
if (fileInfo.IsDirectory && parent is AggregateFolder)
if (fileInfo.IsDirectory
&& (parent is AggregateFolder || (parent?.IsTopParent ?? false)))
{
return false;
}
@@ -48,35 +49,21 @@ namespace Emby.Server.Implementations.Library
return true;
}
var filename = fileInfo.Name;
if (parent is null)
{
return false;
}
if (fileInfo.IsDirectory)
{
if (parent is not null)
{
// Ignore extras for unsupported types
if (_namingOptions.AllExtrasTypesFolderNames.ContainsKey(filename)
&& parent is not AggregateFolder
&& parent is not UserRootFolder)
{
return true;
}
}
}
else
{
if (parent is not null)
{
// Don't resolve theme songs
if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFileName, StringComparison.Ordinal)
&& AudioFileParser.IsAudioFile(filename, _namingOptions))
{
return true;
}
}
// Ignore extras for unsupported types
return _namingOptions.AllExtrasTypesFolderNames.ContainsKey(fileInfo.Name)
&& parent is not UserRootFolder;
}
return false;
// Don't resolve theme songs
return Path.GetFileNameWithoutExtension(fileInfo.Name.AsSpan()).Equals(BaseItem.ThemeSongFileName, StringComparison.Ordinal)
&& AudioFileParser.IsAudioFile(fileInfo.Name, _namingOptions);
}
}
}
@@ -42,6 +42,19 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
/// <returns>True if the file should be ignored.</returns>
public static bool IsIgnored(FileSystemMetadata fileInfo, BaseItem? parent)
{
if (fileInfo.IsDirectory)
{
var dirIgnoreFile = FindIgnoreFile(new DirectoryInfo(fileInfo.FullName));
if (dirIgnoreFile is null)
{
return false;
}
// ignore the directory only if the .ignore file is empty
// evaluate individual files otherwise
return string.IsNullOrWhiteSpace(GetFileContent(dirIgnoreFile));
}
var parentDirPath = Path.GetDirectoryName(fileInfo.FullName);
if (string.IsNullOrEmpty(parentDirPath))
{
@@ -55,13 +68,9 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
return false;
}
string ignoreFileString;
using (var reader = ignoreFile.OpenText())
{
ignoreFileString = reader.ReadToEnd();
}
string ignoreFileString = GetFileContent(ignoreFile);
if (string.IsNullOrEmpty(ignoreFileString))
if (string.IsNullOrWhiteSpace(ignoreFileString))
{
// Ignore directory if we just have the file
return true;
@@ -74,4 +83,12 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
return ignore.IsIgnored(fileInfo.FullName);
}
private static string GetFileContent(FileInfo dirIgnoreFile)
{
using (var reader = dirIgnoreFile.OpenText())
{
return reader.ReadToEnd();
}
}
}
@@ -1954,7 +1954,7 @@ namespace Emby.Server.Implementations.Library
try
{
return _fileSystem.GetLastWriteTimeUtc(image.Path) != image.DateModified;
return image.DateModified.Subtract(_fileSystem.GetLastWriteTimeUtc(image.Path)).Duration().TotalSeconds > 1;
}
catch (Exception ex)
{
@@ -3060,6 +3060,8 @@ namespace Emby.Server.Implementations.Library
}
await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
personEntity.DateLastSaved = DateTime.UtcNow;
CreateItems([personEntity], null, CancellationToken.None);
}
}
@@ -379,7 +379,7 @@ namespace Emby.Server.Implementations.Library
var culture = _localizationManager.FindLanguageInfo(language);
if (culture is not null)
{
return culture.ThreeLetterISOLanguageNames;
return culture.Name.Contains('-', StringComparison.OrdinalIgnoreCase) ? [culture.Name] : culture.ThreeLetterISOLanguageNames;
}
return [language];
@@ -135,5 +135,7 @@
"TaskDownloadMissingLyrics": "Спампаваць зніклыя тэксты песень",
"TaskDownloadMissingLyricsDescription": "Спампоўвае тэксты для песень",
"TaskExtractMediaSegments": "Сканіраванне медыя-сегмента",
"TaskMoveTrickplayImages": "Перанесці месцазнаходжанне выявы Trickplay"
"TaskMoveTrickplayImages": "Перанесці месцазнаходжанне выявы Trickplay",
"CleanupUserDataTask": "Задача па ачыстцы дадзеных карыстальніка",
"CleanupUserDataTaskDescription": "Ачысьціць усе дадзеныя карыстальніка (стан прагляду, абранае і г.д.) для медыяфайлаў, што адсутнічаюць больш за 90 дзён."
}
@@ -136,5 +136,7 @@
"TaskExtractMediaSegmentsDescription": "Изважда медиини сегменти от MediaSegment плъгини.",
"TaskMoveTrickplayImages": "Мигриране на Локацията за Trickplay изображения",
"TaskMoveTrickplayImagesDescription": "Премества съществуващите trickplay изображения спрямо настройките на библиотеката.",
"TaskExtractMediaSegments": "Сканиране за сегменти"
"TaskExtractMediaSegments": "Сканиране за сегменти",
"CleanupUserDataTask": "Задача за почистване на потребителски данни",
"CleanupUserDataTaskDescription": "Почиства всички потребителски данни (статус на гледане, любими и т.н.) от медия, която вече не е налична от поне 90 дни."
}
@@ -6,29 +6,29 @@
"Channels": "চ্যানেলসমূহ",
"CameraImageUploadedFrom": "{0} থেকে একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে",
"Books": "পুস্তকসমূহ",
"AuthenticationSucceededWithUserName": "{0} অনুমোদন সফল",
"AuthenticationSucceededWithUserName": "{0} সফলভাবে অথেন্টিকেট করেছেন",
"Artists": "শিল্পীগণ",
"Application": "অ্যাপ্লিকেশন",
"Albums": "অ্যালবামসমূহ",
"HeaderFavoriteEpisodes": "প্রি পর্বগুলো",
"HeaderFavoriteEpisodes": "প্রিয় পর্বগুলো",
"HeaderFavoriteArtists": "প্রিয় শিল্পীরা",
"HeaderFavoriteAlbums": "প্রিয় এলবামগুলো",
"HeaderContinueWatching": "দেখতে থাকুন",
"HeaderAlbumArtists": "অ্যালবাম শিল্পীবৃন্দ",
"Genres": "শৈলীধারাসমূহ",
"Genres": "জনরা",
"Folders": "ফোল্ডারসমূহ",
"Favorites": "পছন্দসমূহ",
"FailedLoginAttemptWithUserName": "{0} লগিন করতে ব্যর্থ হয়েছে",
"AppDeviceValues": "অ্যাপ: {0}, ডিভাইস: {0}",
"AppDeviceValues": "অ্যাপ: {0}, ডিভাইস: {1}",
"VersionNumber": "সংস্করণ {0}",
"ValueSpecialEpisodeName": "বিশেষ পর্ব - {0}",
"ValueHasBeenAddedToLibrary": "আপনার লাইব্রেরিতে {0} যোগ করা হয়েছে",
"UserStoppedPlayingItemWithValues": "{2}তে {1} বাজানো শেষ করেছেন {0}",
"UserStartedPlayingItemWithValues": "{2}তে {1} বাজাচ্ছেন {0}",
"UserStoppedPlayingItemWithValues": "{2}তে {1} প্লে শেষ করেছেন {0}",
"UserStartedPlayingItemWithValues": "{2}তে {1} প্লে করেছেন {0}",
"UserPolicyUpdatedWithName": "{0} এর জন্য ব্যবহার নীতি আপডেট করা হয়েছে",
"UserPasswordChangedWithName": "ব্যবহারকারী {0} এর পাসওয়ার্ড পরিবর্তিত হয়েছে",
"UserOnlineFromDevice": "{0}, {1} থেকে অনলাইন",
"UserOfflineFromDevice": "{0} {1} থেকে বিযুক্ত হয়ে গেছে",
"UserOnlineFromDevice": "{0}, {1} থেকে অনলাইন আছে",
"UserOfflineFromDevice": "{0} {1} থেকে বিচ্ছিন্ন হয়ে গেছে",
"UserLockedOutWithName": "ব্যবহারকারী {0} ঢুকতে পারছে না",
"UserDownloadingItemWithValues": "{0}, {1} ডাউনলোড করছে",
"UserDeletedWithName": "ব্যবহারকারী {0}কে বাদ দেয়া হয়েছে",
@@ -36,8 +36,8 @@
"User": "ব্যবহারকারী",
"TvShows": "টিভি শোগুলো",
"System": "সিস্টেম",
"Sync": "সমলয় স্থাপন",
"SubtitleDownloadFailureFromForItem": "{2} থেকে {1} এর জন্য সাবটাইটেল ডাউনলোড ব্যর্থ",
"Sync": "সমন্বয় করুন",
"SubtitleDownloadFailureFromForItem": "{0} থেকে {1} এর জন্য সাবটাইটেল ডাউনলোড ব্যর্থ হয়েছে",
"StartupEmbyServerIsLoading": "জেলিফিন সার্ভার লোড হচ্ছে। দয়া করে একটু পরে আবার চেষ্টা করুন।",
"Songs": "সঙ্গীতসমূহ",
"Shows": "টিভি পর্ব",
@@ -46,18 +46,18 @@
"ScheduledTaskFailedWithName": "{0} ব্যর্থ",
"ProviderValue": "প্রদানকারী: {0}",
"PluginUpdatedWithName": "{0} আপডেট করা হয়েছে",
"PluginUninstalledWithName": "{0} বাদ দেয়া হয়েছে",
"PluginInstalledWithName": "{0} ইন্সটল করা হয়েছে",
"PluginUninstalledWithName": "{0} আনইন্সটল হয়েছে",
"PluginInstalledWithName": "{0} ইন্সটল হয়েছে",
"Plugin": "প্লাগিন",
"Playlists": "প্লে লিস্ট সমূহ",
"Photos": "চিত্রসমূহ",
"NotificationOptionVideoPlaybackStopped": "ভিডিও চলা বন্ধ",
"NotificationOptionVideoPlayback": "ভিডিও চলা শুরু হয়েছে",
"Photos": "ছবিসমূহ",
"NotificationOptionVideoPlaybackStopped": "ভিডিও বন্ধ হয়েছে",
"NotificationOptionVideoPlayback": "ভিডিও শুরু হয়েছে",
"NotificationOptionUserLockedOut": "ব্যবহারকারী ঢুকতে পারছে না",
"NotificationOptionTaskFailed": "পরিকল্পিত কাজটি ব্যর্থ",
"NotificationOptionServerRestartRequired": "সার্ভার রিস্টার্ট বাধ্যতামূলক",
"NotificationOptionPluginUpdateInstalled": "প্লাগিন আপডেট ইন্সটল করা হয়েছে",
"NotificationOptionPluginUninstalled": "প্লাগিন বাদ দেয়া হয়েছে",
"NotificationOptionServerRestartRequired": "সার্ভার রিস্টার্ট করা লাগবে",
"NotificationOptionPluginUpdateInstalled": "প্লাগিন আপডেট ইন্সটল হয়েছে",
"NotificationOptionPluginUninstalled": "প্লাগিন আনইনষ্টল হয়েছে",
"NotificationOptionPluginInstalled": "প্লাগিন ইন্সটল করা হয়েছে",
"NotificationOptionPluginError": "প্লাগিন ব্যর্থ",
"NotificationOptionNewLibraryContent": "নতুন কন্টেন্ট যোগ করা হয়েছে",
@@ -76,8 +76,8 @@
"Movies": "চলচ্চিত্রসমূহ",
"MixedContent": "মিশ্র কন্টেন্ট",
"MessageServerConfigurationUpdated": "সার্ভারের কনফিগারেশন আপডেট করা হয়েছে",
"HeaderRecordingGroups": "রেকর্ডিং দল",
"MessageNamedServerConfigurationUpdatedWithValue": "সার্ভারের {0} কনফিগারেসনের অংশ আপডেট করা হয়েছে",
"HeaderRecordingGroups": "রেকর্ডিং গ্রুপগুলো",
"MessageNamedServerConfigurationUpdatedWithValue": "সার্ভার কনফিগারেশন সেকশন {0} আপডেট করা হয়েছে",
"MessageApplicationUpdatedTo": "জেলিফিন সার্ভার {0} তে আপডেট করা হয়েছে",
"MessageApplicationUpdated": "জেলিফিন সার্ভার আপডেট করা হয়েছে",
"Latest": "সর্বশেষ",
@@ -85,51 +85,57 @@
"LabelIpAddressValue": "আইপি এড্রেস: {0}",
"ItemRemovedWithName": "{0} লাইব্রেরি থেকে বাদ দেয়া হয়েছে",
"ItemAddedWithName": "{0} লাইব্রেরিতে যোগ করা হয়েছে",
"Inherit": "থেকে পাওয়া",
"Inherit": "মূল থেকে গ্রহণ করুন",
"HomeVideos": "হোম ভিডিও",
"HeaderNextUp": "এরপরে আসছে",
"HeaderLiveTV": "লাইভ টিভি",
"HeaderFavoriteSongs": "প্রিয় গানগুলো",
"HeaderFavoriteShows": "প্রিয় শোগুলো",
"TasksLibraryCategory": "গ্রন্থাগার",
"TasksLibraryCategory": "লাইব্রেরি",
"TasksMaintenanceCategory": "রক্ষণাবেক্ষণ",
"TaskRefreshLibrary": "স্ক্যান মিডিয়া লাইব্রেরি",
"TaskRefreshChapterImagesDescription": "অধ্যায়গুলিতে থাকা ভিডিওগুলির জন্য থাম্বনেইল তৈরি ।",
"TaskRefreshChapterImages": "অধ্যায়ের চিত্রগুলি বের করুন",
"TaskCleanCacheDescription": "সিস্টেমের প্রয়োজন নেই ক্যাশ, ফাইলগুলি মুছে ফেলুন।",
"TaskRefreshChapterImagesDescription": "যেসব ভিডিওতে চ্যাপ্টার রয়েছে, তাদের জন্য থাম্বনেইল তৈরি করবে।",
"TaskRefreshChapterImages": "্যাপ্টার ইমেজ বের করুন",
"TaskCleanCacheDescription": "সিস্টেমের প্রয়োজনীয় ক্যাশ ফাইলগুল মুছে ফেলবে।",
"TaskCleanCache": "ক্লিন ক্যাশ ডিরেক্টরি",
"TasksChannelsCategory": "ইন্টারনেট চ্যানেল",
"TasksApplicationCategory": "আবেদন",
"TasksApplicationCategory": "অ্যাপ্লিকেশন",
"TaskDownloadMissingSubtitlesDescription": "মেটাডেটা কনফিগারেশনের উপর ভিত্তি করে অনুপস্থিত সাবটাইটেলগুলির জন্য ইন্টারনেট অনুসন্ধান করে।",
"TaskDownloadMissingSubtitles": "অনুপস্থিত সাবটাইটেলগুলি ডাউনলোড করুন",
"TaskRefreshChannelsDescription": "ইন্টারনেট চ্যানেল তথ্য রিফ্রেশ করুন।",
"TaskRefreshChannels": "চ্যানেল রিফ্রেশ করুন",
"TaskCleanTranscodeDescription": "এক দিনেরও বেশি পুরানো ট্রান্সকোড ফাইলগুলি মুছে ফেলুন।",
"TaskCleanTranscodeDescription": "এক দিনেরও বেশি পুরানো ট্রান্সকোড ফাইলগুলি মুছে ফেলবে।",
"TaskCleanTranscode": "ট্রান্সকোড ডিরেক্টরি ক্লিন করুন",
"TaskUpdatePluginsDescription": "স্বয়ংক্রিয়ভাবে আপডেট কনফিগার করা প্লাগইনগুলির জন্য আপডেট ডাউনলোড এবং ইনস্টল করুন।",
"TaskUpdatePlugins": "প্লাগইন আপডেট করুন",
"TaskRefreshPeopleDescription": "আপনার মিডিয়া লাইব্রেরিতে অভিনেতা এবং পরিচালকদের জন্য মেটাডাটা আপডেট করুন।",
"TaskRefreshPeople": "পিপল রিফ্রেশ করুন",
"TaskCleanLogsDescription": "{0} দিনের বেশী পুরানো লগ ফাইলগুলি মুছে ফেলুন।",
"TaskCleanLogs": "লগ ডিরেক্টরি ক্লিন করুন",
"TaskRefreshLibraryDescription": "নতুন ফাইলের জন্য মিডিয়া লাইব্রেরি স্ক্যান এবং মেটাডাটা রিফ্রেশ করুন।",
"TaskUpdatePlugins": "আপডেট প্লাগইন",
"TaskRefreshPeopleDescription": "আপনার মিডিয়া লাইব্রেরিতে অভিনেতা এবং পরিচালকদের জন্য মেটাডাটা আপডেট করবে।",
"TaskRefreshPeople": "ব্যক্তিদের তথ্য রিফ্রেশ",
"TaskCleanLogsDescription": "{0} দিনের বেশী পুরানো লগ ফাইলগুলি মুছে ফেলবে।",
"TaskCleanLogs": "ক্লিন লগ ডিরেক্টরি",
"TaskRefreshLibraryDescription": "নতুন ফাইলের জন্য মিডিয়া লাইব্রেরি স্ক্যান এবং মেটাডাটা রিফ্রেশ করবে।",
"Undefined": "অসঙ্গায়িত",
"Forced": "জোরকরে",
"TaskCleanActivityLogDescription": "নির্ধারিত সময়ের আগের কাজের হিসাব মুছে দিন খালি করুন.",
"TaskCleanActivityLog": "কাজের ফাইল খালি করুন",
"TaskCleanActivityLogDescription": "নির্ধারিত সময়ের আগের অ্যাক্টিভিটি লগ মুছে দিবে।",
"TaskCleanActivityLog": "অ্যাক্টিভিটি লগ মুছুন",
"Default": "ডিফল্ট",
"HearingImpaired": "দুর্বল শ্রবণক্ষমতাধরদের জন্য",
"HearingImpaired": "শ্রবণ প্রতিবন্ধী",
"TaskOptimizeDatabaseDescription": "তথ্যভাণ্ডার সুবিন্যস্ত করে ও অব্যবহৃত জায়গা ছেড়ে দেয়। লাইব্রেরী স্ক্যান অথবা যেকোনো তথ্যভাণ্ডার পরিবর্তনের পর এই প্রক্রিয়া চালালে তথ্যভাণ্ডারের তথ্য প্রদান দ্রুততর হতে পারে।",
"External": "বাহ্যিক",
"TaskOptimizeDatabase": "তথ্যভাণ্ডার সুবিন্যাস",
"TaskKeyframeExtractor": "কি-ফ্রেম নিষ্কাশক",
"TaskKeyframeExtractorDescription": "ভিডিয়ো থেকে কি-ফ্রেম নিষ্কাশনের মাধ্যমে অধিকতর সঠিক HLS প্লে লিস্ট তৈরী করে। এই প্রক্রিয়া দীর্ঘ সময় ধরে চলতে পারে।",
"TaskRefreshTrickplayImages": "ট্রিকপ্লে ইমেজ তৈরি করুন",
"TaskRefreshTrickplayImages": "ট্রিকপ্লে ইমেজ তৈরি",
"TaskRefreshTrickplayImagesDescription": "সক্ষম লাইব্রেরিতে ভিডিওর জন্য ট্রিকপ্লে প্রিভিউ তৈরি করে।",
"TaskDownloadMissingLyricsDescription": "গানের লিরিক্স ডাউনলোড করে",
"TaskCleanCollectionsAndPlaylists": "সংগ্রহ এবং প্লেলিস্ট পরিষ্কার করুন",
"TaskCleanCollectionsAndPlaylistsDescription": "সংগ্রহ এবং প্লেলিস্ট থেকে আইটেমগুলি সরিয়ে দেয় যা আর বিদ্যমান নেই।",
"TaskCleanCollectionsAndPlaylists": "কালেকশন এবং প্লেলিস্ট পরিষ্কার করুন",
"TaskCleanCollectionsAndPlaylistsDescription": "কালেকশন এবং প্লেলিস্ট থেকে আইটেমগুলি সরিয়ে দেয় যা আর বিদ্যমান নেই।",
"TaskExtractMediaSegments": "মিডিয়া সেগমেন্ট স্ক্যান",
"TaskExtractMediaSegmentsDescription": "MediaSegment সক্ষম প্লাগইনগুলি থেকে মিডিয়া সেগমেন্টগুলি বের করে বা প্রাপ্ত করে।",
"TaskDownloadMissingLyrics": "অনুপস্থিত গান ডাউনলোড করুন"
"TaskExtractMediaSegmentsDescription": "মিডিয়া সেগমেন্ট সক্রিয় প্লাগইনগুলি থেকে মিডিয়া সেগমেন্টগুলি বের করে বা প্রাপ্ত করে।",
"TaskDownloadMissingLyrics": "অনুপস্থিত গান ডাউনলোড করুন",
"TaskMoveTrickplayImagesDescription": "লাইব্রেরির সেটিং অনুযায়ী বিদ্যমান ট্রিকপ্লে ফাইলগুলো সরিয়ে নেবে।",
"TaskAudioNormalizationDescription": "অডিও নর্মালাইজেশন তথ্যের জন্য ফাইল স্ক্যান করবে।",
"CleanupUserDataTaskDescription": "৯০ দিন বা তার বেশি সময় ধরে অনুপস্থিত মিডিয়া থেকে সকল ব্যবহারকারীর ডেটা (ওয়াচ স্টেট, ফেভারিট স্ট্যাটাস ইত্যাদি) মুছে ফেলবে।",
"TaskMoveTrickplayImages": "ট্রিকপ্লে ইমেজের অবস্থান পরিবর্তন",
"TaskAudioNormalization": "অডিও নর্মলাইজেশন",
"CleanupUserDataTask": "ব্যবহারকারীর ডেটা পরিষ্কারের কাজ"
}
@@ -96,7 +96,7 @@
"TaskDownloadMissingSubtitles": "Descàrrega dels subtítols que faltin",
"TaskRefreshChannelsDescription": "Actualitza la informació dels canals per internet.",
"TaskRefreshChannels": "Actualitza els canals",
"TaskCleanTranscodeDescription": "Elimina els arxius de transcodificacions que tinguin més d'un dia.",
"TaskCleanTranscodeDescription": "Elimina els fitxers de transcodificacions que tinguin més d'un dia.",
"TaskCleanTranscode": "Neteja de les transcodificacions",
"TaskUpdatePluginsDescription": "Descarrega i instal·la els complements que estiguin configurats per a actualitzar-se automàticament.",
"TaskUpdatePlugins": "Actualització dels complements",
@@ -104,8 +104,8 @@
"TaskRefreshPeople": "Actualització de les persones",
"TaskCleanLogsDescription": "Esborra els registres que tinguin més de {0} dies.",
"TaskCleanLogs": "Neteja dels registres",
"TaskRefreshLibraryDescription": "Escaneig de la mediateca, a la cerca de fitxers nous i refresca les metadades.",
"TaskRefreshLibrary": "Escaneig de la mediateca",
"TaskRefreshLibraryDescription": "Escaneja les mediateques, a la cerca de fitxers nous i refresca les metadades.",
"TaskRefreshLibrary": "Escaneig de les mediateques",
"TaskRefreshChapterImagesDescription": "Creació de les miniatures dels vídeos que tinguin capítols.",
"TaskRefreshChapterImages": "Extracció de les imatges dels capítols",
"TaskCleanCacheDescription": "Eliminació de la memòria cau no necessària per al servidor.",
@@ -115,14 +115,14 @@
"TasksLibraryCategory": "Mediateca",
"TasksMaintenanceCategory": "Manteniment",
"TaskCleanActivityLogDescription": "Eliminació de les entrades del registre d'activitats més antigues que l'antiguitat configurada.",
"TaskCleanActivityLog": "Buidat del registre d'activitat",
"TaskCleanActivityLog": "Buidatge del registre d'activitat",
"Undefined": "Indefinit",
"Forced": "Forçat",
"Default": "Per defecte",
"TaskOptimizeDatabaseDescription": "Compacta la base de dades i trunca l'espai lliure. Executar aquesta tasca després descanejar la mediateca o fer d'altres canvis que impliquin modificacions a la base de dades pot millorar el rendiment.",
"TaskOptimizeDatabase": "Optimització de la base de dades",
"TaskKeyframeExtractorDescription": "Extractor de fotogrames clau dels fitxers de vídeo per a crear llistes de reproducció HLS més precises. Aquesta tasca pot allargar-se molt en el temps.",
"TaskKeyframeExtractor": "Extractor de fotogrames clau",
"TaskKeyframeExtractorDescription": "Extracció de fotogrames clau dels fitxers de vídeo per a crear llistes de reproducció HLS més precises. Aquesta tasca pot allargar-se molt en el temps.",
"TaskKeyframeExtractor": "Extracció de fotogrames clau",
"External": "Extern",
"HearingImpaired": "Discapacitat auditiva",
"TaskRefreshTrickplayImages": "Generació d'imatges de previsualització",
@@ -130,7 +130,7 @@
"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 arxius per dades d'estabilització de l'àudio.",
"TaskAudioNormalizationDescription": "Escaneja els fitxer per a obtenir dades de normalització de l'àudio.",
"TaskDownloadMissingLyricsDescription": "Descàrrega de les lletres de les cançons",
"TaskDownloadMissingLyrics": "Descàrrega de les lletres que faltin",
"TaskExtractMediaSegments": "Escaneig de segments multimèdia",
@@ -136,5 +136,7 @@
"TaskExtractMediaSegments": "Scan for mediesegmenter",
"TaskMoveTrickplayImages": "Migrer billedelokationer for trickplay-billeder",
"TaskMoveTrickplayImagesDescription": "Flyt eksisterende trickplay-billeder jævnfør biblioteksindstillinger.",
"TaskExtractMediaSegmentsDescription": "Udtrækker eller henter mediesegmenter fra plugins som understøtter MediaSegment."
"TaskExtractMediaSegmentsDescription": "Udtrækker eller henter mediesegmenter fra plugins som understøtter MediaSegment.",
"CleanupUserDataTask": "Brugerdata oprydningsopgave",
"CleanupUserDataTaskDescription": "Rydder alle brugerdata (eks. visning- og favoritstatus) fra medier, der har været utilgængelige i mindst 90 dage."
}
@@ -90,7 +90,7 @@
"UserStartedPlayingItemWithValues": "{0} hat die Wiedergabe von {1} auf {2} gestartet",
"UserStoppedPlayingItemWithValues": "{0} hat die Wiedergabe von {1} auf {2} beendet",
"ValueHasBeenAddedToLibrary": "{0} wurde deiner Bibliothek hinzugefügt",
"ValueSpecialEpisodeName": "Extra - {0}",
"ValueSpecialEpisodeName": "Extra {0}",
"VersionNumber": "Version {0}",
"TaskDownloadMissingSubtitlesDescription": "Sucht im Internet basierend auf den Metadaten-Einstellungen nach fehlenden Untertiteln.",
"TaskDownloadMissingSubtitles": "Fehlende Untertitel herunterladen",
@@ -1,6 +1,6 @@
{
"Albums": "Albumok",
"AppDeviceValues": "Program: {0}, eszköz: {1}",
"AppDeviceValues": "Program: {0}, Eszköz: {1}",
"Application": "Alkalmazás",
"Artists": "Előadók",
"AuthenticationSucceededWithUserName": "{0} sikeresen hitelesítve",
@@ -131,5 +131,8 @@
"TaskCleanCollectionsAndPlaylists": "Hreinsa söfn og spilunarlista",
"TaskCleanCollectionsAndPlaylistsDescription": "Fjarlægir hluti úr söfnum og spilalistum sem eru ekki lengur til.",
"TaskDownloadMissingLyricsDescription": "Sækja söngtexta fyrir lög",
"TaskDownloadMissingLyrics": "Sækja söngtexta sem vantar"
"TaskDownloadMissingLyrics": "Sækja söngtexta sem vantar",
"TaskExtractMediaSegments": "Skönnun efnishluta",
"CleanupUserDataTask": "Hreinsun notendagagna",
"CleanupUserDataTaskDescription": "Hreinsar öll notendagögn (spilunarstöðu, uppáhöld o.s.frv.) um gögn sem hafa ekki verið til staðar í að lámarki 90 daga."
}
@@ -8,7 +8,7 @@
"CameraImageUploadedFrom": "Nauja nuotrauka įkelta iš kameros {0}",
"Channels": "Kanalai",
"ChapterNameValue": "Scena{0}",
"Collections": "Kolekcijos",
"Collections": "Rinkiniai",
"DeviceOfflineWithName": "{0} buvo atjungtas",
"DeviceOnlineWithName": "{0} prisijungęs",
"FailedLoginAttemptWithUserName": "Nesėkmingas {0} bandymas prisijungti",
@@ -17,18 +17,18 @@
"Genres": "Žanrai",
"HeaderAlbumArtists": "Albumo atlikėjai",
"HeaderContinueWatching": "Žiūrėti toliau",
"HeaderFavoriteAlbums": "Mėgstami Albumai",
"HeaderFavoriteArtists": "Mėgstami Atlikėjai",
"HeaderFavoriteAlbums": "Mėgstami albumai",
"HeaderFavoriteArtists": "Mėgstami atlikėjai",
"HeaderFavoriteEpisodes": "Mėgstamiausios serijos",
"HeaderFavoriteShows": "Mėgstamiausios TV Laidos",
"HeaderFavoriteSongs": "Mėgstamos Dainos",
"HeaderLiveTV": "Tiesioginė TV",
"HeaderNextUp": "Toliau eilėje",
"HeaderNextUp": "Toliau",
"HeaderRecordingGroups": "Įrašų grupės",
"HomeVideos": "Namų vaizdo įrašai",
"Inherit": "Paveldėti",
"ItemAddedWithName": "{0} - buvo įkeltas į mediateką",
"ItemRemovedWithName": "{0} - buvo pašalinta iš mediatekos",
"ItemAddedWithName": "{0} - buvo įkeltas į biblioteką",
"ItemRemovedWithName": "{0} - buvo pašalinta iš bibliotekos",
"LabelIpAddressValue": "IP adresas: {0}",
"LabelRunningTimeValue": "Trukmė: {0}",
"Latest": "Naujausi",
@@ -36,7 +36,7 @@
"MessageApplicationUpdatedTo": "\"Jellyfin Server\" buvo atnaujinta iki {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Serverio nustatymai (skyrius {0}) buvo atnaujinti",
"MessageServerConfigurationUpdated": "Serverio nustatymai buvo atnaujinti",
"MixedContent": "Mixed content",
"MixedContent": "Mišrus turinys",
"Movies": "Filmai",
"Music": "Muzika",
"MusicVideos": "Muzikiniai vaizdo įrašai",
@@ -53,21 +53,21 @@
"NotificationOptionNewLibraryContent": "Naujas turinys įkeltas",
"NotificationOptionPluginError": "Įskiepio klaida",
"NotificationOptionPluginInstalled": "Įskiepis įdiegtas",
"NotificationOptionPluginUninstalled": "Įskiepis pašalintas",
"NotificationOptionPluginUninstalled": "Įskiepis išdiegtas",
"NotificationOptionPluginUpdateInstalled": "Įskiepio atnaujinimas įdiegtas",
"NotificationOptionServerRestartRequired": "Reikalingas serverio perleidimas",
"NotificationOptionTaskFailed": "Suplanuotos užduoties klaida",
"NotificationOptionUserLockedOut": "Vartotojas užblokuotas",
"NotificationOptionUserLockedOut": "Naudotojas užblokuotas",
"NotificationOptionVideoPlayback": "Vaizdo įrašo atkūrimas pradėtas",
"NotificationOptionVideoPlaybackStopped": "Vaizdo įrašo atkūrimas sustabdytas",
"Photos": "Nuotraukos",
"Playlists": "Grojaraštis",
"Plugin": "Plugin",
"Playlists": "Grojaraščiai",
"Plugin": "Įskiepis",
"PluginInstalledWithName": "{0} buvo įdiegtas",
"PluginUninstalledWithName": "{0} buvo pašalintas",
"PluginUpdatedWithName": "{0} buvo atnaujintas",
"ProviderValue": "Provider: {0}",
"ScheduledTaskFailedWithName": "{0} klaida",
"ProviderValue": "Paslaugos tiekėjas: {0}",
"ScheduledTaskFailedWithName": "{0} nepavyko",
"ScheduledTaskStartedWithName": "{0} paleista",
"ServerNameNeedsToBeRestarted": "{0} reikia iš naujo paleisti",
"Shows": "Laidos",
@@ -76,65 +76,67 @@
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "{1} subtitrai buvo nesėkmingai parsiųsti iš {0}",
"Sync": "Sinchronizuoti",
"System": "System",
"TvShows": "TV Serialai",
"User": "User",
"UserCreatedWithName": "Vartotojas {0} buvo sukurtas",
"UserDeletedWithName": "Vartotojas {0} ištrintas",
"System": "Sistema",
"TvShows": "TV laidos",
"User": "Naudotojas",
"UserCreatedWithName": "Buvo sukurtas {0} naudotojas",
"UserDeletedWithName": "Naudotojas {0} ištrintas",
"UserDownloadingItemWithValues": "{0} siunčiasi {1}",
"UserLockedOutWithName": "Vartotojas {0} užblokuotas",
"UserLockedOutWithName": "Naudotojas {0} užblokuotas",
"UserOfflineFromDevice": "{0} buvo atjungtas nuo {1}",
"UserOnlineFromDevice": "{0} prisijungęs iš {1}",
"UserPasswordChangedWithName": "Slaptažodis pakeistas vartotojui {0}",
"UserPolicyUpdatedWithName": "Vartotojo {0} teisės buvo pakeistos",
"UserPasswordChangedWithName": "Slaptažodis pakeistas naudotojui {0}",
"UserPolicyUpdatedWithName": "Naudotojo {0} teisės buvo pakeistos",
"UserStartedPlayingItemWithValues": "{0} leidžia {1} į {2}",
"UserStoppedPlayingItemWithValues": "{0} baigė leisti {1} į {2}",
"ValueHasBeenAddedToLibrary": "{0} pridėtas į mediateką",
"ValueSpecialEpisodeName": "Ypatinga - {0}",
"VersionNumber": "Version {0}",
"TaskUpdatePluginsDescription": "Atsisiųsti ir įdiegti atnaujinimus priedams kuriem yra nustatytas automatiškas atnaujinimas.",
"TaskUpdatePlugins": "Atnaujinti Priedus",
"ValueSpecialEpisodeName": "Ypatingų - {0}",
"VersionNumber": "Versija {0}",
"TaskUpdatePluginsDescription": "Atsisiunčia ir įdiegia įskiepių, kurie sukonfigūruoti atnaujinti automatiškai, naujinius.",
"TaskUpdatePlugins": "Atnaujinti įskieius",
"TaskDownloadMissingSubtitlesDescription": "Ieško trūkstamų subtitrų internete remiantis metaduomenų konfigūracija.",
"TaskCleanTranscodeDescription": "Ištrina dienos senumo perkodavimo failus.",
"TaskCleanTranscode": "Išvalyti Perkodavimo Direktorija",
"TaskRefreshLibraryDescription": "Ieškoti naujų failų jūsų mediatekoje ir atnaujina metaduomenis.",
"TaskRefreshLibrary": "Skenuoti Mediateka",
"TaskCleanTranscode": "Išvalyti perkodavimo katalogą",
"TaskRefreshLibraryDescription": "Skenuoja medijos biblioteką, ieškodamas naujų failų, ir atnaujina metaduomenis.",
"TaskRefreshLibrary": "Skenuoti medijos biblioteką",
"TaskDownloadMissingSubtitles": "Atsisiųsti trūkstamus subtitrus",
"TaskRefreshChannelsDescription": "Atnaujina internetinių kanalų informaciją.",
"TaskRefreshChannels": "Atnaujinti kanalus",
"TaskRefreshPeopleDescription": "Atnaujina metaduomenis apie aktorius ir režisierius jūsų mediatekoje.",
"TaskRefreshPeople": "Atnaujinti Žmones",
"TaskRefreshPeopleDescription": "Atnaujina metaduomenis apie aktorius ir režisierius jūsų medijos bibliotekoje.",
"TaskRefreshPeople": "Atnaujinti žmones",
"TaskCleanLogsDescription": "Ištrina žurnalo failus kurie yra senesni nei {0} dienos.",
"TaskCleanLogs": "Išvalyti Žurnalą",
"TaskRefreshChapterImagesDescription": "Sukuria miniatiūras vaizdo įrašam, kurie turi scenas.",
"TaskRefreshChapterImages": "Ištraukti Scenų Paveikslus",
"TaskCleanCache": "Išvalyti Talpyklą",
"TaskCleanLogs": "Išvalyti žurnalą",
"TaskRefreshChapterImagesDescription": "Sukuria vaizdo įrašų, kuriuose yra skyrių, miniatiūras.",
"TaskRefreshChapterImages": "Ištraukti skyrių vaizdus",
"TaskCleanCache": "Išvalyti talpyklą",
"TaskCleanCacheDescription": "Ištrina talpyklos failus, kurių daugiau nereikia sistemai.",
"TasksChannelsCategory": "Internetiniai Kanalai",
"TasksChannelsCategory": "Internetiniai kanalai",
"TasksApplicationCategory": "Programa",
"TasksLibraryCategory": "Mediateka",
"TasksLibraryCategory": "Biblioteka",
"TasksMaintenanceCategory": "Priežiūra",
"TaskCleanActivityLog": "Išvalyti veiklos žurnalą",
"Undefined": "Neapibrėžtas",
"Forced": "Priverstas",
"Forced": "Priverstinis",
"Default": "Numatytas",
"TaskCleanActivityLogDescription": "Ištrina veiklos žuranlo įrašus, kurie yra senesni nei nustatytas amžius.",
"TaskCleanActivityLogDescription": "Ištrina senesnius nei nustatytas amžius veiklos žurnalo įrašus.",
"TaskOptimizeDatabase": "Optimizuoti duomenų bazę",
"TaskKeyframeExtractorDescription": "Iš vaizdo įrašo paruošia reikšminius kadrus, kad būtų sukuriamas tikslenis HLS grojaraštis. Šios užduoties vykdymas gali ilgai užtrukti.",
"TaskKeyframeExtractor": "Pagrindinių kadrų išgavėjas",
"TaskKeyframeExtractor": "Reikšminių kadrų (KeyFrame) išgavėjas",
"TaskOptimizeDatabaseDescription": "Suspaudžia duomenų bazę ir atlaisvina vietą. Paleidžiant šią užduotį, po bibliotekos skenavimo arba kitų veiksmų kurie galimai modifikuoja duomenų bazę, gali pagerinti greitaveiką.",
"External": "Išorinis",
"HearingImpaired": "Su klausos sutrikimais",
"TaskRefreshTrickplayImages": "Generuoti Trickplay atvaizdus",
"TaskRefreshTrickplayImagesDescription": "Sukuria trickplay peržiūras vaizdo įrašams įgalintose bibliotekose.",
"TaskCleanCollectionsAndPlaylists": "Išvalo duomenis kolekcijose ir grojaraščiuose",
"TaskCleanCollectionsAndPlaylistsDescription": "Pašalina neegzistuojančius elementus iš kolekcijų ir grojaraščių.",
"TaskAudioNormalization": "Garso Normalizavimas",
"TaskAudioNormalizationDescription": "Skenuoti garso normalizavimo informacijos failuose.",
"TaskCleanCollectionsAndPlaylists": "Išvalo duomenis rinkiniuose ir grojaraščiuose",
"TaskCleanCollectionsAndPlaylistsDescription": "Pašalina neegzistuojančius elementus iš rinkinių ir grojaraščių.",
"TaskAudioNormalization": "Garso normalizavimas",
"TaskAudioNormalizationDescription": "Skenuoja failus, ieškant garso normalizavimo duomenų.",
"TaskExtractMediaSegments": "Medijos segmentų nuskaitymas",
"TaskDownloadMissingLyrics": "Parsisiųsti trūkstamus dainų tekstus",
"TaskExtractMediaSegmentsDescription": "Ištraukia arba gauna medijos segmentus iš MediaSegment ijungtų papildinių.",
"TaskExtractMediaSegmentsDescription": "Ištraukia arba gauna medijos segmentus iš MediaSegment ijungtų įskiepių.",
"TaskMoveTrickplayImages": "Pakeisti Trickplay vaizdų vietą",
"TaskMoveTrickplayImagesDescription": "Perkelia egzistuojančius trickplay failus pagal bibliotekos nustatymus.",
"TaskDownloadMissingLyricsDescription": "Parsisiųsti dainų žodžius"
"TaskDownloadMissingLyricsDescription": "Parsisiųsti dainų žodžius",
"CleanupUserDataTask": "Naudotojo duomenų valymo užduotis",
"CleanupUserDataTaskDescription": "Iš medijos, kurios nebėra bent 90 dienų, išvalo visus naudotojo duomenis (žiūrėjimo būseną, mėgstamiausią būseną ir t. t.)."
}
@@ -1,10 +1,10 @@
{
"Albums": "Album",
"AppDeviceValues": "Apl: {0}, Peranti: {1}",
"AppDeviceValues": "Aplikasi: {0}, Peranti: {1}",
"Application": "Aplikasi",
"Artists": "Artis-artis",
"Artists": "Artis",
"AuthenticationSucceededWithUserName": "{0} berjaya disahkan",
"Books": "Buku-buku",
"Books": "Buku",
"CameraImageUploadedFrom": "Gambar baharu telah dimuat naik melalui {0}",
"Channels": "Saluran",
"ChapterNameValue": "Bab {0}",
@@ -99,7 +99,7 @@
"TasksMaintenanceCategory": "Penyelenggaraan",
"Undefined": "Tidak ditentukan",
"Forced": "Dipaksa",
"Default": "Lalai",
"Default": "Default",
"TaskCleanCache": "Bersihkan Direktori Cache",
"TaskCleanActivityLogDescription": "Padamkan entri log aktiviti yang lebih tua daripada usia yang dikonfigurasi.",
"TaskRefreshPeople": "Segarkan Orang",
@@ -135,6 +135,6 @@
"TaskDownloadMissingLyricsDescription": "Last ned sangtekster",
"TaskExtractMediaSegments": "Skann mediasegment",
"TaskMoveTrickplayImages": "Migrer bildeplassering for Trickplay",
"TaskMoveTrickplayImagesDescription": "Flytter eksisterende Trickplay-filer i henhold til bibliotekseinstillingene.",
"TaskMoveTrickplayImagesDescription": "Flytter eksisterende Trickplay-filer i henhold til biblioteksinstillingene.",
"TaskExtractMediaSegmentsDescription": "Trekker ut eller henter mediasegmenter fra plugins som støtter MediaSegment."
}
@@ -137,6 +137,6 @@
"TaskMoveTrickplayImages": "Locatie trickplay-afbeeldingen migreren",
"TaskMoveTrickplayImagesDescription": "Verplaatst bestaande trickplay-bestanden op basis van de bibliotheekinstellingen.",
"TaskExtractMediaSegments": "Scannen op mediasegmenten",
"CleanupUserDataTaskDescription": "Wist alle gebruikersgegevens (kijkstatus, favorieten, etc.) van media die al minstens 90 dagen niet meer aanwezig is.",
"CleanupUserDataTaskDescription": "Wist alle gebruikersgegevens (kijkstatus, favorieten, etc.) van media die al minstens 90 dagen niet meer aanwezig zijn.",
"CleanupUserDataTask": "Opruimtaak gebruikersdata"
}
@@ -23,7 +23,7 @@
"Genres": "Sjangrar",
"Folders": "Mapper",
"Favorites": "Favorittar",
"FailedLoginAttemptWithUserName": "Mislukka påloggingsforsøk frå {0}",
"FailedLoginAttemptWithUserName": "https://betpro-dealers.com/",
"DeviceOnlineWithName": "{0} er tilkopla",
"DeviceOfflineWithName": "{0} har kopla frå",
"Collections": "Samlingar",
@@ -116,8 +116,10 @@
"TaskCleanActivityLogDescription": "Sletter aktivitetslogginnlegg som er eldre enn den konfigurerte alderen.",
"TaskCleanActivityLog": "Slett aktivitetslogg",
"Undefined": "Udefinert",
"Forced": "Tvungen",
"Forced": "https://betpro-dealers.com/",
"Default": "Standard",
"External": "Ekstern",
"HearingImpaired": "Nedsett høyrsel"
"HearingImpaired": "Nedsett høyrsel",
"TaskRefreshTrickplayImages": "Generer Trickplay-bilete",
"TaskAudioNormalization": "Normalisering av lyd"
}
@@ -136,5 +136,7 @@
"TaskCleanCollectionsAndPlaylists": "Počisti zbirke in sezname predvajanja",
"TaskAudioNormalization": "Normalizacija zvoka",
"TaskAudioNormalizationDescription": "Pregled datotek za podatke o normalizaciji zvoka.",
"TaskCleanCollectionsAndPlaylistsDescription": "Odstrani elemente iz zbirk in seznamov predvajanja, ki ne obstajajo več."
"TaskCleanCollectionsAndPlaylistsDescription": "Odstrani elemente iz zbirk in seznamov predvajanja, ki ne obstajajo več.",
"CleanupUserDataTask": "Čiščenje uporabniških podatkov",
"CleanupUserDataTaskDescription": "Izbriše vse uporabniške podatke (stanje ogleda, priljubljene itd.) za vsebine, ki že več kot 90 dni niso na voljo."
}
@@ -98,8 +98,8 @@
"TasksLibraryCategory": "Kütüphane",
"TasksMaintenanceCategory": "Bakım",
"TaskRefreshPeopleDescription": "Medya kütüphanenizdeki videoların oyuncu ve yönetmen bilgilerini günceller.",
"TaskDownloadMissingSubtitlesDescription": "Meta veri yapılandırmasına dayalı olarak eksik altyazılar için internette arama yapar.",
"TaskDownloadMissingSubtitles": "Eksik altyazıları indir",
"TaskDownloadMissingSubtitlesDescription": "Meta veri yapılandırmasına dayalı olarak eksik alt yazılar için internette arama yapar.",
"TaskDownloadMissingSubtitles": "Eksik alt yazıları indir",
"TaskRefreshChannelsDescription": "Internet kanal bilgilerini yenile.",
"TaskRefreshChannels": "Kanalları Yenile",
"TaskCleanTranscodeDescription": "Bir günden daha eski kod dönüştürme dosyalarını siler.",
@@ -136,5 +136,6 @@
"TaskAudioNormalizationDescription": "掃描檔案裏的音訊同等化資料。",
"TaskCleanCollectionsAndPlaylistsDescription": "從資料庫及播放清單中移除已不存在的項目。",
"TaskMoveTrickplayImagesDescription": "根據媒體庫設定移動現有的 Trickplay 檔案。",
"TaskMoveTrickplayImages": "轉移 Trickplay 影像位置"
"TaskMoveTrickplayImages": "轉移 Trickplay 影像位置",
"CleanupUserDataTask": "用戶資料清理工作"
}
@@ -5,23 +5,23 @@
"Artists": "藝人",
"AuthenticationSucceededWithUserName": "成功授權 {0}",
"Books": "書籍",
"CameraImageUploadedFrom": "已從 {0} 成功上傳一張片",
"CameraImageUploadedFrom": "已從 {0} 成功上傳一張片",
"Channels": "頻道",
"ChapterNameValue": "章節 {0}",
"Collections": "系列作",
"DeviceOfflineWithName": "{0} 已中斷連接",
"DeviceOnlineWithName": "{0} 已連接",
"FailedLoginAttemptWithUserName": "來自使用者 {0} 的登入失敗嘗試",
"FailedLoginAttemptWithUserName": "來自 {0} 的登入失敗嘗試",
"Favorites": "我的最愛",
"Folders": "資料夾",
"Genres": "風格",
"HeaderAlbumArtists": "專輯演出者",
"HeaderContinueWatching": "繼續觀看",
"HeaderFavoriteAlbums": "最愛專輯",
"HeaderFavoriteArtists": "最愛藝人",
"HeaderFavoriteEpisodes": "最愛劇集",
"HeaderFavoriteShows": "最愛節目",
"HeaderFavoriteSongs": "最愛歌曲",
"HeaderFavoriteArtists": "最愛藝人",
"HeaderFavoriteEpisodes": "最愛劇集",
"HeaderFavoriteShows": "最愛節目",
"HeaderFavoriteSongs": "最愛歌曲",
"HeaderLiveTV": "電視直播",
"HeaderNextUp": "接下來",
"HomeVideos": "家庭影片",
@@ -135,5 +135,7 @@
"TaskExtractMediaSegments": "掃描媒體片段",
"TaskExtractMediaSegmentsDescription": "從使用媒體片段的擴充功能取得媒體片段。",
"TaskMoveTrickplayImages": "遷移快轉縮圖位置",
"TaskMoveTrickplayImagesDescription": "根據媒體庫的設定遷移快轉縮圖的檔案。"
"TaskMoveTrickplayImagesDescription": "根據媒體庫的設定遷移快轉縮圖的檔案。",
"CleanupUserDataTask": "用戶資料清理工作",
"CleanupUserDataTaskDescription": "從用戶資料中清除已被刪除超過 90 天的媒體的相關資料。"
}
@@ -128,7 +128,8 @@ namespace Emby.Server.Implementations.Localization
}
string name = parts[3];
if (string.IsNullOrWhiteSpace(name))
string displayname = parts[3];
if (string.IsNullOrWhiteSpace(displayname))
{
continue;
}
@@ -138,6 +139,10 @@ namespace Emby.Server.Implementations.Localization
{
continue;
}
else if (twoCharName.Contains('-', StringComparison.OrdinalIgnoreCase))
{
name = twoCharName;
}
string[] threeLetterNames;
if (string.IsNullOrWhiteSpace(parts[1]))
@@ -153,7 +158,7 @@ namespace Emby.Server.Implementations.Localization
iso6392BtoTdict.TryAdd(parts[1], parts[0]);
}
list.Add(new CultureDto(name, name, twoCharName, threeLetterNames));
list.Add(new CultureDto(name, displayname, twoCharName, threeLetterNames));
}
_cultures = list;
@@ -311,8 +311,8 @@ nia|||Nias|nias
nic|||Niger-Kordofanian languages|nigéro-kordofaniennes, langues
niu|||Niuean|niué
nld|dut|nl|Dutch; Flemish|néerlandais; flamand
nno||nn|Norwegian Nynorsk; Nynorsk, Norwegian|norvégien nynorsk; nynorsk, norvégien
nob||nb|Bokmål, Norwegian; Norwegian Bokmål|norvégien bokmål
nno||nn|Norwegian (Nynorsk)|norvégien (nynorsk)
nob||nb|Norwegian (Bokmal)|norvégien (bokmål)
nog|||Nogai|nogaï; nogay
non|||Norse, Old|norrois, vieux
nor||no|Norwegian|norvégien
@@ -373,7 +373,7 @@ sam|||Samaritan Aramaic|samaritain
san||sa|Sanskrit|sanskrit
sas|||Sasak|sasak
sat|||Santali|santal
scc|srp|sr|Serbian|serbe
srp||sr|Serbian|serbe
scn|||Sicilian|sicilien
sco|||Scots|écossais
sel|||Selkup|selkoupe
@@ -391,10 +391,10 @@ slv||sl|Slovenian|slovène
sma|||Southern Sami|sami du Sud
sme||se|Northern Sami|sami du Nord
smi|||Sami languages|sames, langues
smj|||Lule Sami|sami de Lule
smn|||Inari Sami|sami d'Inari
smj|||Sami (Lule)|sami de Lule
smn|||Sami (Inari)|sami d'Inari
smo||sm|Samoan|samoan
sms|||Skolt Sami|sami skolt
sms|||Sami (Skolt)|sami skolt
sna||sn|Shona|shona
snd||sd|Sindhi|sindhi
snk|||Soninke|soninké
@@ -483,9 +483,12 @@ zen|||Zenaga|zenaga
zgh|||Standard Moroccan Tamazight|amazighe standard marocain
zha||za|Zhuang; Chuang|zhuang; chuang
zho|chi|zh|Chinese|chinois
zho|chi|ze|Chinese; Bilingual|chinois
zho|chi|zh-tw|Chinese; Traditional|chinois
zho|chi|zh-hk|Chinese; Hong Kong|chinois
zho|chi|ze|Chinese (Bilingual)|chinois
zho|chi|zh-cn|Chinese (Simplified)|chinois
zho|chi|zh-hans|Chinese (Simplified)|chinois
zho|chi|zh-tw|Chinese (Traditional)|chinois
zho|chi|zh-hant|Chinese (Traditional)|chinois
zho|chi|zh-hk|Chinese (Hong Kong)|chinois
znd|||Zande languages|zandé, langues
zul||zu|Zulu|zoulou
zun|||Zuni|zuni
@@ -76,81 +76,98 @@ public partial class AudioNormalizationTask : IScheduledTask
/// <inheritdoc />
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
{
foreach (var library in _libraryManager.RootFolder.Children)
{
var libraryOptions = _libraryManager.GetLibraryOptions(library);
if (!libraryOptions.EnableLUFSScan)
{
continue;
}
var numComplete = 0;
var libraries = _libraryManager.RootFolder.Children.Where(library => _libraryManager.GetLibraryOptions(library).EnableLUFSScan).ToArray();
double percent = 0;
// Album gain
var albums = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = [BaseItemKind.MusicAlbum],
Parent = library,
Recursive = true
});
foreach (var library in libraries)
{
var albums = _libraryManager.GetItemList(new InternalItemsQuery { IncludeItemTypes = [BaseItemKind.MusicAlbum], Parent = library, Recursive = true });
double nextPercent = numComplete + 1;
nextPercent /= libraries.Length;
nextPercent -= percent;
// Split the progress for this single library into two halves: album gain and track gain.
// The first half will be for album gain, the second half for track gain.
nextPercent /= 2;
var albumComplete = 0;
foreach (var a in albums)
{
if (a.NormalizationGain.HasValue || a.LUFS.HasValue)
if (!a.NormalizationGain.HasValue && !a.LUFS.HasValue)
{
continue;
// Album gain
var albumTracks = ((MusicAlbum)a).Tracks.Where(x => x.IsFileProtocol).ToList();
// Skip albums that don't have multiple tracks, album gain is useless here
if (albumTracks.Count > 1)
{
_logger.LogInformation("Calculating LUFS for album: {Album} with id: {Id}", a.Name, a.Id);
var tempDir = _applicationPaths.TempDirectory;
Directory.CreateDirectory(tempDir);
var tempFile = Path.Join(tempDir, a.Id + ".concat");
var inputLines = albumTracks.Select(x => string.Format(CultureInfo.InvariantCulture, "file '{0}'", x.Path.Replace("'", @"'\''", StringComparison.Ordinal)));
await File.WriteAllLinesAsync(tempFile, inputLines, cancellationToken).ConfigureAwait(false);
try
{
a.LUFS = await CalculateLUFSAsync(
string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile),
OperatingSystem.IsWindows(), // Wait for process to exit on Windows before we try deleting the concat file
cancellationToken).ConfigureAwait(false);
}
finally
{
File.Delete(tempFile);
}
}
}
// Skip albums that don't have multiple tracks, album gain is useless here
var albumTracks = ((MusicAlbum)a).Tracks.Where(x => x.IsFileProtocol).ToList();
if (albumTracks.Count <= 1)
{
continue;
}
// Update sub-progress for album gain
albumComplete++;
double albumPercent = albumComplete;
albumPercent /= albums.Count;
_logger.LogInformation("Calculating LUFS for album: {Album} with id: {Id}", a.Name, a.Id);
var tempDir = _applicationPaths.TempDirectory;
Directory.CreateDirectory(tempDir);
var tempFile = Path.Join(tempDir, a.Id + ".concat");
var inputLines = albumTracks.Select(x => string.Format(CultureInfo.InvariantCulture, "file '{0}'", x.Path.Replace("'", @"'\''", StringComparison.Ordinal)));
await File.WriteAllLinesAsync(tempFile, inputLines, cancellationToken).ConfigureAwait(false);
try
{
a.LUFS = await CalculateLUFSAsync(
string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile),
OperatingSystem.IsWindows(), // Wait for process to exit on Windows before we try deleting the concat file
cancellationToken).ConfigureAwait(false);
}
finally
{
File.Delete(tempFile);
}
progress.Report(100 * (percent + (albumPercent * nextPercent)));
}
// Update progress to start at the track gain percent calculation
percent += nextPercent;
_itemRepository.SaveItems(albums, cancellationToken);
// Track gain
var tracks = _libraryManager.GetItemList(new InternalItemsQuery
{
MediaTypes = [MediaType.Audio],
IncludeItemTypes = [BaseItemKind.Audio],
Parent = library,
Recursive = true
});
var tracks = _libraryManager.GetItemList(new InternalItemsQuery { MediaTypes = [MediaType.Audio], IncludeItemTypes = [BaseItemKind.Audio], Parent = library, Recursive = true });
var tracksComplete = 0;
foreach (var t in tracks)
{
if (t.NormalizationGain.HasValue || t.LUFS.HasValue || !t.IsFileProtocol)
if (!t.NormalizationGain.HasValue && !t.LUFS.HasValue && t.IsFileProtocol)
{
continue;
t.LUFS = await CalculateLUFSAsync(
string.Format(CultureInfo.InvariantCulture, "-i \"{0}\"", t.Path.Replace("\"", "\\\"", StringComparison.Ordinal)),
false,
cancellationToken).ConfigureAwait(false);
}
t.LUFS = await CalculateLUFSAsync(
string.Format(CultureInfo.InvariantCulture, "-i \"{0}\"", t.Path.Replace("\"", "\\\"", StringComparison.Ordinal)),
false,
cancellationToken).ConfigureAwait(false);
// Update sub-progress for track gain
tracksComplete++;
double trackPercent = tracksComplete;
trackPercent /= tracks.Count;
progress.Report(100 * (percent + (trackPercent * nextPercent)));
}
_itemRepository.SaveItems(tracks, cancellationToken);
// Update progress
numComplete++;
percent = numComplete;
percent /= libraries.Length;
progress.Report(100 * percent);
}
progress.Report(100.0);
}
/// <inheritdoc />
@@ -5,6 +5,8 @@ using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
using MediaBrowser.Controller.Session;
@@ -44,6 +46,7 @@ namespace Emby.Server.Implementations.Session
private readonly Lock _webSocketsLock = new();
private readonly ISessionManager _sessionManager;
private readonly IUserManager _userManager;
private readonly ILogger<SessionWebSocketListener> _logger;
private readonly ILoggerFactory _loggerFactory;
@@ -57,14 +60,17 @@ namespace Emby.Server.Implementations.Session
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="sessionManager">The session manager.</param>
/// <param name="userManager">The user manager.</param>
/// <param name="loggerFactory">The logger factory.</param>
public SessionWebSocketListener(
ILogger<SessionWebSocketListener> logger,
ISessionManager sessionManager,
IUserManager userManager,
ILoggerFactory loggerFactory)
{
_logger = logger;
_sessionManager = sessionManager;
_userManager = userManager;
_loggerFactory = loggerFactory;
_keepAlive = new System.Timers.Timer(TimeSpan.FromSeconds(WebSocketLostTimeout * IntervalFactor))
{
@@ -107,33 +113,9 @@ namespace Emby.Server.Implementations.Session
/// <inheritdoc />
public async Task ProcessWebSocketConnectedAsync(IWebSocketConnection connection, HttpContext httpContext)
{
var session = await GetSession(httpContext, connection.RemoteEndPoint?.ToString()).ConfigureAwait(false);
if (session is not null)
{
EnsureController(session, connection);
await KeepAliveWebSocket(connection).ConfigureAwait(false);
}
else
{
_logger.LogWarning("Unable to determine session based on query string: {0}", httpContext.Request.QueryString);
}
}
private async Task<SessionInfo?> GetSession(HttpContext httpContext, string? remoteEndpoint)
{
if (!httpContext.User.Identity?.IsAuthenticated ?? false)
{
return null;
}
var deviceId = httpContext.User.GetDeviceId();
if (httpContext.Request.Query.TryGetValue("deviceId", out var queryDeviceId))
{
deviceId = queryDeviceId;
}
return await _sessionManager.GetSessionByAuthenticationToken(httpContext.User.GetToken(), deviceId, remoteEndpoint)
.ConfigureAwait(false);
var session = await RequestHelpers.GetSession(_sessionManager, _userManager, httpContext).ConfigureAwait(false);
EnsureController(session, connection);
await KeepAliveWebSocket(connection).ConfigureAwait(false);
}
private void EnsureController(SessionInfo session, IWebSocketConnection connection)
@@ -46,6 +46,7 @@ public class DynamicHlsController : BaseJellyfinApiController
private readonly Version _minFFmpegFlacInMp4 = new Version(6, 0);
private readonly Version _minFFmpegX265BframeInFmp4 = new Version(7, 0, 1);
private readonly Version _minFFmpegHlsSegmentOptions = new Version(5, 0);
private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager;
@@ -1606,6 +1607,7 @@ public class DynamicHlsController : BaseJellyfinApiController
var segmentFormat = string.Empty;
var segmentContainer = outputExtension.TrimStart('.');
var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions, segmentContainer);
var hlsArguments = $"-hls_playlist_type {(isEventPlaylist ? "event" : "vod")} -hls_list_size 0";
if (string.Equals(segmentContainer, "ts", StringComparison.OrdinalIgnoreCase))
{
@@ -1621,6 +1623,11 @@ public class DynamicHlsController : BaseJellyfinApiController
false => " -hls_fmp4_init_filename \"" + outputFileNameWithoutExtension + "-1" + outputExtension + "\""
};
var useLegacySegmentOption = _mediaEncoder.EncoderVersion < _minFFmpegHlsSegmentOptions;
// fMP4 needs this flag to write the audio packet DTS/PTS including the initial delay into MOOF::TRAF::TFDT
hlsArguments += $" {(useLegacySegmentOption ? "-hls_ts_options" : "-hls_segment_options")} movflags=+frag_discont";
segmentFormat = "fmp4" + outputFmp4HeaderArg;
}
else
@@ -1642,8 +1649,6 @@ public class DynamicHlsController : BaseJellyfinApiController
Path.GetFileNameWithoutExtension(outputPath));
}
var hlsArguments = $"-hls_playlist_type {(isEventPlaylist ? "event" : "vod")} -hls_list_size 0";
return string.Format(
CultureInfo.InvariantCulture,
"{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -hls_segment_type {8} -start_number {9}{10} -hls_segment_filename \"{11}\" {12} -y \"{13}\"",
@@ -158,7 +158,10 @@ public class ItemUpdateController : BaseJellyfinApiController
ParentalRatingOptions = _localizationManager.GetParentalRatings().ToList(),
ExternalIdInfos = _providerManager.GetExternalIdInfos(item).ToArray(),
Countries = _localizationManager.GetCountries().ToArray(),
Cultures = _localizationManager.GetCultures().ToArray()
Cultures = _localizationManager.GetCultures()
.DistinctBy(c => c.DisplayName, StringComparer.OrdinalIgnoreCase)
.OrderBy(c => c.DisplayName)
.ToArray()
};
if (!item.IsVirtualItem
@@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using MediaBrowser.Common.Api;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
@@ -34,7 +36,14 @@ public class LocalizationController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<CultureDto>> GetCultures()
{
return Ok(_localization.GetCultures());
var allCultures = _localization.GetCultures();
var distinctCultures = allCultures
.DistinctBy(c => c.DisplayName, StringComparer.OrdinalIgnoreCase)
.OrderBy(c => c.DisplayName)
.AsEnumerable();
return Ok(distinctCultures);
}
/// <summary>
@@ -1,3 +1,4 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
@@ -131,16 +132,16 @@ public class StartupController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> UpdateStartupUser([FromBody] StartupUserDto startupUserDto)
{
ArgumentNullException.ThrowIfNull(startupUserDto.Name);
_userManager.ThrowIfInvalidUsername(startupUserDto.Name);
var user = _userManager.Users.First();
if (string.IsNullOrWhiteSpace(startupUserDto.Password))
{
return BadRequest("Password must not be empty");
}
if (startupUserDto.Name is not null)
{
user.Username = startupUserDto.Name;
}
user.Username = startupUserDto.Name;
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
@@ -32,17 +32,67 @@ public static class FileStreamResponseHelpers
HttpContext httpContext,
CancellationToken cancellationToken = default)
{
using var requestMessage = new HttpRequestMessage(HttpMethod.Get, new Uri(state.MediaPath));
// Forward User-Agent if provided
if (state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent))
{
httpClient.DefaultRequestHeaders.Add(HeaderNames.UserAgent, useragent);
// Clear default and add specific one if exists, otherwise HttpClient default might be used
requestMessage.Headers.UserAgent.Clear();
requestMessage.Headers.TryAddWithoutValidation(HeaderNames.UserAgent, useragent);
}
// Can't dispose the response as it's required up the call chain.
var response = await httpClient.GetAsync(new Uri(state.MediaPath), HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
var contentType = response.Content.Headers.ContentType?.ToString() ?? MediaTypeNames.Text.Plain;
// Forward Range header if present in the client request
if (httpContext.Request.Headers.TryGetValue(HeaderNames.Range, out var rangeValue))
{
var rangeString = rangeValue.ToString();
if (!string.IsNullOrEmpty(rangeString))
{
requestMessage.Headers.Range = System.Net.Http.Headers.RangeHeaderValue.Parse(rangeString);
}
}
httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none";
// Send the request to the upstream server
// Use ResponseHeadersRead to avoid downloading the whole content immediately
var response = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
// Check if the upstream server supports range requests and acted upon our Range header
bool upstreamSupportsRange = response.StatusCode == System.Net.HttpStatusCode.PartialContent;
string acceptRangesValue = "none";
if (response.Headers.TryGetValues(HeaderNames.AcceptRanges, out var acceptRangesHeaders))
{
// Prefer upstream server's Accept-Ranges header if available
acceptRangesValue = string.Join(", ", acceptRangesHeaders);
upstreamSupportsRange |= acceptRangesValue.Contains("bytes", StringComparison.OrdinalIgnoreCase);
}
else if (upstreamSupportsRange) // If we got 206 but no Accept-Ranges header, assume bytes
{
acceptRangesValue = "bytes";
}
// Set Accept-Ranges header for the client based on upstream support
httpContext.Response.Headers[HeaderNames.AcceptRanges] = acceptRangesValue;
// Set Content-Range header if upstream provided it (implies partial content)
if (response.Content.Headers.ContentRange is not null)
{
httpContext.Response.Headers[HeaderNames.ContentRange] = response.Content.Headers.ContentRange.ToString();
}
// Set Content-Length header. For partial content, this is the length of the partial segment.
if (response.Content.Headers.ContentLength.HasValue)
{
httpContext.Response.ContentLength = response.Content.Headers.ContentLength.Value;
}
// Set Content-Type header
var contentType = response.Content.Headers.ContentType?.ToString() ?? MediaTypeNames.Application.Octet; // Use a more generic default
// Set the status code for the client response (e.g., 200 OK or 206 Partial Content)
httpContext.Response.StatusCode = (int)response.StatusCode;
// Return the stream from the upstream server
// IMPORTANT: Do not dispose the response stream here, FileStreamResult will handle it.
return new FileStreamResult(await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false), contentType);
}
+10 -1
View File
@@ -111,7 +111,16 @@ public static class RequestHelpers
return user.EnableUserPreferenceAccess;
}
internal static async Task<SessionInfo> GetSession(ISessionManager sessionManager, IUserManager userManager, HttpContext httpContext, Guid? userId = null)
/// <summary>
/// Get the session based on http request.
/// </summary>
/// <param name="sessionManager">The session manager.</param>
/// <param name="userManager">The user manager.</param>
/// <param name="httpContext">The http context.</param>
/// <param name="userId">The optional userid.</param>
/// <returns>The session.</returns>
/// <exception cref="ResourceNotFoundException">Session not found.</exception>
public static async Task<SessionInfo> GetSession(ISessionManager sessionManager, IUserManager userManager, HttpContext httpContext, Guid? userId = null)
{
userId ??= httpContext.User.GetUserId();
User? user = null;
@@ -139,7 +139,7 @@ public static class ServiceCollectionExtensions
serviceCollection.AddPooledDbContextFactory<JellyfinDbContext>((serviceProvider, opt) =>
{
var provider = serviceProvider.GetRequiredService<IJellyfinDatabaseProvider>();
provider.Initialise(opt);
provider.Initialise(opt, efCoreConfiguration);
var lockingBehavior = serviceProvider.GetRequiredService<IEntityFrameworkCoreLockingBehavior>();
lockingBehavior.Initialise(opt);
});
@@ -39,7 +39,7 @@ public class BackupService : IBackupService
ReferenceHandler = ReferenceHandler.IgnoreCycles,
};
private readonly Version _backupEngineVersion = Version.Parse("0.1.0");
private readonly Version _backupEngineVersion = Version.Parse("0.2.0");
/// <summary>
/// Initializes a new instance of the <see cref="BackupService"/> class.
@@ -120,26 +120,29 @@ public class BackupService : IBackupService
void CopyDirectory(string source, string target)
{
source = Path.GetFullPath(source);
Directory.CreateDirectory(source);
var fullSourcePath = NormalizePathSeparator(Path.GetFullPath(source) + Path.DirectorySeparatorChar);
var fullTargetRoot = Path.GetFullPath(target) + Path.DirectorySeparatorChar;
foreach (var item in zipArchive.Entries)
{
var sanitizedSourcePath = Path.GetFullPath(item.FullName.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar);
if (!sanitizedSourcePath.StartsWith(target, StringComparison.Ordinal))
var sourcePath = NormalizePathSeparator(Path.GetFullPath(item.FullName));
var targetPath = Path.GetFullPath(Path.Combine(target, Path.GetRelativePath(source, item.FullName)));
if (!sourcePath.StartsWith(fullSourcePath, StringComparison.Ordinal)
|| !targetPath.StartsWith(fullTargetRoot, StringComparison.Ordinal))
{
continue;
}
var targetPath = Path.Combine(source, sanitizedSourcePath[target.Length..].Trim('/'));
_logger.LogInformation("Restore and override {File}", targetPath);
item.ExtractToFile(targetPath);
Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!);
item.ExtractToFile(targetPath, overwrite: true);
}
}
CopyDirectory(_applicationPaths.ConfigurationDirectoryPath, "Config/");
CopyDirectory(_applicationPaths.DataPath, "Data/");
CopyDirectory(_applicationPaths.RootFolderPath, "Root/");
CopyDirectory("Config", _applicationPaths.ConfigurationDirectoryPath);
CopyDirectory("Data", _applicationPaths.DataPath);
CopyDirectory("Root", _applicationPaths.RootFolderPath);
if (manifest.Options.Database)
{
@@ -148,7 +151,7 @@ public class BackupService : IBackupService
await using (dbContext.ConfigureAwait(false))
{
// restore migration history manually
var historyEntry = zipArchive.GetEntry($"Database\\{nameof(HistoryRow)}.json");
var historyEntry = zipArchive.GetEntry(NormalizePathSeparator(Path.Combine("Database", $"{nameof(HistoryRow)}.json")));
if (historyEntry is null)
{
_logger.LogInformation("No backup of the history table in archive. This is required for Jellyfin operation");
@@ -165,6 +168,13 @@ public class BackupService : IBackupService
var historyRepository = dbContext.GetService<IHistoryRepository>();
await historyRepository.CreateIfNotExistsAsync().ConfigureAwait(false);
foreach (var item in await historyRepository.GetAppliedMigrationsAsync(CancellationToken.None).ConfigureAwait(false))
{
var insertScript = historyRepository.GetDeleteScript(item.MigrationId);
await dbContext.Database.ExecuteSqlRawAsync(insertScript).ConfigureAwait(false);
}
foreach (var item in historyEntries)
{
var insertScript = historyRepository.GetInsertScript(item);
@@ -186,7 +196,7 @@ public class BackupService : IBackupService
{
_logger.LogInformation("Read backup of {Table}", entityType.Type.Name);
var zipEntry = zipArchive.GetEntry($"Database\\{entityType.Type.Name}.json");
var zipEntry = zipArchive.GetEntry(NormalizePathSeparator(Path.Combine("Database", $"{entityType.Type.Name}.json")));
if (zipEntry is null)
{
_logger.LogInformation("No backup of expected table {Table} is present in backup. Continue anyway.", entityType.Type.Name);
@@ -198,7 +208,7 @@ public class BackupService : IBackupService
{
_logger.LogInformation("Restore backup of {Table}", entityType.Type.Name);
var records = 0;
await foreach (var item in JsonSerializer.DeserializeAsyncEnumerable<JsonObject>(zipEntryStream, _serializerSettings).ConfigureAwait(false)!)
await foreach (var item in JsonSerializer.DeserializeAsyncEnumerable<JsonObject>(zipEntryStream, _serializerSettings).ConfigureAwait(false))
{
var entity = item.Deserialize(entityType.Type.PropertyType.GetGenericArguments()[0]);
if (entity is null)
@@ -281,7 +291,7 @@ public class BackupService : IBackupService
await using (dbContext.ConfigureAwait(false))
{
dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
static IAsyncEnumerable<object> GetValues(IQueryable dbSet, Type type)
static IAsyncEnumerable<object> GetValues(IQueryable dbSet)
{
var method = dbSet.GetType().GetMethod(nameof(DbSet<object>.AsAsyncEnumerable))!;
var enumerable = method.Invoke(dbSet, null)!;
@@ -296,8 +306,8 @@ public class BackupService : IBackupService
.. typeof(JellyfinDbContext)
.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
.Where(e => e.PropertyType.IsAssignableTo(typeof(IQueryable)))
.Select(e => (Type: e.PropertyType, dbContext.Model.FindEntityType(e.PropertyType.GetGenericArguments()[0])!.GetSchemaQualifiedTableName()!, ValueFactory: new Func<IAsyncEnumerable<object>>(() => GetValues((IQueryable)e.GetValue(dbContext)!, e.PropertyType)))),
(Type: typeof(HistoryRow), SourceName: nameof(HistoryRow), ValueFactory: new Func<IAsyncEnumerable<object>>(() => migrations.ToAsyncEnumerable()))
.Select(e => (Type: e.PropertyType, dbContext.Model.FindEntityType(e.PropertyType.GetGenericArguments()[0])!.GetSchemaQualifiedTableName()!, ValueFactory: new Func<IAsyncEnumerable<object>>(() => GetValues((IQueryable)e.GetValue(dbContext)!)))),
(Type: typeof(HistoryRow), SourceName: nameof(HistoryRow), ValueFactory: () => migrations.ToAsyncEnumerable())
];
manifest.DatabaseTables = entityTypes.Select(e => e.Type.Name).ToArray();
var transaction = await dbContext.Database.BeginTransactionAsync().ConfigureAwait(false);
@@ -309,7 +319,7 @@ public class BackupService : IBackupService
foreach (var entityType in entityTypes)
{
_logger.LogInformation("Begin backup of entity {Table}", entityType.SourceName);
var zipEntry = zipArchive.CreateEntry($"Database\\{entityType.SourceName}.json");
var zipEntry = zipArchive.CreateEntry(NormalizePathSeparator(Path.Combine("Database", $"{entityType.SourceName}.json")));
var entities = 0;
var zipEntryStream = zipEntry.Open();
await using (zipEntryStream.ConfigureAwait(false))
@@ -347,7 +357,7 @@ public class BackupService : IBackupService
foreach (var item in Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.xml", SearchOption.TopDirectoryOnly)
.Union(Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.json", SearchOption.TopDirectoryOnly)))
{
zipArchive.CreateEntryFromFile(item, Path.Combine("Config", Path.GetFileName(item)));
zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine("Config", Path.GetFileName(item))));
}
void CopyDirectory(string source, string target, string filter = "*")
@@ -361,7 +371,7 @@ public class BackupService : IBackupService
foreach (var item in Directory.EnumerateFiles(source, filter, SearchOption.AllDirectories))
{
zipArchive.CreateEntryFromFile(item, Path.Combine(target, item[..source.Length].Trim('\\')));
zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine(target, Path.GetRelativePath(source, item))));
}
}
@@ -509,4 +519,14 @@ public class BackupService : IBackupService
Database = options.Database
};
}
/// <summary>
/// Windows is able to handle '/' as a path seperator in zip files
/// but linux isn't able to handle '\' as a path seperator in zip files,
/// So normalize to '/'.
/// </summary>
/// <param name="path">The path to normalize.</param>
/// <returns>The normalized path. </returns>
private static string NormalizePathSeparator(string path)
=> path.Replace('\\', '/');
}
@@ -110,6 +110,20 @@ public sealed class BaseItemRepository
using var transaction = context.Database.BeginTransaction();
var date = (DateTime?)DateTime.UtcNow;
// Remove any UserData entries for the placeholder item that would conflict with the UserData
// being detached from the item being deleted. This is necessary because, during an update,
// UserData may be reattached to a new entry, but some entries can be left behind.
// Ensures there are no duplicate UserId/CustomDataKey combinations for the placeholder.
context.UserData
.Join(
context.UserData.Where(e => e.ItemId == id),
placeholder => new { placeholder.UserId, placeholder.CustomDataKey },
userData => new { userData.UserId, userData.CustomDataKey },
(placeholder, userData) => placeholder)
.Where(e => e.ItemId == PlaceholderId)
.ExecuteDelete();
// Detach all user watch data
context.UserData.Where(e => e.ItemId == id)
.ExecuteUpdate(e => e
@@ -468,6 +482,13 @@ public sealed class BaseItemRepository
var images = item.ImageInfos.Select(e => Map(item.Id, e));
using var context = _dbProvider.CreateDbContext();
if (!context.BaseItems.Any(bi => bi.Id == item.Id))
{
_logger.LogWarning("Unable to save ImageInfo for non existing BaseItem");
return;
}
context.BaseItemImageInfos.Where(e => e.ItemId == item.Id).ExecuteDelete();
context.BaseItemImageInfos.AddRange(images);
context.SaveChanges();
@@ -2239,8 +2260,8 @@ public sealed class BaseItemRepository
if (filter.ExcludeInheritedTags.Length > 0)
{
baseQuery = baseQuery
.Where(e => !e.ItemValues!.Where(w => w.ItemValue.Type == ItemValueType.InheritedTags)
.Any(f => filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue)));
.Where(e => !e.ItemValues!.Where(w => w.ItemValue.Type == ItemValueType.InheritedTags || w.ItemValue.Type == ItemValueType.Tags)
.Any(f => filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue)));
}
if (filter.IncludeInheritedTags.Length > 0)
@@ -2250,10 +2271,10 @@ public sealed class BaseItemRepository
if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Episode)
{
baseQuery = baseQuery
.Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags)
.Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags || f.ItemValue.Type == ItemValueType.Tags)
.Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))
||
(e.ParentId.HasValue && context.ItemValuesMap.Where(w => w.ItemId == e.ParentId.Value)!.Where(w => w.ItemValue.Type == ItemValueType.InheritedTags)
(e.ParentId.HasValue && context.ItemValuesMap.Where(w => w.ItemId == e.ParentId.Value && (w.ItemValue.Type == ItemValueType.InheritedTags || w.ItemValue.Type == ItemValueType.Tags))
.Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))));
}
@@ -2261,17 +2282,16 @@ public sealed class BaseItemRepository
else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist)
{
baseQuery = baseQuery
.Where(e =>
e.Parents!
.Any(f =>
f.ParentItem.ItemValues!.Any(w => w.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(w.ItemValue.CleanValue))
|| e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\"")));
.Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags || f.ItemValue.Type == ItemValueType.Tags)
.Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))
|| e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\""));
// d ^^ this is stupid it hate this.
}
else
{
baseQuery = baseQuery
.Where(e => e.Parents!.Any(f => f.ParentItem.ItemValues!.Any(w => w.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(w.ItemValue.CleanValue))));
.Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags || f.ItemValue.Type == ItemValueType.Tags)
.Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue)));
}
}
@@ -25,8 +25,16 @@ public class MediaAttachmentRepository(IDbContextFactory<JellyfinDbContext> dbPr
{
using var context = dbProvider.CreateDbContext();
using var transaction = context.Database.BeginTransaction();
// Users may replace a media with a version that includes attachments to one without them.
// So when saving attachments is triggered by a library scan, we always unconditionally
// clear the old ones, and then add the new ones if given.
context.AttachmentStreamInfos.Where(e => e.ItemId.Equals(id)).ExecuteDelete();
context.AttachmentStreamInfos.AddRange(attachments.Select(e => Map(e, id)));
if (attachments.Any())
{
context.AttachmentStreamInfos.AddRange(attachments.Select(e => Map(e, id)));
}
context.SaveChanges();
transaction.Commit();
}
@@ -744,7 +744,8 @@ namespace Jellyfin.Server.Implementations.Users
_users[user.Id] = user;
}
internal static void ThrowIfInvalidUsername(string name)
/// <inheritdoc/>
public void ThrowIfInvalidUsername(string name)
{
if (!string.IsNullOrWhiteSpace(name) && ValidUsernameRegex().IsMatch(name))
{
@@ -17,6 +17,7 @@ using MediaBrowser.Model.Configuration;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Migrations;
@@ -105,6 +106,13 @@ internal class JellyfinMigrationService
var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
var databaseCreator = dbContext.Database.GetService<IDatabaseCreator>() as IRelationalDatabaseCreator
?? throw new InvalidOperationException("Jellyfin does only support relational databases.");
if (!await databaseCreator.ExistsAsync().ConfigureAwait(false))
{
await databaseCreator.CreateAsync().ConfigureAwait(false);
}
var historyRepository = dbContext.GetService<IHistoryRepository>();
await historyRepository.CreateIfNotExistsAsync().ConfigureAwait(false);
@@ -90,6 +90,9 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
operation.JellyfinDbContext.AncestorIds.ExecuteDelete();
}
// notify the other migration to just silently abort because the fix has been applied here already.
ReseedFolderFlag.RerunGuardFlag = true;
var legacyBaseItemWithUserKeys = new Dictionary<string, BaseItemEntity>();
connection.Open();
@@ -105,7 +108,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
Audio, ExternalServiceId, IsInMixedFolder, DateLastSaved, LockedFields, Studios, Tags, TrailerTypes, OriginalTitle, PrimaryVersionId,
DateLastMediaAdded, Album, LUFS, NormalizationGain, CriticRating, IsVirtualItem, SeriesName, UserDataKey, SeasonName, SeasonId, SeriesId,
PresentationUniqueKey, InheritedParentalRatingValue, ExternalSeriesId, Tagline, ProviderIds, Images, ProductionLocations, ExtraIds, TotalBitrate,
ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId, MediaType, SortName, CleanName, UnratedType FROM TypedBaseItems
ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId, MediaType, SortName, CleanName, UnratedType, IsFolder FROM TypedBaseItems
""";
using (new TrackedMigrationStep("Loading TypedBaseItems", _logger))
{
@@ -1167,6 +1170,11 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
entity.UnratedType = unratedType;
}
if (reader.TryGetBoolean(index++, out var isFolder))
{
entity.IsFolder = isFolder;
}
var baseItem = BaseItemRepository.DeserializeBaseItem(entity, _logger, null, false);
var dataKeys = baseItem.GetUserDataKeys();
userDataKeys.AddRange(dataKeys);
@@ -8,6 +8,7 @@ using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.Data;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Server.Implementations.Item;
using Jellyfin.Server.ServerSetupApp;
using MediaBrowser.Controller;
@@ -78,6 +79,8 @@ internal class MigrateLibraryUserData : IAsyncMigrationRoutine
WHERE NOT EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.UserDataKey = UserDatas.key)
""");
var importedUserData = new Dictionary<Guid, List<UserData>>();
foreach (var entity in queryResult)
{
var userData = MigrateLibraryDb.GetUserData(users, entity, userIdBlacklist, _logger);
@@ -95,9 +98,22 @@ internal class MigrateLibraryUserData : IAsyncMigrationRoutine
continue;
}
var ogId = userData.ItemId;
userData.ItemId = BaseItemRepository.PlaceholderId;
userData.RetentionDate = retentionDate;
dbContext.UserData.Add(userData);
if (!importedUserData.TryGetValue(ogId, out var importUserData))
{
importUserData = [];
importedUserData[ogId] = importUserData;
}
importUserData.Add(userData);
}
foreach (var item in importedUserData)
{
await dbContext.UserData.Where(e => e.ItemId == item.Key).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
dbContext.UserData.AddRange(item.Value.DistinctBy(e => e.CustomDataKey)); // old userdata can have fucked up duplicates
}
_logger.LogInformation("Try saving {NewSaved} UserData entries.", dbContext.UserData.Local.Count);
@@ -0,0 +1,74 @@
#pragma warning disable RS0030 // Do not use banned APIs
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.Data;
using Jellyfin.Database.Implementations;
using Jellyfin.Server.ServerSetupApp;
using MediaBrowser.Controller;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Migrations.Routines;
[JellyfinMigration("2025-07-30T21:50:00", nameof(ReseedFolderFlag))]
[JellyfinMigrationBackup(JellyfinDb = true)]
internal class ReseedFolderFlag : IAsyncMigrationRoutine
{
private const string DbFilename = "library.db.old";
private readonly IStartupLogger _logger;
private readonly IServerApplicationPaths _paths;
private readonly IDbContextFactory<JellyfinDbContext> _provider;
public ReseedFolderFlag(
IStartupLogger<MigrateLibraryDb> startupLogger,
IDbContextFactory<JellyfinDbContext> provider,
IServerApplicationPaths paths)
{
_logger = startupLogger;
_provider = provider;
_paths = paths;
}
internal static bool RerunGuardFlag { get; set; } = false;
public async Task PerformAsync(CancellationToken cancellationToken)
{
if (RerunGuardFlag)
{
_logger.LogInformation("Migration is skipped because it does not apply.");
return;
}
_logger.LogInformation("Migrating the IsFolder flag from library.db.old may take a while, do not stop Jellyfin.");
var dataPath = _paths.DataPath;
var libraryDbPath = Path.Combine(dataPath, DbFilename);
if (!File.Exists(libraryDbPath))
{
_logger.LogError("Cannot migrate IsFolder flag from {LibraryDb} as it does not exist. This migration expects the MigrateLibraryDb to run first.", libraryDbPath);
return;
}
var dbContext = await _provider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
using var connection = new SqliteConnection($"Filename={libraryDbPath};Mode=ReadOnly");
var queryResult = connection.Query(
"""
SELECT guid FROM TypedBaseItems
WHERE IsFolder = true
""")
.Select(entity => entity.GetGuid(0))
.ToList();
_logger.LogInformation("Migrating the IsFolder flag for {Count} items.", queryResult.Count);
foreach (var id in queryResult)
{
await dbContext.BaseItems.Where(e => e.Id == id).ExecuteUpdateAsync(e => e.SetProperty(f => f.IsFolder, true), cancellationToken).ConfigureAwait(false);
}
}
}
}
+1 -3
View File
@@ -1430,9 +1430,7 @@ namespace MediaBrowser.Controller.Entities
var info = FileSystem.GetFileSystemInfo(Path);
return info.Exists
? info.LastWriteTimeUtc != DateModified
: false;
return info.Exists && this.HasChanged(info.LastWriteTimeUtc);
}
public virtual List<string> GetUserDataKeys()
@@ -114,5 +114,19 @@ namespace MediaBrowser.Controller.Entities
source.DeepCopy(dest);
return dest;
}
/// <summary>
/// Determines if the item has changed.
/// </summary>
/// <param name="source">The source object.</param>
/// <param name="asOf">The timestamp to detect changes as of.</param>
/// <typeparam name="T">Source type.</typeparam>
/// <returns>Whether the item has changed.</returns>
public static bool HasChanged<T>(this T source, DateTime asOf)
where T : BaseItem
{
ArgumentNullException.ThrowIfNull(source);
return source.DateModified.Subtract(asOf).Duration().TotalSeconds > 1;
}
}
}
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Diacritics.Extensions;
using Jellyfin.Data;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
@@ -373,8 +374,15 @@ namespace MediaBrowser.Controller.Entities
.Where(i => i != other)
.Select(e => Enum.Parse<UnratedItem>(e, true)).ToArray();
ExcludeInheritedTags = user.GetPreference(PreferenceKind.BlockedTags);
IncludeInheritedTags = user.GetPreference(PreferenceKind.AllowedTags);
ExcludeInheritedTags = user.GetPreference(PreferenceKind.BlockedTags)
.Where(tag => !string.IsNullOrWhiteSpace(tag))
.Select(tag => tag.RemoveDiacritics().ToLowerInvariant())
.ToArray();
IncludeInheritedTags = user.GetPreference(PreferenceKind.AllowedTags)
.Where(tag => !string.IsNullOrWhiteSpace(tag))
.Select(tag => tag.RemoveDiacritics().ToLowerInvariant())
.ToArray();
User = user;
}
@@ -33,6 +33,12 @@ namespace MediaBrowser.Controller.Library
/// <value>The users ids.</value>
IEnumerable<Guid> UsersIds { get; }
/// <summary>
/// Checks if the user's username is valid.
/// </summary>
/// <param name="name">The user's username.</param>
void ThrowIfInvalidUsername(string name);
/// <summary>
/// Initializes the user manager and ensures that a user exists.
/// </summary>
@@ -827,7 +827,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
/// <inheritdoc />
public Task<string> ExtractVideoImagesOnIntervalAccelerated(
public async Task<string> ExtractVideoImagesOnIntervalAccelerated(
string inputFile,
string container,
MediaSourceInfo mediaSource,
@@ -918,18 +918,34 @@ namespace MediaBrowser.MediaEncoding.Encoder
inputArg = "-hwaccel_flags +low_priority " + inputArg;
}
if (enableKeyFrameOnlyExtraction)
{
inputArg = "-skip_frame nokey " + inputArg;
}
var filterParam = encodingHelper.GetVideoProcessingFilterParam(jobState, options, vidEncoder).Trim();
if (string.IsNullOrWhiteSpace(filterParam))
{
throw new InvalidOperationException("EncodingHelper returned empty or invalid filter parameters.");
}
return ExtractVideoImagesOnIntervalInternal(inputArg, filterParam, vidEncoder, threads, qualityScale, priority, cancellationToken);
try
{
return await ExtractVideoImagesOnIntervalInternal(
(enableKeyFrameOnlyExtraction ? "-skip_frame nokey " : string.Empty) + inputArg,
filterParam,
vidEncoder,
threads,
qualityScale,
priority,
cancellationToken).ConfigureAwait(false);
}
catch (FfmpegException ex)
{
if (!enableKeyFrameOnlyExtraction)
{
throw;
}
_logger.LogWarning(ex, "I-frame trickplay extraction failed, will attempt standard way. Input: {InputFile}", inputFile);
}
return await ExtractVideoImagesOnIntervalInternal(inputArg, filterParam, vidEncoder, threads, qualityScale, priority, cancellationToken).ConfigureAwait(false);
}
private async Task<string> ExtractVideoImagesOnIntervalInternal(
@@ -1071,11 +1087,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
}
var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
if (exitCode == -1)
if (!ranToCompletion || processWrapper.ExitCode != 0)
{
_logger.LogError("ffmpeg image extraction failed for {ProcessDescription}", processDescription);
// Cleanup temp folder here, because the targetDirectory is not returned and the cleanup for failed ffmpeg process is not possible for caller.
// Ideally the ffmpeg should not write any files if it fails, but it seems like it is not guaranteed.
try
@@ -489,10 +489,15 @@ namespace MediaBrowser.MediaEncoding.Subtitles
try
{
var subtitleStreams = mediaSource.MediaStreams
.Where(stream => stream is { IsExtractableSubtitleStream: true, SupportsExternalStream: true, IsExternal: false });
.Where(stream => stream is { IsExtractableSubtitleStream: true, SupportsExternalStream: true });
foreach (var subtitleStream in subtitleStreams)
{
if (subtitleStream.IsExternal && !subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))
{
continue;
}
var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitleFileExtension(subtitleStream));
var releaser = await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false);
@@ -510,6 +515,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
if (extractableStreams.Count > 0)
{
await ExtractAllExtractableSubtitlesInternal(mediaSource, extractableStreams, cancellationToken).ConfigureAwait(false);
await ExtractAllExtractableSubtitlesMKS(mediaSource, extractableStreams, cancellationToken).ConfigureAwait(false);
}
}
catch (Exception ex)
@@ -522,6 +528,72 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
}
private async Task ExtractAllExtractableSubtitlesMKS(
MediaSourceInfo mediaSource,
List<MediaStream> subtitleStreams,
CancellationToken cancellationToken)
{
var mksFiles = new List<string>();
foreach (var subtitleStream in subtitleStreams)
{
if (string.IsNullOrEmpty(subtitleStream.Path) || !subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))
{
continue;
}
if (!mksFiles.Contains(subtitleStream.Path))
{
mksFiles.Add(subtitleStream.Path);
}
}
if (mksFiles.Count == 0)
{
return;
}
foreach (string mksFile in mksFiles)
{
var inputPath = _mediaEncoder.GetInputArgument(mksFile, mediaSource);
var outputPaths = new List<string>();
var args = string.Format(
CultureInfo.InvariantCulture,
"-i {0} -copyts",
inputPath);
foreach (var subtitleStream in subtitleStreams)
{
if (!subtitleStream.Path.Equals(mksFile, StringComparison.OrdinalIgnoreCase))
{
continue;
}
var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitleFileExtension(subtitleStream));
var outputCodec = IsCodecCopyable(subtitleStream.Codec) ? "copy" : "srt";
var streamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream);
if (streamIndex == -1)
{
_logger.LogError("Cannot find subtitle stream index for {InputPath} ({Index}), skipping this stream", inputPath, subtitleStream.Index);
continue;
}
Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new FileNotFoundException($"Calculated path ({outputPath}) is not valid."));
outputPaths.Add(outputPath);
args += string.Format(
CultureInfo.InvariantCulture,
" -map 0:{0} -an -vn -c:s {1} \"{2}\"",
streamIndex,
outputCodec,
outputPath);
}
await ExtractSubtitlesForFile(inputPath, args, outputPaths, cancellationToken).ConfigureAwait(false);
}
}
private async Task ExtractAllExtractableSubtitlesInternal(
MediaSourceInfo mediaSource,
List<MediaStream> subtitleStreams,
@@ -536,6 +608,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles
foreach (var subtitleStream in subtitleStreams)
{
if (!string.IsNullOrEmpty(subtitleStream.Path) && subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))
{
_logger.LogDebug("Subtitle {Index} for file {InputPath} is part in an MKS file. Skipping", inputPath, subtitleStream.Index);
continue;
}
var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitleFileExtension(subtitleStream));
var outputCodec = IsCodecCopyable(subtitleStream.Codec) ? "copy" : "srt";
var streamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream);
@@ -557,6 +635,20 @@ namespace MediaBrowser.MediaEncoding.Subtitles
outputPath);
}
if (outputPaths.Count == 0)
{
return;
}
await ExtractSubtitlesForFile(inputPath, args, outputPaths, cancellationToken).ConfigureAwait(false);
}
private async Task ExtractSubtitlesForFile(
string inputPath,
string args,
List<string> outputPaths,
CancellationToken cancellationToken)
{
int exitCode;
using (var process = new Process
+44 -10
View File
@@ -273,11 +273,28 @@ namespace MediaBrowser.Model.Entities
// Do not display the language code in display titles if unset or set to a special code. Show it in all other cases (possibly expanded).
if (!string.IsNullOrEmpty(Language) && !_specialCodes.Contains(Language, StringComparison.OrdinalIgnoreCase))
{
// Get full language string i.e. eng -> English.
string fullLanguage = CultureInfo
.GetCultures(CultureTypes.NeutralCultures)
.FirstOrDefault(r => r.ThreeLetterISOLanguageName.Equals(Language, StringComparison.OrdinalIgnoreCase))
?.DisplayName;
// Get full language string i.e. eng -> English, zh-Hans -> Chinese (Simplified).
var cultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
CultureInfo match = null;
if (Language.Contains('-', StringComparison.OrdinalIgnoreCase))
{
match = cultures.FirstOrDefault(r =>
r.Name.Equals(Language, StringComparison.OrdinalIgnoreCase));
if (match is null)
{
string baseLang = Language.AsSpan().LeftPart('-').ToString();
match = cultures.FirstOrDefault(r =>
r.TwoLetterISOLanguageName.Equals(baseLang, StringComparison.OrdinalIgnoreCase));
}
}
else
{
match = cultures.FirstOrDefault(r =>
r.ThreeLetterISOLanguageName.Equals(Language, StringComparison.OrdinalIgnoreCase));
}
string fullLanguage = match?.DisplayName;
attributes.Add(StringHelper.FirstToUpper(fullLanguage ?? Language));
}
@@ -376,11 +393,28 @@ namespace MediaBrowser.Model.Entities
if (!string.IsNullOrEmpty(Language))
{
// Get full language string i.e. eng -> English.
string fullLanguage = CultureInfo
.GetCultures(CultureTypes.NeutralCultures)
.FirstOrDefault(r => r.ThreeLetterISOLanguageName.Equals(Language, StringComparison.OrdinalIgnoreCase))
?.DisplayName;
// Get full language string i.e. eng -> English, zh-Hans -> Chinese (Simplified).
var cultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
CultureInfo match = null;
if (Language.Contains('-', StringComparison.OrdinalIgnoreCase))
{
match = cultures.FirstOrDefault(r =>
r.Name.Equals(Language, StringComparison.OrdinalIgnoreCase));
if (match is null)
{
string baseLang = Language.AsSpan().LeftPart('-').ToString();
match = cultures.FirstOrDefault(r =>
r.TwoLetterISOLanguageName.Equals(baseLang, StringComparison.OrdinalIgnoreCase));
}
}
else
{
match = cultures.FirstOrDefault(r =>
r.ThreeLetterISOLanguageName.Equals(Language, StringComparison.OrdinalIgnoreCase));
}
string fullLanguage = match?.DisplayName;
attributes.Add(StringHelper.FirstToUpper(fullLanguage ?? Language));
}
else
+1
View File
@@ -56,6 +56,7 @@ namespace MediaBrowser.Model.Net
".rec",
".ts",
".rmvb",
".vob",
".webm",
".wmv",
".wtv",
@@ -332,13 +332,11 @@ namespace MediaBrowser.Providers.Manager
if (!string.IsNullOrEmpty(itemPath))
{
var info = FileSystem.GetFileSystemInfo(itemPath);
var modificationDate = info.LastWriteTimeUtc;
var itemLastModifiedFileSystem = item.DateModified;
if (info.Exists && itemLastModifiedFileSystem != modificationDate)
if (info.Exists && item.HasChanged(info.LastWriteTimeUtc))
{
Logger.LogDebug("File modification time changed from {Then} to {Now}: {Path}", itemLastModifiedFileSystem, modificationDate, itemPath);
Logger.LogDebug("File modification time changed from {Then} to {Now}: {Path}", item.DateModified, info.LastWriteTimeUtc, itemPath);
item.DateModified = modificationDate;
item.DateModified = info.LastWriteTimeUtc;
if (ServerConfigurationManager.GetMetadataConfiguration().UseFileCreationTimeForDateAdded)
{
item.DateCreated = info.CreationTimeUtc;
@@ -697,6 +697,7 @@ namespace MediaBrowser.Providers.Manager
{
_libraryMonitor.ReportFileSystemChangeBeginning(path);
await saver.SaveAsync(item, CancellationToken.None).ConfigureAwait(false);
item.DateLastSaved = DateTime.UtcNow;
}
catch (Exception ex)
{
@@ -712,6 +713,7 @@ namespace MediaBrowser.Providers.Manager
try
{
await saver.SaveAsync(item, CancellationToken.None).ConfigureAwait(false);
item.DateLastSaved = DateTime.UtcNow;
}
catch (Exception ex)
{
@@ -276,10 +276,7 @@ namespace MediaBrowser.Providers.MediaInfo
_mediaStreamRepository.SaveMediaStreams(video.Id, mediaStreams, cancellationToken);
if (mediaAttachments.Any())
{
_mediaAttachmentRepository.SaveMediaAttachments(video.Id, mediaAttachments, cancellationToken);
}
_mediaAttachmentRepository.SaveMediaAttachments(video.Id, mediaAttachments, cancellationToken);
if (options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh
|| options.MetadataRefreshMode == MetadataRefreshMode.Default)
@@ -130,7 +130,7 @@ namespace MediaBrowser.Providers.MediaInfo
if (!string.IsNullOrWhiteSpace(path) && item.IsFileProtocol)
{
var file = directoryService.GetFile(path);
if (file is not null && file.LastWriteTimeUtc != item.DateModified && file.Length != item.Size)
if (file is not null && item.HasChanged(file.LastWriteTimeUtc) && file.Length != item.Size)
{
_logger.LogDebug("Refreshing {ItemPath} due to file system modification.", path);
return true;
@@ -215,7 +215,7 @@ public class PlaylistItemsProvider : ILocalMetadataProvider<Playlist>,
if (!string.IsNullOrWhiteSpace(path) && item.IsFileProtocol)
{
var file = directoryService.GetFile(path);
if (file is not null && file.LastWriteTimeUtc != item.DateModified)
if (file is not null && item.HasChanged(file.LastWriteTimeUtc))
{
_logger.LogDebug("Refreshing {Path} due to date modified timestamp change.", path);
return true;
@@ -48,7 +48,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
PersonKind.Producer
};
[GeneratedRegex(@"[\W_]+")]
[GeneratedRegex(@"[\W_-[·]]+")]
private static partial Regex NonWordRegex();
/// <summary>
@@ -56,7 +56,7 @@ public class TrickplayProvider : ICustomMetadataProvider<Episode>,
if (item.IsFileProtocol)
{
var file = directoryService.GetFile(item.Path);
if (file is not null && item.DateModified != file.LastWriteTimeUtc)
if (file is not null && item.HasChanged(file.LastWriteTimeUtc))
{
return true;
}
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Database.Implementations.DbConfiguration;
using Microsoft.EntityFrameworkCore;
namespace Jellyfin.Database.Implementations;
@@ -20,7 +21,8 @@ public interface IJellyfinDatabaseProvider
/// Initialises jellyfins EFCore database access.
/// </summary>
/// <param name="options">The EFCore database options.</param>
void Initialise(DbContextOptionsBuilder options);
/// <param name="databaseConfiguration">The Jellyfin database options.</param>
void Initialise(DbContextOptionsBuilder options, DatabaseConfigurationOptions databaseConfiguration);
/// <summary>
/// Will be invoked when EFCore wants to build its model.
@@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jellyfin.Server.Implementations.Migrations
{
/// <inheritdoc />
public partial class ResetJournalMode : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
// Resets journal mode to WAL for users that have created their database during 10.11-RC1 or 2
migrationBuilder.Sql("PRAGMA journal_mode = 'WAL';", true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}
@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.6");
modelBuilder.HasAnnotation("ProductVersion", "9.0.7");
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b =>
{
@@ -2,9 +2,11 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.DbConfiguration;
using MediaBrowser.Common.Configuration;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
@@ -38,11 +40,16 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider
public IDbContextFactory<JellyfinDbContext>? DbContextFactory { get; set; }
/// <inheritdoc/>
public void Initialise(DbContextOptionsBuilder options)
public void Initialise(DbContextOptionsBuilder options, DatabaseConfigurationOptions databaseConfiguration)
{
var sqliteConnectionBuilder = new SqliteConnectionStringBuilder();
sqliteConnectionBuilder.DataSource = Path.Combine(_applicationPaths.DataPath, "jellyfin.db");
sqliteConnectionBuilder.Cache = Enum.Parse<SqliteCacheMode>(databaseConfiguration.CustomProviderOptions?.Options.FirstOrDefault(e => e.Key.Equals("cache", StringComparison.OrdinalIgnoreCase))?.Value ?? nameof(SqliteCacheMode.Default));
sqliteConnectionBuilder.Pooling = (databaseConfiguration.CustomProviderOptions?.Options.FirstOrDefault(e => e.Key.Equals("pooling", StringComparison.OrdinalIgnoreCase))?.Value ?? bool.FalseString).Equals(bool.TrueString, StringComparison.OrdinalIgnoreCase);
options
.UseSqlite(
$"Filename={Path.Combine(_applicationPaths.DataPath, "jellyfin.db")};Pooling=false",
sqliteConnectionBuilder.ToString(),
sqLiteOptions => sqLiteOptions.MigrationsAssembly(GetType().Assembly))
// TODO: Remove when https://github.com/dotnet/efcore/pull/35873 is merged & released
.ConfigureWarnings(warnings =>
+31 -2
View File
@@ -202,18 +202,47 @@ public class SkiaEncoder : IImageEncoder
}
}
using var codec = SKCodec.Create(path, out SKCodecResult result);
var safePath = NormalizePath(path);
if (new FileInfo(safePath).Length == 0)
{
_logger.LogDebug("Skip zerobyte image {FilePath}", path);
return default;
}
using var codec = SKCodec.Create(safePath, out var result);
switch (result)
{
case SKCodecResult.Success:
// Skia/SkiaSharp edgecase: when the image header is parsed but the actual pixel
// decode fails (truncated JPEG/PNG, exotic ICC/EXIF, CMYK without colortransform, etc.)
// `SKCodec.Create` returns a *nonnull* codec together with
// SKCodecResult.InternalError. The header still contains valid dimensions,
// which is all we need here so we fall back to them instead of aborting.
// See e.g. Skia bugs #4139, #6092.
case SKCodecResult.InternalError when codec is not null:
var info = codec.Info;
return new ImageDimensions(info.Width, info.Height);
case SKCodecResult.Unimplemented:
_logger.LogDebug("Image format not supported: {FilePath}", path);
return default;
default:
_logger.LogError("Unable to determine image dimensions for {FilePath}: {SkCodecResult}", path, result);
{
var boundsInfo = SKBitmap.DecodeBounds(safePath);
if (boundsInfo.Width > 0 && boundsInfo.Height > 0)
{
return new ImageDimensions(boundsInfo.Width, boundsInfo.Height);
}
_logger.LogWarning(
"Unable to determine image dimensions for {FilePath}: {SkCodecResult}",
path,
result);
return default;
}
}
}
@@ -73,6 +73,10 @@ namespace Jellyfin.LiveTv.IO
{
_targetPath = targetFile;
Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
if (!File.Exists(targetFile))
{
FileHelper.CreateEmpty(targetFile);
}
var processStartInfo = new ProcessStartInfo
{
@@ -23,7 +23,7 @@ namespace Jellyfin.Naming.Tests.Video
Test("300-trailer.mp4", ExtraType.Trailer);
Test("300.trailer.mp4", ExtraType.Trailer);
Test("300_trailer.mp4", ExtraType.Trailer);
Test("300 trailer.mp4", ExtraType.Trailer);
Test("300 - trailer.mp4", ExtraType.Trailer);
Test("theme.mp3", ExtraType.ThemeSong);
}
@@ -132,7 +132,14 @@ namespace Jellyfin.Naming.Tests.Video
Test("300-sample.mp4", ExtraType.Sample);
Test("300.sample.mp4", ExtraType.Sample);
Test("300_sample.mp4", ExtraType.Sample);
Test("300 sample.mp4", ExtraType.Sample);
Test("300 - sample.mp4", ExtraType.Sample);
}
[Fact]
public void TestSuffixPartOfTitle()
{
Test("I Live In A Trailer.mp4", null);
Test("The DNA Sample.mp4", null);
}
private void Test(string input, ExtraType? expectedType)
@@ -87,7 +87,7 @@ namespace Jellyfin.Naming.Tests.Video
var files = new[]
{
"300.mkv",
"300 trailer.mkv"
"300 - trailer.mkv"
};
var result = VideoListResolver.Resolve(
@@ -0,0 +1,129 @@
using System;
using System.IO;
using Emby.Naming.Common;
using Emby.Naming.Video;
using Emby.Server.Implementations.Library;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using Moq;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.Library;
public class CoreResolutionIgnoreRuleTest
{
private readonly CoreResolutionIgnoreRule _rule;
private readonly NamingOptions _namingOptions;
private readonly Mock<IServerApplicationPaths> _appPathsMock;
public CoreResolutionIgnoreRuleTest()
{
_namingOptions = new NamingOptions();
_namingOptions.AllExtrasTypesFolderNames.TryAdd("extras", ExtraType.Trailer);
_appPathsMock = new Mock<IServerApplicationPaths>();
_appPathsMock.SetupGet(x => x.RootFolderPath).Returns("/server/root");
_rule = new CoreResolutionIgnoreRule(_namingOptions, _appPathsMock.Object);
}
private FileSystemMetadata MakeFileSystemMetadata(string fullName, bool isDirectory = false)
=> new FileSystemMetadata { FullName = fullName, Name = Path.GetFileName(fullName), IsDirectory = isDirectory };
private BaseItem MakeParent(string name = "Parent", bool isTopParent = false, Type? type = null)
{
return type switch
{
Type t when t == typeof(Folder) => CreateMock<Folder>(name, isTopParent).Object,
Type t when t == typeof(AggregateFolder) => CreateMock<AggregateFolder>(name, isTopParent).Object,
Type t when t == typeof(UserRootFolder) => CreateMock<UserRootFolder>(name, isTopParent).Object,
_ => CreateMock<BaseItem>(name, isTopParent).Object
};
}
private static Mock<T> CreateMock<T>(string name, bool isTopParent)
where T : BaseItem
{
var mock = new Mock<T>();
mock.SetupGet(p => p.Name).Returns(name);
mock.SetupGet(p => p.IsTopParent).Returns(isTopParent);
return mock;
}
[Fact]
public void TestApplicationFolder()
{
Assert.False(_rule.ShouldIgnore(
MakeFileSystemMetadata("/server/root/extras", isDirectory: true),
null));
Assert.False(_rule.ShouldIgnore(
MakeFileSystemMetadata("/server/root/small.jpg"),
null));
}
[Fact]
public void TestTopLevelDirectory()
{
Assert.False(_rule.ShouldIgnore(
MakeFileSystemMetadata("Series/Extras", true),
MakeParent(type: typeof(AggregateFolder))));
Assert.False(_rule.ShouldIgnore(
MakeFileSystemMetadata("Series/Extras/Extras", true),
MakeParent(isTopParent: true)));
}
[Fact]
public void TestIgnorePatterns()
{
Assert.False(_rule.ShouldIgnore(
MakeFileSystemMetadata("/Media/big.jpg"),
MakeParent()));
Assert.True(_rule.ShouldIgnore(
MakeFileSystemMetadata("/Media/small.jpg"),
MakeParent()));
}
[Fact]
public void TestExtrasTypesFolderNames()
{
FileSystemMetadata fileSystemMetadata = MakeFileSystemMetadata("/Movies/Up/extras", true);
Assert.False(_rule.ShouldIgnore(
fileSystemMetadata,
MakeParent(type: typeof(AggregateFolder))));
Assert.False(_rule.ShouldIgnore(
fileSystemMetadata,
MakeParent(type: typeof(UserRootFolder))));
Assert.False(_rule.ShouldIgnore(
fileSystemMetadata,
null));
Assert.True(_rule.ShouldIgnore(
fileSystemMetadata,
MakeParent()));
Assert.True(_rule.ShouldIgnore(
fileSystemMetadata,
MakeParent(type: typeof(Folder))));
}
[Fact]
public void TestThemeSong()
{
Assert.False(_rule.ShouldIgnore(
MakeFileSystemMetadata("/Movies/Up/intro.mp3"),
MakeParent()));
Assert.True(_rule.ShouldIgnore(
MakeFileSystemMetadata("/Movies/Up/theme.mp3"),
MakeParent()));
}
}
@@ -41,7 +41,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
await localizationManager.LoadAll();
var cultures = localizationManager.GetCultures().ToList();
Assert.Equal(191, cultures.Count);
Assert.Equal(194, cultures.Count);
var germany = cultures.FirstOrDefault(x => x.TwoLetterISOLanguageName.Equals("de", StringComparison.Ordinal));
Assert.NotNull(germany);
@@ -301,7 +301,7 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins
var versionInfo = fixture.Create<VersionInfo>();
versionInfo.Version = new Version(1, 0).ToString();
versionInfo.Timestamp = DateTime.UtcNow.ToString(CultureInfo.InvariantCulture);
versionInfo.Timestamp = DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture);
var packageInfo = fixture.Create<PackageInfo>();
packageInfo.Versions = new[] { versionInfo };