Compare commits

...

85 Commits

Author SHA1 Message Date
renovate[bot] 982e0c9370 Update dependency Polly to 8.6.2 (#14455)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-12 09:47:56 +02:00
JPVenson 55e681b9a6 Add SerilogExpressions reference (#14438) 2025-07-11 23:38:41 -06:00
JPVenson 7ba77804c4 Added advanced parallel process calculation (#14437) 2025-07-11 23:27:13 -06:00
Bas af6f5a8ed0 Translated using Weblate (Dutch)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/nl/
2025-07-11 18:13:18 +00:00
Ella Veter 1162fcebf8 Translated using Weblate (Dutch)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/nl/
2025-07-11 16:50:00 +00:00
renovate[bot] 38d0367c42 Update dependency dotnet-ef to 9.0.7 (#14440)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-09 00:47:46 +02:00
renovate[bot] 7d3372018f Update Microsoft to 9.0.7 (#14441)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-09 00:46:44 +02:00
renovate[bot] 8629831658 Update dependency z440.atl.core to v7 (#14391) 2025-07-07 18:10:48 -06:00
Nyanmisaka db55d983f8 Only enable VAAPI MJPEG encoder on Intel iHD driver (#14433) 2025-07-07 18:05:14 -06:00
JPVenson 4d5ba8d7a5 Only save images when changed (#14425) 2025-07-07 09:14:01 -06:00
Bond-009 6d4169a449 Update dependency Diacritics to v4 (#14418)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-05 11:08:38 +02:00
Shane Powell 8dcb0bfecb Merge pull request #14309 from shanepowell/MediaInfoFixs
Fix PeopleHelper.AddPerson Exceptions due to bad meta-data extracts.
2025-07-04 19:22:27 +02:00
Bond-009 844d69ab64 Read ALBUMARTISTS in preference to ARTISTS when PreferNonstandardArtistsTag set (#14413)
Jellyfin implemented ARTISTS multivalue tag but did not implement the equivalent ALBUMARTISTS multivalue tag.  This change adds ALBUMARTISTS support.  If present and PreferNonstandardArtistsTag is set, ALBUMARTISTS will be used in preference to ALBUMARTIST.  As with ARTISTS, the intent is to offer support for multiple album artists without affecting software that does not read ALBUMARTIST as a multivalued tag.

Example album before/after:

    ALBUM           : Amici e Rivali
    ARTIST          : Lawrence Brownlee / Michael Spyres
    album_artist    : Lawrence Brownlee
    ARTISTS         : Lawrence Brownlee;Michael Spyres
    ALBUMARTISTS    : Lawrence Brownlee;Michael Spyres

Before ALBUMARTISTS support, Jellyfin reports:
    Album Artist: Lawrence Brownlee [hyperlinked]
    On each track Artist: Lawrence Brownlee, Michael Spyres

After ALBUMARTISTS support, Jellyfin reoprts:
    Album Artist: Lawrence Brownlee [hyperlinked], Michael Spyres [hyperlinked]
    On each track Artist: none shown (no other artists in source metadata)

This is ideal as both key artists are hyperlinkable from their albums.

References to other products implementing ALBUMARTISTS:
- Navidrome: https://www.navidrome.org/docs/usage/tagging-guidelines/#handling-multiple-artists-and-collaborations
- Kodi: https://kodi.wiki/view/Music_tagging#albumartists
- MusicBrainz Picard: https://picard-docs.musicbrainz.org/en/variables/variables_basic.html (_albumartists tag)

Co-authored-by: Evan <evan@MacBook-Pro.local>
2025-07-04 19:19:26 +02:00
Bond-009 5c36b44484 Fix seasons random (#13224) (#14335)
Sorting was always enabled so removed the `enableSorting` parameter
in QueryResult method.

Co-authored-by: Maxime <>
2025-07-04 19:18:38 +02:00
renovate[bot] 4e4d7e7764 Update github/codeql-action action to v3.29.2 (#14400)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-03 17:18:29 +02:00
renovate[bot] 4c268a3579 Update dependency Diacritics to v4 2025-07-03 15:15:02 +00:00
Yago Raña Gayoso 77bcd2f5f6 Translated using Weblate (Galician)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/gl/
2025-07-02 19:07:02 +00:00
Bill Thornton 8406924471 Merge pull request #14414 from thornbill/revert-pt-translations
Revert invalid Portuguese translations
2025-07-02 10:52:11 -04:00
Bill Thornton 67fd4ce187 Revert "Translated using Weblate (Portuguese)"
This reverts commit dde306b170.
2025-07-02 10:47:44 -04:00
Bill Thornton b37b39773a Revert "Translated using Weblate (Portuguese)"
This reverts commit 9c817a97a9.
2025-07-02 10:47:31 -04:00
akshay 6f98767aed Translated using Weblate (Marathi)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/mr/
2025-07-02 09:49:41 +00:00
Tal Sarid 643460f484 Translated using Weblate (Hebrew)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/he/
2025-07-02 08:16:42 +00:00
Evan a4231bf428 Read ALBUMARTISTS in preference to ARTISTS when PreferNonstandardArtistsTag set
Jellyfin implemented ARTISTS multivalue tag but did not implement the equivalent ALBUMARTISTS multivalue tag.  This change adds ALBUMARTISTS support.  If present and PreferNonstandardArtistsTag is set, ALBUMARTISTS will be used in preference to ALBUMARTIST.  As with ARTISTS, the intent is to offer support for multiple album artists without affecting software that does not read ALBUMARTIST as a multivalued tag.

Example album before/after:

    ALBUM           : Amici e Rivali
    ARTIST          : Lawrence Brownlee / Michael Spyres
    album_artist    : Lawrence Brownlee
    ARTISTS         : Lawrence Brownlee;Michael Spyres
    ALBUMARTISTS    : Lawrence Brownlee;Michael Spyres

Before ALBUMARTISTS support, Jellyfin reports:
    Album Artist: Lawrence Brownlee [hyperlinked]
    On each track Artist: Lawrence Brownlee, Michael Spyres

After ALBUMARTISTS support, Jellyfin reoprts:
    Album Artist: Lawrence Brownlee [hyperlinked], Michael Spyres [hyperlinked]
    On each track Artist: none shown (no other artists in source metadata)

This is ideal as both key artists are hyperlinkable from their albums.

References to other products implementing ALBUMARTISTS:
- Navidrome: https://www.navidrome.org/docs/usage/tagging-guidelines/#handling-multiple-artists-and-collaborations
- Kodi: https://kodi.wiki/view/Music_tagging#albumartists
- MusicBrainz Picard: https://picard-docs.musicbrainz.org/en/variables/variables_basic.html (_albumartists tag)
2025-07-02 07:55:24 +08:00
Edson 9c817a97a9 Translated using Weblate (Portuguese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt/
2025-07-01 18:43:41 +00:00
Deleted User dde306b170 Translated using Weblate (Portuguese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt/
2025-07-01 08:19:32 +00:00
Tal Sarid e2b61d951b Translated using Weblate (Hebrew)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/he/
2025-07-01 00:32:48 +00:00
Gargotaire 9eff25bfed Translated using Weblate (Catalan)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ca/
2025-07-01 00:32:47 +00:00
Tommaso Morganti ff4484eb4a Translated using Weblate (Italian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/it/
2025-06-29 16:35:11 +00:00
Gargotaire 62b2adbf66 Translated using Weblate (Catalan)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ca/
2025-06-29 16:35:11 +00:00
gnattu 9ac8c2a2fa Fix compiler warning for CodeMigration.cs (#14390) 2025-06-29 08:00:29 -06:00
renovate[bot] 90e72fb687 Update dependency BitFaster.Caching to 2.5.4 (#14392)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-29 10:23:38 +02:00
Acrotos 630846798d Translated using Weblate (Romanian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ro/
2025-06-28 20:50:29 +00:00
Petrică Andrei-Cosmin 9d5be19a27 Translated using Weblate (Romanian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ro/
2025-06-28 20:50:29 +00:00
renovate[bot] 6058ab50f8 Update github/codeql-action action to v3.29.1 (#14389)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-28 11:23:12 +02:00
Ruben Teixeira e3b379052d Translated using Weblate (Portuguese (Portugal))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_PT/
2025-06-28 00:39:50 +00:00
Nirwan 0b6f4b2bd9 Translated using Weblate (Indonesian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/id/
2025-06-27 11:27:47 +00:00
Varun Sudarshan 4f6db1bc22 Translated using Weblate (Kannada)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/kn/
2025-06-27 02:51:17 +00:00
Oatavandi 8c8c71125c Translated using Weblate (Tamil)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ta/
2025-06-27 02:51:17 +00:00
Tim Eisele c6e568692e Fix modification checks and make sure to use UTC (#14347) 2025-06-26 17:50:37 -06:00
Archie d5a76bdff8 Changed misspell of 'temporarily' in 503 response (#14377) 2025-06-26 17:49:07 -06:00
Nyanmisaka ebdc756547 Fix -fps_mode option being applied on input (#14379) 2025-06-26 17:48:43 -06:00
stelle 10d0cec7b9 Translated using Weblate (Malay)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ms/
2025-06-25 13:51:17 +00:00
Hasan Abdulaal 10cc651790 Translated using Weblate (Arabic)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ar/
2025-06-25 13:51:16 +00:00
theguymadmax 7d18f3d6ed Improve cast and crew handling (#14370) 2025-06-24 17:48:36 -06:00
Max Rumpf 9b8c12d433 Adapt LrcLyricParser to new LrcParser version (#14263) 2025-06-24 16:59:09 -06:00
JPVenson ba0eb87371 Add migration to migrate disconnected UserData too (#14339) 2025-06-23 08:36:49 -06:00
Mason Weigand d561cef81f RemoveNowPlayingItem should also set FullNowPlayingItem to null (#14360) bb 2025-06-23 08:31:25 -06:00
theguymadmax b528c1100f Fix missing music genre metadata (#14332) 2025-06-23 08:30:59 -06:00
Tim Eisele 96c9f4fdad Make keyframe extraction task cancellable (#14368) 2025-06-23 08:29:42 -06:00
Onni Saarni 6d077fcf40 Translated using Weblate (Finnish)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fi/
2025-06-22 07:12:05 +00:00
Niels van Velzen ab99b2bad3 Merge pull request #14341 from Shadowghost/misc
Cleanup Migration
2025-06-22 08:49:24 +02:00
Tim Eisele db36be7a6b Skip missing images when creating collages (#14344)
* Skip missing images when creating collages
2025-06-21 12:23:08 +02:00
Romulo Alves 85f158e1dd Translated using Weblate (Portuguese (Brazil))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_BR/
2025-06-20 03:36:14 +00:00
RingoJet e1365bd253 Translated using Weblate (Arabic)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ar/
2025-06-20 03:36:14 +00:00
Shoham Peller 1ec66adc30 fix(devcontainer): correct apt-packages path (#14235)
Feature path was incorrect, which failed the creation of the
devcontainer
2025-06-19 14:46:55 +02:00
Shadowghost af0bcbc652 Fixup 2025-06-19 12:33:46 +02:00
Tim Eisele b2312466e1 Update Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
Co-authored-by: Bond-009 <bond.009@outlook.com>
2025-06-19 11:40:40 +02:00
Bond-009 cc7915c2e6 Merge pull request #14338 from jellyfin/renovate/polly-monorepo
Update dependency Polly to 8.6.1
2025-06-19 11:28:13 +02:00
Bond-009 a537c66da1 Merge pull request #14322 from gnattu/disable-hdr-non-hdr-clients
Don't check dynamic metadata removal for static HDR formats
2025-06-19 11:27:23 +02:00
Bond-009 a43adf42f3 Merge pull request #14324 from jellyfin/renovate/microsoft
Update dependency System.Linq.Async to 6.0.3
2025-06-19 11:26:54 +02:00
Bond-009 6996c8a1de Merge pull request #14330 from jellyfin/qsv-dx11-static-pool
Fix QSV "static surface pool size exceeded" on Windows
2025-06-19 11:26:37 +02:00
Bond-009 f976630003 Merge pull request #14333 from theguymadmax/add-act-naming-option
Add act as a stacking option for music albums
2025-06-19 11:25:46 +02:00
Bond-009 965cf93419 Merge pull request #14328 from crobibero/trickplay-info
Use dto instead of db object when returning trickplay
2025-06-19 11:24:03 +02:00
Bond-009 70ea3f863a Merge pull request #14327 from crobibero/fix-startup
Fix startup logger, startup health check
2025-06-19 11:20:35 +02:00
Tim Eisele 989aef18af Update Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
Co-authored-by: Niels van Velzen <nielsvanvelzen@users.noreply.github.com>
2025-06-18 23:08:05 +02:00
Shadowghost ccb917b8df Cleanup logging and user data import skip on missing user 2025-06-18 21:33:32 +02:00
queeup 7cf6389ab5 Translated using Weblate (Turkish)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/tr/
2025-06-18 15:47:10 +00:00
renovate[bot] 2473b89a8d Update dependency Polly to 8.6.1 2025-06-18 09:36:02 +00:00
Yusuke, Hirota 6575c69a4e Translated using Weblate (Japanese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ja/
2025-06-18 09:01:34 +00:00
nextlooper42 66d594836c Translated using Weblate (Slovak)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sk/
2025-06-18 09:01:34 +00:00
Roi Gabay 43028f735f Translated using Weblate (Hebrew)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/he/
2025-06-18 09:01:34 +00:00
Maxime e83b992eef Fix seasons random (#13224)
Sorting was always enabled so removed the `enableSorting` parameter
in QueryResult method.
2025-06-18 00:41:09 +02:00
theguymadmax 8368d10d1b Add act as a stacking option for music albums 2025-06-17 12:16:32 -04:00
Nyanmisaka e8291fc856 Fix QSV "static surface pool size exceeded" on Windows
d3d11va doesn't support dynamic pool size, use vpp filter ctx to relay to prevent encoder async and bframes from exhausting the decoder pool.
2025-06-17 15:02:13 +08:00
Aindriú Mac Giolla Eoin 308707476d Translated using Weblate (Irish)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ga/
2025-06-17 01:01:35 +00:00
Daniel Szente e252589900 Translated using Weblate (Hungarian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hu/
2025-06-17 01:01:34 +00:00
Mathieu Funk 1220cac255 Translated using Weblate (English (United Kingdom))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/en_GB/
2025-06-17 01:01:34 +00:00
Gallyam Biktashev 7218d82c21 Translated using Weblate (Russian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ru/
2025-06-17 01:01:34 +00:00
bdubz a4524eb2ad Translated using Weblate (Spanish)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/es/
2025-06-17 01:01:34 +00:00
Thadah D. Denyse 553ba56389 Translated using Weblate (Basque)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/eu/
2025-06-17 01:01:34 +00:00
Cody Robibero afa2103d42 Use dto instead of db object when returning trickplay 2025-06-16 18:55:21 -06:00
Cody Robibero 7256c9c89d Fix startup logger, startup health check 2025-06-16 18:32:29 -06:00
renovate[bot] f3cdaeaa12 Update dependency System.Linq.Async to 6.0.3 2025-06-16 21:23:20 +00:00
gnattu 368808eba4 Don't check dynamic metadata removal for static HDR formats
Those videos have no metadata to be removed at all, just force transcoding when the client does not support it.
2025-06-17 02:29:16 +08:00
renovate[bot] 0fc8ed6aeb Update danielpalme/ReportGenerator-GitHub-Action action to v5.4.8 (#14316) 2025-06-16 09:15:23 +02:00
83 changed files with 2846 additions and 395 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "9.0.6",
"version": "9.0.7",
"commands": [
"dotnet-ef"
]
+1 -1
View File
@@ -13,7 +13,7 @@
"dotnetRuntimeVersions": "9.0",
"aspNetCoreRuntimeVersions": "9.0"
},
"ghcr.io/devcontainers-contrib/features/apt-packages:1": {
"ghcr.io/devcontainers-extra/features/apt-packages:1": {
"preserve_apt_list": false,
"packages": [
"libfontconfig1"
+3 -3
View File
@@ -27,11 +27,11 @@ jobs:
dotnet-version: '9.0.x'
- name: Initialize CodeQL
uses: github/codeql-action/init@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
uses: github/codeql-action/autobuild@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
--verbosity minimal
- name: Merge code coverage results
uses: danielpalme/ReportGenerator-GitHub-Action@c9576654e2fea2faa7b69e59550b3805bf6a9977 # v5.4.7
uses: danielpalme/ReportGenerator-GitHub-Action@4c0f60daf67483745c34efdeadd4c4e78a19991e # v5.4.8
with:
reports: "**/coverage.cobertura.xml"
targetdir: "merged/"
+1
View File
@@ -198,6 +198,7 @@
- [revam](https://github.com/revam)
- [allesmi](https://github.com/allesmi)
- [ThunderClapLP](https://github.com/ThunderClapLP)
- [Shoham Peller](https://github.com/spellr)
# Emby Contributors
+33 -32
View File
@@ -9,12 +9,12 @@
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
<PackageVersion Include="AutoFixture" Version="4.18.1" />
<PackageVersion Include="BDInfo" Version="0.8.0" />
<PackageVersion Include="BitFaster.Caching" Version="2.5.3" />
<PackageVersion Include="BitFaster.Caching" Version="2.5.4" />
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.4.0-pre.1" />
<PackageVersion Include="BlurHashSharp" Version="1.4.0-pre.1" />
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
<PackageVersion Include="Diacritics" Version="3.3.29" />
<PackageVersion Include="Diacritics" Version="4.0.14" />
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
<PackageVersion Include="FsCheck.Xunit" Version="3.3.0" />
@@ -24,31 +24,31 @@
<PackageVersion Include="Ignore" Version="0.2.1" />
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
<PackageVersion Include="libse" Version="4.0.12" />
<PackageVersion Include="LrcParser" Version="2025.228.1" />
<PackageVersion Include="LrcParser" Version="2025.623.0" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.6" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.6" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.7" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.7" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.6" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.6" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.6" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.6" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.6" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.6" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.6" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.6" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.6" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.6" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.6" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.6" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.6" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.6" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.6" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.6" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.6" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.6" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.6" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.6" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.7" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.7" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.7" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.7" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.7" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.7" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.7" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.7" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.7" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.7" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.7" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.7" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.7" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.7" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.7" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.7" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.7" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.7" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.7" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageVersion Include="MimeTypes" Version="2.5.2" />
<PackageVersion Include="Morestachio" Version="5.0.1.631" />
@@ -59,9 +59,10 @@
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.1" />
<PackageVersion Include="prometheus-net" Version="8.2.1" />
<PackageVersion Include="Polly" Version="8.6.0" />
<PackageVersion Include="Polly" Version="8.6.2" />
<PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
<PackageVersion Include="Serilog.Expressions" Version="5.0.0" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="9.0.0" />
<PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
@@ -79,12 +80,12 @@
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageVersion Include="System.Globalization" Version="4.3.0" />
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.6" />
<PackageVersion Include="System.Text.Json" Version="9.0.6" />
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.6" />
<PackageVersion Include="System.Linq.Async" Version="6.0.3" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.7" />
<PackageVersion Include="System.Text.Json" Version="9.0.7" />
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.7" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
<PackageVersion Include="z440.atl.core" Version="6.26.0" />
<PackageVersion Include="z440.atl.core" Version="7.0.0" />
<PackageVersion Include="TMDbLib" Version="2.2.0" />
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
@@ -92,4 +93,4 @@
<PackageVersion Include="Xunit.SkippableFact" Version="1.5.23" />
<PackageVersion Include="xunit" Version="2.9.3" />
</ItemGroup>
</Project>
</Project>
+2 -1
View File
@@ -188,7 +188,8 @@ namespace Emby.Naming.Common
"disk",
"vol",
"volume",
"part"
"part",
"act"
};
ArtistSubfolders = new[]
+1 -1
View File
@@ -108,7 +108,7 @@ public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IH
var dateTaken = image.ImageTag.DateTime;
if (dateTaken.HasValue)
{
item.DateCreated = dateTaken.Value;
item.DateCreated = dateTaken.Value.ToUniversalTime();
item.PremiereDate = dateTaken.Value;
item.ProductionYear = dateTaken.Value.Year;
}
@@ -1065,7 +1065,12 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.Trickplay))
{
dto.Trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult();
var trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult();
dto.Trickplay = trickplay.ToDictionary(
mediaStream => mediaStream.Key,
mediaStream => mediaStream.Value.ToDictionary(
width => width.Key,
width => new TrickplayInfoDto(width.Value)));
}
dto.ExtraType = video.ExtraType;
@@ -57,7 +57,7 @@ namespace Emby.Server.Implementations.HttpServer
RemoteEndPoint = remoteEndPoint;
_jsonOptions = JsonDefaults.Options;
LastActivityDate = DateTime.Now;
LastActivityDate = DateTime.UtcNow;
}
/// <inheritdoc />
@@ -43,13 +43,11 @@ namespace Emby.Server.Implementations.Images
protected IImageProcessor ImageProcessor { get; set; }
protected virtual IReadOnlyCollection<ImageType> SupportedImages { get; }
= new ImageType[] { ImageType.Primary };
= [ImageType.Primary];
/// <inheritdoc />
public string Name => "Dynamic Image Provider";
protected virtual int MaxImageAgeDays => 7;
public int Order => 0;
protected virtual bool Supports(BaseItem item) => true;
@@ -292,8 +290,14 @@ namespace Emby.Server.Implementations.Images
protected virtual bool HasChangedByDate(BaseItem item, ItemImageInfo image)
{
var age = DateTime.UtcNow - image.DateModified;
return age.TotalDays > MaxImageAgeDays;
var path = image.Path;
if (!string.IsNullOrEmpty(path))
{
var modificationDate = FileSystem.GetLastWriteTimeUtc(path);
return image.DateModified != modificationDate;
}
return false;
}
protected string CreateSingleImage(IEnumerable<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType)
@@ -1981,6 +1981,8 @@ namespace Emby.Server.Implementations.Library
return;
}
var anyChange = false;
foreach (var img in outdated)
{
var image = img;
@@ -2012,6 +2014,7 @@ namespace Emby.Server.Implementations.Library
try
{
size = _imageProcessor.GetImageDimensions(item, image);
anyChange = image.Width != size.Width || image.Height != size.Height;
image.Width = size.Width;
image.Height = size.Height;
}
@@ -2019,23 +2022,29 @@ namespace Emby.Server.Implementations.Library
{
_logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path);
size = default;
anyChange = image.Width != size.Width || image.Height != size.Height;
image.Width = 0;
image.Height = 0;
}
try
{
image.BlurHash = _imageProcessor.GetImageBlurHash(image.Path, size);
var blurhash = _imageProcessor.GetImageBlurHash(image.Path, size);
anyChange = anyChange || !blurhash.Equals(image.BlurHash, StringComparison.Ordinal);
image.BlurHash = blurhash;
}
catch (Exception ex)
{
_logger.LogError(ex, "Cannot compute blurhash for {ImagePath}", image.Path);
anyChange = anyChange || !string.IsNullOrEmpty(image.BlurHash);
image.BlurHash = string.Empty;
}
try
{
image.DateModified = _fileSystem.GetLastWriteTimeUtc(image.Path);
var modifiedDate = _fileSystem.GetLastWriteTimeUtc(image.Path);
anyChange = anyChange || modifiedDate != image.DateModified;
image.DateModified = modifiedDate;
}
catch (Exception ex)
{
@@ -2043,20 +2052,28 @@ namespace Emby.Server.Implementations.Library
}
}
_itemRepository.SaveImages(item);
if (anyChange)
{
_itemRepository.SaveImages(item);
}
RegisterItem(item);
}
/// <inheritdoc />
public async Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
{
_itemRepository.SaveItems(items, cancellationToken);
foreach (var item in items)
{
item.DateLastSaved = DateTime.UtcNow;
await RunMetadataSavers(item, updateReason).ConfigureAwait(false);
// Modify again, so saved value is after write time of externally saved metadata
item.DateLastSaved = DateTime.UtcNow;
}
_itemRepository.SaveItems(items, cancellationToken);
if (ItemUpdated is not null)
{
foreach (var item in items)
@@ -2097,8 +2114,6 @@ namespace Emby.Server.Implementations.Library
await ProviderManager.SaveMetadataAsync(item, updateReason).ConfigureAwait(false);
}
item.DateLastSaved = DateTime.UtcNow;
await UpdateImagesAsync(item, updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false);
}
@@ -2384,12 +2399,13 @@ namespace Emby.Server.Implementations.Library
isNew = true;
}
var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval;
var lastRefreshedUtc = item.DateLastRefreshed;
var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval;
if (!refresh && !item.DisplayParentId.IsEmpty())
{
var displayParent = GetItemById(item.DisplayParentId);
refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed;
refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc;
}
if (refresh)
@@ -2447,12 +2463,13 @@ namespace Emby.Server.Implementations.Library
isNew = true;
}
var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval;
var lastRefreshedUtc = item.DateLastRefreshed;
var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval;
if (!refresh && !item.DisplayParentId.IsEmpty())
{
var displayParent = GetItemById(item.DisplayParentId);
refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed;
refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc;
}
if (refresh)
@@ -2522,12 +2539,13 @@ namespace Emby.Server.Implementations.Library
item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
}
var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval;
var lastRefreshedUtc = item.DateLastRefreshed;
var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval;
if (!refresh && !item.DisplayParentId.IsEmpty())
{
var displayParent = GetItemById(item.DisplayParentId);
refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed;
refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc;
}
if (refresh)
@@ -2991,13 +3009,12 @@ namespace Emby.Server.Implementations.Library
{
var path = Person.GetPath(person.Name);
var info = Directory.CreateDirectory(path);
var lastWriteTime = info.LastWriteTimeUtc;
personEntity = new Person()
{
Name = person.Name,
Id = GetItemByNameId<Person>(path),
DateCreated = info.CreationTimeUtc,
DateModified = lastWriteTime,
DateModified = info.LastWriteTimeUtc,
Path = path
};
@@ -140,7 +140,7 @@ namespace Emby.Server.Implementations.Library
if (fileCreationDate is not null)
{
var dateCreated = fileCreationDate;
if (dateCreated.Equals(DateTime.MinValue))
if (dateCreated == DateTime.MinValue)
{
dateCreated = DateTime.UtcNow;
}
@@ -136,5 +136,7 @@
"TaskExtractMediaSegments": "فحص مقاطع الوسائط",
"TaskExtractMediaSegmentsDescription": "يستخرج مقاطع وسائط من إضافات MediaSegment المُفعّلة.",
"TaskMoveTrickplayImages": "تغيير مكان صور المعاينة السريعة",
"TaskMoveTrickplayImagesDescription": "تُنقل ملفات التشغيل السريع الحالية بناءً على إعدادات المكتبة."
"TaskMoveTrickplayImagesDescription": "تُنقل ملفات التشغيل السريع الحالية بناءً على إعدادات المكتبة.",
"CleanupUserDataTask": "مهمة تنظيف بيانات المستخدم",
"CleanupUserDataTaskDescription": "مسح جميع بيانات المستخدم (حالة المشاهدة، والحالة المفضلة وما إلى ذلك) من الوسائط التي لم تعد موجودة لمدة 90 يومًا على الأقل."
}
@@ -43,7 +43,7 @@
"NameInstallFailed": "{0} instal·lació fallida",
"NameSeasonNumber": "Temporada {0}",
"NameSeasonUnknown": "Temporada desconeguda",
"NewVersionIsAvailable": "Una nova versió del servidor de Jellyfin està disponible per a descarregar.",
"NewVersionIsAvailable": "Hi ha disponible una versió nova del servidor de Jellyfin per a la descàrrega.",
"NotificationOptionApplicationUpdateAvailable": "Actualització de l'aplicatiu disponible",
"NotificationOptionApplicationUpdateInstalled": "Actualització de l'aplicatiu instal·lada",
"NotificationOptionAudioPlayback": "Reproducció d'àudio iniciada",
@@ -64,7 +64,7 @@
"Playlists": "Llistes de reproducció",
"Plugin": "Complement",
"PluginInstalledWithName": "{0} ha estat instal·lat",
"PluginUninstalledWithName": "S'ha instalat {0}",
"PluginUninstalledWithName": "S'ha instal·lat {0}",
"PluginUpdatedWithName": "S'ha actualitzat {0}",
"ProviderValue": "Proveïdor: {0}",
"ScheduledTaskFailedWithName": "{0} ha fallat",
@@ -93,50 +93,50 @@
"ValueSpecialEpisodeName": "Especial - {0}",
"VersionNumber": "Versió {0}",
"TaskDownloadMissingSubtitlesDescription": "Cerca a internet els subtítols que faltin a partir de la configuració de metadades.",
"TaskDownloadMissingSubtitles": "Descarrega els subtítols que faltin",
"TaskDownloadMissingSubtitles": "Descàrrega dels subtítols que faltin",
"TaskRefreshChannelsDescription": "Actualitza la informació dels canals per internet.",
"TaskRefreshChannels": "Actualitza els canals",
"TaskCleanTranscodeDescription": "Elimina els arxius de transcodificacions que tinguin més d'un dia.",
"TaskCleanTranscode": "Neteja les transcodificacions",
"TaskCleanTranscode": "Neteja de les transcodificacions",
"TaskUpdatePluginsDescription": "Descarrega i instal·la els complements que estiguin configurats per a actualitzar-se automàticament.",
"TaskUpdatePlugins": "Actualitza els complements",
"TaskRefreshPeopleDescription": "Actualitza les metadades dels actors i directors de la mediateca.",
"TaskRefreshPeople": "Actualitza les persones",
"TaskUpdatePlugins": "Actualització dels complements",
"TaskRefreshPeopleDescription": "Actualització de les metadades dels actors i directors de la mediateca.",
"TaskRefreshPeople": "Actualització de les persones",
"TaskCleanLogsDescription": "Esborra els registres que tinguin més de {0} dies.",
"TaskCleanLogs": "Neteja els registres",
"TaskRefreshLibraryDescription": "Escaneja la mediateca, a la cerca de fitxers nous i refresca les metadades.",
"TaskRefreshLibrary": "Escaneja la mediateca",
"TaskRefreshChapterImagesDescription": "Crea les miniatures dels vídeos que tinguin capítols.",
"TaskRefreshChapterImages": "Extreu les imatges dels capítols",
"TaskCleanCacheDescription": "Elimina la memòria cau no necessària per al servidor.",
"TaskCleanCache": "Elimina la memòria cau",
"TaskCleanLogs": "Neteja dels registres",
"TaskRefreshLibraryDescription": "Escaneig de la mediateca, a la cerca de fitxers nous i refresca les metadades.",
"TaskRefreshLibrary": "Escaneig de la mediateca",
"TaskRefreshChapterImagesDescription": "Creació de les miniatures dels vídeos que tinguin capítols.",
"TaskRefreshChapterImages": "Extracció de les imatges dels capítols",
"TaskCleanCacheDescription": "Eliminació de la memòria cau no necessària per al servidor.",
"TaskCleanCache": "Eliminació de la memòria cau",
"TasksChannelsCategory": "Canals per internet",
"TasksApplicationCategory": "Aplicatiu",
"TasksLibraryCategory": "Mediateca",
"TasksMaintenanceCategory": "Manteniment",
"TaskCleanActivityLogDescription": "Eliminades les entrades del registre d'activitats més antigues que l'antiguitat configurada.",
"TaskCleanActivityLog": "Buida el registre d'activitat",
"TaskCleanActivityLogDescription": "Eliminació de les entrades del registre d'activitats més antigues que l'antiguitat configurada.",
"TaskCleanActivityLog": "Buidat 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": "Optimitza la base de dades",
"TaskKeyframeExtractorDescription": "Extreu fotogrames clau dels fitxers de vídeo per crear llistes de reproducció HLS més precises. Aquesta tasca pot allargar-se molt en el temps.",
"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",
"External": "Extern",
"HearingImpaired": "Discapacitat auditiva",
"TaskRefreshTrickplayImages": "Genera imatges de previsualització",
"TaskRefreshTrickplayImagesDescription": "Crea imatges de previsualització per vídeos en les mediateques habilitades.",
"TaskRefreshTrickplayImages": "Generació d'imatges de previsualització",
"TaskRefreshTrickplayImagesDescription": "Creació d'imatges de previsualització per a vídeos en les mediateques habilitades.",
"TaskCleanCollectionsAndPlaylistsDescription": "Esborra elements de col·leccions i llistes de reproducció que ja no existeixen.",
"TaskCleanCollectionsAndPlaylists": "Neteja les col·leccions i llistes de reproducció",
"TaskCleanCollectionsAndPlaylists": "Neteja de les col·leccions i llistes de reproducció",
"TaskAudioNormalization": "Estabilització de l'àudio",
"TaskAudioNormalizationDescription": "Escaneja arxius per dades d'estabilització de l'àudio.",
"TaskDownloadMissingLyricsDescription": "Baixa les lletres de les cançons",
"TaskDownloadMissingLyrics": "Baixa les lletres que falten",
"TaskDownloadMissingLyricsDescription": "Descàrrega de les lletres de les cançons",
"TaskDownloadMissingLyrics": "Descàrrega de les lletres que faltin",
"TaskExtractMediaSegments": "Escaneig de segments multimèdia",
"TaskExtractMediaSegmentsDescription": "Extreu o obté segments multimèdia usant els connectors MediaSegment activats.",
"TaskMoveTrickplayImages": "Migra la ubicació de la imatge de Trickplay",
"TaskMoveTrickplayImagesDescription": "Mou els fitxers trickplay existents segons la configuració de la mediateca.",
"TaskMoveTrickplayImages": "Migració de la ubicació de la imatge de previsualització",
"TaskMoveTrickplayImagesDescription": "Mou els fitxers existents d'imatges de previsualització segons la configuració de la mediateca.",
"CleanupUserDataTaskDescription": "Neteja totes les dades d'usuari (estat de la visualització, estat dels preferits, etc.) del contingut multimèdia que no ha estat present durant almenys 90 dies.",
"CleanupUserDataTask": "Tasca de neteja de dades d'usuari"
}
@@ -136,5 +136,7 @@
"TaskExtractMediaSegments": "Media Segment Scan",
"TaskExtractMediaSegmentsDescription": "Extracts or obtains media segments from MediaSegment enabled plugins.",
"TaskMoveTrickplayImages": "Migrate Trickplay Image Location",
"TaskMoveTrickplayImagesDescription": "Moves existing trickplay files according to the library settings."
"TaskMoveTrickplayImagesDescription": "Moves existing trickplay files according to the library settings.",
"CleanupUserDataTask": "User data cleanup task",
"CleanupUserDataTaskDescription": "Cleans all user data (Watch state, favourite status etc) from media that is no longer present for at least 90 days."
}
@@ -136,5 +136,7 @@
"TaskMoveTrickplayImagesDescription": "Mueve archivos de trickplay existentes según la configuración de la biblioteca.",
"TaskExtractMediaSegments": "Escaneo de segmentos de medios",
"TaskExtractMediaSegmentsDescription": "Extrae u obtiene segmentos de medios de plugins habilitados para MediaSegment.",
"TaskMoveTrickplayImages": "Migrar la ubicación de la imagen de Trickplay"
"TaskMoveTrickplayImages": "Migrar la ubicación de la imagen de Trickplay",
"CleanupUserDataTask": "Tarea de limpieza de datos del usuario",
"CleanupUserDataTaskDescription": "Limpia todos los datos del usuario (estado de visualización, favoritos, etc.) de los medios que ya no están disponibles desde hace al menos 90 días."
}
@@ -135,5 +135,7 @@
"TaskExtractMediaSegmentsDescription": "Media segmentuak atera edo lortzen ditu MediaSegment gaituta duten pluginetik.",
"TaskMoveTrickplayImages": "Aldatu Trickplay irudien kokalekua",
"TaskMoveTrickplayImagesDescription": "Lehendik dauden trickplay fitxategiak liburutegiaren ezarpenen arabera mugitzen dira.",
"TaskAudioNormalizationDescription": "Audio normalizazio datuak lortzeko fitxategiak eskaneatzen ditu."
"TaskAudioNormalizationDescription": "Audio normalizazio datuak lortzeko fitxategiak eskaneatzen ditu.",
"CleanupUserDataTaskDescription": "Gutxienez 90 egunez dagoeneko existitzen ez den multimediatik erabiltzaile-datu guztiak (ikusteko egoera, gogokoen egoera, etab.) garbitzen ditu.",
"CleanupUserDataTask": "Erabiltzaileen datuak garbitzeko zeregina"
}
@@ -135,5 +135,7 @@
"TaskDownloadMissingLyricsDescription": "Ladataan sanoituksia",
"TaskExtractMediaSegmentsDescription": "Poimii tai hankkii mediasegmenttejä MediaSegment-yhteensopivista laajennuksista.",
"TaskMoveTrickplayImages": "Siirrä Trickplay-kuvien sijainti",
"TaskMoveTrickplayImagesDescription": "Siirtää olemassa olevia trickplay-tiedostoja kirjaston asetusten mukaan."
"TaskMoveTrickplayImagesDescription": "Siirtää olemassa olevia trickplay-tiedostoja kirjaston asetusten mukaan.",
"CleanupUserDataTask": "Käyttäjätietojen puhdistustehtävä",
"CleanupUserDataTaskDescription": "Puhdistaa kaikki käyttäjätiedot (katselutila, suosikit ym.) medioista, joita ei ole ollut saatavilla yli 90 päivään."
}
@@ -135,5 +135,7 @@
"TaskUpdatePlugins": "Nuashonraigh Breiseáin",
"TaskCleanTranscodeDescription": "Scriostar comhaid traschódaithe níos mó ná lá amháin d'aois.",
"TaskCleanTranscode": "Eolaire Transcode Glan",
"TaskDownloadMissingSubtitles": "Íosluchtaigh fotheidil ar iarraidh"
"TaskDownloadMissingSubtitles": "Íosluchtaigh fotheidil ar iarraidh",
"CleanupUserDataTask": "Tasc glantacháin sonraí úsáideora",
"CleanupUserDataTaskDescription": "Glanann sé gach sonraí úsáideora (stádas faire, stádas is fearr leat srl.) ó mheáin nach bhfuil i láthair a thuilleadh ar feadh 90 lá ar a laghad."
}
@@ -135,5 +135,6 @@
"TaskMoveTrickplayImages": "Migrar a localización da imaxe de Trickplay",
"TaskMoveTrickplayImagesDescription": "Move os ficheiros de reprodución con trickplay existentes segundo a configuración da biblioteca.",
"TaskRefreshTrickplayImages": "Xerar imaxes de Trickplay",
"TaskAudioNormalizationDescription": "Analiza ficheiros para obter datos de normalización de audio."
"TaskAudioNormalizationDescription": "Analiza ficheiros para obter datos de normalización de audio.",
"CleanupUserDataTask": "Tarefa de limpeza de datos do usuario"
}
@@ -32,8 +32,8 @@
"LabelIpAddressValue": "Ip כתובת: {0}",
"LabelRunningTimeValue": "משך צפייה: {0}",
"Latest": "אחרון",
"MessageApplicationUpdated": "שרת ג'ליפין עודכן",
"MessageApplicationUpdatedTo": "שרת ג'ליפין עודכן לגרסה {0}",
"MessageApplicationUpdated": "שרת Jellyfin עודכן",
"MessageApplicationUpdatedTo": "שרת Jellyfin עודכן לגרסה {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "סעיף הגדרת השרת {0} עודכן",
"MessageServerConfigurationUpdated": "תצורת השרת עודכנה",
"MixedContent": "תוכן מעורב",
@@ -43,7 +43,7 @@
"NameInstallFailed": "התקנת {0} נכשלה",
"NameSeasonNumber": "עונה {0}",
"NameSeasonUnknown": "עונה לא ידועה",
"NewVersionIsAvailable": "גרסה חדשה של שרת ג'ליפין זמינה להורדה.",
"NewVersionIsAvailable": "גרסה חדשה של שרת Jellyfin זמינה להורדה.",
"NotificationOptionApplicationUpdateAvailable": "קיים עדכון זמין ליישום",
"NotificationOptionApplicationUpdateInstalled": "עדכון ליישום הותקן",
"NotificationOptionAudioPlayback": "ניגון שמע החל",
@@ -72,7 +72,7 @@
"ServerNameNeedsToBeRestarted": "{0} דורש הפעלה מחדש",
"Shows": "סדרות",
"Songs": "שירים",
"StartupEmbyServerIsLoading": "שרת ג'ליפין טוען. נא לנסות שוב בקרוב.",
"StartupEmbyServerIsLoading": "שרת Jellyfin בתהליך טעינה. נא לנסות שוב בקרוב.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "הורדת כתוביות מ־{0} עבור {1} נכשלה",
"Sync": "סנכרון",
@@ -100,14 +100,14 @@
"TasksLibraryCategory": "ספרייה",
"TasksMaintenanceCategory": "תחזוקה",
"TaskUpdatePlugins": "עדכן תוספים",
"TaskRefreshPeopleDescription": "מעדכן מטא נתונים עבור שחקנים ובמאים בספריית המדיה שלך.",
"TaskRefreshPeopleDescription": "מעדכן מטא-דאטה עבור שחקנים ובמאים בספריית המדיה שלך.",
"TaskRefreshPeople": "רענן אנשים",
"TaskCleanLogsDescription": "מוחק קבצי יומן בני יותר מ- {0} ימים.",
"TaskCleanLogs": "ניקוי תיקיית יומן",
"TaskRefreshLibraryDescription": "סורק את ספריית המדיה שלך אחר קבצים חדשים ומרענן מטא נתונים.",
"TaskRefreshLibraryDescription": "סורק את ספריית המדיה שלך אחר קבצים חדשים ומרענן מטא-דאטה.",
"TaskRefreshChapterImagesDescription": "יוצר תמונות ממוזערות לסרטונים שיש להם פרקים.",
"TasksChannelsCategory": "ערוצי אינטרנט",
"TaskDownloadMissingSubtitlesDescription": "חפש באינטרנט עבור הכתוביות החסרות בהתבסס על המטהיאטה.",
"TaskDownloadMissingSubtitlesDescription": "חפש באינטרנט כתוביות חסרות בהתבסס על המטא-דאטה.",
"TaskDownloadMissingSubtitles": "הורד כתוביות חסרות",
"TaskRefreshChannelsDescription": "רענן פרטי ערוץ אינטרנטי.",
"TaskRefreshChannels": "רענן ערוץ",
@@ -136,5 +136,7 @@
"TaskMoveTrickplayImages": "העברת מיקום של תמונות Trickplay",
"TaskExtractMediaSegments": "סריקת מדיה",
"TaskExtractMediaSegmentsDescription": "מחלץ חלקי מדיה מתוספים המאפשרים זאת.",
"TaskMoveTrickplayImagesDescription": "הזזת קבצי Trickplay קיימים בהתאם להגדרות הספרייה."
"TaskMoveTrickplayImagesDescription": "הזזת קבצי Trickplay קיימים בהתאם להגדרות הספרייה.",
"CleanupUserDataTaskDescription": "ניקוי כל המידע של המשתמש (מצב צפייה, מועדפים וכו) ממדיה שאינה קיימת מעל 90 יום.",
"CleanupUserDataTask": "משימת ניקוי מידע משתמש"
}
@@ -136,5 +136,7 @@
"TaskDownloadMissingLyricsDescription": "Zenék szövegének letöltése",
"TaskMoveTrickplayImages": "Trickplay képek helyének átköltöztetése",
"TaskMoveTrickplayImagesDescription": "A médiatár-beállításoknak megfelelően áthelyezi a meglévő trickplay fájlokat.",
"TaskExtractMediaSegmentsDescription": "Kinyeri vagy megszerzi a médiaszegmenseket a MediaSegment támogatással rendelkező bővítményekből."
"TaskExtractMediaSegmentsDescription": "Kinyeri vagy megszerzi a médiaszegmenseket a MediaSegment támogatással rendelkező bővítményekből.",
"CleanupUserDataTaskDescription": "Legalább 90 napja nem elérhető médiákhoz kapcsolódó összes felhasználói adat (pl. megtekintési állapot, kedvencek) törlése.",
"CleanupUserDataTask": "Felhasználói adatok tisztítása feladat"
}
@@ -129,5 +129,13 @@
"TaskAudioNormalizationDescription": "Pindai file untuk data normalisasi audio.",
"TaskAudioNormalization": "Normalisasi Audio",
"TaskCleanCollectionsAndPlaylists": "Bersihkan koleksi dan daftar putar",
"TaskCleanCollectionsAndPlaylistsDescription": "Menghapus item dari koleksi dan daftar putar yang sudah tidak ada."
"TaskCleanCollectionsAndPlaylistsDescription": "Menghapus item dari koleksi dan daftar putar yang sudah tidak ada.",
"TaskDownloadMissingLyricsDescription": "Unduh lirik untuk lagu",
"TaskExtractMediaSegmentsDescription": "Mengekstrak atau memperoleh segmen media dari plugin yang mendukung MediaSegment.",
"TaskMoveTrickplayImagesDescription": "Memindahkan file trickplay yang sudah ada sesuai dengan pengaturan pustaka.",
"CleanupUserDataTaskDescription": "Membersihkan semua data pengguna (status tontonan, status favorit, dll.) dari media yang sudah tidak ada selama setidaknya 90 hari.",
"TaskExtractMediaSegments": "Scan Segmen media",
"TaskMoveTrickplayImages": "Migrasikan Lokasi Gambar Trickplay",
"TaskDownloadMissingLyrics": "Unduh Lirik yang Hilang",
"CleanupUserDataTask": "Tugas Pembersihan Data Pengguna"
}
@@ -136,5 +136,7 @@
"TaskMoveTrickplayImages": "Sposta le immagini Trickplay",
"TaskMoveTrickplayImagesDescription": "Sposta le immagini Trickplay esistenti secondo la configurazione della libreria.",
"TaskExtractMediaSegmentsDescription": "Estrae o ottiene segmenti multimediali dai plugin abilitati MediaSegment.",
"TaskExtractMediaSegments": "Scansiona Segmento Media"
"TaskExtractMediaSegments": "Scansiona Segmento Media",
"CleanupUserDataTask": "Task di pulizia dei dati utente",
"CleanupUserDataTaskDescription": "Pulisce tutti i dati utente (stato di visione, status preferiti, ecc.) dai contenuti non più presenti da almeno 90 giorni."
}
@@ -135,5 +135,7 @@
"TaskMoveTrickplayImages": "Trickplayの画像を移動",
"TaskMoveTrickplayImagesDescription": "ライブラリ設定によりTrickplayのファイルを移動。",
"TaskDownloadMissingLyrics": "失われた歌詞をダウンロード",
"TaskExtractMediaSegmentsDescription": "MediaSegment 対応プラグインからメディア セグメントを抽出または取得します。"
"TaskExtractMediaSegmentsDescription": "MediaSegment 対応プラグインからメディア セグメントを抽出または取得します。",
"CleanupUserDataTask": "ユーザーデータのクリーンアップタスク",
"CleanupUserDataTaskDescription": "90日以上存在しないメディアに対して、視聴状態やお気に入り状態などのユーザーデータをすべて削除します。"
}
@@ -25,7 +25,7 @@
"DeviceOfflineWithName": "{0} ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ",
"DeviceOnlineWithName": "{0} ಸಂಪರ್ಕಗೊಂಡಿದೆ",
"External": "ಹೊರಗಿನ",
"FailedLoginAttemptWithUserName": "{0} ರಿಂದ ವಿಫಲ ಲಾಗಿನ್ ಪ್ರಯತ್ನ",
"FailedLoginAttemptWithUserName": "ವಿಫಲ ಲಾಗಿನ್ ಪ್ರಯತ್ನ ಸಂಖ್ಯೆ {0}",
"Favorites": "ಮೆಚ್ಚಿನವುಗಳು",
"Folders": "ಫೋಲ್ಡರ್‌ಗಳು",
"Forced": "ಬಲವಂತವಾಗಿ",
@@ -123,5 +123,13 @@
"TaskUpdatePlugins": "ಪ್ಲಗಿನ್‌ಗಳನ್ನು ನವೀಕರಿಸಿ",
"TaskCleanTranscode": "ಟ್ರಾನ್ಸ್‌ಕೋಡ್ ಡೈರೆಕ್ಟರಿಯನ್ನು ಸ್ವಚ್ಛಗೊಳಿಸಿ",
"TaskRefreshChannels": "ಚಾನಲ್‌ಗಳನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡಿ",
"TaskRefreshChannelsDescription": "ಇಂಟರ್ನೆಟ್ ಚಾನಲ್ ಮಾಹಿತಿಯನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ."
"TaskRefreshChannelsDescription": "ಇಂಟರ್ನೆಟ್ ಚಾನಲ್ ಮಾಹಿತಿಯನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ.",
"TaskAudioNormalizationDescription": "ಧ್ವನಿ ಸಾಮಾನ್ಯೀಕರಣ ಮಾಹಿತಿಗಾಗಿ ಕಡತ‌ಗಳನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡುತ್ತದೆ.",
"TaskDownloadMissingLyricsDescription": "ಹಾಡುಗಳಿಗೆ ಸಾಹಿತ್ಯ ಪಡೆಯಿರಿ",
"TaskExtractMediaSegments": "ಮಾಧ್ಯಮ ವಿಭಾಗದ ಹುಡುಕು",
"TaskDownloadMissingLyrics": "ಇಲ್ಲದ ಸಾಹಿತ್ಯವನ್ನು ಪಡೆಯಿರಿ",
"TaskAudioNormalization": "ಧ್ವನಿ ಸಾಮಾನ್ಯೀಕರಣ",
"TaskRefreshTrickplayImages": "ಟ್ರಿಕ್‌ಪ್ಲೇ ಚಿತ್ರಗಳನ್ನು ರಚಿಸಿ",
"TaskCleanCollectionsAndPlaylists": "ಸಂಗ್ರಹಗಳು ಮತ್ತು ಪ್ಲೇಪಟ್ಟಿಗಳನ್ನು ಸ್ವಚ್ಛಗೊಳಿಸಿ",
"TaskCleanCollectionsAndPlaylistsDescription": "ಇಲ್ಲದ ಸಂಗ್ರಹಗಳು ಮತ್ತು ಪ್ಲೇಪಟ್ಟಿಗಳಿಂದ ವಸ್ತುಗಳನ್ನು ತೆಗೆದುಹಾಕುತ್ತದೆ."
}
@@ -130,5 +130,7 @@
"TaskExtractMediaSegments": "मिडिया विभाग तपासणी",
"TaskMoveTrickplayImages": "ट्रिकप्ले प्रतिमेचे स्थान स्थलांतर करा",
"TaskDownloadMissingLyrics": "उपलब्ध नसलेली गीतपट्टी (Lyrics) डाउनलोड करा",
"TaskAudioNormalization": "ऑडिओ सामान्यीकरण"
"TaskAudioNormalization": "ऑडिओ सामान्यीकरण",
"TaskAudioNormalizationDescription": "ऑडिओ सामान्यीकरणाचा डाटा स्कॅन करतो.",
"TaskDownloadMissingLyricsDescription": "गाण्यांची गीतपट्टी (Lyrics) डाउनलोड करतो"
}
@@ -136,5 +136,7 @@
"TaskCleanCollectionsAndPlaylists": "Bersihkan koleksi dan senarai audio video",
"TaskAudioNormalization": "Normalisasi Audio",
"TaskAudioNormalizationDescription": "Mengimbas fail-fail untuk data normalisasi audio.",
"TaskCleanCollectionsAndPlaylistsDescription": "Mengalih keluar item daripada koleksi dan senarai audio video yang tidak wujud lagi."
"TaskCleanCollectionsAndPlaylistsDescription": "Mengalih keluar item daripada koleksi dan senarai audio video yang tidak wujud lagi.",
"CleanupUserDataTaskDescription": "Membersihkan semua data pengguna (keadaan tontonan, status kegemaran, dan sebagainya) daripada media yang tidak lagi wujud sekurang-kurangnya selama 90 hari.",
"CleanupUserDataTask": "Tugas pembersihan data pengguna"
}
@@ -136,5 +136,7 @@
"TaskMoveTrickplayImagesDescription": "Move os arquivos do trickplay de acordo com as configurações da biblioteca.",
"TaskExtractMediaSegments": "Varredura do segmento de mídia",
"TaskExtractMediaSegmentsDescription": "Extrai ou obtém segmentos de mídia de plug-ins habilitados para MediaSegment.",
"TaskMoveTrickplayImages": "Migrar o local da imagem do Trickplay"
"TaskMoveTrickplayImages": "Migrar o local da imagem do Trickplay",
"CleanupUserDataTask": "Tarefa de limpeza de dados do usuário",
"CleanupUserDataTaskDescription": "Limpa todos os dados do usuário (estado de visualização, status de favorito, etc.) de mídias que não estão presentes por pelo menos 90 dias."
}
@@ -136,5 +136,7 @@
"TaskMoveTrickplayImages": "Migrar a localização da imagem do Trickplay",
"TaskDownloadMissingLyricsDescription": "Transferir letra para músicas",
"TaskExtractMediaSegmentsDescription": "Extrai ou obtém segmentos de multimédia a partir de plugins com suporte para MediaSegment.",
"TaskMoveTrickplayImagesDescription": "Move os ficheiros trickplay existentes de acordo com as definições da mediateca."
"TaskMoveTrickplayImagesDescription": "Move os ficheiros trickplay existentes de acordo com as definições da mediateca.",
"CleanupUserDataTaskDescription": "Apaga todos os dados de utilizador (estados de reprodução, favoritos, etc) de arquivos média não presentes há 90 dias ou mais.",
"CleanupUserDataTask": "Limpeza de dados de utilizador"
}
@@ -98,7 +98,7 @@
"TaskCleanTranscodeDescription": "Șterge fișierele de transcodare mai vechi de o zi.",
"TaskCleanTranscode": "Curățați directorul de transcodare",
"TaskUpdatePluginsDescription": "Descarcă și instalează actualizări pentru extensiile care sunt configurate să se actualizeze automat.",
"TaskUpdatePlugins": "Actualizați Extensile",
"TaskUpdatePlugins": "Actualizați Extensiile",
"TaskRefreshPeopleDescription": "Actualizează metadatele pentru actori și regizori din biblioteca media.",
"TaskRefreshPeople": "Actualizează Persoanele",
"TaskCleanLogsDescription": "Șterge fișierele jurnal care au mai mult de {0} zile.",
@@ -135,5 +135,7 @@
"TaskExtractMediaSegmentsDescription": "Extrage sau obține segmentele media de la pluginurile MediaSegment activate.",
"TaskMoveTrickplayImages": "Migrează locația imaginii Trickplay",
"TaskDownloadMissingLyrics": "Descarcă versurile lipsă",
"TaskDownloadMissingLyricsDescription": "Descarcă versuri pentru melodii"
"TaskDownloadMissingLyricsDescription": "Descarcă versuri pentru melodii",
"CleanupUserDataTask": "Sarcina de curatare a datelor utilizatorului",
"CleanupUserDataTaskDescription": "Sterge toate datele utilizatorului (starea vizionarii, starea favoritelor etc.) de pe suporturile media care nu mai sunt prezente timp de cel puțin 90 de zile."
}
@@ -138,5 +138,5 @@
"TaskExtractMediaSegmentsDescription": "Извлекает или получает медиасегменты из плагинов MediaSegment.",
"TaskMoveTrickplayImagesDescription": "Перемещает существующие файлы trickplay в соответствии с настройками медиатеки.",
"CleanupUserDataTask": "Задача очистки пользовательских данных",
"CleanupUserDataTaskDescription": "Очищает все пользовательские данные (состояние просмотра, статус избранного и т.д.) с носителей, на которых больше нет информации, по крайней мере, в течение 90 дней."
"CleanupUserDataTaskDescription": "Очищает все пользовательские данные (состояние просмотра, статус избранного и т.д.) с медиа, отсутствующих по меньшей мере в течение 90 дней."
}
@@ -136,5 +136,7 @@
"TaskMoveTrickplayImages": "Presunúť umiestnenie obrázkov Trickplay",
"TaskMoveTrickplayImagesDescription": "Presunie existujúce súbory Trickplay podľa nastavení knižnice.",
"TaskDownloadMissingLyrics": "Stiahnuť chýbajúce texty piesní",
"TaskDownloadMissingLyricsDescription": "Stiahne texty pre piesne"
"TaskDownloadMissingLyricsDescription": "Stiahne texty pre piesne",
"CleanupUserDataTask": "Prečistiť používateľské dáta",
"CleanupUserDataTaskDescription": "Vyčistí všetky dáta používateľa (stav sledovania, stav obľúbených atď.) z médií, ktoré už neexistujú aspoň 90 dní."
}
@@ -21,7 +21,7 @@
"Inherit": "மரபுரிமையாகப் பெறு",
"HeaderRecordingGroups": "பதிவு குழுக்கள்",
"Folders": "கோப்புறைகள்",
"FailedLoginAttemptWithUserName": "{0} இன் உள்நுழைவு முயற்சி தோல்வியடைந்தது",
"FailedLoginAttemptWithUserName": "{0} இலிருந்து உள்நுழைவு முயற்சி தோல்வியடைந்தது",
"DeviceOnlineWithName": "{0} இணைக்கப்பட்டது",
"DeviceOfflineWithName": "{0} துண்டிக்கப்பட்டது",
"Collections": "தொகுப்புகள்",
@@ -133,5 +133,9 @@
"TaskDownloadMissingLyrics": "விடுபட்ட பாடல் வரிகளைப் பதிவிறக்கவும்",
"TaskDownloadMissingLyricsDescription": "பாடல்களுக்கான வரிகளைப் பதிவிறக்குகிறது",
"TaskMoveTrickplayImages": "ட்ரிக்பிளே பட இருப்பிடத்தை நகர்த்து",
"TaskMoveTrickplayImagesDescription": "நூலக அமைப்புகளுக்கு ஏற்ப ஏற்கனவே உள்ள ட்ரிக்பிளே கோப்புகளை நகர்த்துகிறது."
"TaskMoveTrickplayImagesDescription": "நூலக அமைப்புகளுக்கு ஏற்ப ஏற்கனவே உள்ள ட்ரிக்பிளே கோப்புகளை நகர்த்துகிறது.",
"TaskExtractMediaSegments": "மீடியா பிரிவு ஸ்கேன்",
"TaskExtractMediaSegmentsDescription": "மீடியாசெக்மென்ட் இயக்கப்பட்ட செருகுநிரல்களிலிருந்து மீடியா பிரிவுகளைப் பிரித்தெடுக்கிறது அல்லது பெறுகிறது.",
"CleanupUserDataTaskDescription": "குறைந்தது 90 நாட்களுக்கு இல்லாத மீடியாவிலிருந்து அனைத்து பயனர் தரவையும் (கண்காணிப்பு நிலை, பிடித்த நிலை போன்றவை) சுத்தம் செய்கிறது.",
"CleanupUserDataTask": "பயனர் தரவை சுத்தம் செய்யும் பணி"
}
@@ -136,5 +136,7 @@
"TaskMoveTrickplayImagesDescription": "Mevcut trickplay dosyalarını kütüphane ayarlarına göre taşır.",
"TaskDownloadMissingLyrics": "Eksik şarkı sözlerini indir",
"TaskDownloadMissingLyricsDescription": "Şarkı sözlerini indirir",
"TaskExtractMediaSegmentsDescription": "MediaSegment özelliği etkin olan eklentilerden medya segmentlerini çıkarır veya alır."
"TaskExtractMediaSegmentsDescription": "MediaSegment özelliği etkin olan eklentilerden medya segmentlerini çıkarır veya alır.",
"CleanupUserDataTask": "Kullanıcı verisi temizleme görevi",
"CleanupUserDataTaskDescription": "En az 90 gün boyunca artık mevcut olmayan medyadaki tüm kullanıcı verilerini (İzleme durumu, favori durumu vb.) temizler."
}
@@ -423,7 +423,7 @@ namespace Emby.Server.Implementations.Plugins
Overview = packageInfo.Overview,
Owner = packageInfo.Owner,
TargetAbi = versionInfo.TargetAbi ?? string.Empty,
Timestamp = string.IsNullOrEmpty(versionInfo.Timestamp) ? DateTime.MinValue : DateTime.Parse(versionInfo.Timestamp, CultureInfo.InvariantCulture),
Timestamp = string.IsNullOrEmpty(versionInfo.Timestamp) ? DateTime.MinValue : DateTime.Parse(versionInfo.Timestamp, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal),
Version = versionInfo.Version,
Status = status == PluginStatus.Disabled ? PluginStatus.Disabled : PluginStatus.Active, // Keep disabled state.
AutoUpdate = true,
@@ -474,6 +474,7 @@ namespace Emby.Server.Implementations.Session
private void RemoveNowPlayingItem(SessionInfo session)
{
session.NowPlayingItem = null;
session.FullNowPlayingItem = null;
session.PlayState = new PlayerStateInfo();
if (!string.IsNullOrEmpty(session.DeviceId))
@@ -5,7 +5,6 @@ using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Sorting
{
@@ -256,7 +256,7 @@ public sealed class BaseItemRepository
dbQuery = ApplyGroupingFilter(dbQuery, filter);
dbQuery = ApplyQueryPaging(dbQuery, filter);
result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserialiseBaseItem(w, filter.SkipDeserialization)).ToArray();
result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
result.StartIndex = filter.StartIndex ?? 0;
return result;
}
@@ -275,7 +275,7 @@ public sealed class BaseItemRepository
dbQuery = ApplyGroupingFilter(dbQuery, filter);
dbQuery = ApplyQueryPaging(dbQuery, filter);
return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserialiseBaseItem(w, filter.SkipDeserialization)).ToArray();
return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
}
/// <inheritdoc/>
@@ -317,7 +317,7 @@ public sealed class BaseItemRepository
mainquery = ApplyGroupingFilter(mainquery, filter);
mainquery = ApplyQueryPaging(mainquery, filter);
return mainquery.AsEnumerable().Where(e => e is not null).Select(w => DeserialiseBaseItem(w, filter.SkipDeserialization)).ToArray();
return mainquery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
}
/// <inheritdoc />
@@ -540,7 +540,7 @@ public sealed class BaseItemRepository
}
var itemValueMaps = tuples
.Select(e => (Item: e.Item, Values: GetItemValuesToSave(e.Item, e.InheritedTags)))
.Select(e => (e.Item, Values: GetItemValuesToSave(e.Item, e.InheritedTags)))
.ToArray();
var allListedItemValues = itemValueMaps
.SelectMany(f => f.Values)
@@ -567,7 +567,7 @@ public sealed class BaseItemRepository
var itemValuesStore = existingValues.Concat(missingItemValues).ToArray();
var valueMap = itemValueMaps
.Select(f => (Item: f.Item, Values: f.Values.Select(e => itemValuesStore.First(g => g.Value == e.Value && g.Type == e.MagicNumber)).ToArray()))
.Select(f => (f.Item, Values: f.Values.Select(e => itemValuesStore.First(g => g.Value == e.Value && g.Type == e.MagicNumber)).DistinctBy(e => e.ItemValueId).ToArray()))
.ToArray();
var mappedValues = context.ItemValuesMap.Where(e => ids.Contains(e.ItemId)).ToList();
@@ -655,7 +655,7 @@ public sealed class BaseItemRepository
return null;
}
return DeserialiseBaseItem(item);
return DeserializeBaseItem(item);
}
/// <summary>
@@ -701,12 +701,12 @@ public sealed class BaseItemRepository
dto.TotalBitrate = entity.TotalBitrate;
dto.ExternalId = entity.ExternalId;
dto.Size = entity.Size;
dto.Genres = entity.Genres?.Split('|') ?? [];
dto.DateCreated = entity.DateCreated.GetValueOrDefault();
dto.DateModified = entity.DateModified.GetValueOrDefault();
dto.Genres = string.IsNullOrWhiteSpace(entity.Genres) ? [] : entity.Genres.Split('|');
dto.DateCreated = entity.DateCreated ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
dto.DateModified = entity.DateModified ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
dto.ChannelId = entity.ChannelId ?? Guid.Empty;
dto.DateLastRefreshed = entity.DateLastRefreshed.GetValueOrDefault();
dto.DateLastSaved = entity.DateLastSaved.GetValueOrDefault();
dto.DateLastRefreshed = entity.DateLastRefreshed ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
dto.DateLastSaved = entity.DateLastSaved ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
dto.OwnerId = string.IsNullOrWhiteSpace(entity.OwnerId) ? Guid.Empty : (Guid.TryParse(entity.OwnerId, out var ownerId) ? ownerId : Guid.Empty);
dto.Width = entity.Width.GetValueOrDefault();
dto.Height = entity.Height.GetValueOrDefault();
@@ -733,7 +733,7 @@ public sealed class BaseItemRepository
dto.ExtraIds = string.IsNullOrWhiteSpace(entity.ExtraIds) ? [] : entity.ExtraIds.Split('|').Select(e => Guid.Parse(e)).ToArray();
dto.ProductionLocations = entity.ProductionLocations?.Split('|') ?? [];
dto.Studios = entity.Studios?.Split('|') ?? [];
dto.Tags = entity.Tags?.Split('|') ?? [];
dto.Tags = string.IsNullOrWhiteSpace(entity.Tags) ? [] : entity.Tags.Split('|');
if (dto is IHasProgramAttributes hasProgramAttributes)
{
@@ -807,7 +807,7 @@ public sealed class BaseItemRepository
if (dto is Folder folder)
{
folder.DateLastMediaAdded = entity.DateLastMediaAdded;
folder.DateLastMediaAdded = entity.DateLastMediaAdded ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
}
return dto;
@@ -867,11 +867,11 @@ public sealed class BaseItemRepository
entity.ExternalId = dto.ExternalId;
entity.Size = dto.Size;
entity.Genres = string.Join('|', dto.Genres);
entity.DateCreated = dto.DateCreated;
entity.DateModified = dto.DateModified;
entity.DateCreated = dto.DateCreated == DateTime.MinValue ? null : dto.DateCreated;
entity.DateModified = dto.DateModified == DateTime.MinValue ? null : dto.DateModified;
entity.ChannelId = dto.ChannelId;
entity.DateLastRefreshed = dto.DateLastRefreshed;
entity.DateLastSaved = dto.DateLastSaved;
entity.DateLastRefreshed = dto.DateLastRefreshed == DateTime.MinValue ? null : dto.DateLastRefreshed;
entity.DateLastSaved = dto.DateLastSaved == DateTime.MinValue ? null : dto.DateLastSaved;
entity.OwnerId = dto.OwnerId.ToString();
entity.Width = dto.Width;
entity.Height = dto.Height;
@@ -981,7 +981,7 @@ public sealed class BaseItemRepository
if (dto is Folder folder)
{
entity.DateLastMediaAdded = folder.DateLastMediaAdded;
entity.DateLastMediaAdded = folder.DateLastMediaAdded == DateTime.MinValue ? null : folder.DateLastMediaAdded;
entity.IsFolder = folder.IsFolder;
}
@@ -1017,7 +1017,7 @@ public sealed class BaseItemRepository
return type.GetCustomAttribute<RequiresSourceSerialisationAttribute>() == null;
}
private BaseItemDto DeserialiseBaseItem(BaseItemEntity baseItemEntity, bool skipDeserialization = false)
private BaseItemDto DeserializeBaseItem(BaseItemEntity baseItemEntity, bool skipDeserialization = false)
{
ArgumentNullException.ThrowIfNull(baseItemEntity, nameof(baseItemEntity));
if (_serverConfigurationManager?.Configuration is null)
@@ -1026,7 +1026,7 @@ public sealed class BaseItemRepository
}
var typeToSerialise = GetType(baseItemEntity.Type);
return BaseItemRepository.DeserialiseBaseItem(
return BaseItemRepository.DeserializeBaseItem(
baseItemEntity,
_logger,
_appHost,
@@ -1034,7 +1034,7 @@ public sealed class BaseItemRepository
}
/// <summary>
/// Deserialises a BaseItemEntity and sets all properties.
/// Deserializes a BaseItemEntity and sets all properties.
/// </summary>
/// <param name="baseItemEntity">The DB entity.</param>
/// <param name="logger">Logger.</param>
@@ -1042,9 +1042,9 @@ public sealed class BaseItemRepository
/// <param name="skipDeserialization">If only mapping should be processed.</param>
/// <returns>A mapped BaseItem.</returns>
/// <exception cref="InvalidOperationException">Will be thrown if an invalid serialisation is requested.</exception>
public static BaseItemDto DeserialiseBaseItem(BaseItemEntity baseItemEntity, ILogger logger, IServerApplicationHost? appHost, bool skipDeserialization = false)
public static BaseItemDto DeserializeBaseItem(BaseItemEntity baseItemEntity, ILogger logger, IServerApplicationHost? appHost, bool skipDeserialization = false)
{
var type = GetType(baseItemEntity.Type) ?? throw new InvalidOperationException("Cannot deserialise unknown type.");
var type = GetType(baseItemEntity.Type) ?? throw new InvalidOperationException("Cannot deserialize unknown type.");
BaseItemDto? dto = null;
if (TypeRequiresDeserialization(type) && baseItemEntity.Data is not null && !skipDeserialization)
{
@@ -1060,7 +1060,7 @@ public sealed class BaseItemRepository
if (dto is null)
{
dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deserialise unknown type.");
dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deserialize unknown type.");
}
return Map(baseItemEntity, dto, appHost);
@@ -1206,7 +1206,7 @@ public sealed class BaseItemRepository
.Where(e => e is not null)
.Select(e =>
{
return (DeserialiseBaseItem(e.item, filter.SkipDeserialization), e.itemCount);
return (DeserializeBaseItem(e.item, filter.SkipDeserialization), e.itemCount);
})
];
}
@@ -1221,7 +1221,7 @@ public sealed class BaseItemRepository
.Where(e => e is not null)
.Select<BaseItemEntity, (BaseItemDto, ItemCounts?)>(e =>
{
return (DeserialiseBaseItem(e, filter.SkipDeserialization), null);
return (DeserializeBaseItem(e, filter.SkipDeserialization), null);
})
];
}
@@ -1302,7 +1302,7 @@ public sealed class BaseItemRepository
{
Path = appHost?.ExpandVirtualPath(e.Path) ?? e.Path,
BlurHash = e.Blurhash is null ? null : Encoding.UTF8.GetString(e.Blurhash),
DateModified = e.DateModified,
DateModified = e.DateModified ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc),
Height = e.Height,
Width = e.Width,
Type = (ImageType)e.ImageType
@@ -253,7 +253,7 @@ namespace Jellyfin.Server.Extensions
c.AddSwaggerTypeMappings();
c.SchemaFilter<IgnoreEnumSchemaFilter>();
c.OperationFilter<RetryOnTemporarlyUnavailableFilter>();
c.OperationFilter<RetryOnTemporarilyUnavailableFilter>();
c.OperationFilter<SecurityRequirementsOperationFilter>();
c.OperationFilter<FileResponseFilter>();
c.OperationFilter<FileRequestFilter>();
@@ -6,13 +6,13 @@ using Swashbuckle.AspNetCore.SwaggerGen;
namespace Jellyfin.Server.Filters;
internal class RetryOnTemporarlyUnavailableFilter : IOperationFilter
internal class RetryOnTemporarilyUnavailableFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
operation.Responses.Add("503", new OpenApiResponse()
{
Description = "The server is currently starting or is temporarly not available.",
Description = "The server is currently starting or is temporarily not available.",
Headers = new Dictionary<string, OpenApiHeader>()
{
{
+1
View File
@@ -53,6 +53,7 @@
<PackageReference Include="prometheus-net.AspNetCore" />
<PackageReference Include="Serilog.AspNetCore" />
<PackageReference Include="Serilog.Enrichers.Thread" />
<PackageReference Include="Serilog.Expressions" />
<PackageReference Include="Serilog.Settings.Configuration" />
<PackageReference Include="Serilog.Sinks.Async" />
<PackageReference Include="Serilog.Sinks.Console" />
@@ -0,0 +1,168 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Database.Implementations;
using Jellyfin.Server.ServerSetupApp;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Migrations.Routines;
/// <summary>
/// Migration to fix dates saved in the database to always be UTC.
/// </summary>
[JellyfinMigration("2025-06-20T18:00:00", nameof(FixDates))]
public class FixDates : IAsyncMigrationRoutine
{
private const int PageSize = 5000;
private readonly ILogger _logger;
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
/// <summary>
/// Initializes a new instance of the <see cref="FixDates"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="startupLogger">The startup logger for Startup UI integration.</param>
/// <param name="dbProvider">Instance of the <see cref="IDbContextFactory{JellyfinDbContext}"/> interface.</param>
public FixDates(
ILogger<FixDates> logger,
IStartupLogger<FixDates> startupLogger,
IDbContextFactory<JellyfinDbContext> dbProvider)
{
_logger = startupLogger.With(logger);
_dbProvider = dbProvider;
}
/// <inheritdoc />
public async Task PerformAsync(CancellationToken cancellationToken)
{
if (!TimeZoneInfo.Local.Equals(TimeZoneInfo.Utc))
{
using var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
var sw = Stopwatch.StartNew();
await FixBaseItemsAsync(context, sw, cancellationToken).ConfigureAwait(false);
sw.Reset();
await FixChaptersAsync(context, sw, cancellationToken).ConfigureAwait(false);
sw.Reset();
await FixBaseItemImageInfos(context, sw, cancellationToken).ConfigureAwait(false);
}
}
private async Task FixBaseItemsAsync(JellyfinDbContext context, Stopwatch sw, CancellationToken cancellationToken)
{
int itemCount = 0;
var baseQuery = context.BaseItems.OrderBy(e => e.Id);
var records = baseQuery.Count();
_logger.LogInformation("Fixing dates for {Count} BaseItems.", records);
sw.Start();
await foreach (var result in context.BaseItems.OrderBy(e => e.Id)
.WithPartitionProgress(
(partition) =>
_logger.LogInformation(
"Processing BaseItems batch {BatchNumber} ({ProcessedSoFar}/{TotalRecords}) - Time: {ElapsedTime}",
partition + 1,
Math.Min((partition + 1) * PageSize, records),
records,
sw.Elapsed))
.PartitionEagerAsync(PageSize, cancellationToken)
.WithCancellation(cancellationToken)
.ConfigureAwait(false))
{
result.DateCreated = ToUniversalTime(result.DateCreated);
result.DateLastMediaAdded = ToUniversalTime(result.DateLastMediaAdded);
result.DateLastRefreshed = ToUniversalTime(result.DateLastRefreshed);
result.DateLastSaved = ToUniversalTime(result.DateLastSaved);
result.DateModified = ToUniversalTime(result.DateModified);
itemCount++;
}
var saveCount = await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
_logger.LogInformation("BaseItems: Processed {ItemCount} items, saved {SaveCount} changes in {ElapsedTime}", itemCount, saveCount, sw.Elapsed);
}
private async Task FixChaptersAsync(JellyfinDbContext context, Stopwatch sw, CancellationToken cancellationToken)
{
int itemCount = 0;
var baseQuery = context.Chapters;
var records = baseQuery.Count();
_logger.LogInformation("Fixing dates for {Count} Chapters.", records);
sw.Start();
await foreach (var result in context.Chapters.OrderBy(e => e.ItemId)
.WithPartitionProgress(
(partition) =>
_logger.LogInformation(
"Processing Chapter batch {BatchNumber} ({ProcessedSoFar}/{TotalRecords}) - Time: {ElapsedTime}",
partition + 1,
Math.Min((partition + 1) * PageSize, records),
records,
sw.Elapsed))
.PartitionEagerAsync(PageSize, cancellationToken)
.WithCancellation(cancellationToken)
.ConfigureAwait(false))
{
result.ImageDateModified = ToUniversalTime(result.ImageDateModified, true);
itemCount++;
}
var saveCount = await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
_logger.LogInformation("Chapters: Processed {ItemCount} items, saved {SaveCount} changes in {ElapsedTime}", itemCount, saveCount, sw.Elapsed);
}
private async Task FixBaseItemImageInfos(JellyfinDbContext context, Stopwatch sw, CancellationToken cancellationToken)
{
int itemCount = 0;
var baseQuery = context.BaseItemImageInfos;
var records = baseQuery.Count();
_logger.LogInformation("Fixing dates for {Count} BaseItemImageInfos.", records);
sw.Start();
await foreach (var result in context.BaseItemImageInfos.OrderBy(e => e.Id)
.WithPartitionProgress(
(partition) =>
_logger.LogInformation(
"Processing BaseItemImageInfos batch {BatchNumber} ({ProcessedSoFar}/{TotalRecords}) - Time: {ElapsedTime}",
partition + 1,
Math.Min((partition + 1) * PageSize, records),
records,
sw.Elapsed))
.PartitionEagerAsync(PageSize, cancellationToken)
.WithCancellation(cancellationToken)
.ConfigureAwait(false))
{
result.DateModified = ToUniversalTime(result.DateModified);
itemCount++;
}
var saveCount = await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
_logger.LogInformation("BaseItemImageInfos: Processed {ItemCount} items, saved {SaveCount} changes in {ElapsedTime}", itemCount, saveCount, sw.Elapsed);
}
private DateTime? ToUniversalTime(DateTime? dateTime, bool isUTC = false)
{
if (dateTime is null)
{
return null;
}
if (dateTime.Value.Year == 1 && dateTime.Value.Month == 1 && dateTime.Value.Day == 1)
{
return null;
}
if (dateTime.Value.Kind == DateTimeKind.Utc || isUTC)
{
return dateTime.Value;
}
return dateTime.Value.ToUniversalTime();
}
}
@@ -94,7 +94,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
connection.Open();
var baseItemIds = new HashSet<Guid>();
using (var operation = GetPreparedDbContext("moving TypedBaseItem"))
using (var operation = GetPreparedDbContext("Moving TypedBaseItem"))
{
const string typedBaseItemsQuery =
"""
@@ -121,13 +121,13 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
}
}
using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.BaseItems.Local.Count} BaseItem entries", _logger))
using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.BaseItems.Local.Count} BaseItem entries", _logger))
{
operation.JellyfinDbContext.SaveChanges();
}
}
using (var operation = GetPreparedDbContext("moving ItemValues"))
using (var operation = GetPreparedDbContext("Moving ItemValues"))
{
// do not migrate inherited types as they are now properly mapped in search and lookup.
const string itemValueQuery =
@@ -138,7 +138,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
// EFCores local lookup sucks. We cannot use context.ItemValues.Local here because its just super slow.
var localItems = new Dictionary<(int Type, string Value), (Database.Implementations.Entities.ItemValue ItemValue, List<Guid> ItemIds)>();
using (new TrackedMigrationStep("loading ItemValues", _logger))
using (new TrackedMigrationStep("Loading ItemValues", _logger))
{
foreach (SqliteDataReader dto in connection.Query(itemValueQuery))
{
@@ -166,13 +166,13 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
}
}
using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.ItemValues.Local.Count} ItemValues entries", _logger))
using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.ItemValues.Local.Count} ItemValues entries", _logger))
{
operation.JellyfinDbContext.SaveChanges();
}
}
using (var operation = GetPreparedDbContext("moving UserData"))
using (var operation = GetPreparedDbContext("Moving UserData"))
{
var queryResult = connection.Query(
"""
@@ -181,14 +181,14 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.UserDataKey = UserDatas.key)
""");
using (new TrackedMigrationStep("loading UserData", _logger))
using (new TrackedMigrationStep("Loading UserData", _logger))
{
var users = operation.JellyfinDbContext.Users.AsNoTracking().ToImmutableArray();
var users = operation.JellyfinDbContext.Users.AsNoTracking().ToArray();
var userIdBlacklist = new HashSet<int>();
foreach (var entity in queryResult)
{
var userData = GetUserData(users, entity, userIdBlacklist);
var userData = GetUserData(users, entity, userIdBlacklist, _logger);
if (userData is null)
{
var userDataId = entity.GetString(0);
@@ -212,19 +212,17 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
userData.ItemId = refItem.Id;
operation.JellyfinDbContext.UserData.Add(userData);
}
users.Clear();
}
legacyBaseItemWithUserKeys.Clear();
using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.UserData.Local.Count} UserData entries", _logger))
using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.UserData.Local.Count} UserData entries", _logger))
{
operation.JellyfinDbContext.SaveChanges();
}
}
using (var operation = GetPreparedDbContext("moving MediaStreamInfos"))
using (var operation = GetPreparedDbContext("Moving MediaStreamInfos"))
{
const string mediaStreamQuery =
"""
@@ -237,7 +235,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = MediaStreams.ItemId)
""";
using (new TrackedMigrationStep("loading MediaStreamInfos", _logger))
using (new TrackedMigrationStep("Loading MediaStreamInfos", _logger))
{
foreach (SqliteDataReader dto in connection.Query(mediaStreamQuery))
{
@@ -245,13 +243,13 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
}
}
using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.MediaStreamInfos.Local.Count} MediaStreamInfos entries", _logger))
using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.MediaStreamInfos.Local.Count} MediaStreamInfos entries", _logger))
{
operation.JellyfinDbContext.SaveChanges();
}
}
using (var operation = GetPreparedDbContext("moving AttachmentStreamInfos"))
using (var operation = GetPreparedDbContext("Moving AttachmentStreamInfos"))
{
const string mediaAttachmentQuery =
"""
@@ -260,7 +258,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = mediaattachments.ItemId)
""";
using (new TrackedMigrationStep("loading AttachmentStreamInfos", _logger))
using (new TrackedMigrationStep("Loading AttachmentStreamInfos", _logger))
{
foreach (SqliteDataReader dto in connection.Query(mediaAttachmentQuery))
{
@@ -268,13 +266,13 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
}
}
using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.AttachmentStreamInfos.Local.Count} AttachmentStreamInfos entries", _logger))
using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.AttachmentStreamInfos.Local.Count} AttachmentStreamInfos entries", _logger))
{
operation.JellyfinDbContext.SaveChanges();
}
}
using (var operation = GetPreparedDbContext("moving People"))
using (var operation = GetPreparedDbContext("Moving People"))
{
const string personsQuery =
"""
@@ -284,14 +282,14 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
var peopleCache = new Dictionary<string, (People Person, List<PeopleBaseItemMap> Items)>();
using (new TrackedMigrationStep("loading People", _logger))
using (new TrackedMigrationStep("Loading People", _logger))
{
foreach (SqliteDataReader reader in connection.Query(personsQuery))
{
var itemId = reader.GetGuid(0);
if (!baseItemIds.Contains(itemId))
{
_logger.LogError("Dont save person {0} because its not in use by any BaseItem", reader.GetString(1));
_logger.LogError("Not saving person {0} because it's not in use by any BaseItem", reader.GetString(1));
continue;
}
@@ -330,13 +328,13 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
peopleCache.Clear();
}
using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.Peoples.Local.Count} People entries and {operation.JellyfinDbContext.PeopleBaseItemMap.Local.Count} maps", _logger))
using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.Peoples.Local.Count} People entries and {operation.JellyfinDbContext.PeopleBaseItemMap.Local.Count} maps", _logger))
{
operation.JellyfinDbContext.SaveChanges();
}
}
using (var operation = GetPreparedDbContext("moving Chapters"))
using (var operation = GetPreparedDbContext("Moving Chapters"))
{
const string chapterQuery =
"""
@@ -344,7 +342,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = Chapters2.ItemId)
""";
using (new TrackedMigrationStep("loading Chapters", _logger))
using (new TrackedMigrationStep("Loading Chapters", _logger))
{
foreach (SqliteDataReader dto in connection.Query(chapterQuery))
{
@@ -353,13 +351,13 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
}
}
using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.Chapters.Local.Count} Chapters entries", _logger))
using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.Chapters.Local.Count} Chapters entries", _logger))
{
operation.JellyfinDbContext.SaveChanges();
}
}
using (var operation = GetPreparedDbContext("moving AncestorIds"))
using (var operation = GetPreparedDbContext("Moving AncestorIds"))
{
const string ancestorIdsQuery =
"""
@@ -370,7 +368,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = AncestorIds.AncestorId)
""";
using (new TrackedMigrationStep("loading AncestorIds", _logger))
using (new TrackedMigrationStep("Loading AncestorIds", _logger))
{
foreach (SqliteDataReader dto in connection.Query(ancestorIdsQuery))
{
@@ -379,7 +377,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
}
}
using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.AncestorIds.Local.Count} AncestorId entries", _logger))
using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.AncestorIds.Local.Count} AncestorId entries", _logger))
{
operation.JellyfinDbContext.SaveChanges();
}
@@ -404,19 +402,20 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
return new DatabaseMigrationStep(dbContext, operationName, _logger);
}
private UserData? GetUserData(ImmutableArray<User> users, SqliteDataReader dto, HashSet<int> userIdBlacklist)
internal static UserData? GetUserData(User[] users, SqliteDataReader dto, HashSet<int> userIdBlacklist, ILogger logger)
{
var internalUserId = dto.GetInt32(1);
var user = users.FirstOrDefault(e => e.InternalId == internalUserId);
if (userIdBlacklist.Contains(internalUserId))
{
return null;
}
var user = users.FirstOrDefault(e => e.InternalId == internalUserId);
if (user is null)
{
if (userIdBlacklist.Contains(internalUserId))
{
return null;
}
userIdBlacklist.Add(internalUserId);
_logger.LogError("Tried to find user with index '{Idx}' but there are only '{MaxIdx}' users.", internalUserId, users.Length);
logger.LogError("Tried to find user with index '{Idx}' but there are only '{MaxIdx}' users.", internalUserId, users.Length);
return null;
}
@@ -1168,7 +1167,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
entity.UnratedType = unratedType;
}
var baseItem = BaseItemRepository.DeserialiseBaseItem(entity, _logger, null, false);
var baseItem = BaseItemRepository.DeserializeBaseItem(entity, _logger, null, false);
var dataKeys = baseItem.GetUserDataKeys();
userDataKeys.AddRange(dataKeys);
@@ -0,0 +1,107 @@
#pragma warning disable RS0030 // Do not use banned APIs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.Data;
using Jellyfin.Database.Implementations;
using Jellyfin.Server.Implementations.Item;
using Jellyfin.Server.ServerSetupApp;
using MediaBrowser.Controller;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Migrations.Routines;
[JellyfinMigration("2025-06-18T01:00:00", nameof(MigrateLibraryUserData))]
[JellyfinMigrationBackup(JellyfinDb = true)]
internal class MigrateLibraryUserData : IAsyncMigrationRoutine
{
private const string DbFilename = "library.db.old";
private readonly IStartupLogger _logger;
private readonly IServerApplicationPaths _paths;
private readonly IDbContextFactory<JellyfinDbContext> _provider;
public MigrateLibraryUserData(
IStartupLogger<MigrateLibraryDb> startupLogger,
IDbContextFactory<JellyfinDbContext> provider,
IServerApplicationPaths paths)
{
_logger = startupLogger;
_provider = provider;
_paths = paths;
}
public async Task PerformAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Migrating the userdata from library.db.old may take a while, do not stop Jellyfin.");
var dataPath = _paths.DataPath;
var libraryDbPath = Path.Combine(dataPath, DbFilename);
if (!File.Exists(libraryDbPath))
{
_logger.LogError("Cannot migrate userdata from {LibraryDb} as it does not exist. This migration expects the MigrateLibraryDb to run first.", libraryDbPath);
return;
}
var dbContext = await _provider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
if (!await dbContext.BaseItems.AnyAsync(e => e.Id == BaseItemRepository.PlaceholderId, cancellationToken).ConfigureAwait(false))
{
// the placeholder baseitem has been deleted by the librarydb migration so we need to readd it.
await dbContext.BaseItems.AddAsync(
new Database.Implementations.Entities.BaseItemEntity()
{
Id = BaseItemRepository.PlaceholderId,
Type = "PLACEHOLDER",
Name = "This is a placeholder item for UserData that has been detacted from its original item"
},
cancellationToken)
.ConfigureAwait(false);
await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}
var users = dbContext.Users.AsNoTracking().ToArray();
var userIdBlacklist = new HashSet<int>();
using var connection = new SqliteConnection($"Filename={libraryDbPath};Mode=ReadOnly");
var retentionDate = DateTime.UtcNow;
var queryResult = connection.Query(
"""
SELECT key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex FROM UserDatas
WHERE NOT EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.UserDataKey = UserDatas.key)
""");
foreach (var entity in queryResult)
{
var userData = MigrateLibraryDb.GetUserData(users, entity, userIdBlacklist, _logger);
if (userData is null)
{
var userDataId = entity.GetString(0);
var internalUserId = entity.GetInt32(1);
if (!userIdBlacklist.Contains(internalUserId))
{
_logger.LogError("Was not able to migrate user data with key {0} because its id {InternalId} does not match any existing user.", userDataId, internalUserId);
userIdBlacklist.Add(internalUserId);
}
continue;
}
userData.ItemId = BaseItemRepository.PlaceholderId;
userData.RetentionDate = retentionDate;
dbContext.UserData.Add(userData);
}
_logger.LogInformation("Try saving {NewSaved} UserData entries.", dbContext.UserData.Local.Count);
await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}
}
}
@@ -82,7 +82,7 @@ internal class CodeMigration(Type migrationType, JellyfinMigrationAttribute meta
}
}
private class NestedStartupLogger<TCategory> : StartupLogger<TCategory>, IStartupLogger<TCategory>
private class NestedStartupLogger<TCategory> : StartupLogger<TCategory>
{
public NestedStartupLogger(ILogger logger, StartupLogTopic topic) : base(logger, topic)
{
@@ -19,7 +19,6 @@ using MediaBrowser.Model.System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
@@ -29,6 +28,8 @@ using Microsoft.Extensions.Primitives;
using Morestachio;
using Morestachio.Framework.IO.SingleStream;
using Morestachio.Rendering;
using Serilog;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace Jellyfin.Server.ServerSetupApp;
@@ -143,8 +144,10 @@ public sealed class SetupServer : IDisposable
var config = _configurationManager.GetNetworkConfiguration()!;
_startupServer = Host.CreateDefaultBuilder(["hostBuilder:reloadConfigOnChange=false"])
.UseConsoleLifetime()
.UseSerilog()
.ConfigureServices(serv =>
{
serv.AddSingleton(this);
serv.AddHealthChecks()
.AddCheck<SetupHealthcheck>("StartupCheck");
serv.Configure<ForwardedHeadersOptions>(options =>
+4 -11
View File
@@ -1423,23 +1423,16 @@ namespace MediaBrowser.Controller.Entities
public virtual bool RequiresRefresh()
{
if (string.IsNullOrEmpty(Path) || DateModified == default)
if (string.IsNullOrEmpty(Path) || DateModified == DateTime.MinValue)
{
return false;
}
var info = FileSystem.GetFileSystemInfo(Path);
if (info.Exists)
{
if (info.IsDirectory)
{
return info.LastWriteTimeUtc != DateModified;
}
return info.LastWriteTimeUtc != DateModified;
}
return false;
return info.Exists
? info.LastWriteTimeUtc != DateModified
: false;
}
public virtual List<string> GetUserDataKeys()
+4 -4
View File
@@ -695,7 +695,7 @@ namespace MediaBrowser.Controller.Entities
items = GetRecursiveChildren(user, query);
}
return PostFilterAndSort(items, query, true);
return PostFilterAndSort(items, query);
}
if (this is not UserRootFolder
@@ -959,10 +959,10 @@ namespace MediaBrowser.Controller.Entities
items = GetChildren(user, true, childQuery).Where(filter);
}
return PostFilterAndSort(items, query, true);
return PostFilterAndSort(items, query);
}
protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query, bool enableSorting)
protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query)
{
var user = query.User;
@@ -995,7 +995,7 @@ namespace MediaBrowser.Controller.Entities
items = UserViewBuilder.FilterForAdjacency(items.ToList(), query.AdjacentTo.Value);
}
return UserViewBuilder.SortAndPage(items, null, query, LibraryManager, enableSorting);
return UserViewBuilder.SortAndPage(items, null, query, LibraryManager);
}
private static IEnumerable<BaseItem> CollapseBoxSetItemsIfNeeded(
@@ -179,7 +179,7 @@ namespace MediaBrowser.Controller.Entities.TV
var items = GetEpisodes(user, query.DtoOptions, true).Where(filter);
return PostFilterAndSort(items, query, false);
return PostFilterAndSort(items, query);
}
/// <summary>
@@ -80,7 +80,7 @@ namespace MediaBrowser.Controller.Entities
PresetViews = query.PresetViews
});
return UserViewBuilder.SortAndPage(result, null, query, LibraryManager, true);
return UserViewBuilder.SortAndPage(result, null, query, LibraryManager);
}
public override int GetChildCount(User user)
@@ -438,22 +438,18 @@ namespace MediaBrowser.Controller.Entities
items = FilterForAdjacency(items.ToList(), query.AdjacentTo.Value);
}
return SortAndPage(items, totalRecordLimit, query, libraryManager, true);
return SortAndPage(items, totalRecordLimit, query, libraryManager);
}
public static QueryResult<BaseItem> SortAndPage(
IEnumerable<BaseItem> items,
int? totalRecordLimit,
InternalItemsQuery query,
ILibraryManager libraryManager,
bool enableSorting)
ILibraryManager libraryManager)
{
if (enableSorting)
if (query.OrderBy.Count > 0)
{
if (query.OrderBy.Count > 0)
{
items = libraryManager.Sort(items, query.User, query.OrderBy);
}
items = libraryManager.Sort(items, query.User, query.OrderBy);
}
var itemsArray = totalRecordLimit.HasValue ? items.Take(totalRecordLimit.Value).ToArray() : items.ToArray();
@@ -96,14 +96,33 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
}
}
private bool ShouldForceSequentialOperation()
{
// if the user either set the setting to 1 or it's unset and we have fewer than 4 cores it's better to run sequentially.
var fanoutSetting = _serverConfigurationManager.Configuration.LibraryScanFanoutConcurrency;
return fanoutSetting == 1 || (fanoutSetting <= 0 && Environment.ProcessorCount <= 3);
}
private int CalculateScanConcurrencyLimit()
{
// when this is invoked, we already checked ShouldForceSequentialOperation for the sequential check.
var fanoutConcurrency = _serverConfigurationManager.Configuration.LibraryScanFanoutConcurrency;
if (fanoutConcurrency <= 0)
{
// in case the user did not set a limit manually, we can assume he has 3 or more cores as already checked by ShouldForceSequentialOperation.
return Environment.ProcessorCount - 3;
}
return fanoutConcurrency;
}
private void Worker()
{
lock (_taskLock)
{
var fanoutConcurrency = _serverConfigurationManager.Configuration.LibraryScanFanoutConcurrency;
var parallelism = (fanoutConcurrency > 0 ? fanoutConcurrency : Environment.ProcessorCount) - _taskRunners.Count;
_logger.LogDebug("Spawn {NumberRunners} new runners.", parallelism);
for (int i = 0; i < parallelism; i++)
var operationFanout = Math.Max(0, CalculateScanConcurrencyLimit() - _taskRunners.Count);
_logger.LogDebug("Spawn {NumberRunners} new runners.", operationFanout);
for (int i = 0; i < operationFanout; i++)
{
var stopToken = new CancellationTokenSource();
var combinedSource = CancellationTokenSource.CreateLinkedTokenSource(stopToken.Token, _hostApplicationLifetime.ApplicationStopping);
@@ -223,7 +242,7 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
};
}).ToArray();
if (_serverConfigurationManager.Configuration.LibraryScanFanoutConcurrency == 1)
if (ShouldForceSequentialOperation())
{
_logger.LogDebug("Process sequentially.");
try
@@ -230,10 +230,10 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var hwType = encodingOptions.HardwareAccelerationType;
// Only Intel has VA-API MJPEG encoder
// Only enable VA-API MJPEG encoder on Intel iHD driver.
// Legacy platforms supported ONLY by i965 do not support MJPEG encoder.
if (hwType == HardwareAccelerationType.vaapi
&& !(_mediaEncoder.IsVaapiDeviceInteliHD
|| _mediaEncoder.IsVaapiDeviceInteli965))
&& !_mediaEncoder.IsVaapiDeviceInteliHD)
{
return _defaultMjpegEncoder;
}
@@ -2390,6 +2390,12 @@ namespace MediaBrowser.Controller.MediaEncoding
|| (requestHasSDR && videoStream.VideoRangeType == VideoRangeType.DOVIWithSDR)
|| (requestHasHDR10 && videoStream.VideoRangeType == VideoRangeType.HDR10Plus)))
{
// If the video stream is in a static HDR format, don't allow copy if the client does not support HDR10 or HLG.
if (videoStream.VideoRangeType is VideoRangeType.HDR10 or VideoRangeType.HLG)
{
return false;
}
// Check complicated cases where we need to remove dynamic metadata
// Conservatively refuse to copy if the encoder can't remove dynamic metadata,
// but a removal is required for compatability reasons.
@@ -4442,6 +4448,13 @@ namespace MediaBrowser.Controller.MediaEncoding
var swapOutputWandH = doVppTranspose && swapWAndH;
var hwScaleFilter = GetHwScaleFilter("vpp", "qsv", outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
// d3d11va doesn't support dynamic pool size, use vpp filter ctx to relay
// to prevent encoder async and bframes from exhausting the decoder pool.
if (!string.IsNullOrEmpty(hwScaleFilter) && isD3d11vaDecoder)
{
hwScaleFilter += ":passthrough=0";
}
if (!string.IsNullOrEmpty(hwScaleFilter) && doVppTranspose)
{
hwScaleFilter += $":transpose={transposeDir}";
@@ -7138,7 +7151,8 @@ namespace MediaBrowser.Controller.MediaEncoding
inputModifier += " -async " + state.InputAudioSync;
}
if (!string.IsNullOrEmpty(state.InputVideoSync))
// The -fps_mode option cannot be applied to input
if (!string.IsNullOrEmpty(state.InputVideoSync) && _mediaEncoder.EncoderVersion < new Version(5, 1))
{
inputModifier += GetVideoSyncOption(state.InputVideoSync, _mediaEncoder.EncoderVersion);
}
@@ -235,11 +235,11 @@ namespace MediaBrowser.LocalMetadata.Savers
{
if (item is Person)
{
await writer.WriteElementStringAsync(null, "DeathDate", null, item.EndDate.Value.ToLocalTime().ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)).ConfigureAwait(false);
await writer.WriteElementStringAsync(null, "DeathDate", null, item.EndDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)).ConfigureAwait(false);
}
else if (item is not Episode)
{
await writer.WriteElementStringAsync(null, "EndDate", null, item.EndDate.Value.ToLocalTime().ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)).ConfigureAwait(false);
await writer.WriteElementStringAsync(null, "EndDate", null, item.EndDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)).ConfigureAwait(false);
}
}
+1 -1
View File
@@ -569,7 +569,7 @@ namespace MediaBrowser.Model.Dto
/// Gets or sets the trickplay manifest.
/// </summary>
/// <value>The trickplay manifest.</value>
public Dictionary<string, Dictionary<int, TrickplayInfo>> Trickplay { get; set; }
public Dictionary<string, Dictionary<int, TrickplayInfoDto>> Trickplay { get; set; }
/// <summary>
/// Gets or sets the type of the location.
@@ -0,0 +1,62 @@
using System;
using Jellyfin.Database.Implementations.Entities;
namespace MediaBrowser.Model.Dto;
/// <summary>
/// The trickplay api model.
/// </summary>
public record TrickplayInfoDto
{
/// <summary>
/// Initializes a new instance of the <see cref="TrickplayInfoDto"/> class.
/// </summary>
/// <param name="info">The trickplay info.</param>
public TrickplayInfoDto(TrickplayInfo info)
{
ArgumentNullException.ThrowIfNull(info);
Width = info.Width;
Height = info.Height;
TileWidth = info.TileWidth;
TileHeight = info.TileHeight;
ThumbnailCount = info.ThumbnailCount;
Interval = info.Interval;
Bandwidth = info.Bandwidth;
}
/// <summary>
/// Gets the width of an individual thumbnail.
/// </summary>
public int Width { get; init; }
/// <summary>
/// Gets the height of an individual thumbnail.
/// </summary>
public int Height { get; init; }
/// <summary>
/// Gets the amount of thumbnails per row.
/// </summary>
public int TileWidth { get; init; }
/// <summary>
/// Gets the amount of thumbnails per column.
/// </summary>
public int TileHeight { get; init; }
/// <summary>
/// Gets the total amount of non-black thumbnails.
/// </summary>
public int ThumbnailCount { get; init; }
/// <summary>
/// Gets the interval in milliseconds between each trickplay thumbnail.
/// </summary>
public int Interval { get; init; }
/// <summary>
/// Gets the peak bandwidth usage in bits per second.
/// </summary>
public int Bandwidth { get; init; }
}
+10 -3
View File
@@ -8,21 +8,28 @@ public class LyricLineCue
/// <summary>
/// Initializes a new instance of the <see cref="LyricLineCue"/> class.
/// </summary>
/// <param name="position">The start of the character index of the lyric.</param>
/// <param name="position">The start character index of the cue.</param>
/// <param name="endPosition">The end character index of the cue.</param>
/// <param name="start">The start of the timestamp the lyric is synced to in ticks.</param>
/// <param name="end">The end of the timestamp the lyric is synced to in ticks.</param>
public LyricLineCue(int position, long start, long? end)
public LyricLineCue(int position, int endPosition, long start, long? end)
{
Position = position;
EndPosition = endPosition;
Start = start;
End = end;
}
/// <summary>
/// Gets the character index of the lyric.
/// Gets the start character index of the cue.
/// </summary>
public int Position { get; }
/// <summary>
/// Gets the end character index of the cue.
/// </summary>
public int EndPosition { get; }
/// <summary>
/// Gets the timestamp the lyric is synced to in ticks.
/// </summary>
+36 -26
View File
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Jellyfin.Extensions;
using LrcParser.Model;
@@ -66,47 +67,56 @@ public partial class LrcLyricParser : ILyricParser
}
List<LyricLine> lyricList = [];
for (var l = 0; l < sortedLyricData.Count; l++)
for (var lineIndex = 0; lineIndex < sortedLyricData.Count; lineIndex++)
{
var cues = new List<LyricLineCue>();
var lyric = sortedLyricData[l];
var lyric = sortedLyricData[lineIndex];
if (lyric.TimeTags.Count != 0)
// Extract cues from time tags
var cues = new List<LyricLineCue>();
if (lyric.TimeTags.Count > 0)
{
var keys = lyric.TimeTags.Keys.ToList();
int current = 0, next = 1;
while (next < keys.Count)
for (var tagIndex = 0; tagIndex < keys.Count - 1; tagIndex++)
{
var currentKey = keys[current];
var currentKey = keys[tagIndex];
var nextKey = keys[tagIndex + 1];
var currentPos = currentKey.State == IndexState.End ? currentKey.Index + 1 : currentKey.Index;
var nextPos = nextKey.State == IndexState.End ? nextKey.Index + 1 : nextKey.Index;
var currentMs = lyric.TimeTags[currentKey] ?? 0;
var nextMs = lyric.TimeTags[keys[next]] ?? 0;
cues.Add(new LyricLineCue(
position: Math.Max(currentKey.Index, 0),
start: TimeSpan.FromMilliseconds(currentMs).Ticks,
end: TimeSpan.FromMilliseconds(nextMs).Ticks));
current++;
next++;
var nextMs = lyric.TimeTags[keys[tagIndex + 1]] ?? 0;
var currentSlice = lyric.Text[currentPos..nextPos];
var currentSliceTrimmed = currentSlice.Trim();
if (currentSliceTrimmed.Length > 0)
{
cues.Add(new LyricLineCue(
position: currentPos,
endPosition: nextPos,
start: TimeSpan.FromMilliseconds(currentMs).Ticks,
end: TimeSpan.FromMilliseconds(nextMs).Ticks));
}
}
var lastKey = keys[current];
var lastKey = keys[^1];
var lastPos = lastKey.State == IndexState.End ? lastKey.Index + 1 : lastKey.Index;
var lastMs = lyric.TimeTags[lastKey] ?? 0;
var lastSlice = lyric.Text[lastPos..];
var lastSliceTrimmed = lastSlice.Trim();
cues.Add(new LyricLineCue(
position: Math.Max(lastKey.Index, 0),
start: TimeSpan.FromMilliseconds(lastMs).Ticks,
end: l + 1 < sortedLyricData.Count ? TimeSpan.FromMilliseconds(sortedLyricData[l + 1].StartTime).Ticks : null));
if (lastSliceTrimmed.Length > 0)
{
cues.Add(new LyricLineCue(
position: lastPos,
endPosition: lyric.Text.Length,
start: TimeSpan.FromMilliseconds(lastMs).Ticks,
end: lineIndex + 1 < sortedLyricData.Count ? TimeSpan.FromMilliseconds(sortedLyricData[lineIndex + 1].StartTime).Ticks : null));
}
}
long lyricStartTicks = TimeSpan.FromMilliseconds(lyric.StartTime).Ticks;
lyricList.Add(new LyricLine(WhitespaceRegex().Replace(lyric.Text.Trim(), " "), lyricStartTicks, cues));
lyricList.Add(new LyricLine(lyric.Text, lyricStartTicks, cues));
}
return new LyricDto { Lyrics = lyricList };
}
// Replacement is required until https://github.com/karaoke-dev/LrcParser/issues/83 is resolved.
[GeneratedRegex(@"\s+")]
private static partial Regex WhitespaceRegex();
}
@@ -73,11 +73,11 @@ namespace MediaBrowser.Providers.Manager
public virtual int Order => 0;
private FileSystemMetadata TryGetFile(string path, IDirectoryService directoryService)
private FileSystemMetadata TryGetFileSystemMetadata(string path, IDirectoryService directoryService)
{
try
{
return directoryService.GetFile(path);
return directoryService.GetFileSystemEntry(path);
}
catch (Exception ex)
{
@@ -92,7 +92,7 @@ namespace MediaBrowser.Providers.Manager
var updateType = ItemUpdateType.None;
var libraryOptions = LibraryManager.GetLibraryOptions(item);
var isFirstRefresh = item.DateLastRefreshed.Date == DateTime.MinValue.Date;
var isFirstRefresh = item.DateLastRefreshed == DateTime.MinValue;
var hasRefreshedMetadata = true;
var hasRefreshedImages = true;
@@ -225,7 +225,7 @@ namespace MediaBrowser.Providers.Manager
{
if (item.IsFileProtocol)
{
var file = TryGetFile(item.Path, refreshOptions.DirectoryService);
var file = TryGetFileSystemMetadata(item.Path, refreshOptions.DirectoryService);
if (file is not null)
{
item.DateModified = file.LastWriteTimeUtc;
@@ -1180,12 +1180,12 @@ namespace MediaBrowser.Providers.Manager
target.LockedFields = target.LockedFields.Concat(source.LockedFields).Distinct().ToArray();
}
if (source.DateCreated != default)
if (source.DateCreated != DateTime.MinValue)
{
target.DateCreated = source.DateCreated;
}
if (replaceData || source.DateModified != default)
if (replaceData || source.DateModified != DateTime.MinValue)
{
target.DateModified = source.DateModified;
}
@@ -669,8 +669,13 @@ namespace MediaBrowser.Providers.Manager
private async Task SaveMetadataAsync(BaseItem item, ItemUpdateType updateType, IEnumerable<IMetadataSaver> savers)
{
var libraryOptions = _libraryManager.GetLibraryOptions(item);
var applicableSavers = savers.Where(i => IsSaverEnabledForItem(i, item, libraryOptions, updateType, false)).ToList();
if (applicableSavers.Count == 0)
{
return;
}
foreach (var saver in savers.Where(i => IsSaverEnabledForItem(i, item, libraryOptions, updateType, false)))
foreach (var saver in applicableSavers)
{
_logger.LogDebug("Saving {Item} to {Saver}", item.Path ?? item.Name, saver.Name);
@@ -714,6 +719,8 @@ namespace MediaBrowser.Providers.Manager
}
}
}
_libraryManager.CreateItem(item, null);
}
/// <summary>
@@ -192,7 +192,20 @@ namespace MediaBrowser.Providers.MediaInfo
if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast))
{
var people = new List<PersonInfo>();
var albumArtists = string.IsNullOrEmpty(trackAlbumArtist) ? [] : trackAlbumArtist.Split(InternalValueSeparator);
string[]? albumArtists = null;
if (libraryOptions.PreferNonstandardArtistsTag)
{
TryGetSanitizedAdditionalFields(track, "ALBUMARTISTS", out var albumArtistsTagString);
if (albumArtistsTagString is not null)
{
albumArtists = albumArtistsTagString.Split(InternalValueSeparator);
}
}
if (albumArtists is null || albumArtists.Length == 0)
{
albumArtists = string.IsNullOrEmpty(trackAlbumArtist) ? [] : trackAlbumArtist.Split(InternalValueSeparator);
}
if (libraryOptions.UseCustomTagDelimiters)
{
@@ -205,7 +218,7 @@ namespace MediaBrowser.Providers.MediaInfo
{
PeopleHelper.AddPerson(people, new PersonInfo
{
Name = albumArtist.Trim(),
Name = albumArtist,
Type = PersonKind.AlbumArtist
});
}
@@ -237,7 +250,7 @@ namespace MediaBrowser.Providers.MediaInfo
{
PeopleHelper.AddPerson(people, new PersonInfo
{
Name = performer.Trim(),
Name = performer,
Type = PersonKind.Artist
});
}
@@ -251,7 +264,7 @@ namespace MediaBrowser.Providers.MediaInfo
{
PeopleHelper.AddPerson(people, new PersonInfo
{
Name = composer.Trim(),
Name = composer,
Type = PersonKind.Composer
});
}
@@ -340,9 +353,10 @@ namespace MediaBrowser.Providers.MediaInfo
genres = genres.Trimmed().Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
audio.Genres = options.ReplaceAllMetadata || audio.Genres is null || audio.Genres.Length == 0
? genres
: audio.Genres;
if (options.ReplaceAllMetadata || audio.Genres is null || audio.Genres.Length == 0 || audio.Genres.All(string.IsNullOrWhiteSpace))
{
audio.Genres = genres;
}
}
TryGetSanitizedAdditionalFields(track, "REPLAYGAIN_TRACK_GAIN", out var trackGainTag);
@@ -435,7 +449,11 @@ namespace MediaBrowser.Providers.MediaInfo
// Save extracted lyrics if they exist,
// and if the audio doesn't yet have lyrics.
var lyrics = track.Lyrics.SynchronizedLyrics.Count > 0 ? track.Lyrics.FormatSynchToLRC() : track.Lyrics.UnsynchronizedLyrics;
// ATL supports both SRT and LRC formats as synchronized lyrics, but we only want to save LRC format.
var supportedLyrics = track.Lyrics.Where(l => l.Format != LyricsInfo.LyricsFormat.SRT).ToList();
var candidateSynchronizedLyric = supportedLyrics.FirstOrDefault(l => l.Format is not LyricsInfo.LyricsFormat.UNSYNCHRONIZED and not LyricsInfo.LyricsFormat.OTHER && l.SynchronizedLyrics is not null);
var candidateUnsynchronizedLyric = supportedLyrics.FirstOrDefault(l => l.Format is LyricsInfo.LyricsFormat.UNSYNCHRONIZED or LyricsInfo.LyricsFormat.OTHER && l.UnsynchronizedLyrics is not null);
var lyrics = candidateSynchronizedLyric is not null ? candidateSynchronizedLyric.FormatSynch() : candidateUnsynchronizedLyric?.UnsynchronizedLyrics;
if (!string.IsNullOrWhiteSpace(lyrics)
&& tryExtractEmbeddedLyrics)
{
@@ -6,6 +6,7 @@ using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Chapters;
@@ -516,12 +517,15 @@ namespace MediaBrowser.Providers.MediaInfo
foreach (var person in data.People)
{
PeopleHelper.AddPerson(people, new PersonInfo
if (!string.IsNullOrWhiteSpace(person.Name))
{
Name = person.Name.Trim(),
Type = person.Type,
Role = person.Role.Trim()
});
PeopleHelper.AddPerson(people, new PersonInfo
{
Name = person.Name,
Type = person.Type,
Role = person.Role.Trim()
});
}
}
_libraryManager.UpdatePeople(video, people);
@@ -200,20 +200,26 @@ public class AlbumMetadataService : MetadataService<MusicAlbum, AlbumInfo>
foreach (var albumArtist in item.AlbumArtists)
{
PeopleHelper.AddPerson(people, new PersonInfo
if (!string.IsNullOrWhiteSpace(albumArtist))
{
Name = albumArtist.Trim(),
Type = PersonKind.AlbumArtist
});
PeopleHelper.AddPerson(people, new PersonInfo
{
Name = albumArtist,
Type = PersonKind.AlbumArtist
});
}
}
foreach (var artist in item.Artists)
{
PeopleHelper.AddPerson(people, new PersonInfo
if (!string.IsNullOrWhiteSpace(artist))
{
Name = artist.Trim(),
Type = PersonKind.Artist
});
PeopleHelper.AddPerson(people, new PersonInfo
{
Name = artist,
Type = PersonKind.Artist
});
}
}
LibraryManager.UpdatePeople(item, people);
@@ -38,6 +38,21 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// </summary>
public int MaxCastMembers { get; set; } = 15;
/// <summary>
/// Gets or sets a value indicating the maximum number of crew members to fetch for an item.
/// </summary>
public int MaxCrewMembers { get; set; } = 15;
/// <summary>
/// Gets or sets a value indicating whether to hide cast members without profile images.
/// </summary>
public bool HideMissingCastMembers { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to hide crew members without profile images.
/// </summary>
public bool HideMissingCrewMembers { get; set; }
/// <summary>
/// Gets or sets a value indicating the poster image size to fetch.
/// </summary>
@@ -25,9 +25,24 @@
<input is="emby-checkbox" type="checkbox" id="importSeasonName" />
<span>Import season name from metadata fetched for series.</span>
</label>
<div class="inputContainer">
<input is="emby-input" type="number" id="maxCastMembers" pattern="[0-9]*" required min="0" max="1000" label="Max Cast Members" />
<div class="fieldDescription">The maximum number of cast members to fetch for an item.</div>
<div class="verticalSection">
<h2>Cast & Crew Settings</h2>
<div class="inputContainer">
<input is="emby-input" type="number" id="maxCastMembers" pattern="[0-9]*" required min="0" max="1000" label="Max Cast Members" />
<div class="fieldDescription">The maximum number of cast members to fetch for an item.</div>
</div>
<div class="inputContainer">
<input is="emby-input" type="number" id="maxCrewMembers" pattern="[0-9]*" required min="0" max="1000" label="Max Crew Members" />
<div class="fieldDescription">The maximum number of crew members to fetch for an item.</div>
</div>
<label class="checkboxContainer">
<input is="emby-checkbox" type="checkbox" id="hideMissingCastMembers" />
<span>Hide cast members without profile images.</span>
</label>
<label class="checkboxContainer">
<input is="emby-checkbox" type="checkbox" id="hideMissingCrewMembers" />
<span>Hide crew members without profile images.</span>
</label>
</div>
<div class="verticalSection verticalSection-extrabottompadding">
<h2>Image Scaling</h2>
@@ -129,6 +144,8 @@
document.querySelector('#excludeTagsSeries').checked = config.ExcludeTagsSeries;
document.querySelector('#excludeTagsMovies').checked = config.ExcludeTagsMovies;
document.querySelector('#importSeasonName').checked = config.ImportSeasonName;
document.querySelector('#hideMissingCastMembers').checked = config.HideMissingCastMembers;
document.querySelector('#hideMissingCrewMembers').checked = config.HideMissingCrewMembers;
var maxCastMembers = document.querySelector('#maxCastMembers');
maxCastMembers.value = config.MaxCastMembers;
@@ -137,12 +154,18 @@
cancelable: false
}));
var maxCrewMembers = document.querySelector('#maxCrewMembers');
maxCrewMembers.value = config.MaxCrewMembers;
maxCrewMembers.dispatchEvent(new Event('change', {
bubbles: true,
cancelable: false
}));
pluginConfig = config;
configureImageScaling();
});
});
document.querySelector('.configForm')
.addEventListener('submit', function (e) {
Dashboard.showLoadingMsg();
@@ -153,6 +176,9 @@
config.ExcludeTagsMovies = document.querySelector('#excludeTagsMovies').checked;
config.ImportSeasonName = document.querySelector('#importSeasonName').checked;
config.MaxCastMembers = document.querySelector('#maxCastMembers').value;
config.MaxCrewMembers = document.querySelector('#maxCrewMembers').value;
config.HideMissingCastMembers = document.querySelector('#hideMissingCastMembers').checked;
config.HideMissingCrewMembers = document.querySelector('#hideMissingCrewMembers').checked;
config.PosterSize = document.querySelector('#selectPosterSize').value;
config.BackdropSize = document.querySelector('#selectBackdropSize').value;
config.LogoSize = document.querySelector('#selectLogoSize').value;
@@ -144,6 +144,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
{
var tmdbId = info.GetProviderId(MetadataProvider.Tmdb);
var imdbId = info.GetProviderId(MetadataProvider.Imdb);
var config = Plugin.Instance.Configuration;
if (string.IsNullOrEmpty(tmdbId) && string.IsNullOrEmpty(imdbId))
{
@@ -249,12 +250,26 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
if (movieResult.Credits?.Cast is not null)
{
foreach (var actor in movieResult.Credits.Cast.OrderBy(a => a.Order).Take(Plugin.Instance.Configuration.MaxCastMembers))
var castQuery = movieResult.Credits.Cast.AsEnumerable();
if (config.HideMissingCastMembers)
{
castQuery = castQuery.Where(a => !string.IsNullOrEmpty(a.ProfilePath));
}
castQuery = castQuery.OrderBy(a => a.Order).Take(config.MaxCastMembers);
foreach (var actor in castQuery)
{
if (string.IsNullOrWhiteSpace(actor.Name))
{
continue;
}
var personInfo = new PersonInfo
{
Name = actor.Name.Trim(),
Role = actor.Character.Trim(),
Role = actor.Character?.Trim() ?? string.Empty,
Type = PersonKind.Actor,
SortOrder = actor.Order
};
@@ -275,32 +290,47 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
if (movieResult.Credits?.Crew is not null)
{
foreach (var person in movieResult.Credits.Crew)
{
// Normalize this
var type = TmdbUtils.MapCrewToPersonType(person);
var crewQuery = movieResult.Credits.Crew
.Select(crewMember => new
{
CrewMember = crewMember,
PersonType = TmdbUtils.MapCrewToPersonType(crewMember)
})
.Where(entry =>
TmdbUtils.WantedCrewKinds.Contains(entry.PersonType) ||
TmdbUtils.WantedCrewTypes.Contains(entry.CrewMember.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase));
if (!TmdbUtils.WantedCrewKinds.Contains(type)
&& !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase))
if (config.HideMissingCrewMembers)
{
crewQuery = crewQuery.Where(entry => !string.IsNullOrEmpty(entry.CrewMember.ProfilePath));
}
crewQuery = crewQuery.Take(config.MaxCrewMembers);
foreach (var entry in crewQuery)
{
var crewMember = entry.CrewMember;
if (string.IsNullOrWhiteSpace(crewMember.Name))
{
continue;
}
var personInfo = new PersonInfo
{
Name = person.Name.Trim(),
Role = person.Job?.Trim(),
Type = type
Name = crewMember.Name.Trim(),
Role = crewMember.Job?.Trim() ?? string.Empty,
Type = entry.PersonType
};
if (!string.IsNullOrWhiteSpace(person.ProfilePath))
if (!string.IsNullOrWhiteSpace(crewMember.ProfilePath))
{
personInfo.ImageUrl = _tmdbClientManager.GetProfileUrl(person.ProfilePath);
personInfo.ImageUrl = _tmdbClientManager.GetProfileUrl(crewMember.ProfilePath);
}
if (person.Id > 0)
if (crewMember.Id > 0)
{
personInfo.SetProviderId(MetadataProvider.Tmdb, person.Id.ToString(CultureInfo.InvariantCulture));
personInfo.SetProviderId(MetadataProvider.Tmdb, crewMember.Id.ToString(CultureInfo.InvariantCulture));
}
metadataResult.AddPerson(personInfo);
@@ -81,6 +81,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
public async Task<MetadataResult<Episode>> GetMetadata(EpisodeInfo info, CancellationToken cancellationToken)
{
var metadataResult = new MetadataResult<Episode>();
var config = Plugin.Instance.Configuration;
// Allowing this will dramatically increase scan times
if (info.IsMissingEpisode)
@@ -206,52 +207,106 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
if (credits?.Cast is not null)
{
foreach (var actor in credits.Cast.OrderBy(a => a.Order).Take(Plugin.Instance.Configuration.MaxCastMembers))
var castQuery = config.HideMissingCastMembers
? credits.Cast.Where(a => !string.IsNullOrEmpty(a.ProfilePath)).OrderBy(a => a.Order)
: credits.Cast.OrderBy(a => a.Order);
foreach (var actor in castQuery.Take(config.MaxCastMembers))
{
metadataResult.AddPerson(new PersonInfo
if (string.IsNullOrWhiteSpace(actor.Name))
{
continue;
}
var personInfo = new PersonInfo
{
Name = actor.Name.Trim(),
Role = actor.Character.Trim(),
Role = actor.Character?.Trim() ?? string.Empty,
Type = PersonKind.Actor,
SortOrder = actor.Order
});
SortOrder = actor.Order,
ImageUrl = _tmdbClientManager.GetProfileUrl(actor.ProfilePath)
};
if (actor.Id > 0)
{
personInfo.SetProviderId(MetadataProvider.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture));
}
metadataResult.AddPerson(personInfo);
}
}
if (credits?.GuestStars is not null)
{
foreach (var guest in credits.GuestStars.OrderBy(a => a.Order).Take(Plugin.Instance.Configuration.MaxCastMembers))
{
metadataResult.AddPerson(new PersonInfo
{
Name = guest.Name.Trim(),
Role = guest.Character.Trim(),
Type = PersonKind.GuestStar,
SortOrder = guest.Order
});
}
}
var guestQuery = config.HideMissingCastMembers
? credits.GuestStars.Where(a => !string.IsNullOrEmpty(a.ProfilePath)).OrderBy(a => a.Order)
: credits.GuestStars.OrderBy(a => a.Order);
// and the rest from crew
if (credits?.Crew is not null)
{
foreach (var person in credits.Crew)
foreach (var guest in guestQuery.Take(config.MaxCastMembers))
{
// Normalize this
var type = TmdbUtils.MapCrewToPersonType(person);
if (!TmdbUtils.WantedCrewKinds.Contains(type)
&& !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase))
if (string.IsNullOrWhiteSpace(guest.Name))
{
continue;
}
metadataResult.AddPerson(new PersonInfo
var personInfo = new PersonInfo
{
Name = person.Name.Trim(),
Role = person.Job?.Trim(),
Type = type
});
Name = guest.Name.Trim(),
Role = guest.Character?.Trim() ?? string.Empty,
Type = PersonKind.GuestStar,
SortOrder = guest.Order,
ImageUrl = _tmdbClientManager.GetProfileUrl(guest.ProfilePath)
};
if (guest.Id > 0)
{
personInfo.SetProviderId(MetadataProvider.Tmdb, guest.Id.ToString(CultureInfo.InvariantCulture));
}
metadataResult.AddPerson(personInfo);
}
}
if (credits?.Crew is not null)
{
var crewQuery = credits.Crew
.Select(crewMember => new
{
CrewMember = crewMember,
PersonType = TmdbUtils.MapCrewToPersonType(crewMember)
})
.Where(entry =>
TmdbUtils.WantedCrewKinds.Contains(entry.PersonType) ||
TmdbUtils.WantedCrewTypes.Contains(entry.CrewMember.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase));
if (config.HideMissingCrewMembers)
{
crewQuery = crewQuery.Where(entry => !string.IsNullOrEmpty(entry.CrewMember.ProfilePath));
}
foreach (var entry in crewQuery.Take(config.MaxCrewMembers))
{
var crewMember = entry.CrewMember;
if (string.IsNullOrWhiteSpace(crewMember.Name))
{
continue;
}
var personInfo = new PersonInfo
{
Name = crewMember.Name.Trim(),
Role = crewMember.Job?.Trim() ?? string.Empty,
Type = entry.PersonType,
ImageUrl = _tmdbClientManager.GetProfileUrl(crewMember.ProfilePath)
};
if (crewMember.Id > 0)
{
personInfo.SetProviderId(MetadataProvider.Tmdb, crewMember.Id.ToString(CultureInfo.InvariantCulture));
}
metadataResult.AddPerson(personInfo);
}
}
@@ -42,6 +42,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
public async Task<MetadataResult<Season>> GetMetadata(SeasonInfo info, CancellationToken cancellationToken)
{
var result = new MetadataResult<Season>();
var config = Plugin.Instance.Configuration;
info.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out string? seriesTmdbId);
@@ -65,10 +66,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
result.Item = new Season
{
IndexNumber = seasonNumber,
Overview = seasonResult.Overview
Overview = seasonResult.Overview,
PremiereDate = seasonResult.AirDate,
ProductionYear = seasonResult.AirDate?.Year
};
if (Plugin.Instance.Configuration.ImportSeasonName)
if (config.ImportSeasonName)
{
result.Item.Name = seasonResult.Name;
}
@@ -77,47 +80,81 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
// TODO why was this disabled?
var credits = seasonResult.Credits;
if (credits?.Cast is not null)
{
var cast = credits.Cast.OrderBy(c => c.Order).Take(Plugin.Instance.Configuration.MaxCastMembers).ToList();
for (var i = 0; i < cast.Count; i++)
var castQuery = config.HideMissingCastMembers
? credits.Cast.Where(a => !string.IsNullOrEmpty(a.ProfilePath)).OrderBy(a => a.Order)
: credits.Cast.OrderBy(a => a.Order);
foreach (var actor in castQuery.Take(config.MaxCastMembers))
{
var member = cast[i];
result.AddPerson(new PersonInfo
if (string.IsNullOrWhiteSpace(actor.Name))
{
Name = member.Name.Trim(),
Role = member.Character.Trim(),
continue;
}
var personInfo = new PersonInfo
{
Name = actor.Name.Trim(),
Role = actor.Character?.Trim() ?? string.Empty,
Type = PersonKind.Actor,
SortOrder = member.Order
});
SortOrder = actor.Order,
ImageUrl = _tmdbClientManager.GetProfileUrl(actor.ProfilePath)
};
if (actor.Id > 0)
{
personInfo.SetProviderId(MetadataProvider.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture));
}
result.AddPerson(personInfo);
}
}
if (credits?.Crew is not null)
{
foreach (var person in credits.Crew)
{
// Normalize this
var type = TmdbUtils.MapCrewToPersonType(person);
var crewQuery = credits.Crew
.Select(crewMember => new
{
CrewMember = crewMember,
PersonType = TmdbUtils.MapCrewToPersonType(crewMember)
})
.Where(entry =>
TmdbUtils.WantedCrewKinds.Contains(entry.PersonType) ||
TmdbUtils.WantedCrewTypes.Contains(entry.CrewMember.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase));
if (!TmdbUtils.WantedCrewKinds.Contains(type)
&& !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase))
if (config.HideMissingCrewMembers)
{
crewQuery = crewQuery.Where(entry => !string.IsNullOrEmpty(entry.CrewMember.ProfilePath));
}
foreach (var entry in crewQuery.Take(config.MaxCrewMembers))
{
var crewMember = entry.CrewMember;
if (string.IsNullOrWhiteSpace(crewMember.Name))
{
continue;
}
result.AddPerson(new PersonInfo
var personInfo = new PersonInfo
{
Name = person.Name.Trim(),
Role = person.Job?.Trim(),
Type = type
});
Name = crewMember.Name.Trim(),
Role = crewMember.Job?.Trim() ?? string.Empty,
Type = entry.PersonType,
ImageUrl = _tmdbClientManager.GetProfileUrl(crewMember.ProfilePath)
};
if (crewMember.Id > 0)
{
personInfo.SetProviderId(MetadataProvider.Tmdb, crewMember.Id.ToString(CultureInfo.InvariantCulture));
}
result.AddPerson(personInfo);
}
}
result.Item.PremiereDate = seasonResult.AirDate;
result.Item.ProductionYear = seasonResult.AirDate?.Year;
return result;
}
@@ -323,17 +323,31 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
private IEnumerable<PersonInfo> GetPersons(TvShow seriesResult)
{
var config = Plugin.Instance.Configuration;
if (seriesResult.Credits?.Cast is not null)
{
foreach (var actor in seriesResult.Credits.Cast.OrderBy(a => a.Order).Take(Plugin.Instance.Configuration.MaxCastMembers))
IEnumerable<Cast> castQuery = seriesResult.Credits.Cast.OrderBy(a => a.Order);
if (config.HideMissingCastMembers)
{
castQuery = castQuery.Where(a => !string.IsNullOrEmpty(a.ProfilePath));
}
foreach (var actor in castQuery.Take(config.MaxCastMembers))
{
if (string.IsNullOrWhiteSpace(actor.Name))
{
continue;
}
var personInfo = new PersonInfo
{
Name = actor.Name.Trim(),
Role = actor.Character.Trim(),
Role = actor.Character?.Trim() ?? string.Empty,
Type = PersonKind.Actor,
SortOrder = actor.Order,
ImageUrl = _tmdbClientManager.GetPosterUrl(actor.ProfilePath)
ImageUrl = _tmdbClientManager.GetProfileUrl(actor.ProfilePath)
};
if (actor.Id > 0)
@@ -347,30 +361,44 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
if (seriesResult.Credits?.Crew is not null)
{
var keepTypes = new[]
{
PersonType.Director,
PersonType.Writer,
PersonType.Producer
};
var crewQuery = seriesResult.Credits.Crew
.Select(crewMember => new
{
CrewMember = crewMember,
PersonType = TmdbUtils.MapCrewToPersonType(crewMember)
})
.Where(entry =>
TmdbUtils.WantedCrewKinds.Contains(entry.PersonType) ||
TmdbUtils.WantedCrewTypes.Contains(entry.CrewMember.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase));
foreach (var person in seriesResult.Credits.Crew)
if (config.HideMissingCrewMembers)
{
// Normalize this
var type = TmdbUtils.MapCrewToPersonType(person);
crewQuery = crewQuery.Where(entry => !string.IsNullOrEmpty(entry.CrewMember.ProfilePath));
}
if (!TmdbUtils.WantedCrewKinds.Contains(type)
&& !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase))
foreach (var entry in crewQuery.Take(config.MaxCrewMembers))
{
var crewMember = entry.CrewMember;
if (string.IsNullOrWhiteSpace(crewMember.Name))
{
continue;
}
yield return new PersonInfo
var personInfo = new PersonInfo
{
Name = person.Name.Trim(),
Role = person.Job?.Trim(),
Type = type
Name = crewMember.Name.Trim(),
Role = crewMember.Job?.Trim() ?? string.Empty,
Type = entry.PersonType,
ImageUrl = _tmdbClientManager.GetProfileUrl(crewMember.ProfilePath)
};
if (crewMember.Id > 0)
{
personInfo.SetProviderId(MetadataProvider.Tmdb, crewMember.Id.ToString(CultureInfo.InvariantCulture));
}
yield return personInfo;
}
}
}
@@ -1,5 +1,6 @@
#pragma warning disable CS1591
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
@@ -22,7 +22,7 @@ public class BaseItemImageInfo
/// <summary>
/// Gets or Sets the time the image was last modified.
/// </summary>
public DateTime DateModified { get; set; }
public DateTime? DateModified { get; set; }
/// <summary>
/// Gets or Sets the imagetype.
@@ -82,7 +82,7 @@ public static class QueryPartitionHelpers
/// <typeparam name="TEntity">The entity to load.</typeparam>
/// <param name="partitionInfo">The source query.</param>
/// <param name="partitionSize">The number of elements to load per partition.</param>
/// <param name="cancellationToken">The cancelation token.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A enumerable representing the whole of the query.</returns>
public static async IAsyncEnumerable<TEntity> PartitionAsync<TEntity>(this ProgressablePartitionReporting<TEntity> partitionInfo, int partitionSize, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
@@ -98,7 +98,7 @@ public static class QueryPartitionHelpers
/// <typeparam name="TEntity">The entity to load.</typeparam>
/// <param name="partitionInfo">The source query.</param>
/// <param name="partitionSize">The number of elements to load per partition.</param>
/// <param name="cancellationToken">The cancelation token.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A enumerable representing the whole of the query.</returns>
public static async IAsyncEnumerable<TEntity> PartitionEagerAsync<TEntity>(this ProgressablePartitionReporting<TEntity> partitionInfo, int partitionSize, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
@@ -115,7 +115,7 @@ public static class QueryPartitionHelpers
/// <param name="query">The source query.</param>
/// <param name="partitionSize">The number of elements to load per partition.</param>
/// <param name="progressablePartition">Reporting helper.</param>
/// <param name="cancellationToken">The cancelation token.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A enumerable representing the whole of the query.</returns>
public static async IAsyncEnumerable<TEntity> PartitionAsync<TEntity>(
this IOrderedQueryable<TEntity> query,
@@ -154,7 +154,7 @@ public static class QueryPartitionHelpers
/// <param name="query">The source query.</param>
/// <param name="partitionSize">The number of elements to load per partition.</param>
/// <param name="progressablePartition">Reporting helper.</param>
/// <param name="cancellationToken">The cancelation token.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A enumerable representing the whole of the query.</returns>
public static async IAsyncEnumerable<TEntity> PartitionEagerAsync<TEntity>(
this IOrderedQueryable<TEntity> query,
@@ -0,0 +1,37 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jellyfin.Server.Implementations.Migrations
{
/// <inheritdoc />
public partial class BaseItemImageInfoDateModifiedNullable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<DateTime>(
name: "DateModified",
table: "BaseItemImageInfos",
type: "TEXT",
nullable: true,
oldClrType: typeof(DateTime),
oldType: "TEXT");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<DateTime>(
name: "DateModified",
table: "BaseItemImageInfos",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
oldClrType: typeof(DateTime),
oldType: "TEXT",
oldNullable: true);
}
}
}
@@ -418,7 +418,7 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<byte[]>("Blurhash")
.HasColumnType("BLOB");
b.Property<DateTime>("DateModified")
b.Property<DateTime?>("DateModified")
.HasColumnType("TEXT");
b.Property<int>("Height")
+9 -3
View File
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.IO;
using SkiaSharp;
namespace Jellyfin.Drawing.Skia;
@@ -27,12 +28,17 @@ public static class SkiaHelper
currentIndex = 0;
}
SKBitmap? bitmap = skiaEncoder.Decode(paths[currentIndex], false, null, out _);
var imagePath = paths[currentIndex];
imagesTested[currentIndex] = 0;
currentIndex++;
if (!Path.Exists(imagePath))
{
continue;
}
SKBitmap? bitmap = skiaEncoder.Decode(imagePath, false, null, out _);
if (bitmap is not null)
{
newIndex = currentIndex;
@@ -1166,7 +1166,7 @@ namespace Jellyfin.LiveTv.Channels
}
}
if (isNew || forceUpdate || item.DateLastRefreshed == default)
if (isNew || forceUpdate || item.DateLastRefreshed == DateTime.MinValue)
{
_providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.Normal);
}
@@ -75,6 +75,8 @@ public class KeyframeExtractionScheduledTask : IScheduledTask
var videos = _libraryManager.GetItemList(query);
foreach (var video in videos)
{
cancellationToken.ThrowIfCancellationRequested();
// Only local files supported
var path = video.Path;
if (File.Exists(path))
@@ -20,22 +20,28 @@ public static class LrcLyricParserTests
var line1 = parsed.Lyrics[0];
Assert.Equal("Every night that goes between", line1.Text);
Assert.NotNull(line1.Cues);
Assert.Equal(9, line1.Cues.Count);
Assert.Equal(5, line1.Cues.Count);
Assert.Equal(68400000, line1.Cues[0].Start);
Assert.Equal(72000000, line1.Cues[0].End);
Assert.Equal(0, line1.Cues[0].Position);
Assert.Equal(5, line1.Cues[0].EndPosition);
Assert.Equal(6, line1.Cues[1].Position);
Assert.Equal(11, line1.Cues[1].EndPosition);
Assert.Equal(12, line1.Cues[2].Position);
var line5 = parsed.Lyrics[4];
Assert.Equal("Every night you do not come", line5.Text);
Assert.NotNull(line5.Cues);
Assert.Equal(11, line5.Cues.Count);
Assert.Equal(377300000, line5.Cues[5].Start);
Assert.Equal(380000000, line5.Cues[5].End);
Assert.Equal(6, line5.Cues.Count);
Assert.Equal(375200000, line5.Cues[2].Start);
Assert.Equal(377300000, line5.Cues[2].End);
var lastLine = parsed.Lyrics[^1];
Assert.Equal("I have always been a storm", lastLine.Text);
Assert.NotNull(lastLine.Cues);
Assert.Equal(11, lastLine.Cues.Count);
Assert.Equal(6, lastLine.Cues.Count);
Assert.Equal(2358000000, lastLine.Cues[^1].Start);
Assert.Equal(26, lastLine.Cues[^1].EndPosition);
Assert.Null(lastLine.Cues[^1].End);
}
}
@@ -22,7 +22,7 @@ namespace Jellyfin.Providers.Tests.Manager
{
var newLocked = new[] { MetadataField.Genres, MetadataField.Cast };
var newString = "new";
var newDate = DateTime.Now;
var newDate = DateTime.UtcNow;
var oldLocked = new[] { MetadataField.Genres };
var oldString = "old";
@@ -39,6 +39,7 @@ namespace Jellyfin.Providers.Tests.Manager
DateCreated = newDate
}
};
if (defaultDate)
{
source.Item.DateCreated = default;
@@ -141,8 +142,8 @@ namespace Jellyfin.Providers.Tests.Manager
{ "ProductionYear", 1, 2 },
{ "CommunityRating", 1.0f, 2.0f },
{ "CriticRating", 1.0f, 2.0f },
{ "EndDate", DateTime.UnixEpoch, DateTime.Now },
{ "PremiereDate", DateTime.UnixEpoch, DateTime.Now },
{ "EndDate", DateTime.UnixEpoch, DateTime.UtcNow },
{ "PremiereDate", DateTime.UnixEpoch, DateTime.UtcNow },
{ "Video3DFormat", Video3DFormat.HalfSideBySide, Video3DFormat.FullSideBySide }
};
@@ -185,7 +185,7 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins
Description = packageInfo.Description,
Overview = packageInfo.Overview,
TargetAbi = packageInfo.Versions[0].TargetAbi!,
Timestamp = DateTime.Parse(packageInfo.Versions[0].Timestamp!, CultureInfo.InvariantCulture),
Timestamp = DateTimeOffset.Parse(packageInfo.Versions[0].Timestamp!, CultureInfo.InvariantCulture).UtcDateTime,
Changelog = packageInfo.Versions[0].Changelog!,
Version = new Version(1, 0).ToString(),
ImagePath = string.Empty
@@ -221,7 +221,7 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins
Description = packageInfo.Description,
Overview = packageInfo.Overview,
TargetAbi = packageInfo.Versions[0].TargetAbi!,
Timestamp = DateTime.Parse(packageInfo.Versions[0].Timestamp!, CultureInfo.InvariantCulture),
Timestamp = DateTimeOffset.Parse(packageInfo.Versions[0].Timestamp!, CultureInfo.InvariantCulture).UtcDateTime,
Changelog = packageInfo.Versions[0].Changelog!,
Version = packageInfo.Versions[0].Version,
ImagePath = string.Empty